Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
6.3 kB · 195 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196// Scheduler lease lock for .claude/scheduled_tasks.json.//// When multiple Claude sessions run in the same project directory, only one// should drive the cron scheduler. The first session to acquire this lock// becomes the scheduler; others stay passive and periodically probe the lock.// If the owner dies (PID no longer running), a passive session takes over.//// Pattern mirrors computerUseLock.ts: O_EXCL atomic create, PID liveness// probe, stale-lock recovery, cleanup-on-exit.
import { mkdir, readFile, unlink, writeFile } from 'fs/promises'import { dirname, join } from 'path'import { z } from 'zod/v4'import { getProjectRoot, getSessionId } from '../bootstrap/state.js'import { registerCleanup } from './cleanupRegistry.js'import { logForDebugging } from './debug.js'import { getErrnoCode } from './errors.js'import { isProcessRunning } from './genericProcessUtils.js'import { safeParseJSON } from './json.js'import { lazySchema } from './lazySchema.js'import { jsonStringify } from './slowOperations.js'
const LOCK_FILE_REL = join('.claude', 'scheduled_tasks.lock')
const schedulerLockSchema = lazySchema(() => z.object({ sessionId: z.string(), pid: z.number(), acquiredAt: z.number(), }),)type SchedulerLock = z.infer<ReturnType<typeof schedulerLockSchema>>
/** * Options for out-of-REPL callers (Agent SDK daemon) that don't have * bootstrap state. When omitted, falls back to getProjectRoot() + * getSessionId() as before. lockIdentity should be stable for the lifetime * of one daemon process (e.g. a randomUUID() captured at startup). */export type SchedulerLockOptions = { dir?: string lockIdentity?: string}
let unregisterCleanup: (() => void) | undefined// Suppress repeat "held by X" log lines when polling a live owner.let lastBlockedBy: string | undefined
function getLockPath(dir?: string): string { return join(dir ?? getProjectRoot(), LOCK_FILE_REL)}
async function readLock(dir?: string): Promise<SchedulerLock | undefined> { let raw: string try { raw = await readFile(getLockPath(dir), 'utf8') } catch { return undefined } const result = schedulerLockSchema().safeParse(safeParseJSON(raw, false)) return result.success ? result.data : undefined}
async function tryCreateExclusive( lock: SchedulerLock, dir?: string,): Promise<boolean> { const path = getLockPath(dir) const body = jsonStringify(lock) try { await writeFile(path, body, { flag: 'wx' }) return true } catch (e: unknown) { const code = getErrnoCode(e) if (code === 'EEXIST') return false if (code === 'ENOENT') { // .claude/ doesn't exist yet — create it and retry once. In steady // state the dir already exists (scheduled_tasks.json lives there), // so this path is hit at most once. await mkdir(dirname(path), { recursive: true }) try { await writeFile(path, body, { flag: 'wx' }) return true } catch (retryErr: unknown) { if (getErrnoCode(retryErr) === 'EEXIST') return false throw retryErr } } throw e }}
function registerLockCleanup(opts?: SchedulerLockOptions): void { unregisterCleanup?.() unregisterCleanup = registerCleanup(async () => { await releaseSchedulerLock(opts) })}
/** * Try to acquire the scheduler lock for the current session. * Returns true on success, false if another live session holds it. * * Uses O_EXCL ('wx') for atomic test-and-set. If the file exists: * - Already ours → true (idempotent re-acquire) * - Another live PID → false * - Stale (PID dead / corrupt) → unlink and retry exclusive create once * * If two sessions race to recover a stale lock, only one create succeeds. */export async function tryAcquireSchedulerLock( opts?: SchedulerLockOptions,): Promise<boolean> { const dir = opts?.dir // "sessionId" in the lock file is really just a stable owner key. REPL // uses getSessionId(); daemon callers supply their own UUID. PID remains // the liveness signal regardless. const sessionId = opts?.lockIdentity ?? getSessionId() const lock: SchedulerLock = { sessionId, pid: process.pid, acquiredAt: Date.now(), }
if (await tryCreateExclusive(lock, dir)) { lastBlockedBy = undefined registerLockCleanup(opts) logForDebugging( `[ScheduledTasks] acquired scheduler lock (PID ${process.pid})`, ) return true }
const existing = await readLock(dir)
// Already ours (idempotent). After --resume the session ID is restored // but the process has a new PID — update the lock file so other sessions // see a live PID and don't steal it. if (existing?.sessionId === sessionId) { if (existing.pid !== process.pid) { await writeFile(getLockPath(dir), jsonStringify(lock)) registerLockCleanup(opts) } return true }
// Corrupt or unparseable — treat as stale. // Another live session — blocked. if (existing && isProcessRunning(existing.pid)) { if (lastBlockedBy !== existing.sessionId) { lastBlockedBy = existing.sessionId logForDebugging( `[ScheduledTasks] scheduler lock held by session ${existing.sessionId} (PID ${existing.pid})`, ) } return false }
// Stale — unlink and retry the exclusive create once. if (existing) { logForDebugging( `[ScheduledTasks] recovering stale scheduler lock from PID ${existing.pid}`, ) } await unlink(getLockPath(dir)).catch(() => {}) if (await tryCreateExclusive(lock, dir)) { lastBlockedBy = undefined registerLockCleanup(opts) return true } // Another session won the recovery race. return false}
/** * Release the scheduler lock if the current session owns it. */export async function releaseSchedulerLock( opts?: SchedulerLockOptions,): Promise<void> { unregisterCleanup?.() unregisterCleanup = undefined lastBlockedBy = undefined
const dir = opts?.dir const sessionId = opts?.lockIdentity ?? getSessionId() const existing = await readLock(dir) if (!existing || existing.sessionId !== sessionId) return try { await unlink(getLockPath(dir)) logForDebugging('[ScheduledTasks] released scheduler lock') } catch { // Already gone. }}