mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
test(ssh): add private-console Windows diagnostic
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -12,8 +12,9 @@ const repoRoot = resolve(import.meta.dirname, '../..')
|
||||
const sourceRoot = join(repoRoot, 'native', 'windows-ssh-no-input-launcher')
|
||||
const sourcePaths = [
|
||||
'OrcaSshNoInputLauncher.cs',
|
||||
'WindowsAnonymousPipeSet.cs',
|
||||
'WindowsCommandLine.cs',
|
||||
'WindowsPrivateConsoleInput.cs',
|
||||
'WindowsSshChildIo.cs',
|
||||
'WindowsSshChildProcess.cs'
|
||||
].map((name) => join(sourceRoot, name))
|
||||
const outputPath = readArg('--output') ?? join(sourceRoot, '.build', 'orca-ssh-no-input.exe')
|
||||
|
||||
@@ -19,6 +19,7 @@ let fixtureRoot
|
||||
let launcherPath
|
||||
let childFixturePath
|
||||
let handleProbePath
|
||||
let consoleEofProbePath
|
||||
|
||||
beforeAll(() => {
|
||||
if (process.platform !== 'win32') {
|
||||
@@ -28,6 +29,7 @@ beforeAll(() => {
|
||||
launcherPath = join(fixtureRoot, 'orca-ssh-no-input.exe')
|
||||
childFixturePath = join(fixtureRoot, 'child-fixture.cjs')
|
||||
handleProbePath = join(fixtureRoot, 'windows-launcher-handle-probe.exe')
|
||||
consoleEofProbePath = join(fixtureRoot, 'windows-console-eof-probe.exe')
|
||||
writeFileSync(childFixturePath, childFixtureSource, 'utf8')
|
||||
const build = spawnSync(process.execPath, [buildScript, '--output', launcherPath], {
|
||||
cwd: projectRoot,
|
||||
@@ -57,6 +59,27 @@ beforeAll(() => {
|
||||
{ cwd: projectRoot, encoding: 'utf8' }
|
||||
)
|
||||
expect(probeBuild.status, `${probeBuild.stdout}\n${probeBuild.stderr}`).toBe(0)
|
||||
|
||||
const eofProbeSource = join(
|
||||
projectRoot,
|
||||
'native',
|
||||
'windows-ssh-no-input-launcher-test',
|
||||
'WindowsConsoleEofProbe.cs'
|
||||
)
|
||||
const eofProbeBuild = spawnSync(
|
||||
compilerPath,
|
||||
[
|
||||
'/nologo',
|
||||
'/target:exe',
|
||||
'/platform:anycpu',
|
||||
'/optimize+',
|
||||
'/warnaserror+',
|
||||
`/out:${consoleEofProbePath}`,
|
||||
eofProbeSource
|
||||
],
|
||||
{ cwd: projectRoot, encoding: 'utf8' }
|
||||
)
|
||||
expect(eofProbeBuild.status, `${eofProbeBuild.stdout}\n${eofProbeBuild.stderr}`).toBe(0)
|
||||
}, 20_000)
|
||||
|
||||
afterAll(() => {
|
||||
@@ -68,16 +91,28 @@ 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 pipeSource = readFileSync(join(sourceRoot, 'WindowsAnonymousPipeSet.cs'), 'utf8')
|
||||
const consoleSource = readFileSync(join(sourceRoot, 'WindowsPrivateConsoleInput.cs'), 'utf8')
|
||||
const pipeSource = readFileSync(join(sourceRoot, 'WindowsSshChildIo.cs'), 'utf8')
|
||||
|
||||
expect(processSource).toContain('ProcThreadAttributeHandleList')
|
||||
expect(processSource).toContain('IntPtr.Size * 3')
|
||||
expect(processSource).toContain('CreateSuspended')
|
||||
expect(processSource).toContain('CreateNoWindow')
|
||||
expect(processSource).not.toContain('CreateNoWindow')
|
||||
expect(processSource).toContain('JobObjectLimitKillOnJobClose')
|
||||
expect(processSource).toContain('WaitForMultipleObjects')
|
||||
expect(processSource).toContain('if (processStarted && !assignedToJob)')
|
||||
expect(pipeSource).toContain('CloseHandleChecked(stdinWrite)')
|
||||
expect(consoleSource).toContain('AllocConsole()')
|
||||
expect(consoleSource).toContain('"CONIN$"')
|
||||
expect(consoleSource).toContain('ShowWindow(window, SwHide)')
|
||||
expect(consoleSource).toContain('WriteConsoleInput')
|
||||
expect(consoleSource).toContain('console.QueueEndOfInput()')
|
||||
expect(processSource).toContain('WindowsPrivateConsoleInput.Create()')
|
||||
expect(consoleSource.indexOf('console.QueueEndOfInput()')).toBeLessThan(
|
||||
consoleSource.indexOf('return console;')
|
||||
)
|
||||
expect(processSource.indexOf('WindowsPrivateConsoleInput.Create()')).toBeLessThan(
|
||||
processSource.indexOf('CreateProcess(')
|
||||
)
|
||||
expect(pipeSource).toContain('PumpCompletionTimeoutMilliseconds')
|
||||
expect(pipeSource).toContain('pumpFailureSignal.Set()')
|
||||
})
|
||||
@@ -95,14 +130,29 @@ describe('Windows SSH no-input launcher', () => {
|
||||
}
|
||||
)
|
||||
|
||||
itWindows('preserves exact arguments and provides child stdin EOF before execution', () => {
|
||||
itWindows('preserves exact arguments and qualifies private-console stdin', () => {
|
||||
const args = ['', 'space value', 'quote"value', 'trailing slash\\', 'line one\nline two', '雪']
|
||||
const result = runLauncher('arguments-and-stdin', args, {
|
||||
input: Buffer.from('this input must not reach the SSH child', 'utf8')
|
||||
})
|
||||
|
||||
expect(result.status, result.stderr.toString('utf8')).toBe(0)
|
||||
expect(JSON.parse(result.stdout.toString('utf8'))).toEqual({ args, stdinBytes: 0 })
|
||||
expect(JSON.parse(result.stdout.toString('utf8'))).toEqual({
|
||||
args,
|
||||
stdinIsTTY: true
|
||||
})
|
||||
})
|
||||
|
||||
itWindows('queues cooked private-console EOF before child execution', () => {
|
||||
const result = spawnSync(launcherPath, [consoleEofProbePath], {
|
||||
cwd: fixtureRoot,
|
||||
encoding: 'utf8',
|
||||
timeout: 10_000,
|
||||
windowsHide: true
|
||||
})
|
||||
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
expect(result.stdout).toBe('ORCA-PRIVATE-CONSOLE-EOF bytes=0')
|
||||
})
|
||||
|
||||
itWindows('preserves binary stdout and stderr and propagates the child exit code', () => {
|
||||
@@ -219,12 +269,10 @@ async function waitForProcessExit(pid, timeoutMs) {
|
||||
|
||||
const childFixtureSource = String.raw`const mode = process.argv[2]
|
||||
if (mode === 'arguments-and-stdin') {
|
||||
let stdinBytes = 0
|
||||
process.stdin.on('data', (chunk) => { stdinBytes += chunk.length })
|
||||
process.stdin.on('end', () => {
|
||||
process.stdout.write(JSON.stringify({ args: process.argv.slice(3), stdinBytes }))
|
||||
})
|
||||
process.stdin.resume()
|
||||
process.stdout.write(JSON.stringify({
|
||||
args: process.argv.slice(3),
|
||||
stdinIsTTY: process.stdin.isTTY === true
|
||||
}))
|
||||
} else if (mode === 'binary-output') {
|
||||
process.stdout.write(Buffer.from([0x00, 0xff, 0x10, 0x0a, 0x80]))
|
||||
process.stderr.write(Buffer.from([0xfe, 0x00, 0x7f, 0x0d, 0x0a]))
|
||||
|
||||
+23
-4
@@ -1,6 +1,6 @@
|
||||
# SSH Relay Runtime Distribution — Short Implementation Checklist
|
||||
|
||||
Last updated: 2026-07-15
|
||||
Last updated: 2026-07-16
|
||||
|
||||
Use this file to track the project. The
|
||||
[detailed evidence ledger](./2026-07-14-ssh-relay-github-release-implementation-checklist.md)
|
||||
@@ -9,8 +9,8 @@ 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 Windows
|
||||
launcher compiler correction, 2026-07-15, Codex implementation owner.**
|
||||
Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected private-
|
||||
console Windows diagnostic, 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
|
||||
@@ -168,7 +168,26 @@ ARM64 live evidence next. Do not package or call it from production; SignPath re
|
||||
`29550660572`, and independent x64/ARM64 jobs `87792250731`/`87792250710`: both legacy compilers
|
||||
reject one constant/structure identifier collision before emitting an executable. The isolated
|
||||
correction renames only the integer information-class constant; focused 12/12 and release contracts
|
||||
285/285 pass locally. Commit/push and rerun both architectures next.
|
||||
285/285 pass locally. Corrected exact head `6a716de14`, run `29551003702`, and x64/ARM64 jobs
|
||||
`87793308016`/`87793308044` then pass legacy compilation and every native launcher contract plus the
|
||||
complete 101-file/859-case artifact contract step, but the live diagnostic still times out after
|
||||
8024.0679/8031.6544 milliseconds. Both observe ended stdout, a closed SSH channel, no sentinel or
|
||||
process exit, and zero stderr; both exact profile teardowns remain RED on locked `UsrClass.dat`.
|
||||
`E-M6-WINDOWS-NOINPUT-LAUNCHER-LIVE-CI-RED-001` therefore falsifies Node's pipe type and broad
|
||||
inherited-handle leakage. Next test only a hidden launcher-owned real-console stdin with console EOF
|
||||
queued before child creation while retaining private output pipes, exact handle inheritance, job
|
||||
ownership, and bounded cancellation. Keep it runner-temp-only and disconnected from product;
|
||||
SignPath remains deferred.
|
||||
`E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001` records the replacement candidate locally green. It
|
||||
captures launcher output before detaching, allocates and hides a private console, queues Ctrl+Z plus
|
||||
Enter into `CONIN$` before child creation, and shares that console with the suspended/job-owned SSH
|
||||
child while retaining private output pipes and the exact three-handle list. A dedicated native probe
|
||||
requires character-device stdin, successful `GetConsoleMode`, and a zero-byte cooked `ReadFile`.
|
||||
Focused 119/119 with seven native skips, all 285 release contracts with five native skips, typecheck,
|
||||
full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. Commit/push
|
||||
this artifact-only package and require fresh x64/ARM64 compiler, console/EOF, launcher, live SSH, and
|
||||
exact teardown proof; no product, Beta, fallback, tuple, publication, default, or SignPath work is
|
||||
included.
|
||||
`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
|
||||
|
||||
+94
-16
@@ -7,9 +7,9 @@ This file is the detailed evidence ledger. Use the tracker above for project sta
|
||||
work; keep exact commands, runner identities, hashes, metrics, and residual gaps here.
|
||||
|
||||
Date created: 2026-07-14<br>
|
||||
Last updated: 2026-07-15<br>
|
||||
Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows launcher compiler correction**. `E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001` records exact head `1f220a7135b83ef4c5f035ab729c114e828bf5b3`, run `29550660572`, and the same `CS0102` identifier collision from Windows x64/ARM64 jobs `87792250731`/`87792250710`. Rename only the integer job-information-class constant, re-prove local contracts, and require fresh native x64/ARM64 evidence. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain deferred.<br>
|
||||
Session checkpoint: **LAUNCHER LOCAL CONTRACTS GREEN / X64 AND ARM64 LEGACY COMPILERS RED / ONE-IDENTIFIER CORRECTION ACTIVE — 2026-07-15, Codex implementation owner** — local focused 119/119, release contracts 285/285, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. Both native jobs independently reject the constant/structure name collision before producing an executable or reaching live SSH; the runner-temp build, live fixture, full-size test, upload, production caller, legacy behavior, Beta, tuple publication, and SignPath remain untouched. Rename only `JobObjectExtendedLimitInformation` constant to `JobObjectExtendedLimitInformationClass`, commit/push, and rerun both native architectures.<br>
|
||||
Last updated: 2026-07-16<br>
|
||||
Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-16, Codex implementation owner: disconnected private-console Windows diagnostic native proof**. `E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001` records the unpackaged, unsigned, runner-temp diagnostic locally green. Commit/push only this artifact-only package and require fresh Windows x64/ARM64 legacy-compiler, real-console qualification, pre-execution cooked EOF, argv/binary/exit/handle-exclusion/cancellation, live lifecycle, and exact teardown proof. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain deferred.<br>
|
||||
Session checkpoint: **PRIVATE CONSOLE CONTRACTS LOCALLY GREEN / X64 AND ARM64 NATIVE PROOF REQUIRED — 2026-07-16, Codex implementation owner** — the launcher captures its caller output streams, detaches from any inherited console, allocates and hides a private console, queues Ctrl+Z plus Enter into `CONIN$` before child creation, and shares that console with the suspended/job-owned child while retaining private stdout/stderr pipes and the exact three-handle list. A dedicated native probe requires character-device stdin, successful `GetConsoleMode`, and zero-byte cooked `ReadFile`. Local focused 119/119 with seven native skips, release contracts 285/285 with five native skips, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. No product caller, full-size 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)
|
||||
|
||||
@@ -19627,8 +19627,8 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de
|
||||
`eee686d7f6203c88f00d607711d3816aaf444c61`, artifact run `29549252407`, Windows x64 job
|
||||
`87788106340` on `windows-2022`, and Windows ARM64 job `87788106351` on `windows-11-arm`.
|
||||
- Exact log command: `gh api repos/stablyai/orca/actions/jobs/<job-id>/logs | rg -n -C 15
|
||||
'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile
|
||||
teardown:|UsrClass.dat|regular-file-eof-no-n|stdinFixtureRemoved|sentinel|stderrBytes'` for each
|
||||
'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile
|
||||
teardown:|UsrClass.dat|regular-file-eof-no-n|stdinFixtureRemoved|sentinel|stderrBytes'` for each
|
||||
job.
|
||||
- Native result: x64 returns `success=false`, `timedOut=true`, `sentinel=false`,
|
||||
`stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, `stderrBytes=0`,
|
||||
@@ -19678,15 +19678,15 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de
|
||||
references in that step. Runtime archives, release evidence, product imports, production system-
|
||||
SSH arguments/handles, legacy fallback/default, Beta, tuple publication, and signing are unchanged.
|
||||
- Focused command: `/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 cases with six declared native
|
||||
--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 cases with six declared native
|
||||
skips in 4.73 seconds real; maximum RSS is 167,936,000 bytes and peak footprint is 98,735,864
|
||||
bytes.
|
||||
- Release contracts: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts
|
||||
--maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and
|
||||
--maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and
|
||||
285/285 runnable cases with four declared Windows skips in 25.07 seconds real; maximum RSS is
|
||||
195,428,352 bytes and peak footprint is 97,638,160 bytes. One preceding quoted-glob invocation
|
||||
selected no files and exited 1; it is recorded as operator error and is not evidence.
|
||||
@@ -19709,7 +19709,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de
|
||||
`1f220a7135b83ef4c5f035ab729c114e828bf5b3`, artifact run `29550660572`, Windows x64 job
|
||||
`87792250731` on `windows-2022`, and Windows ARM64 job `87792250710` on `windows-11-arm`.
|
||||
- Exact log command: `gh api repos/stablyai/orca/actions/jobs/<job-id>/logs | rg -n -C 8
|
||||
'WindowsSshChildProcess.cs\\(14|error CS0102|Failed Suites|runtime-windows-no-input-launcher'`
|
||||
'WindowsSshChildProcess.cs\\(14|error CS0102|Failed Suites|runtime-windows-no-input-launcher'`
|
||||
for each job.
|
||||
- Native result: both architectures reach the purpose-named contract suite and the .NET Framework
|
||||
compiler independently returns `CS0102`: the integer job-information-class constant and the
|
||||
@@ -19729,6 +19729,84 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de
|
||||
declared Windows skips. Syntax, `git diff --check`, and protected-resolver zero-diff pass. Commit/
|
||||
push the correction and require both native architectures from the corrected exact head.
|
||||
|
||||
### E-M6-WINDOWS-NOINPUT-LAUNCHER-LIVE-CI-RED-001 — private redirected pipes do not settle Win32-OpenSSH
|
||||
|
||||
- Date/owner/head/run: 2026-07-16, Codex implementation owner; exact pushed head
|
||||
`6a716de14884f9c08c1e08140f87849f137622e2`, artifact run `29551003702`, Windows x64 job
|
||||
`87793308016` on `windows-2022`, and Windows ARM64 job `87793308044` on `windows-11-arm`.
|
||||
GitHub records the jobs from 2026-07-17 02:53Z through 03:03Z.
|
||||
- Native launcher proof: both architectures compile with the legacy .NET Framework compiler and
|
||||
pass exact argv quoting, pre-execution stdin EOF, binary stdout/stderr, exit-code propagation,
|
||||
unrelated inherited-handle exclusion, job-owned cancellation, and bounded settlement. Each full
|
||||
artifact contract step passes 101 files, 859 runnable cases, and 19 declared skips. Both jobs
|
||||
build and verify the unchanged full runtime before starting the disconnected live fixture.
|
||||
- Live x64 result: `elapsedMs=8024.0679`, `success=false`, `timedOut=true`, `sentinel=false`,
|
||||
`stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, and
|
||||
`stderrBytes=0`.
|
||||
- Live ARM64 result: `elapsedMs=8031.6544` with the same remaining lifecycle fields. Both official
|
||||
server logs prove authentication and remote command-session closure. The launcher remains alive
|
||||
until the bounded diagnostic terminates it, so this is not transfer or enablement evidence.
|
||||
- Teardown result: both exact owned-profile teardowns remain RED because `UsrClass.dat` is locked.
|
||||
The workflow correctly stops before the unchanged product full-size Windows transfer and upload.
|
||||
All non-Windows primary jobs and both Linux oldest-userland supplements pass in the same run.
|
||||
- Interpretation: Node's Windows pipe type and broad inherited-handle leakage are falsified as the
|
||||
cause. Win32-OpenSSH issue #1330 separately reports the same hang when all standard handles are
|
||||
redirected, including cases where `-n`/NUL and `-T` do not help, while a real console stdin
|
||||
behaves differently. Do not add sentinel-based forced success or kill-after-output: trustworthy
|
||||
remote exit-status framing would be a separate reviewed protocol contract.
|
||||
- Next bounded diagnostic: allocate a launcher-owned private console, hide it, open `CONIN$`, queue
|
||||
console EOF input before `CreateProcessW`, and share that console with the SSH child. Retain
|
||||
private stdout/stderr pipes, the exact three-handle inheritance list, suspended job assignment,
|
||||
bounded pumps, exit propagation, and cancellation ownership. First prove the precise console EOF
|
||||
semantics with a native purpose-named probe, then require the live x64 and ARM64 lifecycle and
|
||||
exact teardown. Keep the executable runner-temp-only and disconnected from product packaging.
|
||||
|
||||
### E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001 — private real-console candidate is locally contract-green
|
||||
|
||||
- Date/owner/base/runner: 2026-07-16, Codex implementation owner; uncommitted capability atop exact
|
||||
private-pipe RED head `6a716de14884f9c08c1e08140f87849f137622e2` on Apple arm64, macOS 26.2
|
||||
build 25C56, Node 26.5.0, and pnpm 10.24.0.
|
||||
- Source audit: PowerShell/openssh-portable exact source `e98b11e5de035cbd0eecbddc95a87da42f14671f`
|
||||
initializes Win32 standard handles as synchronous external I/O, classifies console input as
|
||||
`FILE_TYPE_CHAR`, and routes it through the dedicated synchronous console `ReadFile` path in
|
||||
`contrib/win32/win32compat/w32fd.c`, `fileio.c`, and `termio.c`. Orca's existing `-T` in
|
||||
`system-ssh-args.ts` prevents real-console stdin from requesting a remote PTY.
|
||||
- Artifact boundary: the launcher captures its caller's stdout/stderr streams before detaching from
|
||||
any inherited console, allocates a private console, hides any console window, opens inheritable
|
||||
`CONIN$`, selects processed line input, flushes it, and queues Ctrl+Z key-down/up plus Enter key-
|
||||
down/up before `CreateProcessW`. The child shares that private console; `CREATE_NO_WINDOW` is
|
||||
deliberately absent because it would detach the child from the console. Private anonymous stdout/
|
||||
stderr pipes, exact three-handle inheritance, suspended job assignment, bounded pumps, exit
|
||||
propagation, and kill-on-job-close cancellation remain intact.
|
||||
- Native contract design: a purpose-named legacy-compiler probe rejects non-character stdin or
|
||||
failed `GetConsoleMode`, selects cooked input, directly calls `ReadFile`, and requires zero bytes.
|
||||
The existing suite separately requires console-qualified stdin plus exact argv, binary output,
|
||||
exit propagation, unrelated inherited-handle exclusion, and bounded job-owned cancellation.
|
||||
Five Windows-native cases are declared skips locally; neither the console semantics nor the live
|
||||
OpenSSH result is claimed without x64 and ARM64 runners.
|
||||
- Focused command: `/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 seven declared
|
||||
native/live skips in 3.27 seconds real; maximum RSS is 171,180,032 bytes and peak footprint is
|
||||
96,999,352 bytes.
|
||||
- Release contracts: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1
|
||||
--reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285/285 runnable
|
||||
cases with five declared Windows skips in 16.17 seconds. One prior quoted-glob invocation selected
|
||||
no files and exited 1; it is operator error and not evidence.
|
||||
- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 3.55 seconds real with maximum RSS
|
||||
1,257,357,312 bytes and peak footprint 97,507,040 bytes. `/usr/bin/time -l pnpm lint` passes in
|
||||
19.82 seconds real with the same 26 existing warnings, all 41 reliability gates, and the 355-entry
|
||||
max-lines ratchet; maximum RSS is 2,041,757,696 bytes and peak footprint 97,179,360 bytes. Purpose-
|
||||
file formatting, syntax, `git diff --check`, and protected-resolver zero-diff pass.
|
||||
- Residual boundary: commit/push this exact artifact-only package and require native x64/ARM64
|
||||
legacy compilation plus all purpose-named contracts before interpreting the live result. A green
|
||||
live diagnostic still would not authorize product packaging/caller, full-size transfer, fallback,
|
||||
Beta, tuple enablement, publication, default behavior, or signing. A RED native probe or live
|
||||
result must be recorded before selecting another mechanism such as ConPTY.
|
||||
|
||||
## Accepted Gaps
|
||||
|
||||
No product gap is accepted merely because it appears in this list. Each entry requires explicit
|
||||
@@ -19787,10 +19865,10 @@ The project is not complete until every applicable item below is checked with ev
|
||||
|
||||
## Next Required Action
|
||||
|
||||
Commit and push only the one-identifier compiler correction recorded by
|
||||
`E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001`, then require exact-head Windows x64 and ARM64 legacy-
|
||||
compiler, argv/EOF/binary/exit/handle-exclusion/cancellation, live diagnostic, and exact profile/
|
||||
directory teardown evidence. Do not
|
||||
Commit and push only the private real-console stdin diagnostic recorded by
|
||||
`E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001`, then require exact-head Windows x64 and ARM64 legacy-
|
||||
compiler, console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, live
|
||||
diagnostic, 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
|
||||
|
||||
@@ -1657,15 +1657,21 @@
|
||||
created zero-length regular-file descriptor that is already at EOF before
|
||||
<code>CreateProcess</code>; its fixture cleanup passes, but the direct client
|
||||
still times out after the server closes the command session. The next bounded
|
||||
artifact-only capability is a disconnected Windows launcher with private
|
||||
artifact-only capability was a disconnected Windows launcher with private
|
||||
anonymous stdin/stdout/stderr pipes, the stdin writer closed before
|
||||
<code>CreateProcessW</code>, an exact inherited-handle list, hidden process
|
||||
creation, bounded stdout/stderr pumps, exit propagation, and an owned job object
|
||||
that kills the SSH child when the launcher is cancelled. Build and live-test it
|
||||
on native x64/arm64 before packaging, signing, or adding 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;
|
||||
that kills the SSH child when the launcher is cancelled. Exact native x64/arm64
|
||||
evidence proves that launcher compiles and passes all of those contracts, but
|
||||
Win32-OpenSSH still remains alive after authenticated remote command-session
|
||||
closure. This falsifies Node's pipe type and broad inherited-handle leakage. The
|
||||
next artifact-only diagnostic uses a hidden launcher-owned private real console,
|
||||
queues console EOF before child creation, and shares that console with the SSH
|
||||
child while retaining private output pipes, exact handle inheritance, job
|
||||
ownership, and bounded cancellation. Build and live-test it on native x64/arm64
|
||||
before packaging, signing, or adding 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
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
internal static class WindowsConsoleEofProbe
|
||||
{
|
||||
private const int StandardInputHandle = -10;
|
||||
private const uint FileTypeChar = 0x0002;
|
||||
private const uint EnableProcessedInput = 0x0001;
|
||||
private const uint EnableLineInput = 0x0002;
|
||||
|
||||
private static int Main()
|
||||
{
|
||||
IntPtr input = GetStdHandle(StandardInputHandle);
|
||||
uint mode;
|
||||
if (input == IntPtr.Zero || input == new IntPtr(-1) || GetFileType(input) != FileTypeChar)
|
||||
{
|
||||
Console.Error.WriteLine("stdin is not a character-device handle");
|
||||
return 2;
|
||||
}
|
||||
if (!GetConsoleMode(input, out mode))
|
||||
{
|
||||
Console.Error.WriteLine("stdin is not a console input handle");
|
||||
return 3;
|
||||
}
|
||||
if (!SetConsoleMode(input, mode | EnableProcessedInput | EnableLineInput))
|
||||
{
|
||||
Console.Error.WriteLine("unable to select cooked console input");
|
||||
return 4;
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[1];
|
||||
uint bytesRead;
|
||||
if (!ReadFile(input, buffer, (uint)buffer.Length, out bytesRead, IntPtr.Zero))
|
||||
{
|
||||
Console.Error.WriteLine("console ReadFile failed: {0}", Marshal.GetLastWin32Error());
|
||||
return 5;
|
||||
}
|
||||
Console.Out.Write("ORCA-PRIVATE-CONSOLE-EOF bytes={0}", bytesRead);
|
||||
return bytesRead == 0 ? 0 : 6;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetStdHandle(int standardHandle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint GetFileType(IntPtr file);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetConsoleMode(IntPtr console, out uint mode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetConsoleMode(IntPtr console, uint mode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool ReadFile(
|
||||
IntPtr file,
|
||||
[Out] byte[] buffer,
|
||||
uint bytesToRead,
|
||||
out uint bytesRead,
|
||||
IntPtr overlapped
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
internal sealed class WindowsPrivateConsoleInput : IDisposable
|
||||
{
|
||||
private const uint EnableProcessedInput = 0x0001;
|
||||
private const uint EnableLineInput = 0x0002;
|
||||
private const uint GenericRead = 0x80000000;
|
||||
private const uint GenericWrite = 0x40000000;
|
||||
private const uint FileShareRead = 0x00000001;
|
||||
private const uint FileShareWrite = 0x00000002;
|
||||
private const uint OpenExisting = 3;
|
||||
private const uint HandleFlagInherit = 0x00000001;
|
||||
private const short KeyEvent = 0x0001;
|
||||
private const ushort VirtualKeyZ = 0x5a;
|
||||
private const ushort VirtualKeyReturn = 0x0d;
|
||||
private const uint MapVirtualKeyToScanCode = 0;
|
||||
private const uint LeftCtrlPressed = 0x0008;
|
||||
private const int SwHide = 0;
|
||||
private const int ErrorInvalidHandle = 6;
|
||||
|
||||
private IntPtr inputHandle;
|
||||
private bool ownsConsole;
|
||||
|
||||
private WindowsPrivateConsoleInput()
|
||||
{
|
||||
}
|
||||
|
||||
internal IntPtr InputHandle { get { return inputHandle; } }
|
||||
|
||||
internal static WindowsPrivateConsoleInput Create()
|
||||
{
|
||||
WindowsPrivateConsoleInput console = new WindowsPrivateConsoleInput();
|
||||
try
|
||||
{
|
||||
console.DetachFromInheritedConsole();
|
||||
if (!AllocConsole())
|
||||
{
|
||||
throw LastWin32("AllocConsole failed.");
|
||||
}
|
||||
console.ownsConsole = true;
|
||||
HideConsoleWindow();
|
||||
|
||||
console.inputHandle = CreateFile(
|
||||
"CONIN$",
|
||||
GenericRead | GenericWrite,
|
||||
FileShareRead | FileShareWrite,
|
||||
IntPtr.Zero,
|
||||
OpenExisting,
|
||||
0,
|
||||
IntPtr.Zero
|
||||
);
|
||||
if (console.inputHandle == new IntPtr(-1))
|
||||
{
|
||||
console.inputHandle = IntPtr.Zero;
|
||||
throw LastWin32("Unable to open the private console input buffer.");
|
||||
}
|
||||
if (!SetConsoleMode(console.inputHandle, EnableProcessedInput | EnableLineInput))
|
||||
{
|
||||
throw LastWin32("SetConsoleMode failed for private console input.");
|
||||
}
|
||||
if (!SetHandleInformation(
|
||||
console.inputHandle,
|
||||
HandleFlagInherit,
|
||||
HandleFlagInherit
|
||||
))
|
||||
{
|
||||
throw LastWin32("Unable to make private console input inheritable.");
|
||||
}
|
||||
if (!FlushConsoleInputBuffer(console.inputHandle))
|
||||
{
|
||||
throw LastWin32("FlushConsoleInputBuffer failed.");
|
||||
}
|
||||
console.QueueEndOfInput();
|
||||
return console;
|
||||
}
|
||||
catch
|
||||
{
|
||||
console.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (inputHandle != IntPtr.Zero)
|
||||
{
|
||||
CloseHandle(inputHandle);
|
||||
inputHandle = IntPtr.Zero;
|
||||
}
|
||||
if (ownsConsole)
|
||||
{
|
||||
FreeConsole();
|
||||
ownsConsole = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueEndOfInput()
|
||||
{
|
||||
// Why: Win32 console EOF is Ctrl+Z followed by Enter in cooked input mode; queue it before
|
||||
// child creation so the SSH input worker cannot race launcher startup.
|
||||
InputRecord[] records = new InputRecord[]
|
||||
{
|
||||
CreateKeyRecord(true, VirtualKeyZ, '\u001a', LeftCtrlPressed),
|
||||
CreateKeyRecord(false, VirtualKeyZ, '\u001a', LeftCtrlPressed),
|
||||
CreateKeyRecord(true, VirtualKeyReturn, '\r', 0),
|
||||
CreateKeyRecord(false, VirtualKeyReturn, '\r', 0)
|
||||
};
|
||||
uint written;
|
||||
if (!WriteConsoleInput(inputHandle, records, (uint)records.Length, out written))
|
||||
{
|
||||
throw LastWin32("WriteConsoleInputW failed.");
|
||||
}
|
||||
if (written != (uint)records.Length)
|
||||
{
|
||||
throw new Win32Exception("WriteConsoleInputW queued an incomplete EOF sequence.");
|
||||
}
|
||||
}
|
||||
|
||||
private void DetachFromInheritedConsole()
|
||||
{
|
||||
if (!FreeConsole())
|
||||
{
|
||||
int error = Marshal.GetLastWin32Error();
|
||||
if (error != ErrorInvalidHandle)
|
||||
{
|
||||
throw new Win32Exception(error, "FreeConsole failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HideConsoleWindow()
|
||||
{
|
||||
IntPtr window = GetConsoleWindow();
|
||||
if (window == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ShowWindow(window, SwHide);
|
||||
if (IsWindowVisible(window))
|
||||
{
|
||||
throw new Win32Exception("The private SSH console window remained visible.");
|
||||
}
|
||||
}
|
||||
|
||||
private static InputRecord CreateKeyRecord(
|
||||
bool keyDown,
|
||||
ushort virtualKey,
|
||||
char character,
|
||||
uint controlState
|
||||
)
|
||||
{
|
||||
InputRecord record = new InputRecord();
|
||||
record.EventType = KeyEvent;
|
||||
record.KeyEvent = new KeyEventRecord();
|
||||
record.KeyEvent.KeyDown = keyDown;
|
||||
record.KeyEvent.RepeatCount = 1;
|
||||
record.KeyEvent.VirtualKeyCode = virtualKey;
|
||||
record.KeyEvent.VirtualScanCode = (ushort)MapVirtualKey(virtualKey, MapVirtualKeyToScanCode);
|
||||
record.KeyEvent.UnicodeChar = character;
|
||||
record.KeyEvent.ControlKeyState = controlState;
|
||||
return record;
|
||||
}
|
||||
|
||||
private static Win32Exception LastWin32(string message)
|
||||
{
|
||||
return new Win32Exception(Marshal.GetLastWin32Error(), message);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
|
||||
private struct InputRecord
|
||||
{
|
||||
[FieldOffset(0)] internal short EventType;
|
||||
[FieldOffset(4)] internal KeyEventRecord KeyEvent;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct KeyEventRecord
|
||||
{
|
||||
[MarshalAs(UnmanagedType.Bool)] internal bool KeyDown;
|
||||
internal ushort RepeatCount;
|
||||
internal ushort VirtualKeyCode;
|
||||
internal ushort VirtualScanCode;
|
||||
internal char UnicodeChar;
|
||||
internal uint ControlKeyState;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool FreeConsole();
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern IntPtr GetConsoleWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool ShowWindow(IntPtr window, int command);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsWindowVisible(IntPtr window);
|
||||
|
||||
[DllImport(
|
||||
"kernel32.dll",
|
||||
EntryPoint = "CreateFileW",
|
||||
CharSet = CharSet.Unicode,
|
||||
SetLastError = true
|
||||
)]
|
||||
private static extern IntPtr CreateFile(
|
||||
string fileName,
|
||||
uint desiredAccess,
|
||||
uint shareMode,
|
||||
IntPtr securityAttributes,
|
||||
uint creationDisposition,
|
||||
uint flagsAndAttributes,
|
||||
IntPtr templateFile
|
||||
);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetConsoleMode(IntPtr consoleInput, uint mode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool FlushConsoleInputBuffer(IntPtr consoleInput);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "WriteConsoleInputW", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool WriteConsoleInput(
|
||||
IntPtr consoleInput,
|
||||
[In] InputRecord[] records,
|
||||
uint recordCount,
|
||||
out uint recordsWritten
|
||||
);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint MapVirtualKey(uint code, uint mapType);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(IntPtr handle);
|
||||
}
|
||||
+24
-22
@@ -6,12 +6,14 @@ using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
internal sealed class WindowsAnonymousPipeSet : IDisposable
|
||||
internal sealed class WindowsSshChildIo : IDisposable
|
||||
{
|
||||
private const uint HandleFlagInherit = 0x00000001;
|
||||
private const int PumpCompletionTimeoutMilliseconds = 5000;
|
||||
|
||||
private IntPtr stdinRead;
|
||||
private readonly IntPtr stdinRead;
|
||||
private readonly Stream stdoutDestination;
|
||||
private readonly Stream stderrDestination;
|
||||
private IntPtr stdoutWrite;
|
||||
private IntPtr stderrWrite;
|
||||
private SafeFileHandle stdoutRead;
|
||||
@@ -21,8 +23,15 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable
|
||||
private Exception pumpFailure;
|
||||
private readonly ManualResetEvent pumpFailureSignal = new ManualResetEvent(false);
|
||||
|
||||
private WindowsAnonymousPipeSet()
|
||||
private WindowsSshChildIo(
|
||||
IntPtr stdinRead,
|
||||
Stream stdoutDestination,
|
||||
Stream stderrDestination
|
||||
)
|
||||
{
|
||||
this.stdinRead = stdinRead;
|
||||
this.stdoutDestination = stdoutDestination;
|
||||
this.stderrDestination = stderrDestination;
|
||||
}
|
||||
|
||||
internal IntPtr StdinRead { get { return stdinRead; } }
|
||||
@@ -33,21 +42,23 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable
|
||||
get { return pumpFailureSignal.SafeWaitHandle.DangerousGetHandle(); }
|
||||
}
|
||||
|
||||
internal static WindowsAnonymousPipeSet Create()
|
||||
internal static WindowsSshChildIo Create(
|
||||
IntPtr stdinRead,
|
||||
Stream stdoutDestination,
|
||||
Stream stderrDestination
|
||||
)
|
||||
{
|
||||
WindowsAnonymousPipeSet pipes = new WindowsAnonymousPipeSet();
|
||||
WindowsSshChildIo pipes = new WindowsSshChildIo(
|
||||
stdinRead,
|
||||
stdoutDestination,
|
||||
stderrDestination
|
||||
);
|
||||
try
|
||||
{
|
||||
SecurityAttributes security = new SecurityAttributes();
|
||||
security.Length = Marshal.SizeOf(typeof(SecurityAttributes));
|
||||
security.InheritHandle = true;
|
||||
|
||||
IntPtr stdinWrite;
|
||||
CreateCheckedPipe(out pipes.stdinRead, out stdinWrite, security);
|
||||
SetNonInheritable(stdinWrite);
|
||||
// Why: Win32-OpenSSH must see EOF before CreateProcessW, not after startup.
|
||||
CloseHandleChecked(stdinWrite);
|
||||
|
||||
IntPtr stdoutReadHandle;
|
||||
CreateCheckedPipe(out stdoutReadHandle, out pipes.stdoutWrite, security);
|
||||
SetNonInheritable(stdoutReadHandle);
|
||||
@@ -68,7 +79,6 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable
|
||||
|
||||
internal void CloseChildEndsInParent()
|
||||
{
|
||||
CloseOwnedHandle(ref stdinRead);
|
||||
CloseOwnedHandle(ref stdoutWrite);
|
||||
CloseOwnedHandle(ref stderrWrite);
|
||||
}
|
||||
@@ -77,11 +87,11 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable
|
||||
{
|
||||
SafeFileHandle stdoutHandle = stdoutRead;
|
||||
stdoutRead = null;
|
||||
stdoutPump = StartPump(stdoutHandle, Console.OpenStandardOutput(), "stdout");
|
||||
stdoutPump = StartPump(stdoutHandle, stdoutDestination, "stdout");
|
||||
|
||||
SafeFileHandle stderrHandle = stderrRead;
|
||||
stderrRead = null;
|
||||
stderrPump = StartPump(stderrHandle, Console.OpenStandardError(), "stderr");
|
||||
stderrPump = StartPump(stderrHandle, stderrDestination, "stderr");
|
||||
}
|
||||
|
||||
internal void CompletePumps()
|
||||
@@ -208,14 +218,6 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseHandleChecked(IntPtr handle)
|
||||
{
|
||||
if (!CloseHandle(handle))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "CloseHandle failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseOwnedHandle(ref IntPtr handle)
|
||||
{
|
||||
if (handle != IntPtr.Zero)
|
||||
@@ -8,7 +8,6 @@ internal static class WindowsSshChildProcess
|
||||
{
|
||||
private const uint CreateSuspended = 0x00000004;
|
||||
private const uint ExtendedStartupInfoPresent = 0x00080000;
|
||||
private const uint CreateNoWindow = 0x08000000;
|
||||
private const uint StartfUseStdHandles = 0x00000100;
|
||||
private const uint JobObjectLimitKillOnJobClose = 0x00002000;
|
||||
private const int JobObjectExtendedLimitInformationClass = 9;
|
||||
@@ -19,7 +18,14 @@ internal static class WindowsSshChildProcess
|
||||
|
||||
internal static int Run(string executablePath, string[] args)
|
||||
{
|
||||
using (WindowsAnonymousPipeSet pipes = WindowsAnonymousPipeSet.Create())
|
||||
Stream launcherStdout = Console.OpenStandardOutput();
|
||||
Stream launcherStderr = Console.OpenStandardError();
|
||||
using (WindowsPrivateConsoleInput console = WindowsPrivateConsoleInput.Create())
|
||||
using (WindowsSshChildIo pipes = WindowsSshChildIo.Create(
|
||||
console.InputHandle,
|
||||
launcherStdout,
|
||||
launcherStderr
|
||||
))
|
||||
{
|
||||
IntPtr job = IntPtr.Zero;
|
||||
IntPtr attributeList = IntPtr.Zero;
|
||||
@@ -32,7 +38,9 @@ internal static class WindowsSshChildProcess
|
||||
job = CreateKillOnCloseJob();
|
||||
StartupInfoEx startup = CreateStartupInfo(pipes, out attributeList, out inheritedHandles);
|
||||
StringBuilder commandLine = WindowsCommandLine.Build(executablePath, args);
|
||||
uint flags = CreateSuspended | ExtendedStartupInfoPresent | CreateNoWindow;
|
||||
// Why: the SSH child must share the hidden private console for CONIN$ to remain a
|
||||
// real console handle; CREATE_NO_WINDOW would detach it from that console.
|
||||
uint flags = CreateSuspended | ExtendedStartupInfoPresent;
|
||||
if (!CreateProcess(
|
||||
executablePath,
|
||||
commandLine,
|
||||
@@ -125,7 +133,7 @@ internal static class WindowsSshChildProcess
|
||||
}
|
||||
|
||||
private static StartupInfoEx CreateStartupInfo(
|
||||
WindowsAnonymousPipeSet pipes,
|
||||
WindowsSshChildIo pipes,
|
||||
out IntPtr attributeList,
|
||||
out IntPtr inheritedHandles
|
||||
)
|
||||
|
||||
@@ -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-pipe-launcher-no-n'
|
||||
type DiagnosticMode = 'private-console-launcher-no-n'
|
||||
|
||||
type DiagnosticResult = {
|
||||
mode: DiagnosticMode
|
||||
@@ -48,10 +48,10 @@ async function runDiagnostic(): Promise<DiagnosticResult> {
|
||||
if (!sshPath) {
|
||||
throw new Error('Native Windows OpenSSH client is unavailable')
|
||||
}
|
||||
const mode: DiagnosticMode = 'private-pipe-launcher-no-n'
|
||||
const mode: DiagnosticMode = 'private-console-launcher-no-n'
|
||||
const startedAt = performance.now()
|
||||
// Why: the disconnected launcher creates private child pipes so Win32-OpenSSH receives EOF
|
||||
// before CreateProcessW without depending on Node's Windows named-pipe behavior.
|
||||
// Why: redirected stdio hangs in Win32-OpenSSH even with private pipes; this diagnostic gives
|
||||
// only stdin a hidden real-console boundary with EOF queued before CreateProcessW.
|
||||
const child = spawn(
|
||||
launcherPath as string,
|
||||
[sshPath, ...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'],
|
||||
@@ -138,16 +138,16 @@ async function runDiagnostic(): Promise<DiagnosticResult> {
|
||||
|
||||
describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => {
|
||||
it(
|
||||
'qualifies private-pipe stdin EOF without the Windows null-input option',
|
||||
'qualifies private-console stdin EOF without the Windows null-input option',
|
||||
{ timeout: 15_000 },
|
||||
async () => {
|
||||
expect(process.platform).toBe('win32')
|
||||
const privatePipeLauncherNoN = await runDiagnostic()
|
||||
const privateConsoleLauncherNoN = await runDiagnostic()
|
||||
console.log(
|
||||
`ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privatePipeLauncherNoN })}`
|
||||
`ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleLauncherNoN })}`
|
||||
)
|
||||
|
||||
expect(privatePipeLauncherNoN.success).toBe(true)
|
||||
expect(privateConsoleLauncherNoN.success).toBe(true)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user