From b1cabe9fbb1eb2495554f18e4c6b1d25129131e5 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 15 Mar 2025 00:07:28 +0900 Subject: [PATCH 1/4] Add some code for #183 --- .../WelsonJS.Launcher/ExecutablesCollector.cs | 115 ++++++++++++++++++ .../WelsonJS.Launcher/ResourceServer.cs | 60 +++++++++ .../WelsonJS.Launcher.csproj | 2 + 3 files changed, 177 insertions(+) create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs new file mode 100644 index 0000000..4970fa6 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs @@ -0,0 +1,115 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace WelsonJS.Launcher +{ + public class ExecutablesCollector + { + private List executables = new List(); + + public ExecutablesCollector() + { + executables.AddRange(GetInstalledSoftwareExecutables()); + executables.AddRange(GetExecutablesFromPath()); + } + + public List GetExecutables() + { + return executables; + } + + private List GetInstalledSoftwareExecutables() + { + List executables = new List(); + string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; + + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryKey)) + { + if (key != null) + { + foreach (string subKeyName in key.GetSubKeyNames()) + { + using (RegistryKey subKey = key.OpenSubKey(subKeyName)) + { + string installLocation = subKey?.GetValue("InstallLocation") as string; + string uninstallString = subKey?.GetValue("UninstallString") as string; + + List executablePaths = FindExecutables(installLocation, uninstallString); + executables.AddRange(executablePaths); + } + } + } + } + + return executables; + } + + private List FindExecutables(string installLocation, string uninstallString) + { + List executables = new List(); + + if (!string.IsNullOrEmpty(installLocation) && Directory.Exists(installLocation)) + { + try + { + List executableFiles = Directory.GetFiles(installLocation, "*.exe", SearchOption.AllDirectories) + .OrderByDescending(f => new FileInfo(f).Length) + .ToList(); + executables.AddRange(executableFiles); + } + catch (Exception) { } + } + + if (!string.IsNullOrEmpty(uninstallString)) + { + if (TryParseExecutablePath(uninstallString, out string executablePath)) + { + executables.Add(executablePath); + } + } + + return executables; + } + + private static bool TryParseExecutablePath(string s, out string path) + { + Match match = Regex.Match(s, @"(?<=""|^)([a-zA-Z]:\\[^""\s]+\.exe)"); + + if (match.Success) + { + path = match.Value; + return true; + } + + path = null; + return false; + } + + private List GetExecutablesFromPath() + { + List executables = new List(); + string pathEnv = Environment.GetEnvironmentVariable("PATH"); + + if (!string.IsNullOrEmpty(pathEnv)) + { + foreach (string path in pathEnv.Split(';')) + { + if (Directory.Exists(path)) + { + try + { + executables.AddRange(Directory.GetFiles(path, "*.exe", SearchOption.TopDirectoryOnly)); + } + catch (Exception) { } + } + } + } + + return executables; + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 09ddf0e..55dc8f3 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -1,12 +1,15 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using System.Xml.Linq; namespace WelsonJS.Launcher { @@ -18,6 +21,7 @@ namespace WelsonJS.Launcher private bool _isRunning; private string _prefix; private string _resourceName; + private ExecutablesCollector _executablesCollector; public ResourceServer(string prefix, string resourceName) { @@ -25,6 +29,7 @@ namespace WelsonJS.Launcher _listener = new HttpListener(); _listener.Prefixes.Add(prefix); _resourceName = resourceName; + _executablesCollector = new ExecutablesCollector(); } public string GetPrefix() @@ -81,15 +86,62 @@ namespace WelsonJS.Launcher { string path = context.Request.Url.AbsolutePath.TrimStart('/'); + // Serve the favicon.ico file if ("favicon.ico".Equals(path, StringComparison.OrdinalIgnoreCase)) { ServeResource(context, GetResource("favicon"), "image/x-icon"); return; } + // Serve the code completion (word suggestion) + if (path.StartsWith("completion/", StringComparison.OrdinalIgnoreCase)) + { + ServeCompletion(context, path.Substring("completion/".Length)); + return; + } + + // Serve a resource ServeResource(context, GetResource(_resourceName), "text/html"); } + private void ServeCompletion(HttpListenerContext context, string word) + { + int statusCode = 200; + + List executables = _executablesCollector.GetExecutables(); + + CompletionItem[] completionItems = executables + .Where(exec => exec.IndexOf(word, 0, StringComparison.OrdinalIgnoreCase) > -1) + .Select(exec => new CompletionItem + { + label = Path.GetFileName(exec), + kind = "Text", + documentation = "An executable file", + insertText = exec + }) + .ToArray(); + + XElement response = new XElement("suggestions", + completionItems.Select(item => new XElement("item", + new XElement("label", item.label), + new XElement("kind", item.kind), + new XElement("documentation", item.documentation), + new XElement("insertText", item.insertText) + )) + ); + + byte[] data = Encoding.UTF8.GetBytes( + "\r\n" + + response.ToString() + ); + + context.Response.StatusCode = statusCode; + context.Response.ContentType = "application/xml"; + context.Response.ContentLength64 = data.Length; + context.Response.OutputStream.Write(data, 0, data.Length); + context.Response.OutputStream.Close(); + } + private void ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html") { int statusCode = 200; @@ -159,4 +211,12 @@ namespace WelsonJS.Launcher } } } + + public class CompletionItem + { + public string label { get; set; } + public string kind { get; set; } + public string documentation { get; set; } + public string insertText { get; set; } + } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index 756c646..85c3ec2 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -68,6 +68,7 @@ + @@ -76,6 +77,7 @@ EnvForm.cs + Form From 96bd29c06a58621351ba4dfa255c2865820f5145 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 15 Mar 2025 00:23:20 +0900 Subject: [PATCH 2/4] Adopt the comments of the AI reviewers --- .../WelsonJS.Launcher/ExecutablesCollector.cs | 13 +++++-- .../WelsonJS.Launcher/ResourceServer.cs | 38 +++++++++++-------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs index 4970fa6..b4e44ef 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs @@ -1,6 +1,7 @@ using Microsoft.Win32; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -61,7 +62,10 @@ namespace WelsonJS.Launcher .ToList(); executables.AddRange(executableFiles); } - catch (Exception) { } + catch (Exception ex) + { + Debug.WriteLine($"Error enumerating executables in '{installLocation}': {ex}"); + } } if (!string.IsNullOrEmpty(uninstallString)) @@ -77,7 +81,7 @@ namespace WelsonJS.Launcher private static bool TryParseExecutablePath(string s, out string path) { - Match match = Regex.Match(s, @"(?<=""|^)([a-zA-Z]:\\[^""\s]+\.exe)"); + Match match = Regex.Match(s, @"(?<=""|^)([a-zA-Z]:\\[^""]+\.exe)", RegexOptions.IgnoreCase); if (match.Success) { @@ -104,7 +108,10 @@ namespace WelsonJS.Launcher { executables.AddRange(Directory.GetFiles(path, "*.exe", SearchOption.TopDirectoryOnly)); } - catch (Exception) { } + catch (Exception ex) + { + Debug.WriteLine($"Error enumerating executables in '{path}': {ex}"); + } } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 55dc8f3..e70228a 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -114,19 +114,19 @@ namespace WelsonJS.Launcher .Where(exec => exec.IndexOf(word, 0, StringComparison.OrdinalIgnoreCase) > -1) .Select(exec => new CompletionItem { - label = Path.GetFileName(exec), - kind = "Text", - documentation = "An executable file", - insertText = exec + Label = Path.GetFileName(exec), + Kind = "Text", + Documentation = "An executable file", + InsertText = exec }) .ToArray(); XElement response = new XElement("suggestions", completionItems.Select(item => new XElement("item", - new XElement("label", item.label), - new XElement("kind", item.kind), - new XElement("documentation", item.documentation), - new XElement("insertText", item.insertText) + new XElement("label", item.Label), + new XElement("kind", item.Kind), + new XElement("documentation", item.Documentation), + new XElement("insertText", item.InsertText) )) ); @@ -138,8 +138,11 @@ namespace WelsonJS.Launcher context.Response.StatusCode = statusCode; context.Response.ContentType = "application/xml"; context.Response.ContentLength64 = data.Length; - context.Response.OutputStream.Write(data, 0, data.Length); - context.Response.OutputStream.Close(); + using (Stream outputStream = context.Response.OutputStream) + { + context.Response.OutputStream.Write(data, 0, data.Length); + context.Response.OutputStream.Close(); + } } private void ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html") @@ -157,8 +160,11 @@ namespace WelsonJS.Launcher context.Response.StatusCode = statusCode; context.Response.ContentType = mimeType; context.Response.ContentLength64 = data.Length; - context.Response.OutputStream.Write(data, 0, data.Length); - context.Response.OutputStream.Close(); + using (Stream outputStream = context.Response.OutputStream) + { + context.Response.OutputStream.Write(data, 0, data.Length); + context.Response.OutputStream.Close(); + } } private byte[] GetResource(string resourceName) @@ -214,9 +220,9 @@ namespace WelsonJS.Launcher public class CompletionItem { - public string label { get; set; } - public string kind { get; set; } - public string documentation { get; set; } - public string insertText { get; set; } + public string Label { get; set; } + public string Kind { get; set; } + public string Documentation { get; set; } + public string InsertText { get; set; } } } From 67227048593bacc27b3807d0380b7340af39d318 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 15 Mar 2025 00:37:03 +0900 Subject: [PATCH 3/4] Update ResourceServer.cs --- WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index e70228a..8a56427 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -140,8 +140,7 @@ namespace WelsonJS.Launcher context.Response.ContentLength64 = data.Length; using (Stream outputStream = context.Response.OutputStream) { - context.Response.OutputStream.Write(data, 0, data.Length); - context.Response.OutputStream.Close(); + outputStream.Write(data, 0, data.Length); } } @@ -162,8 +161,7 @@ namespace WelsonJS.Launcher context.Response.ContentLength64 = data.Length; using (Stream outputStream = context.Response.OutputStream) { - context.Response.OutputStream.Write(data, 0, data.Length); - context.Response.OutputStream.Close(); + outputStream.Write(data, 0, data.Length); } } From 1f2a1e79b79a77dabe7907ac559fcf77f6476fa4 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 15 Mar 2025 00:46:07 +0900 Subject: [PATCH 4/4] Update ResourceServer.cs --- .../WelsonJS.Launcher/ResourceServer.cs | 82 +++++++++++-------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 8a56427..ff5c4ca 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -108,39 +108,57 @@ namespace WelsonJS.Launcher { int statusCode = 200; - List executables = _executablesCollector.GetExecutables(); - - CompletionItem[] completionItems = executables - .Where(exec => exec.IndexOf(word, 0, StringComparison.OrdinalIgnoreCase) > -1) - .Select(exec => new CompletionItem - { - Label = Path.GetFileName(exec), - Kind = "Text", - Documentation = "An executable file", - InsertText = exec - }) - .ToArray(); - - XElement response = new XElement("suggestions", - completionItems.Select(item => new XElement("item", - new XElement("label", item.Label), - new XElement("kind", item.Kind), - new XElement("documentation", item.Documentation), - new XElement("insertText", item.InsertText) - )) - ); - - byte[] data = Encoding.UTF8.GetBytes( - "\r\n" + - response.ToString() - ); - - context.Response.StatusCode = statusCode; - context.Response.ContentType = "application/xml"; - context.Response.ContentLength64 = data.Length; - using (Stream outputStream = context.Response.OutputStream) + try { - outputStream.Write(data, 0, data.Length); + List executables = _executablesCollector.GetExecutables(); + + CompletionItem[] completionItems = executables + .Where(exec => exec.IndexOf(word, 0, StringComparison.OrdinalIgnoreCase) > -1) + .Take(100) // Limit results to prevent excessive response sizes + .Select(exec => new CompletionItem + { + Label = Path.GetFileName(exec), + Kind = "Text", + Documentation = "An executable file", + InsertText = exec + }) + .ToArray(); + + XElement response = new XElement("suggestions", + completionItems.Select(item => new XElement("item", + new XElement("label", item.Label), + new XElement("kind", item.Kind), + new XElement("documentation", item.Documentation), + new XElement("insertText", item.InsertText) + )) + ); + + byte[] data = Encoding.UTF8.GetBytes( + "\r\n" + + response.ToString() + ); + + context.Response.StatusCode = statusCode; + context.Response.ContentType = "application/xml"; + context.Response.ContentLength64 = data.Length; + using (Stream outputStream = context.Response.OutputStream) + { + outputStream.Write(data, 0, data.Length); + } + } + catch (Exception ex) + { + byte[] errorData = Encoding.UTF8.GetBytes( + "\r\n" + + $"Failed to process completion request. {ex.Message}" + ); + context.Response.StatusCode = 500; + context.Response.ContentType = "application/xml"; + context.Response.ContentLength64 = errorData.Length; + using (Stream outputStream = context.Response.OutputStream) + { + outputStream.Write(errorData, 0, errorData.Length); + } } }