// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.Core.Runtime; using SharpEmu.Core.Cpu; using SharpEmu.GUI; using SharpEmu.HLE; using SharpEmu.Logging; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; namespace SharpEmu.CLI; internal static partial class Program { private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI"); private static readonly object ConsoleMirrorSync = new(); private static StreamWriter? _consoleMirrorFile; private const int DefaultImportTraceLimit = 32; private const string MitigatedChildFlag = "--sharpemu-mitigated-child"; private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000; private const uint INFINITE = 0xFFFFFFFF; private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007; private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; private const int JobObjectExtendedLimitInformation = 9; private const int STARTF_USESTDHANDLES = 0x00000100; private const uint HANDLE_FLAG_INHERIT = 0x00000001; private const string MitigatedChildEnvironment = "SHARPEMU_MITIGATED_CHILD"; private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40; private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28; private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32; private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40; private const int ATTACH_PARENT_PROCESS = -1; private const int STD_INPUT_HANDLE = -10; private const int STD_OUTPUT_HANDLE = -11; private const int STD_ERROR_HANDLE = -12; private const uint GENERIC_READ = 0x80000000; private const uint GENERIC_WRITE = 0x40000000; private const uint FILE_SHARE_READ = 0x00000001; private const uint FILE_SHARE_WRITE = 0x00000002; private const uint OPEN_EXISTING = 3; [STAThread] private static int Main(string[] args) { try { return Run(args); } finally { DropConsoleFileMirror(); SharpEmuLog.Shutdown(); } } private static int Run(string[] args) { args = NormalizeInternalArguments(args, out var isMitigatedChild); if (args.Length == 0 && !isMitigatedChild) { // No arguments: open the desktop frontend. Any argument selects // the classic CLI behavior below. return GuiLauncher.Run(); } // The executable uses the GUI subsystem, so CLI mode has to connect // itself to a console before the first write. EnsureCliConsole(); UseUtf8ConsoleOutput(); if (isMitigatedChild && TryGetLogFileArgument(args, out var earlyLogFilePath)) { TryEnableConsoleFileMirror(earlyLogFilePath); } Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args"); if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode)) { return childExitCode; } if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath)) { PrintUsage(); return 1; } if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath)) { TryEnableConsoleFileMirror(logFilePath); } SharpEmuLog.MinimumLevel = logLevel; Log.Info(BuildInfo.Banner); Log.Info(HostSystemInfo.Summary); ebootPath = Path.GetFullPath(ebootPath); Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}"); if (!File.Exists(ebootPath)) { Log.Error($"EBOOT file was not found: {ebootPath}"); return 2; } Console.Error.WriteLine("[DEBUG] Creating runtime..."); using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions); OrbisGen2Result result; try { Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}"); result = runtime.Run(ebootPath); Console.Error.WriteLine($"[DEBUG] Result: {result}"); } catch (Exception ex) { Console.Error.WriteLine($"[DEBUG] Exception: {ex}"); Log.Error("SharpEmu failed to run.", ex); return 3; } Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})"); if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary)) { Log.Info(runtime.LastSessionSummary); } if (!string.IsNullOrWhiteSpace(runtime.LastBasicBlockTrace)) { Log.Info("BB trace:"); Log.Info(runtime.LastBasicBlockTrace); } if (!string.IsNullOrWhiteSpace(runtime.LastMilestoneLog)) { Log.Info(runtime.LastMilestoneLog); } if (result != OrbisGen2Result.ORBIS_GEN2_OK && !string.IsNullOrWhiteSpace(runtime.LastExecutionDiagnostics)) { Log.Warn(runtime.LastExecutionDiagnostics); } if (runtimeOptions.ImportTraceLimit > 0 && !string.IsNullOrWhiteSpace(runtime.LastExecutionTrace)) { Log.Info("Import trace:"); Log.Info(runtime.LastExecutionTrace); } return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4; } private static void EnsureCliConsole() { if (!OperatingSystem.IsWindows()) { return; } // Standard handles already provided (pipes or file redirection, e.g. // when the GUI or a script launches us): use them as-is. if (IsHandleValid(GetStdHandle(STD_OUTPUT_HANDLE)) && IsHandleValid(GetStdHandle(STD_ERROR_HANDLE))) { return; } // Prefer the console of the parent process (interactive terminal); // create one only when started with arguments but no terminal at all // (e.g. a shortcut), so usage and errors remain visible. if (!AttachConsole(ATTACH_PARENT_PROCESS) && GetConsoleWindow() == 0) { _ = AllocConsole(); } RebindStdHandleToConsole(STD_OUTPUT_HANDLE); RebindStdHandleToConsole(STD_ERROR_HANDLE); } /// /// Makes console writes UTF-8 so the GUI's pipe reader (and any modern /// terminal) decodes non-ASCII text correctly. Without this, redirected /// output falls back to the OS ANSI code page and characters like "—" /// arrive mangled. /// private static void UseUtf8ConsoleOutput() { try { // Also recreates the redirected Console.Out/Error writers with // the new encoding. Console.OutputEncoding = Encoding.UTF8; return; } catch (Exception) { // No attached console (GUI-subsystem child with piped output): // wrap the raw handles instead. } if (Console.IsOutputRedirected) { Console.SetOut(new StreamWriter( Console.OpenStandardOutput(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true, }); } if (Console.IsErrorRedirected) { Console.SetError(new StreamWriter( Console.OpenStandardError(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true, }); } } private static void RebindStdHandleToConsole(int stdHandle) { if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0) { return; } var conOut = CreateFileW( "CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0); if (IsHandleValid(conOut)) { _ = SetStdHandle(stdHandle, conOut); } } private static bool IsHandleValid(nint handle) { return handle != 0 && handle != -1; } private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild) { isMitigatedChild = false; var trustedMitigatedChild = string.Equals( Environment.GetEnvironmentVariable(MitigatedChildEnvironment), "1", StringComparison.Ordinal); if (args.Length == 0) { return args; } var list = new List(args.Length); foreach (var arg in args) { if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal)) { isMitigatedChild = trustedMitigatedChild; continue; } list.Add(arg); } return list.ToArray(); } private static bool TryRunMitigatedChild(string[] args, out int childExitCode) { childExitCode = 0; if (!OperatingSystem.IsWindows()) { return false; } if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_MITIGATION_RELAUNCH"), "1", StringComparison.Ordinal)) { return false; } var processPath = Environment.ProcessPath; if (string.IsNullOrWhiteSpace(processPath)) { return false; } var childArgs = new string[args.Length + 1]; childArgs[0] = MitigatedChildFlag; for (var i = 0; i < args.Length; i++) { childArgs[i + 1] = args[i]; } var commandLine = BuildCommandLine(processPath, childArgs); var startupInfoEx = new STARTUPINFOEX(); startupInfoEx.StartupInfo.cb = Marshal.SizeOf(); ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo); nint attributeList = 0; nint mitigationPolicies = 0; var previousChildEnvironment = Environment.GetEnvironmentVariable(MitigatedChildEnvironment); try { nuint attributeListSize = 0; _ = InitializeProcThreadAttributeList(0, 1, 0, ref attributeListSize); attributeList = Marshal.AllocHGlobal((nint)attributeListSize); if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize)) { childExitCode = 5; Console.Error.WriteLine($"[ERROR] Failed to initialize mitigation attributes: {Marshal.GetLastWin32Error()}"); return true; } startupInfoEx.lpAttributeList = attributeList; var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF; var policy2 = PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF | PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF; mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2); Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1)); Marshal.WriteInt64(nint.Add(mitigationPolicies, sizeof(long)), unchecked((long)policy2)); if (!UpdateProcThreadAttribute( attributeList, 0, (nint)PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, mitigationPolicies, (nuint)(sizeof(ulong) * 2), 0, 0)) { childExitCode = 5; Console.Error.WriteLine($"[ERROR] Failed to apply mitigation attributes: {Marshal.GetLastWin32Error()}"); return true; } var cmdLineBuilder = new StringBuilder(commandLine); nint jobHandle = 0; Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1"); var created = CreateProcessW( processPath, cmdLineBuilder, 0, 0, true, EXTENDED_STARTUPINFO_PRESENT, 0, Environment.CurrentDirectory, ref startupInfoEx, out var processInfo); Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment); if (!created) { childExitCode = 5; Console.Error.WriteLine($"[ERROR] Failed to launch mitigated child process: {Marshal.GetLastWin32Error()}"); return true; } try { jobHandle = CreateJobObjectW(0, null); if (jobHandle != 0 && TryEnableKillOnJobClose(jobHandle) && !AssignProcessToJobObject(jobHandle, processInfo.hProcess)) { CloseHandle(jobHandle); jobHandle = 0; } ConsoleCancelEventHandler? cancelHandler = null; EventHandler? processExitHandler = null; cancelHandler = (_, eventArgs) => { _ = TerminateProcess(processInfo.hProcess, 1); eventArgs.Cancel = true; }; processExitHandler = (_, _) => { _ = TerminateProcess(processInfo.hProcess, 1); }; Console.CancelKeyPress += cancelHandler; AppDomain.CurrentDomain.ProcessExit += processExitHandler; _ = WaitForSingleObject(processInfo.hProcess, INFINITE); Console.CancelKeyPress -= cancelHandler; AppDomain.CurrentDomain.ProcessExit -= processExitHandler; if (!GetExitCodeProcess(processInfo.hProcess, out var exitCode)) { return false; } childExitCode = unchecked((int)exitCode); Console.Error.WriteLine("[DEBUG] Running in mitigated child process (CET/CFG disabled)."); return true; } finally { if (jobHandle != 0) { CloseHandle(jobHandle); } CloseHandle(processInfo.hThread); CloseHandle(processInfo.hProcess); } } finally { Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment); if (attributeList != 0) { DeleteProcThreadAttributeList(attributeList); Marshal.FreeHGlobal(attributeList); } if (mitigationPolicies != 0) { Marshal.FreeHGlobal(mitigationPolicies); } } } private static bool TryGetLogFileArgument(IReadOnlyList args, out string path) { for (var i = 0; i < args.Count; i++) { var argument = args[i]; if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase)) { if (i + 1 < args.Count && !string.IsNullOrWhiteSpace(args[i + 1]) && !args[i + 1].StartsWith("--", StringComparison.Ordinal) && ShouldConsumeLogFilePath(args, i + 1)) { path = args[i + 1]; return true; } path = BuildDefaultLogFilePath(TryFindEbootPathToken(args)); return true; } const string logFilePrefix = "--log-file="; if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(argument[logFilePrefix.Length..])) { path = argument[logFilePrefix.Length..]; return true; } } path = string.Empty; return false; } private static string BuildDefaultLogFilePath(string? ebootPath) { var baseDirectory = AppContext.BaseDirectory; var logsDirectory = Path.Combine(baseDirectory, "user", "logs"); var name = TryReadTitleId(ebootPath) ?? "UNKNOWN"; foreach (var invalid in Path.GetInvalidFileNameChars()) { name = name.Replace(invalid, '_'); } return Path.Combine(logsDirectory, $"{name}-{DateTime.Now:yyyyMMdd-HHmmss}.log"); } private static string? TryReadTitleId(string? ebootPath) { if (string.IsNullOrWhiteSpace(ebootPath)) { return null; } try { var directory = Path.GetDirectoryName(Path.GetFullPath(ebootPath)); if (string.IsNullOrEmpty(directory)) { return null; } foreach (var paramPath in new[] { Path.Combine(directory, "sce_sys", "param.json"), Path.Combine(directory, "param.json"), }) { if (!File.Exists(paramPath)) { continue; } using var stream = File.OpenRead(paramPath); using var document = JsonDocument.Parse(stream); if (document.RootElement.TryGetProperty("titleId", out var titleIdElement) && titleIdElement.ValueKind == JsonValueKind.String) { var titleId = titleIdElement.GetString(); if (!string.IsNullOrWhiteSpace(titleId)) { return titleId.Trim(); } } } } catch { // Logging should never block launch; unknown title ids use a stable fallback. } return null; } private static string? TryFindEbootPathToken(IReadOnlyList args) { for (var i = args.Count - 1; i >= 0; i--) { var argument = args[i]; if (string.IsNullOrWhiteSpace(argument) || argument.StartsWith("--", StringComparison.Ordinal)) { continue; } return argument; } return null; } private static bool ShouldConsumeLogFilePath(IReadOnlyList args, int candidateIndex) { var candidate = args[candidateIndex]; if (LooksLikeLogFilePath(candidate)) { return true; } for (var i = candidateIndex + 1; i < args.Count; i++) { var argument = args[i]; if (!string.IsNullOrWhiteSpace(argument) && !argument.StartsWith("--", StringComparison.Ordinal)) { return true; } } return false; } private static bool LooksLikeLogFilePath(string path) { var extension = Path.GetExtension(path); return string.Equals(extension, ".log", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".txt", StringComparison.OrdinalIgnoreCase); } private static void TryEnableConsoleFileMirror(string path) { lock (ConsoleMirrorSync) { if (_consoleMirrorFile is not null) { return; } try { var directory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } var stream = new FileStream( path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite, bufferSize: 4096, FileOptions.SequentialScan); _consoleMirrorFile = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true, }; Console.SetOut(new TeeTextWriter(Console.Out, _consoleMirrorFile)); Console.SetError(new TeeTextWriter(Console.Error, _consoleMirrorFile)); Console.Error.WriteLine($"[DEBUG] Log file: {Path.GetFullPath(path)}"); } catch (Exception ex) { Console.Error.WriteLine($"[WARN] Could not open log file '{path}': {ex.Message}"); } } } private static void DropConsoleFileMirror() { lock (ConsoleMirrorSync) { try { _consoleMirrorFile?.Flush(); _consoleMirrorFile?.Dispose(); } catch { } _consoleMirrorFile = null; } } private static void ConfigureInheritedStdHandles(ref STARTUPINFO startupInfo) { if (!OperatingSystem.IsWindows()) { return; } var input = GetStdHandle(STD_INPUT_HANDLE); var output = GetStdHandle(STD_OUTPUT_HANDLE); var error = GetStdHandle(STD_ERROR_HANDLE); if (!IsHandleValid(output) && !IsHandleValid(error)) { return; } if (IsHandleValid(input)) { _ = SetHandleInformation(input, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); startupInfo.hStdInput = input; } if (IsHandleValid(output)) { _ = SetHandleInformation(output, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); startupInfo.hStdOutput = output; } if (IsHandleValid(error)) { _ = SetHandleInformation(error, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); startupInfo.hStdError = error; } startupInfo.dwFlags |= STARTF_USESTDHANDLES; } private static string BuildCommandLine(string processPath, IReadOnlyList args) { var builder = new StringBuilder(); builder.Append(QuoteArgument(processPath)); for (var i = 0; i < args.Count; i++) { builder.Append(' '); builder.Append(QuoteArgument(args[i])); } return builder.ToString(); } private static bool TryEnableKillOnJobClose(nint jobHandle) { var extendedLimitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION { BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION { LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, }, }; var size = Marshal.SizeOf(); var memory = Marshal.AllocHGlobal(size); try { Marshal.StructureToPtr(extendedLimitInfo, memory, false); return SetInformationJobObject( jobHandle, JobObjectExtendedLimitInformation, memory, unchecked((uint)size)); } finally { Marshal.FreeHGlobal(memory); } } private static string QuoteArgument(string argument) { if (argument.Length == 0) { return "\"\""; } var needsQuotes = false; foreach (var c in argument) { if (char.IsWhiteSpace(c) || c == '"') { needsQuotes = true; break; } } if (!needsQuotes) { return argument; } var builder = new StringBuilder(argument.Length + 2); builder.Append('"'); var backslashCount = 0; foreach (var c in argument) { if (c == '\\') { backslashCount++; continue; } if (c == '"') { builder.Append('\\', (backslashCount * 2) + 1); builder.Append('"'); backslashCount = 0; continue; } if (backslashCount > 0) { builder.Append('\\', backslashCount); backslashCount = 0; } builder.Append(c); } if (backslashCount > 0) { builder.Append('\\', backslashCount * 2); } builder.Append('"'); return builder.ToString(); } private static void PrintUsage() { Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=] [--log-level=] [--log-file[=]] "); Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin"""); } private static bool TryParseArguments( string[] args, out string ebootPath, out SharpEmuRuntimeOptions runtimeOptions, out LogLevel logLevel, out string? logFilePath) { if (args.Length == 0) { ebootPath = string.Empty; runtimeOptions = default; logLevel = SharpEmuLog.MinimumLevel; logFilePath = null; return false; } var strictDynlibResolution = false; var importTraceLimit = 0; var cpuEngine = CpuExecutionEngine.NativeOnly; logFilePath = null; logLevel = SharpEmuLog.MinimumLevel; var pathTokens = new List(args.Length); for (var i = 0; i < args.Length; i++) { var argument = args[i]; if (string.Equals(argument, "--strict", StringComparison.OrdinalIgnoreCase)) { strictDynlibResolution = true; continue; } if (string.Equals(argument, "--trace-imports", StringComparison.OrdinalIgnoreCase)) { importTraceLimit = DefaultImportTraceLimit; if (i + 1 < args.Length && int.TryParse(args[i + 1], out var explicitLimit)) { importTraceLimit = Math.Max(0, explicitLimit); i++; } continue; } if (string.Equals(argument, "--log-level", StringComparison.OrdinalIgnoreCase)) { if (i + 1 >= args.Length || !SharpEmuLog.TryParseLevel(args[i + 1], out logLevel)) { ebootPath = string.Empty; runtimeOptions = default; logFilePath = null; return false; } i++; continue; } if (string.Equals(argument, "--cpu-engine", StringComparison.OrdinalIgnoreCase)) { if (i + 1 >= args.Length || !TryParseCpuEngine(args[i + 1], out cpuEngine)) { ebootPath = string.Empty; runtimeOptions = default; logFilePath = null; return false; } i++; continue; } if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase)) { if (i + 1 < args.Length && !string.IsNullOrWhiteSpace(args[i + 1]) && !args[i + 1].StartsWith("--", StringComparison.Ordinal) && ShouldConsumeLogFilePath(args, i + 1)) { logFilePath = args[++i]; } else { logFilePath = BuildDefaultLogFilePath(TryFindEbootPathToken(args)); } continue; } const string logLevelPrefix = "--log-level="; if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase)) { var valueText = argument[logLevelPrefix.Length..]; if (!SharpEmuLog.TryParseLevel(valueText, out logLevel)) { ebootPath = string.Empty; runtimeOptions = default; return false; } continue; } const string cpuEnginePrefix = "--cpu-engine="; if (argument.StartsWith(cpuEnginePrefix, StringComparison.OrdinalIgnoreCase)) { var valueText = argument[cpuEnginePrefix.Length..]; if (!TryParseCpuEngine(valueText, out cpuEngine)) { ebootPath = string.Empty; runtimeOptions = default; logLevel = SharpEmuLog.MinimumLevel; logFilePath = null; return false; } continue; } const string tracePrefix = "--trace-imports="; if (argument.StartsWith(tracePrefix, StringComparison.OrdinalIgnoreCase)) { var valueText = argument[tracePrefix.Length..]; if (!int.TryParse(valueText, out importTraceLimit)) { ebootPath = string.Empty; runtimeOptions = default; logLevel = SharpEmuLog.MinimumLevel; return false; } importTraceLimit = Math.Max(0, importTraceLimit); continue; } const string logFilePrefix = "--log-file="; if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase)) { logFilePath = argument[logFilePrefix.Length..]; if (string.IsNullOrWhiteSpace(logFilePath)) { ebootPath = string.Empty; runtimeOptions = default; logLevel = SharpEmuLog.MinimumLevel; return false; } continue; } if (argument.StartsWith("--", StringComparison.Ordinal)) { ebootPath = string.Empty; runtimeOptions = default; logLevel = SharpEmuLog.MinimumLevel; logFilePath = null; return false; } pathTokens.Add(argument); } if (pathTokens.Count == 0) { ebootPath = string.Empty; runtimeOptions = default; logLevel = SharpEmuLog.MinimumLevel; logFilePath = null; return false; } ebootPath = string.Join(' ', pathTokens); runtimeOptions = new SharpEmuRuntimeOptions { CpuEngine = cpuEngine, StrictDynlibResolution = strictDynlibResolution, ImportTraceLimit = importTraceLimit, }; return true; } private static bool TryParseCpuEngine(string valueText, out CpuExecutionEngine engine) { if (string.Equals(valueText, "native", StringComparison.OrdinalIgnoreCase) || string.Equals(valueText, "native-only", StringComparison.OrdinalIgnoreCase)) { engine = CpuExecutionEngine.NativeOnly; return true; } engine = CpuExecutionEngine.NativeOnly; return false; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct STARTUPINFO { public int cb; public nint lpReserved; public nint lpDesktop; public nint lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public nint lpReserved2; public nint hStdInput; public nint hStdOutput; public nint hStdError; } [StructLayout(LayoutKind.Sequential)] private struct STARTUPINFOEX { public STARTUPINFO StartupInfo; public nint lpAttributeList; } [StructLayout(LayoutKind.Sequential)] private struct PROCESS_INFORMATION { public nint hProcess; public nint hThread; public int dwProcessId; public int dwThreadId; } [StructLayout(LayoutKind.Sequential)] private struct JOBOBJECT_BASIC_LIMIT_INFORMATION { public long PerProcessUserTimeLimit; public long PerJobUserTimeLimit; public uint LimitFlags; public nuint MinimumWorkingSetSize; public nuint MaximumWorkingSetSize; public uint ActiveProcessLimit; public nint Affinity; public uint PriorityClass; public uint SchedulingClass; } [StructLayout(LayoutKind.Sequential)] private struct IO_COUNTERS { public ulong ReadOperationCount; public ulong WriteOperationCount; public ulong OtherOperationCount; public ulong ReadTransferCount; public ulong WriteTransferCount; public ulong OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo; public nuint ProcessMemoryLimit; public nuint JobMemoryLimit; public nuint PeakProcessMemoryUsed; public nuint PeakJobMemoryUsed; } private sealed class TeeTextWriter : TextWriter { private readonly TextWriter _primary; private readonly TextWriter _mirror; public TeeTextWriter(TextWriter primary, TextWriter mirror) { _primary = primary; _mirror = mirror; } public override Encoding Encoding => _primary.Encoding; public override void Write(char value) { lock (ConsoleMirrorSync) { _primary.Write(value); _mirror.Write(value); } } public override void Write(string? value) { lock (ConsoleMirrorSync) { _primary.Write(value); _mirror.Write(value); } } public override void WriteLine(string? value) { lock (ConsoleMirrorSync) { _primary.WriteLine(value); _mirror.WriteLine(value); } } public override void Flush() { lock (ConsoleMirrorSync) { _primary.Flush(); _mirror.Flush(); } } } [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool InitializeProcThreadAttributeList( nint lpAttributeList, int dwAttributeCount, int dwFlags, ref nuint lpSize); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UpdateProcThreadAttribute( nint lpAttributeList, uint dwFlags, nint attribute, nint lpValue, nuint cbSize, nint lpPreviousValue, nint lpReturnSize); [DllImport("kernel32.dll")] private static extern void DeleteProcThreadAttributeList(nint lpAttributeList); [DllImport("kernel32.dll", EntryPoint = "CreateJobObjectW", SetLastError = true, CharSet = CharSet.Unicode)] private static extern nint CreateJobObjectW(nint lpJobAttributes, string? lpName); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetInformationJobObject( nint hJob, int jobObjectInfoClass, nint lpJobObjectInfo, uint cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool AssignProcessToJobObject(nint hJob, nint hProcess); [DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CreateProcessW( string applicationName, StringBuilder commandLine, nint processAttributes, nint threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint creationFlags, nint environment, string currentDirectory, ref STARTUPINFOEX startupInfo, out PROCESS_INFORMATION processInformation); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObject(nint handle, uint milliseconds); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetExitCodeProcess(nint process, out uint exitCode); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool TerminateProcess(nint process, uint exitCode); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CloseHandle(nint handle); [DllImport("kernel32.dll")] private static extern nint GetConsoleWindow(); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool AttachConsole(int processId); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool AllocConsole(); [DllImport("kernel32.dll", SetLastError = true)] private static extern nint GetStdHandle(int stdHandle); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetStdHandle(int stdHandle, nint handle); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetHandleInformation(nint handle, uint mask, uint flags); [DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)] private static extern nint CreateFileW( string fileName, uint desiredAccess, uint shareMode, nint securityAttributes, uint creationDisposition, uint flagsAndAttributes, nint templateFile); }