From 130a6fd76721b6acfcbc20f71e0329d50b3dd9de Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 26 Sep 2025 17:04:14 +0900 Subject: [PATCH 1/8] Add ChakraCore integration and native bootstrap logic Introduces JsCore for ChakraCore P/Invoke, JsSerializer for JSON utilities via JS, and NativeBootstrap for robust native DLL loading. Updates Program.cs to initialize native dependencies at startup and registers new source files in the project file. --- WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs | 171 ++++++++++ .../WelsonJS.Launcher/JsSerializer.cs | 293 ++++++++++++++++++ .../WelsonJS.Launcher/NativeBootstrap.cs | 147 +++++++++ WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 7 + .../WelsonJS.Launcher.csproj | 3 + 5 files changed, 621 insertions(+) create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs new file mode 100644 index 0000000..38db84a --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs @@ -0,0 +1,171 @@ +// JsCore.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.Runtime.InteropServices; + +namespace WelsonJS.Launcher +{ + public sealed class JsCore : IDisposable + { + private IntPtr _runtime = IntPtr.Zero; + private IntPtr _context = IntPtr.Zero; + private bool _disposed; + + public JsCore() + { + Check(JsCreateRuntime(0, IntPtr.Zero, out _runtime), nameof(JsCreateRuntime)); + Check(JsCreateContext(_runtime, out _context), nameof(JsCreateContext)); + Check(JsSetCurrentContext(_context), nameof(JsSetCurrentContext)); + } + + /// + /// Evaluates JavaScript and returns the result converted to string (via JsConvertValueToString). + /// + public string EvaluateToString(string script, string sourceUrl = "repl") + { + if (_disposed) throw new ObjectDisposedException(nameof(JsCore)); + if (script is null) throw new ArgumentNullException(nameof(script)); + + Check(JsRunScript(script, IntPtr.Zero, sourceUrl, out var result), nameof(JsRunScript)); + + // Convert result -> JsString + Check(JsConvertValueToString(result, out var jsString), nameof(JsConvertValueToString)); + + // Extract pointer/length (UTF-16) and marshal to managed string + Check(JsStringToPointer(jsString, out var p, out var len), nameof(JsStringToPointer)); + return Marshal.PtrToStringUni(p, checked((int)len)); + } + + /// + /// Evaluates JavaScript for side effects; discards the result. + /// + public void Execute(string script, string sourceUrl = "repl") + { + _ = EvaluateToString(script, sourceUrl); + } + + private static void Check(JsErrorCode code, string op) + { + if (code != JsErrorCode.JsNoError) + throw new InvalidOperationException($"{op} failed with {code} (0x{(int)code:X})."); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + try + { + // Unset the current context from the SAME physical thread that set it. + JsSetCurrentContext(IntPtr.Zero); + } + catch + { + // Swallow to ensure runtime is disposed. + } + finally + { + if (_runtime != IntPtr.Zero) + { + JsDisposeRuntime(_runtime); + _runtime = IntPtr.Zero; + } + _context = IntPtr.Zero; + } + + GC.SuppressFinalize(this); + } + + ~JsCore() => Dispose(); + + // ========================= + // P/Invoke surface (as given) + // ========================= + + // Essential (expanded) JsErrorCode set matching ChakraCore’s headers layout. + // Values are grouped by category bases (0x10000, 0x20000, ...). + public enum JsErrorCode + { + // Success + JsNoError = 0, + + // Category bases (useful when inspecting ranges) + JsErrorCategoryUsage = 0x10000, + JsErrorCategoryEngine = 0x20000, + JsErrorCategoryScript = 0x30000, + JsErrorCategoryFatal = 0x40000, + + // Usage errors (0x10001+) + JsErrorInvalidArgument = 0x10001, + JsErrorNullArgument = 0x10002, + JsErrorNoCurrentContext = 0x10003, + JsErrorInExceptionState = 0x10004, + JsErrorNotImplemented = 0x10005, + JsErrorWrongThread = 0x10006, + JsErrorRuntimeInUse = 0x10007, + JsErrorBadSerializedScript = 0x10008, + JsErrorInDisabledState = 0x10009, + JsErrorCannotDisableExecution = 0x1000A, + JsErrorHeapEnumInProgress = 0x1000B, + JsErrorArgumentNotObject = 0x1000C, + JsErrorInProfileCallback = 0x1000D, + JsErrorInThreadServiceCallback = 0x1000E, + JsErrorCannotSerializeDebugScript = 0x1000F, + JsErrorAlreadyDebuggingContext = 0x10010, + JsErrorAlreadyProfilingContext = 0x10011, + JsErrorIdleNotEnabled = 0x10012, + + // Engine errors (0x20001+) + JsErrorOutOfMemory = 0x20001, + JsErrorBadFPUState = 0x20002, + + // Script errors (0x30001+) + JsErrorScriptException = 0x30001, + JsErrorScriptCompile = 0x30002, + JsErrorScriptTerminated = 0x30003, + JsErrorScriptEvalDisabled = 0x30004, + + // Fatal (0x40001) + JsErrorFatal = 0x40001, + + // Misc/diagnostic (0x50000+) + JsErrorWrongRuntime = 0x50000, + JsErrorDiagAlreadyInDebugMode = 0x50001, + JsErrorDiagNotInDebugMode = 0x50002, + JsErrorDiagNotAtBreak = 0x50003, + JsErrorDiagInvalidHandle = 0x50004, + JsErrorDiagObjectNotFound = 0x50005, + JsErrorDiagUnableToPerformAction = 0x50006, + } + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsCreateRuntime(uint attributes, IntPtr callback, out IntPtr runtime); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsCreateContext(IntPtr runtime, out IntPtr context); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsSetCurrentContext(IntPtr context); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] + public static extern JsErrorCode JsRunScript(string script, IntPtr sourceContext, string sourceUrl, out IntPtr result); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsConvertValueToString(IntPtr value, out IntPtr stringValue); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsStringToPointer(IntPtr value, out IntPtr buffer, out UIntPtr length); + + // Note: Unsetting is typically done via JsSetCurrentContext(IntPtr.Zero) + // Kept here only if your build exposes this symbol. + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "JsSetCurrentContext")] + public static extern JsErrorCode JsUnSetCurrentContext(IntPtr zero); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsDisposeRuntime(IntPtr runtime); + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs new file mode 100644 index 0000000..b82b1a5 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs @@ -0,0 +1,293 @@ +// JsSerializer.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; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace WelsonJS.Launcher +{ + public sealed class JsSerializer : IDisposable + { + private readonly JsCore _core; + private readonly bool _ownsCore; + + public JsSerializer() : this(new JsCore(), true) { } + + public JsSerializer(JsCore core, bool ownsCore) + { + if (core == null) throw new ArgumentNullException("core"); + _core = core; + _ownsCore = ownsCore; + } + + public bool IsValid(string json) + { + if (json == null) throw new ArgumentNullException("json"); + string script = + "(function(){try{JSON.parse(" + Q(json) + ");return '1';}catch(_){return '0';}})()"; + string r = _core.EvaluateToString(script); + return r == "1"; + } + + public string Minify(string json) + { + if (json == null) throw new ArgumentNullException("json"); + string script = "JSON.stringify(JSON.parse(" + Q(json) + "))"; + return _core.EvaluateToString(script); + } + + public string Pretty(string json, int space) + { + if (json == null) throw new ArgumentNullException("json"); + space = Clamp(space, 0, 10); + string script = "JSON.stringify(JSON.parse(" + Q(json) + "),null," + space.ToString(CultureInfo.InvariantCulture) + ")"; + return _core.EvaluateToString(script); + } + + public string Normalize(string json) + { + return Minify(json); + } + + /// + /// Extracts a value by a simple path of property names (numeric segment as string = array index). + /// Returns the selected value as a JSON string. + /// + public string Extract(string json, params string[] path) + { + if (path == null) path = new string[0]; + object[] mixed = new object[path.Length]; + for (int i = 0; i < path.Length; i++) mixed[i] = path[i]; + return Extract(json, mixed); + } + + /// + /// Extracts by a mixed path. Segments can be strings (object keys) or integers (array indices). + /// Returns the selected value as a JSON string (e.g., a JS string returns with quotes). + /// Usage: Extract(json, "items", 0, "name") + /// + public string Extract(string json, params object[] path) + { + if (json == null) throw new ArgumentNullException("json"); + if (path == null) path = new object[0]; + + string jsPath = BuildJsPath(path); + + var sb = new StringBuilder(); + sb.Append("(function(){var v=JSON.parse(").Append(Q(json)).Append(");"); + sb.Append("var p=").Append(jsPath).Append(";"); + sb.Append("for(var i=0;i(ReferenceEqualityComparer.Instance), 0); + string script = "JSON.stringify((" + expr + "),null," + space.ToString(CultureInfo.InvariantCulture) + ")"; + return _core.EvaluateToString(script); + } + + private static int Clamp(int v, int min, int max) + { + if (v < min) return min; + if (v > max) return max; + return v; + } + + /// + /// Encode a .NET string as a JS double-quoted string literal. + /// + private static string Q(string s) + { + if (s == null) return "null"; + var sb = new StringBuilder(s.Length + 16); + sb.Append('"'); + for (int i = 0; i < s.Length; i++) + { + char ch = s[i]; + switch (ch) + { + case '\\': sb.Append(@"\\"); break; + case '"': sb.Append("\\\""); break; + case '\b': sb.Append(@"\b"); break; + case '\f': sb.Append(@"\f"); break; + case '\n': sb.Append(@"\n"); break; + case '\r': sb.Append(@"\r"); break; + case '\t': sb.Append(@"\t"); break; + default: + if (char.IsControl(ch)) + { + sb.Append("\\u"); + sb.Append(((int)ch).ToString("X4")); + } + else + { + sb.Append(ch); + } + break; + } + } + sb.Append('"'); + return sb.ToString(); + } + + /// + /// Builds a JS array literal representing the path. + /// Numeric segments are emitted as numbers; others as strings. + /// + private static string BuildJsPath(object[] segments) + { + if (segments == null || segments.Length == 0) return "[]"; + + var sb = new StringBuilder(); + sb.Append('['); + for (int i = 0; i < segments.Length; i++) + { + if (i > 0) sb.Append(','); + + object seg = segments[i]; + + // Treat integral types as numbers for array indexing + if (seg is sbyte || seg is byte || + seg is short || seg is ushort || + seg is int || seg is uint || + seg is long || seg is ulong) + { + sb.Append(Convert.ToString(seg, CultureInfo.InvariantCulture)); + } + else + { + string str = (seg == null) ? string.Empty : Convert.ToString(seg, CultureInfo.InvariantCulture); + sb.Append(Q(str)); + } + } + sb.Append(']'); + return sb.ToString(); + } + + private static bool IsNumeric(object v) + { + if (v == null) return false; + Type t = v.GetType(); + t = Nullable.GetUnderlyingType(t) ?? t; + return t == typeof(byte) || t == typeof(sbyte) || + t == typeof(short) || t == typeof(ushort) || + t == typeof(int) || t == typeof(uint) || + t == typeof(long) || t == typeof(ulong) || + t == typeof(float) || t == typeof(double) || + t == typeof(decimal); + } + + private static bool IsImmutableLike(object v) + { + return v is string || v is bool || + v is byte || v is sbyte || + v is short || v is ushort || + v is int || v is uint || + v is long || v is ulong || + v is float || v is double || v is decimal || + v is Guid || v is DateTime || v is DateTimeOffset; + } + + /// + /// Builds a safe JS expression for a .NET value (no engine calls here). + /// Engine will stringify the produced expression via JSON.stringify. + /// + private static string BuildJsExpression(object value, HashSet seen, int depth) + { + if (depth > 64) return "null"; // depth guard + + if (value == null) return "null"; + + // Primitives + if (value is string) return Q((string)value); + if (value is bool) return ((bool)value) ? "true" : "false"; + if (IsNumeric(value)) + return Convert.ToString(value, CultureInfo.InvariantCulture); + + // Common value-like types → stringify as JS strings + if (value is Guid) + return Q(((Guid)value).ToString()); + if (value is DateTime) + return Q(((DateTime)value).ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)); + if (value is DateTimeOffset) + return Q(((DateTimeOffset)value).ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)); + if (value is byte[]) + return Q(Convert.ToBase64String((byte[])value)); + + // Prevent circular refs for reference types + if (!IsImmutableLike(value) && !seen.Add(value)) + return "null"; + + // IDictionary (string keys only) + if (value is IDictionary) + { + var map = (IDictionary)value; + var sb = new StringBuilder(); + sb.Append('{'); + bool first = true; + foreach (DictionaryEntry kv in map) + { + string key = kv.Key as string; + if (key == null) continue; // JSON keys must be strings + if (!first) sb.Append(','); + first = false; + sb.Append(Q(key)).Append(':') + .Append(BuildJsExpression(kv.Value, seen, depth + 1)); + } + sb.Append('}'); + return sb.ToString(); + } + + // IEnumerable → array + if (value is IEnumerable) + { + var seq = (IEnumerable)value; + var sb = new StringBuilder(); + sb.Append('['); + bool first = true; + foreach (object item in seq) + { + if (!first) sb.Append(','); + first = false; + sb.Append(BuildJsExpression(item, seen, depth + 1)); + } + sb.Append(']'); + return sb.ToString(); + } + + // Fallback → ToString() as JS string + string s = value.ToString(); + return Q(s ?? string.Empty); + } + + public void Dispose() + { + if (_ownsCore) + _core.Dispose(); + } + + /// + /// Reference equality comparer for cycle detection (works on .NET Framework). + /// + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + private ReferenceEqualityComparer() { } + public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); + bool IEqualityComparer.Equals(object x, object y) { return object.ReferenceEquals(x, y); } + int IEqualityComparer.GetHashCode(object obj) + { + return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } + } + } +} \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs new file mode 100644 index 0000000..1b91330 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs @@ -0,0 +1,147 @@ +// NativeBootstrap.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.Reflection; +using System.Runtime.InteropServices; +using System.Collections.Generic; + +namespace WelsonJS.Launcher +{ + public static class NativeBootstrap + { + // Win32 APIs + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr LoadLibrary(string lpFileName); + + [DllImport("kernel32", SetLastError = true)] + private static extern bool SetDefaultDllDirectories(int DirectoryFlags); + + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "AddDllDirectory")] + private static extern IntPtr AddDllDirectory(string newDirectory); + + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool SetDllDirectory(string lpPathName); + + private const int LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000; + + /// + /// Tries to load native libraries in the following order: + /// 1) %APPDATA%\{appDataSubdirectory}\{dllName} + /// 2) Application base directory\{dllName} + /// + /// Must be called before any P/Invoke usage. + /// + public static void Init(IEnumerable dllNames, string appDataSubdirectory, ICompatibleLogger logger) + { + if (dllNames == null) throw new ArgumentNullException(nameof(dllNames)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string appDataPath = string.IsNullOrEmpty(appDataSubdirectory) + ? appData + : Path.Combine(appData, appDataSubdirectory); + + string asmLocation = Assembly.GetEntryAssembly()?.Location + ?? Assembly.GetExecutingAssembly().Location + ?? AppContext.BaseDirectory; + string appBaseDirectory = Path.GetDirectoryName(asmLocation) ?? AppContext.BaseDirectory; + + var triedPaths = new List(); + + foreach (string dllName in dllNames) + { + if (string.IsNullOrWhiteSpace(dllName)) + continue; + + // 1) %APPDATA% subdirectory + string candidate1 = Path.Combine(appDataPath, dllName); + triedPaths.Add(candidate1); + if (TryLoad(candidate1, logger)) return; + + // 2) Application base directory + string candidate2 = Path.Combine(appBaseDirectory, dllName); + triedPaths.Add(candidate2); + if (TryLoad(candidate2, logger)) return; + } + + string message = "Failed to load requested native libraries.\n" + + "Tried:\n " + string.Join("\n ", triedPaths); + logger.Error(message); + throw new FileNotFoundException(message); + } + + private static bool TryLoad(string fullPath, ICompatibleLogger logger) + { + try + { + if (!File.Exists(fullPath)) + { + logger.Info($"Not found: {fullPath}"); + return false; + } + + string directoryPath = Path.GetDirectoryName(fullPath) ?? AppContext.BaseDirectory; + if (!TryRegisterSearchDirectory(directoryPath, logger)) + { + logger.Warn($"Could not register search directory: {directoryPath}"); + } + + logger.Info($"Loading: {fullPath}"); + IntPtr handle = LoadLibrary(fullPath); + if (handle == IntPtr.Zero) + { + int err = Marshal.GetLastWin32Error(); + logger.Warn($"LoadLibrary failed for {fullPath} (Win32Error={err})."); + return false; + } + + logger.Info($"Successfully loaded: {fullPath}"); + return true; + } + catch (Exception ex) + { + logger.Warn($"Exception while loading {fullPath}: {ex.Message}"); + return false; + } + } + + private static bool TryRegisterSearchDirectory(string directoryPath, ICompatibleLogger logger) + { + try + { + bool ok = SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + if (!ok) + { + int e = Marshal.GetLastWin32Error(); + logger.Warn($"SetDefaultDllDirectories failed (Win32Error={e}), fallback to SetDllDirectory."); + return SetDllDirectory(directoryPath); + } + + IntPtr cookie = AddDllDirectory(directoryPath); + if (cookie == IntPtr.Zero) + { + int e = Marshal.GetLastWin32Error(); + logger.Warn($"AddDllDirectory failed (Win32Error={e}), fallback to SetDllDirectory."); + return SetDllDirectory(directoryPath); + } + + logger.Info($"Registered native DLL search directory: {directoryPath}"); + return true; + } + catch (EntryPointNotFoundException) + { + logger.Warn("DefaultDllDirectories API not available. Using SetDllDirectory fallback."); + return SetDllDirectory(directoryPath); + } + catch (Exception ex) + { + logger.Warn($"Register search directory failed for {directoryPath}: {ex.Message}"); + return false; + } + } + } +} \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 23547c6..485a3f3 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -15,6 +15,7 @@ namespace WelsonJS.Launcher { internal static class Program { + private const string _appDataSubDirectory = "WelsonJS"; private static readonly ICompatibleLogger _logger; public static Mutex _mutex; @@ -22,7 +23,13 @@ namespace WelsonJS.Launcher static Program() { + // set up logger _logger = new TraceLogger(); + + // load native libraries + NativeBootstrap.Init(new string[] { + "ChakraCore.dll" + }, _appDataSubDirectory, _logger); } [STAThread] diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index e9a04b4..c76f31e 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -88,6 +88,9 @@ + + + From 4af01706de0ec2791eee94df83f9fde3275e40f0 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 26 Sep 2025 17:13:39 +0900 Subject: [PATCH 2/8] Add .gitmodules for ChakraCore submodule Introduces a .gitmodules file to track the ChakraCore submodule under WelsonJS.Toolkit/ChakraCore, referencing the official ChakraCore repository. --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..317e9ac --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "WelsonJS.Toolkit/ChakraCore"] + path = WelsonJS.Toolkit/ChakraCore + url = https://github.com/chakra-core/ChakraCore From 3fe04d1113a3dd44799c282391562662ecfe3f30 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 27 Sep 2025 13:01:18 +0900 Subject: [PATCH 3/8] Fix submodule gitlink for ChakraCore --- WelsonJS.Toolkit/ChakraCore | 1 + 1 file changed, 1 insertion(+) create mode 160000 WelsonJS.Toolkit/ChakraCore diff --git a/WelsonJS.Toolkit/ChakraCore b/WelsonJS.Toolkit/ChakraCore new file mode 160000 index 0000000..36becec --- /dev/null +++ b/WelsonJS.Toolkit/ChakraCore @@ -0,0 +1 @@ +Subproject commit 36becec43348f259e8bee08cf2fcd171bfe56f42 From 83a037dfa21f87185708602d050455fdb108c88a Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 27 Sep 2025 22:52:13 +0900 Subject: [PATCH 4/8] Download setup and ChakraCore in AppVeyor build Added PowerShell steps to download the unsigned setup executable and ChakraCore.dll into the artifacts directory during the AppVeyor after_build phase. --- .appveyor.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index fab198a..aac253a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -26,6 +26,8 @@ after_build: - 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: 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/chakracore-build/x86_release/ChakraCore.dll" -Destination "artifacts\ChakraCore.dll" - cmd: 7z a artifacts.zip artifacts\* artifacts: @@ -44,4 +46,4 @@ notifications: - gnh1201@catswords.re.kr subject: "Build {{status}}: {{projectName}} {{buildVersion}}" on_build_success: false - on_build_failure: true + on_build_failure: true \ No newline at end of file From 87020d35acf589d6b06fefcfadd92ee83004037e Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 28 Sep 2025 00:07:41 +0900 Subject: [PATCH 5/8] Refactor logger interface and add JsNative interop layer Updated ICompatibleLogger to accept params object[] for flexible logging. Refactored TraceLogger to support the new interface and improved formatting. Added JsNative.cs to encapsulate ChakraCore P/Invoke interop, and updated JsCore to use JsNative for all native calls. Modified all resource tools to accept and use ICompatibleLogger for consistent logging. Updated project file to include new and updated sources. --- .../WelsonJS.Launcher/ICompatibleLogger.cs | 6 +- WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs | 152 +++--------------- .../WelsonJS.Launcher/JsNative.cs | 93 +++++++++++ .../WelsonJS.Launcher/ResourceServer.cs | 14 +- .../ResourceTools/ChromiumDevTools.cs | 5 +- .../ResourceTools/Completion.cs | 11 +- .../ResourceTools/DnsQuery.cs | 5 +- .../ResourceTools/IpQuery.cs | 5 +- .../ResourceTools/Settings.cs | 5 +- .../ResourceTools/TwoFactorAuth.cs | 5 +- .../WelsonJS.Launcher/ResourceTools/Whois.cs | 5 +- .../WelsonJS.Launcher/TraceLogger.cs | 29 +++- .../WelsonJS.Launcher.csproj | 2 + 13 files changed, 187 insertions(+), 150 deletions(-) create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/JsNative.cs diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ICompatibleLogger.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ICompatibleLogger.cs index 74b323f..16d4edb 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ICompatibleLogger.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ICompatibleLogger.cs @@ -10,8 +10,8 @@ namespace WelsonJS.Launcher { public interface ICompatibleLogger { - void Info(string message); - void Warn(string message); - void Error(string message); + void Info(params object[] args); + void Warn(params object[] args); + void Error(params object[] args); } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs index 38db84a..53af2b5 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs @@ -4,53 +4,47 @@ // https://github.com/gnh1201/welsonjs // using System; +using System.Globalization; using System.Runtime.InteropServices; namespace WelsonJS.Launcher { public sealed class JsCore : IDisposable { - private IntPtr _runtime = IntPtr.Zero; - private IntPtr _context = IntPtr.Zero; + private JsNative.JsRuntime _rt; + private JsNative.JsContext _ctx; private bool _disposed; public JsCore() { - Check(JsCreateRuntime(0, IntPtr.Zero, out _runtime), nameof(JsCreateRuntime)); - Check(JsCreateContext(_runtime, out _context), nameof(JsCreateContext)); - Check(JsSetCurrentContext(_context), nameof(JsSetCurrentContext)); + Check(JsNative.JsCreateRuntime(JsNative.JsRuntimeAttributes.None, null, out _rt), "JsCreateRuntime"); + Check(JsNative.JsCreateContext(_rt, out _ctx), "JsCreateContext"); + Check(JsNative.JsSetCurrentContext(_ctx), "JsSetCurrentContext"); } - /// - /// Evaluates JavaScript and returns the result converted to string (via JsConvertValueToString). - /// public string EvaluateToString(string script, string sourceUrl = "repl") { if (_disposed) throw new ObjectDisposedException(nameof(JsCore)); - if (script is null) throw new ArgumentNullException(nameof(script)); + if (script == null) throw new ArgumentNullException(nameof(script)); - Check(JsRunScript(script, IntPtr.Zero, sourceUrl, out var result), nameof(JsRunScript)); + JsNative.JsValue result; + Check(JsNative.JsRunScript(script, UIntPtr.Zero, sourceUrl, out result), "JsRunScript"); - // Convert result -> JsString - Check(JsConvertValueToString(result, out var jsString), nameof(JsConvertValueToString)); + JsNative.JsValue jsStr; + Check(JsNative.JsConvertValueToString(result, out jsStr), "JsConvertValueToString"); - // Extract pointer/length (UTF-16) and marshal to managed string - Check(JsStringToPointer(jsString, out var p, out var len), nameof(JsStringToPointer)); - return Marshal.PtrToStringUni(p, checked((int)len)); + IntPtr p; + UIntPtr len; + Check(JsNative.JsStringToPointer(jsStr, out p, out len), "JsStringToPointer"); + + int chars = checked((int)len); + return Marshal.PtrToStringUni(p, chars); } - /// - /// Evaluates JavaScript for side effects; discards the result. - /// - public void Execute(string script, string sourceUrl = "repl") + private static void Check(JsNative.JsErrorCode code, string op) { - _ = EvaluateToString(script, sourceUrl); - } - - private static void Check(JsErrorCode code, string op) - { - if (code != JsErrorCode.JsNoError) - throw new InvalidOperationException($"{op} failed with {code} (0x{(int)code:X})."); + if (code != JsNative.JsErrorCode.JsNoError) + throw new InvalidOperationException(op + " failed: " + code + " (0x" + ((int)code).ToString("X", CultureInfo.InvariantCulture) + ")"); } public void Dispose() @@ -60,112 +54,18 @@ namespace WelsonJS.Launcher try { - // Unset the current context from the SAME physical thread that set it. - JsSetCurrentContext(IntPtr.Zero); - } - catch - { - // Swallow to ensure runtime is disposed. + // Unset current context + JsNative.JsSetCurrentContext(new JsNative.JsContext { Handle = IntPtr.Zero }); } + catch { /* ignore */ } finally { - if (_runtime != IntPtr.Zero) - { - JsDisposeRuntime(_runtime); - _runtime = IntPtr.Zero; - } - _context = IntPtr.Zero; + if (_rt.Handle != IntPtr.Zero) + JsNative.JsDisposeRuntime(_rt); } - GC.SuppressFinalize(this); } - ~JsCore() => Dispose(); - - // ========================= - // P/Invoke surface (as given) - // ========================= - - // Essential (expanded) JsErrorCode set matching ChakraCore’s headers layout. - // Values are grouped by category bases (0x10000, 0x20000, ...). - public enum JsErrorCode - { - // Success - JsNoError = 0, - - // Category bases (useful when inspecting ranges) - JsErrorCategoryUsage = 0x10000, - JsErrorCategoryEngine = 0x20000, - JsErrorCategoryScript = 0x30000, - JsErrorCategoryFatal = 0x40000, - - // Usage errors (0x10001+) - JsErrorInvalidArgument = 0x10001, - JsErrorNullArgument = 0x10002, - JsErrorNoCurrentContext = 0x10003, - JsErrorInExceptionState = 0x10004, - JsErrorNotImplemented = 0x10005, - JsErrorWrongThread = 0x10006, - JsErrorRuntimeInUse = 0x10007, - JsErrorBadSerializedScript = 0x10008, - JsErrorInDisabledState = 0x10009, - JsErrorCannotDisableExecution = 0x1000A, - JsErrorHeapEnumInProgress = 0x1000B, - JsErrorArgumentNotObject = 0x1000C, - JsErrorInProfileCallback = 0x1000D, - JsErrorInThreadServiceCallback = 0x1000E, - JsErrorCannotSerializeDebugScript = 0x1000F, - JsErrorAlreadyDebuggingContext = 0x10010, - JsErrorAlreadyProfilingContext = 0x10011, - JsErrorIdleNotEnabled = 0x10012, - - // Engine errors (0x20001+) - JsErrorOutOfMemory = 0x20001, - JsErrorBadFPUState = 0x20002, - - // Script errors (0x30001+) - JsErrorScriptException = 0x30001, - JsErrorScriptCompile = 0x30002, - JsErrorScriptTerminated = 0x30003, - JsErrorScriptEvalDisabled = 0x30004, - - // Fatal (0x40001) - JsErrorFatal = 0x40001, - - // Misc/diagnostic (0x50000+) - JsErrorWrongRuntime = 0x50000, - JsErrorDiagAlreadyInDebugMode = 0x50001, - JsErrorDiagNotInDebugMode = 0x50002, - JsErrorDiagNotAtBreak = 0x50003, - JsErrorDiagInvalidHandle = 0x50004, - JsErrorDiagObjectNotFound = 0x50005, - JsErrorDiagUnableToPerformAction = 0x50006, - } - - [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern JsErrorCode JsCreateRuntime(uint attributes, IntPtr callback, out IntPtr runtime); - - [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern JsErrorCode JsCreateContext(IntPtr runtime, out IntPtr context); - - [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern JsErrorCode JsSetCurrentContext(IntPtr context); - - [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] - public static extern JsErrorCode JsRunScript(string script, IntPtr sourceContext, string sourceUrl, out IntPtr result); - - [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern JsErrorCode JsConvertValueToString(IntPtr value, out IntPtr stringValue); - - [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern JsErrorCode JsStringToPointer(IntPtr value, out IntPtr buffer, out UIntPtr length); - - // Note: Unsetting is typically done via JsSetCurrentContext(IntPtr.Zero) - // Kept here only if your build exposes this symbol. - [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "JsSetCurrentContext")] - public static extern JsErrorCode JsUnSetCurrentContext(IntPtr zero); - - [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] - public static extern JsErrorCode JsDisposeRuntime(IntPtr runtime); + ~JsCore() { Dispose(); } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsNative.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsNative.cs new file mode 100644 index 0000000..3f44823 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsNative.cs @@ -0,0 +1,93 @@ +using System; +using System.Runtime.InteropServices; + +namespace WelsonJS.Launcher +{ + public static class JsNative + { + // === Enums / handles === + [Flags] + public enum JsRuntimeAttributes : uint + { + None = 0x00000000, + DisableBackgroundWork = 0x00000001, + AllowScriptInterrupt = 0x00000002, + EnableIdleProcessing = 0x00000004, + DisableNativeCodeGeneration = 0x00000008, + EnableExperimentalFeatures = 0x00000010, + } + + // ChakraCore typedefs are opaque pointers; represent as IntPtr + public struct JsRuntime { public IntPtr Handle; } + public struct JsContext { public IntPtr Handle; } + public struct JsValue { public IntPtr Handle; } + + // JsErrorCode (essential subset; expand as needed) + public enum JsErrorCode + { + JsNoError = 0, + + // Usage + JsErrorInvalidArgument = 0x10001, + JsErrorNullArgument = 0x10002, + JsErrorNoCurrentContext = 0x10003, + JsErrorInExceptionState = 0x10004, + JsErrorNotImplemented = 0x10005, + JsErrorWrongThread = 0x10006, + JsErrorRuntimeInUse = 0x10007, + + // Script + JsErrorScriptException = 0x30001, + JsErrorScriptCompile = 0x30002, + JsErrorScriptTerminated = 0x30003, + + // Engine + JsErrorOutOfMemory = 0x20001, + + // Fatal + JsErrorFatal = 0x40001, + } + + // Thread service callback: __stdcall + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate JsErrorCode JsThreadServiceCallback(IntPtr callback, IntPtr callbackState); + + // ======= FIXED SIGNATURES (StdCall + Unicode) ======= + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)] + public static extern JsErrorCode JsCreateRuntime( + JsRuntimeAttributes attributes, + JsThreadServiceCallback threadService, // pass null if unused + out JsRuntime runtime); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)] + public static extern JsErrorCode JsCreateContext( + JsRuntime runtime, + out JsContext newContext); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)] + public static extern JsErrorCode JsSetCurrentContext(JsContext context); + + // JsSourceContext is size_t → UIntPtr; strings are wide-char + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode)] + public static extern JsErrorCode JsRunScript( + string script, + UIntPtr sourceContext, + string sourceUrl, + out JsValue result); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)] + public static extern JsErrorCode JsConvertValueToString(JsValue value, out JsValue stringValue); + + // Returns pointer to UTF-16 buffer + length (size_t) for a JsString value + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)] + public static extern JsErrorCode JsStringToPointer( + JsValue value, + out IntPtr buffer, + out UIntPtr length); + + // Unset by passing "invalid" context (JS_INVALID_REFERENCE is typically null) + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.StdCall)] + public static extern JsErrorCode JsDisposeRuntime(JsRuntime runtime); + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 6d23016..be77132 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -60,13 +60,13 @@ namespace WelsonJS.Launcher }, TaskScheduler.Default); // Add resource tools - _tools.Add(new ResourceTools.Completion(this, _httpClient)); - _tools.Add(new ResourceTools.Settings(this, _httpClient)); - _tools.Add(new ResourceTools.ChromiumDevTools(this, _httpClient)); - _tools.Add(new ResourceTools.DnsQuery(this, _httpClient)); - _tools.Add(new ResourceTools.IpQuery(this, _httpClient)); - _tools.Add(new ResourceTools.TwoFactorAuth(this, _httpClient)); - _tools.Add(new ResourceTools.Whois(this, _httpClient)); + _tools.Add(new ResourceTools.Completion(this, _httpClient, _logger)); + _tools.Add(new ResourceTools.Settings(this, _httpClient, _logger)); + _tools.Add(new ResourceTools.ChromiumDevTools(this, _httpClient, _logger)); + _tools.Add(new ResourceTools.DnsQuery(this, _httpClient, _logger)); + _tools.Add(new ResourceTools.IpQuery(this, _httpClient, _logger)); + _tools.Add(new ResourceTools.TwoFactorAuth(this, _httpClient, _logger)); + _tools.Add(new ResourceTools.Whois(this, _httpClient, _logger)); // Register the prefix _listener.Prefixes.Add(prefix); diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs index 81feeb3..d56c5ec 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs @@ -19,13 +19,16 @@ namespace WelsonJS.Launcher.ResourceTools { private readonly ResourceServer Server; private readonly HttpClient _httpClient; + private readonly ICompatibleLogger _logger; private readonly WebSocketManager _wsManager = new WebSocketManager(); private const string Prefix = "devtools/"; - public ChromiumDevTools(ResourceServer server, HttpClient httpClient) + public ChromiumDevTools(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger) { Server = server; + _httpClient = httpClient; + _logger = logger; } public bool CanHandle(string path) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs index 57bb482..34abe58 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs @@ -22,13 +22,16 @@ namespace WelsonJS.Launcher.ResourceTools { private readonly ResourceServer Server; private readonly HttpClient _httpClient; + private readonly ICompatibleLogger _logger; private const string Prefix = "completion/"; private readonly ConcurrentBag DiscoveredExecutables = new ConcurrentBag(); - public Completion(ResourceServer server, HttpClient httpClient) + public Completion(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger) { Server = server; + _httpClient = httpClient; + _logger = logger; Task.Run(async () => await SafeDiscoverAsync(DiscoverFromInstalledSoftware)); Task.Run(async () => await SafeDiscoverAsync(DiscoverFromPathVariable)); @@ -160,7 +163,7 @@ namespace WelsonJS.Launcher.ResourceTools { if (!Directory.Exists(path)) { - Trace.TraceInformation("Directory does not exist: {0}", path); + _logger.Info("Directory does not exist: {0}", path); return; } @@ -175,7 +178,7 @@ namespace WelsonJS.Launcher.ResourceTools } catch (Exception ex) { - Trace.TraceInformation("Error enumerating executables in '{0}': {1}", path, ex.Message); + _logger.Info("Error enumerating executables in '{0}': {1}", path, ex.Message); } } @@ -195,7 +198,7 @@ namespace WelsonJS.Launcher.ResourceTools } catch (Exception ex) { - Trace.TraceError($"Discovery failed: {ex.Message}"); + _logger.Error($"Discovery failed: {ex.Message}"); } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs index 3b4c8b1..831294b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs @@ -18,16 +18,19 @@ namespace WelsonJS.Launcher.ResourceTools { private readonly ResourceServer Server; private readonly HttpClient _httpClient; + private readonly ICompatibleLogger _logger; private const string Prefix = "dns-query/"; private string DnsServer; private const int DnsPort = 53; private const int Timeout = 5000; private static readonly Random _random = new Random(); - public DnsQuery(ResourceServer server, HttpClient httpClient) + public DnsQuery(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger) { Server = server; + _httpClient = httpClient; + _logger = logger; DnsServer = Program.GetAppConfig("DnsServerAddress"); } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs index 3a3aa6b..68d7400 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs @@ -14,12 +14,15 @@ namespace WelsonJS.Launcher.ResourceTools { private readonly ResourceServer Server; private readonly HttpClient _httpClient; + private readonly ICompatibleLogger _logger; private const string Prefix = "ip-query/"; - public IpQuery(ResourceServer server, HttpClient httpClient) + public IpQuery(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger) { Server = server; + _httpClient = httpClient; + _logger = logger; } public bool CanHandle(string path) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs index 2f74940..6d6fd94 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs @@ -20,12 +20,15 @@ namespace WelsonJS.Launcher.ResourceTools { private readonly ResourceServer Server; private readonly HttpClient _httpClient; + private readonly ICompatibleLogger _logger; private const string Prefix = "settings"; - public Settings(ResourceServer server, HttpClient httpClient) + public Settings(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger) { Server = server; + _httpClient = httpClient; + _logger = logger; } public bool CanHandle(string path) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs index e7094e2..aa44ba6 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs @@ -19,14 +19,17 @@ namespace WelsonJS.Launcher.ResourceTools { private readonly ResourceServer Server; private readonly HttpClient _httpClient; + private readonly ICompatibleLogger _logger; private const string Prefix = "tfa/"; private const string Base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; private static readonly int[] ValidKeyCharLengths = new[] { 16, 32 }; - public TwoFactorAuth(ResourceServer server, HttpClient httpClient) + public TwoFactorAuth(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger) { Server = server; + _httpClient = httpClient; + _logger = logger; } public bool CanHandle(string path) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs index 45ae8b9..fc82e85 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs @@ -15,12 +15,15 @@ namespace WelsonJS.Launcher.ResourceTools { private readonly ResourceServer Server; private readonly HttpClient _httpClient; + private readonly ICompatibleLogger _logger; private const string Prefix = "whois/"; - public Whois(ResourceServer server, HttpClient httpClient) + public Whois(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger) { Server = server; + _httpClient = httpClient; + _logger = logger; } public bool CanHandle(string path) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/TraceLogger.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/TraceLogger.cs index 5f86237..6efbaf6 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/TraceLogger.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/TraceLogger.cs @@ -6,7 +6,9 @@ // We use the ICompatibleLogger interface to maintain a BCL-first style. // This allows for later replacement with logging libraries such as ILogger or Log4Net. // +using System; using System.Diagnostics; +using System.Linq; namespace WelsonJS.Launcher { @@ -21,7 +23,7 @@ namespace WelsonJS.Launcher _logFileName = (typeof(TraceLogger).Namespace ?? "WelsonJS.Launcher") + ".log"; Trace.Listeners.Add(new TextWriterTraceListener(_logFileName)); } - catch (System.Exception ex) + catch (Exception ex) { // Fallback when the process cannot write to the working directory Trace.Listeners.Add(new ConsoleTraceListener()); @@ -30,8 +32,27 @@ namespace WelsonJS.Launcher Trace.AutoFlush = true; } - public void Info(string message) => Trace.TraceInformation(message); - public void Warn(string message) => Trace.TraceWarning(message); - public void Error(string message) => Trace.TraceError(message); + public void Info(params object[] args) => Trace.TraceInformation(Format(args)); + public void Warn(params object[] args) => Trace.TraceWarning(Format(args)); + public void Error(params object[] args) => Trace.TraceError(Format(args)); + + private static string Format(object[] args) + { + if (args == null || args.Length == 0) return string.Empty; + + if (args.Length == 1) + return args[0]?.ToString() ?? string.Empty; + + string format = args[0]?.ToString() ?? string.Empty; + try + { + return string.Format(format, args.Skip(1).ToArray()); + } + catch + { + // In case of mismatched format placeholders + return string.Join(" ", args); + } + } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index c76f31e..a5b2e8e 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -89,8 +89,10 @@ + + From 2db47bca9a6459c5dcbb315f10f90e65a3eae061 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 28 Sep 2025 00:48:58 +0900 Subject: [PATCH 6/8] Add optional Authenticode signature validation for native DLLs Enhanced NativeBootstrap to support optional Authenticode signature validation and custom certificate validators when loading native libraries. Added 'NativeRequireSigned' configuration to app.config and resources, allowing signature enforcement to be toggled. Updated Program.cs to use the new option during initialization. --- .../WelsonJS.Launcher/NativeBootstrap.cs | 107 +++++++++++++++++- WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 12 +- .../Properties/Resources.Designer.cs | 9 ++ .../Properties/Resources.resx | 3 + .../WelsonJS.Launcher.csproj | 1 - WelsonJS.Toolkit/WelsonJS.Launcher/app.config | 1 + 6 files changed, 123 insertions(+), 10 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs index 1b91330..609169b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs @@ -4,10 +4,11 @@ // https://github.com/gnh1201/welsonjs // using System; +using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.InteropServices; -using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; namespace WelsonJS.Launcher { @@ -33,9 +34,18 @@ namespace WelsonJS.Launcher /// 1) %APPDATA%\{appDataSubdirectory}\{dllName} /// 2) Application base directory\{dllName} /// + /// Signatures: + /// - requireSigned = true : Only loads DLLs with a valid Authenticode chain. + /// - certValidator != null : Additional custom validation (e.g., pinning). + /// /// Must be called before any P/Invoke usage. /// - public static void Init(IEnumerable dllNames, string appDataSubdirectory, ICompatibleLogger logger) + public static void Init( + IEnumerable dllNames, + string appDataSubdirectory, + ICompatibleLogger logger, + bool requireSigned = false, + Func certValidator = null) { if (dllNames == null) throw new ArgumentNullException(nameof(dllNames)); if (logger == null) throw new ArgumentNullException(nameof(logger)); @@ -60,12 +70,12 @@ namespace WelsonJS.Launcher // 1) %APPDATA% subdirectory string candidate1 = Path.Combine(appDataPath, dllName); triedPaths.Add(candidate1); - if (TryLoad(candidate1, logger)) return; + if (TryLoad(candidate1, logger, requireSigned, certValidator)) return; // 2) Application base directory string candidate2 = Path.Combine(appBaseDirectory, dllName); triedPaths.Add(candidate2); - if (TryLoad(candidate2, logger)) return; + if (TryLoad(candidate2, logger, requireSigned, certValidator)) return; } string message = "Failed to load requested native libraries.\n" + @@ -74,7 +84,11 @@ namespace WelsonJS.Launcher throw new FileNotFoundException(message); } - private static bool TryLoad(string fullPath, ICompatibleLogger logger) + private static bool TryLoad( + string fullPath, + ICompatibleLogger logger, + bool requireSigned, + Func certValidator) { try { @@ -84,6 +98,14 @@ namespace WelsonJS.Launcher return false; } + // Optional signature validation + if (!ValidateSignatureIfRequired(fullPath, requireSigned, certValidator, logger)) + { + // If requireSigned=false we never reach here (it would return true). + logger.Warn($"Signature validation failed: {fullPath}"); + return false; + } + string directoryPath = Path.GetDirectoryName(fullPath) ?? AppContext.BaseDirectory; if (!TryRegisterSearchDirectory(directoryPath, logger)) { @@ -109,6 +131,81 @@ namespace WelsonJS.Launcher } } + /// + /// If requireSigned=false, returns true (no check). + /// If requireSigned=true, verifies Authenticode chain and optional custom validator. + /// + private static bool ValidateSignatureIfRequired( + string path, + bool requireSigned, + Func certValidator, + ICompatibleLogger logger) + { + if (!requireSigned) + { + // No signature requirement: allow loading regardless of signature. + return true; + } + + try + { + // Throws on unsigned files. + var baseCert = X509Certificate.CreateFromSignedFile(path); + if (baseCert == null) + { + logger.Warn("No certificate extracted from file."); + return false; + } + + var cert = new X509Certificate2(baseCert); + + var chain = new X509Chain + { + ChainPolicy = + { + RevocationMode = X509RevocationMode.Online, + RevocationFlag = X509RevocationFlag.ExcludeRoot, + VerificationFlags = X509VerificationFlags.NoFlag, + VerificationTime = DateTime.UtcNow + } + }; + + bool chainOk = chain.Build(cert); + if (!chainOk) + { + foreach (var status in chain.ChainStatus) + logger.Warn($"Cert chain status: {status.Status} - {status.StatusInformation?.Trim()}"); + return false; + } + + // Optional extra validation, e.g. thumbprint or subject pinning. + if (certValidator != null) + { + bool ok = false; + try { ok = certValidator(cert); } + catch (Exception ex) + { + logger.Warn($"Custom certificate validator threw: {ex.Message}"); + return false; + } + + if (!ok) + { + logger.Warn("Custom certificate validator rejected the certificate."); + return false; + } + } + + logger.Info($"Signature validated. Subject='{cert.Subject}', Thumbprint={cert.Thumbprint}"); + return true; + } + catch (Exception ex) + { + logger.Warn($"Signature check failed: {ex.Message}"); + return false; + } + } + private static bool TryRegisterSearchDirectory(string directoryPath, ICompatibleLogger logger) { try diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 485a3f3..682a4f4 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -15,7 +15,6 @@ namespace WelsonJS.Launcher { internal static class Program { - private const string _appDataSubDirectory = "WelsonJS"; private static readonly ICompatibleLogger _logger; public static Mutex _mutex; @@ -27,9 +26,14 @@ namespace WelsonJS.Launcher _logger = new TraceLogger(); // load native libraries - NativeBootstrap.Init(new string[] { - "ChakraCore.dll" - }, _appDataSubDirectory, _logger); + string appDataSubDirectory = "WelsonJS"; + bool requireSigned = GetAppConfig("NativeRequireSigned").Equals("true"); + NativeBootstrap.Init( + dllNames: new[] { "ChakraCore.dll" }, + appDataSubdirectory: appDataSubDirectory, + logger: _logger, + requireSigned: requireSigned + ); } [STAThread] diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index 35d0713..fdc2c3d 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -286,6 +286,15 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// false과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string NativeRequireSigned { + get { + return ResourceManager.GetString("NativeRequireSigned", resourceCulture); + } + } + /// /// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index d198f04..dc3dc74 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -208,4 +208,7 @@ 5 + + false + \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index a5b2e8e..0d5a5fc 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -92,7 +92,6 @@ - diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index 8078a76..a26dab3 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -21,6 +21,7 @@ + From 91f19bc14825b52346b20e9fdca63f34bb88a02e Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 28 Sep 2025 01:02:35 +0900 Subject: [PATCH 7/8] Update WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 682a4f4..b15d93a 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -27,7 +27,11 @@ namespace WelsonJS.Launcher // load native libraries string appDataSubDirectory = "WelsonJS"; - bool requireSigned = GetAppConfig("NativeRequireSigned").Equals("true"); + bool requireSigned = string.Equals( + GetAppConfig("NativeRequireSigned"), + "true", + StringComparison.OrdinalIgnoreCase); + NativeBootstrap.Init( dllNames: new[] { "ChakraCore.dll" }, appDataSubdirectory: appDataSubDirectory, From 9547d245b78b1f320b2e35b410b58efba2aa156a Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 28 Sep 2025 01:06:40 +0900 Subject: [PATCH 8/8] Update WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs index b82b1a5..5eddd2d 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs @@ -123,6 +123,8 @@ namespace WelsonJS.Launcher case '\n': sb.Append(@"\n"); break; case '\r': sb.Append(@"\r"); break; case '\t': sb.Append(@"\t"); break; + case '\u2028': sb.Append("\\u2028"); break; + case '\u2029': sb.Append("\\u2029"); break; default: if (char.IsControl(ch)) {