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