mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
161 lines
5.7 KiB
JavaScript
161 lines
5.7 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/ratchet-baseline.mjs'
|
|
import { createGatedCallCollector } from './gated-call-use-collector.mjs'
|
|
import { isMainBlockingSource } from './main-blocking-source-paths.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', 'src/preload']
|
|
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'],
|
|
exportPattern: /Sync$/,
|
|
defaultAdvice: PROCESS_ADVICE,
|
|
advice: { execFileSync: PROCESS_ADVICE, spawnSync: PROCESS_ADVICE, execSync: PROCESS_ADVICE }
|
|
},
|
|
{ modules: ['@orca/process-host'], advice: { runProcessSync: PROCESS_ADVICE } },
|
|
{
|
|
modules: ['src/shared/secure-storage/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'
|
|
],
|
|
advice: { gitExecFileSync: 'use gitExecFileAsync' }
|
|
},
|
|
{
|
|
modules: ['src/main/wsl/discovery/distro-catalog'],
|
|
advice: {
|
|
getWslHome: 'use getWslHomeAsync',
|
|
listWslDistros: 'use listWslDistrosAsync'
|
|
}
|
|
},
|
|
{
|
|
modules: ['src/main/wsl/filesystem/unc-path-access'],
|
|
advice: {
|
|
wslUncDirectoryExists: 'use wslUncDirectoryExistsAsync'
|
|
}
|
|
},
|
|
{
|
|
modules: ['src/main/wsl/discovery/wsl-availability'],
|
|
advice: { isWslAvailable: 'use isWslAvailableAsync' }
|
|
},
|
|
{
|
|
modules: ['src/main/pty/shell-startup/pwsh'],
|
|
advice: { isPwshAvailable: 'use isPwshAvailableAsync' }
|
|
},
|
|
{
|
|
modules: ['src/main/windows/windows-directory-acl'],
|
|
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/static-analysis/process-boundaries/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}/`)) &&
|
|
isMainBlockingSource(file) &&
|
|
!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()
|
|
}
|