forked from sanbuphy/learn-coding-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpreventSleep.ts
More file actions
165 lines (143 loc) · 4.48 KB
/
preventSleep.ts
File metadata and controls
165 lines (143 loc) · 4.48 KB
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
/**
* 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
}
}
}