Add connection monitor for managed transports

This commit is contained in:
Namhyeon, Go 2025-10-28 13:42:35 +09:00
parent 6e759008dd
commit b8362e570c
12 changed files with 726 additions and 9 deletions

View File

@ -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<TParameters, TConnection>
where TConnection : class
{
private readonly ConcurrentDictionary<string, TConnection> _pool = new ConcurrentDictionary<string, TConnection>();
private readonly ConcurrentDictionary<string, (TConnection Connection, TParameters Parameters)> _pool
= new ConcurrentDictionary<string, (TConnection, TParameters)>();
/// <summary>
/// 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);
}
}
/// <summary>
/// Removes the connection associated with the provided cache key.
/// </summary>
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;
}
/// <summary>
/// Provides a snapshot of the currently tracked connections.
/// </summary>
protected IReadOnlyList<ConnectionSnapshot> 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;
}
/// <summary>
/// Executes an action against the managed connection, retrying once if the first attempt fails.
/// </summary>
@ -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.
}
}
/// <summary>
/// Represents an immutable snapshot of a managed connection.
/// </summary>
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; }
}
}
}

View File

@ -0,0 +1,132 @@
namespace WelsonJS.Launcher
{
partial class ConnectionMonitorForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
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;
}
}

View File

@ -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<string>();
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<IManagedConnectionProvider> providers = ConnectionMonitorRegistry.GetProviders();
lvConnections.BeginUpdate();
lvConnections.Items.Clear();
foreach (var provider in providers)
{
IReadOnlyCollection<ManagedConnectionStatus> 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; }
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -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
{
/// <summary>
/// Keeps track of connection providers that should appear in the monitor UI.
/// </summary>
public static class ConnectionMonitorRegistry
{
private static readonly object _syncRoot = new object();
private static readonly List<IManagedConnectionProvider> _providers = new List<IManagedConnectionProvider>();
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<IManagedConnectionProvider> GetProviders()
{
lock (_syncRoot)
{
return _providers.ToArray();
}
}
}
}

View File

@ -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
{
/// <summary>
/// Exposes connection status information for use by the connection monitor UI.
/// </summary>
public interface IManagedConnectionProvider
{
/// <summary>
/// Gets a human-friendly name for the connection type managed by this provider.
/// </summary>
string ConnectionType { get; }
/// <summary>
/// Retrieves the current connections handled by the provider.
/// </summary>
IReadOnlyCollection<ManagedConnectionStatus> GetStatuses();
/// <summary>
/// Attempts to close the connection associated with the supplied cache key.
/// </summary>
bool TryClose(string key);
}
}

View File

@ -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;

View File

@ -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())

View File

@ -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
{
/// <summary>
/// Represents the state of a managed connection for UI presentation.
/// </summary>
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; }
}
}

View File

@ -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<SerialPortManager.ConnectionParameters, SerialPort>
public sealed class SerialPortManager : ConnectionManagerBase<SerialPortManager.ConnectionParameters, SerialPort>, 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<ManagedConnectionStatus> GetStatuses()
{
var snapshots = SnapshotConnections();
var result = new List<ManagedConnectionStatus>(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<string> SendAndReceiveInternalAsync(
SerialPort port,
int bufferSize,

View File

@ -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<WebSocketManager.Endpoint, ClientWebSocket>
public sealed class WebSocketManager : ConnectionManagerBase<WebSocketManager.Endpoint, ClientWebSocket>, 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<ManagedConnectionStatus> GetStatuses()
{
var snapshots = SnapshotConnections();
var result = new List<ManagedConnectionStatus>(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<byte[]> until EndOfMessage

View File

@ -89,11 +89,20 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ConnectionManagerBase.cs" />
<Compile Include="ConnectionMonitorForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="ConnectionMonitorForm.Designer.cs">
<DependentUpon>ConnectionMonitorForm.cs</DependentUpon>
</Compile>
<Compile Include="ConnectionMonitorRegistry.cs" />
<Compile Include="ICompatibleLogger.cs" />
<Compile Include="IManagedConnectionProvider.cs" />
<Compile Include="IResourceTool.cs" />
<Compile Include="JsCore.cs" />
<Compile Include="JsNative.cs" />
<Compile Include="JsSerializer.cs" />
<Compile Include="ManagedConnectionStatus.cs" />
<Compile Include="NativeBootstrap.cs" />
<Compile Include="ResourceTools\ImageColorPicker.cs" />
<Compile Include="ResourceTools\IpQuery.cs" />
@ -136,6 +145,9 @@
<EmbeddedResource Include="EnvForm.resx">
<DependentUpon>EnvForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="ConnectionMonitorForm.resx">
<DependentUpon>ConnectionMonitorForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="InstancesForm.resx">
<DependentUpon>InstancesForm.cs</DependentUpon>
</EmbeddedResource>