diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs new file mode 100644 index 0000000..b4e44ef --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ExecutablesCollector.cs @@ -0,0 +1,122 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Diagnostics; +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 ex) + { + Debug.WriteLine($"Error enumerating executables in '{installLocation}': {ex}"); + } + } + + 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]:\\[^""]+\.exe)", RegexOptions.IgnoreCase); + + 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 ex) + { + Debug.WriteLine($"Error enumerating executables in '{path}': {ex}"); + } + } + } + } + + return executables; + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 09ddf0e..ff5c4ca 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,82 @@ 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; + + try + { + 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); + } + } + } + private void ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html") { int statusCode = 200; @@ -105,8 +177,10 @@ 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) + { + outputStream.Write(data, 0, data.Length); + } } private byte[] GetResource(string resourceName) @@ -159,4 +233,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