mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
fa2616d224
* [macos/linux] Cross-platform host memory, TLS, and ABI layer for POSIX Introduces the foundation for running SharpEmu on macOS (osx-x64 under Rosetta 2) and Linux (linux-x64). The CPU backend executes guest x86-64 code natively, so these targets run the whole process as x86-64; this commit replaces the Windows-only host primitives with platform-dispatched equivalents so the guest boots and services HLE calls off Windows. Memory (HostMemory.cs, new): a Win32-semantics facade over mmap/mprotect/munmap with a shadow region table answering VirtualQuery. PhysicalVirtualMemory, DirectExecutionBackend, StubManager, and the two Kernel*CompatExports now go through it instead of kernel32 P/Invokes. Exact-address requests use MAP_FIXED_NOREPLACE (Linux) / guarded MAP_FIXED (macOS) so they match Win32 "map there or fail" semantics. TLS + host helpers (PosixHostStubs.cs, new): pthread-backed TLS and Win64-ABI-compatible stubs for the kernel32 helpers the backend embeds into emitted x86-64 code (TlsGetValue, QueryPerformanceCounter, SwitchToThread, Sleep). A Win64->SysV thunk wraps managed callbacks, since .NET on POSIX compiles them for the SysV ABI while the emitted call sites use Win64. Guest address layout: the 0x7FFx window is Windows-only (dyld shared cache / Rosetta runtime live there on POSIX), so stack/TLS/stub regions relocate to 0x6FFx off Windows. Vectored exception handling is gated off on POSIX for now (guest faults are not yet recovered) — the signal-based bridge is the next step. Also adds osx-x64 to the RID list and a Docker-based Linux smoke-test script. Status: on both macOS (Rosetta) and Linux (amd64), the guest now boots, runs native x86-64 code, and dispatches HLE imports. macOS stops at a Rosetta translation-cache issue; Linux runs ~252 imports through C++ static-init before hitting the missing fault handler (SIGSEGV). * [posix] Bridge the vectored exception handler to sigaction(SIGSEGV/SIGBUS/SIGILL) Guest faults on macOS/Linux previously terminated the process because the recovery logic in DirectExecutionBackend.Exceptions.cs was Windows-only. This adds a POSIX front-end that reuses the existing handler bodies: - DirectExecutionBackend.PosixSignals.cs installs SA_SIGINFO handlers via an [UnmanagedCallersOnly] entry, rebuilds the Win64 EXCEPTION_POINTERS / CONTEXT view from the platform mcontext (Darwin __ss thread state via the mcontext pointer at ucontext+48, Linux glibc gregs at ucontext+40 -- offsets verified against the headers on both platforms), runs the same chain as the VEH path (TryRecoverUnresolvedSentinel trap-sentinel recovery, TryHandleLazyCommittedPage demand paging, VectoredHandler diagnostics incl. FS/GS TLS-fault detection), and writes register changes back into the mcontext so sigreturn resumes the repaired guest. Unrecovered faults chain to the previously installed handler so the .NET runtime keeps mapping its own faults to managed exceptions. - The whole recovery path is warmed up with fabricated inputs before the handlers are installed. This is required under Rosetta 2: the signal trampoline cannot enter x86 code that has never been executed (and so never translated) -- a cold handler is silently never invoked and the faulting instruction retries forever (reproduced and verified in an isolated .NET test under Rosetta for Linux). It also keeps first-fault JIT work out of the signal frame. - Handlers run without SA_ONSTACK: the runtime's alternate stacks are too small for the diagnostic path, while guest (2MB) and host thread stacks match where Windows dispatches exceptions anyway. - The raw reads in the shared fault diagnostics (stack qwords, RBP walk, code bytes at RIP) now probe the region table on POSIX before touching memory, since a nested SIGSEGV inside the handler would kill the process before diagnostics finish. Windows keeps its try/catch reads. - Escape hatches: SHARPEMU_DISABLE_POSIX_SIGNALS=1 skips installation, SHARPEMU_DISABLE_RAW_HANDLER=1 disables sentinel recovery (parity with Windows), SHARPEMU_LOG_POSIX_SIGNALS=1 traces every delivery (first 16 and every 1024th are always traced). Verified with the test game: Linux (amd64 container) previously died with SIGSEGV right after import #252; it now recovers/diagnoses signals and the run proceeds to the real next blocker, an unpatched negative-offset guest TLS read (fault at TLS base - 0x1708), which gets the full NATIVE EXCEPTION dump before terminating. macOS is unchanged: the bridge installs and the game still stops at the known Rosetta translation-cache error at import 12, which is the next work item. * [posix] Fix guest memory layout faults: TLS prefix, exact mmap, map search base Three fixes that take the test game from dying during libc init to running its full main loop on macOS and Linux: - Static TLS blocks live below the TCB (FreeBSD amd64 variant II) and libc.prx reaches past -0x1700, but only a 4KB prefix was mapped below the TLS base. The prefix is now 64KB on POSIX (Windows keeps 4KB); the fault was a read at TLS base - 0x1708 during libc init. - HostMemory exact allocation on macOS used MAP_FIXED, which silently maps over untracked host memory. The direct-memory allocator's address scan walked into the .NET runtime's JIT heap and replaced live code, which under Rosetta 2 surfaced as "no code fragment associated with the given arm pc". Exact placement now passes the address as a hint and fails on relocation, like MAP_FIXED_NOREPLACE does on Linux. - sceKernelMapDirectMemory/MapFlexibleMemory searched for free space starting at 4GB, which is the Mach-O image base on macOS. The default search base is 0x20_0000_0000 on POSIX, and TryAllocateAtOrAbove now asks the kernel for a placement instead of page-stepping through host- owned address space (Rosetta ignores mmap hints for whole VA windows), over-allocating when the caller needs more than page alignment. Windows behavior is unchanged; all divergences are platform-guarded. * [macos] Video presenter on the main thread, MoltenVK support, window keyboard input Gets the test game from a headless loop to a playable window on macOS: - AppKit traps with SIGILL ("NSUpdateCycleInitialize() is called off the main thread") when GLFW runs on a worker thread. The CLI now moves emulation onto a worker thread on macOS and parks the real main thread in HostMainThread.Pump(); the presenter posts its whole window loop there instead of spawning a thread, and a shutdown handler asks the render loop to close the window so the pump unwinds on guest exit. - MoltenVK: enable VK_KHR_portability_enumeration (+ the portability instance flag) and VK_KHR_portability_subset when advertised, and gate robustBufferAccess2 on the device actually supporting it (Metal does not; the old code keyed it off robustImageAccess2 and vkCreateDevice failed with ErrorFeatureNotPresent). - Input: pad exports polled user32 GetAsyncKeyState, so POSIX hosts threw DllNotFoundException per scePadReadState call. The presenter now attaches the window's keyboard via Silk.NET.Input into HostWindowInput, and the pad exports map the existing VK-code layout onto it off Windows. Headless hosts (Linux containers) report a disconnected keyboard and fall back to neutral pad data silently. GLFW needs an x86-64 Vulkan loader under Rosetta: place a universal libMoltenVK.dylib next to SharpEmu named libvulkan.1.dylib (Homebrew's arm64-only copy cannot load into the x86-64 process) and export DYLD_LIBRARY_PATH to that directory. Verified: Dreaming Sarah boots to a MoltenVK-backed 2560x1440 window on macOS (Apple M4, Rosetta 2), renders the intro, title, and menus, and keyboard input drives it into gameplay. Linux (amd64 container) runs the same build headless through millions of imports with no faults. Windows paths unchanged; arm64 and x64 builds clean. * [posix] CoreAudio playback, self-contained MoltenVK loading, input/log polish - Audio: sceAudioOut ports now play through an AudioQueue backend on macOS (stereo PCM16 with the same 32KB backpressure pacing as the WinMM path). The WinMM port and the new CoreAudio port share an IHostAudioPort interface and sample converter; hosts without a backend (Linux containers) keep the silent fallback. - MoltenVK: GLFW resolves Vulkan with dlopen("libvulkan.1.dylib"), which cannot see the app-local universal MoltenVK build, so the presenter now feeds vkGetInstanceProcAddr straight into glfwInitVulkanLoader (GLFW 3.4) before creating the window. No DYLD_LIBRARY_PATH needed; the CLI also preloads the dylib for Silk.NET and prints setup hints when it is missing. scripts/fetch-macos-moltenvk.sh stages the official universal dylib next to a build. - The virtual-range allocator's failure trace now names the address and length instead of "AllocateAt invocation threw". Investigated and documented (not port defects): the savedata transaction failure is identical on Linux and macOS (HLE argument-register mapping for sceSaveDataCreateTransactionResource), and the in-game tile speckling has no platform-specific code in its path - the one macOS-only delta is that MoltenVK lacks robustBufferAccess2, so out-of-bounds shader reads return garbage instead of zeros. Verified on macOS: window, audio backend, and keyboard input all come up with zero environment configuration; the game runs to gameplay. Linux headless run unchanged (silent audio, no faults). Windows paths untouched; arm64 and x64 builds clean. * [cpu] Preserve guest registers and flags across patched TLS accesses The TLS patch handler replaces guest `mov reg, fs:[...]` instructions, which preserve every other register and the flags - but the handler loaded the TLS index into ecx and called TlsGetValue (Win64: clobbers rcx/rdx/r8-r11) with `sub/add rsp` trashing the arithmetic flags. Guest code that keeps live values or comparison results across a TLS access computed garbage deterministically. The handler now saves rcx, rdx, r8-r11, and the flags around the call, keeping the same inner stack alignment. This applies to the load patches and both store-helper stubs, on every platform. Also in this change, from the rendering-artifact investigation: - The present blit picks linear filtering for any fractional scale (nearest only for integer upscales): a 3840x2160 guest frame blitted into a 2560x1440 swapchain with nearest silently dropped every third row/column. - ClampViewport no longer trims the guest viewport rectangle to the render target; trimming changed the guest's scale/offset and skewed texel addressing. Vulkan permits viewports beyond the framebuffer (the scissor confines rendering), so only spec bounds are enforced. - Env-gated diagnostics grown during the investigation: guest texture dumps (SHARPEMU_TEXTURE_DUMP_DIR), aliased guest-image readback dumps (SHARPEMU_TRACE_GUEST_IMAGES=alias), small-render-target write movies (SHARPEMU_TRACE_GUEST_WRITES=small), unattended input injection (SHARPEMU_AUTO_CROSS=secs,...), viewport nudging (SHARPEMU_VIEWPORT_EPSILON), chunked-draw toggle (SHARPEMU_DISABLE_CHUNKED_DRAWS), and rect-list/draw vertex traces. Known remaining issue (root cause narrowed, not yet fixed): the game's terrain texture pages are corrupted in guest memory before any GPU work - the mound's solid-fill 32x32 tiles decode to fully transparent texels and the grass page has deterministic gaps, byte-identical across runs. Ruled out: memcpy/memmove/memset/realloc HLE semantics, sampler wrap modes, texel-boundary rounding, chunked draws, viewport handling. Next step is auditing the Chowdren asset decode path (custom compressed images) against the emulator's import surface. * [linux] ALSA playback backend for sceAudioOut sceAudioOut ports on Linux now play through libasound instead of the silent fallback. The PCM device opens in blocking mode with ~170ms of device buffer (the time-equivalent of the 32KB queue the WinMM and CoreAudio ports keep), so snd_pcm_writei provides the same backpressure pacing without a managed queue. Underruns and suspend/resume go through snd_pcm_recover with one retry per submit; anything else drops the buffer rather than stalling the guest. The "default" device routes through PulseAudio/PipeWire on desktops and straight to hardware on bare ALSA; SHARPEMU_ALSA_DEVICE overrides it (the null device makes the path testable in containers). A missing libasound or device fails port creation and lands in the existing silent fallback. Verified in an amd64 container: the test game opens the port (backend=alsa, 48kHz stereo float32) and streams sceAudioOutOutput through the null device for a full run; without a usable device the port logs a warning and falls back to silent. Playback on real Linux audio hardware has not been tested. * [fixes] Address review feedback: commit bounds, CoreAudio shutdown, dump errors - HostMemory: a MEM_COMMIT that runs past its reservation now fails like Win32 instead of committing a prefix and reporting success. All current callers already clamp their ranges to the region, so this only guards future callers. - CoreAudioPort: Dispose wakes a submitter waiting on backpressure and the wait treats ObjectDisposedException as a timed-out wait, so closing a port during playback can no longer throw. A failed AudioQueueStart tears the queue down and fails fast instead of leaving an undrainable queue that stalls every later submit on its timeout. - AgcExports: texture dumping catches all write failures (bad path, permissions), logging a warning instead of crashing when SHARPEMU_TEXTURE_DUMP_DIR points somewhere unusable. Verified with the Linux container run: game boots and streams audio with the stricter commit check, and a dump dir under /proc produces warnings instead of taking the process down. * [ci] Build linux-x64 and osx-x64 archives Adds a build-posix matrix job (ubuntu-latest / macos-latest) mirroring the Windows build: locked restore, Release build, self-contained CLI publish, and a tar.gz artifact per RID (tar keeps the executable bit). The macOS archive also stages the universal MoltenVK dylib via scripts/fetch-macos-moltenvk.sh so the artifact runs without any manual Vulkan setup. The release job still only ships the Windows archive. * [cli] Keep POSIX glfw natives outside the single-file bundle The KeepGlfwOutsideSingleFile target only matched filenames starting with 'glfw', which covers Windows (glfw3.dll) but not libglfw.3.dylib / libglfw.so.3. Those got embedded into the single-file bundle, and Silk.NET's library loader does not probe the bundle extraction directory, so a published build died with "Couldn't find a suitable window platform" (and the glfwInitVulkanLoader wiring, which loads the library from AppContext.BaseDirectory, could not run either). Keeping the POSIX names loose next to the executable fixes both, the same way the Windows build already handled it. Found by running the CI-built osx-x64 archive: video failed while local loose-file builds worked. With the fix the published single-file build opens the MoltenVK window, wires the loader, and reaches gameplay. * [ci] Publish linux-x64 and osx-x64 release archives The build-posix artifacts now ship as per-RID GitHub releases on main pushes and manual dispatches, tagged the same way as the win64 ones (<rid>-<ref>-<sha>). Archives stay tar.gz so the executable bit survives extraction. * [cli] Fail early on non-x86-64 host processes The CPU backend executes guest x86-64 code natively, so the process must be x86-64 (win-x64/linux-x64 on x64 hardware, osx-x64 under Rosetta 2 on Apple Silicon). An arm64 process previously failed deep inside emulation startup, indistinguishable from MoltenVK, signal handler, or guest memory problems. CLI mode now checks the process architecture up front and exits with a message naming the supported execution model (and the Rosetta install command on macOS). The GUI-only path stays usable on arm64. * [video] Log the selected Vulkan device name and API version The presenter never named the GPU it picked, so a 'no video' report could not be told apart from a real windowing failure without guessing. It now logs the device name, type, and API version right after selection. A software rasterizer (llvmpipe/lavapipe/SwiftShader) shows up here and typically lacks the device features the translated shaders need, which is the likely cause when a window opens and presents frames but nothing draws. * [video] Steer GLFW to XWayland on Wayland sessions GLFW's native Wayland backend does not reliably map the Vulkan window with some drivers (NVIDIA in particular): frames present but the window never becomes visible, so the game runs with audio and no picture. A report on an RTX 5080 showed exactly this — all device features present, frames presenting, but the log had 'libdecor-gtk.so failed to init' and a 1.4x-scaled window, both Wayland tells. On a Wayland session that also exposes an X server (DISPLAY set), the presenter now clears WAYLAND_DISPLAY for its own process before GLFW initializes, so GLFW selects its dependable X11/XWayland backend. SHARPEMU_ENABLE_WAYLAND=1 opts back into native Wayland. Headless (no DISPLAY) and non-Linux hosts are unaffected. * [video] Force GLFW X11 backend via the platform init hint, log the platform The previous attempt cleared WAYLAND_DISPLAY to steer GLFW off Wayland, but a reporter still hit the native-Wayland path (the Wayland-only libdecor error persisted), so that env trick doesn't switch GLFW. Use GLFW's supported mechanism instead: glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11) before GLFW initializes, called into the same libglfw GLFW itself loads (the pattern InitializeMacVulkanLoader already uses). Still gated on a Wayland session with an X server present (DISPLAY set) so we never force X11 where XWayland can't catch it, and still overridable with SHARPEMU_ENABLE_WAYLAND=1. Also logs 'GLFW windowing platform in use: <backend>' after init via glfwGetPlatform, so a 'no window' report shows X11 vs Wayland outright. Verified on macOS: the readback correctly reports Cocoa and the presenter is unaffected (the fix is a no-op off Linux). * [video] Run the GLFW window on the main thread on Linux too GLFW requires window creation and event processing on the process main thread on every platform: initialization, window creation, and glfwPollEvents are main-thread-only, and X11 in particular has a single event queue that must be serviced there. A window created and polled on another thread may never map — which is why the game ran (audio, imports, even Vulkan present) with no visible window on Linux. macOS already routed the window loop to the main-thread pump (AppKit needs it); Windows is fine because it has a per-thread event queue. Linux was the gap: it spawned a background thread for the presenter. Extend the existing HostMainThread pattern to Linux — emulation runs on a worker, the main thread pumps the window work the presenter posts. Refs GLFW intro guide (thread-safety): init, window creation, and event processing are restricted to the main thread. Verified: macOS still boots to its window unchanged; the Linux headless container runs to millions of imports with no deadlock or regression. On-screen confirmation on a real Linux desktop is still pending, but this is the documented root cause for a windowless-but-running Linux session. * [posix] Skip Win32 native guest workers * [vulkan] Synchronize offscreen targets before present * [vulkan] Transition fresh textures from undefined layout * [vulkan] Report swapchain pixels before source readback * [vulkan] Emit requested guest image diagnostics * [agc] Diagnose guest texture fallbacks * [linux] Keep guest GPU mappings in low address space * [video] Reduce diagnostic stalls and drain complete frames * [memory] Harden packed GPU address handling * [readme] Document Linux and macOS release support * [posix] Integrate the host platform abstraction * [posix] Restore guest thread address window * [video] Run the performance HUD on POSIX hosts The FPS/CPU/work HUD bailed out unless the host was Windows; only the per-thread hottest-thread scan actually needs Windows APIs. Keep that scan Windows-only (POSIX reports 'idle') and let the rest of the HUD run everywhere — the title is already set from the render thread, which owns the window on macOS and Linux. * [posix] Implement native guest worker threads Guest entry stubs must not run above CLR-managed frames on CLR-created threads (see the NativeWorker preamble); the PR previously fell back to the inline calli path on POSIX, which reproduced the documented 'attempted to call a UnmanagedCallersOnly method from managed code' fail-fast (observed after Dreaming Sarah's menu select) and left the runtime's suspension machinery walking guest frames. Provide the missing POSIX half of the worker loop: - PosixHostStubs grows Win64-convention WaitForSingleObject/SetEvent/ ExitThread stubs backed by dispatch semaphores (macOS) / unnamed POSIX semaphores (Linux) plus pthread_exit, with EINTR retry in the wait. - Worker events are creatable/signalable/waitable from managed code too, so NativeGuestExecutor.Run keeps its handshake (AutoResetEvent stays on Windows byte-for-byte). - PosixHostThreading implements CreateNativeThread/WaitForThreadExit/ CloseThreadHandle over pthreads (liveness probed with pthread_kill(0), then joined). - RunPrologue/RunEpilogue are routed through the existing Win64->SysV thunks, so the emitted loop stays identical across platforms. * [macos] Disable concurrent GC under Rosetta's write-watch hazard Background GC's write-watch revisit (SoftwareWriteWatch::GetDirty -> FlushProcessWriteBuffers) calls thread_get_register_pointer_values on every thread; under Rosetta 2 that Mach call stalls indefinitely on threads executing translated guest code. The background mark phase then never finishes and every allocating or Monitor-taking thread wedges behind it — observed as Dreaming Sarah freezing at the menu/loading screen with FPS 0 in 5 of 7 runs, dispatcher/watchdog parked in Monitor.Enter and all BGC threads waiting in t_join. Non-concurrent GC never takes that path; a 5-minute soak now holds 22-31 fps in-game with zero stalls. Windows and Linux keep concurrent GC. * [diag] Periodic guest-thread snapshots with gate-owner tracking SHARPEMU_PERIODIC_SNAPSHOT_SECONDS=N dumps the stall snapshot every N seconds even while imports are progressing, for soft stalls where the game stops advancing but threads keep spinning. The periodic dump never touches the guest-thread gate (it must keep reporting when the gate is what's wedged): it reads a lock-free owner record — every gate acquisition now goes through LockGate(site), which notes site/thread — and walks the thread table without the lock, tolerating torn reads. SHARPEMU_PERIODIC_SNAPSHOT_FILE redirects the dump to a side file for the case where the console itself is wedged (frozen log mirror was one of the observed failure modes). * [nuget] Add osx-x64 RID targets to lock files * [cpu] Back off the guest join poll TryJoinThread polled the host thread at a fixed 1ms; a game main thread joining a long-lived worker (Dreaming Sarah parks there for the whole session) burned ~5% of managed CPU in Join/Sleep syscalls. Ramp the poll interval to 10ms once the join is clearly long-lived — exit detection latency for long joins moves from ~1ms to at most 10ms, and short-lived joins still resolve on the first 1ms polls. * [nuget] Add linux-x64/win-x64 RID targets to lock files * [posix] Keep guest stacks clear of the import-stub descent The import-stub region descends from 0x7000_0000_0000 on the same 16MB grid as the guest thread windows; moving stacks to 0x6FFF_E000_0000 put them inside the stub region's 64-module descent range (floor 0x6FFF_C000_0000), silently consuming the top ~32 stack slots on hosts with many loaded modules. Drop the POSIX stack base to 0x6FFF_A000_0000: 512MB below the stub floor, still 2.5GB above the TLS window. Windows keeps 0x7FFF_E000_0000 (its bands are ~15TB apart). * [pad] Read window gamepads on POSIX hosts XInput and the DualSense hid reader are Windows-only, which left macOS/Linux with keyboard input only. The presenter's Silk/GLFW input context already enumerates gamepads on both platforms, so track their state in HostWindowInput (event-driven on the window thread, snapshot guarded like the key set) translated to ORBIS conventions: GLFW's Xbox layout maps A/B/X/Y to Cross/Circle/Square/Triangle, sticks bias from -1..1 to 0..255 with Y growing down, and triggers rescale from GLFW's -1..1 resting-at--1 range with digital L2/R2 bits past 25%. The merge into ReadHostInputState is gated to non-Windows so a physical pad is never sampled twice through both a native reader and GLFW. Hotplug is handled via ConnectionChanged; with no pad connected the path is inert. Untested against a physical controller (none attached to the dev host); axis conventions follow the GLFW gamepad-mapping contract. * [nuget] Refresh lock files after cross-RID restores * [posix] Adopt the host audio/input seams from main Main's #192 abstracted audio output and pad/keyboard input behind IHostAudioOutput/IHostInput; re-express the POSIX backends behind them: - CoreAudioPort/AlsaAudioPort move to Host/Posix as PosixCoreAudioStream/PosixAlsaAudioStream implementing IHostAudioStream. The seam converts to stereo PCM16 before Submit, so the ports' own conversion (and IHostAudioPort/AudioSampleConverter) is gone; queueing and backpressure are unchanged. - PosixHostAudio selects CoreAudio (macOS) / ALSA (Linux) as the platform's IHostAudioOutput. - PosixHostInput implements IHostInput over an IPosixWindowInputSource that HostWindowInput registers when the presenter attaches the window's GLFW input context: keyboard with virtual-key translation, the window gamepad snapshot (now in seam HostGamepadState/HostGamepadButtons terms), and keyboard-connected as the focus signal. Rumble/lightbar no-op (GLFW has no such API). - PadExports drops its direct HostWindowInput gamepad merge — pads now flow through IHostInput.GetGamepadStates like every platform. - PosixHostThreading.RequestTimerResolution is a documented no-op. All three RIDs build; SharpEmu.Libs.Tests pass (26/26). * [nuget] Regenerate GUI lock file for RID-less locked restore Local cross-RID builds stamped a win-x64 runtimes section into SharpEmu.GUI's lock file; the project declares no RuntimeIdentifiers, so CI's 'dotnet restore --locked-mode' failed with NU1004 on every platform. Regenerated via a plain solution restore (--force-evaluate), matching what the workflow validates.
1323 lines
43 KiB
C#
1323 lines
43 KiB
C#
// 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.Libs.VideoOut;
|
|
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);
|
|
}
|
|
|
|
if (!CheckHostArchitecture())
|
|
{
|
|
return 5;
|
|
}
|
|
|
|
if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
|
|
{
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
PreloadMacVulkanLoader();
|
|
}
|
|
|
|
// GLFW requires window creation and event processing on the
|
|
// process main thread: AppKit demands it on macOS, and X11 has a
|
|
// single event queue that must be serviced from the main thread
|
|
// (a window created and polled off it may never map, which showed
|
|
// as a running game with no visible window on Linux). Emulation
|
|
// moves to a worker thread and the main thread services the window
|
|
// work the video presenter posts. Windows keeps a per-thread event
|
|
// queue, so its window stays on the presenter's own thread.
|
|
var exitCode = 0;
|
|
HostMainThread.Enable();
|
|
var emulation = new Thread(() =>
|
|
{
|
|
try
|
|
{
|
|
exitCode = RunEmulator(args, isMitigatedChild);
|
|
}
|
|
finally
|
|
{
|
|
HostMainThread.Shutdown();
|
|
}
|
|
}, 32 * 1024 * 1024)
|
|
{
|
|
Name = "SharpEmu Emulation",
|
|
};
|
|
emulation.Start();
|
|
HostMainThread.Pump();
|
|
emulation.Join();
|
|
return exitCode;
|
|
}
|
|
|
|
return RunEmulator(args, isMitigatedChild);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The supported host execution model, checked before any emulation
|
|
/// starts: the CPU backend executes guest x86-64 code natively, so the
|
|
/// host process must be x86-64 — win-x64/linux-x64 on x64 hardware, or
|
|
/// osx-x64 under Rosetta 2 on Apple Silicon (Rosetta translates the
|
|
/// whole process, so it still reports as X64 here). An arm64 process
|
|
/// (e.g. the osx-arm64 build) can browse the GUI but cannot run games;
|
|
/// failing up front distinguishes that from MoltenVK, signal-handler,
|
|
/// or guest-memory startup problems.
|
|
/// </summary>
|
|
private static bool CheckHostArchitecture()
|
|
{
|
|
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][ERROR] Unsupported process architecture " +
|
|
$"{RuntimeInformation.ProcessArchitecture}: guest code executes " +
|
|
"natively, so SharpEmu must run as an x86-64 process.");
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
Console.Error.WriteLine(
|
|
"[LOADER][ERROR] On Apple Silicon, use the osx-x64 build under " +
|
|
"Rosetta 2 (install with: softwareupdate --install-rosetta).");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Makes a Vulkan loader visible to GLFW's dlopen("libvulkan.1.dylib").
|
|
/// Homebrew's Vulkan libraries are arm64-only and cannot load into this
|
|
/// x86-64 (Rosetta 2) process, so a universal libMoltenVK.dylib placed
|
|
/// next to the executable (named libvulkan.1.dylib) is preloaded here;
|
|
/// dyld then resolves GLFW's bare-name dlopen to the loaded image.
|
|
/// </summary>
|
|
private static void PreloadMacVulkanLoader()
|
|
{
|
|
var candidates = new[]
|
|
{
|
|
Path.Combine(AppContext.BaseDirectory, "libvulkan.1.dylib"),
|
|
Path.Combine(AppContext.BaseDirectory, "libMoltenVK.dylib"),
|
|
Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
".sharpemu", "x64lib", "libvulkan.1.dylib"),
|
|
};
|
|
foreach (var candidate in candidates)
|
|
{
|
|
if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out _))
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] Vulkan loader preloaded: {candidate}");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (NativeLibrary.TryLoad("libvulkan.1.dylib", out _))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
"[LOADER][WARN] No x86-64 Vulkan loader found; video output will be unavailable. " +
|
|
"Place a universal libMoltenVK.dylib (from the MoltenVK releases) next to SharpEmu " +
|
|
"as libvulkan.1.dylib.");
|
|
}
|
|
|
|
private static int RunEmulator(string[] args, bool isMitigatedChild)
|
|
{
|
|
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;
|
|
ConsoleCancelEventHandler? cancelHandler = null;
|
|
try
|
|
{
|
|
cancelHandler = (_, eventArgs) =>
|
|
{
|
|
eventArgs.Cancel = true;
|
|
VideoOutExports.NotifyHostInterrupt();
|
|
};
|
|
Console.CancelKeyPress += cancelHandler;
|
|
|
|
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;
|
|
}
|
|
finally
|
|
{
|
|
if (cancelHandler is not null)
|
|
{
|
|
Console.CancelKeyPress -= cancelHandler;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<string>(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<STARTUPINFOEX>();
|
|
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<string> 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<string> 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<string> 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<string> 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<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
|
|
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=<native>] [--log-level=<level>] [--log-file[=<path>]] <path-to-eboot.bin>");
|
|
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<string>(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);
|
|
}
|