Merge pull request #328 from gnh1201/dev

ChakraCore and JS (like a JSON) serialization
This commit is contained in:
Namhyeon Go 2025-09-28 01:43:08 +09:00 committed by GitHub
commit ccfd8c37ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 807 additions and 25 deletions

View File

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

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "WelsonJS.Toolkit/ChakraCore"]
path = WelsonJS.Toolkit/ChakraCore
url = https://github.com/chakra-core/ChakraCore

@ -0,0 +1 @@
Subproject commit 36becec43348f259e8bee08cf2fcd171bfe56f42

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

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

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

@ -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);
}
/// <summary>
/// Extracts a value by a simple path of property names (numeric segment as string = array index).
/// Returns the selected value as a JSON string.
/// </summary>
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);
}
/// <summary>
/// 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")
/// </summary>
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<p.length;i++){var k=p[i];");
sb.Append("if(Array.isArray(v) && typeof k==='number'){ v=v[k]; }");
sb.Append("else { v=(v==null?null:v[k]); }}");
sb.Append("return JSON.stringify(v);})()");
return _core.EvaluateToString(sb.ToString());
}
public string Serialize(object value, int space)
{
space = Clamp(space, 0, 10);
string expr = BuildJsExpression(value, new HashSet<object>(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;
}
/// <summary>
/// Encode a .NET string as a JS double-quoted string literal.
/// </summary>
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();
}
/// <summary>
/// Builds a JS array literal representing the path.
/// Numeric segments are emitted as numbers; others as strings.
/// </summary>
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;
}
/// <summary>
/// Builds a safe JS expression for a .NET value (no engine calls here).
/// Engine will stringify the produced expression via JSON.stringify.
/// </summary>
private static string BuildJsExpression(object value, HashSet<object> 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();
}
/// <summary>
/// Reference equality comparer for cycle detection (works on .NET Framework).
/// </summary>
private sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
private ReferenceEqualityComparer() { }
public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer();
bool IEqualityComparer<object>.Equals(object x, object y) { return object.ReferenceEquals(x, y); }
int IEqualityComparer<object>.GetHashCode(object obj)
{
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
}
}
}

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
public static void Init(
IEnumerable<string> dllNames,
string appDataSubdirectory,
ICompatibleLogger logger,
bool requireSigned = false,
Func<X509Certificate2, bool> 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<string>();
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<X509Certificate2, bool> 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;
}
}
/// <summary>
/// If requireSigned=false, returns true (no check).
/// If requireSigned=true, verifies Authenticode chain and optional custom validator.
/// </summary>
private static bool ValidateSignatureIfRequired(
string path,
bool requireSigned,
Func<X509Certificate2, bool> 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;
}
}
}
}

View File

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

View File

@ -286,6 +286,15 @@ namespace WelsonJS.Launcher.Properties {
}
}
/// <summary>
/// false과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string NativeRequireSigned {
get {
return ResourceManager.GetString("NativeRequireSigned", resourceCulture);
}
}
/// <summary>
/// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>

View File

@ -208,4 +208,7 @@
<data name="ChromiumDevToolsTimeout" xml:space="preserve">
<value>5</value>
</data>
<data name="NativeRequireSigned" xml:space="preserve">
<value>false</value>
</data>
</root>

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

@ -88,6 +88,10 @@
<ItemGroup>
<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\IpQuery.cs" />
<Compile Include="ResourceTools\Settings.cs" />
<Compile Include="ResourceTools\Completion.cs" />

View File

@ -21,6 +21,7 @@
<add key="CitiApiKey" value=""/>
<add key="CitiApiPrefix" value="https://api.criminalip.io/v1/"/>
<add key="DateTimeFormat" value="yyyy-MM-dd HH:mm:ss"/>
<add key="NativeRequireSigned" value="false"/>
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>