mirror of
https://github.com/gnh1201/welsonjs.git
synced 2025-12-08 15:24:07 +00:00
Merge pull request #361 from gnh1201/dev
Introduce the Catswords.Phantomizer to load DLL files via network
This commit is contained in:
commit
2040a02628
|
|
@ -25,6 +25,7 @@ after_build:
|
||||||
#- cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Toolkit\bin\x86\%CONFIGURATION%\* artifacts\
|
#- cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Toolkit\bin\x86\%CONFIGURATION%\* artifacts\
|
||||||
- cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Service\bin\x86\%CONFIGURATION%\* artifacts\
|
- cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Service\bin\x86\%CONFIGURATION%\* artifacts\
|
||||||
- cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Launcher\bin\x86\%CONFIGURATION%\* artifacts\
|
- cmd: xcopy /s /y WelsonJS.Toolkit\WelsonJS.Launcher\bin\x86\%CONFIGURATION%\* artifacts\
|
||||||
|
- cmd: xcopy /s /y WelsonJS.Toolkit\Catswords.Phantomizer\bin\%CONFIGURATION%\netstandard2.0\* artifacts\
|
||||||
- cmd: nuget pack WelsonJS.Toolkit\WelsonJS.Toolkit\ -properties Configuration=%CONFIGURATION% -properties Platform=x86 -OutputDirectory artifacts\
|
- cmd: nuget pack WelsonJS.Toolkit\WelsonJS.Toolkit\ -properties Configuration=%CONFIGURATION% -properties Platform=x86 -OutputDirectory artifacts\
|
||||||
- ps: Start-BitsTransfer -Source "https://catswords.blob.core.windows.net/welsonjs/welsonjs_setup_unsigned.exe" -Destination "artifacts\welsonjs_setup.exe"
|
- ps: Start-BitsTransfer -Source "https://catswords.blob.core.windows.net/welsonjs/welsonjs_setup_unsigned.exe" -Destination "artifacts\welsonjs_setup.exe"
|
||||||
- ps: Start-BitsTransfer -Source "https://catswords.blob.core.windows.net/welsonjs/chakracore-build/x86_release/ChakraCore.dll" -Destination "artifacts\ChakraCore.dll"
|
- ps: Start-BitsTransfer -Source "https://catswords.blob.core.windows.net/welsonjs/chakracore-build/x86_release/ChakraCore.dll" -Destination "artifacts\ChakraCore.dll"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
// AssemblyLoader.cs
|
// AssemblyLoader.cs (Catswords.Phantomizer)
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: MIT
|
||||||
// SPDX-FileCopyrightText: Namhyeon Go <gnh1201@catswords.re.kr>, 2025 Catswords OSS and WelsonJS Contributors
|
// SPDX-FileCopyrightText: Namhyeon Go <gnh1201@catswords.re.kr>, 2025 Catswords OSS and WelsonJS Contributors
|
||||||
// https://github.com/gnh1201/welsonjs
|
// https://github.com/gnh1201/welsonjs
|
||||||
//
|
//
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
@ -12,14 +13,14 @@ using System.Net.Http;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace WelsonJS.Launcher
|
namespace Catswords.Phantomizer
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Network-aware loader for managed (.NET) and native (C/C++) binaries.
|
/// Network-aware loader for managed (.NET) and native (C/C++) binaries.
|
||||||
/// - Managed assemblies resolve via AssemblyResolve
|
/// - Managed assemblies resolve via AssemblyResolve
|
||||||
/// - Native modules explicitly loaded via LoadNativeModules(...)
|
/// - Native modules explicitly loaded via LoadNativeModules(...)
|
||||||
/// - All DLLs must have valid Authenticode signatures
|
/// - All DLLs must have valid Authenticode signatures
|
||||||
/// - Cached at: %APPDATA%\WelsonJS\assembly\{Name}\{Version}\
|
/// - Cached at: %APPDATA%\Catswords\assembly\{Name}\{Version}\
|
||||||
/// - BaseUrl must be set by Main() before calling Register()
|
/// - BaseUrl must be set by Main() before calling Register()
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AssemblyLoader
|
public static class AssemblyLoader
|
||||||
|
|
@ -30,12 +31,12 @@ namespace WelsonJS.Launcher
|
||||||
/// Must be set before Register() or LoadNativeModules().
|
/// Must be set before Register() or LoadNativeModules().
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string BaseUrl { get; set; } = null;
|
public static string BaseUrl { get; set; } = null;
|
||||||
public static ICompatibleLogger Logger { get; set; } = null;
|
public static string LoaderNamespace { get; set; } = typeof(AssemblyLoader).Namespace;
|
||||||
|
public static string AppName { get; set; } = "Catswords";
|
||||||
|
|
||||||
private static readonly object SyncRoot = new object();
|
private static readonly object SyncRoot = new object();
|
||||||
private static bool _registered;
|
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
|
AutomaticDecompression = DecompressionMethods.None
|
||||||
|
|
@ -168,20 +169,20 @@ namespace WelsonJS.Launcher
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||||
{
|
{
|
||||||
Logger?.Error("AssemblyLoader.Register() called but BaseUrl is not set.");
|
Trace.TraceError("AssemblyLoader.Register() called but BaseUrl is not set.");
|
||||||
throw new InvalidOperationException("AssemblyLoader.BaseUrl must be configured before Register().");
|
throw new InvalidOperationException("AssemblyLoader.BaseUrl must be configured before Register().");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!BaseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
if (!BaseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
Logger?.Error("AssemblyLoader.BaseUrl must use HTTPS for security.");
|
Trace.TraceError("AssemblyLoader.BaseUrl must use HTTPS for security.");
|
||||||
throw new InvalidOperationException("AssemblyLoader.BaseUrl must use HTTPS.");
|
throw new InvalidOperationException("AssemblyLoader.BaseUrl must use HTTPS.");
|
||||||
}
|
}
|
||||||
|
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
|
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
|
||||||
_registered = true;
|
_registered = true;
|
||||||
|
|
||||||
Logger?.Info("AssemblyLoader: AssemblyResolve handler registered.");
|
Trace.TraceInformation("AssemblyLoader: AssemblyResolve handler registered.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,17 +204,17 @@ namespace WelsonJS.Launcher
|
||||||
lock (SyncRoot)
|
lock (SyncRoot)
|
||||||
{
|
{
|
||||||
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
string cacheDir = Path.Combine(appData, "WelsonJS", "assembly", ownerAssemblyName, versionString);
|
string cacheDir = Path.Combine(appData, AppName, "assembly", ownerAssemblyName, versionString);
|
||||||
Directory.CreateDirectory(cacheDir);
|
Directory.CreateDirectory(cacheDir);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!SetDllDirectory(cacheDir))
|
if (!SetDllDirectory(cacheDir))
|
||||||
Logger?.Warn("SetDllDirectory failed for: {0}", cacheDir);
|
Trace.TraceWarning("SetDllDirectory failed for: {0}", cacheDir);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger?.Warn("SetDllDirectory threw exception: {0}", ex.Message);
|
Trace.TraceWarning("SetDllDirectory threw exception: {0}", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (string raw in fileNames)
|
foreach (string raw in fileNames)
|
||||||
|
|
@ -228,11 +229,11 @@ namespace WelsonJS.Launcher
|
||||||
{
|
{
|
||||||
string url = $"{BaseUrl.TrimEnd('/')}/native/{ownerAssemblyName}/{versionString}/{fileName}";
|
string url = $"{BaseUrl.TrimEnd('/')}/native/{ownerAssemblyName}/{versionString}/{fileName}";
|
||||||
DownloadFile(url, localPath);
|
DownloadFile(url, localPath);
|
||||||
Logger?.Info("Downloaded native module: {0}", fileName);
|
Trace.TraceInformation("Downloaded native module: {0}", fileName);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger?.Info("Using cached native module: {0}", localPath);
|
Trace.TraceInformation("Using cached native module: {0}", localPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureSignedFileOrThrow(localPath, fileName);
|
EnsureSignedFileOrThrow(localPath, fileName);
|
||||||
|
|
@ -241,12 +242,12 @@ namespace WelsonJS.Launcher
|
||||||
if (h == IntPtr.Zero)
|
if (h == IntPtr.Zero)
|
||||||
{
|
{
|
||||||
int errorCode = Marshal.GetLastWin32Error();
|
int errorCode = Marshal.GetLastWin32Error();
|
||||||
Logger?.Error("LoadLibrary failed for {0} with error code {1}", localPath, errorCode);
|
Trace.TraceError("LoadLibrary failed for {0} with error code {1}", localPath, errorCode);
|
||||||
throw new InvalidOperationException($"Failed to load native module: {fileName} (error: {errorCode})");
|
throw new InvalidOperationException($"Failed to load native module: {fileName} (error: {errorCode})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger?.Info("Loaded native module: {0}", fileName);
|
Trace.TraceInformation("Loaded native module: {0}", fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +278,7 @@ namespace WelsonJS.Launcher
|
||||||
|
|
||||||
private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
|
private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
|
||||||
{
|
{
|
||||||
Logger?.Info("AssemblyResolve: {0}", args.Name);
|
Trace.TraceInformation("AssemblyResolve: {0}", args.Name);
|
||||||
|
|
||||||
AssemblyName req = new AssemblyName(args.Name);
|
AssemblyName req = new AssemblyName(args.Name);
|
||||||
string simpleName = req.Name;
|
string simpleName = req.Name;
|
||||||
|
|
@ -290,7 +291,7 @@ namespace WelsonJS.Launcher
|
||||||
var entryName = entry.GetName().Name;
|
var entryName = entry.GetName().Name;
|
||||||
if (string.Equals(simpleName, entryName, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(simpleName, entryName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
Logger?.Info("AssemblyResolve: skipping entry assembly {0}", simpleName);
|
Trace.TraceInformation("AssemblyResolve: skipping entry assembly {0}", simpleName);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -301,7 +302,7 @@ namespace WelsonJS.Launcher
|
||||||
lock (SyncRoot)
|
lock (SyncRoot)
|
||||||
{
|
{
|
||||||
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
string cacheDir = Path.Combine(appData, "WelsonJS", "assembly", simpleName, versionStr);
|
string cacheDir = Path.Combine(appData, AppName, "assembly", simpleName, versionStr);
|
||||||
string dllPath = Path.Combine(cacheDir, simpleName + ".dll");
|
string dllPath = Path.Combine(cacheDir, simpleName + ".dll");
|
||||||
|
|
||||||
Directory.CreateDirectory(cacheDir);
|
Directory.CreateDirectory(cacheDir);
|
||||||
|
|
@ -310,16 +311,16 @@ namespace WelsonJS.Launcher
|
||||||
{
|
{
|
||||||
string url = $"{BaseUrl.TrimEnd('/')}/managed/{simpleName}/{versionStr}/{simpleName}.dll";
|
string url = $"{BaseUrl.TrimEnd('/')}/managed/{simpleName}/{versionStr}/{simpleName}.dll";
|
||||||
DownloadFile(url, dllPath);
|
DownloadFile(url, dllPath);
|
||||||
Logger?.Info("Downloaded managed assembly: {0}", simpleName);
|
Trace.TraceInformation("Downloaded managed assembly: {0}", simpleName);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger?.Info("Using cached managed assembly: {0}", dllPath);
|
Trace.TraceInformation("Using cached managed assembly: {0}", dllPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!File.Exists(dllPath))
|
if (!File.Exists(dllPath))
|
||||||
{
|
{
|
||||||
Logger?.Warn("AssemblyResolve: managed assembly not found after download attempt: {0}", simpleName);
|
Trace.TraceWarning("AssemblyResolve: managed assembly not found after download attempt: {0}", simpleName);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,13 +346,13 @@ namespace WelsonJS.Launcher
|
||||||
|
|
||||||
if (isDll && TryDownloadCompressedFile(gzUrl, dest))
|
if (isDll && TryDownloadCompressedFile(gzUrl, dest))
|
||||||
{
|
{
|
||||||
Logger?.Info("Downloaded and decompressed file to: {0}", dest);
|
Trace.TraceInformation("Downloaded and decompressed file to: {0}", dest);
|
||||||
downloaded = true;
|
downloaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!downloaded)
|
if (!downloaded)
|
||||||
{
|
{
|
||||||
Logger?.Info("Downloading file from: {0}", url);
|
Trace.TraceInformation("Downloading file from: {0}", url);
|
||||||
res = Http.GetAsync(url).GetAwaiter().GetResult();
|
res = Http.GetAsync(url).GetAwaiter().GetResult();
|
||||||
res.EnsureSuccessStatusCode();
|
res.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
|
@ -361,7 +362,7 @@ namespace WelsonJS.Launcher
|
||||||
s.CopyTo(fs);
|
s.CopyTo(fs);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger?.Info("Downloaded file to: {0}", dest);
|
Trace.TraceInformation("Downloaded file to: {0}", dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!File.Exists(dest))
|
if (!File.Exists(dest))
|
||||||
|
|
@ -371,12 +372,12 @@ namespace WelsonJS.Launcher
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
Logger?.Error("Network or I/O error downloading {0}: {1}", url, ex.Message);
|
Trace.TraceError("Network or I/O error downloading {0}: {1}", url, ex.Message);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger?.Error("Unexpected error downloading {0}: {1}", url, ex.Message);
|
Trace.TraceError("Unexpected error downloading {0}: {1}", url, ex.Message);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -396,7 +397,7 @@ namespace WelsonJS.Launcher
|
||||||
{
|
{
|
||||||
if (res.StatusCode == HttpStatusCode.NotFound)
|
if (res.StatusCode == HttpStatusCode.NotFound)
|
||||||
{
|
{
|
||||||
Logger?.Info("No gzipped variant at {0}; falling back to uncompressed URL.", gzUrl);
|
Trace.TraceInformation("No gzipped variant at {0}; falling back to uncompressed URL.", gzUrl);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -419,12 +420,12 @@ namespace WelsonJS.Launcher
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
Logger?.Warn("Network or I/O error downloading compressed file from {0}: {1}", gzUrl, ex.Message);
|
Trace.TraceWarning("Network or I/O error downloading compressed file from {0}: {1}", gzUrl, ex.Message);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger?.Error("Unexpected error downloading compressed file from {0}: {1}", gzUrl, ex.Message);
|
Trace.TraceError("Unexpected error downloading compressed file from {0}: {1}", gzUrl, ex.Message);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -437,7 +438,7 @@ namespace WelsonJS.Launcher
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger?.Info("Failed to delete temporary file {0}: {1}", tempFile, ex.Message);
|
Trace.TraceInformation("Failed to delete temporary file {0}: {1}", tempFile, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +462,7 @@ namespace WelsonJS.Launcher
|
||||||
{
|
{
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
Logger?.Error("File does not exist for signature verification: {0}", logicalName);
|
Trace.TraceError("File does not exist for signature verification: {0}", logicalName);
|
||||||
throw new FileNotFoundException("File not found for signature verification: " + logicalName, path);
|
throw new FileNotFoundException("File not found for signature verification: " + logicalName, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,17 +470,17 @@ namespace WelsonJS.Launcher
|
||||||
|
|
||||||
if (status == FileSignatureStatus.Valid)
|
if (status == FileSignatureStatus.Valid)
|
||||||
{
|
{
|
||||||
Logger?.Info("Signature OK: {0}", logicalName);
|
Trace.TraceInformation("Signature OK: {0}", logicalName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status == FileSignatureStatus.NoSignature)
|
if (status == FileSignatureStatus.NoSignature)
|
||||||
{
|
{
|
||||||
Logger?.Error("BLOCKED unsigned binary: {0}", logicalName);
|
Trace.TraceError("BLOCKED unsigned binary: {0}", logicalName);
|
||||||
throw new InvalidOperationException("Unsigned binary blocked: " + logicalName);
|
throw new InvalidOperationException("Unsigned binary blocked: " + logicalName);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger?.Error("BLOCKED invalid signature: {0}", logicalName);
|
Trace.TraceError("BLOCKED invalid signature: {0}", logicalName);
|
||||||
throw new InvalidOperationException("Invalid signature: " + logicalName);
|
throw new InvalidOperationException("Invalid signature: " + logicalName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<Company>Catswords Research</Company>
|
||||||
|
<Authors>Namhyeon Go, 2025 Catswords OSS and WelsonJS Contributors</Authors>
|
||||||
|
<PackageProjectUrl>https://github.com/gnh1201/welsonjs</PackageProjectUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<RepositoryUrl>https://github.com/gnh1201/welsonjs</RepositoryUrl>
|
||||||
|
<PackageTags>loader</PackageTags>
|
||||||
|
<Description>Catswords.Phantomizer is an HTTP-based dynamic-link library (DLL) loader designed for .NET applications. It allows your application to fetch and load assemblies directly from your CDN (Azure Blob, S3, Cloudflare R2, etc.) at runtime, with optional GZip compression support.</Description>
|
||||||
|
<Copyright>Namhyeon Go, 2025 Catswords OSS and WelsonJS Contributors</Copyright>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
21
WelsonJS.Toolkit/Catswords.Phantomizer/LICENSE
Normal file
21
WelsonJS.Toolkit/Catswords.Phantomizer/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) Namhyeon Go <gnh1201@catswords.re.kr>, 2025 Catswords OSS and WelsonJS Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
117
WelsonJS.Toolkit/Catswords.Phantomizer/README.md
Normal file
117
WelsonJS.Toolkit/Catswords.Phantomizer/README.md
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Catswords.Phantomizer
|
||||||
|
|
||||||
|
**Catswords.Phantomizer** is an HTTP-based dynamic-link library (DLL) loader designed for .NET applications.
|
||||||
|
It allows your application to fetch and load assemblies directly from your CDN (Azure Blob, S3, Cloudflare R2, etc.) at runtime, with optional GZip compression support.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
* Load managed (`*.dll`) and native (`*.dll`) assemblies over HTTP
|
||||||
|
* Optional `.dll.gz` decompression for faster network delivery
|
||||||
|
* CDN-friendly URL structure
|
||||||
|
* Easy bootstrap through a small embedded loader
|
||||||
|
* Loader is implemented using **pure .NET BCL only**, ensuring stable operation without external dependencies
|
||||||
|
* Built-in **code-signing verification** support to ensure assemblies are trusted and tamper-free
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 How to Use
|
||||||
|
|
||||||
|
### 1. Embed Phantomizer into your project
|
||||||
|
|
||||||
|
Add `Catswords.Phantomizer.dll.gz` to your `Resources.resx` file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Initialize Phantomizer at application startup
|
||||||
|
|
||||||
|
Place the following code inside your `Main` method, static constructor, or any early entry point:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
static Program() {
|
||||||
|
// ...
|
||||||
|
InitializeAssemblyLoader();
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InitializeAssemblyLoader()
|
||||||
|
{
|
||||||
|
byte[] gzBytes = Properties.Resources.Phantomizer;
|
||||||
|
|
||||||
|
byte[] dllBytes;
|
||||||
|
using (var input = new MemoryStream(gzBytes))
|
||||||
|
using (var gz = new GZipStream(input, CompressionMode.Decompress))
|
||||||
|
using (var output = new MemoryStream())
|
||||||
|
{
|
||||||
|
gz.CopyTo(output);
|
||||||
|
dllBytes = output.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assembly phantomAsm = Assembly.Load(dllBytes);
|
||||||
|
Type loaderType = phantomAsm.GetType("Catswords.Phantomizer.AssemblyLoader", true);
|
||||||
|
|
||||||
|
loaderType.GetProperty("BaseUrl")?.SetValue(null, GetAppConfig("AssemblyBaseUrl")); // Set your CDN base URL
|
||||||
|
loaderType.GetProperty("LoaderNamespace")?.SetValue(null, typeof(Program).Namespace);
|
||||||
|
loaderType.GetProperty("AppName")?.SetValue(null, "WelsonJS"); // Set your application name
|
||||||
|
loaderType.GetMethod("Register")?.Invoke(null, null);
|
||||||
|
|
||||||
|
var loadNativeModulesMethod = loaderType.GetMethod(
|
||||||
|
"LoadNativeModules",
|
||||||
|
BindingFlags.Public | BindingFlags.Static,
|
||||||
|
binder: null,
|
||||||
|
types: new[] { typeof(string), typeof(Version), typeof(string[]) },
|
||||||
|
modifiers: null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loadNativeModulesMethod == null)
|
||||||
|
throw new InvalidOperationException("LoadNativeModules(string, Version, string[]) method not found.");
|
||||||
|
|
||||||
|
loadNativeModulesMethod.Invoke(null, new object[]
|
||||||
|
{
|
||||||
|
"ChakraCore",
|
||||||
|
new Version(1, 13, 0, 0),
|
||||||
|
new[] { "ChakraCore.dll" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Upload your DLL files to a CDN
|
||||||
|
|
||||||
|
Upload your managed and native assemblies to your CDN following the URL pattern below.
|
||||||
|
|
||||||
|
#### 📁 URL Rules
|
||||||
|
|
||||||
|
| Type | Example URL | Description |
|
||||||
|
| ------------------ | ----------------------------------------------------------------------------------- | ------------------------------- |
|
||||||
|
| Managed DLL | `https://example.cdn.tld/packages/managed/MyManagedLib/1.0.0.0/MyManagedLib.dll` | Normal .NET assembly |
|
||||||
|
| Managed DLL (GZip) | `https://example.cdn.tld/packages/managed/MyManagedLib/1.0.0.0/MyManagedLib.dll.gz` | GZip-compressed .NET assembly |
|
||||||
|
| Native DLL | `https://example.cdn.tld/packages/native/MyNativeLib/1.0.0.0/MyNativeLib.dll` | Native assembly |
|
||||||
|
| Native DLL (GZip) | `https://example.cdn.tld/packages/native/MyNativeLib/1.0.0.0/MyNativeLib.dll.gz` | GZip-compressed native assembly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 🎉 Start loading assemblies over HTTP
|
||||||
|
|
||||||
|
Once Phantomizer is initialized, your application will automatically fetch missing assemblies from your CDN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Download the pre-compiled file
|
||||||
|
* [Download Catswords.Phantomizer.dll.gz (catswords.blob.core.windows.net)](https://catswords.blob.core.windows.net/welsonjs/packages/managed/Catswords.Phantomizer/1.0.0.0/Catswords.Phantomizer.dll.gz)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Report abuse
|
||||||
|
* [GitHub Security Advisories (gnh1201/welsonjs)](https://github.com/gnh1201/welsonjs/security)
|
||||||
|
* [abuse@catswords.re.kr](mailto:abuse@catswords.re.kr)
|
||||||
|
|
||||||
|
## Join the community
|
||||||
|
I am always open. Collaboration, opportunities, and community activities are all welcome.
|
||||||
|
|
||||||
|
* ActivityPub [@catswords_oss@catswords.social](https://catswords.social/@catswords_oss)
|
||||||
|
* XMPP [catswords@conference.omemo.id](xmpp:catswords@conference.omemo.id?join)
|
||||||
|
* [Join Catswords OSS on Microsoft Teams (teams.live.com)](https://teams.live.com/l/community/FEACHncAhq8ldnojAI)
|
||||||
|
* [Join Catswords OSS #welsonjs on Discord (discord.gg)](https://discord.gg/XKG5CjtXEj)
|
||||||
|
|
@ -8,7 +8,9 @@ using System.Collections.Generic;
|
||||||
using System.Configuration;
|
using System.Configuration;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using WelsonJS.Launcher.Telemetry;
|
using WelsonJS.Launcher.Telemetry;
|
||||||
|
|
@ -33,10 +35,7 @@ namespace WelsonJS.Launcher
|
||||||
_logger = new TraceLogger();
|
_logger = new TraceLogger();
|
||||||
|
|
||||||
// load external assemblies
|
// load external assemblies
|
||||||
AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl");
|
InitializeAssemblyLoader();
|
||||||
AssemblyLoader.Logger = _logger;
|
|
||||||
AssemblyLoader.Register();
|
|
||||||
AssemblyLoader.LoadNativeModules("ChakraCore", new Version(1, 13, 0, 0), new[] { "ChakraCore.dll" });
|
|
||||||
|
|
||||||
// telemetry
|
// telemetry
|
||||||
try
|
try
|
||||||
|
|
@ -114,6 +113,49 @@ namespace WelsonJS.Launcher
|
||||||
_mutex.Dispose();
|
_mutex.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void InitializeAssemblyLoader()
|
||||||
|
{
|
||||||
|
byte[] gzBytes = Properties.Resources.Phantomizer;
|
||||||
|
|
||||||
|
byte[] dllBytes;
|
||||||
|
using (var input = new MemoryStream(gzBytes))
|
||||||
|
using (var gz = new GZipStream(input, CompressionMode.Decompress))
|
||||||
|
using (var output = new MemoryStream())
|
||||||
|
{
|
||||||
|
gz.CopyTo(output);
|
||||||
|
dllBytes = output.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assembly phantomAsm = Assembly.Load(dllBytes);
|
||||||
|
Type loaderType = phantomAsm.GetType("Catswords.Phantomizer.AssemblyLoader", true);
|
||||||
|
|
||||||
|
loaderType.GetProperty("BaseUrl")?.SetValue(null, GetAppConfig("AssemblyBaseUrl"));
|
||||||
|
loaderType.GetProperty("LoaderNamespace")?.SetValue(null, typeof(Program).Namespace);
|
||||||
|
loaderType.GetProperty("AppName")?.SetValue(null, "WelsonJS");
|
||||||
|
loaderType.GetMethod("Register")?.Invoke(null, null);
|
||||||
|
|
||||||
|
var loadNativeModulesMethod = loaderType.GetMethod(
|
||||||
|
"LoadNativeModules",
|
||||||
|
BindingFlags.Public | BindingFlags.Static,
|
||||||
|
binder: null,
|
||||||
|
types: new[] { typeof(string), typeof(Version), typeof(string[]) },
|
||||||
|
modifiers: null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loadNativeModulesMethod == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("LoadNativeModules(string, Version, string[]) method not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNativeModulesMethod.Invoke(null, new object[]
|
||||||
|
{
|
||||||
|
"ChakraCore",
|
||||||
|
new Version(1, 13, 0, 0),
|
||||||
|
new[] { "ChakraCore.dll" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void RecordFirstDeployTime(string directory, string instanceId)
|
public static void RecordFirstDeployTime(string directory, string instanceId)
|
||||||
{
|
{
|
||||||
// get current time
|
// get current time
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,16 @@ namespace WelsonJS.Launcher.Properties {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System.Byte[] 형식의 지역화된 리소스를 찾습니다.
|
||||||
|
/// </summary>
|
||||||
|
internal static byte[] Phantomizer {
|
||||||
|
get {
|
||||||
|
object obj = ResourceManager.GetObject("Phantomizer", resourceCulture);
|
||||||
|
return ((byte[])(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다.
|
/// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -241,4 +241,7 @@
|
||||||
<data name="AssemblyBaseUrl" xml:space="preserve">
|
<data name="AssemblyBaseUrl" xml:space="preserve">
|
||||||
<value>https://catswords.blob.core.windows.net/welsonjs/packages</value>
|
<value>https://catswords.blob.core.windows.net/welsonjs/packages</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Phantomizer" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||||
|
<value>..\Resources\Catswords.Phantomizer.dll.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
Binary file not shown.
|
|
@ -88,7 +88,6 @@
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="AssemblyLoader.cs" />
|
|
||||||
<Compile Include="ICompatibleLogger.cs" />
|
<Compile Include="ICompatibleLogger.cs" />
|
||||||
<Compile Include="IResourceTool.cs" />
|
<Compile Include="IResourceTool.cs" />
|
||||||
<Compile Include="JsCore.cs" />
|
<Compile Include="JsCore.cs" />
|
||||||
|
|
@ -171,6 +170,7 @@
|
||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Include="Resources\Catswords.Phantomizer.dll.gz" />
|
||||||
<None Include="Resources\icon_link_128.png" />
|
<None Include="Resources\icon_link_128.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "WelsonJS.Cryptography", "We
|
||||||
EndProject
|
EndProject
|
||||||
Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "WelsonJS.Cryptography.Test", "WelsonJS.Cryptography.Test\WelsonJS.Cryptography.Test.vbproj", "{C65EC34B-71C7-47CF-912E-D304283EB412}"
|
Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "WelsonJS.Cryptography.Test", "WelsonJS.Cryptography.Test\WelsonJS.Cryptography.Test.vbproj", "{C65EC34B-71C7-47CF-912E-D304283EB412}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catswords.Phantomizer", "Catswords.Phantomizer\Catswords.Phantomizer.csproj", "{59C67003-C14E-4703-927F-318BFF15F903}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -81,6 +83,14 @@ Global
|
||||||
{C65EC34B-71C7-47CF-912E-D304283EB412}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C65EC34B-71C7-47CF-912E-D304283EB412}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{C65EC34B-71C7-47CF-912E-D304283EB412}.Release|x86.ActiveCfg = Release|x86
|
{C65EC34B-71C7-47CF-912E-D304283EB412}.Release|x86.ActiveCfg = Release|x86
|
||||||
{C65EC34B-71C7-47CF-912E-D304283EB412}.Release|x86.Build.0 = Release|x86
|
{C65EC34B-71C7-47CF-912E-D304283EB412}.Release|x86.Build.0 = Release|x86
|
||||||
|
{59C67003-C14E-4703-927F-318BFF15F903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{59C67003-C14E-4703-927F-318BFF15F903}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{59C67003-C14E-4703-927F-318BFF15F903}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{59C67003-C14E-4703-927F-318BFF15F903}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{59C67003-C14E-4703-927F-318BFF15F903}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{59C67003-C14E-4703-927F-318BFF15F903}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{59C67003-C14E-4703-927F-318BFF15F903}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{59C67003-C14E-4703-927F-318BFF15F903}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user