diff --git a/.appveyor.yml b/.appveyor.yml index 8a2822a..aac253a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -27,6 +27,7 @@ after_build: - 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: @@ -45,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 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 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 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 new file mode 100644 index 0000000..53af2b5 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs @@ -0,0 +1,71 @@ +// 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.Globalization; +using System.Runtime.InteropServices; + +namespace WelsonJS.Launcher +{ + public sealed class JsCore : IDisposable + { + private JsNative.JsRuntime _rt; + private JsNative.JsContext _ctx; + private bool _disposed; + + public JsCore() + { + Check(JsNative.JsCreateRuntime(JsNative.JsRuntimeAttributes.None, null, out _rt), "JsCreateRuntime"); + Check(JsNative.JsCreateContext(_rt, out _ctx), "JsCreateContext"); + Check(JsNative.JsSetCurrentContext(_ctx), "JsSetCurrentContext"); + } + + public string EvaluateToString(string script, string sourceUrl = "repl") + { + if (_disposed) throw new ObjectDisposedException(nameof(JsCore)); + if (script == null) throw new ArgumentNullException(nameof(script)); + + JsNative.JsValue result; + Check(JsNative.JsRunScript(script, UIntPtr.Zero, sourceUrl, out result), "JsRunScript"); + + JsNative.JsValue jsStr; + Check(JsNative.JsConvertValueToString(result, out jsStr), "JsConvertValueToString"); + + IntPtr p; + UIntPtr len; + Check(JsNative.JsStringToPointer(jsStr, out p, out len), "JsStringToPointer"); + + int chars = checked((int)len); + return Marshal.PtrToStringUni(p, chars); + } + + private static void Check(JsNative.JsErrorCode code, string op) + { + if (code != JsNative.JsErrorCode.JsNoError) + throw new InvalidOperationException(op + " failed: " + code + " (0x" + ((int)code).ToString("X", CultureInfo.InvariantCulture) + ")"); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + try + { + // Unset current context + JsNative.JsSetCurrentContext(new JsNative.JsContext { Handle = IntPtr.Zero }); + } + catch { /* ignore */ } + finally + { + if (_rt.Handle != IntPtr.Zero) + JsNative.JsDisposeRuntime(_rt); + } + GC.SuppressFinalize(this); + } + + ~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/JsSerializer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs new file mode 100644 index 0000000..5eddd2d --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs @@ -0,0 +1,295 @@ +// 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; + case '\u2028': sb.Append("\\u2028"); break; + case '\u2029': sb.Append("\\u2029"); 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..609169b --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs @@ -0,0 +1,244 @@ +// 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.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; + +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} + /// + /// 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, + bool requireSigned = false, + Func certValidator = null) + { + 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, requireSigned, certValidator)) return; + + // 2) Application base directory + string candidate2 = Path.Combine(appBaseDirectory, dllName); + triedPaths.Add(candidate2); + if (TryLoad(candidate2, logger, requireSigned, certValidator)) 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, + bool requireSigned, + Func certValidator) + { + try + { + if (!File.Exists(fullPath)) + { + logger.Info($"Not found: {fullPath}"); + 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)) + { + 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; + } + } + + /// + /// 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 + { + 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..b15d93a 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -22,7 +22,22 @@ namespace WelsonJS.Launcher static Program() { + // set up logger _logger = new TraceLogger(); + + // load native libraries + string appDataSubDirectory = "WelsonJS"; + bool requireSigned = string.Equals( + GetAppConfig("NativeRequireSigned"), + "true", + StringComparison.OrdinalIgnoreCase); + + 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/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 e9a04b4..0d5a5fc 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -88,6 +88,10 @@ + + + + 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 @@ +