src/vs/platform/agentHost/node/agentHostLockfile.ts
123 LOC · 121 covered · 2 uncovered · 24 ranges · 23 concepts · 18 introducers · 17 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
agentHostLockfile.ts ×4
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as os from 'os';
import { join } from '../../../base/common/path.js';
import { ILogService } from '../../log/common/log.js';
import { IRemoteAgentHostState, parseRemoteAgentHostState } from '../common/remoteAgentHostMetadata.js';
import { dialAgentHostHost, validateShellToken } from './sshRemoteAgentHostHelpers.js';
const LOG_PREFIX = '[AgentHostLockfile]';
/**
* Local-filesystem variant of {@link getAgentHostLockfile}. Returns an
* absolute path resolved against the current user's home directory rather
* than the shell-style `~/<...>` path used over SSH. Anchored on
* `serverDataFolderName` so it stays in sync with the Rust CLI (see
* `cli/src/state.rs::agent_host_root`). Both inputs are validated for
* safe characters as defense-in-depth.
*/
export function getLocalAgentHostLockfilePath(serverDataFolderName: string, quality: string): string {
const d = validateShellToken(serverDataFolderName, 'server data folder name');
agentHostLockfile.ts ×1
const q = validateShellToken(quality, 'quality');
return join(os.homedir(), d, 'cli', `agent-host-${q}.lock`);
}
/**
* Read and parse the canonical agent-host lockfile from a local path.
* Returns `undefined` if the file does not exist, cannot be read, or
* does not contain a valid {@link IRemoteAgentHostState}.
*/
export async function readLocalAgentHostLockfile(lockfilePath: string, logService?: ILogService): Promise<IRemoteAgentHostState | undefined> {
agentHostLockfile.ts ×1
let raw: string;
try {
raw = await fs.promises.readFile(lockfilePath, 'utf8');
} catch (err: unknown) {
if (code !== 'ENOENT') {
logService?.warn(`${LOG_PREFIX} Failed to read agent host lockfile ${lockfilePath}: ${err}`);
}
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
logService?.info(`${LOG_PREFIX} Agent host lockfile ${lockfilePath} contains invalid JSON`);
agentHostLockfile.ts ×1
return undefined;
}
const state = parseRemoteAgentHostState(parsed);
if (!state) {
logService?.info(`${LOG_PREFIX} Agent host lockfile ${lockfilePath} does not match expected schema`);
agentHostLockfile.ts ×1
return undefined;
}
}
/**
* Mirrors the SSH-side {@link FindRunningAgentHostResult}, applied to a local
* lockfile path. PID liveness is tested via `process.kill(pid, 0)`, which
* sends no signal but reports whether the OS has a process with that PID.
*/
export type LocalAgentHostLookupResult =
| { readonly kind: 'notFound' }
| { readonly kind: 'stale'; readonly pid: number }
| { readonly kind: 'compatible'; readonly pid: number; readonly host: string; readonly port: number; readonly connectionToken: string | undefined };
/**
* Read the lockfile and verify the recorded PID is still alive. Returns
* `notFound` on missing/corrupt files, `stale` on dead PIDs, and
* `compatible` for any live process.
*
* The recorded protocol version is intentionally NOT checked here: the
* agent host server is downloaded on demand and may speak a newer
* protocol than the consumer was built with. The renderer↔AH handshake
* surfaces any genuine incompatibility.
*/
export async function readActiveAgentHostFromLockfile(lockfilePath: string, logService: ILogService): Promise<LocalAgentHostLookupResult> {
agentHostLockfile.ts ×2
const state = await readLocalAgentHostLockfile(lockfilePath, logService);
if (!state) {
}
if (!isPidAlive(state.pid)) {
logService.info(`${LOG_PREFIX} Stale agent host lockfile ${lockfilePath} (PID ${state.pid} not running)`);
agentHostLockfile.ts ×1
return { kind: 'stale', pid: state.pid };
}
logService.info(`${LOG_PREFIX} Found running agent host via ${lockfilePath}: PID ${state.pid}, port ${state.port}`);
return {
kind: 'compatible',
pid: state.pid,
host: dialAgentHostHost(state.host),
port: state.port,
connectionToken: state.connectionToken ?? undefined,
}
/**
* Returns `true` if a process with the given PID exists. Uses signal 0
* which never delivers a signal but performs the existence/permission
* check. EPERM means the process exists but we cannot signal it (still
* counts as alive).
*/
export function isPidAlive(pid: number): boolean {
}
process.kill(pid, 0);
return true;
} catch (err: unknown) {
// EPERM: process exists but we lack permission to signal it (still alive).
// ESRCH: no such process.
// On Windows, `process.kill` with signal 0 throws ESRCH for missing PIDs.
return code === 'EPERM';
}