From cfcfaf3e1fafeec7ac32edb5c43a9d9219aaf204 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 2 Nov 2025 18:09:59 +0900 Subject: [PATCH] Add WinForms interface for WebSocket management --- .../WelsonJS.Launcher/MainForm.Designer.cs | 13 +- .../WelsonJS.Launcher/MainForm.cs | 5 + .../WebSocketManagerForm.Designer.cs | 278 +++++++++++++++++ .../WelsonJS.Launcher/WebSocketManagerForm.cs | 288 ++++++++++++++++++ .../WebSocketManagerForm.resx | 15 + .../WelsonJS.Launcher.csproj | 9 + 6 files changed, 606 insertions(+), 2 deletions(-) create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.Designer.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.resx diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs index 76ba271..be45706 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs @@ -135,6 +135,7 @@ this.runAsAdministratorToolStripMenuItem, this.globalSettingsToolStripMenuItem, this.startTheEditorToolStripMenuItem, + this.webSocketManagerToolStripMenuItem, this.openCopilotToolStripMenuItem}); this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem"; this.settingsToolStripMenuItem.Size = new System.Drawing.Size(62, 20); @@ -174,9 +175,16 @@ this.startTheEditorToolStripMenuItem.Size = new System.Drawing.Size(196, 22); this.startTheEditorToolStripMenuItem.Text = "Start the editor..."; this.startTheEditorToolStripMenuItem.Click += new System.EventHandler(this.startCodeEditorToolStripMenuItem_Click); - // + // + // webSocketManagerToolStripMenuItem + // + this.webSocketManagerToolStripMenuItem.Name = "webSocketManagerToolStripMenuItem"; + this.webSocketManagerToolStripMenuItem.Size = new System.Drawing.Size(196, 22); + this.webSocketManagerToolStripMenuItem.Text = "WebSocket manager..."; + this.webSocketManagerToolStripMenuItem.Click += new System.EventHandler(this.webSocketManagerToolStripMenuItem_Click); + // // openCopilotToolStripMenuItem - // + // this.openCopilotToolStripMenuItem.Name = "openCopilotToolStripMenuItem"; this.openCopilotToolStripMenuItem.Size = new System.Drawing.Size(196, 22); this.openCopilotToolStripMenuItem.Text = "Open the Copilot..."; @@ -291,6 +299,7 @@ private System.Windows.Forms.ToolStripMenuItem openCodeEditorToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem openLauncherToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem openCopilotToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem webSocketManagerToolStripMenuItem; private System.Windows.Forms.SaveFileDialog saveFileDialog1; private System.Windows.Forms.Button btnStartTheEditor; private System.Windows.Forms.Button btnJoinTheCommunity; diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs index f55da4b..e3ce7dd 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs @@ -335,6 +335,11 @@ namespace WelsonJS.Launcher Program.OpenWebBrowser(Program.GetAppConfig("CopilotUrl")); } + private void webSocketManagerToolStripMenuItem_Click(object sender, EventArgs e) + { + (new WebSocketManagerForm()).Show(); + } + private void btnJoinTheCommunity_Click(object sender, EventArgs e) { Program.OpenWebBrowser(Program.GetAppConfig("RepositoryUrl")); diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.Designer.cs new file mode 100644 index 0000000..df51939 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.Designer.cs @@ -0,0 +1,278 @@ +// WebSocketManagerForm.Designer.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 +{ + partial class WebSocketManagerForm + { + /// + /// 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 + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.lvConnections = new System.Windows.Forms.ListView(); + this.chHost = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chPort = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chPath = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chStatus = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chUpdated = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.btnRefresh = new System.Windows.Forms.Button(); + this.gbNewConnection = new System.Windows.Forms.GroupBox(); + this.btnConnect = new System.Windows.Forms.Button(); + this.txtPath = new System.Windows.Forms.TextBox(); + this.lblPath = new System.Windows.Forms.Label(); + this.nudPort = new System.Windows.Forms.NumericUpDown(); + this.lblPort = new System.Windows.Forms.Label(); + this.txtHost = new System.Windows.Forms.TextBox(); + this.lblHost = new System.Windows.Forms.Label(); + this.btnDisconnect = new System.Windows.Forms.Button(); + this.btnClose = new System.Windows.Forms.Button(); + this.statusTimer = new System.Windows.Forms.Timer(this.components); + this.gbNewConnection.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudPort)).BeginInit(); + this.SuspendLayout(); + // + // lvConnections + // + this.lvConnections.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.chHost, + this.chPort, + this.chPath, + this.chStatus, + this.chUpdated}); + this.lvConnections.FullRowSelect = true; + this.lvConnections.HideSelection = false; + this.lvConnections.Location = new System.Drawing.Point(12, 12); + this.lvConnections.MultiSelect = false; + this.lvConnections.Name = "lvConnections"; + this.lvConnections.Size = new System.Drawing.Size(601, 188); + 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); + // + // chHost + // + this.chHost.Text = "Host"; + this.chHost.Width = 150; + // + // chPort + // + this.chPort.Text = "Port"; + this.chPort.Width = 80; + // + // chPath + // + this.chPath.Text = "Path"; + this.chPath.Width = 150; + // + // chStatus + // + this.chStatus.Text = "Status"; + this.chStatus.Width = 120; + // + // chUpdated + // + this.chUpdated.Text = "Last Updated"; + this.chUpdated.Width = 180; + // + // btnRefresh + // + this.btnRefresh.Location = new System.Drawing.Point(518, 206); + this.btnRefresh.Name = "btnRefresh"; + this.btnRefresh.Size = new System.Drawing.Size(95, 23); + this.btnRefresh.TabIndex = 1; + this.btnRefresh.Text = "Refresh"; + this.btnRefresh.UseVisualStyleBackColor = true; + this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click); + // + // gbNewConnection + // + this.gbNewConnection.Controls.Add(this.btnConnect); + this.gbNewConnection.Controls.Add(this.txtPath); + this.gbNewConnection.Controls.Add(this.lblPath); + this.gbNewConnection.Controls.Add(this.nudPort); + this.gbNewConnection.Controls.Add(this.lblPort); + this.gbNewConnection.Controls.Add(this.txtHost); + this.gbNewConnection.Controls.Add(this.lblHost); + this.gbNewConnection.Location = new System.Drawing.Point(12, 235); + this.gbNewConnection.Name = "gbNewConnection"; + this.gbNewConnection.Size = new System.Drawing.Size(601, 100); + this.gbNewConnection.TabIndex = 2; + this.gbNewConnection.TabStop = false; + this.gbNewConnection.Text = "New connection"; + // + // btnConnect + // + this.btnConnect.Location = new System.Drawing.Point(470, 56); + this.btnConnect.Name = "btnConnect"; + this.btnConnect.Size = new System.Drawing.Size(112, 27); + this.btnConnect.TabIndex = 6; + this.btnConnect.Text = "Connect"; + this.btnConnect.UseVisualStyleBackColor = true; + this.btnConnect.Click += new System.EventHandler(this.btnConnect_Click); + // + // txtPath + // + this.txtPath.Location = new System.Drawing.Point(70, 60); + this.txtPath.Name = "txtPath"; + this.txtPath.Size = new System.Drawing.Size(318, 21); + this.txtPath.TabIndex = 5; + this.txtPath.TextChanged += new System.EventHandler(this.ConnectionInputChanged); + // + // lblPath + // + this.lblPath.AutoSize = true; + this.lblPath.Location = new System.Drawing.Point(15, 63); + this.lblPath.Name = "lblPath"; + this.lblPath.Size = new System.Drawing.Size(31, 12); + this.lblPath.TabIndex = 4; + this.lblPath.Text = "Path"; + // + // nudPort + // + this.nudPort.Location = new System.Drawing.Point(308, 26); + this.nudPort.Maximum = new decimal(new int[] { + 65535, + 0, + 0, + 0}); + this.nudPort.Minimum = new decimal(new int[] { + 1, + 0, + 0, + 0}); + this.nudPort.Name = "nudPort"; + this.nudPort.Size = new System.Drawing.Size(80, 21); + this.nudPort.TabIndex = 3; + this.nudPort.Value = new decimal(new int[] { + 80, + 0, + 0, + 0}); + this.nudPort.ValueChanged += new System.EventHandler(this.ConnectionInputChanged); + // + // lblPort + // + this.lblPort.AutoSize = true; + this.lblPort.Location = new System.Drawing.Point(268, 29); + this.lblPort.Name = "lblPort"; + this.lblPort.Size = new System.Drawing.Size(29, 12); + this.lblPort.TabIndex = 2; + this.lblPort.Text = "Port"; + // + // txtHost + // + this.txtHost.Location = new System.Drawing.Point(70, 26); + this.txtHost.Name = "txtHost"; + this.txtHost.Size = new System.Drawing.Size(180, 21); + this.txtHost.TabIndex = 1; + this.txtHost.TextChanged += new System.EventHandler(this.ConnectionInputChanged); + // + // lblHost + // + this.lblHost.AutoSize = true; + this.lblHost.Location = new System.Drawing.Point(15, 29); + this.lblHost.Name = "lblHost"; + this.lblHost.Size = new System.Drawing.Size(31, 12); + this.lblHost.TabIndex = 0; + this.lblHost.Text = "Host"; + // + // btnDisconnect + // + this.btnDisconnect.Location = new System.Drawing.Point(12, 341); + this.btnDisconnect.Name = "btnDisconnect"; + this.btnDisconnect.Size = new System.Drawing.Size(126, 27); + this.btnDisconnect.TabIndex = 3; + this.btnDisconnect.Text = "Disconnect"; + this.btnDisconnect.UseVisualStyleBackColor = true; + this.btnDisconnect.Click += new System.EventHandler(this.btnDisconnect_Click); + // + // btnClose + // + this.btnClose.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.btnClose.Location = new System.Drawing.Point(487, 341); + this.btnClose.Name = "btnClose"; + this.btnClose.Size = new System.Drawing.Size(126, 27); + this.btnClose.TabIndex = 4; + this.btnClose.Text = "Close"; + this.btnClose.UseVisualStyleBackColor = true; + this.btnClose.Click += new System.EventHandler(this.btnClose_Click); + // + // statusTimer + // + this.statusTimer.Interval = 2000; + this.statusTimer.Tick += new System.EventHandler(this.statusTimer_Tick); + // + // WebSocketManagerForm + // + this.AcceptButton = this.btnConnect; + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.btnClose; + this.ClientSize = new System.Drawing.Size(625, 380); + this.Controls.Add(this.btnClose); + this.Controls.Add(this.btnDisconnect); + this.Controls.Add(this.gbNewConnection); + this.Controls.Add(this.btnRefresh); + this.Controls.Add(this.lvConnections); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "WebSocketManagerForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "WebSocket Manager"; + this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.WebSocketManagerForm_FormClosed); + this.Load += new System.EventHandler(this.WebSocketManagerForm_Load); + this.gbNewConnection.ResumeLayout(false); + this.gbNewConnection.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudPort)).EndInit(); + this.ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.ListView lvConnections; + private System.Windows.Forms.ColumnHeader chHost; + private System.Windows.Forms.ColumnHeader chPort; + private System.Windows.Forms.ColumnHeader chPath; + private System.Windows.Forms.ColumnHeader chStatus; + private System.Windows.Forms.ColumnHeader chUpdated; + private System.Windows.Forms.Button btnRefresh; + private System.Windows.Forms.GroupBox gbNewConnection; + private System.Windows.Forms.Button btnConnect; + private System.Windows.Forms.TextBox txtPath; + private System.Windows.Forms.Label lblPath; + private System.Windows.Forms.NumericUpDown nudPort; + private System.Windows.Forms.Label lblPort; + private System.Windows.Forms.TextBox txtHost; + private System.Windows.Forms.Label lblHost; + private System.Windows.Forms.Button btnDisconnect; + private System.Windows.Forms.Button btnClose; + private System.Windows.Forms.Timer statusTimer; + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.cs new file mode 100644 index 0000000..ea3e65e --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.cs @@ -0,0 +1,288 @@ +// WebSocketManagerForm.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.Net.WebSockets; +using System.Windows.Forms; + +namespace WelsonJS.Launcher +{ + public partial class WebSocketManagerForm : Form + { + private sealed class ConnectionInfo + { + public string Host; + public int Port; + public string Path; + } + + private sealed class ConnectionRecord + { + public ConnectionInfo Info; + public ClientWebSocket Socket; + public string LastStatus; + } + + private readonly WebSocketManager _manager; + private readonly ConcurrentDictionary _records; + private readonly string _dateTimeFormat; + + public WebSocketManagerForm() + { + InitializeComponent(); + + _manager = new WebSocketManager(); + _records = new ConcurrentDictionary(); + + string format = Program.GetAppConfig("DateTimeFormat"); + if (string.IsNullOrWhiteSpace(format)) + { + format = "yyyy-MM-dd HH:mm:ss"; + } + + _dateTimeFormat = format; + btnConnect.Enabled = false; + } + + private void WebSocketManagerForm_Load(object sender, EventArgs e) + { + statusTimer.Start(); + UpdateButtons(); + } + + private void WebSocketManagerForm_FormClosed(object sender, FormClosedEventArgs e) + { + statusTimer.Stop(); + } + + private void ConnectionInputChanged(object sender, EventArgs e) + { + btnConnect.Enabled = !string.IsNullOrWhiteSpace(txtHost.Text); + } + + private async void btnConnect_Click(object sender, EventArgs e) + { + string host = txtHost.Text.Trim(); + string path = NormalizePath(txtPath.Text); + int port = (int)nudPort.Value; + + if (string.IsNullOrEmpty(host)) + { + MessageBox.Show("Host is required.", "WebSocket Manager", MessageBoxButtons.OK, MessageBoxIcon.Warning); + txtHost.Focus(); + return; + } + + btnConnect.Enabled = false; + + try + { + ConnectionInfo info = new ConnectionInfo + { + Host = host, + Port = port, + Path = path + }; + + ClientWebSocket socket = await _manager.GetOrCreateAsync(info.Host, info.Port, info.Path); + + string key = BuildKey(info.Host, info.Port, info.Path); + ConnectionRecord record = _records.GetOrAdd(key, k => new ConnectionRecord { Info = info }); + record.Info = info; + record.Socket = socket; + record.LastStatus = socket.State.ToString(); + + AddOrUpdateListViewItem(record, record.LastStatus); + RefreshStatuses(); + } + catch (Exception ex) + { + MessageBox.Show("Failed to connect: " + ex.Message, "WebSocket Manager", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + btnConnect.Enabled = !string.IsNullOrWhiteSpace(txtHost.Text); + } + } + + private void btnDisconnect_Click(object sender, EventArgs e) + { + ConnectionRecord record = GetSelectedRecord(); + if (record == null) + { + return; + } + + try + { + _manager.Remove(record.Info.Host, record.Info.Port, record.Info.Path); + + if (record.Socket != null) + { + try + { + record.Socket.Dispose(); + } + catch + { + // Ignore dispose errors from socket + } + record.Socket = null; + } + + record.LastStatus = "Closed"; + AddOrUpdateListViewItem(record, record.LastStatus); + } + catch (Exception ex) + { + MessageBox.Show("Failed to disconnect: " + ex.Message, "WebSocket Manager", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void btnRefresh_Click(object sender, EventArgs e) + { + RefreshStatuses(); + } + + private void btnClose_Click(object sender, EventArgs e) + { + Close(); + } + + private void statusTimer_Tick(object sender, EventArgs e) + { + RefreshStatuses(); + } + + private void lvConnections_SelectedIndexChanged(object sender, EventArgs e) + { + UpdateButtons(); + } + + private void UpdateButtons() + { + btnDisconnect.Enabled = lvConnections.SelectedItems.Count > 0; + } + + private void RefreshStatuses() + { + foreach (ListViewItem item in lvConnections.Items) + { + ConnectionRecord record = item.Tag as ConnectionRecord; + if (record == null) + { + continue; + } + + string status = record.LastStatus ?? "Unknown"; + if (record.Socket != null) + { + try + { + status = record.Socket.State.ToString(); + record.LastStatus = status; + } + catch (ObjectDisposedException) + { + record.Socket = null; + status = "Disposed"; + record.LastStatus = status; + } + } + + if (item.SubItems.Count >= 5) + { + item.SubItems[3].Text = status; + item.SubItems[4].Text = DateTime.Now.ToString(_dateTimeFormat); + } + } + } + + private void AddOrUpdateListViewItem(ConnectionRecord record, string status) + { + string key = BuildKey(record.Info.Host, record.Info.Port, record.Info.Path); + string formattedPath = FormatPath(record.Info.Path); + string timestamp = DateTime.Now.ToString(_dateTimeFormat); + + ListViewItem item; + if (lvConnections.Items.ContainsKey(key)) + { + item = lvConnections.Items[key]; + item.SubItems[0].Text = record.Info.Host; + item.SubItems[1].Text = record.Info.Port.ToString(); + item.SubItems[2].Text = formattedPath; + item.SubItems[3].Text = status; + item.SubItems[4].Text = timestamp; + item.Tag = record; + } + else + { + item = new ListViewItem(new string[] + { + record.Info.Host, + record.Info.Port.ToString(), + formattedPath, + status, + timestamp + }); + item.Name = key; + item.Tag = record; + lvConnections.Items.Add(item); + } + } + + private ConnectionRecord GetSelectedRecord() + { + if (lvConnections.SelectedItems.Count == 0) + { + return null; + } + + return lvConnections.SelectedItems[0].Tag as ConnectionRecord; + } + + private static string NormalizePath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return string.Empty; + } + + string trimmed = path.Trim(); + while (trimmed.StartsWith("/", StringComparison.Ordinal)) + { + trimmed = trimmed.Substring(1); + } + + return trimmed; + } + + private static string FormatPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return "/"; + } + + return path.StartsWith("/", StringComparison.Ordinal) ? path : "/" + path; + } + + private static string BuildKey(string host, int port, string path) + { + if (host == null) + { + host = string.Empty; + } + + if (path == null) + { + path = string.Empty; + } + + return host.ToLowerInvariant() + ":" + port + "/" + path; + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.resx new file mode 100644 index 0000000..1510323 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManagerForm.resx @@ -0,0 +1,15 @@ + + + + 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/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index 5bc8b61..c5a314f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -127,6 +127,12 @@ GlobalSettingsForm.cs + + Form + + + WebSocketManagerForm.cs + @@ -147,6 +153,9 @@ GlobalSettingsForm.cs + + WebSocketManagerForm.cs + SettingsSingleFileGenerator