source dump of claude code
at main 165 lines 4.6 kB view raw
1/** 2 * Prevents macOS from sleeping while Claude is working. 3 * 4 * Uses the built-in `caffeinate` command to create a power assertion that 5 * prevents idle sleep. This keeps the Mac awake during API requests and 6 * tool execution so long-running operations don't get interrupted. 7 * 8 * The caffeinate process is spawned with a timeout and periodically restarted. 9 * This provides self-healing behavior: if the Node process is killed with 10 * SIGKILL (which doesn't run cleanup handlers), the orphaned caffeinate will 11 * automatically exit after the timeout expires. 12 * 13 * Only runs on macOS - no-op on other platforms. 14 */ 15import { type ChildProcess, spawn } from 'child_process' 16import { registerCleanup } from '../utils/cleanupRegistry.js' 17import { logForDebugging } from '../utils/debug.js' 18 19// Caffeinate timeout in seconds. Process auto-exits after this duration. 20// We restart it before expiry to maintain continuous sleep prevention. 21const CAFFEINATE_TIMEOUT_SECONDS = 300 // 5 minutes 22 23// Restart interval - restart caffeinate before it expires. 24// Use 4 minutes to give plenty of buffer before the 5 minute timeout. 25const RESTART_INTERVAL_MS = 4 * 60 * 1000 26 27let caffeinateProcess: ChildProcess | null = null 28let restartInterval: ReturnType<typeof setInterval> | null = null 29let refCount = 0 30let cleanupRegistered = false 31 32/** 33 * Increment the reference count and start preventing sleep if needed. 34 * Call this when starting work that should keep the Mac awake. 35 */ 36export function startPreventSleep(): void { 37 refCount++ 38 39 if (refCount === 1) { 40 spawnCaffeinate() 41 startRestartInterval() 42 } 43} 44 45/** 46 * Decrement the reference count and allow sleep if no more work is pending. 47 * Call this when work completes. 48 */ 49export function stopPreventSleep(): void { 50 if (refCount > 0) { 51 refCount-- 52 } 53 54 if (refCount === 0) { 55 stopRestartInterval() 56 killCaffeinate() 57 } 58} 59 60/** 61 * Force stop preventing sleep, regardless of reference count. 62 * Use this for cleanup on exit. 63 */ 64export function forceStopPreventSleep(): void { 65 refCount = 0 66 stopRestartInterval() 67 killCaffeinate() 68} 69 70function startRestartInterval(): void { 71 // Only run on macOS 72 if (process.platform !== 'darwin') { 73 return 74 } 75 76 // Already running 77 if (restartInterval !== null) { 78 return 79 } 80 81 restartInterval = setInterval(() => { 82 // Only restart if we still need sleep prevention 83 if (refCount > 0) { 84 logForDebugging('Restarting caffeinate to maintain sleep prevention') 85 killCaffeinate() 86 spawnCaffeinate() 87 } 88 }, RESTART_INTERVAL_MS) 89 90 // Don't let the interval keep the Node process alive 91 restartInterval.unref() 92} 93 94function stopRestartInterval(): void { 95 if (restartInterval !== null) { 96 clearInterval(restartInterval) 97 restartInterval = null 98 } 99} 100 101function spawnCaffeinate(): void { 102 // Only run on macOS 103 if (process.platform !== 'darwin') { 104 return 105 } 106 107 // Already running 108 if (caffeinateProcess !== null) { 109 return 110 } 111 112 // Register cleanup on first use to ensure caffeinate is killed on exit 113 if (!cleanupRegistered) { 114 cleanupRegistered = true 115 registerCleanup(async () => { 116 forceStopPreventSleep() 117 }) 118 } 119 120 try { 121 // -i: Create an assertion to prevent idle sleep 122 // This is the least aggressive option - display can still sleep 123 // -t: Timeout in seconds - caffeinate exits automatically after this 124 // This provides self-healing if Node is killed with SIGKILL 125 caffeinateProcess = spawn( 126 'caffeinate', 127 ['-i', '-t', String(CAFFEINATE_TIMEOUT_SECONDS)], 128 { 129 stdio: 'ignore', 130 }, 131 ) 132 133 // Don't let caffeinate keep the Node process alive 134 caffeinateProcess.unref() 135 136 const thisProc = caffeinateProcess 137 caffeinateProcess.on('error', err => { 138 logForDebugging(`caffeinate spawn error: ${err.message}`) 139 if (caffeinateProcess === thisProc) caffeinateProcess = null 140 }) 141 142 caffeinateProcess.on('exit', () => { 143 if (caffeinateProcess === thisProc) caffeinateProcess = null 144 }) 145 146 logForDebugging('Started caffeinate to prevent sleep') 147 } catch { 148 // Silently fail - caffeinate not available or spawn failed 149 caffeinateProcess = null 150 } 151} 152 153function killCaffeinate(): void { 154 if (caffeinateProcess !== null) { 155 const proc = caffeinateProcess 156 caffeinateProcess = null 157 try { 158 // SIGKILL for immediate termination - SIGTERM could be delayed 159 proc.kill('SIGKILL') 160 logForDebugging('Stopped caffeinate, allowing sleep') 161 } catch { 162 // Process may have already exited 163 } 164 } 165}