Download src/utils/computerUse/computerUseLock.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 7.14 kB
-
https://huggingface.co/chenbhao/codev/resolve/714ab7d15ffd78992a4c3d4e2bf153c2f62f4841/src/utils/computerUse/computerUseLock.ts
- Command line
-
hf download hf://chenbhao/codev@714ab7d15ffd78992a4c3d4e2bf153c2f62f4841/src/utils/computerUse/computerUseLock.ts
-
curl -L -o computerUseLock.ts https://huggingface.co/chenbhao/codev/resolve/714ab7d15ffd78992a4c3d4e2bf153c2f62f4841/src/utils/computerUse/computerUseLock.ts
7.14 kB
| import { mkdir, readFile, unlink, writeFile } from 'fs/promises' | |
| import { join } from 'path' | |
| import { getSessionId } from '../../bootstrap/state.js' | |
| import { registerCleanup } from '../../utils/cleanupRegistry.js' | |
| import { logForDebugging } from '../../utils/debug.js' | |
| import { getClaudeConfigHomeDir } from '../../utils/envUtils.js' | |
| import { jsonParse, jsonStringify } from '../../utils/slowOperations.js' | |
| import { getErrnoCode } from '../errors.js' | |
| const LOCK_FILENAME = 'computer-use.lock' | |
| // Holds the unregister function for the shutdown cleanup handler. | |
| // Set when the lock is acquired, cleared when released. | |
| let unregisterCleanup: (() => void) | undefined | |
| type ComputerUseLock = { | |
| readonly sessionId: string | |
| readonly pid: number | |
| readonly acquiredAt: number | |
| } | |
| export type AcquireResult = | |
| | { readonly kind: 'acquired'; readonly fresh: boolean } | |
| | { readonly kind: 'blocked'; readonly by: string } | |
| export type CheckResult = | |
| | { readonly kind: 'free' } | |
| | { readonly kind: 'held_by_self' } | |
| | { readonly kind: 'blocked'; readonly by: string } | |
| const FRESH: AcquireResult = { kind: 'acquired', fresh: true } | |
| const REENTRANT: AcquireResult = { kind: 'acquired', fresh: false } | |
| function isComputerUseLock(value: unknown): value is ComputerUseLock { | |
| if (typeof value !== 'object' || value === null) return false | |
| return ( | |
| 'sessionId' in value && | |
| typeof value.sessionId === 'string' && | |
| 'pid' in value && | |
| typeof value.pid === 'number' | |
| ) | |
| } | |
| function getLockPath(): string { | |
| return join(getClaudeConfigHomeDir(), LOCK_FILENAME) | |
| } | |
| async function readLock(): Promise<ComputerUseLock | undefined> { | |
| try { | |
| const raw = await readFile(getLockPath(), 'utf8') | |
| const parsed: unknown = jsonParse(raw) | |
| return isComputerUseLock(parsed) ? parsed : undefined | |
| } catch { | |
| return undefined | |
| } | |
| } | |
| /** | |
| * Check whether a process is still running (signal 0 probe). | |
| * | |
| * Note: there is a small window for PID reuse β if the owning process | |
| * exits and an unrelated process is assigned the same PID, the check | |
| * will return true. This is extremely unlikely in practice. | |
| */ | |
| function isProcessRunning(pid: number): boolean { | |
| try { | |
| process.kill(pid, 0) | |
| return true | |
| } catch { | |
| return false | |
| } | |
| } | |
| /** | |
| * Attempt to create the lock file atomically with O_EXCL. | |
| * Returns true on success, false if the file already exists. | |
| * Throws for other errors. | |
| */ | |
| async function tryCreateExclusive(lock: ComputerUseLock): Promise<boolean> { | |
| try { | |
| await writeFile(getLockPath(), jsonStringify(lock), { flag: 'wx' }) | |
| return true | |
| } catch (e: unknown) { | |
| if (getErrnoCode(e) === 'EEXIST') return false | |
| throw e | |
| } | |
| } | |
| /** | |
| * Register a shutdown cleanup handler so the lock is released even if | |
| * turn-end cleanup is never reached (e.g. the user runs /exit while | |
| * a tool call is in progress). | |
| */ | |
| function registerLockCleanup(): void { | |
| unregisterCleanup?.() | |
| unregisterCleanup = registerCleanup(async () => { | |
| await releaseComputerUseLock() | |
| }) | |
| } | |
| /** | |
| * Check lock state without acquiring. Used for `request_access` / | |
| * `list_granted_applications` β the package's `defersLockAcquire` contract: | |
| * these tools check but don't take the lock, so the enter-notification and | |
| * overlay don't fire while the model is only asking for permission. | |
| * | |
| * Does stale-PID recovery (unlinks) so a dead session's lock doesn't block | |
| * `request_access`. Does NOT create β that's `tryAcquireComputerUseLock`'s job. | |
| */ | |
| export async function checkComputerUseLock(): Promise<CheckResult> { | |
| const existing = await readLock() | |
| if (!existing) return { kind: 'free' } | |
| if (existing.sessionId === getSessionId()) return { kind: 'held_by_self' } | |
| if (isProcessRunning(existing.pid)) { | |
| return { kind: 'blocked', by: existing.sessionId } | |
| } | |
| logForDebugging( | |
| `Recovering stale computer-use lock from session ${existing.sessionId} (PID ${existing.pid})`, | |
| ) | |
| await unlink(getLockPath()).catch(() => {}) | |
| return { kind: 'free' } | |
| } | |
| /** | |
| * Zero-syscall check: does THIS process believe it holds the lock? | |
| * True iff `tryAcquireComputerUseLock` succeeded and `releaseComputerUseLock` | |
| * hasn't run yet. Used to gate the per-turn release in `cleanup.ts` so | |
| * non-CU turns don't touch disk. | |
| */ | |
| export function isLockHeldLocally(): boolean { | |
| return unregisterCleanup !== undefined | |
| } | |
| /** | |
| * Try to acquire the computer-use lock for the current session. | |
| * | |
| * `{kind: 'acquired', fresh: true}` β first tool call of a CU turn. Callers fire | |
| * enter notifications on this. `{kind: 'acquired', fresh: false}` β re-entrant, | |
| * same session already holds it. `{kind: 'blocked', by}` β another live session | |
| * holds it. | |
| * | |
| * Uses O_EXCL (open 'wx') for atomic test-and-set β the OS guarantees at | |
| * most one process sees the create succeed. If the file already exists, | |
| * we check ownership and PID liveness; for a stale lock we unlink and | |
| * retry the exclusive create once. If two sessions race to recover the | |
| * same stale lock, only one create succeeds (the other reads the winner). | |
| */ | |
| export async function tryAcquireComputerUseLock(): Promise<AcquireResult> { | |
| const sessionId = getSessionId() | |
| const lock: ComputerUseLock = { | |
| sessionId, | |
| pid: process.pid, | |
| acquiredAt: Date.now(), | |
| } | |
| await mkdir(getClaudeConfigHomeDir(), { recursive: true }) | |
| // Fresh acquisition. | |
| if (await tryCreateExclusive(lock)) { | |
| registerLockCleanup() | |
| return FRESH | |
| } | |
| const existing = await readLock() | |
| // Corrupt/unparseable β treat as stale (can't extract a blocking ID). | |
| if (!existing) { | |
| await unlink(getLockPath()).catch(() => {}) | |
| if (await tryCreateExclusive(lock)) { | |
| registerLockCleanup() | |
| return FRESH | |
| } | |
| return { kind: 'blocked', by: (await readLock())?.sessionId ?? 'unknown' } | |
| } | |
| // Already held by this session. | |
| if (existing.sessionId === sessionId) return REENTRANT | |
| // Another live session holds it β blocked. | |
| if (isProcessRunning(existing.pid)) { | |
| return { kind: 'blocked', by: existing.sessionId } | |
| } | |
| // Stale lock β recover. Unlink then retry the exclusive create. | |
| // If another session is also recovering, one EEXISTs and reads the winner. | |
| logForDebugging( | |
| `Recovering stale computer-use lock from session ${existing.sessionId} (PID ${existing.pid})`, | |
| ) | |
| await unlink(getLockPath()).catch(() => {}) | |
| if (await tryCreateExclusive(lock)) { | |
| registerLockCleanup() | |
| return FRESH | |
| } | |
| return { kind: 'blocked', by: (await readLock())?.sessionId ?? 'unknown' } | |
| } | |
| /** | |
| * Release the computer-use lock if the current session owns it. Returns | |
| * `true` if we actually unlinked the file (i.e., we held it) β callers fire | |
| * exit notifications on this. Idempotent: subsequent calls return `false`. | |
| */ | |
| export async function releaseComputerUseLock(): Promise<boolean> { | |
| unregisterCleanup?.() | |
| unregisterCleanup = undefined | |
| const existing = await readLock() | |
| if (!existing || existing.sessionId !== getSessionId()) return false | |
| try { | |
| await unlink(getLockPath()) | |
| logForDebugging('Released computer-use lock') | |
| return true | |
| } catch { | |
| return false | |
| } | |
| } | |