From 5747713f9953b8828fb9ce3eec75a4d841c0f594 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 7 Dec 2025 00:23:51 +0900 Subject: [PATCH 1/4] ntroduce separate HttpClient instances for raw and compressed HTTP transfer modes Added two HttpClient instances to distinguish between legacy (no Accept-Encoding) and modern compressed HTTP transfer behaviors. - LegacyHttp: Sends no Accept-Encoding header. Used when requesting .dll.gz files, ensuring that the server delivers the file exactly as-is without applying HTTP-level compression. - Http: Enables AutomaticDecompression and advertises Accept-Encoding (gzip, deflate). When the server supports HTTP content compression, even a regular .dll file can be transmitted in compressed form and transparently decompressed by the client. This separation prevents ambiguities between: - File-level compression (.dll.gz) - Transport-level compression (Content-Encoding: gzip/deflate) and ensures predictable behavior when downloading assemblies depending on server capabilities. --- .../WelsonJS.Launcher/AssemblyLoader.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs index 8d0873c..1a52248 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs @@ -1,6 +1,6 @@ // AssemblyLoader.cs // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// SPDX-FileCopyrightText: Namhyeon Go , 2025 Catswords OSS and WelsonJS Contributors // https://github.com/gnh1201/welsonjs // using System; @@ -36,10 +36,23 @@ namespace WelsonJS.Launcher private static bool _registered; private static readonly string LoaderNamespace = typeof(AssemblyLoader).Namespace ?? "WelsonJS.Launcher"; - private static readonly HttpClient Http = new HttpClient + private static readonly HttpClientHandler LegacyHttpHandler = new HttpClientHandler(); + private static readonly HttpClientHandler HttpHandler = new HttpClientHandler { - Timeout = TimeSpan.FromSeconds(300) // 5 minutes + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; + private static readonly HttpClient LegacyHttp = CreateClient(LegacyHttpHandler); // No the Accept-Encoding (e.g., gzip, deflate) header + private static readonly HttpClient Http = CreateClient(HttpHandler); // With the Accept-Encoding (e.g., gzip, deflate) header + + private static HttpClient CreateClient(HttpMessageHandler handler) + { + var client = new HttpClient(handler, disposeHandler: false) + { + Timeout = TimeSpan.FromSeconds(300) // 5 minutes + }; + + return client; + } // -------------------- kernel32 native loading -------------------- @@ -327,9 +340,9 @@ namespace WelsonJS.Launcher bool isDll = url.EndsWith(".dll", StringComparison.OrdinalIgnoreCase); // *.dll.gz bool downloaded = false; - if (isDll && TryDownloadGzipToFile(gzUrl, dest)) + if (isDll && TryDownloadCompressedFile(gzUrl, dest)) { - Logger.Info("Downloaded and decompressed gzip file to: {0}", dest); + Logger.Info("Downloaded and decompressed file to: {0}", dest); downloaded = true; } @@ -365,13 +378,13 @@ namespace WelsonJS.Launcher } - private static bool TryDownloadGzipToFile(string gzUrl, string dest) + private static bool TryDownloadCompressedFile(string gzUrl, string dest) { string tempFile = dest + ".tmp"; try { - using (var res = Http.GetAsync(gzUrl).GetAwaiter().GetResult()) + using (var res = LegacyHttp.GetAsync(gzUrl).GetAwaiter().GetResult()) { if (res.StatusCode == HttpStatusCode.NotFound) return false; From e90808e517dd8878a781563c43c99fc4c2f4c7a3 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 7 Dec 2025 02:22:44 +0900 Subject: [PATCH 2/4] Null-check logger before use Null-check logger before use --- .../WelsonJS.Launcher/AssemblyLoader.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs index 1a52248..83d141f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs @@ -165,20 +165,20 @@ namespace WelsonJS.Launcher if (string.IsNullOrWhiteSpace(BaseUrl)) { - Logger.Error("AssemblyLoader.Register() called but BaseUrl is not set."); + Logger?.Error("AssemblyLoader.Register() called but BaseUrl is not set."); throw new InvalidOperationException("AssemblyLoader.BaseUrl must be configured before Register()."); } if (!BaseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { - Logger.Error("AssemblyLoader.BaseUrl must use HTTPS for security."); + Logger?.Error("AssemblyLoader.BaseUrl must use HTTPS for security."); throw new InvalidOperationException("AssemblyLoader.BaseUrl must use HTTPS."); } AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; _registered = true; - Logger.Info("AssemblyLoader: AssemblyResolve handler registered."); + Logger?.Info("AssemblyLoader: AssemblyResolve handler registered."); } } @@ -206,11 +206,11 @@ namespace WelsonJS.Launcher try { if (!SetDllDirectory(cacheDir)) - Logger.Warn("SetDllDirectory failed for: {0}", cacheDir); + Logger?.Warn("SetDllDirectory failed for: {0}", cacheDir); } catch (Exception ex) { - Logger.Warn("SetDllDirectory threw exception: {0}", ex.Message); + Logger?.Warn("SetDllDirectory threw exception: {0}", ex.Message); } foreach (string raw in fileNames) @@ -225,11 +225,11 @@ namespace WelsonJS.Launcher { string url = $"{BaseUrl.TrimEnd('/')}/native/{ownerAssemblyName}/{versionString}/{fileName}"; DownloadFile(url, localPath); - Logger.Info("Downloaded native module: {0}", fileName); + Logger?.Info("Downloaded native module: {0}", fileName); } else { - Logger.Info("Using cached native module: {0}", localPath); + Logger?.Info("Using cached native module: {0}", localPath); } EnsureSignedFileOrThrow(localPath, fileName); @@ -238,12 +238,12 @@ namespace WelsonJS.Launcher if (h == IntPtr.Zero) { int errorCode = Marshal.GetLastWin32Error(); - Logger.Error("LoadLibrary failed for {0} with error code {1}", localPath, errorCode); + Logger?.Error("LoadLibrary failed for {0} with error code {1}", localPath, errorCode); throw new InvalidOperationException($"Failed to load native module: {fileName} (error: {errorCode})"); } else { - Logger.Info("Loaded native module: {0}", fileName); + Logger?.Info("Loaded native module: {0}", fileName); } } } @@ -274,7 +274,7 @@ namespace WelsonJS.Launcher private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args) { - Logger.Info("AssemblyResolve: {0}", args.Name); + Logger?.Info("AssemblyResolve: {0}", args.Name); AssemblyName req = new AssemblyName(args.Name); string simpleName = req.Name; @@ -287,7 +287,7 @@ namespace WelsonJS.Launcher var entryName = entry.GetName().Name; if (string.Equals(simpleName, entryName, StringComparison.OrdinalIgnoreCase)) { - Logger.Info("AssemblyResolve: skipping entry assembly {0}", simpleName); + Logger?.Info("AssemblyResolve: skipping entry assembly {0}", simpleName); return null; } } @@ -307,16 +307,16 @@ namespace WelsonJS.Launcher { string url = $"{BaseUrl.TrimEnd('/')}/managed/{simpleName}/{versionStr}/{simpleName}.dll"; DownloadFile(url, dllPath); - Logger.Info("Downloaded managed assembly: {0}", simpleName); + Logger?.Info("Downloaded managed assembly: {0}", simpleName); } else { - Logger.Info("Using cached managed assembly: {0}", dllPath); + Logger?.Info("Using cached managed assembly: {0}", dllPath); } if (!File.Exists(dllPath)) { - Logger.Warn("AssemblyResolve: managed assembly not found after download attempt: {0}", simpleName); + Logger?.Warn("AssemblyResolve: managed assembly not found after download attempt: {0}", simpleName); return null; } @@ -342,13 +342,13 @@ namespace WelsonJS.Launcher if (isDll && TryDownloadCompressedFile(gzUrl, dest)) { - Logger.Info("Downloaded and decompressed file to: {0}", dest); + Logger?.Info("Downloaded and decompressed file to: {0}", dest); downloaded = true; } if (!downloaded) { - Logger.Info("Downloading file from: {0}", url); + Logger?.Info("Downloading file from: {0}", url); res = Http.GetAsync(url).GetAwaiter().GetResult(); res.EnsureSuccessStatusCode(); @@ -358,7 +358,7 @@ namespace WelsonJS.Launcher s.CopyTo(fs); } - Logger.Info("Downloaded file to: {0}", dest); + Logger?.Info("Downloaded file to: {0}", dest); } if (!File.Exists(dest)) @@ -368,7 +368,7 @@ namespace WelsonJS.Launcher } catch (Exception ex) { - Logger.Error("Error downloading {0}: {1}", url, ex.Message); + Logger?.Error("Error downloading {0}: {1}", url, ex.Message); throw; } finally @@ -443,7 +443,7 @@ namespace WelsonJS.Launcher { if (!File.Exists(path)) { - Logger.Error("File does not exist for signature verification: {0}", logicalName); + Logger?.Error("File does not exist for signature verification: {0}", logicalName); throw new FileNotFoundException("File not found for signature verification: " + logicalName, path); } @@ -451,17 +451,17 @@ namespace WelsonJS.Launcher if (status == FileSignatureStatus.Valid) { - Logger.Info("Signature OK: {0}", logicalName); + Logger?.Info("Signature OK: {0}", logicalName); return; } if (status == FileSignatureStatus.NoSignature) { - Logger.Error("BLOCKED unsigned binary: {0}", logicalName); + Logger?.Error("BLOCKED unsigned binary: {0}", logicalName); throw new InvalidOperationException("Unsigned binary blocked: " + logicalName); } - Logger.Error("BLOCKED invalid signature: {0}", logicalName); + Logger?.Error("BLOCKED invalid signature: {0}", logicalName); throw new InvalidOperationException("Invalid signature: " + logicalName); } From b3416f9a5f94132695ebb5a18695c705393a579b Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 7 Dec 2025 02:26:45 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Dual=20HttpClient=20setup=20matches=20inten?= =?UTF-8?q?t;=20make=20Legacy=20behavior=20explicitly=20=E2=80=9Cno=20deco?= =?UTF-8?q?mpression=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dual HttpClient setup matches intent; make Legacy behavior explicitly “no decompression” --- WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs index 83d141f..7a8507d 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs @@ -36,13 +36,16 @@ namespace WelsonJS.Launcher private static bool _registered; private static readonly string LoaderNamespace = typeof(AssemblyLoader).Namespace ?? "WelsonJS.Launcher"; - private static readonly HttpClientHandler LegacyHttpHandler = new HttpClientHandler(); + private static readonly HttpClientHandler LegacyHttpHandler = new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.None + }; private static readonly HttpClientHandler HttpHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; - private static readonly HttpClient LegacyHttp = CreateClient(LegacyHttpHandler); // No the Accept-Encoding (e.g., gzip, deflate) header - private static readonly HttpClient Http = CreateClient(HttpHandler); // With the Accept-Encoding (e.g., gzip, deflate) header + private static readonly HttpClient LegacyHttp = CreateClient(LegacyHttpHandler); // Does not send Accept-Encoding (gzip, deflate) + private static readonly HttpClient Http = CreateClient(HttpHandler); // Sends Accept-Encoding (gzip, deflate) and auto-decompresses private static HttpClient CreateClient(HttpMessageHandler handler) { @@ -387,7 +390,10 @@ namespace WelsonJS.Launcher using (var res = LegacyHttp.GetAsync(gzUrl).GetAwaiter().GetResult()) { if (res.StatusCode == HttpStatusCode.NotFound) + { + Logger?.Info("No gzipped variant at {0}; falling back to uncompressed URL.", gzUrl); return false; + } res.EnsureSuccessStatusCode(); From e8dbf69491fd5293750f7603ffe261099c7aff32 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 7 Dec 2025 02:42:26 +0900 Subject: [PATCH 4/4] Clearify an exceptions Clearify an exceptions --- .../WelsonJS.Launcher/AssemblyLoader.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs index 7a8507d..28575be 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/AssemblyLoader.cs @@ -369,9 +369,14 @@ namespace WelsonJS.Launcher throw new FileNotFoundException("File not found after download", dest); } } + catch (HttpRequestException ex) + { + Logger?.Error("Network or I/O error downloading {0}: {1}", url, ex.Message); + throw; + } catch (Exception ex) { - Logger?.Error("Error downloading {0}: {1}", url, ex.Message); + Logger?.Error("Unexpected error downloading {0}: {1}", url, ex.Message); throw; } finally @@ -404,14 +409,23 @@ namespace WelsonJS.Launcher gz.CopyTo(fs); } + if (File.Exists(dest)) + File.Delete(dest); + File.Move(tempFile, dest); return true; } } + catch (HttpRequestException ex) + { + Logger?.Warn("Network or I/O error downloading compressed file from {0}: {1}", gzUrl, ex.Message); + throw; + } catch (Exception ex) { - Logger?.Warn("Failed to download or decompress gzipped file from {0}: {1}", gzUrl, ex.Message); + Logger?.Error("Unexpected error downloading compressed file from {0}: {1}", gzUrl, ex.Message); + throw; } finally { @@ -423,12 +437,10 @@ namespace WelsonJS.Launcher } catch (Exception ex) { - Logger?.Info("Failed to delete temp file {0}: {1}", tempFile, ex.Message); + Logger?.Info("Failed to delete temporary file {0}: {1}", tempFile, ex.Message); } } } - - return false; }