Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
such as `proxy`, cloud bridge, or `limrun`.
- Runner/process lease: backend helper mutual-exclusion guard for platform
runners or tools; it is not the remote client ownership boundary.
- Host process primitive: low-level host PID helpers in `src/utils/host-process.ts` for liveness,
start-time/command reads, process listing, process-tree expansion, PID de-duplication, and
best-effort signaling. It must not own domain cleanup policy such as browser ownership markers,
runner lease reclamation, daemon takeover checks, or app-log PID metadata verification.
- Command surface: catalog of public command identity, interface exposure, adapter policy, and shared command metadata across CLI, Node.js, MCP, and batch entrypoints.
- Daemon command registry: daemon-side source of truth for command route ownership and request-policy traits, including admission exemptions, session locking, selector validation, replay-scoped actions, recording invalidation, Android dialog guards, and request provider device resolution.
- Runner command traits: per-command-type classification for iOS/macOS runner lifecycle behavior, distinct from the public command surface and daemon command registry. The Swift runner traits classify interaction, read-only, and runner-lifecycle axes for XCTest execution; Swift resolves the alert command as read-only only for its `get` action. The TypeScript runner command traits classify daemon-side runner send/recovery policy such as read-only retry routing, readiness probes, and recent-healthy-mutation preflight skips; the TypeScript table is command-type keyed and currently classifies alert as read-only for daemon retry policy. Each side keeps one source of truth keyed by runner command type.
Expand Down
3 changes: 1 addition & 2 deletions fallow-baselines/health.json
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,6 @@
"src/cli/parser/args.ts:high impact",
"src/daemon/snapshot-presentation/tree.ts:high impact",
"src/platforms/apple/core/perf-xml.ts:high impact",
"src/utils/process-identity.ts:high impact",
"src/platforms/web/json-utils.ts:high impact",
"src/utils/rect-center.ts:high impact",
"src/daemon/handlers/session-doctor-output.ts:high impact",
Expand Down Expand Up @@ -670,4 +669,4 @@
"src/daemon/client/daemon-client-lifecycle.ts:complexity",
"src/client/client-companion-tunnel-worker.ts:complexity"
]
}
}
2 changes: 1 addition & 1 deletion scripts/clean-daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { resolveDaemonPaths } from '../src/daemon/config.ts';
import {
isAgentDeviceDaemonProcess,
stopProcessForTakeover,
} from '../src/utils/process-identity.ts';
} from '../src/daemon/daemon-process.ts';
import { cleanupRunnerLeasesForOwner } from '../src/platforms/apple/core/runner/runner-lease.ts';
import { runnerLeaseCleanupAdapter } from '../src/platforms/apple/core/runner/runner-disposal.ts';

Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/client-metro-companion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ vi.mock('../utils/exec.ts', () => ({
runCmdDetached: vi.fn(),
}));

