ai-or-die 0.1.95 → 0.1.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ai-or-die.js +6 -1
- package/package.json +1 -1
- package/src/hibernation-guard.js +202 -0
- package/src/keepalive-manager.js +79 -15
- package/src/server.js +11 -0
package/bin/ai-or-die.js
CHANGED
|
@@ -46,7 +46,8 @@ program
|
|
|
46
46
|
.option('--sticky-notes-model <url>', 'override the sticky-note model GGUF download URL')
|
|
47
47
|
.option('--sticky-notes-threads <number>', 'CPU threads for sticky-note inference (default: auto — three-quarters of the cores on CPU, gentle on GPU)')
|
|
48
48
|
.option('--no-keepalive', 'disable keeping the machine awake while the server runs (Windows only; on by default)')
|
|
49
|
-
.option('--keepalive-display', 'also keep the display on (default keeps the system awake but lets the monitor sleep)')
|
|
49
|
+
.option('--keepalive-display', 'also keep the display on (default keeps the system awake but lets the monitor sleep)')
|
|
50
|
+
.option('--disable-hibernation', 'disable OS hibernation + set sleep/hibernate timeouts to Never via elevated powercfg (Windows only; off by default; prompts for UAC; needed to stop host-initiated hibernation on Hyper-V guests)');
|
|
50
51
|
|
|
51
52
|
// Auto-open is OFF by default and opt-in via --open. Legacy callers may still pass
|
|
52
53
|
// --no-open (the old opt-out flag); filter it out so it parses harmlessly as a no-op.
|
|
@@ -138,6 +139,10 @@ async function main() {
|
|
|
138
139
|
// the display on.
|
|
139
140
|
keepalive: options.keepalive !== false && process.env.AIORDIE_DISABLE_KEEPALIVE !== '1',
|
|
140
141
|
keepaliveDisplay: options.keepaliveDisplay === true || process.env.AIORDIE_KEEPALIVE_DISPLAY === '1',
|
|
142
|
+
// Opt-in: disable OS hibernation (+ sleep/hibernate timeouts -> Never) via
|
|
143
|
+
// an elevated powercfg. Off by default. The wake assertion can't block a
|
|
144
|
+
// host-initiated hibernate (Hyper-V vmicshutdown); this removes the target.
|
|
145
|
+
disableHibernation: options.disableHibernation === true || process.env.AIORDIE_DISABLE_HIBERNATION === '1',
|
|
141
146
|
// Mesh binds loopback-only always: the tailnet `serve` proxy reaches the
|
|
142
147
|
// port, the LAN never does (even with --https). Other modes: all interfaces.
|
|
143
148
|
bindHost: options.mesh ? '127.0.0.1' : undefined,
|
package/package.json
CHANGED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const childProcess = require('child_process');
|
|
4
|
+
const KeepaliveManager = require('./keepalive-manager');
|
|
5
|
+
|
|
6
|
+
// Opt-in remediation for HOST-INITIATED sleep on Windows (primarily Hyper-V
|
|
7
|
+
// guests). The keepalive wake assertion (SetThreadExecutionState /
|
|
8
|
+
// PowerRequestSystemRequired) only suppresses the IDLE timer; Windows documents
|
|
9
|
+
// that it "cannot be used to prevent ... standby or hibernate" when the suspend
|
|
10
|
+
// is requested programmatically. On a Hyper-V guest, the host's Guest Shutdown
|
|
11
|
+
// Service (`vmicshutdown`) requests hibernation through the Application API,
|
|
12
|
+
// which the assertion cannot block. The only reliable fix is to remove the sleep
|
|
13
|
+
// target itself: `powercfg /hibernate off` (+ sleep/hibernate timeouts -> Never).
|
|
14
|
+
// On a Hyper-V guest S3 standby is typically unavailable, so disabling S4
|
|
15
|
+
// hibernate eliminates the vector entirely.
|
|
16
|
+
//
|
|
17
|
+
// That is a PRIVILEGED, PERSISTENT machine change (survives our process exit;
|
|
18
|
+
// reversible with `powercfg /hibernate on`), so it is OFF by default and gated
|
|
19
|
+
// behind --disable-hibernation / AIORDIE_DISABLE_HIBERNATION=1. When enabled, one
|
|
20
|
+
// long-lived in-box `powershell.exe` (spawned like the keepalive helper) runs
|
|
21
|
+
// ONCE at startup:
|
|
22
|
+
// 1. Reads HKLM\...\Power\HibernateEnabled (non-privileged). If already 0 it
|
|
23
|
+
// prints SKIPPED and exits -- no UAC prompt, so a machine that is already
|
|
24
|
+
// configured never prompts again.
|
|
25
|
+
// 2. Otherwise it launches an ELEVATED cmd.exe via `Start-Process -Verb RunAs`
|
|
26
|
+
// (UAC is the OS approve/deny; the flag is the user's pre-approval) that runs
|
|
27
|
+
// the powercfg remediation, then prints APPLIED / DENIED / ERROR.
|
|
28
|
+
// Best-effort and non-fatal: a denied UAC prompt, a headless/remote session with
|
|
29
|
+
// no interactive desktop, or any spawn failure logs a warning and the server
|
|
30
|
+
// continues normally. Never throws into startup. Windows-only; instant no-op on
|
|
31
|
+
// macOS/Linux. See docs/specs/keepalive.md and docs/adrs/0030-hibernation-guard.md.
|
|
32
|
+
|
|
33
|
+
// The remediation, chained under ONE elevation (one UAC prompt) via `cmd /c a & b`.
|
|
34
|
+
// All constants (no user input), so there is no command-injection surface. `& `
|
|
35
|
+
// runs every command regardless of a prior one's exit; success is judged by
|
|
36
|
+
// re-reading HibernateEnabled afterward (not the chain's exit code, which only
|
|
37
|
+
// reflects the last command), so an early failure can't be masked.
|
|
38
|
+
const REMEDIATION = [
|
|
39
|
+
'powercfg /hibernate off',
|
|
40
|
+
'powercfg /change standby-timeout-ac 0',
|
|
41
|
+
'powercfg /change standby-timeout-dc 0',
|
|
42
|
+
'powercfg /change hibernate-timeout-ac 0',
|
|
43
|
+
'powercfg /change hibernate-timeout-dc 0',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const READY_TIMEOUT_MS = 60000;
|
|
47
|
+
|
|
48
|
+
class HibernationGuard {
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this._enabled = !!options.enabled;
|
|
51
|
+
this._platform = options.platform || process.platform;
|
|
52
|
+
this._spawn = options.spawn || childProcess.spawn;
|
|
53
|
+
this._logger = options.logger || console;
|
|
54
|
+
this._readyTimeoutMs = options.readyTimeoutMs || READY_TIMEOUT_MS;
|
|
55
|
+
|
|
56
|
+
this._ran = false;
|
|
57
|
+
this._child = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---- pure builders (static so tests assert them without spawning) ----
|
|
61
|
+
|
|
62
|
+
static buildScript(ppid = process.pid) {
|
|
63
|
+
const argline = '/c ' + REMEDIATION.join(' & ');
|
|
64
|
+
return [
|
|
65
|
+
// 'Stop' turns a blocked/failed cmdlet into a caught terminating error
|
|
66
|
+
// rather than a silent partial run.
|
|
67
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
68
|
+
// Trusted-integer parent-PID tag for triage (never user input).
|
|
69
|
+
`# aod-hibernation ppid=${ppid}`,
|
|
70
|
+
// (1) Non-privileged detect. Skip (no UAC) when hibernate is already off.
|
|
71
|
+
`$he = $null`,
|
|
72
|
+
`try { $he = (Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Power' -Name 'HibernateEnabled' -ErrorAction Stop).HibernateEnabled } catch { $he = $null }`,
|
|
73
|
+
`if ($he -eq 0) { [Console]::Out.WriteLine('SKIPPED'); exit 0 }`,
|
|
74
|
+
// (2) One elevated cmd.exe runs the whole powercfg chain under a single UAC
|
|
75
|
+
// prompt. Absolute cmd path (PATH-hijack hardening). WaitForExit + ExitCode
|
|
76
|
+
// is more reliable than -Wait with -Verb RunAs for reading the result.
|
|
77
|
+
`$cmd = Join-Path $env:SystemRoot 'System32\\cmd.exe'`,
|
|
78
|
+
`$argline = '${argline}'`,
|
|
79
|
+
`try {`,
|
|
80
|
+
` $p = Start-Process -FilePath $cmd -Verb RunAs -WindowStyle Hidden -PassThru -ArgumentList $argline`,
|
|
81
|
+
` $p.WaitForExit()`,
|
|
82
|
+
// Verify the critical end-state directly rather than trusting the exit code:
|
|
83
|
+
// `cmd /c a & b & c` reports only the LAST command's exit, so an early
|
|
84
|
+
// failure could be masked. APPLIED iff hibernation is now actually off.
|
|
85
|
+
` $after = $null`,
|
|
86
|
+
` try { $after = (Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Power' -Name 'HibernateEnabled' -ErrorAction Stop).HibernateEnabled } catch { $after = $null }`,
|
|
87
|
+
` if ($after -eq 0) { [Console]::Out.WriteLine('APPLIED') } else { [Console]::Out.WriteLine('ERROR'); [Console]::Error.WriteLine('hibernate still enabled after powercfg (exit=' + $p.ExitCode + ')') }`,
|
|
88
|
+
`} catch {`,
|
|
89
|
+
// Detect a declined UAC by the Win32 error code (1223 = ERROR_CANCELLED),
|
|
90
|
+
// locale-independent; walk the inner-exception chain, then fall back to text.
|
|
91
|
+
` $cancelled = $false`,
|
|
92
|
+
` $e = $_.Exception`,
|
|
93
|
+
` while ($null -ne $e) { if (($e -is [System.ComponentModel.Win32Exception]) -and ($e.NativeErrorCode -eq 1223)) { $cancelled = $true; break }; $e = $e.InnerException }`,
|
|
94
|
+
` if (-not $cancelled -and $_.Exception.Message -match 'cancel') { $cancelled = $true }`,
|
|
95
|
+
` if ($cancelled) { [Console]::Out.WriteLine('DENIED') } else { [Console]::Out.WriteLine('ERROR'); [Console]::Error.WriteLine($_.Exception.Message) }`,
|
|
96
|
+
`}`,
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static buildArgs(ppid = process.pid) {
|
|
101
|
+
return [
|
|
102
|
+
'-NoProfile',
|
|
103
|
+
'-NonInteractive',
|
|
104
|
+
'-ExecutionPolicy', 'Bypass',
|
|
105
|
+
'-Command', HibernationGuard.buildScript(ppid),
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Attempt the remediation once. No-op unless enabled on win32. Idempotent.
|
|
110
|
+
// Fire-and-forget: the server never awaits this (the UAC prompt is interactive
|
|
111
|
+
// and may take a while). Never throws.
|
|
112
|
+
run() {
|
|
113
|
+
if (this._ran) return;
|
|
114
|
+
if (!this._enabled || this._platform !== 'win32') return;
|
|
115
|
+
this._ran = true;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const ps = KeepaliveManager.powershellPath();
|
|
119
|
+
const args = HibernationGuard.buildArgs();
|
|
120
|
+
const child = this._spawn(ps, args, {
|
|
121
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
shell: false,
|
|
124
|
+
});
|
|
125
|
+
this._child = child;
|
|
126
|
+
|
|
127
|
+
// Never let the helper pin the parent's event loop.
|
|
128
|
+
this._safe(() => child.unref());
|
|
129
|
+
this._safe(() => child.stdout && child.stdout.unref && child.stdout.unref());
|
|
130
|
+
this._safe(() => child.stderr && child.stderr.unref && child.stderr.unref());
|
|
131
|
+
|
|
132
|
+
let out = '';
|
|
133
|
+
let err = '';
|
|
134
|
+
if (child.stdout) {
|
|
135
|
+
this._safe(() => child.stdout.setEncoding && child.stdout.setEncoding('utf8'));
|
|
136
|
+
child.stdout.on('data', (d) => { out += String(d); });
|
|
137
|
+
child.stdout.on('error', () => {});
|
|
138
|
+
}
|
|
139
|
+
if (child.stderr) {
|
|
140
|
+
this._safe(() => child.stderr.setEncoding && child.stderr.setEncoding('utf8'));
|
|
141
|
+
child.stderr.on('data', (d) => { err += String(d); });
|
|
142
|
+
child.stderr.on('error', () => {});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let settled = false;
|
|
146
|
+
const finish = (status, hint) => {
|
|
147
|
+
if (settled) return;
|
|
148
|
+
settled = true;
|
|
149
|
+
this._safe(() => clearTimeout(timer));
|
|
150
|
+
if (this._child === child) this._child = null;
|
|
151
|
+
this._logOutcome(status, hint);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// The UAC prompt blocks the helper until the user responds; only reap after
|
|
155
|
+
// a generous timeout so a genuinely stuck helper cannot linger forever.
|
|
156
|
+
const timer = setTimeout(() => {
|
|
157
|
+
finish('ERROR', 'timed out waiting for the elevation prompt');
|
|
158
|
+
this._safe(() => child.kill && child.kill());
|
|
159
|
+
}, this._readyTimeoutMs);
|
|
160
|
+
if (timer.unref) timer.unref();
|
|
161
|
+
|
|
162
|
+
child.on('error', (e) => finish('ERROR', (e && e.message) || 'spawn error'));
|
|
163
|
+
child.on('close', () => {
|
|
164
|
+
const m = out.match(/\b(APPLIED|SKIPPED|DENIED|ERROR)\b/);
|
|
165
|
+
finish(m ? m[1] : 'ERROR', err.trim());
|
|
166
|
+
});
|
|
167
|
+
} catch (err) {
|
|
168
|
+
this._child = null;
|
|
169
|
+
this._safe(() => this._logger.debug && this._logger.debug(
|
|
170
|
+
'hibernation-guard: failed to start (continuing):', err && err.message));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_logOutcome(status, hint) {
|
|
175
|
+
const L = this._logger;
|
|
176
|
+
switch (status) {
|
|
177
|
+
case 'APPLIED':
|
|
178
|
+
this._safe(() => L.log && L.log(
|
|
179
|
+
'hibernation guard: hibernation disabled + sleep/hibernate timeouts set to Never (host-initiated hibernation suppressed)'));
|
|
180
|
+
break;
|
|
181
|
+
case 'SKIPPED':
|
|
182
|
+
this._safe(() => L.log && L.log(
|
|
183
|
+
'hibernation guard: hibernation already disabled — nothing to do'));
|
|
184
|
+
break;
|
|
185
|
+
case 'DENIED':
|
|
186
|
+
this._safe(() => L.warn && L.warn(
|
|
187
|
+
'⚠ hibernation guard: elevation declined — hibernation left enabled. A Hyper-V host can still hibernate this machine. Run `powercfg /hibernate off` as admin, or relaunch without --disable-hibernation.'));
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
this._safe(() => L.warn && L.warn(
|
|
191
|
+
`⚠ hibernation guard: could not disable hibernation${hint ? ` (${hint})` : ''}; the host may still hibernate this machine. Run \`powercfg /hibernate off\` as admin.`));
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_safe(fn) {
|
|
197
|
+
try { return fn(); } catch (_) { /* the guard must never throw into the caller */ }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = HibernationGuard;
|
|
202
|
+
module.exports.REMEDIATION = REMEDIATION;
|
package/src/keepalive-manager.js
CHANGED
|
@@ -6,15 +6,26 @@ const path = require('path');
|
|
|
6
6
|
// Keep the host machine awake while the ai-or-die server runs (Windows 11).
|
|
7
7
|
//
|
|
8
8
|
// Mechanism: spawn ONE long-lived Windows PowerShell (5.1, in-box) helper that
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
9
|
+
// holds TWO redundant kernel32 power assertions, then blocks on stdin:
|
|
10
|
+
// 1. SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) -- the legacy
|
|
11
|
+
// per-thread assertion (shows in `powercfg /requests` with no reason text).
|
|
12
|
+
// 2. PowerCreateRequest + PowerSetRequest(PowerRequestSystemRequired) with a
|
|
13
|
+
// REASON_CONTEXT SimpleReasonString of "GitHub Copilot CLI session active".
|
|
14
|
+
// This is the exact API + reason string the GitHub Copilot CLI /keep-alive
|
|
15
|
+
// registers (its bundled cli-native.node calls the same kernel32 Power*
|
|
16
|
+
// functions with that wide string), so `powercfg /requests` shows a
|
|
17
|
+
// byte-identical "GitHub Copilot CLI session active" line.
|
|
18
|
+
// Holding both is deliberate belt-and-suspenders (chosen over Power*-only): if
|
|
19
|
+
// either API is refused at runtime the other still keeps the machine awake. The
|
|
20
|
+
// helper prints OK once AT LEAST ONE assertion is held. When the parent closes
|
|
21
|
+
// stdin (graceful release) OR dies (the OS tears down the pipe -> ReadLine()
|
|
22
|
+
// returns null), the helper clears both and exits. Windows also drops a thread's
|
|
23
|
+
// execution-state flags AND releases a process's power requests when the holding
|
|
24
|
+
// process dies, so even taskkill /F cannot leak past reboot. No native deps.
|
|
15
25
|
//
|
|
16
26
|
// Windows-only by design; an instant no-op on macOS/Linux. See
|
|
17
|
-
// docs/specs/keepalive.md and
|
|
27
|
+
// docs/specs/keepalive.md and
|
|
28
|
+
// docs/adrs/0029-windows-keepalive-power-request.md (supersedes ADR-0028).
|
|
18
29
|
|
|
19
30
|
// SetThreadExecutionState flags as DECIMAL uint32 literals. PowerShell 5.1
|
|
20
31
|
// parses 0x80000001 as a negative Int32 and the [uint32] cast then throws, so
|
|
@@ -26,6 +37,15 @@ const ES_CONTINUOUS = 2147483648;
|
|
|
26
37
|
const ES_SYSTEM = 2147483649;
|
|
27
38
|
const ES_SYSTEM_DISPLAY = 2147483651;
|
|
28
39
|
|
|
40
|
+
// POWER_REQUEST_TYPE enum values passed to PowerSetRequest / PowerClearRequest.
|
|
41
|
+
// Small ints, so no PS 5.1 hex/Int32 parsing hazard (unlike the ES_ flags).
|
|
42
|
+
const PR_DISPLAY = 0; // PowerRequestDisplayRequired
|
|
43
|
+
const PR_SYSTEM = 1; // PowerRequestSystemRequired
|
|
44
|
+
|
|
45
|
+
// REASON_CONTEXT.SimpleReasonString shown by `powercfg /requests`. Byte-for-byte
|
|
46
|
+
// the string the GitHub Copilot CLI registers, so the entry is indistinguishable.
|
|
47
|
+
const KEEPALIVE_REASON = 'GitHub Copilot CLI session active';
|
|
48
|
+
|
|
29
49
|
const READY_TIMEOUT_MS = 5000;
|
|
30
50
|
|
|
31
51
|
class KeepaliveManager {
|
|
@@ -52,7 +72,25 @@ class KeepaliveManager {
|
|
|
52
72
|
// ---- pure builders (static so tests assert them without spawning) ----
|
|
53
73
|
|
|
54
74
|
static buildScript(displayRequired, ppid = process.pid) {
|
|
55
|
-
const
|
|
75
|
+
const stesAssert = displayRequired ? ES_SYSTEM_DISPLAY : ES_SYSTEM;
|
|
76
|
+
// One Add-Type carrying the REASON_CONTEXT struct (SIMPLE_STRING layout:
|
|
77
|
+
// Version + Flags + a single LPWStr) and the four kernel32 P/Invokes.
|
|
78
|
+
// Attributes are fully qualified so the compile never depends on Add-Type's
|
|
79
|
+
// default `using`s. The native REASON_CONTEXT is a union (SimpleReasonString
|
|
80
|
+
// XOR a larger Detailed struct); we only ever use SIMPLE_STRING, but two
|
|
81
|
+
// long pad fields make the marshalled struct >= the native union size
|
|
82
|
+
// (32 bytes x64 / 28 x86) so PowerCreateRequest can never read past the [In]
|
|
83
|
+
// buffer. Marshalling verified on a real Windows host.
|
|
84
|
+
const members = [
|
|
85
|
+
'[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]',
|
|
86
|
+
'public struct REASON_CONTEXT { public uint Version; public uint Flags; [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] public string Reason; public long Pad0; public long Pad1; }',
|
|
87
|
+
'[System.Runtime.InteropServices.DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint e);',
|
|
88
|
+
'[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] public static extern System.IntPtr PowerCreateRequest(ref REASON_CONTEXT Context);',
|
|
89
|
+
'[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] public static extern bool PowerSetRequest(System.IntPtr PowerRequest, int RequestType);',
|
|
90
|
+
'[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] public static extern bool PowerClearRequest(System.IntPtr PowerRequest, int RequestType);',
|
|
91
|
+
'[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(System.IntPtr hObject);',
|
|
92
|
+
].join('\n');
|
|
93
|
+
|
|
56
94
|
return [
|
|
57
95
|
// 'Stop' is load-bearing: under Constrained Language Mode / WDAC /
|
|
58
96
|
// AppLocker / EDR, Add-Type (which writes a .cs to %TEMP% and shells to
|
|
@@ -65,13 +103,39 @@ class KeepaliveManager {
|
|
|
65
103
|
// input) so a stale/orphaned helper is identifiable for triage:
|
|
66
104
|
// Get-CimInstance Win32_Process -Filter "Name='powershell.exe'"
|
|
67
105
|
`# aod-keepalive ppid=${ppid}`,
|
|
68
|
-
`Add-Type -Name P -Namespace W -MemberDefinition '
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
`if ([W.P]::SetThreadExecutionState([uint32]${
|
|
106
|
+
`Add-Type -Name P -Namespace W -MemberDefinition '${members}'`,
|
|
107
|
+
`$held = $false`,
|
|
108
|
+
// (1) Legacy per-thread assertion. Redundant safety net; carries no name in
|
|
109
|
+
// `powercfg /requests`.
|
|
110
|
+
`if ([W.P]::SetThreadExecutionState([uint32]${stesAssert}) -ne 0) { $held = $true }`,
|
|
111
|
+
// (2) Named power request -- this is the "GitHub Copilot CLI session active"
|
|
112
|
+
// line. Flags=1 is POWER_REQUEST_CONTEXT_SIMPLE_STRING; Version=0.
|
|
113
|
+
`$ctx = New-Object 'W.P+REASON_CONTEXT'`,
|
|
114
|
+
`$ctx.Version = 0`,
|
|
115
|
+
`$ctx.Flags = 1`,
|
|
116
|
+
`$ctx.Reason = '${KEEPALIVE_REASON}'`,
|
|
117
|
+
`$h = [W.P]::PowerCreateRequest([ref]$ctx)`,
|
|
118
|
+
// PowerCreateRequest returns NULL or INVALID_HANDLE_VALUE (-1) on failure.
|
|
119
|
+
`$hv = $h.ToInt64()`,
|
|
120
|
+
`$powerSys = $false`,
|
|
121
|
+
...(displayRequired ? [`$powerDisp = $false`] : []),
|
|
122
|
+
// Set each request type independently so "at least one held" counts a
|
|
123
|
+
// display-only success too, and each is cleared only if it was set.
|
|
124
|
+
`if ($hv -ne 0 -and $hv -ne -1) {`,
|
|
125
|
+
` if ([W.P]::PowerSetRequest($h, ${PR_SYSTEM})) { $held = $true; $powerSys = $true }`,
|
|
126
|
+
...(displayRequired ? [` if ([W.P]::PowerSetRequest($h, ${PR_DISPLAY})) { $held = $true; $powerDisp = $true }`] : []),
|
|
127
|
+
`}`,
|
|
128
|
+
// Only latch "started" (block on stdin) when SOMETHING is actually held, so
|
|
129
|
+
// a total failure surfaces as ready=false instead of a silent awake-less
|
|
130
|
+
// block. Close a valid-but-unused handle before exiting so nothing lingers.
|
|
131
|
+
`if (-not $held) { if ($hv -ne 0 -and $hv -ne -1) { [void][W.P]::CloseHandle($h) } [Console]::Error.WriteLine('SetThreadExecutionState and PowerSetRequest both failed'); exit 1 }`,
|
|
132
|
+
`[Console]::Out.WriteLine('OK'); [Console]::Out.Flush()`,
|
|
73
133
|
`while ($null -ne [Console]::In.ReadLine()) {}`,
|
|
134
|
+
// Release each held assertion on EOF; process death also drops all of them.
|
|
74
135
|
`[void][W.P]::SetThreadExecutionState([uint32]${ES_CONTINUOUS})`,
|
|
136
|
+
`if ($powerSys) { [void][W.P]::PowerClearRequest($h, ${PR_SYSTEM}) }`,
|
|
137
|
+
...(displayRequired ? [`if ($powerDisp) { [void][W.P]::PowerClearRequest($h, ${PR_DISPLAY}) }`] : []),
|
|
138
|
+
`if ($hv -ne 0 -and $hv -ne -1) { [void][W.P]::CloseHandle($h) }`,
|
|
75
139
|
].join('\n');
|
|
76
140
|
}
|
|
77
141
|
|
|
@@ -206,8 +270,8 @@ class KeepaliveManager {
|
|
|
206
270
|
const hint = firstErr || 'powershell.exe unavailable or blocked (Constrained Language Mode / WDAC / AV)';
|
|
207
271
|
this._safe(() => this._logger.warn && this._logger.warn(
|
|
208
272
|
`⚠ keepalive: could not hold the wake assertion; the machine may sleep (${hint}). Disable with --no-keepalive.`));
|
|
209
|
-
// Reap a helper that timed out without exiting (e.g.
|
|
210
|
-
//
|
|
273
|
+
// Reap a helper that timed out without exiting (e.g. both assertions were
|
|
274
|
+
// refused yet it is somehow blocked on stdin) so it cannot leak.
|
|
211
275
|
if (this._child === child) this._safe(() => this.releaseSync());
|
|
212
276
|
}
|
|
213
277
|
}).catch(() => {});
|
package/src/server.js
CHANGED
|
@@ -34,6 +34,7 @@ const CircularBuffer = require('./utils/circular-buffer');
|
|
|
34
34
|
const MinHeap = require('./utils/eviction-heap');
|
|
35
35
|
const RestartManager = require('./restart-manager');
|
|
36
36
|
const KeepaliveManager = require('./keepalive-manager');
|
|
37
|
+
const HibernationGuard = require('./hibernation-guard');
|
|
37
38
|
const { ControlEventBus, EVENT_KINDS: CONTROL_EVENT_KINDS } = require('./control/event-bus');
|
|
38
39
|
const TranscriptBuffer = require('./sticky-note-transcript');
|
|
39
40
|
const { createControlRouter } = require('./control/routes');
|
|
@@ -287,6 +288,13 @@ class ClaudeCodeWebServer {
|
|
|
287
288
|
process.env.AIORDIE_DISABLE_KEEPALIVE !== '1',
|
|
288
289
|
keepDisplayOn: !!options.keepaliveDisplay,
|
|
289
290
|
});
|
|
291
|
+
// Opt-in, one-shot at startup: disable OS hibernation via elevated powercfg
|
|
292
|
+
// (Windows only). The wake assertion above only blocks the idle timer; a
|
|
293
|
+
// host-initiated hibernate (Hyper-V vmicshutdown) needs the target removed.
|
|
294
|
+
// Off by default; same underTest/CI gating as keepalive.
|
|
295
|
+
this.hibernationGuard = new HibernationGuard({
|
|
296
|
+
enabled: !!options.disableHibernation && !underTest && !isCI,
|
|
297
|
+
});
|
|
290
298
|
|
|
291
299
|
this.sessionStore = new SessionStore(options.sessionStoreOptions);
|
|
292
300
|
this.usageReader = new UsageReader(this.sessionDurationHours);
|
|
@@ -3552,6 +3560,9 @@ class ClaudeCodeWebServer {
|
|
|
3552
3560
|
// Now listening — hold the OS awake for the server's lifetime
|
|
3553
3561
|
// (Windows only; no-op elsewhere; never throws).
|
|
3554
3562
|
this.keepaliveManager.start();
|
|
3563
|
+
// Opt-in one-shot: disable hibernation (elevated) so a Hyper-V host
|
|
3564
|
+
// can't hibernate us. Fire-and-forget; off by default; never throws.
|
|
3565
|
+
this.hibernationGuard.run();
|
|
3555
3566
|
resolve(server);
|
|
3556
3567
|
}
|
|
3557
3568
|
};
|