Skip to content

Commit ce8ecbf

Browse files
authored
Merge pull request #61 from narumiruna/fix/caffeinate-release-inhibitors
fix(caffeinate): release sleep inhibitors on shutdown
2 parents 8d6ea43 + 476e287 commit ce8ecbf

2 files changed

Lines changed: 47 additions & 11 deletions

File tree

MEMORY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- `MEMORY.md` is not auto-loaded; check it before non-trivial debugging or design work when prior project context may matter.
66
- npm can show a scoped package dist-tag (for example `latest`) while `npm view <package>` still returns 404; fix visibility with `npm access set status=public <package> --otp=<otp>` or publish a bumped version.
77
- `npm access set status=public <package>` cannot create brand-new scoped packages; if it returns access 404, first run `npm publish --workspace <package> --access public`.
8-
- For sleep-inhibitor extensions on WSL, prefer Windows `powershell.exe` with `SetThreadExecutionState`; `systemd-inhibit` may exist but fail without a usable systemd/logind session.
8+
- For sleep-inhibitor extensions on WSL, prefer Windows `powershell.exe` with `SetThreadExecutionState`; `systemd-inhibit` may exist but fail without a usable systemd/logind session. Ensure Windows inhibitors exit on stdin EOF, clear `ES_CONTINUOUS`, and pass execution-state flags as `[uint32]'0x...'`; ensure Unix inhibitors are parent-bound and trap cleanup, or Pi shutdown can leave a power request/process active.
99
- When testing TypeScript extensions via direct Node import, avoid parameter properties; Node's strip-only TypeScript loader rejects them even though `tsc` accepts them.
1010
- ty/ruff LSP servers may request `workspace/configuration`; respond with per-item empty config objects or diagnostic requests can hang.
1111
- pi-statusline is display-only; avoid prompt interception or customization commands unless intentionally reintroduced.

extensions/pi-caffeinate/src/caffeinate.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface InhibitorCommand {
1111
command: string;
1212
args: string[];
1313
description: string;
14+
releaseOnStdinClose?: boolean;
1415
}
1516

1617
interface CaffeinateState {
@@ -92,7 +93,7 @@ function startInhibitor(ctx: ExtensionContext) {
9293
try {
9394
const child = spawn(command.command, command.args, {
9495
detached: false,
95-
stdio: ["ignore", "pipe", "pipe"],
96+
stdio: [command.releaseOnStdinClose ? "pipe" : "ignore", "pipe", "pipe"],
9697
});
9798

9899
state.process = child;
@@ -144,13 +145,24 @@ function stopInhibitor(ctx: ExtensionContext, reason: string, options: { notify?
144145
child.removeAllListeners("error");
145146

146147
if (!child.killed) {
147-
if (process.platform === "win32") {
148+
if (state.command?.releaseOnStdinClose && child.stdin && !child.stdin.destroyed) {
149+
child.stdin.end();
150+
if (child.exitCode === null && child.signalCode === null) {
151+
const killTimer = setTimeout(() => {
152+
if (child.exitCode === null && child.signalCode === null && !child.killed) child.kill();
153+
}, 2000);
154+
killTimer.unref();
155+
child.once("exit", () => clearTimeout(killTimer));
156+
}
157+
} else if (process.platform === "win32") {
148158
child.kill();
149159
} else {
150160
child.kill("SIGTERM");
151161
}
152162
}
153163

164+
state.command = undefined;
165+
154166
if (options.notify !== false) {
155167
ctx.ui.notify(`Released pi-caffeinate (${reason}).`, "info");
156168
}
@@ -164,7 +176,7 @@ function getInhibitorCommand(): InhibitorCommand | undefined {
164176
}
165177

166178
if (process.platform === "darwin") {
167-
return { command: "caffeinate", args: ["-dimsu"], description: "caffeinate" };
179+
return parentBoundUnixCommand("caffeinate", ["-dimsu"], "caffeinate");
168180
}
169181

170182
if (process.platform === "linux") {
@@ -173,22 +185,22 @@ function getInhibitorCommand(): InhibitorCommand | undefined {
173185
}
174186

175187
if (commandExists("systemd-inhibit")) {
176-
return {
177-
command: "systemd-inhibit",
178-
args: [
188+
return parentBoundUnixCommand(
189+
"systemd-inhibit",
190+
[
179191
"--what=idle:sleep",
180192
"--who=pi-caffeinate",
181193
"--why=Pi agent is running",
182194
"--mode=block",
183195
"sleep",
184196
"infinity",
185197
],
186-
description: "systemd-inhibit",
187-
};
198+
"systemd-inhibit",
199+
);
188200
}
189201

190202
if (commandExists("caffeinate")) {
191-
return { command: "caffeinate", args: ["-dimsu"], description: "caffeinate" };
203+
return parentBoundUnixCommand("caffeinate", ["-dimsu"], "caffeinate");
192204
}
193205
}
194206

@@ -199,6 +211,29 @@ function getInhibitorCommand(): InhibitorCommand | undefined {
199211
return undefined;
200212
}
201213

214+
function parentBoundUnixCommand(
215+
command: string,
216+
args: string[],
217+
description: string,
218+
): InhibitorCommand {
219+
return {
220+
command: "sh",
221+
args: [
222+
"-c",
223+
unixParentBoundScript(),
224+
"pi-caffeinate-watch",
225+
String(process.pid),
226+
command,
227+
...args,
228+
],
229+
description,
230+
};
231+
}
232+
233+
function unixParentBoundScript() {
234+
return `parent=$1; shift; "$@" & child=$!; ( while kill -0 "$parent" 2>/dev/null; do sleep 5; done; kill "$child" 2>/dev/null ) & watcher=$!; cleanup() { kill "$watcher" 2>/dev/null; kill "$child" 2>/dev/null; wait "$child" 2>/dev/null; }; trap 'cleanup; exit 0' INT TERM HUP EXIT; wait "$child"; status=$?; kill "$watcher" 2>/dev/null; trap - EXIT; exit "$status"`;
235+
}
236+
202237
function commandExists(command: string) {
203238
const path = process.env.PATH ?? "";
204239
const extensions = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
@@ -262,11 +297,12 @@ function windowsPowerInhibitorCommand(command: string): InhibitorCommand {
262297
command,
263298
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", windowsInhibitorScript()],
264299
description: "PowerShell SetThreadExecutionState",
300+
releaseOnStdinClose: true,
265301
};
266302
}
267303

268304
function windowsInhibitorScript() {
269-
return `Add-Type -Namespace Native -Name Power -MemberDefinition '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);'; while ($true) { [Native.Power]::SetThreadExecutionState(0x80000000 -bor 0x00000001 -bor 0x00000002) | Out-Null; Start-Sleep -Seconds 30 }`;
305+
return `$ErrorActionPreference = 'Stop'; Add-Type -Namespace Native -Name Power -MemberDefinition '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);'; $flags = [uint32]'0x80000003'; $release = [uint32]'0x80000000'; $stdin = [Console]::OpenStandardInput(); $buffer = New-Object byte[] 1; $readTask = $stdin.ReadAsync($buffer, 0, 1); try { while ($true) { [Native.Power]::SetThreadExecutionState($flags) | Out-Null; if ($readTask.Wait(30000)) { break } } } finally { [Native.Power]::SetThreadExecutionState($release) | Out-Null }`;
270306
}
271307

272308
function isWsl() {

0 commit comments

Comments
 (0)