vi.mock('../utils/process-identity.ts', () => ({
vi.mock('../utils/host-process.ts', () => ({
isProcessAlive: vi.fn(),
readProcessCommand: vi.fn(),
readProcessStartTime: vi.fn(),
Expand All @@ -21,7 +21,7 @@ import {
readProcessCommand,
readProcessStartTime,
waitForProcessExit,
} from '../utils/process-identity.ts';
} from '../utils/host-process.ts';
import { ensureMetroCompanion, stopMetroCompanion } from '../metro/client-metro-companion.ts';
import { ensureReactDevtoolsCompanion } from '../client/client-react-devtools-companion.ts';

Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/client-metro-startup-cleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ vi.mock('../utils/exec.ts', () => ({
runCmdSync: vi.fn(),
}));

vi.mock('../utils/process-identity.ts', () => ({
vi.mock('../utils/host-process.ts', () => ({
waitForProcessExit: vi.fn(),
}));

import { runCmdDetached } from '../utils/exec.ts';
import { waitForProcessExit } from '../utils/process-identity.ts';
import { waitForProcessExit } from '../utils/host-process.ts';
import { prepareMetroRuntime } from '../metro/client-metro.ts';

afterEach(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/client-metro.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import os from 'node:os';
import path from 'node:path';
import { prepareMetroRuntime, reloadMetro } from '../metro/client-metro.ts';
import { AppError } from '../kernel/errors.ts';
import { isProcessAlive, waitForProcessExit } from '../utils/process-identity.ts';
import { isProcessAlive, waitForProcessExit } from '../utils/host-process.ts';

const TEST_TOKEN = 'agent-device-proxy-test-token';

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/daemon-entrypoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
trackDownloadableArtifact,
} from '../daemon/artifact-tracking.ts';
import { runCmdBackground } from '../utils/exec.ts';
import { isProcessAlive, waitForProcessExit } from '../utils/process-identity.ts';
import { isProcessAlive, waitForProcessExit } from '../utils/host-process.ts';
import { waitForHttpOk } from './test-utils/index.ts';

type DaemonInfoFile = {
Expand Down
2 changes: 1 addition & 1 deletion src/client/client-companion-tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
readProcessCommand,
readProcessStartTime,
waitForProcessExit,
} from '../utils/process-identity.ts';
} from '../utils/host-process.ts';

const COMPANION_TUNNEL_TERM_TIMEOUT_MS = 1_000;
const COMPANION_TUNNEL_KILL_TIMEOUT_MS = 1_000;
Expand Down
18 changes: 18 additions & 0 deletions src/daemon/__tests__/daemon-process.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { isAgentDeviceDaemonCommand } from '../daemon-process.ts';

test('isAgentDeviceDaemonCommand matches expected daemon command', () => {
assert.equal(isAgentDeviceDaemonCommand('node /tmp/agent-device/dist/src/daemon.js'), true);
assert.equal(
isAgentDeviceDaemonCommand('node /tmp/agent-device/dist/src/internal/daemon.js'),
true,
);
assert.equal(
isAgentDeviceDaemonCommand(
'node --experimental-strip-types /worktrees/agent-device/src/daemon.ts',
),
true,
);
assert.equal(isAgentDeviceDaemonCommand('node -e "setInterval(() => {}, 1000)"'), false);
});
2 changes: 1 addition & 1 deletion src/daemon/app-log-process.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import { readProcessCommand, readProcessStartTime } from '../utils/process-identity.ts';
import { readProcessCommand, readProcessStartTime } from '../utils/host-process.ts';
import type { LogBackend } from './network-log.ts';
import type { ExecResult } from '../utils/exec.ts';

Expand Down
5 changes: 1 addition & 4 deletions src/daemon/client/daemon-client-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import fs from 'node:fs';
import { emitDiagnostic } from '../../utils/diagnostics.ts';
import {
isAgentDeviceDaemonProcess,
stopProcessForTakeover,
} from '../../utils/process-identity.ts';
import { isAgentDeviceDaemonProcess, stopProcessForTakeover } from '../daemon-process.ts';
import { shellQuote } from '../../utils/shell-quote.ts';
import { resolveDaemonPaths, type DaemonPaths, type DaemonServerMode } from '../config.ts';

Expand Down
2 changes: 1 addition & 1 deletion src/daemon/client/daemon-client-timeout.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AppError } from '../../kernel/errors.ts';
import { runCmdSync } from '../../utils/exec.ts';
import { emitDiagnostic } from '../../utils/diagnostics.ts';
import { isAgentDeviceDaemonProcess } from '../../utils/process-identity.ts';
import { isAgentDeviceDaemonProcess } from '../daemon-process.ts';
import { PUBLIC_COMMANDS } from '../../command-catalog.ts';
import { resolveCommandTimeoutPolicy } from '../../core/command-descriptor/registry.ts';
import type { DaemonPaths } from '../config.ts';
Expand Down
55 changes: 55 additions & 0 deletions src/daemon/daemon-process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
isProcessAlive,
readProcessCommand,
readProcessStartTime,
waitForProcessExit,
} from '../utils/host-process.ts';

const DAEMON_COMMAND_PATTERNS = [
/(^|[/\s"'=])dist\/src\/daemon\.js($|[\s"'])/,
/(^|[/\s"'=])dist\/src\/internal\/daemon\.js($|[\s"'])/,
/(^|[/\s"'=])src\/daemon\.ts($|[\s"'])/,
];

export function isAgentDeviceDaemonCommand(command: string): boolean {
const normalized = command.toLowerCase().replaceAll('\\', '/');
if (!normalized.includes('agent-device')) return false;
return DAEMON_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized));
}

export function isAgentDeviceDaemonProcess(pid: number, expectedStartTime?: string): boolean {
if (!isProcessAlive(pid)) return false;
if (expectedStartTime) {
const actualStartTime = readProcessStartTime(pid);
if (!actualStartTime || actualStartTime !== expectedStartTime) return false;
}
const command = readProcessCommand(pid);
if (!command) return false;
return isAgentDeviceDaemonCommand(command);
}

function trySignalProcess(pid: number, signal: NodeJS.Signals): boolean {
try {
process.kill(pid, signal);
return true;
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === 'ESRCH' || code === 'EPERM') return false;
throw err;
}
}

export async function stopProcessForTakeover(
pid: number,
options: {
termTimeoutMs: number;
killTimeoutMs: number;
expectedStartTime?: string;
},
): Promise<void> {
if (!isAgentDeviceDaemonProcess(pid, options.expectedStartTime)) return;
if (!trySignalProcess(pid, 'SIGTERM')) return;
if (await waitForProcessExit(pid, options.termTimeoutMs)) return;
if (!trySignalProcess(pid, 'SIGKILL')) return;
await waitForProcessExit(pid, options.killTimeoutMs);
}
30 changes: 7 additions & 23 deletions src/daemon/handlers/record-trace-ios-simulator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sleep } from '../../utils/timeouts.ts';
import { emitDiagnostic } from '../../utils/diagnostics.ts';
import type { ExecResult } from '../../utils/exec.ts';
import { signalPidsBestEffort, uniquePositivePids } from '../../utils/host-process.ts';
import { formatRecordTraceError } from '../record-trace-errors.ts';
import type { SessionState } from '../types.ts';
import type { RecordTraceDeps } from './record-trace-types.ts';
Expand Down Expand Up @@ -88,8 +89,8 @@ async function signalMatchingIosSimulatorRecorders(
return;
}

const pids = uniquePositivePids(parseProcessIds(result.stdout));
const signaled = signalProcessIds(pids, signal);
const pids = uniquePositivePids(parseProcessIds(result.stdout), { excludePid: process.pid });
const signaled = signalPidsBestEffort(pids, signal);

emitDiagnostic({
level: signaled > 0 ? 'warn' : 'debug',
Expand Down Expand Up @@ -124,8 +125,10 @@ async function signalSessionOwnedIosSimulatorRecorders(
}

const childResult = await findChildProcessIds(deps, recorderPid, recording.outPath, signal);
const pids = uniquePositivePids([recorderPid, ...childResult.pids]);
const signaled = signalProcessIds(pids, signal);
const pids = uniquePositivePids([recorderPid, ...childResult.pids], {
excludePid: process.pid,
});
const signaled = signalPidsBestEffort(pids, signal);

emitDiagnostic({
level: signaled > 0 ? 'warn' : 'debug',
Expand Down Expand Up @@ -173,25 +176,6 @@ async function findChildProcessIds(
};
}

function uniquePositivePids(values: number[]): number[] {
return Array.from(new Set(values)).filter(
(pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid,
);
}

function signalProcessIds(pids: number[], signal: NodeJS.Signals): number {
let signaled = 0;
for (const pid of pids) {
try {
process.kill(pid, signal);
signaled += 1;
} catch {
// Process already exited or cannot be signaled; cleanup remains best-effort.
}
}
return signaled;
}

function parseProcessIds(stdout: string): number[] {
return stdout
.split(/\s+/)
Expand Down
4 changes: 2 additions & 2 deletions src/daemon/server/server-lifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fs from 'node:fs';
import { isAgentDeviceDaemonProcess } from '../../utils/process-identity.ts';
import { isAgentDeviceDaemonProcess } from '../daemon-process.ts';

export { readVersion } from '../../utils/version.ts';
export { readProcessStartTime } from '../../utils/process-identity.ts';
export { readProcessStartTime } from '../../utils/host-process.ts';
export { resolveDaemonCodeSignature } from '../code-signature.ts';

export type DaemonLockInfo = {
Expand Down
2 changes: 1 addition & 1 deletion src/metro/client-metro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
import { AppError } from '../kernel/errors.ts';
import { runCmdSync, runCmdDetached } from '../utils/exec.ts';
import { resolveUserPath } from '../utils/path-resolution.ts';
import { waitForProcessExit } from '../utils/process-identity.ts';
import { waitForProcessExit } from '../utils/host-process.ts';
import {
detectProjectRuntimeKindFromPackageJson,
readProjectPackageJson,
Expand Down
6 changes: 3 additions & 3 deletions src/platforms/apple/core/__tests__/runner-adoption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
tryAdoptRunnerSessionFromLease,
} from '../runner/runner-adoption.ts';
import { sendRunnerCommandOnce } from '../runner/runner-transport.ts';
import { isProcessAlive } from '../../../../utils/process-identity.ts';
import { isProcessAlive } from '../../../../utils/host-process.ts';
import {
resolveExpectedRunnerCacheMetadata,
resolveRunnerDerivedPath,
Expand All @@ -25,8 +25,8 @@ vi.mock('../runner/runner-transport.ts', async (importOriginal) => {
const actual = await importOriginal<typeof import('../runner/runner-transport.ts')>();
return { ...actual, sendRunnerCommandOnce: vi.fn() };
});
vi.mock('../../../../utils/process-identity.ts', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../../utils/process-identity.ts')>();
vi.mock('../../../../utils/host-process.ts', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../../utils/host-process.ts')>();
return { ...actual, isProcessAlive: vi.fn(() => false) };
});

Expand Down
6 changes: 3 additions & 3 deletions src/platforms/apple/core/__tests__/runner-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ vi.mock('../../../../utils/exec.ts', async () => {
};
});

vi.mock('../../../../utils/process-identity.ts', async () => {
const actual = await vi.importActual<typeof import('../../../../utils/process-identity.ts')>(
'../../../../utils/process-identity.ts',
vi.mock('../../../../utils/host-process.ts', async () => {
const actual = await vi.importActual<typeof import('../../../../utils/host-process.ts')>(
'../../../../utils/host-process.ts',
);
return {
...actual,
Expand Down
2 changes: 1 addition & 1 deletion src/platforms/apple/core/runner/runner-adoption.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path';
import { resolveIosSimulatorDeviceSetPath } from '../../../../utils/device-isolation.ts';
import { emitDiagnostic } from '../../../../utils/diagnostics.ts';
import { isProcessAlive } from '../../../../utils/process-identity.ts';
import { isProcessAlive } from '../../../../utils/host-process.ts';
import { parseBooleanLiteral } from '../../../../utils/source-value.ts';
import type { DeviceInfo } from '../../../../kernel/device.ts';
import type { ExecResult } from '../../../../utils/exec.ts';
Expand Down
2 changes: 1 addition & 1 deletion src/platforms/apple/core/runner/runner-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { AppError } from '../../../../kernel/errors.ts';
import { emitDiagnostic } from '../../../../utils/diagnostics.ts';
import { readProcessStartTime } from '../../../../utils/process-identity.ts';
import { readProcessStartTime } from '../../../../utils/host-process.ts';
import { acquireProcessLock, type ProcessLockOwner } from '../../../../utils/process-lock.ts';
import { isEnvTruthy } from '../../../../utils/retry.ts';
import { findProjectRoot } from '../../../../utils/version.ts';
Expand Down
2 changes: 1 addition & 1 deletion src/platforms/apple/core/runner/runner-device-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AppError } from '../../../../kernel/errors.ts';
import { isIosFamily, type DeviceInfo } from '../../../../kernel/device.ts';
import { resolveIosSimulatorDeviceSetPath } from '../../../../utils/device-isolation.ts';
import { emitDiagnostic } from '../../../../utils/diagnostics.ts';
import { readProcessStartTime } from '../../../../utils/process-identity.ts';
import { readProcessStartTime } from '../../../../utils/host-process.ts';
import { acquireProcessLock, type ProcessLockOwner } from '../../../../utils/process-lock.ts';

const XCTEST_DEVICE_SET_BASE_NAME = 'XCTestDevices';
Expand Down
10 changes: 6 additions & 4 deletions src/platforms/apple/core/runner/runner-disposal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { emitDiagnostic } from '../../../../utils/diagnostics.ts';
import type { DeviceInfo } from '../../../../kernel/device.ts';
import { isProcessAlive, isProcessGroupAlive } from '../../../../utils/process-identity.ts';
import {
isProcessAlive,
isProcessGroupAlive,
signalPidsBestEffort,
} from '../../../../utils/host-process.ts';
import type { ExecBackgroundResult } from '../../../../utils/exec.ts';
import { cleanupTempFile, waitForRunner } from './runner-transport.ts';
import { withRunnerCommandId, type RunnerCommand } from './runner-contract.ts';
Expand Down Expand Up @@ -187,9 +191,7 @@ async function killRunnerProcessTree(
try {
process.kill(-pid, signal);
} catch {}
try {
process.kill(pid, signal);
} catch {}
signalPidsBestEffort([pid], signal);
const pkillSignal = signal === 'SIGINT' ? 'INT' : signal === 'SIGTERM' ? 'TERM' : 'KILL';
try {
await runAppleToolCommand('pkill', [`-${pkillSignal}`, '-P', String(pid)], {
Expand Down
2 changes: 1 addition & 1 deletion src/platforms/apple/core/runner/runner-lease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import path from 'node:path';
import { emitDiagnostic } from '../../../../utils/diagnostics.ts';
import { AppError } from '../../../../kernel/errors.ts';
import { acquireProcessLock } from '../../../../utils/process-lock.ts';
import { isProcessAlive, readProcessStartTime } from '../../../../utils/process-identity.ts';
import { isProcessAlive, readProcessStartTime } from '../../../../utils/host-process.ts';
import type { RunnerLogicalLeaseContext } from '../../../../core/runner-lease-context.ts';

const RUNNER_LEASE_SCHEMA_VERSION = 1;
Expand Down
Loading
Loading