From e9cc898608f596d70c1973ff8e51d3d4e33ba3d7 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 2 Nov 2025 17:59:10 +0900 Subject: [PATCH] Revert "Merge pull request #339 from gnh1201/codex/abstract-connection-management-for-serial-port" This reverts commit a638a7a6e94866c6229161f8d93247ec1b2c562c, reversing changes made to cab9013f18f3e964fd865cda4d838af24fab460e. --- .../ConnectionManagerBase.cs | 258 ------------------ .../ConnectionMonitorForm.Designer.cs | 132 --------- .../ConnectionMonitorForm.cs | 137 ---------- .../ConnectionMonitorForm.resx | 120 -------- .../ConnectionMonitorRegistry.cs | 56 ---- .../IManagedConnectionProvider.cs | 30 -- .../WelsonJS.Launcher/MainForm.Designer.cs | 10 - .../WelsonJS.Launcher/MainForm.cs | 12 - .../ManagedConnectionStatus.cs | 32 --- .../WelsonJS.Launcher/SerialPortManager.cs | 239 ---------------- .../WelsonJS.Launcher/WebSocketManager.cs | 165 +++++------ .../WelsonJS.Launcher.csproj | 15 - 12 files changed, 68 insertions(+), 1138 deletions(-) delete mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs delete mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.Designer.cs delete mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.cs delete mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.resx delete mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorRegistry.cs delete mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/IManagedConnectionProvider.cs delete mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ManagedConnectionStatus.cs delete mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs deleted file mode 100644 index 5723bd2..0000000 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs +++ /dev/null @@ -1,258 +0,0 @@ -// ConnectionManagerBase.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.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace WelsonJS.Launcher -{ - /// - /// Provides a reusable pattern for keeping long-lived connections alive and - /// recreating them transparently when the underlying connection becomes invalid. - /// - /// A descriptor used to create a unique key for each connection. - /// The concrete connection type. - public abstract class ConnectionManagerBase - where TConnection : class - { - private readonly ConcurrentDictionary _pool - = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _openLocks - = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _opLocks - = new ConcurrentDictionary(); - - /// - /// Creates a unique cache key for the given connection parameters. - /// - protected abstract string CreateKey(TParameters parameters); - - /// - /// Establishes a new connection using the provided parameters. - /// - protected abstract Task OpenConnectionAsync(TParameters parameters, CancellationToken token); - - /// - /// Validates whether the existing connection is still usable. - /// - protected abstract bool IsConnectionValid(TConnection connection); - - /// - /// Releases the resources associated with a connection instance. - /// - protected virtual void CloseConnection(TConnection connection) - { - if (connection is IDisposable disposable) - { - disposable.Dispose(); - } - } - - /// - /// Retrieves a cached connection or creates a new one if needed. - /// - protected async Task GetOrCreateAsync(TParameters parameters, CancellationToken token) - { - string key = CreateKey(parameters); - - if (_pool.TryGetValue(key, out var existing) && IsConnectionHealthy(existing.Connection)) - { - return existing.Connection; - } - - var gate = _openLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); - await gate.WaitAsync(token).ConfigureAwait(false); - try - { - if (_pool.TryGetValue(key, out existing) && IsConnectionHealthy(existing.Connection)) - { - return existing.Connection; - } - - if (existing.Connection != null && !IsConnectionHealthy(existing.Connection)) - { - RemoveInternal(key, existing.Connection); - } - - var connection = await OpenConnectionAsync(parameters, token).ConfigureAwait(false); - _pool[key] = (connection, parameters); - return connection; - } - finally - { - gate.Release(); - } - } - - /// - /// Removes the connection associated with the provided parameters. - /// - public void Remove(TParameters parameters) - { - string key = CreateKey(parameters); - if (_pool.TryRemove(key, out var entry)) - { - 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. - /// - protected async Task ExecuteWithRetryAsync( - TParameters parameters, - Func> operation, - int maxAttempts, - CancellationToken token) - { - if (operation == null) throw new ArgumentNullException(nameof(operation)); - if (maxAttempts < 1) throw new ArgumentOutOfRangeException(nameof(maxAttempts)); - - Exception lastError = null; - var key = CreateKey(parameters); - var opLock = _opLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); - - for (int attempt = 0; attempt < maxAttempts; attempt++) - { - await opLock.WaitAsync(token).ConfigureAwait(false); - try - { - token.ThrowIfCancellationRequested(); - var connection = await GetOrCreateAsync(parameters, token).ConfigureAwait(false); - return await operation(connection, token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - lastError = ex; - Remove(parameters); - if (attempt == maxAttempts - 1) - { - throw; - } - } - finally - { - opLock.Release(); - } - } - - 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)) - { - _pool.TryRemove(key, out _); - } - - if (connection != null) - { - CloseSafely(connection); - } - } - - private void CloseSafely(TConnection connection) - { - try - { - CloseConnection(connection); - } - catch - { - // 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 deleted file mode 100644 index a555de8..0000000 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.Designer.cs +++ /dev/null @@ -1,132 +0,0 @@ -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 deleted file mode 100644 index c7a9bbf..0000000 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.cs +++ /dev/null @@ -1,137 +0,0 @@ -// 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 deleted file mode 100644 index bdd5b01..0000000 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.resx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 deleted file mode 100644 index fa639c4..0000000 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorRegistry.cs +++ /dev/null @@ -1,56 +0,0 @@ -// 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 deleted file mode 100644 index 44e055a..0000000 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/IManagedConnectionProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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 e5faf00..76ba271 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs @@ -39,7 +39,6 @@ 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(); @@ -133,7 +132,6 @@ this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.userdefinedVariablesToolStripMenuItem, this.instancesToolStripMenuItem, - this.connectionMonitorToolStripMenuItem, this.runAsAdministratorToolStripMenuItem, this.globalSettingsToolStripMenuItem, this.startTheEditorToolStripMenuItem, @@ -155,13 +153,6 @@ 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 // @@ -291,7 +282,6 @@ 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 446650e..f55da4b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs @@ -23,7 +23,6 @@ namespace WelsonJS.Launcher private string _workingDirectory; private string _instanceId; private string _scriptName; - private ConnectionMonitorForm _connectionMonitorForm; public MainForm(ICompatibleLogger logger = null) { @@ -277,17 +276,6 @@ 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 deleted file mode 100644 index 263d5c2..0000000 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ManagedConnectionStatus.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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 deleted file mode 100644 index feadbcd..0000000 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs +++ /dev/null @@ -1,239 +0,0 @@ -// SerialPortManager.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.IO; -using System.IO.Ports; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace WelsonJS.Launcher -{ - public sealed class SerialPortManager : ConnectionManagerBase, IManagedConnectionProvider - { - private const string ConnectionTypeName = "Serial Port"; - - public struct ConnectionParameters - { - public ConnectionParameters( - string portName, - int baudRate, - Parity parity = Parity.None, - int dataBits = 8, - StopBits stopBits = StopBits.One, - Handshake handshake = Handshake.None, - int readTimeout = 500, - int writeTimeout = 500, - int readBufferSize = 1024, - bool resetBuffersBeforeRequest = false) - { - if (string.IsNullOrWhiteSpace(portName)) throw new ArgumentNullException(nameof(portName)); - - PortName = portName; - BaudRate = baudRate; - Parity = parity; - DataBits = dataBits; - StopBits = stopBits; - Handshake = handshake; - ReadTimeout = readTimeout; - WriteTimeout = writeTimeout; - ReadBufferSize = readBufferSize > 0 ? readBufferSize : 1024; - ResetBuffersBeforeRequest = resetBuffersBeforeRequest; - } - - public string PortName { get; } - public int BaudRate { get; } - public Parity Parity { get; } - public int DataBits { get; } - public StopBits StopBits { get; } - public Handshake Handshake { get; } - public int ReadTimeout { get; } - public int WriteTimeout { get; } - public int ReadBufferSize { get; } - public bool ResetBuffersBeforeRequest { get; } - } - - public SerialPortManager() - { - ConnectionMonitorRegistry.RegisterProvider(this); - } - - public string ConnectionType => ConnectionTypeName; - - protected override string CreateKey(ConnectionParameters parameters) - { - return string.Join(",", new object[] - { - parameters.PortName.ToUpperInvariant(), - parameters.BaudRate, - parameters.Parity, - parameters.DataBits, - parameters.StopBits, - parameters.Handshake, - parameters.ReadTimeout, - parameters.WriteTimeout - }); - } - - protected override Task OpenConnectionAsync(ConnectionParameters parameters, CancellationToken token) - { - token.ThrowIfCancellationRequested(); - - var port = new SerialPort(parameters.PortName, parameters.BaudRate, parameters.Parity, parameters.DataBits, parameters.StopBits) - { - Handshake = parameters.Handshake, - ReadTimeout = parameters.ReadTimeout, - WriteTimeout = parameters.WriteTimeout - }; - - try - { - port.Open(); - return Task.FromResult(port); - } - catch - { - port.Dispose(); - throw; - } - } - - protected override bool IsConnectionValid(SerialPort connection) - { - return connection != null && connection.IsOpen; - } - - protected override void CloseConnection(SerialPort connection) - { - try - { - if (connection != null && connection.IsOpen) - { - connection.Close(); - } - } - finally - { - connection?.Dispose(); - } - } - - public Task ExecuteAsync( - ConnectionParameters parameters, - Func> operation, - int maxAttempts = 2, - CancellationToken cancellationToken = default) - { - if (operation == null) throw new ArgumentNullException(nameof(operation)); - return ExecuteWithRetryAsync(parameters, operation, maxAttempts, cancellationToken); - } - - public async Task SendAndReceiveAsync( - ConnectionParameters parameters, - string message, - Encoding encoding, - CancellationToken cancellationToken = default) - { - if (encoding == null) throw new ArgumentNullException(nameof(encoding)); - byte[] payload = encoding.GetBytes(message ?? string.Empty); - - return await ExecuteWithRetryAsync( - parameters, - (port, token) => SendAndReceiveInternalAsync( - port, - parameters.ReadBufferSize, - payload, - encoding, - parameters.ResetBuffersBeforeRequest, - token), - 2, - 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, - byte[] payload, - Encoding encoding, - bool resetBuffers, - CancellationToken token) - { - if (resetBuffers) - { - port.DiscardInBuffer(); - port.DiscardOutBuffer(); - } - - if (payload.Length > 0) - { - await Task.Run(() => port.Write(payload, 0, payload.Length), token).ConfigureAwait(false); - } - - using (var stream = new MemoryStream()) - { - var buffer = new byte[bufferSize]; - const int MaxResponseBytes = 1 * 1024 * 1024; // 1 MiB safety cap - while (true) - { - try - { - int read = await Task.Run(() => port.Read(buffer, 0, buffer.Length), token).ConfigureAwait(false); - if (read > 0) - { - if (stream.Length + read > MaxResponseBytes) - throw new InvalidOperationException("Serial response exceeded maximum allowed size."); - stream.Write(buffer, 0, read); - continue; // keep reading until idle timeout - } - } - catch (TimeoutException) - { - break; - } - } - - return encoding.GetString(stream.ToArray()); - } - } - } -} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs index 433e13b..4d953c3 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs @@ -4,7 +4,7 @@ // https://github.com/gnh1201/welsonjs // using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Net.WebSockets; using System.Security.Cryptography; using System.Text; @@ -13,82 +13,86 @@ using System.Threading.Tasks; namespace WelsonJS.Launcher { - public sealed class WebSocketManager : ConnectionManagerBase, IManagedConnectionProvider + public class WebSocketManager { - private const string ConnectionTypeName = "WebSocket"; - - public struct Endpoint + private class Entry { - public Endpoint(string host, int port, string path) - { - Host = host ?? throw new ArgumentNullException(nameof(host)); - Port = port; - Path = path ?? string.Empty; - } - - public string Host { get; } - public int Port { get; } - public string Path { get; } + public ClientWebSocket Socket; + public string Host; + public int Port; + public string Path; } - public WebSocketManager() - { - ConnectionMonitorRegistry.RegisterProvider(this); - } + private readonly ConcurrentDictionary _pool = new ConcurrentDictionary(); - public string ConnectionType => ConnectionTypeName; - - protected override string CreateKey(Endpoint parameters) + // Create a unique cache key using MD5 hash + private string MakeKey(string host, int port, string path) { - string raw = parameters.Host + ":" + parameters.Port + "/" + parameters.Path; + string raw = host + ":" + port + "/" + path; using (var md5 = MD5.Create()) { byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(raw)); - return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); + return BitConverter.ToString(hash).Replace("-", "").ToLower(); } } - protected override async Task OpenConnectionAsync(Endpoint parameters, CancellationToken token) + // Get an open WebSocket or connect a new one + public async Task GetOrCreateAsync(string host, int port, string path) { - var socket = new ClientWebSocket(); - var uri = new Uri($"ws://{parameters.Host}:{parameters.Port}/{parameters.Path}"); + string key = MakeKey(host, port, path); + + if (_pool.TryGetValue(key, out var entry)) + { + var sock = entry.Socket; + + if (sock == null || sock.State != WebSocketState.Open) + { + Remove(host, port, path); + } + else + { + return sock; + } + } + + var newSock = new ClientWebSocket(); + var uri = new Uri($"ws://{host}:{port}/{path}"); try { - await socket.ConnectAsync(uri, token).ConfigureAwait(false); - return socket; + await newSock.ConnectAsync(uri, CancellationToken.None); + + _pool[key] = new Entry + { + Socket = newSock, + Host = host, + Port = port, + Path = path + }; + + return newSock; } catch (Exception ex) { - socket.Dispose(); + newSock.Dispose(); + Remove(host, port, path); throw new WebSocketException("WebSocket connection failed", ex); } } - protected override bool IsConnectionValid(ClientWebSocket connection) - { - return connection != null && connection.State == WebSocketState.Open; - } - - protected override void CloseConnection(ClientWebSocket connection) - { - try - { - connection?.Abort(); - } - catch - { - // Ignore abort exceptions. - } - finally - { - connection?.Dispose(); - } - } - + // Remove a socket from the pool and dispose it public void Remove(string host, int port, string path) { - Remove(new Endpoint(host, port, path)); + string key = MakeKey(host, port, path); + if (_pool.TryRemove(key, out var entry)) + { + try + { + entry.Socket?.Abort(); + entry.Socket?.Dispose(); + } + catch { /* Ignore dispose exceptions */ } + } } // Send and receive with automatic retry on first failure @@ -99,68 +103,35 @@ namespace WelsonJS.Launcher ? new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSec)) : new CancellationTokenSource(); - try + for (int attempt = 0; attempt < 2; attempt++) { - return await ExecuteWithRetryAsync( - new Endpoint(host, port, path), - (socket, token) => TrySendAndReceiveAsync(socket, buf, token), - 2, - cts.Token).ConfigureAwait(false); - } - finally - { - cts.Dispose(); - } - } - - 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"; + return await TrySendAndReceiveAsync(host, port, path, buf, cts.Token); } catch { - state = "Unknown"; + Remove(host, port, path); + if (attempt == 1) throw; } - - 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); + throw new InvalidOperationException("Unreachable"); } // 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 - private async Task TrySendAndReceiveAsync(ClientWebSocket socket, byte[] buf, CancellationToken token) + private async Task TrySendAndReceiveAsync(string host, int port, string path, byte[] buf, CancellationToken token) { try { - if (socket.State != WebSocketState.Open) + var sock = await GetOrCreateAsync(host, port, path); + if (sock.State != WebSocketState.Open) throw new WebSocketException("WebSocket is not in an open state"); // Send request as a single text frame - await socket.SendAsync(new ArraySegment(buf), WebSocketMessageType.Text, true, token).ConfigureAwait(false); + await sock.SendAsync(new ArraySegment(buf), WebSocketMessageType.Text, true, token); // Fixed-size read buffer for I/O (does NOT cap total message size) byte[] readBuffer = new byte[8192]; @@ -171,12 +142,12 @@ namespace WelsonJS.Launcher while (true) { - var res = await socket.ReceiveAsync(new ArraySegment(readBuffer), token).ConfigureAwait(false); + var res = await sock.ReceiveAsync(new ArraySegment(readBuffer), token); if (res.MessageType == WebSocketMessageType.Close) { - try { await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing as requested by server", token).ConfigureAwait(false); } catch { } - throw new WebSocketException($"WebSocket closed by server: {socket.CloseStatus} {socket.CloseStatusDescription}"); + try { await sock.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing as requested by server", token); } catch { } + throw new WebSocketException($"WebSocket closed by server: {sock.CloseStatus} {sock.CloseStatusDescription}"); } if (res.Count > 0) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index b171397..5bc8b61 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -80,7 +80,6 @@ - @@ -88,21 +87,11 @@ - - - Form - - - ConnectionMonitorForm.cs - - - - @@ -139,15 +128,11 @@ GlobalSettingsForm.cs - EnvForm.cs - - ConnectionMonitorForm.cs - InstancesForm.cs