From a0360c3d06561a144cde846bfd72f7e164f597e9 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 28 Sep 2025 15:24:28 +0900 Subject: [PATCH 01/17] Fix broken "rdbl.io" links Fix broken "rdbl.io" links --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e1450a6..18a32b2 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgnh1201%2Fwelsonjs.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgnh1201%2Fwelsonjs?ref=badge_shield) [![AppVeyor Status](https://ci.appveyor.com/api/projects/status/github/gnh1201/welsonjs?svg=true)](https://ci.appveyor.com/project/gnh1201/welsonjs) [![DOI 10.5281/zenodo.11382384](https://zenodo.org/badge/DOI/10.5281/zenodo.11382384.svg)](https://doi.org/10.5281/zenodo.11382384) -[![ChatGPT available](https://img.shields.io/badge/ChatGPT-74aa9c?logo=openai&logoColor=white)](https://catswords-oss.rdbl.io/5719744820/5510319392) -[![Anthropic available](https://img.shields.io/badge/Anthropic-000000?logo=Anthropic&logoColor=white)](https://catswords-oss.rdbl.io/5719744820/5510319392) -[![Grok available](https://img.shields.io/badge/Grok-000000?logo=x&logoColor=white)](https://catswords-oss.rdbl.io/5719744820/5510319392) -[![Google Gemini available](https://img.shields.io/badge/Google%20Gemini-886FBF?logo=googlegemini&logoColor=fff)](https://catswords-oss.rdbl.io/5719744820/5510319392) +[![ChatGPT available](https://img.shields.io/badge/ChatGPT-74aa9c?logo=openai&logoColor=white)](#) +[![Anthropic available](https://img.shields.io/badge/Anthropic-000000?logo=Anthropic&logoColor=white)](#) +[![Grok available](https://img.shields.io/badge/Grok-000000?logo=x&logoColor=white)](#) +[![Google Gemini available](https://img.shields.io/badge/Google%20Gemini-886FBF?logo=googlegemini&logoColor=fff)](#) [![slideshare.net presentation](https://img.shields.io/badge/SlideShare-black?logo=slideshare)](https://www.slideshare.net/slideshow/welsonjs-javascript-framework-presentation-2024/276005486) [![YouTube promotion video](https://img.shields.io/badge/YouTube-red?logo=youtube)](https://youtu.be/JavH7Dms8-U) [![Discord chat](https://img.shields.io/discord/359930650330923008?logo=discord)](https://discord.gg/XKG5CjtXEj) @@ -33,7 +33,7 @@ WelsonJS = ***W***indows + ***El***ectr***on***-like + ***Javascript(JS)*** + :h * Free code signing provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org/) * [F1Security(에프원시큐리티)](https://f1security.co.kr/) provides [industry-leading](https://www.ksecurity.or.kr/kisis/subIndex/469.do) web security services. * [Microsoft ISV Success Program](https://www.microsoft.com/en-us/isv/isv-success), Grow your business with powerful tools. -* :zap: [Integrations (e.g., Aviation, Shopping)...](https://catswords-oss.rdbl.io/5719744820/8278298336) +* :zap: Integrations (e.g., Aviation, Shopping)... ## System Requirements * **Operating Systems**: Windows XP SP3 or later (Currently, Windows 11 24H2) @@ -73,9 +73,9 @@ WelsonJS is tailored for developers who need a reliable, lightweight JavaScript * Compatible with modern JavaScript specifications: [module.exports](https://nodejs.org/api/modules.html#moduleexports), CommonJS, UMD compatibility, [NPM(Node Package Manager)](https://www.npmjs.com/) compatibility * Support a device debugging protocol clients: [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/), [ADB(Android Debug Bridge)](https://source.android.com/docs/setup/build/adb) * RPC(Remote Procedure Call) protocol clients: [gRPC](https://grpc.io/), [JSON-RPC 2.0](https://www.jsonrpc.org/specification) -* Various types of HTTP clients: [XHR(MSXML)](https://developer.mozilla.org/docs/Glossary/XMLHttpRequest), [cURL](https://curl.se/), [BITS](https://en.m.wikipedia.org/w/index.php?title=Background_Intelligent_Transfer_Service), [CERT](https://github.com/MicrosoftDocs/windowsserverdocs/blob/main/WindowsServerDocs/administration/windows-commands/certutil.md), [Web Proxy, SEO/SERP](https://catswords-oss.rdbl.io/5719744820/1706431912) +* Various types of HTTP clients: [XHR(MSXML)](https://developer.mozilla.org/docs/Glossary/XMLHttpRequest), [cURL](https://curl.se/), [BITS](https://en.m.wikipedia.org/w/index.php?title=Background_Intelligent_Transfer_Service), [CERT](https://github.com/MicrosoftDocs/windowsserverdocs/blob/main/WindowsServerDocs/administration/windows-commands/certutil.md), Web Proxy, SEO and SERP. * The native toolkit for Windows environments: Write a Windows Service Application with JavaScript, Control a window handle, Cryptography (e.g., [ISO/IEC 18033-3:2010](https://www.iso.org/standard/54531.html) aka. [HIGHT](https://seed.kisa.or.kr/kisa/algorithm/EgovHightInfo.do)), [Named Shared Memory](https://learn.microsoft.com/en-us/windows/win32/memory/creating-named-shared-memory) based [IPC](https://qiita.com/gnh1201/items/4e70dccdb7adacf0ace5), [NuGet package](https://www.nuget.org/packages/WelsonJS.Toolkit) -* Generative AI integrations: [Multiple LLM and sLLM](https://catswords-oss.rdbl.io/5719744820/5510319392) (e.g., ChatGPT, Claude, ...) +* Generative AI (LLM) integrations: ChatGPT, Claude, Gemini, ... * Aviation Data integrations: [AviationStack](https://aviationstack.com?utm_source=FirstPromoter&utm_medium=Affiliate&fpr=namhyeon71), [SerpApi Google Flights API](https://serpapi.com/google-flights-api?utm_source=welsonjs) * VM infrastructure tool integrations: [OVFTool for Broadcom/VMware infrastructures](https://developer.broadcom.com/tools/open-virtualization-format-ovf-tool/latest) * ***:fire: NEW!*** Windows bulit-in database engine AKA. [ESENT (ESE) database](https://learn.microsoft.com/en-us/windows/win32/extensible-storage-engine/database-overview) interface library (WelsonJS.Esent) @@ -123,7 +123,7 @@ ended say() ## How to release my application? The WelsonJS framework suggests the following application release methods: -* **Compress to Zip, and use the launcher**: Compress the files and directories necessary for running the project into a Zip file, and distribute it along with the [WelsonJS Launcher](https://catswords-oss.rdbl.io/5719744820/4131485779). +* **Compress to Zip, and use the launcher**: Compress the files and directories necessary for running the project into a Zip file, and distribute it along with the WelsonJS Launcher. * **Build a setup file**: Use [Inno Setup](https://jrsoftware.org/isinfo.php). The setup profile (the `setup.iss` file) is already included. * **Copy all directories and files**: This is the simplest and most straightforward method. From 4239353a44cb68ed770dda98f0911f8a4ba0be11 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 10 Oct 2025 09:43:24 +0900 Subject: [PATCH 02/17] Update README.md --- README.md | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 18a32b2..b8fe64d 100644 --- a/README.md +++ b/README.md @@ -151,22 +151,11 @@ The WelsonJS framework suggests the following application release methods: * :sunglasses: Information security companies in Republic of Korea - Use case development * :sunglasses: Travel planning(e.g., Airlines, Hotels, Ticketing) related companies - Use case development * :sunglasses: Probability-based game prediction in a data analytics company - Use case development -* :eyes: [Facebook Group "Javascript Programming"(javascript4u)](https://www.facebook.com/javascript4u/posts/build-a-windows-desktop-apps-with-javascript-html-and-cssmorioh-javascript-html-/1484014618472735/) -* :eyes: [morioh.com](https://morioh.com/a/23c427a82bf1/build-a-windows-desktop-apps-with-javascript-html-and-css) -* :eyes: CSDN -* :eyes: Qiita - Knowledge-base about WSH environment +* :sunglasses: Qiita - Knowledge-base about WSH environment * :sunglasses: Redsky Software - PoC(Proof of Concept) of the CommonJS on WSH environment * :sunglasses: Inspired by a small-sized JavaScript payload demonstrated by a cybersecurity related group. * :sunglasses: Inspired by the use of Named Shared Memory in a cross-runtime IPC implementation written by the unidentified developer. -* :eyes: Fediverse -* :eyes: [Hacker News](https://news.ycombinator.com/item?id=41316782) -* :eyes: [WebToolsWeekly](https://webtoolsweekly.com/archives/issue-585/) -* :eyes: [GeekNews](https://news.hada.io/weekly/202441) in GeekNews Weekly (2024-09-30 ~ 2024-10-06) -* :eyes: [daily.dev](https://app.daily.dev/posts/js-libraries-svg-tools-json-databases-8quregz3a) -* :eyes: [PitchHut](https://www.pitchhut.com/project/proj_Ya136OLSW5at) -* :eyes: [Disquiet](https://dis.qa/nv6T6) -* :eyes: [Node Weekly](https://nodeweekly.com/issues/582) -* :eyes: [Zhouexin (周e信)](https://www.zhouexin.com/issues/321) +* :eyes: [Facebook Group "Javascript Programming"(javascript4u)](https://www.facebook.com/javascript4u/posts/build-a-windows-desktop-apps-with-javascript-html-and-cssmorioh-javascript-html-/1484014618472735/), [morioh.com](https://morioh.com/a/23c427a82bf1/build-a-windows-desktop-apps-with-javascript-html-and-css), CSDN, Fediverse, [Hacker News](https://news.ycombinator.com/item?id=41316782), [WebToolsWeekly](https://webtoolsweekly.com/archives/issue-585/), [GeekNews in GeekNews Weekly (2024-09-30 ~ 2024-10-06)](https://news.hada.io/weekly/202441), [daily.dev](https://app.daily.dev/posts/js-libraries-svg-tools-json-databases-8quregz3a), [PitchHut](https://www.pitchhut.com/project/proj_Ya136OLSW5at), [Disquiet](https://dis.qa/nv6T6), [Node Weekly (#​582 - June 17, 2025)](https://nodeweekly.com/issues/582), [Zhouexin (周e信)](https://www.zhouexin.com/issues/321), [Echo JS](https://www.echojs.com/news/43008), [Telegram Channel @front_end_dev](https://t.me/front_end_dev/9376?ysclid=mgk4a9hqf0853890652) ## Report abuse * [GitHub Security Advisories (gnh1201/welsonjs)](https://github.com/gnh1201/welsonjs/security) From a4eff4a1fa671a45e0d08c7c509060b235c2e04c Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 10 Oct 2025 09:45:00 +0900 Subject: [PATCH 03/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8fe64d..7121d38 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ The WelsonJS framework suggests the following application release methods: * :sunglasses: Redsky Software - PoC(Proof of Concept) of the CommonJS on WSH environment * :sunglasses: Inspired by a small-sized JavaScript payload demonstrated by a cybersecurity related group. * :sunglasses: Inspired by the use of Named Shared Memory in a cross-runtime IPC implementation written by the unidentified developer. -* :eyes: [Facebook Group "Javascript Programming"(javascript4u)](https://www.facebook.com/javascript4u/posts/build-a-windows-desktop-apps-with-javascript-html-and-cssmorioh-javascript-html-/1484014618472735/), [morioh.com](https://morioh.com/a/23c427a82bf1/build-a-windows-desktop-apps-with-javascript-html-and-css), CSDN, Fediverse, [Hacker News](https://news.ycombinator.com/item?id=41316782), [WebToolsWeekly](https://webtoolsweekly.com/archives/issue-585/), [GeekNews in GeekNews Weekly (2024-09-30 ~ 2024-10-06)](https://news.hada.io/weekly/202441), [daily.dev](https://app.daily.dev/posts/js-libraries-svg-tools-json-databases-8quregz3a), [PitchHut](https://www.pitchhut.com/project/proj_Ya136OLSW5at), [Disquiet](https://dis.qa/nv6T6), [Node Weekly (#​582 - June 17, 2025)](https://nodeweekly.com/issues/582), [Zhouexin (周e信)](https://www.zhouexin.com/issues/321), [Echo JS](https://www.echojs.com/news/43008), [Telegram Channel @front_end_dev](https://t.me/front_end_dev/9376?ysclid=mgk4a9hqf0853890652) +* :eyes: [Hacker News](https://news.ycombinator.com/item?id=41316782), [Node Weekly (#​582 - June 17, 2025)](https://nodeweekly.com/issues/582), [WebToolsWeekly](https://webtoolsweekly.com/archives/issue-585/), [GeekNews in GeekNews Weekly (2024-09-30 ~ 2024-10-06)](https://news.hada.io/weekly/202441), [Facebook Group "Javascript Programming"(javascript4u)](https://www.facebook.com/javascript4u/posts/build-a-windows-desktop-apps-with-javascript-html-and-cssmorioh-javascript-html-/1484014618472735/), [morioh.com](https://morioh.com/a/23c427a82bf1/build-a-windows-desktop-apps-with-javascript-html-and-css), CSDN, Fediverse, , [daily.dev](https://app.daily.dev/posts/js-libraries-svg-tools-json-databases-8quregz3a), [PitchHut](https://www.pitchhut.com/project/proj_Ya136OLSW5at), [Disquiet](https://dis.qa/nv6T6), [Zhouexin (周e信)](https://www.zhouexin.com/issues/321), [Echo JS](https://www.echojs.com/news/43008), [Telegram Channel @front_end_dev](https://t.me/front_end_dev/9376?ysclid=mgk4a9hqf0853890652) ## Report abuse * [GitHub Security Advisories (gnh1201/welsonjs)](https://github.com/gnh1201/welsonjs/security) From deefa25ada82439657650df049416cb278f92add Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 10 Oct 2025 09:45:49 +0900 Subject: [PATCH 04/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7121d38..b099180 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ The WelsonJS framework suggests the following application release methods: * :sunglasses: Redsky Software - PoC(Proof of Concept) of the CommonJS on WSH environment * :sunglasses: Inspired by a small-sized JavaScript payload demonstrated by a cybersecurity related group. * :sunglasses: Inspired by the use of Named Shared Memory in a cross-runtime IPC implementation written by the unidentified developer. -* :eyes: [Hacker News](https://news.ycombinator.com/item?id=41316782), [Node Weekly (#​582 - June 17, 2025)](https://nodeweekly.com/issues/582), [WebToolsWeekly](https://webtoolsweekly.com/archives/issue-585/), [GeekNews in GeekNews Weekly (2024-09-30 ~ 2024-10-06)](https://news.hada.io/weekly/202441), [Facebook Group "Javascript Programming"(javascript4u)](https://www.facebook.com/javascript4u/posts/build-a-windows-desktop-apps-with-javascript-html-and-cssmorioh-javascript-html-/1484014618472735/), [morioh.com](https://morioh.com/a/23c427a82bf1/build-a-windows-desktop-apps-with-javascript-html-and-css), CSDN, Fediverse, , [daily.dev](https://app.daily.dev/posts/js-libraries-svg-tools-json-databases-8quregz3a), [PitchHut](https://www.pitchhut.com/project/proj_Ya136OLSW5at), [Disquiet](https://dis.qa/nv6T6), [Zhouexin (周e信)](https://www.zhouexin.com/issues/321), [Echo JS](https://www.echojs.com/news/43008), [Telegram Channel @front_end_dev](https://t.me/front_end_dev/9376?ysclid=mgk4a9hqf0853890652) +* :eyes: [Hacker News](https://news.ycombinator.com/item?id=41316782), [Node Weekly (#​582 - June 17, 2025)](https://nodeweekly.com/issues/582), [WebToolsWeekly](https://webtoolsweekly.com/archives/issue-585/), [GeekNews in GeekNews Weekly (2024-09-30 ~ 2024-10-06)](https://news.hada.io/weekly/202441), [Facebook Group "Javascript Programming"(javascript4u)](https://www.facebook.com/javascript4u/posts/build-a-windows-desktop-apps-with-javascript-html-and-cssmorioh-javascript-html-/1484014618472735/), [morioh.com](https://morioh.com/a/23c427a82bf1/build-a-windows-desktop-apps-with-javascript-html-and-css), CSDN, Fediverse, [daily.dev](https://app.daily.dev/posts/js-libraries-svg-tools-json-databases-8quregz3a), [PitchHut](https://www.pitchhut.com/project/proj_Ya136OLSW5at), [Disquiet](https://dis.qa/nv6T6), [Zhouexin (周e信)](https://www.zhouexin.com/issues/321), [Echo JS](https://www.echojs.com/news/43008), [Telegram Channel @front_end_dev](https://t.me/front_end_dev/9376?ysclid=mgk4a9hqf0853890652) ## Report abuse * [GitHub Security Advisories (gnh1201/welsonjs)](https://github.com/gnh1201/welsonjs/security) From 6e759008dda649c4e9c7ea64a75173bc916b845a Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 28 Oct 2025 13:30:48 +0900 Subject: [PATCH 05/17] Abstract connection management and add serial port support --- .../ConnectionManagerBase.cs | 147 ++++++++++++++ .../WelsonJS.Launcher/SerialPortManager.cs | 183 ++++++++++++++++++ .../WelsonJS.Launcher/WebSocketManager.cs | 135 ++++++------- .../WelsonJS.Launcher.csproj | 3 + 4 files changed, 392 insertions(+), 76 deletions(-) create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs new file mode 100644 index 0000000..68fb8c2 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs @@ -0,0 +1,147 @@ +// 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.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(); + + /// + /// 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) && IsConnectionValid(existing)) + { + return existing; + } + + RemoveInternal(key, existing); + + var connection = await OpenConnectionAsync(parameters, token).ConfigureAwait(false); + _pool[key] = connection; + return connection; + } + + /// + /// Removes the connection associated with the provided parameters. + /// + public void Remove(TParameters parameters) + { + string key = CreateKey(parameters); + if (_pool.TryRemove(key, out var connection)) + { + CloseSafely(connection); + } + } + + /// + /// 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; + + for (int attempt = 0; attempt < maxAttempts; attempt++) + { + token.ThrowIfCancellationRequested(); + var connection = await GetOrCreateAsync(parameters, token).ConfigureAwait(false); + + try + { + return await operation(connection, token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + lastError = ex; + Remove(parameters); + if (attempt == maxAttempts - 1) + { + throw; + } + } + } + + throw lastError ?? new InvalidOperationException("Unreachable state in ExecuteWithRetryAsync"); + } + + 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. + } + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs new file mode 100644 index 0000000..82d2fa3 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs @@ -0,0 +1,183 @@ +// 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.IO; +using System.IO.Ports; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace WelsonJS.Launcher +{ + public sealed class SerialPortManager : ConnectionManagerBase + { + 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) + { + 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; + } + + 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; } + } + + protected override string CreateKey(ConnectionParameters parameters) + { + return string.Join(",", new object[] + { + parameters.PortName.ToUpperInvariant(), + parameters.BaudRate, + parameters.Parity, + parameters.DataBits, + parameters.StopBits, + parameters.Handshake + }); + } + + 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, token), + 2, + cancellationToken).ConfigureAwait(false); + } + + private static async Task SendAndReceiveInternalAsync( + SerialPort port, + int bufferSize, + byte[] payload, + Encoding encoding, + CancellationToken token) + { + 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]; + + while (true) + { + try + { + int read = await Task.Run(() => port.Read(buffer, 0, buffer.Length), token).ConfigureAwait(false); + if (read > 0) + { + stream.Write(buffer, 0, read); + if (read < buffer.Length) + { + break; + } + } + else + { + break; + } + } + 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 4d953c3..f34ebcb 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs @@ -4,7 +4,6 @@ // https://github.com/gnh1201/welsonjs // using System; -using System.Collections.Concurrent; using System.Net.WebSockets; using System.Security.Cryptography; using System.Text; @@ -13,86 +12,73 @@ using System.Threading.Tasks; namespace WelsonJS.Launcher { - public class WebSocketManager + public sealed class WebSocketManager : ConnectionManagerBase { - private class Entry + public struct Endpoint { - public ClientWebSocket Socket; - public string Host; - public int Port; - public string Path; + 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; } } - private readonly ConcurrentDictionary _pool = new ConcurrentDictionary(); - - // Create a unique cache key using MD5 hash - private string MakeKey(string host, int port, string path) + protected override string CreateKey(Endpoint parameters) { - string raw = host + ":" + port + "/" + path; + string raw = parameters.Host + ":" + parameters.Port + "/" + parameters.Path; using (var md5 = MD5.Create()) { byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(raw)); - return BitConverter.ToString(hash).Replace("-", "").ToLower(); + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); } } - // Get an open WebSocket or connect a new one - public async Task GetOrCreateAsync(string host, int port, string path) + protected override async Task OpenConnectionAsync(Endpoint parameters, CancellationToken token) { - 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}"); + var socket = new ClientWebSocket(); + var uri = new Uri($"ws://{parameters.Host}:{parameters.Port}/{parameters.Path}"); try { - await newSock.ConnectAsync(uri, CancellationToken.None); - - _pool[key] = new Entry - { - Socket = newSock, - Host = host, - Port = port, - Path = path - }; - - return newSock; + await socket.ConnectAsync(uri, token).ConfigureAwait(false); + return socket; } catch (Exception ex) { - newSock.Dispose(); - Remove(host, port, path); + socket.Dispose(); throw new WebSocketException("WebSocket connection failed", ex); } } - // Remove a socket from the pool and dispose it + 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(); + } + } + public void Remove(string host, int port, string 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 */ } - } + Remove(new Endpoint(host, port, path)); } // Send and receive with automatic retry on first failure @@ -103,35 +89,32 @@ namespace WelsonJS.Launcher ? new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSec)) : new CancellationTokenSource(); - for (int attempt = 0; attempt < 2; attempt++) + try { - try - { - return await TrySendAndReceiveAsync(host, port, path, buf, cts.Token); - } - catch - { - Remove(host, port, path); - if (attempt == 1) throw; - } + return await ExecuteWithRetryAsync( + new Endpoint(host, port, path), + (socket, token) => TrySendAndReceiveAsync(socket, buf, token), + 2, + cts.Token).ConfigureAwait(false); + } + finally + { + cts.Dispose(); } - - 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(string host, int port, string path, byte[] buf, CancellationToken token) + private async Task TrySendAndReceiveAsync(ClientWebSocket socket, byte[] buf, CancellationToken token) { try { - var sock = await GetOrCreateAsync(host, port, path); - if (sock.State != WebSocketState.Open) + if (socket.State != WebSocketState.Open) throw new WebSocketException("WebSocket is not in an open state"); // Send request as a single text frame - await sock.SendAsync(new ArraySegment(buf), WebSocketMessageType.Text, true, token); + await socket.SendAsync(new ArraySegment(buf), WebSocketMessageType.Text, true, token).ConfigureAwait(false); // Fixed-size read buffer for I/O (does NOT cap total message size) byte[] readBuffer = new byte[8192]; @@ -142,12 +125,12 @@ namespace WelsonJS.Launcher while (true) { - var res = await sock.ReceiveAsync(new ArraySegment(readBuffer), token); + var res = await socket.ReceiveAsync(new ArraySegment(readBuffer), token).ConfigureAwait(false); if (res.MessageType == WebSocketMessageType.Close) { - try { await sock.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing as requested by server", token); } catch { } - throw new WebSocketException($"WebSocket closed by server: {sock.CloseStatus} {sock.CloseStatusDescription}"); + 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}"); } if (res.Count > 0) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index 5bc8b61..e1fc1f4 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -80,6 +80,7 @@ + @@ -87,6 +88,7 @@ + @@ -128,6 +130,7 @@ GlobalSettingsForm.cs + From b8362e570cc4d88187d9d2f865220d76b84b96ff Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 28 Oct 2025 13:42:35 +0900 Subject: [PATCH 06/17] Add connection monitor for managed transports --- .../ConnectionManagerBase.cs | 98 ++++++++++++- .../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 | 48 +++++- .../WelsonJS.Launcher/WebSocketManager.cs | 48 +++++- .../WelsonJS.Launcher.csproj | 12 ++ 12 files changed, 726 insertions(+), 9 deletions(-) create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.Designer.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.resx create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorRegistry.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/IManagedConnectionProvider.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ManagedConnectionStatus.cs diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs index 68fb8c2..9bf436f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs @@ -5,6 +5,7 @@ // using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -19,7 +20,8 @@ namespace WelsonJS.Launcher public abstract class ConnectionManagerBase where TConnection : class { - private readonly ConcurrentDictionary _pool = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _pool + = new ConcurrentDictionary(); /// /// Creates a unique cache key for the given connection parameters. @@ -54,15 +56,15 @@ namespace WelsonJS.Launcher { string key = CreateKey(parameters); - if (_pool.TryGetValue(key, out var existing) && IsConnectionValid(existing)) + if (_pool.TryGetValue(key, out var existing) && IsConnectionHealthy(existing.Connection)) { - return existing; + return existing.Connection; } - RemoveInternal(key, existing); + RemoveInternal(key, existing.Connection); var connection = await OpenConnectionAsync(parameters, token).ConfigureAwait(false); - _pool[key] = connection; + _pool[key] = (connection, parameters); return connection; } @@ -72,12 +74,55 @@ namespace WelsonJS.Launcher public void Remove(TParameters parameters) { string key = CreateKey(parameters); - if (_pool.TryRemove(key, out var connection)) + if (_pool.TryRemove(key, out var entry)) { - CloseSafely(connection); + CloseSafely(entry.Connection); } } + /// + /// Removes the connection associated with the provided cache key. + /// + protected bool TryRemoveByKey(string key) + { + if (string.IsNullOrEmpty(key)) + { + return false; + } + + if (_pool.TryRemove(key, out var entry)) + { + CloseSafely(entry.Connection); + return true; + } + + return false; + } + + /// + /// Provides a snapshot of the currently tracked connections. + /// + protected IReadOnlyList SnapshotConnections() + { + var entries = _pool.ToArray(); + var result = new ConnectionSnapshot[entries.Length]; + + for (int i = 0; i < entries.Length; i++) + { + var entry = entries[i]; + var connection = entry.Value.Connection; + bool isValid = IsConnectionHealthy(connection); + + result[i] = new ConnectionSnapshot( + entry.Key, + entry.Value.Parameters, + connection, + isValid); + } + + return result; + } + /// /// Executes an action against the managed connection, retrying once if the first attempt fails. /// @@ -119,6 +164,23 @@ namespace WelsonJS.Launcher throw lastError ?? new InvalidOperationException("Unreachable state in ExecuteWithRetryAsync"); } + private bool IsConnectionHealthy(TConnection connection) + { + if (connection == null) + { + return false; + } + + try + { + return IsConnectionValid(connection); + } + catch + { + return false; + } + } + private void RemoveInternal(string key, TConnection connection) { if (!string.IsNullOrEmpty(key)) @@ -143,5 +205,27 @@ namespace WelsonJS.Launcher // Ignore dispose exceptions. } } + + /// + /// Represents an immutable snapshot of a managed connection. + /// + protected readonly struct ConnectionSnapshot + { + public ConnectionSnapshot(string key, TParameters parameters, TConnection connection, bool isValid) + { + Key = key; + Parameters = parameters; + Connection = connection; + IsValid = isValid; + } + + public string Key { get; } + + public TParameters Parameters { get; } + + public TConnection Connection { get; } + + public bool IsValid { get; } + } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.Designer.cs new file mode 100644 index 0000000..a555de8 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.Designer.cs @@ -0,0 +1,132 @@ +namespace WelsonJS.Launcher +{ + partial class ConnectionMonitorForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + this.lvConnections = new System.Windows.Forms.ListView(); + this.chType = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chKey = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chState = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chDetails = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.chHealth = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.btnRefresh = new System.Windows.Forms.Button(); + this.btnCloseSelected = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // lvConnections + // + this.lvConnections.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.chType, + this.chKey, + this.chState, + this.chDetails, + this.chHealth}); + this.lvConnections.FullRowSelect = true; + this.lvConnections.HideSelection = false; + this.lvConnections.Location = new System.Drawing.Point(12, 12); + this.lvConnections.MultiSelect = true; + this.lvConnections.Name = "lvConnections"; + this.lvConnections.Size = new System.Drawing.Size(640, 260); + this.lvConnections.TabIndex = 0; + this.lvConnections.UseCompatibleStateImageBehavior = false; + this.lvConnections.View = System.Windows.Forms.View.Details; + this.lvConnections.SelectedIndexChanged += new System.EventHandler(this.lvConnections_SelectedIndexChanged); + // + // chType + // + this.chType.Text = "Type"; + this.chType.Width = 100; + // + // chKey + // + this.chKey.Text = "Key"; + this.chKey.Width = 140; + // + // chState + // + this.chState.Text = "State"; + this.chState.Width = 100; + // + // chDetails + // + this.chDetails.Text = "Details"; + this.chDetails.Width = 220; + // + // chHealth + // + this.chHealth.Text = "Health"; + this.chHealth.Width = 80; + // + // btnRefresh + // + this.btnRefresh.Location = new System.Drawing.Point(12, 280); + this.btnRefresh.Name = "btnRefresh"; + this.btnRefresh.Size = new System.Drawing.Size(95, 30); + this.btnRefresh.TabIndex = 1; + this.btnRefresh.Text = "Refresh"; + this.btnRefresh.UseVisualStyleBackColor = true; + this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click); + // + // btnCloseSelected + // + this.btnCloseSelected.Enabled = false; + this.btnCloseSelected.Location = new System.Drawing.Point(557, 280); + this.btnCloseSelected.Name = "btnCloseSelected"; + this.btnCloseSelected.Size = new System.Drawing.Size(95, 30); + this.btnCloseSelected.TabIndex = 2; + this.btnCloseSelected.Text = "Close"; + this.btnCloseSelected.UseVisualStyleBackColor = true; + this.btnCloseSelected.Click += new System.EventHandler(this.btnCloseSelected_Click); + // + // ConnectionMonitorForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(664, 322); + this.Controls.Add(this.btnCloseSelected); + this.Controls.Add(this.btnRefresh); + this.Controls.Add(this.lvConnections); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.Icon = global::WelsonJS.Launcher.Properties.Resources.favicon; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "ConnectionMonitorForm"; + this.Text = "Connection Monitor"; + this.Load += new System.EventHandler(this.ConnectionMonitorForm_Load); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.ListView lvConnections; + private System.Windows.Forms.ColumnHeader chType; + private System.Windows.Forms.ColumnHeader chKey; + private System.Windows.Forms.ColumnHeader chState; + private System.Windows.Forms.ColumnHeader chDetails; + private System.Windows.Forms.ColumnHeader chHealth; + private System.Windows.Forms.Button btnRefresh; + private System.Windows.Forms.Button btnCloseSelected; + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.cs new file mode 100644 index 0000000..c7a9bbf --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.cs @@ -0,0 +1,137 @@ +// ConnectionMonitorForm.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace WelsonJS.Launcher +{ + public partial class ConnectionMonitorForm : Form + { + public ConnectionMonitorForm() + { + InitializeComponent(); + } + + private void ConnectionMonitorForm_Load(object sender, EventArgs e) + { + RefreshConnections(); + } + + private void btnRefresh_Click(object sender, EventArgs e) + { + RefreshConnections(); + } + + private void lvConnections_SelectedIndexChanged(object sender, EventArgs e) + { + btnCloseSelected.Enabled = lvConnections.SelectedItems.Count > 0; + } + + private void btnCloseSelected_Click(object sender, EventArgs e) + { + if (lvConnections.SelectedItems.Count == 0) + { + return; + } + + var errors = new List(); + bool anyClosed = false; + + foreach (ListViewItem item in lvConnections.SelectedItems) + { + if (item.Tag is ConnectionItemTag tag) + { + try + { + if (tag.Provider.TryClose(tag.Key)) + { + anyClosed = true; + } + else + { + errors.Add($"Unable to close {tag.Provider.ConnectionType} connection {tag.Key}."); + } + } + catch (Exception ex) + { + errors.Add($"{tag.Provider.ConnectionType} {tag.Key}: {ex.Message}"); + } + } + } + + if (anyClosed) + { + RefreshConnections(); + } + + if (errors.Count > 0) + { + MessageBox.Show(string.Join(Environment.NewLine, errors), "Connection Monitor", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } + + private void RefreshConnections() + { + IReadOnlyList providers = ConnectionMonitorRegistry.GetProviders(); + + lvConnections.BeginUpdate(); + lvConnections.Items.Clear(); + + foreach (var provider in providers) + { + IReadOnlyCollection statuses; + try + { + statuses = provider.GetStatuses(); + } + catch (Exception ex) + { + var errorItem = new ListViewItem(provider.ConnectionType) + { + Tag = null + }; + errorItem.SubItems.Add(string.Empty); + errorItem.SubItems.Add("Error"); + errorItem.SubItems.Add(ex.Message); + errorItem.SubItems.Add("Unknown"); + lvConnections.Items.Add(errorItem); + continue; + } + + foreach (var status in statuses) + { + var item = new ListViewItem(status.ConnectionType) + { + Tag = new ConnectionItemTag(provider, status.Key) + }; + item.SubItems.Add(status.Key); + item.SubItems.Add(status.State); + item.SubItems.Add(status.Description); + item.SubItems.Add(status.IsValid ? "Healthy" : "Stale"); + lvConnections.Items.Add(item); + } + } + + lvConnections.EndUpdate(); + + btnCloseSelected.Enabled = lvConnections.SelectedItems.Count > 0; + } + + private sealed class ConnectionItemTag + { + public ConnectionItemTag(IManagedConnectionProvider provider, string key) + { + Provider = provider; + Key = key ?? string.Empty; + } + + public IManagedConnectionProvider Provider { get; } + + public string Key { get; } + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.resx new file mode 100644 index 0000000..bdd5b01 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorRegistry.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorRegistry.cs new file mode 100644 index 0000000..fa639c4 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionMonitorRegistry.cs @@ -0,0 +1,56 @@ +// ConnectionMonitorRegistry.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections.Generic; + +namespace WelsonJS.Launcher +{ + /// + /// Keeps track of connection providers that should appear in the monitor UI. + /// + public static class ConnectionMonitorRegistry + { + private static readonly object _syncRoot = new object(); + private static readonly List _providers = new List(); + + public static void RegisterProvider(IManagedConnectionProvider provider) + { + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + lock (_syncRoot) + { + if (!_providers.Contains(provider)) + { + _providers.Add(provider); + } + } + } + + public static void UnregisterProvider(IManagedConnectionProvider provider) + { + if (provider == null) + { + return; + } + + lock (_syncRoot) + { + _providers.Remove(provider); + } + } + + public static IReadOnlyList GetProviders() + { + lock (_syncRoot) + { + return _providers.ToArray(); + } + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/IManagedConnectionProvider.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/IManagedConnectionProvider.cs new file mode 100644 index 0000000..44e055a --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/IManagedConnectionProvider.cs @@ -0,0 +1,30 @@ +// IManagedConnectionProvider.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System.Collections.Generic; + +namespace WelsonJS.Launcher +{ + /// + /// Exposes connection status information for use by the connection monitor UI. + /// + public interface IManagedConnectionProvider + { + /// + /// Gets a human-friendly name for the connection type managed by this provider. + /// + string ConnectionType { get; } + + /// + /// Retrieves the current connections handled by the provider. + /// + IReadOnlyCollection GetStatuses(); + + /// + /// Attempts to close the connection associated with the supplied cache key. + /// + bool TryClose(string key); + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs index 76ba271..e5faf00 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.Designer.cs @@ -39,6 +39,7 @@ this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.userdefinedVariablesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.instancesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.connectionMonitorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.runAsAdministratorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.globalSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.startTheEditorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -132,6 +133,7 @@ this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.userdefinedVariablesToolStripMenuItem, this.instancesToolStripMenuItem, + this.connectionMonitorToolStripMenuItem, this.runAsAdministratorToolStripMenuItem, this.globalSettingsToolStripMenuItem, this.startTheEditorToolStripMenuItem, @@ -153,6 +155,13 @@ this.instancesToolStripMenuItem.Size = new System.Drawing.Size(196, 22); this.instancesToolStripMenuItem.Text = "Instances"; this.instancesToolStripMenuItem.Click += new System.EventHandler(this.instancesToolStripMenuItem_Click); + // + // connectionMonitorToolStripMenuItem + // + this.connectionMonitorToolStripMenuItem.Name = "connectionMonitorToolStripMenuItem"; + this.connectionMonitorToolStripMenuItem.Size = new System.Drawing.Size(196, 22); + this.connectionMonitorToolStripMenuItem.Text = "Connections..."; + this.connectionMonitorToolStripMenuItem.Click += new System.EventHandler(this.connectionMonitorToolStripMenuItem_Click); // // runAsAdministratorToolStripMenuItem // @@ -282,6 +291,7 @@ private System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem userdefinedVariablesToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem instancesToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem connectionMonitorToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem runAsAdministratorToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem globalSettingsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem startTheEditorToolStripMenuItem; diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs index f55da4b..446650e 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/MainForm.cs @@ -23,6 +23,7 @@ namespace WelsonJS.Launcher private string _workingDirectory; private string _instanceId; private string _scriptName; + private ConnectionMonitorForm _connectionMonitorForm; public MainForm(ICompatibleLogger logger = null) { @@ -276,6 +277,17 @@ namespace WelsonJS.Launcher (new InstancesForm()).Show(); } + private void connectionMonitorToolStripMenuItem_Click(object sender, EventArgs e) + { + if (_connectionMonitorForm == null || _connectionMonitorForm.IsDisposed) + { + _connectionMonitorForm = new ConnectionMonitorForm(); + } + + _connectionMonitorForm.Show(); + _connectionMonitorForm.Focus(); + } + private void runAsAdministratorToolStripMenuItem_Click(object sender, EventArgs e) { if (!IsInAdministrator()) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ManagedConnectionStatus.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ManagedConnectionStatus.cs new file mode 100644 index 0000000..263d5c2 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ManagedConnectionStatus.cs @@ -0,0 +1,32 @@ +// ManagedConnectionStatus.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +namespace WelsonJS.Launcher +{ + /// + /// Represents the state of a managed connection for UI presentation. + /// + public sealed class ManagedConnectionStatus + { + public ManagedConnectionStatus(string connectionType, string key, string state, string description, bool isValid) + { + ConnectionType = connectionType ?? string.Empty; + Key = key ?? string.Empty; + State = state ?? string.Empty; + Description = description ?? string.Empty; + IsValid = isValid; + } + + public string ConnectionType { get; } + + public string Key { get; } + + public string State { get; } + + public string Description { get; } + + public bool IsValid { get; } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs index 82d2fa3..144b5a8 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs @@ -4,6 +4,7 @@ // https://github.com/gnh1201/welsonjs // using System; +using System.Collections.Generic; using System.IO; using System.IO.Ports; using System.Text; @@ -12,8 +13,10 @@ using System.Threading.Tasks; namespace WelsonJS.Launcher { - public sealed class SerialPortManager : ConnectionManagerBase + public sealed class SerialPortManager : ConnectionManagerBase, IManagedConnectionProvider { + private const string ConnectionTypeName = "Serial Port"; + public struct ConnectionParameters { public ConnectionParameters( @@ -51,6 +54,13 @@ namespace WelsonJS.Launcher public int ReadBufferSize { get; } } + public SerialPortManager() + { + ConnectionMonitorRegistry.RegisterProvider(this); + } + + public string ConnectionType => ConnectionTypeName; + protected override string CreateKey(ConnectionParameters parameters) { return string.Join(",", new object[] @@ -133,6 +143,42 @@ namespace WelsonJS.Launcher cancellationToken).ConfigureAwait(false); } + public IReadOnlyCollection GetStatuses() + { + var snapshots = SnapshotConnections(); + var result = new List(snapshots.Count); + + foreach (var snapshot in snapshots) + { + string state; + try + { + state = snapshot.Connection?.IsOpen == true ? "Open" : "Closed"; + } + catch + { + state = "Unknown"; + } + + var parameters = snapshot.Parameters; + string description = $"{parameters.PortName} @ {parameters.BaudRate} bps"; + + result.Add(new ManagedConnectionStatus( + ConnectionTypeName, + snapshot.Key, + state, + description, + snapshot.IsValid)); + } + + return result; + } + + public bool TryClose(string key) + { + return TryRemoveByKey(key); + } + private static async Task SendAndReceiveInternalAsync( SerialPort port, int bufferSize, diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs index f34ebcb..433e13b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs @@ -4,6 +4,7 @@ // https://github.com/gnh1201/welsonjs // using System; +using System.Collections.Generic; using System.Net.WebSockets; using System.Security.Cryptography; using System.Text; @@ -12,8 +13,10 @@ using System.Threading.Tasks; namespace WelsonJS.Launcher { - public sealed class WebSocketManager : ConnectionManagerBase + public sealed class WebSocketManager : ConnectionManagerBase, IManagedConnectionProvider { + private const string ConnectionTypeName = "WebSocket"; + public struct Endpoint { public Endpoint(string host, int port, string path) @@ -28,6 +31,13 @@ namespace WelsonJS.Launcher public string Path { get; } } + public WebSocketManager() + { + ConnectionMonitorRegistry.RegisterProvider(this); + } + + public string ConnectionType => ConnectionTypeName; + protected override string CreateKey(Endpoint parameters) { string raw = parameters.Host + ":" + parameters.Port + "/" + parameters.Path; @@ -103,6 +113,42 @@ namespace WelsonJS.Launcher } } + public IReadOnlyCollection GetStatuses() + { + var snapshots = SnapshotConnections(); + var result = new List(snapshots.Count); + + foreach (var snapshot in snapshots) + { + string state; + try + { + state = snapshot.Connection?.State.ToString() ?? "Unknown"; + } + catch + { + state = "Unknown"; + } + + var endpoint = snapshot.Parameters; + var description = $"ws://{endpoint.Host}:{endpoint.Port}/{endpoint.Path}"; + + result.Add(new ManagedConnectionStatus( + ConnectionTypeName, + snapshot.Key, + state, + description, + snapshot.IsValid)); + } + + return result; + } + + public bool TryClose(string key) + { + return TryRemoveByKey(key); + } + // Actual send and receive implementation that never truncates the accumulated data. // - Uses a fixed-size read buffer ONLY for I/O // - Accumulates dynamically into a List until EndOfMessage diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index e1fc1f4..b171397 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -89,11 +89,20 @@ + + Form + + + ConnectionMonitorForm.cs + + + + @@ -136,6 +145,9 @@ EnvForm.cs + + ConnectionMonitorForm.cs + InstancesForm.cs From c7890aaadb13fb63274dbc630172e92bfbee2cd6 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 28 Oct 2025 14:12:25 +0900 Subject: [PATCH 07/17] Serialize connection creation per key --- .../ConnectionManagerBase.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs index 9bf436f..d2129f5 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs @@ -22,6 +22,8 @@ namespace WelsonJS.Launcher { private readonly ConcurrentDictionary _pool = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _openLocks + = new ConcurrentDictionary(); /// /// Creates a unique cache key for the given connection parameters. @@ -61,11 +63,28 @@ namespace WelsonJS.Launcher return existing.Connection; } - RemoveInternal(key, 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; + } - var connection = await OpenConnectionAsync(parameters, token).ConfigureAwait(false); - _pool[key] = (connection, parameters); - return 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(); + } } /// From 7795946f9fa3b8d02df4290e23de67e26bd59989 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 28 Oct 2025 14:12:31 +0900 Subject: [PATCH 08/17] Serialize per-key operations --- .../WelsonJS.Launcher/ConnectionManagerBase.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs index d2129f5..5723bd2 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ConnectionManagerBase.cs @@ -24,6 +24,8 @@ namespace WelsonJS.Launcher = new ConcurrentDictionary(); private readonly ConcurrentDictionary _openLocks = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _opLocks + = new ConcurrentDictionary(); /// /// Creates a unique cache key for the given connection parameters. @@ -155,14 +157,16 @@ namespace WelsonJS.Launcher 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++) { - token.ThrowIfCancellationRequested(); - var connection = await GetOrCreateAsync(parameters, token).ConfigureAwait(false); - + 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) @@ -178,6 +182,10 @@ namespace WelsonJS.Launcher throw; } } + finally + { + opLock.Release(); + } } throw lastError ?? new InvalidOperationException("Unreachable state in ExecuteWithRetryAsync"); From 3ce8d126288b2b6c143eb78bb0cb07ff52d48894 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 28 Oct 2025 14:12:37 +0900 Subject: [PATCH 09/17] Include timeouts in serial connection key --- WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs index 144b5a8..fa2db4f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs @@ -70,7 +70,9 @@ namespace WelsonJS.Launcher parameters.Parity, parameters.DataBits, parameters.StopBits, - parameters.Handshake + parameters.Handshake, + parameters.ReadTimeout, + parameters.WriteTimeout }); } From 9f4219e971462a393c34b3ad2c16200bd9c7b259 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 28 Oct 2025 14:12:44 +0900 Subject: [PATCH 10/17] Make serial buffer resets optional and improve receive loop --- .../WelsonJS.Launcher/SerialPortManager.cs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs index fa2db4f..21d94e8 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs @@ -28,7 +28,8 @@ namespace WelsonJS.Launcher Handshake handshake = Handshake.None, int readTimeout = 500, int writeTimeout = 500, - int readBufferSize = 1024) + int readBufferSize = 1024, + bool resetBuffersBeforeRequest = false) { if (string.IsNullOrWhiteSpace(portName)) throw new ArgumentNullException(nameof(portName)); @@ -41,6 +42,7 @@ namespace WelsonJS.Launcher ReadTimeout = readTimeout; WriteTimeout = writeTimeout; ReadBufferSize = readBufferSize > 0 ? readBufferSize : 1024; + ResetBuffersBeforeRequest = resetBuffersBeforeRequest; } public string PortName { get; } @@ -52,6 +54,7 @@ namespace WelsonJS.Launcher public int ReadTimeout { get; } public int WriteTimeout { get; } public int ReadBufferSize { get; } + public bool ResetBuffersBeforeRequest { get; } } public SerialPortManager() @@ -140,7 +143,13 @@ namespace WelsonJS.Launcher return await ExecuteWithRetryAsync( parameters, - (port, token) => SendAndReceiveInternalAsync(port, parameters.ReadBufferSize, payload, encoding, token), + (port, token) => SendAndReceiveInternalAsync( + port, + parameters.ReadBufferSize, + payload, + encoding, + parameters.ResetBuffersBeforeRequest, + token), 2, cancellationToken).ConfigureAwait(false); } @@ -186,10 +195,14 @@ namespace WelsonJS.Launcher int bufferSize, byte[] payload, Encoding encoding, + bool resetBuffers, CancellationToken token) { - port.DiscardInBuffer(); - port.DiscardOutBuffer(); + if (resetBuffers) + { + port.DiscardInBuffer(); + port.DiscardOutBuffer(); + } if (payload.Length > 0) { @@ -208,14 +221,17 @@ namespace WelsonJS.Launcher if (read > 0) { stream.Write(buffer, 0, read); - if (read < buffer.Length) + if (port.BytesToRead == 0) { break; } } else { - break; + if (port.BytesToRead == 0) + { + break; + } } } catch (TimeoutException) From 4489c3150919db2bc937f6e730c5e7a3c6427262 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 28 Oct 2025 16:36:58 +0900 Subject: [PATCH 11/17] Guard serial reads with timeout-based termination and cap --- .../WelsonJS.Launcher/SerialPortManager.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs index 21d94e8..feadbcd 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/SerialPortManager.cs @@ -212,7 +212,7 @@ namespace WelsonJS.Launcher using (var stream = new MemoryStream()) { var buffer = new byte[bufferSize]; - + const int MaxResponseBytes = 1 * 1024 * 1024; // 1 MiB safety cap while (true) { try @@ -220,18 +220,10 @@ namespace WelsonJS.Launcher 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); - if (port.BytesToRead == 0) - { - break; - } - } - else - { - if (port.BytesToRead == 0) - { - break; - } + continue; // keep reading until idle timeout } } catch (TimeoutException) From e9cc898608f596d70c1973ff8e51d3d4e33ba3d7 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 2 Nov 2025 17:59:10 +0900 Subject: [PATCH 12/17] 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 From bf3e6ebcf25701193d5ea6cd8cedf0dcd6977cc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:58:07 +0000 Subject: [PATCH 13/17] Bump js-yaml from 4.1.0 to 4.1.1 Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a7234b..54ec10c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "welsonjs", - "version": "0.2.7.12", + "version": "0.2.7.50", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "welsonjs", - "version": "0.2.7.12", + "version": "0.2.7.50", "license": "(GPL-3.0 or MS-RL)", "dependencies": { "core-js": "^3.21.1", @@ -16,7 +16,7 @@ "jquery-form": "^4.3.0", "jquery-toast-plugin": "^1.3.2", "jquery-ui": "^1.13.2", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "jsrender": "^1.0.11", "modernizr": "^3.12.0", "squel": "^5.13.0" @@ -209,9 +209,9 @@ } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dependencies": { "argparse": "^2.0.1" }, @@ -673,9 +673,9 @@ } }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "requires": { "argparse": "^2.0.1" } diff --git a/package.json b/package.json index 365727a..2e7aff4 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "jquery-form": "^4.3.0", "jquery-toast-plugin": "^1.3.2", "jquery-ui": "^1.13.2", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "jsrender": "^1.0.11", "modernizr": "^3.12.0", "squel": "^5.13.0" From 7a49694ff703a3ebbb48a8be98c5b2804e33fcd4 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Wed, 19 Nov 2025 14:33:10 +0900 Subject: [PATCH 14/17] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b099180..234a887 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ [![Discord chat](https://img.shields.io/discord/359930650330923008?logo=discord)](https://discord.gg/XKG5CjtXEj) [![Trustpilot](https://img.shields.io/badge/Trustpilot-00B67A?logo=trustpilot&logoColor=fff)](https://www.trustpilot.com/review/catswords.com) [![Open to work](https://img.shields.io/badge/%23-OPENTOWORK-green)](https://github.com/gnh1201/welsonjs/discussions/167) +[![DeepWiki](https://img.shields.io/badge/DeepWiki-gnh1201%2Fwelsonjs-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/gnh1201/welsonjs) WelsonJS - Build a Windows app on the Windows built-in JavaScript engine. From 54293b89b6bb551331ed0d283721baeb730a4e6c Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Wed, 19 Nov 2025 14:34:31 +0900 Subject: [PATCH 15/17] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 234a887..630c1f5 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,7 @@ [![AppVeyor Status](https://ci.appveyor.com/api/projects/status/github/gnh1201/welsonjs?svg=true)](https://ci.appveyor.com/project/gnh1201/welsonjs) [![DOI 10.5281/zenodo.11382384](https://zenodo.org/badge/DOI/10.5281/zenodo.11382384.svg)](https://doi.org/10.5281/zenodo.11382384) [![ChatGPT available](https://img.shields.io/badge/ChatGPT-74aa9c?logo=openai&logoColor=white)](#) -[![Anthropic available](https://img.shields.io/badge/Anthropic-000000?logo=Anthropic&logoColor=white)](#) -[![Grok available](https://img.shields.io/badge/Grok-000000?logo=x&logoColor=white)](#) -[![Google Gemini available](https://img.shields.io/badge/Google%20Gemini-886FBF?logo=googlegemini&logoColor=fff)](#) +[![Google Gemini available](https://img.shields.io/badge/Gemini-886FBF?logo=googlegemini&logoColor=fff)](#) [![slideshare.net presentation](https://img.shields.io/badge/SlideShare-black?logo=slideshare)](https://www.slideshare.net/slideshow/welsonjs-javascript-framework-presentation-2024/276005486) [![YouTube promotion video](https://img.shields.io/badge/YouTube-red?logo=youtube)](https://youtu.be/JavH7Dms8-U) [![Discord chat](https://img.shields.io/discord/359930650330923008?logo=discord)](https://discord.gg/XKG5CjtXEj) From 5c7a83b1b3c97235ddb2add1ef3fcb7b0be8a560 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 25 Nov 2025 14:06:21 +0900 Subject: [PATCH 16/17] Update README.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0fca51..772d51c 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ WelsonJS is tailored for developers who need a reliable, lightweight JavaScript * AI integrations: LLM based (generative) AI services. (e.g., ChatGPT, Google Gemini) * Aviation Data integrations: [AviationStack](https://aviationstack.com?utm_source=FirstPromoter&utm_medium=Affiliate&fpr=namhyeon71), [SerpApi Google Flights API](https://serpapi.com/google-flights-api?utm_source=welsonjs) * OVFTool (VMware) integration: [OVFTool for Broadcom/VMware infrastructures](https://developer.broadcom.com/tools/open-virtualization-format-ovf-tool/latest) -* ***:fire: NEW!*** Windows bulit-in database engine AKA. [ESENT (ESE) database](https://learn.microsoft.com/en-us/windows/win32/extensible-storage-engine/database-overview) interface library (WelsonJS.Esent) +* ***:fire: NEW!*** Windows built-in database engine AKA. [ESENT (ESE) database](https://learn.microsoft.com/en-us/windows/win32/extensible-storage-engine/database-overview) interface library (WelsonJS.Esent) * ***:fire: NEW!*** WelsonJS JCTG(JavaScript-Click-To-Go): Run WelsonJS script files written in JavaScript directly from Windows File Explorer with a (double) click. Just like an `.exe` file. * Everything you can imagine. From 58ff280bdf40bff487675a2555b9fa56c1bef57d Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 25 Nov 2025 14:07:05 +0900 Subject: [PATCH 17/17] Update README.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 772d51c..115ebda 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ WelsonJS is tailored for developers who need a reliable, lightweight JavaScript * RPC(Remote Procedure Call) protocol clients: [gRPC](https://grpc.io/), [JSON-RPC 2.0](https://www.jsonrpc.org/specification) * Various types of HTTP clients: [XHR(MSXML)](https://developer.mozilla.org/docs/Glossary/XMLHttpRequest), [cURL](https://curl.se/), [BITS](https://en.m.wikipedia.org/w/index.php?title=Background_Intelligent_Transfer_Service), [CERT](https://github.com/MicrosoftDocs/windowsserverdocs/blob/main/WindowsServerDocs/administration/windows-commands/certutil.md), Web Proxy, SEO/SERP * The native toolkit for Windows environments: Write a Windows Service Application with JavaScript, Control a window handle, Cryptography (e.g., [ISO/IEC 18033-3:2010](https://www.iso.org/standard/54531.html) aka. [HIGHT](https://seed.kisa.or.kr/kisa/algorithm/EgovHightInfo.do)), [Named Shared Memory](https://learn.microsoft.com/en-us/windows/win32/memory/creating-named-shared-memory) based [IPC](https://qiita.com/gnh1201/items/4e70dccdb7adacf0ace5), [NuGet package](https://www.nuget.org/packages/WelsonJS.Toolkit) -* AI integrations: LLM based (generative) AI services. (e.g., ChatGPT, Google Gemini) +* AI integrations: LLM-based (generative) AI services (e.g., ChatGPT, Google Gemini). * Aviation Data integrations: [AviationStack](https://aviationstack.com?utm_source=FirstPromoter&utm_medium=Affiliate&fpr=namhyeon71), [SerpApi Google Flights API](https://serpapi.com/google-flights-api?utm_source=welsonjs) * OVFTool (VMware) integration: [OVFTool for Broadcom/VMware infrastructures](https://developer.broadcom.com/tools/open-virtualization-format-ovf-tool/latest) * ***:fire: NEW!*** Windows built-in database engine AKA. [ESENT (ESE) database](https://learn.microsoft.com/en-us/windows/win32/extensible-storage-engine/database-overview) interface library (WelsonJS.Esent)