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.
This commit is contained in:
Namhyeon Go 2025-09-28 00:07:41 +09:00
parent 83a037dfa2
commit 87020d35ac
13 changed files with 187 additions and 150 deletions

View File

@ -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);
}
}

View File

@ -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");
}
/// <summary>
/// Evaluates JavaScript and returns the result converted to string (via JsConvertValueToString).
/// </summary>
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);
}
/// <summary>
/// Evaluates JavaScript for side effects; discards the result.
/// </summary>
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 ChakraCores 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(); }
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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)

View File

@ -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<string> DiscoveredExecutables = new ConcurrentBag<string>();
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}");
}
}
}

View File

@ -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");
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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);
}
}
}
}

View File

@ -89,8 +89,10 @@
<Compile Include="ICompatibleLogger.cs" />
<Compile Include="IResourceTool.cs" />
<Compile Include="JsCore.cs" />
<Compile Include="JsNative.cs" />
<Compile Include="JsSerializer.cs" />
<Compile Include="NativeBootstrap.cs" />
<Compile Include="ResourceTools\ImageProxy.cs" />
<Compile Include="ResourceTools\IpQuery.cs" />
<Compile Include="ResourceTools\Settings.cs" />
<Compile Include="ResourceTools\Completion.cs" />