// Runs the PowerShell startup script on the process MAIN thread, inside the // user's own pwsh. // // Why: Cocoa (macOS) only allows the GUI - NSApplication, windows, dialogs - on // the first thread of the process. A normal pwsh pipeline runs on a worker // thread (ConsoleHost's ReuseThread runspace), so Avalonia cannot be started // from an ordinary `pwsh -File`. Windows (STA relaunch) and Linux (X11/Wayland) // have no such rule; this hook is only engaged on macOS, though it works // anywhere for testing. // // How: .NET calls StartupHook.Initialize() on the main thread BEFORE the app's // Main when DOTNET_STARTUP_HOOKS names this DLL. If IM_MAIN_THREAD_HOOK=1 is // also set and pwsh was started with -File, Initialize opens a UseCurrentThread // runspace right here, runs the script and terminates the process - pwsh's own // console Main never executes; the process is just the engine + runtime host. // Without the env var (child processes, ad-hoc pwsh) it returns immediately and // pwsh starts normally. // // Because it executes inside pwsh, the hook needs no PowerShell SDK, no separate // .NET install and no version matching: whatever pwsh the user has (7.4 or // newer, on .NET 8/9/10/...) brings its own engine, runtime and $PSHOME/ref // compile references, all consistent with each other by construction. using System.Globalization; using System.Management.Automation; using System.Management.Automation.Host; using System.Management.Automation.Runspaces; using System.Runtime.InteropServices; namespace IntuneManagement.MainThreadHook { // Contract used by the PowerShell side (Start-Avalonia.ps1, CoreUIAvalonia.ps1, // tests): the type exists only when the hook engaged, and Verify() throws // if the caller is not on the thread the script was started on. public static class MainThread { public static int ManagedId { get; internal set; } [DllImport("/usr/lib/libSystem.B.dylib")] private static extern int pthread_main_np(); public static bool IsNativeMainThread => !OperatingSystem.IsMacOS() || pthread_main_np() != 0; public static void Verify() { if (Environment.CurrentManagedThreadId != ManagedId || !IsNativeMainThread) throw new InvalidOperationException("The macOS GUI must run on the process main thread."); } } // This is a GUI script host, not an interactive console: output is forwarded // through the streams, nested prompts are deliberately unsupported, and // SetShouldExit captures a script-level `exit N`. internal sealed class HookHost : PSHost { public int? ExitCode { get; private set; } public override Guid InstanceId { get; } = Guid.NewGuid(); public override string Name => "IntuneManagement.MainThreadHook"; public override Version Version => new(1, 0); public override PSHostUserInterface UI => null!; public override CultureInfo CurrentCulture => CultureInfo.CurrentCulture; public override CultureInfo CurrentUICulture => CultureInfo.CurrentUICulture; public override void SetShouldExit(int exitCode) => ExitCode = exitCode; public override void EnterNestedPrompt() => throw new NotSupportedException("Nested prompts are not supported by the main-thread hook."); public override void ExitNestedPrompt() => throw new NotSupportedException("Nested prompts are not supported by the main-thread hook."); public override void NotifyBeginApplication() { } public override void NotifyEndApplication() { } } internal static class ScriptRunner { public const string EngageVariable = "IM_MAIN_THREAD_HOOK"; // Returns null when this pwsh was not started with -File