mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07:40 +00:00
top costs ~263 ms of CPU per sample, so the memory panel only refreshed footprints every 10 s. The new orca-process-footprint helper reads phys_footprint for every same-user process in ~3 ms, so footprints now refresh on the panel's 2 s poll. top stays as the fallback when the helper is missing or fails. Co-authored-by: m4air <m4air@Mac.localdomain>
214 lines
5.9 KiB
JavaScript
Executable File
214 lines
5.9 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import { spawn, spawnSync } from 'node:child_process'
|
|
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
|
|
|
|
if (process.platform === 'win32') {
|
|
runNodeScript('config/scripts/build-windows-cli-launcher.mjs')
|
|
runNodeScript('config/scripts/build-windows-agent-hook.mjs')
|
|
process.exit(0)
|
|
}
|
|
|
|
if (process.platform === 'linux') {
|
|
runNodeScript('config/scripts/build-wayland-clipboard.mjs')
|
|
process.exit(0)
|
|
}
|
|
|
|
if (process.platform !== 'darwin') {
|
|
console.log(`[native-build] no macOS native computer build required on ${process.platform}`)
|
|
process.exit(0)
|
|
}
|
|
|
|
// Each compiler tree needs its own group so cancellation reaches Swift descendants.
|
|
const children = new Map()
|
|
let externalSignal = null
|
|
let stopping = false
|
|
let outputFailed = false
|
|
// Status of the child whose failure started cancellation; siblings we stop are not failures.
|
|
let firstFailure = null
|
|
let forceTimer
|
|
const signalHandlers = new Map()
|
|
|
|
process.on('SIGINT', handlerFor('SIGINT'))
|
|
process.on('SIGTERM', handlerFor('SIGTERM'))
|
|
// Own sessions do not see a terminal hangup; forward it so compilers do not outlive the shell.
|
|
process.on('SIGHUP', handlerFor('SIGHUP'))
|
|
for (const target of [process.stdout, process.stderr]) {
|
|
target.on('error', () => {
|
|
outputFailed = true
|
|
process.exitCode = 1
|
|
stopBuilds()
|
|
})
|
|
}
|
|
|
|
const exitCodes = await Promise.all(
|
|
[
|
|
'build:computer-macos',
|
|
'build:keyboard-layout-macos',
|
|
'build:notification-status-macos',
|
|
'build:process-footprint-macos'
|
|
].map((scriptName) => runPnpmScript(scriptName))
|
|
)
|
|
clearTimeout(forceTimer)
|
|
for (const [signal, handler] of signalHandlers) {
|
|
process.removeListener(signal, handler)
|
|
}
|
|
if (externalSignal) {
|
|
process.kill(process.pid, externalSignal)
|
|
} else if (firstFailure?.signal) {
|
|
// Node ignores some signals (SIGPIPE); the build still failed if the re-raise is a no-op.
|
|
process.exitCode = 1
|
|
process.kill(process.pid, firstFailure.signal)
|
|
} else {
|
|
process.exitCode = firstFailure?.code ?? Math.max(outputFailed ? 1 : 0, ...exitCodes)
|
|
}
|
|
|
|
function handlerFor(signal) {
|
|
if (!signalHandlers.has(signal)) {
|
|
signalHandlers.set(signal, () => {
|
|
externalSignal ??= signal
|
|
stopBuilds(signal)
|
|
})
|
|
}
|
|
return signalHandlers.get(signal)
|
|
}
|
|
|
|
function stopBuilds(signal = 'SIGTERM') {
|
|
if (stopping) {
|
|
return
|
|
}
|
|
stopping = true
|
|
terminateAll(signal)
|
|
if (children.size > 0) {
|
|
forceTimer ??= setTimeout(() => terminateAll('SIGKILL'), 2_000)
|
|
}
|
|
}
|
|
|
|
function terminateAll(signal) {
|
|
for (const [child, label] of children) {
|
|
if (!child.pid) {
|
|
continue
|
|
}
|
|
console.log(`[native-build] stopping ${label} (${signal})`)
|
|
try {
|
|
process.kill(-child.pid, signal)
|
|
} catch {
|
|
// group already gone
|
|
try {
|
|
child.kill(signal)
|
|
} catch {}
|
|
}
|
|
}
|
|
}
|
|
|
|
function runPnpmScript(scriptName) {
|
|
if (stopping) {
|
|
return Promise.resolve(1)
|
|
}
|
|
const label = scriptName.replace(/^build:|-macos$/g, '')
|
|
const { command, prefixArgs, shell } = resolvePnpmCliInvocation()
|
|
const child = spawn(command, [...prefixArgs, 'run', scriptName], {
|
|
detached: true,
|
|
shell,
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
children.set(child, scriptName)
|
|
pipePrefixed(child.stdout, label, process.stdout)
|
|
pipePrefixed(child.stderr, label, process.stderr)
|
|
|
|
return new Promise((resolve) => {
|
|
let failed = false
|
|
child.on('error', (error) => {
|
|
failed = true
|
|
console.error(`[${label}] ${error.message}`)
|
|
if (!stopping) {
|
|
firstFailure = { code: 1, signal: null }
|
|
}
|
|
stopBuilds()
|
|
})
|
|
let exited = false
|
|
let closeTimer
|
|
// A descendant that inherited the pipes must not hold the launcher open forever.
|
|
const armReap = () => {
|
|
clearTimeout(closeTimer)
|
|
// A backpressure pause also delays 'close'; only count time spent actually draining.
|
|
if (child.stdout.isPaused() || child.stderr.isPaused()) {
|
|
return
|
|
}
|
|
closeTimer = setTimeout(() => {
|
|
console.error(`[native-build] ${label} left descendants holding its output; reaping them`)
|
|
try {
|
|
process.kill(-child.pid, 'SIGKILL')
|
|
} catch {}
|
|
child.stdout.destroy()
|
|
child.stderr.destroy()
|
|
}, 2_000)
|
|
}
|
|
for (const stream of [child.stdout, child.stderr]) {
|
|
stream.on('pause', () => clearTimeout(closeTimer))
|
|
stream.on('resume', () => {
|
|
if (exited) {
|
|
armReap()
|
|
}
|
|
})
|
|
}
|
|
child.on('exit', (code, signal) => {
|
|
if (code !== 0 || signal) {
|
|
if (!stopping) {
|
|
firstFailure = { code: code ?? 1, signal }
|
|
}
|
|
stopBuilds()
|
|
}
|
|
exited = true
|
|
armReap()
|
|
})
|
|
// Re-raise the parent's signal only after every child and its output pipes close.
|
|
child.on('close', (code, signal) => {
|
|
clearTimeout(closeTimer)
|
|
children.delete(child)
|
|
resolve(failed || signal ? 1 : (code ?? 1))
|
|
})
|
|
})
|
|
}
|
|
|
|
function pipePrefixed(stream, label, target) {
|
|
stream.setEncoding('utf8')
|
|
let buffer = ''
|
|
stream.on('data', (chunk) => {
|
|
if (target.destroyed) {
|
|
return
|
|
}
|
|
buffer += chunk
|
|
const lines = buffer.split('\n')
|
|
buffer = lines.pop() ?? ''
|
|
for (const line of lines) {
|
|
target.write(`[${label}] ${line}\n`)
|
|
}
|
|
if (target.writableNeedDrain) {
|
|
stream.pause()
|
|
const resume = () => {
|
|
target.off('drain', resume)
|
|
target.off('close', resume)
|
|
stream.resume()
|
|
}
|
|
target.once('drain', resume)
|
|
target.once('close', resume)
|
|
}
|
|
})
|
|
stream.on('end', () => {
|
|
if (buffer.length > 0 && !target.destroyed) {
|
|
target.write(`[${label}] ${buffer}\n`)
|
|
}
|
|
})
|
|
}
|
|
|
|
function runNodeScript(scriptPath) {
|
|
const result = spawnSync(process.execPath, [scriptPath], { stdio: 'inherit' })
|
|
if (result.signal) {
|
|
process.kill(process.pid, result.signal)
|
|
}
|
|
if (result.status !== 0 || result.error) {
|
|
process.exit(result.status ?? 1)
|
|
}
|
|
}
|