mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
* fix(daemon): read the PTY process table without blocking on terminal kill and resize
The terminal daemon force-killed a session by running ps and reading
/proc/<pid>/stat synchronously, and checked the resize foreground group
with execFileSync('ps'). A slow or wedged ps froze every terminal the
daemon serves for up to the ps timeout on each call.
Both paths now use the async process-table read (runProcess with a
timeout that kills the child). SubprocessHandle.forceKill may return a
promise; the termination controller awaits it so a refused signal still
releases the claim for a retry. A force-kill or SIGWINCH whose root
exits while ps runs signals nothing, since the captured groups could
already be recycled. ConPTY force-kill stays synchronous so dispose()
still sees the node-pty kill it must not repeat. The in-process
provider's app-quit sweep keeps the blocking variant. The ps dialect
and table readers move to posix-pty-process-table.ts.
Refs #34410
Refs #33870
* fix(daemon): address review on async PTY process-table reads
Guard only the root-pid fallback on exit so proven groups are still swept
after dispose, wait out at most one ps dialect probe, move the sync-launch
baseline entry with the table reader, and keep tests cross-platform.
Refs #34410
Refs #33870
* fix(daemon): keep the PTY open until an in-flight force-kill sweep finishes
A sweep whose root exits mid-read must not signal: the freed TTY may
belong to another terminal. Dispose therefore defers destroy() until the
sweep settles, so its hangup cannot end the root first.
Refs #34410
Refs #33870
* test(daemon): build the destroy-tracking PTY mock without a type assertion
Refs #34410
Refs #33870
* fix(pty): make the local graceful-deadline force-kill async and stub ps in tests
The in-process provider's graceful-deadline timer now uses the async
group sweep; the app-quit sweep and the root-signal path stay
synchronous. Tests that drove force-kill through fixture pids now stub
the async process-table read instead of querying the host, and a new
test pins that waiters do not chain behind a failed dialect probe.
Session ids are only read for rows on the root's terminal.
Refs #34410
Refs #33870
* fix(pty): hold PTY teardown until an in-flight force-kill sweep settles
Orphan cleanup could destroy a local PTY while the graceful-deadline
sweep was still reading the process table, neutralizing its fallback
kill and hanging up the root first. Both the daemon handle and the
local provider now defer teardown through PendingPtyGroupSweep.
Concurrent force-kill callers in the daemon now await the shared
in-flight request, so a joiner's physical-exit budget starts after the
signals are sent and it sees their rejection.
Refs #34410
Refs #33870
* fix(pty): let disposal and orphan cleanup join the in-flight force-kill sweep
Session disposal now joins the controller's shared force-kill request
instead of starting a second sweep. Local orphan cleanup decides its
escalation only after a pending graceful-deadline sweep settles, so a
refused sweep is escalated before the handle is released rather than
leaving the retry with a neutralized kill.
Refs #34410
Refs #33870
* fix(daemon): SIGKILL live PTY groups before a shutdown deadline exit
The old synchronous sweep sent its signals before the shutdown deadline
timer could run; the async sweep can still be reading the process table
when it fires. The deadline now SIGKILLs each live session's process
group synchronously before exiting (node-pty roots lead their own
group), falling back to the root pid when the group kill is refused.
POSIX only.
Refs #34410
Refs #33870
* chore(ratchet): gate event-loop-blocking calls in the daemon, PTY and orcad code
The terminal daemon and orcad serve every pane from one event loop, so a
synchronous child launch or file write there freezes all of them. A new
count ratchet tracks execFileSync/spawnSync/execSync, runProcessSync,
appendFileSync and writeSync per file under src/main/daemon,
src/main/pty, src/main/runtime and src/main/orcad; counts may only go
down. It reuses the sync-spawn ratchet's import-aware call collector,
now extracted to gated-call-use-collector.mjs, so aliases, namespace
imports and re-exports count and same-named methods on other objects
do not.
Refs #34410
Refs #33870
143 lines
5.2 KiB
JavaScript
143 lines
5.2 KiB
JavaScript
import { globSync, readFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import {
|
|
diffCounts,
|
|
parseCountBaseline,
|
|
pruneCountBaseline,
|
|
totalCount
|
|
} from './ratchet-baseline.mjs'
|
|
import { createGatedCallCollector } from './gated-call-use-collector.mjs'
|
|
import { isTestOnlySourcePath } from './test-only-source-path.mjs'
|
|
|
|
// Synchronous launches freeze all Electron windows; Windows antivirus magnifies the delay. Count launch APIs and synchronous wrappers per file.
|
|
|
|
const ROOT = path.resolve(import.meta.dirname, '..', '..')
|
|
const BASELINE_PATH = 'config/main-sync-spawn-baseline.txt'
|
|
const SCAN_DIRECTORIES = ['src/main', 'src/shared']
|
|
const PROCESS_ADVICE = 'use runProcess or spawnProcess from @orca/process-host'
|
|
const SECURE_ADVICE = `rewrites a DACL on the calling thread, and a directory rewrite blocks on its whole subtree; use restrictWindowsDirectory for a directory, or raise this file's count in ${BASELINE_PATH} (or add a row)`
|
|
// Include package/repo re-exporters so moving an import cannot bypass the gate.
|
|
const GATED_FAMILIES = [
|
|
{
|
|
modules: ['child_process', 'node:child_process'],
|
|
advice: { execFileSync: PROCESS_ADVICE, spawnSync: PROCESS_ADVICE, execSync: PROCESS_ADVICE }
|
|
},
|
|
{ modules: ['@orca/process-host'], advice: { runProcessSync: PROCESS_ADVICE } },
|
|
{
|
|
modules: ['src/shared/secure-path-windows-acl'],
|
|
advice: { restrictWindowsPathSync: SECURE_ADVICE }
|
|
},
|
|
{
|
|
modules: ['src/main/git/command-runner/git-exec-file', 'src/main/git/runner'],
|
|
advice: { gitExecFileSync: 'use gitExecFileAsync' }
|
|
},
|
|
{
|
|
modules: ['src/main/wsl'],
|
|
advice: {
|
|
getWslHome: 'use getWslHomeAsync',
|
|
listWslDistros: 'use listWslDistrosAsync',
|
|
wslUncDirectoryExists: 'use wslUncDirectoryExistsAsync'
|
|
}
|
|
},
|
|
{
|
|
modules: ['src/main/wsl-availability', 'src/main/wsl'],
|
|
advice: { isWslAvailable: 'use isWslAvailableAsync' }
|
|
},
|
|
{ modules: ['src/main/pwsh'], advice: { isPwshAvailable: 'use isPwshAvailableAsync' } },
|
|
{ modules: ['src/main/win32-utils'], advice: { grantDirAcl: 'use grantDirAclAsync' } }
|
|
]
|
|
const collector = createGatedCallCollector(GATED_FAMILIES, 'sync-spawn')
|
|
const BASELINE_HEADER = [
|
|
'# Synchronous child-process launches in production src/main and src/shared, per file: uses of',
|
|
'# execFileSync/spawnSync/execSync, runProcessSync and the repo sync wrappers listed in',
|
|
'# config/scripts/check-main-sync-spawn-ratchet.mjs (synchronous ACL rewrites and grants, which',
|
|
'# can block on a whole directory subtree).',
|
|
'# Some entries run in the daemon or orcad process, or are test harnesses outside the test-path',
|
|
'# conventions. This is a RATCHET: counts may only go DOWN.',
|
|
'# Prune after removing launches: pnpm check:main-sync-spawn-ratchet --prune'
|
|
]
|
|
|
|
function normalized(file) {
|
|
return file.replaceAll('\\', '/')
|
|
}
|
|
|
|
export const gatedSymbolsFor = collector.gatedSymbolsFor
|
|
export const collectSyncLaunchUses = collector.collectUses
|
|
export const adviceFor = collector.adviceFor
|
|
|
|
export function isRatchetedSourcePath(file) {
|
|
return (
|
|
SCAN_DIRECTORIES.some((directory) => file.startsWith(`${directory}/`)) &&
|
|
!file.endsWith('.d.ts') &&
|
|
!isTestOnlySourcePath(file)
|
|
)
|
|
}
|
|
|
|
export function countSyncSpawnUses(sources) {
|
|
const counts = new Map()
|
|
const symbols = new Map()
|
|
for (const [file, contents] of sources) {
|
|
if (!isRatchetedSourcePath(file)) {
|
|
continue
|
|
}
|
|
const uses = collectSyncLaunchUses(file, contents)
|
|
if (uses.count > 0) {
|
|
counts.set(file, uses.count)
|
|
symbols.set(file, uses.symbols)
|
|
}
|
|
}
|
|
return { counts, symbols }
|
|
}
|
|
|
|
export function collectSyncSpawnSources(root = ROOT) {
|
|
return new Map(
|
|
globSync(
|
|
SCAN_DIRECTORIES.map((directory) => `${directory}/**/*.{ts,tsx,mts,cts,js,mjs,cjs}`),
|
|
{ cwd: root, exclude: ['**/node_modules/**'] }
|
|
)
|
|
.map(normalized)
|
|
.filter(isRatchetedSourcePath)
|
|
.sort()
|
|
.map((file) => [file, readFileSync(path.join(root, file), 'utf8')])
|
|
)
|
|
}
|
|
|
|
export function main(root = ROOT) {
|
|
const { counts, symbols } = countSyncSpawnUses(collectSyncSpawnSources(root))
|
|
const { grown, shrunk } = diffCounts(
|
|
counts,
|
|
parseCountBaseline(readFileSync(path.join(root, BASELINE_PATH), 'utf8'))
|
|
)
|
|
for (const { file, now, allowed } of grown) {
|
|
console.error(
|
|
`::error file=${file}::${now} synchronous launch use(s), baseline allows ${allowed}. They block the main thread. ${adviceFor(symbols.get(file))}`
|
|
)
|
|
}
|
|
for (const { file, now, allowed } of shrunk) {
|
|
console.error(
|
|
`::error file=${file}::${now} synchronous launch use(s), baseline still allows ${allowed}. Run: pnpm check:main-sync-spawn-ratchet --prune`
|
|
)
|
|
}
|
|
if (grown.length + shrunk.length > 0) {
|
|
return 1
|
|
}
|
|
console.log(
|
|
`Main sync-spawn ratchet OK — ${totalCount(counts)} use(s) in ${counts.size} legacy file(s).`
|
|
)
|
|
return 0
|
|
}
|
|
|
|
export function prune(root = ROOT) {
|
|
const { counts } = countSyncSpawnUses(collectSyncSpawnSources(root))
|
|
const kept = pruneCountBaseline(root, BASELINE_PATH, BASELINE_HEADER, counts)
|
|
console.log(`Pruned ${BASELINE_PATH} to ${kept} use(s).`)
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
if (process.argv[2] === '--prune') {
|
|
prune()
|
|
}
|
|
process.exitCode = main()
|
|
}
|