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