preventSleep.ts
services/preventSleep.ts
166
Lines
4586
Bytes
3
Exports
3
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with integrations. It contains 166 lines, 3 detected imports, and 3 detected exports.
Important relationships
Detected exports
startPreventSleepstopPreventSleepforceStopPreventSleep
Keywords
caffeinateprocesscaffeinatesleepprocessrestartintervalrefcountvoidtimeoutlogfordebuggingmacos
Detected imports
child_process../utils/cleanupRegistry.js../utils/debug.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
/**
* 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
}
}
}