diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs index 68fb8c2..9bf436f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs @@ -5,6 +5,7 @@ // using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -19,7 +20,8 @@ namespace WelsonJS.Launcher public abstract class ConnectionManagerBase where TConnection : class { - private readonly ConcurrentDictionary _pool = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _pool + = new ConcurrentDictionary(); /// /// Creates a unique cache key for the given connection parameters. @@ -54,15 +56,15 @@ namespace WelsonJS.Launcher { string key = CreateKey(parameters); - if (_pool.TryGetValue(key, out var existing) && IsConnectionValid(existing)) + if (_pool.TryGetValue(key, out var existing) && IsConnectionHealthy(existing.Connection)) { - return existing; + return existing.Connection; } - RemoveInternal(key, existing); + RemoveInternal(key, existing.Connection); var connection = await OpenConnectionAsync(parameters, token).ConfigureAwait(false); - _pool[key] = connection; + _pool[key] = (connection, parameters); return connection; } @@ -72,12 +74,55 @@ namespace WelsonJS.Launcher public void Remove(TParameters parameters) { string key = CreateKey(parameters); - if (_pool.TryRemove(key, out var connection)) + if (_pool.TryRemove(key, out var entry)) { - CloseSafely(connection); + CloseSafely(entry.Connection); } } + /// + /// Removes the connection associated with the provided cache key. + /// + protected bool TryRemoveByKey(string key) + { + if (string.IsNullOrEmpty(key)) + { + return false; + } + + if (_pool.TryRemove(key, out var entry)) + { + CloseSafely(entry.Connection); + return true; + } + + return false; + } + + /// + /// Provides a snapshot of the currently tracked connections. + /// + protected IReadOnlyList SnapshotConnections() + { + var entries = _pool.ToArray(); + var result = new ConnectionSnapshot[entries.Length]; + + for (int i = 0; i < entries.Length; i++) + { + var entry = entries[i]; + var connection = entry.Value.Connection; + bool isValid = IsConnectionHealthy(connection); + + result[i] = new ConnectionSnapshot( + entry.Key, + entry.Value.Parameters, + connection, + isValid); + } + + return result; + } + /// /// Executes an action against the managed connection, retrying once if the first attempt fails. /// @@ -119,6 +164,23 @@ namespace WelsonJS.Launcher throw lastError ?? new InvalidOperationException("Unreachable state in ExecuteWithRetryAsync"); } + private bool IsConnectionHealthy(TConnection connection) + { + if (connection == null) + { + return false; + } + + try + { + return IsConnectionValid(connection); + } + catch + { + return false; + } + } + private void RemoveInternal(string key, TConnection connection) { if (!string.IsNullOrEmpty(key)) @@ -143,5 +205,27 @@ namespace WelsonJS.Launcher // Ignore dispose exceptions. } } + + /// + /// Represents an immutable snapshot of a managed connection. + /// + protected readonly struct ConnectionSnapshot + { + public ConnectionSnapshot(string key, TParameters parameters, TConnection connection, bool isValid) + { + Key = key; + Parameters = parameters; + Connection = connection; + IsValid = isValid; + } + + public string Key { get; } + + public TParameters Parameters { get; } + + public TConnection Connection { get; } + + public bool IsValid { get; } + } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.Designer.cs new file mode 100644 index 0000000..a555de8 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.Designer.cs @@ -0,0 +1,132 @@ +namespace WelsonJS.Launcher +{ + partial class ConnectionMonitorForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + this.lvConnections = new System.Windows.Forms.ListView(); + this.chType = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chKey = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chState = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chDetails = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chHealth = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.btnRefresh = new System.Windows.Forms.Button(); + this.btnCloseSelected = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // lvConnections + // + this.lvConnections.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.chType, + this.chKey, + this.chState, + this.chDetails, + this.chHealth}); + this.lvConnections.FullRowSelect = true; + this.lvConnections.HideSelection = false; + this.lvConnections.Location = new System.Drawing.Point(12, 12); + this.lvConnections.MultiSelect = true; + this.lvConnections.Name = "lvConnections"; + this.lvConnections.Size = new System.Drawing.Size(640, 260); + this.lvConnections.TabIndex = 0; + this.lvConnections.UseCompatibleStateImageBehavior = false; + this.lvConnections.View = System.Windows.Forms.View.Details; + this.lvConnections.SelectedIndexChanged += new System.EventHandler(this.lvConnections_SelectedIndexChanged); + // + // chType + // + this.chType.Text = "Type"; + this.chType.Width = 100; + // + // chKey + // + this.chKey.Text = "Key"; + this.chKey.Width = 140; + // + // chState + // + this.chState.Text = "State"; + this.chState.Width = 100; + // + // chDetails + // + this.chDetails.Text = "Details"; + this.chDetails.Width = 220; + // + // chHealth + // + this.chHealth.Text = "Health"; + this.chHealth.Width = 80; + // + // btnRefresh + // + this.btnRefresh.Location = new System.Drawing.Point(12, 280); + this.btnRefresh.Name = "btnRefresh"; + this.btnRefresh.Size = new System.Drawing.Size(95, 30); + this.btnRefresh.TabIndex = 1; + this.btnRefresh.Text = "Refresh"; + this.btnRefresh.UseVisualStyleBackColor = true; + this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click); + // + // btnCloseSelected + // + this.btnCloseSelected.Enabled = false; + this.btnCloseSelected.Location = new System.Drawing.Point(557, 280); + this.btnCloseSelected.Name = "btnCloseSelected"; + this.btnCloseSelected.Size = new System.Drawing.Size(95, 30); + this.btnCloseSelected.TabIndex = 2; + this.btnCloseSelected.Text = "Close"; + this.btnCloseSelected.UseVisualStyleBackColor = true; + this.btnCloseSelected.Click += new System.EventHandler(this.btnCloseSelected_Click); + // + // ConnectionMonitorForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(664, 322); + this.Controls.Add(this.btnCloseSelected); + this.Controls.Add(this.btnRefresh); + this.Controls.Add(this.lvConnections); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.Icon = global::WelsonJS.Launcher.Properties.Resources.favicon; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "ConnectionMonitorForm"; + this.Text = "Connection Monitor"; + this.Load += new System.EventHandler(this.ConnectionMonitorForm_Load); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.ListView lvConnections; + private System.Windows.Forms.ColumnHeader chType; + private System.Windows.Forms.ColumnHeader chKey; + private System.Windows.Forms.ColumnHeader chState; + private System.Windows.Forms.ColumnHeader chDetails; + private System.Windows.Forms.ColumnHeader chHealth; + private System.Windows.Forms.Button btnRefresh; + private System.Windows.Forms.Button btnCloseSelected; + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.cs new file mode 100644 index 0000000..c7a9bbf --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.cs @@ -0,0 +1,137 @@ +// ConnectionMonitorForm.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace WelsonJS.Launcher +{ + public partial class ConnectionMonitorForm : Form + { + public ConnectionMonitorForm() + { + InitializeComponent(); + } + + private void ConnectionMonitorForm_Load(object sender, EventArgs e) + { + RefreshConnections(); + } + + private void btnRefresh_Click(object sender, EventArgs e) + { + RefreshConnections(); + } + + private void lvConnections_SelectedIndexChanged(object sender, EventArgs e) + { + btnCloseSelected.Enabled = lvConnections.SelectedItems.Count > 0; + } + + private void btnCloseSelected_Click(object sender, EventArgs e) + { + if (lvConnections.SelectedItems.Count == 0) + { + return; + } + + var errors = new List(); + bool anyClosed = false; + + foreach (ListViewItem item in lvConnections.SelectedItems) + { + if (item.Tag is ConnectionItemTag tag) + { + try + { + if (tag.Provider.TryClose(tag.Key)) + { + anyClosed = true; + } + else + { + errors.Add($"Unable to close {tag.Provider.ConnectionType} connection {tag.Key}."); + } + } + catch (Exception ex) + { + errors.Add($"{tag.Provider.ConnectionType} {tag.Key}: {ex.Message}"); + } + } + } + + if (anyClosed) + { + RefreshConnections(); + } + + if (errors.Count > 0) + { + MessageBox.Show(string.Join(Environment.NewLine, errors), "Connection Monitor", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } + + private void RefreshConnections() + { + IReadOnlyList providers = ConnectionMonitorRegistry.GetProviders(); + + lvConnections.BeginUpdate(); + lvConnections.Items.Clear(); + + foreach (var provider in providers) + { + IReadOnlyCollection statuses; + try + { + statuses = provider.GetStatuses(); + } + catch (Exception ex) + { + var errorItem = new ListViewItem(provider.ConnectionType) + { + Tag = null + }; + errorItem.SubItems.Add(string.Empty); + errorItem.SubItems.Add("Error"); + errorItem.SubItems.Add(ex.Message); + errorItem.SubItems.Add("Unknown"); + lvConnections.Items.Add(errorItem); + continue; + } + + foreach (var status in statuses) + { + var item = new ListViewItem(status.ConnectionType) + { + Tag = new ConnectionItemTag(provider, status.Key) + }; + item.SubItems.Add(status.Key); + item.SubItems.Add(status.State); + item.SubItems.Add(status.Description); + item.SubItems.Add(status.IsValid ? "Healthy" : "Stale"); + lvConnections.Items.Add(item); + } + } + + lvConnections.EndUpdate(); + + btnCloseSelected.Enabled = lvConnections.SelectedItems.Count > 0; + } + + private sealed class ConnectionItemTag + { + public ConnectionItemTag(IManagedConnectionProvider provider, string key) + { + Provider = provider; + Key = key ?? string.Empty; + } + + public IManagedConnectionProvider Provider { get; } + + public string Key { get; } + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.resx new file mode 100644 index 0000000..bdd5b01 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorRegistry.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorRegistry.cs new file mode 100644 index 0000000..fa639c4 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorRegistry.cs @@ -0,0 +1,56 @@ +// ConnectionMonitorRegistry.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections.Generic; + +namespace WelsonJS.Launcher +{ + /// + /// Keeps track of connection providers that should appear in the monitor UI. + /// + public static class ConnectionMonitorRegistry + { + private static readonly object _syncRoot = new object(); + private static readonly List _providers = new List(); + + public static void RegisterProvider(IManagedConnectionProvider provider) + { + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + lock (_syncRoot) + { + if (!_providers.Contains(provider)) + { + _providers.Add(provider); + } + } + } + + public static void UnregisterProvider(IManagedConnectionProvider provider) + { + if (provider == null) + { + return; + } + + lock (_syncRoot) + { + _providers.Remove(provider); + } + } + + public static IReadOnlyList GetProviders() + { + lock (_syncRoot) + { + return _providers.ToArray(); + } + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/IManagedConnectionProvider.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/IManagedConnectionProvider.cs new file mode 100644 index 0000000..44e055a --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/IManagedConnectionProvider.cs @@ -0,0 +1,30 @@ +// IManagedConnectionProvider.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System.Collections.Generic; + +namespace WelsonJS.Launcher +{ + /// + /// Exposes connection status information for use by the connection monitor UI. + /// + public interface IManagedConnectionProvider + { + /// + /// Gets a human-friendly name for the connection type managed by this provider. + /// + string ConnectionType { get; } + + /// + /// Retrieves the current connections handled by the provider. + /// + IReadOnlyCollection GetStatuses(); + + /// + /// Attempts to close the connection associated with the supplied cache key. + /// + bool TryClose(string key); + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs index 76ba271..e5faf00 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs @@ -39,6 +39,7 @@ this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.userdefinedVariablesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.instancesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.connectionMonitorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.runAsAdministratorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.globalSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.startTheEditorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -132,6 +133,7 @@ this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.userdefinedVariablesToolStripMenuItem, this.instancesToolStripMenuItem, + this.connectionMonitorToolStripMenuItem, this.runAsAdministratorToolStripMenuItem, this.globalSettingsToolStripMenuItem, this.startTheEditorToolStripMenuItem, @@ -153,6 +155,13 @@ this.instancesToolStripMenuItem.Size = new System.Drawing.Size(196, 22); this.instancesToolStripMenuItem.Text = "Instances"; this.instancesToolStripMenuItem.Click += new System.EventHandler(this.instancesToolStripMenuItem_Click); + // + // connectionMonitorToolStripMenuItem + // + this.connectionMonitorToolStripMenuItem.Name = "connectionMonitorToolStripMenuItem"; + this.connectionMonitorToolStripMenuItem.Size = new System.Drawing.Size(196, 22); + this.connectionMonitorToolStripMenuItem.Text = "Connections..."; + this.connectionMonitorToolStripMenuItem.Click += new System.EventHandler(this.connectionMonitorToolStripMenuItem_Click); // // runAsAdministratorToolStripMenuItem // @@ -282,6 +291,7 @@ private System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem userdefinedVariablesToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem instancesToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem connectionMonitorToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem runAsAdministratorToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem globalSettingsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem startTheEditorToolStripMenuItem; diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs index f55da4b..446650e 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs @@ -23,6 +23,7 @@ namespace WelsonJS.Launcher private string _workingDirectory; private string _instanceId; private string _scriptName; + private ConnectionMonitorForm _connectionMonitorForm; public MainForm(ICompatibleLogger logger = null) { @@ -276,6 +277,17 @@ namespace WelsonJS.Launcher (new InstancesForm()).Show(); } + private void connectionMonitorToolStripMenuItem_Click(object sender, EventArgs e) + { + if (_connectionMonitorForm == null || _connectionMonitorForm.IsDisposed) + { + _connectionMonitorForm = new ConnectionMonitorForm(); + } + + _connectionMonitorForm.Show(); + _connectionMonitorForm.Focus(); + } + private void runAsAdministratorToolStripMenuItem_Click(object sender, EventArgs e) { if (!IsInAdministrator()) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ManagedConnectionStatus.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ManagedConnectionStatus.cs new file mode 100644 index 0000000..263d5c2 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ManagedConnectionStatus.cs @@ -0,0 +1,32 @@ +// ManagedConnectionStatus.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +namespace WelsonJS.Launcher +{ + /// + /// Represents the state of a managed connection for UI presentation. + /// + public sealed class ManagedConnectionStatus + { + public ManagedConnectionStatus(string connectionType, string key, string state, string description, bool isValid) + { + ConnectionType = connectionType ?? string.Empty; + Key = key ?? string.Empty; + State = state ?? string.Empty; + Description = description ?? string.Empty; + IsValid = isValid; + } + + public string ConnectionType { get; } + + public string Key { get; } + + public string State { get; } + + public string Description { get; } + + public bool IsValid { get; } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs index 82d2fa3..144b5a8 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs @@ -4,6 +4,7 @@ // https://github.com/gnh1201/welsonjs // using System; +using System.Collections.Generic; using System.IO; using System.IO.Ports; using System.Text; @@ -12,8 +13,10 @@ using System.Threading.Tasks; namespace WelsonJS.Launcher { - public sealed class SerialPortManager : ConnectionManagerBase + public sealed class SerialPortManager : ConnectionManagerBase, IManagedConnectionProvider { + private const string ConnectionTypeName = "Serial Port"; + public struct ConnectionParameters { public ConnectionParameters( @@ -51,6 +54,13 @@ namespace WelsonJS.Launcher public int ReadBufferSize { get; } } + public SerialPortManager() + { + ConnectionMonitorRegistry.RegisterProvider(this); + } + + public string ConnectionType => ConnectionTypeName; + protected override string CreateKey(ConnectionParameters parameters) { return string.Join(",", new object[] @@ -133,6 +143,42 @@ namespace WelsonJS.Launcher cancellationToken).ConfigureAwait(false); } + public IReadOnlyCollection GetStatuses() + { + var snapshots = SnapshotConnections(); + var result = new List(snapshots.Count); + + foreach (var snapshot in snapshots) + { + string state; + try + { + state = snapshot.Connection?.IsOpen == true ? "Open" : "Closed"; + } + catch + { + state = "Unknown"; + } + + var parameters = snapshot.Parameters; + string description = $"{parameters.PortName} @ {parameters.BaudRate} bps"; + + result.Add(new ManagedConnectionStatus( + ConnectionTypeName, + snapshot.Key, + state, + description, + snapshot.IsValid)); + } + + return result; + } + + public bool TryClose(string key) + { + return TryRemoveByKey(key); + } + private static async Task SendAndReceiveInternalAsync( SerialPort port, int bufferSize, diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs index f34ebcb..433e13b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs @@ -4,6 +4,7 @@ // https://github.com/gnh1201/welsonjs // using System; +using System.Collections.Generic; using System.Net.WebSockets; using System.Security.Cryptography; using System.Text; @@ -12,8 +13,10 @@ using System.Threading.Tasks; namespace WelsonJS.Launcher { - public sealed class WebSocketManager : ConnectionManagerBase + public sealed class WebSocketManager : ConnectionManagerBase, IManagedConnectionProvider { + private const string ConnectionTypeName = "WebSocket"; + public struct Endpoint { public Endpoint(string host, int port, string path) @@ -28,6 +31,13 @@ namespace WelsonJS.Launcher public string Path { get; } } + public WebSocketManager() + { + ConnectionMonitorRegistry.RegisterProvider(this); + } + + public string ConnectionType => ConnectionTypeName; + protected override string CreateKey(Endpoint parameters) { string raw = parameters.Host + ":" + parameters.Port + "/" + parameters.Path; @@ -103,6 +113,42 @@ namespace WelsonJS.Launcher } } + public IReadOnlyCollection GetStatuses() + { + var snapshots = SnapshotConnections(); + var result = new List(snapshots.Count); + + foreach (var snapshot in snapshots) + { + string state; + try + { + state = snapshot.Connection?.State.ToString() ?? "Unknown"; + } + catch + { + state = "Unknown"; + } + + var endpoint = snapshot.Parameters; + var description = $"ws://{endpoint.Host}:{endpoint.Port}/{endpoint.Path}"; + + result.Add(new ManagedConnectionStatus( + ConnectionTypeName, + snapshot.Key, + state, + description, + snapshot.IsValid)); + } + + return result; + } + + public bool TryClose(string key) + { + return TryRemoveByKey(key); + } + // Actual send and receive implementation that never truncates the accumulated data. // - Uses a fixed-size read buffer ONLY for I/O // - Accumulates dynamically into a List until EndOfMessage diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index e1fc1f4..b171397 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -89,11 +89,20 @@ + + Form + + + ConnectionMonitorForm.cs + + + + @@ -136,6 +145,9 @@ EnvForm.cs + + ConnectionMonitorForm.cs + InstancesForm.cs