Files
orca/config/scripts/check-main-sync-spawn-ratchet.mjs
T
32e804dd6f fix(wsl): every local Git call follows its project's runtime unless it says why not (D1c-6) (#33531)
* fix(wsl): every local Git call follows its project's runtime unless it says why not (D1c-6)

* fix(wsl): rebuild Git routes on the store's own writes, never on a timer, and route a new worktree by its link

* fix(wsl): route WSL-made worktree links, aliased checkouts and sync Git; host Git only where marked; no wsl.exe storms

* fix(wsl): map WSL-made worktree links through each project distro's automount root

* fix(wsl): arm the missing-distro cache only with the Git router on, and feed it spawn failures

* fix(wsl): keep synchronous Git host-only, read the setup runner's path from the link, and keep the sync launch under the ratchet

* chore(wsl): move the sync Git runner's process-host import allowance with it

* fix(wsl): read only per-worktree git paths from the link, and only from a git dir that still exists

* fix(wsl): name which Git ran on stream and spawn spans too, so status shows its route

* test(wsl): setup runner lookups declare host Git; cover the link read with the router on and off

The hooks tests expected the setup runner's `rev-parse --git-path` call to
carry `wslDistro: 'Ubuntu'` for a WSL-runtime project. On main that field
never reached Git: main's gitExecFileSync options have no wslDistro and it
calls resolveCommand('git', args, cwd) with no distro override, so the
lookup ran host Git (WSL only when the cwd itself is a WSL path). The tests
pinned call arguments, not what ran. The sync runner now states that with
`hostGit: true` and resolves exactly as main did, so the tests assert that.

New worktree-runner-script-link tests cover the router-gated link read:
router on reads the link; router on with no usable link falls back to host
Git and converts its Linux path; router off makes main's host Git call and
never reads the link.

* test(git): read src/main once in the host-Git routing guard, so it holds under load

* test(wsl): pin the setup runner's sync Git spawn to main's exact launch on POSIX

The setup runner's path lookup now passes hostGit: true where it passed a
wslDistro that gitExecFileSync never read, so the binary, args, cwd and env
it spawns are unchanged. Update the call-shape assertions and pin the POSIX
execFileSync call itself.

(cherry picked from commit 6aae6d4d090e13b75c1d9ad2734f2f717dde7570)

* fix(wsl): await WSL Git admission in spawns only when there is one

With the Git router off admitWslGit returns null; awaiting it anyway added a
microtask to every spawn's admission path that main doesn't have.

(cherry picked from commit 883aaeb90bc23e89cafb039708a565ab3d72620e)

* fix(wsl): keep main's Git scheduling when the D1c Git router is off

The remote-identity sweep moved every user's probes to the background
admission tier, which only the router reads; it now keeps its tier unless the
router is on. Admission wrapping and the spawn stderr tail also step aside
without the router, so those calls run exactly as on main.

(cherry picked from commit 151a3091307c03078d0c0b98487bbe3bf467ec27)

* test(wsl): point the distro allowlist test at the sync runner's new home, as host Git

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 255e85fc93deba83addc4370f889c1b721180188)

* test(wsl): installing the Git router with the gate closed or off Windows leaves none installed

Co-Authored-By: Claude <noreply@anthropic.com>

* perf(wsl): skip the span-only Git resolution on every spawn while the Git router is off

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(wsl): read a distro's automount root only from a running list probed after its stop settled, so it never boots a terminated distro

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-29 01:52:18 -07:00

147 lines
5.3 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/command-runner/git-exec-file-sync',
'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()
}