mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +00:00
fix(pty): contain Windows ConPTY input failures
This commit is contained in:
@@ -789,3 +789,52 @@ index 7b286d3d644c26141df516929703aa6e129df4b2..ec6bf3932c65b89c013ff133dc6bf46a
|
||||
return exports;
|
||||
};
|
||||
|
||||
diff --git a/lib/windowsTerminal.js b/lib/windowsTerminal.js
|
||||
index 3c38f89d14cece8081782a9600ebc582b7c2a62a..7cdfdf75c7396998cf93d135c5de6ef96668ceba 100644
|
||||
--- a/lib/windowsTerminal.js
|
||||
+++ b/lib/windowsTerminal.js
|
||||
@@ -103,6 +103,20 @@ var WindowsTerminal = /** @class */ (function (_super) {
|
||||
_this._name = name;
|
||||
_this._readable = true;
|
||||
_this._writable = true;
|
||||
+ // A ConPTY input-pipe error must retire only this terminal. Without a listener, Node promotes
|
||||
+ // errors such as write EAGAIN to uncaughtException and kills every PTY in the daemon.
|
||||
+ _this._agent.inSocket.on('error', function () {
|
||||
+ if (!_this._writable) {
|
||||
+ return;
|
||||
+ }
|
||||
+ _this._close();
|
||||
+ try {
|
||||
+ _this._agent.kill();
|
||||
+ }
|
||||
+ catch (_a) {
|
||||
+ // The failing pipe may have raced process exit; the terminal is already unwritable.
|
||||
+ }
|
||||
+ });
|
||||
_this._forwardEvents();
|
||||
return _this;
|
||||
}
|
||||
diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts
|
||||
index 13f6c6dbb3d7965c4699568f4704be3b6a33d947..0aacd793831ccfc9ff5c85f0117d87a7534b5d34 100644
|
||||
--- a/src/windowsTerminal.ts
|
||||
+++ b/src/windowsTerminal.ts
|
||||
@@ -115,5 +115,18 @@ export class WindowsTerminal extends Terminal {
|
||||
this._readable = true;
|
||||
this._writable = true;
|
||||
-
|
||||
+ // A ConPTY input-pipe error must retire only this terminal. Without a listener, Node promotes
|
||||
+ // errors such as write EAGAIN to uncaughtException and kills every PTY in the daemon.
|
||||
+ this._agent.inSocket.on('error', () => {
|
||||
+ if (!this._writable) {
|
||||
+ return;
|
||||
+ }
|
||||
+ this._close();
|
||||
+ try {
|
||||
+ this._agent.kill();
|
||||
+ } catch {
|
||||
+ // The failing pipe may have raced process exit; the terminal is already unwritable.
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
this._forwardEvents();
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -115,7 +115,7 @@ patchedDependencies:
|
||||
'@xterm/addon-webgl@0.20.0-beta.299': 94687e89a0115e6e6aa102837f986debdc029c091527ee5eb4a4e17ceaf9473e
|
||||
'@xterm/xterm@6.1.0-beta.303': 98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d
|
||||
lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673
|
||||
node-pty@1.1.0: 9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17
|
||||
node-pty@1.1.0: 9f6143903f8a8da310dd8a668dff51a83eb32a598cfe512c5e004156c6abb62c
|
||||
|
||||
importers:
|
||||
|
||||
@@ -156,7 +156,7 @@ importers:
|
||||
version: 3.3.1
|
||||
node-pty:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(patch_hash=9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17)
|
||||
version: 1.1.0(patch_hash=9f6143903f8a8da310dd8a668dff51a83eb32a598cfe512c5e004156c6abb62c)
|
||||
posthog-node:
|
||||
specifier: ^5.33.3
|
||||
version: 5.33.3
|
||||
@@ -12194,7 +12194,7 @@ snapshots:
|
||||
|
||||
node-int64@0.4.0: {}
|
||||
|
||||
node-pty@1.1.0(patch_hash=9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17):
|
||||
node-pty@1.1.0(patch_hash=9f6143903f8a8da310dd8a668dff51a83eb32a598cfe512c5e004156c6abb62c):
|
||||
dependencies:
|
||||
node-addon-api: 7.1.1
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { MacosLoginSessionDeathWatch } from './macos-login-session-death-watch'
|
||||
import { readCurrentProcessMacSystemResolverHealth } from '../network/macos-system-resolver-health'
|
||||
import { readCurrentDaemonReadyIdentity } from './daemon-ready-identity'
|
||||
import { publishDaemonPidFile } from './daemon-spawner'
|
||||
import { isNativePtyException } from './daemon-native-pty-exception'
|
||||
|
||||
export type ParsedDaemonArgs = {
|
||||
socketPath: string
|
||||
@@ -149,15 +150,7 @@ async function main(): Promise<void> {
|
||||
// crash the daemon — masking those would hide real issues.
|
||||
process.on('uncaughtException', (err) => {
|
||||
const msg = err?.message ?? ''
|
||||
const isNativeError =
|
||||
err?.name === 'Error' &&
|
||||
(msg.includes('pty') ||
|
||||
msg.includes('Pty') ||
|
||||
msg.includes('EIO') ||
|
||||
msg.includes('EPIPE') ||
|
||||
msg.includes('EBADF') ||
|
||||
msg.includes('ENXIO'))
|
||||
if (isNativeError) {
|
||||
if (isNativePtyException(err)) {
|
||||
daemonLog.log('uncaught-exception-suppressed', { name: err?.name, message: msg })
|
||||
console.error('[daemon] Native PTY exception (suppressed):', err)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isNativePtyException } from './daemon-native-pty-exception'
|
||||
|
||||
describe('isNativePtyException', () => {
|
||||
it.each([
|
||||
new Error('write EAGAIN'),
|
||||
new Error('read EIO'),
|
||||
new Error('Pty process exited'),
|
||||
Object.assign(new Error('write failed'), { code: 'EPIPE' })
|
||||
])('contains native PTY failures without killing the daemon', (error) => {
|
||||
expect(isNativePtyException(error)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([new Error('database invariant failed'), new TypeError('logic bug'), 'EAGAIN'])(
|
||||
'does not suppress unrelated or malformed failures',
|
||||
(error) => {
|
||||
expect(isNativePtyException(error)).toBe(false)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
const NATIVE_PTY_ERROR_CODE_PATTERN = /\b(?:EIO|EPIPE|EBADF|ENXIO|EAGAIN)\b/
|
||||
|
||||
export function isNativePtyException(error: unknown): boolean {
|
||||
if (!(error instanceof Error) || error.name !== 'Error') {
|
||||
return false
|
||||
}
|
||||
const code = (error as NodeJS.ErrnoException).code
|
||||
return /\bpty\b/i.test(error.message) || NATIVE_PTY_ERROR_CODE_PATTERN.test(code ?? error.message)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Socket } from 'node:net'
|
||||
import { spawn, type IPty } from 'node-pty'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
type WindowsPtyInternals = IPty & {
|
||||
_agent: { inSocket: Socket }
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform !== 'win32')('node-pty Windows input errors', () => {
|
||||
it('retires only the failed PTY when its ConPTY input pipe emits EAGAIN', async () => {
|
||||
const terminal = spawn(process.env.ComSpec ?? 'cmd.exe', ['/d', '/q'], {
|
||||
cwd: process.cwd(),
|
||||
env: process.env
|
||||
})
|
||||
const input = (terminal as WindowsPtyInternals)._agent.inSocket
|
||||
const exited = new Promise<void>((resolve) => terminal.onExit(() => resolve()))
|
||||
|
||||
try {
|
||||
expect(input.listenerCount('error')).toBeGreaterThan(0)
|
||||
expect(() =>
|
||||
input.emit('error', Object.assign(new Error('write EAGAIN'), { code: 'EAGAIN' }))
|
||||
).not.toThrow()
|
||||
await exited
|
||||
} finally {
|
||||
try {
|
||||
terminal.kill()
|
||||
} catch {}
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user