mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
test(ssh): capture hung Windows client phase
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -91,6 +91,7 @@ afterAll(() => {
|
||||
describe('Windows SSH no-input launcher', () => {
|
||||
it('keeps the artifact source scoped to the reviewed Win32 boundary', () => {
|
||||
const processSource = readFileSync(join(sourceRoot, 'WindowsSshChildProcess.cs'), 'utf8')
|
||||
const programSource = readFileSync(join(sourceRoot, 'OrcaSshNoInputLauncher.cs'), 'utf8')
|
||||
const consoleSource = readFileSync(join(sourceRoot, 'WindowsPrivateConsoleInput.cs'), 'utf8')
|
||||
const outputSource = readFileSync(join(sourceRoot, 'WindowsBoundedOutputFiles.cs'), 'utf8')
|
||||
|
||||
@@ -103,6 +104,9 @@ describe('Windows SSH no-input launcher', () => {
|
||||
expect(processSource).toContain('OutputLimitPollMilliseconds = 50')
|
||||
expect(processSource).toContain('JobSettlementTimeoutMilliseconds = 5000')
|
||||
expect(processSource).toContain('TerminateJobObject')
|
||||
expect(processSource).toContain('diagnosticTimeoutMilliseconds')
|
||||
expect(programSource).toContain('--diagnostic-timeout-ms')
|
||||
expect(programSource).toContain('diagnosticTimeoutMilliseconds > 60000')
|
||||
expect(processSource).toContain('outputs.EnsureWithinLimits()')
|
||||
expect(processSource).toContain('outputs.ReplayOutputs()')
|
||||
expect(processSource).not.toContain('WaitForMultipleObjects')
|
||||
@@ -195,6 +199,26 @@ describe('Windows SSH no-input launcher', () => {
|
||||
expect(listCaptureFiles()).toEqual(capturesBefore)
|
||||
})
|
||||
|
||||
itWindows(
|
||||
'terminates the child job and replays bounded output at the diagnostic deadline',
|
||||
{ timeout: 10_000 },
|
||||
async () => {
|
||||
const capturesBefore = listCaptureFiles()
|
||||
const pidPath = join(fixtureRoot, 'timed-out-child.pid')
|
||||
const result = runLauncher('hold', [pidPath], {}, 250)
|
||||
const childPid = Number.parseInt(readFileSync(pidPath, 'utf8'), 10)
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stdout.toString('utf8')).toBe('ORCA-HOLD-STDOUT')
|
||||
expect(result.stderr.toString('utf8')).toContain('ORCA-HOLD-STDERR')
|
||||
expect(result.stderr.toString('utf8')).toContain(
|
||||
'SSH child reached the 250 ms diagnostic timeout.'
|
||||
)
|
||||
await waitForProcessExit(childPid, 5_000)
|
||||
expect(listCaptureFiles()).toEqual(capturesBefore)
|
||||
}
|
||||
)
|
||||
|
||||
itWindows('does not leak an unrelated inheritable parent handle into the child', () => {
|
||||
const result = spawnSync(handleProbePath, [launcherPath], {
|
||||
cwd: fixtureRoot,
|
||||
@@ -231,15 +255,21 @@ describe('Windows SSH no-input launcher', () => {
|
||||
)
|
||||
})
|
||||
|
||||
function runLauncher(mode, args = [], options = {}) {
|
||||
return spawnSync(launcherPath, [process.execPath, childFixturePath, mode, ...args], {
|
||||
cwd: fixtureRoot,
|
||||
encoding: null,
|
||||
timeout: 10_000,
|
||||
windowsHide: true,
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
...options
|
||||
})
|
||||
function runLauncher(mode, args = [], options = {}, diagnosticTimeoutMs = null) {
|
||||
const diagnosticArgs =
|
||||
diagnosticTimeoutMs == null ? [] : ['--diagnostic-timeout-ms', String(diagnosticTimeoutMs)]
|
||||
return spawnSync(
|
||||
launcherPath,
|
||||
[...diagnosticArgs, process.execPath, childFixturePath, mode, ...args],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
encoding: null,
|
||||
timeout: 10_000,
|
||||
windowsHide: true,
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
...options
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function listCaptureFiles() {
|
||||
@@ -328,6 +358,8 @@ if (mode === 'arguments-and-stdin') {
|
||||
}
|
||||
} else if (mode === 'hold') {
|
||||
require('node:fs').writeFileSync(process.argv[3], String(process.pid))
|
||||
process.stdout.write('ORCA-HOLD-STDOUT')
|
||||
process.stderr.write('ORCA-HOLD-STDERR')
|
||||
setInterval(() => {}, 1000)
|
||||
} else {
|
||||
process.exit(99)
|
||||
|
||||
+18
-3
@@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details.
|
||||
A checked box means the work has evidence in the detailed ledger. Design approval alone does not
|
||||
complete a box.
|
||||
|
||||
Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected private-
|
||||
console plus bounded regular-output Windows diagnostic locally green; exact-head native proof
|
||||
required, 2026-07-16, Codex implementation owner.**
|
||||
Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — bounded verbose
|
||||
Win32-OpenSSH lifecycle capture locally green; exact-head native proof required, 2026-07-16, Codex
|
||||
implementation owner.**
|
||||
`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture
|
||||
correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned
|
||||
Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain
|
||||
@@ -210,6 +210,21 @@ isolation pass. Seven native
|
||||
launcher cases plus live/full-size behavior remain honest local skips. Commit/push only this
|
||||
runner-temp diagnostic and require fresh exact-head Windows x64/ARM64 compilation, overflow,
|
||||
cleanup, cancellation, live lifecycle, and fixture-teardown proof. SignPath remains deferred.
|
||||
Exact head `149a122b735659b47d7718f409d14e1ed2f947c2`, run `29553325782`, and x64/ARM64 jobs
|
||||
`87800290122`/`87800290144` prove 101 files/862 cases with 19 skips, including both new overflow and
|
||||
capture-cleanup cases, plus unchanged reproducibility and full-size cache gates. The live probes
|
||||
still time out after 8017.5611/8042.1902 ms with ended stdout, closed channel, no sentinel/process
|
||||
exit, and zero stderr; both exact teardowns catch locked `UsrClass.dat`. All four non-Windows jobs
|
||||
and both Linux supplements pass. `E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-CI-RED-001` therefore
|
||||
falsifies output pipes as the remaining cause. Next add only an internal diagnostic deadline and
|
||||
bounded replay of verbose OpenSSH output before choosing ConPTY or another client. Production,
|
||||
Beta, fallback, tuple, publication, default, and signing paths remain untouched.
|
||||
`E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001` records that diagnostic locally green: an
|
||||
optional 100-60,000 ms runner-only deadline terminates and settles the owned job before bounded
|
||||
output replay; the live suite uses 6 seconds, `-vvv`, outer 8/10-second safety ceilings, and a 16 KiB
|
||||
trace tail. Focused 119/119, all 285 release contracts, typecheck, full lint/reliability/max-lines,
|
||||
formatting, diff, and protected-resolver isolation pass with honest native skips. Commit/push only
|
||||
this artifact and require x64/ARM64 phase classification before selecting ConPTY or another client.
|
||||
`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft
|
||||
server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1,
|
||||
serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown
|
||||
|
||||
+74
-6
@@ -8,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
|
||||
|
||||
Date created: 2026-07-14<br>
|
||||
Last updated: 2026-07-16<br>
|
||||
Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **Locally green / exact-head native proof required — 2026-07-16, Codex implementation owner: disconnected private-console plus bounded regular-output diagnostic**. `E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001` records the one-variable runner-temp candidate locally green: proven private-console EOF, distinct bounded regular-file output handles, delete-on-close cleanup, bounded job settlement, and post-exit replay. Commit/push only this package and require x64/ARM64 legacy compilation, native contracts, live lifecycle, and exact teardown evidence. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.<br>
|
||||
Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **Locally green / bounded verbose client-lifecycle native proof required — 2026-07-16, Codex implementation owner**. `E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001` records the runner-temp internal deadline, job settlement, bounded replay, and verbose trace-tail contracts locally green. Commit/push only this diagnostic and require both native architectures to identify the last completed client phase before selecting ConPTY or another client. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.<br>
|
||||
Session checkpoint: **PRIVATE CONSOLE EOF PROVEN / PRIVATE OUTPUT PIPES FALSIFIED / BOUNDED REGULAR OUTPUT ACTIVE — 2026-07-16, Codex implementation owner** — exact head `5841b12bc94ecd1027e507e4b741e09fcb0a093d`, run `29552101635`, and x64/ARM64 jobs `87796673326`/`87796673353` both pass 101 files/860 cases with 19 skips, legacy compilation, real-console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, unchanged full-runtime reproducibility, and full-size cache gates. Live diagnostics still time out after 8025.1553/8034.6943 milliseconds with ended stdout, closed channel, no sentinel/process exit, and zero stderr; both exact teardowns catch locked `UsrClass.dat`. All four non-Windows primary jobs and both Linux supplements pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.<br>
|
||||
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)<br>
|
||||
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693)
|
||||
@@ -19891,6 +19891,75 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi
|
||||
Beta option, fallback, full-size Windows transfer claim, tuple, publication, default, signing, or
|
||||
SignPath behavior is authorized.
|
||||
|
||||
### E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-CI-RED-001 — regular output files do not settle Win32-OpenSSH
|
||||
|
||||
- Date/owner/head/run: 2026-07-16, Codex implementation owner; exact pushed head
|
||||
`149a122b735659b47d7718f409d14e1ed2f947c2`, pull-request artifact run `29553325782`, Windows x64
|
||||
job `87800290122` on `windows-2022`, and Windows ARM64 job `87800290144` on `windows-11-arm`.
|
||||
- Native contract proof: both architectures pass legacy .NET Framework compilation, the dedicated
|
||||
console EOF probe, and 101 artifact-contract files with 862 runnable cases and 19 declared skips.
|
||||
The two new cases raise the prior count from 860 to 862 and prove >16 MiB fail-closed output plus
|
||||
successful/overflow delete-on-close cleanup; exact binary replay, argv, exit propagation,
|
||||
unrelated-handle exclusion, job cancellation, and bounded settlement also pass. Both unchanged
|
||||
runtimes pass build-twice identity, closure, smoke, and both full-size cache gates.
|
||||
- X64 live result: `mode=private-console-regular-output-launcher-no-n`,
|
||||
`elapsedMs=8017.5611`, `success=false`, `timedOut=true`, `sentinel=false`, `stdoutEnded=true`,
|
||||
`processExit=null`, `channelClosed=true`, `closeCode=null`, and `stderrBytes=0`.
|
||||
- ARM64 live result: the same lifecycle fields with `elapsedMs=8042.190199999999`. Both official
|
||||
server logs prove accepted public-key authentication, remote command-session start, and remote
|
||||
session close before the client requires bounded external termination. The full-size Windows
|
||||
transfer remains correctly unexecuted after the failed diagnostic.
|
||||
- Teardown result: x64 cannot unload fixture class hive
|
||||
`S-1-5-21-4137868374-391603265-4009536220-1003_Classes`; ARM64 independently cannot unload
|
||||
`S-1-5-21-1882319117-3219095328-2125279949-1001_Classes`. Both catch the locked
|
||||
`C:\Users\orca_ssh_fixture\AppData\Local\Microsoft\Windows\UsrClass.dat` and retained profile
|
||||
directory instead of silently leaking fixture state.
|
||||
- Preserved evidence: Linux x64/ARM64 jobs `87800290119`/`87800290139`, Darwin x64/ARM64 jobs
|
||||
`87800290123`/`87800290135`, and Linux oldest-userland supplements `87800855219`/`87800855211`
|
||||
all pass at the same exact head.
|
||||
- Interpretation/next bound: private pipes, overlapped pipes, zero-length regular stdin, proven
|
||||
console EOF, and now distinct bounded regular-file stdout/stderr all reproduce the live hang.
|
||||
Do not select a production mechanism from speculation. Add only a runner-temp internal deadline
|
||||
that terminates and settles the owned job before replaying bounded output, invoke the exact client
|
||||
with verbose diagnostics, and record the last completed OpenSSH phase on both architectures.
|
||||
ConPTY and alternate-client qualification remain subsequent evidence decisions. No product,
|
||||
Beta, fallback, tuple, publication, default, signing, or SignPath behavior is authorized.
|
||||
|
||||
### E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001 — internal timeout and bounded trace capture are locally green
|
||||
|
||||
- Date/owner/base/runner: 2026-07-16, Codex implementation owner; uncommitted artifact-only package
|
||||
atop exact regular-output RED head `149a122b735659b47d7718f409d14e1ed2f947c2` on Apple arm64,
|
||||
macOS 26.2 build 25C56, Node 26.5.0, and pnpm 10.24.0.
|
||||
- Diagnostic boundary: an optional runner-only `--diagnostic-timeout-ms` accepts 100 through 60,000
|
||||
ms and leaves ordinary launcher calls unbounded as before. At deadline the launcher terminates
|
||||
and waits for its owned job, replays the same bounded regular-file stdout/stderr, then returns a
|
||||
diagnostic failure. The live purpose suite uses 6,000 ms, retains the existing 8,000/10,000 ms
|
||||
external safety ceilings, inserts `-vvv` before the exact OpenSSH destination separator, and logs
|
||||
only a 16 KiB stderr tail plus lifecycle fields. Production argument construction is unchanged.
|
||||
- Native contract design: the new Windows case writes stdout/stderr plus its PID, holds forever,
|
||||
and requires internal timeout failure, exact replay, child-job termination, and unchanged capture-
|
||||
file set. Existing console EOF, argv, binary output, overflow, cleanup, handle exclusion, exit,
|
||||
and external-cancellation cases remain. The new native case is honestly skipped locally.
|
||||
- Syntax/focused evidence: both launcher scripts pass `node --check`.
|
||||
`/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1
|
||||
src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts
|
||||
src/main/ssh/ssh-system-no-input-windows-openssh.test.ts
|
||||
src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts
|
||||
config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs
|
||||
config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with ten declared
|
||||
native/live skips in 2.69 seconds real; maximum RSS is 171,212,800 bytes and peak footprint is
|
||||
97,179,456 bytes.
|
||||
- Release/static evidence: the unquoted release-contract command passes 51 files and 285 runnable
|
||||
cases with eight declared Windows skips in 12.19 seconds. `/usr/bin/time -l pnpm typecheck`
|
||||
passes in 2.57 seconds real with 1,258,979,328-byte maximum RSS and 97,163,024-byte peak
|
||||
footprint. `/usr/bin/time -l pnpm lint` passes in 21.90 seconds real with the 26 existing
|
||||
warnings, all 41 reliability gates, and the 355-entry max-lines ratchet; maximum RSS is
|
||||
2,039,169,024 bytes and peak footprint is 97,195,840 bytes.
|
||||
- Residual boundary: local source contracts do not prove legacy compilation, internal timeout/job
|
||||
settlement, output replay, or useful OpenSSH verbose content on Windows. Commit/push this isolated
|
||||
package and require exact-head x64/ARM64 evidence. Do not select ConPTY, another client, a product
|
||||
caller, Beta, fallback, tuple, publication, default, signing, or SignPath behavior from local proof.
|
||||
|
||||
## Accepted Gaps
|
||||
|
||||
No product gap is accepted merely because it appears in this list. Each entry requires explicit
|
||||
@@ -19949,11 +20018,10 @@ The project is not complete until every applicable item below is checked with ev
|
||||
|
||||
## Next Required Action
|
||||
|
||||
Commit and push only the private-console plus bounded regular-output diagnostic recorded by
|
||||
`E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001`, then require exact-head Windows x64 and
|
||||
ARM64 legacy-compiler, console qualification/EOF, bounded binary output/overflow/cleanup,
|
||||
argv/exit/handle-exclusion/cancellation, live diagnostic, and exact profile/directory teardown
|
||||
evidence. Do not
|
||||
Commit and push only the bounded internal-timeout and verbose-output capture recorded by
|
||||
`E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001`, then require exact-head Windows x64 and
|
||||
ARM64 legacy-compiler, launcher-timeout settlement, verbose-client phase classification, and exact
|
||||
profile/directory teardown evidence. Do not
|
||||
change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until
|
||||
both architectures select the same console-independent strategy. Do not add an
|
||||
Electron/startup/product importer, per-target mode wiring, fallback, tuple
|
||||
|
||||
@@ -1670,12 +1670,15 @@
|
||||
child while retaining private output pipes, exact handle inheritance, job
|
||||
ownership, and bounded cancellation. Native x64/arm64 prove the console handle
|
||||
and zero-byte cooked EOF but the live client still hangs, falsifying stdin alone.
|
||||
The next one-variable diagnostic retains that console stdin and replaces only
|
||||
The next one-variable diagnostic retained that console stdin and replaced only
|
||||
child stdout/stderr pipes with unique delete-on-close regular files under an
|
||||
explicit per-stream byte ceiling, bounded polling/cancellation, incremental
|
||||
post-exit replay, and exact cleanup. Build and live-test it on native x64/arm64
|
||||
before considering ConPTY, another SSH client, packaging, signing, or a product
|
||||
caller. POSIX retains ignored
|
||||
post-exit replay, and exact cleanup. Native x64/arm64 prove those contracts, but
|
||||
the live client still hangs with the same lifecycle tuple, falsifying redirected
|
||||
output pipes as the remaining cause. Before selecting ConPTY or another SSH
|
||||
client, the runner-only launcher must boundedly terminate its own diagnostic
|
||||
child and replay an exact verbose OpenSSH trace to classify the stuck client
|
||||
phase. Do not package, sign, or add a product caller. POSIX retains ignored
|
||||
stdin plus <code>-n</code>; fixture-owned Windows session processes settle
|
||||
before profile hives are boundedly unloaded and native profile deletion begins;
|
||||
<code>MaxSessions=1</code>; interrupted upload; stale lock
|
||||
|
||||
@@ -7,21 +7,40 @@ internal static class OrcaSshNoInputLauncher
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Length < 1)
|
||||
int executableIndex = 0;
|
||||
int diagnosticTimeoutMilliseconds = 0;
|
||||
if (args.Length >= 2 && args[0] == "--diagnostic-timeout-ms")
|
||||
{
|
||||
Console.Error.WriteLine("Usage: orca-ssh-no-input.exe <ssh.exe> [arguments...]");
|
||||
if (!Int32.TryParse(args[1], out diagnosticTimeoutMilliseconds) ||
|
||||
diagnosticTimeoutMilliseconds < 100 ||
|
||||
diagnosticTimeoutMilliseconds > 60000)
|
||||
{
|
||||
Console.Error.WriteLine("--diagnostic-timeout-ms requires 100 through 60000.");
|
||||
return 2;
|
||||
}
|
||||
executableIndex = 2;
|
||||
}
|
||||
if (args.Length <= executableIndex)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"Usage: orca-ssh-no-input.exe [--diagnostic-timeout-ms <milliseconds>] <ssh.exe> [arguments...]"
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
string executablePath = Path.GetFullPath(args[0]);
|
||||
string executablePath = Path.GetFullPath(args[executableIndex]);
|
||||
if (!File.Exists(executablePath))
|
||||
{
|
||||
Console.Error.WriteLine("Unable to locate the SSH executable at \"{0}\".", executablePath);
|
||||
return 2;
|
||||
}
|
||||
|
||||
string[] childArgs = new string[args.Length - 1];
|
||||
Array.Copy(args, 1, childArgs, 0, childArgs.Length);
|
||||
return WindowsSshChildProcess.Run(executablePath, childArgs);
|
||||
string[] childArgs = new string[args.Length - executableIndex - 1];
|
||||
Array.Copy(args, executableIndex + 1, childArgs, 0, childArgs.Length);
|
||||
return WindowsSshChildProcess.Run(
|
||||
executablePath,
|
||||
childArgs,
|
||||
diagnosticTimeoutMilliseconds
|
||||
);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -18,7 +19,11 @@ internal static class WindowsSshChildProcess
|
||||
private const uint OutputLimitPollMilliseconds = 50;
|
||||
private const uint JobSettlementTimeoutMilliseconds = 5000;
|
||||
|
||||
internal static int Run(string executablePath, string[] args)
|
||||
internal static int Run(
|
||||
string executablePath,
|
||||
string[] args,
|
||||
int diagnosticTimeoutMilliseconds
|
||||
)
|
||||
{
|
||||
Stream launcherStdout = Console.OpenStandardOutput();
|
||||
Stream launcherStderr = Console.OpenStandardError();
|
||||
@@ -70,7 +75,22 @@ internal static class WindowsSshChildProcess
|
||||
throw LastWin32("ResumeThread failed.");
|
||||
}
|
||||
|
||||
WaitForChildWithinOutputLimits(process.Process, outputs);
|
||||
bool exited = WaitForChildWithinOutputLimits(
|
||||
process.Process,
|
||||
outputs,
|
||||
diagnosticTimeoutMilliseconds
|
||||
);
|
||||
if (!exited)
|
||||
{
|
||||
// Why: the runner must recover verbose bytes without relying on an external
|
||||
// kill that also destroys the delete-on-close diagnostic captures.
|
||||
TerminateAndSettleJob(ref job);
|
||||
outputs.ReplayOutputs();
|
||||
throw new TimeoutException(
|
||||
"SSH child reached the " + diagnosticTimeoutMilliseconds +
|
||||
" ms diagnostic timeout."
|
||||
);
|
||||
}
|
||||
uint exitCode;
|
||||
if (!GetExitCodeProcess(process.Process, out exitCode))
|
||||
{
|
||||
@@ -177,18 +197,33 @@ internal static class WindowsSshChildProcess
|
||||
}
|
||||
}
|
||||
|
||||
private static void WaitForChildWithinOutputLimits(
|
||||
private static bool WaitForChildWithinOutputLimits(
|
||||
IntPtr process,
|
||||
WindowsBoundedOutputFiles outputs
|
||||
WindowsBoundedOutputFiles outputs,
|
||||
int diagnosticTimeoutMilliseconds
|
||||
)
|
||||
{
|
||||
Stopwatch elapsed = Stopwatch.StartNew();
|
||||
while (true)
|
||||
{
|
||||
uint waitResult = WaitForSingleObject(process, OutputLimitPollMilliseconds);
|
||||
uint waitMilliseconds = OutputLimitPollMilliseconds;
|
||||
if (diagnosticTimeoutMilliseconds > 0)
|
||||
{
|
||||
long remaining = diagnosticTimeoutMilliseconds - elapsed.ElapsedMilliseconds;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (remaining < waitMilliseconds)
|
||||
{
|
||||
waitMilliseconds = (uint)remaining;
|
||||
}
|
||||
}
|
||||
uint waitResult = WaitForSingleObject(process, waitMilliseconds);
|
||||
if (waitResult == WaitObject0)
|
||||
{
|
||||
outputs.EnsureWithinLimits();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (waitResult == WaitFailed)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types'
|
||||
import { buildSshArgs } from './system-ssh-args'
|
||||
import { findSystemSsh } from './system-ssh-binary'
|
||||
|
||||
type DiagnosticMode = 'private-console-regular-output-launcher-no-n'
|
||||
type DiagnosticMode = 'private-console-regular-output-verbose-timeout'
|
||||
|
||||
type DiagnosticResult = {
|
||||
mode: DiagnosticMode
|
||||
@@ -19,6 +19,7 @@ type DiagnosticResult = {
|
||||
channelClosed: boolean
|
||||
closeCode: number | null | 'not-observed'
|
||||
stderrBytes: number
|
||||
stderrTail: string
|
||||
elapsedMs: number
|
||||
}
|
||||
|
||||
@@ -48,19 +49,26 @@ async function runDiagnostic(): Promise<DiagnosticResult> {
|
||||
if (!sshPath) {
|
||||
throw new Error('Native Windows OpenSSH client is unavailable')
|
||||
}
|
||||
const mode: DiagnosticMode = 'private-console-regular-output-launcher-no-n'
|
||||
const mode: DiagnosticMode = 'private-console-regular-output-verbose-timeout'
|
||||
const startedAt = performance.now()
|
||||
// Why: Win32-OpenSSH still hangs with proven console EOF plus piped output, so this diagnostic
|
||||
// isolates stdout/stderr as bounded regular files without changing production transport.
|
||||
const sshArgs = buildSshArgs(createTarget())
|
||||
const destinationSeparator = sshArgs.indexOf('--')
|
||||
if (destinationSeparator < 0) {
|
||||
throw new Error('Windows OpenSSH diagnostic arguments lost the destination separator')
|
||||
}
|
||||
sshArgs.splice(destinationSeparator, 0, '-vvv')
|
||||
const child = spawn(
|
||||
launcherPath as string,
|
||||
[sshPath, ...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'],
|
||||
['--diagnostic-timeout-ms', '6000', sshPath, ...sshArgs, 'echo ORCA-SYSTEM-SSH-OK'],
|
||||
{ stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }
|
||||
)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = ''
|
||||
let stderrBytes = 0
|
||||
let stderrTail = ''
|
||||
let stdoutEnded = false
|
||||
let processExit: number | null | 'not-observed' = 'not-observed'
|
||||
let channelClosed = false
|
||||
@@ -91,6 +99,7 @@ async function runDiagnostic(): Promise<DiagnosticResult> {
|
||||
channelClosed,
|
||||
closeCode,
|
||||
stderrBytes,
|
||||
stderrTail,
|
||||
elapsedMs: performance.now() - startedAt
|
||||
})
|
||||
}
|
||||
@@ -115,6 +124,8 @@ async function runDiagnostic(): Promise<DiagnosticResult> {
|
||||
})
|
||||
child.stderr?.on('data', (chunk: Buffer) => {
|
||||
stderrBytes += chunk.length
|
||||
// Why: retain only the diagnostic tail needed to classify the stuck client phase.
|
||||
stderrTail = (stderrTail + chunk.toString('utf8')).slice(-16 * 1024)
|
||||
})
|
||||
child.on('exit', (code) => {
|
||||
processExit = code
|
||||
@@ -138,16 +149,16 @@ async function runDiagnostic(): Promise<DiagnosticResult> {
|
||||
|
||||
describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => {
|
||||
it(
|
||||
'qualifies private-console EOF plus regular-file output without the Windows null-input option',
|
||||
'captures the verbose client phase after a bounded private-console diagnostic timeout',
|
||||
{ timeout: 15_000 },
|
||||
async () => {
|
||||
expect(process.platform).toBe('win32')
|
||||
const privateConsoleRegularOutputLauncherNoN = await runDiagnostic()
|
||||
const privateConsoleRegularOutputVerboseTimeout = await runDiagnostic()
|
||||
console.log(
|
||||
`ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleRegularOutputLauncherNoN })}`
|
||||
`ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleRegularOutputVerboseTimeout })}`
|
||||
)
|
||||
|
||||
expect(privateConsoleRegularOutputLauncherNoN.success).toBe(true)
|
||||
expect(privateConsoleRegularOutputVerboseTimeout.success).toBe(true)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user