fix(desktop-update): give Windows update steps NUL stdin, not the hand-off console

Steps inherited the hand-off console's stdin, so any step that asks a
question blocked forever. Its prompt went to the captured stdout, which
is shown only after the step exits. `gateway start --all` did exactly
this: it saw an interactive console and asked "Install it now so the
gateway starts on login?". The update stopped after `hermes update exit
code: 0` and never relaunched the app.

Steps now read NUL. Prompts see a non-interactive stdin and take their
defaults. The working-directory self-test also checks that a step's
stdin is not a console, and a new test runs it under a real console.
This commit is contained in:
ethernet
2026-09-24 23:49:18 -04:00
parent 61c06ca3ce
commit a3456765ed
2 changed files with 56 additions and 6 deletions

View File

@@ -898,6 +898,12 @@ public static class HermesUpdateJob {
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetHandleInformation(IntPtr handle, int mask, int flags);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr CreateFile(
string fileName, uint desiredAccess, uint shareMode, ref SecurityAttributes attributes,
uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile
);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CreateProcess(
string applicationName, StringBuilder commandLine,
@@ -912,9 +918,6 @@ public static class HermesUpdateJob {
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool TerminateProcess(IntPtr process, uint exitCode);
[DllImport("kernel32.dll")]
private static extern IntPtr GetStdHandle(int standardHandle);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool TerminateJobObject(IntPtr job, uint exitCode);
@@ -934,6 +937,7 @@ public static class HermesUpdateJob {
IntPtr job = IntPtr.Zero;
IntPtr outRead = IntPtr.Zero, outWrite = IntPtr.Zero;
IntPtr errRead = IntPtr.Zero, errWrite = IntPtr.Zero;
IntPtr nullInput = new IntPtr(-1);
ProcessInformation pi = new ProcessInformation();
try {
job = CreateJobObject(IntPtr.Zero, null);
@@ -946,11 +950,17 @@ public static class HermesUpdateJob {
throw new InvalidOperationException("CreatePipe failed");
if (!SetHandleInformation(outRead, 1, 0) || !SetHandleInformation(errRead, 1, 0))
throw new InvalidOperationException("SetHandleInformation failed");
// Steps read NUL, never the hand-off console. A step that sees a
// console asks its question into the captured stdout, where the
// user cannot see it, and waits for an answer that never comes.
nullInput = CreateFile("NUL", 0x80000000, 0x00000003, ref sa, 3, 0, IntPtr.Zero);
if (nullInput == new IntPtr(-1))
throw new InvalidOperationException("CreateFile(NUL) failed");
StartupInfo si = new StartupInfo();
si.Size = Marshal.SizeOf(typeof(StartupInfo));
si.Flags = 0x00000100; // STARTF_USESTDHANDLES
si.StdInput = GetStdHandle(-10);
si.StdInput = nullInput;
si.StdOutput = outWrite;
si.StdError = errWrite;
StringBuilder commandLine = new StringBuilder("\"" + executable + "\" " + arguments);
@@ -989,6 +999,7 @@ public static class HermesUpdateJob {
if (outWrite != IntPtr.Zero) CloseHandle(outWrite);
if (errRead != IntPtr.Zero) CloseHandle(errRead);
if (errWrite != IntPtr.Zero) CloseHandle(errWrite);
if (nullInput != new IntPtr(-1)) CloseHandle(nullInput);
}
}
@@ -1500,13 +1511,19 @@ try {
if ($SelfTestWorkingDirectory) {
$expectedRoot = [System.IO.Path]::GetFullPath($InstallRoot)
$probeExe = Join-Path $PSHOME "powershell.exe"
$probe = Invoke-HermesStep $probeExe @("-NoProfile", "-Command", "[Environment]::CurrentDirectory") "cwd"
$observed = $probe.Output.Trim()
$probe = Invoke-HermesStep $probeExe @("-NoProfile", "-Command", "[Environment]::CurrentDirectory; [Console]::IsInputRedirected") "cwd"
$observed, $stdinRedirected = @($probe.Output.Trim() -split "`r?`n" | ForEach-Object { $_.Trim() })
if ($probe.Code -ne 0 -or -not [string]::Equals($observed, $expectedRoot, [StringComparison]::OrdinalIgnoreCase)) {
$finalMsg = "WORKING-DIRECTORY SELF-TEST: FAIL expected=$expectedRoot observed=$observed code=$($probe.Code)"
Write-Host $finalMsg
exit 1
}
# A step that can read the hand-off console can block on a prompt nobody sees.
if ($stdinRedirected -ne "True") {
$finalMsg = "WORKING-DIRECTORY SELF-TEST: FAIL step stdin is an interactive console"
Write-Host $finalMsg
exit 1
}
$finalCode = 0
$finalMsg = "WORKING-DIRECTORY SELF-TEST: PASS $observed"
Write-Host $finalMsg