conductor-remote 1.46.2 → 1.47.0
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/README.md +36 -12
- package/bin/cli.js +7 -7
- package/dist/assets/{index-Cox9xTfw.js → index-BQpk3LSC.js} +23 -23
- package/dist/assets/{index-DGZkOBT-.css → index-ajfm3IVV.css} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/scripts/nosleep.js +14 -13
- package/dist-node/scripts/service.js +60 -20
- package/dist-node/src/config.js +34 -0
- package/dist-node/src/nosleep-helper.js +27 -4
- package/dist-node/src/nosleep.js +35 -14
- package/dist-node/src/server.js +1 -1
- package/package.json +3 -2
package/dist-node/src/nosleep.js
CHANGED
|
@@ -29,16 +29,20 @@
|
|
|
29
29
|
* re-`setup` — gated on ioreg's AppleClamshellState: an open lid, or a desktop Mac
|
|
30
30
|
* that has none, keeps the restore-only behaviour, because forcing sleep on a Mac
|
|
31
31
|
* someone may be sitting at is worse than the bug.
|
|
32
|
+
* - **The same window blocks the idle screen lock by default.** The root helper owns
|
|
33
|
+
* a `PreventUserIdleDisplaySleep` assertion through `caffeinate -d`; macOS drops it
|
|
34
|
+
* with the process even if the EXIT trap never runs. The pidfile records that mode
|
|
35
|
+
* so the phone can warn accurately, and the persisted setting can opt out.
|
|
32
36
|
*
|
|
33
37
|
* Stdlib only, strip-clean — see CLAUDE.md.
|
|
34
38
|
*/
|
|
35
39
|
import { execFile, spawn } from 'node:child_process';
|
|
36
40
|
import fs from 'node:fs';
|
|
37
41
|
import { promisify } from 'node:util';
|
|
38
|
-
import { HELPER_PATH, helperReady, PIDFILE_PATH } from "./nosleep-helper.js";
|
|
42
|
+
import { HELPER_PATH, helperFile, helperReady, installedHelper, PIDFILE_PATH } from "./nosleep-helper.js";
|
|
39
43
|
const execFileP = promisify(execFile);
|
|
40
44
|
/** Longest window the API will arm. A phone tap should never be able to disable sleep forever. */
|
|
41
|
-
export const MAX_SECONDS =
|
|
45
|
+
export const MAX_SECONDS = 16 * 3600;
|
|
42
46
|
/**
|
|
43
47
|
* Whether `pid` exists. EPERM is the interesting case: the armed helper runs as root, so
|
|
44
48
|
* signalling it from here is refused, and that refusal is itself proof it is alive.
|
|
@@ -52,7 +56,7 @@ function alive(pid) {
|
|
|
52
56
|
return err.code === 'EPERM';
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
|
-
/** Parse `<pid> <expiry-epoch-seconds>`; expiry 0 means "until stopped". */
|
|
59
|
+
/** Parse `<pid> <expiry-epoch-seconds> <start-token> <prevent-lock>`; expiry 0 means "until stopped". */
|
|
56
60
|
function readPidfile() {
|
|
57
61
|
let raw;
|
|
58
62
|
try {
|
|
@@ -61,12 +65,17 @@ function readPidfile() {
|
|
|
61
65
|
catch {
|
|
62
66
|
return null;
|
|
63
67
|
}
|
|
64
|
-
const [pidRaw, untilRaw] = raw.trim().split(/\s+/);
|
|
68
|
+
const [pidRaw, untilRaw, , preventLockRaw] = raw.trim().split(/\s+/);
|
|
65
69
|
const pid = Number(pidRaw);
|
|
66
70
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
67
71
|
return null;
|
|
68
72
|
const untilSec = Number(untilRaw);
|
|
69
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
pid,
|
|
75
|
+
until: Number.isFinite(untilSec) && untilSec > 0 ? untilSec * 1000 : null,
|
|
76
|
+
// Three-field records came from the old helper, which held no display assertion.
|
|
77
|
+
preventsScreenLock: preventLockRaw === '1'
|
|
78
|
+
};
|
|
70
79
|
}
|
|
71
80
|
/**
|
|
72
81
|
* Armed-ness on its own: a local file read plus a signal probe, no subprocess at all.
|
|
@@ -82,7 +91,13 @@ function armedRecord() {
|
|
|
82
91
|
return rec && alive(rec.pid) ? rec : null;
|
|
83
92
|
}
|
|
84
93
|
function armedState(rec) {
|
|
85
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
available: true,
|
|
96
|
+
armed: true,
|
|
97
|
+
until: rec.until,
|
|
98
|
+
pid: rec.pid,
|
|
99
|
+
preventsScreenLock: rec.preventsScreenLock
|
|
100
|
+
};
|
|
86
101
|
}
|
|
87
102
|
/**
|
|
88
103
|
* Current state. `helperReady()` shells out to sudo, so it is only consulted when nothing
|
|
@@ -92,7 +107,9 @@ export async function nosleepState() {
|
|
|
92
107
|
const rec = armedRecord();
|
|
93
108
|
if (rec)
|
|
94
109
|
return armedState(rec);
|
|
95
|
-
|
|
110
|
+
const ready = await helperReady();
|
|
111
|
+
const current = installedHelper() === helperFile();
|
|
112
|
+
return { available: ready && current, armed: false, until: null, pid: null, preventsScreenLock: false };
|
|
96
113
|
}
|
|
97
114
|
/**
|
|
98
115
|
* Whether the lid is physically shut. `null` means the probe couldn't say — a desktop Mac
|
|
@@ -150,8 +167,8 @@ function sleepSoon(why) {
|
|
|
150
167
|
function unavailable() {
|
|
151
168
|
return {
|
|
152
169
|
ok: false,
|
|
153
|
-
error: 'Passwordless nosleep
|
|
154
|
-
state: { available: false, armed: false, until: null, pid: null }
|
|
170
|
+
error: 'Passwordless nosleep is missing or out of date. Run `conductor-remote nosleep setup` on the Mac.',
|
|
171
|
+
state: { available: false, armed: false, until: null, pid: null, preventsScreenLock: false }
|
|
155
172
|
};
|
|
156
173
|
}
|
|
157
174
|
/**
|
|
@@ -159,8 +176,8 @@ function unavailable() {
|
|
|
159
176
|
* rather than stacking — the helper enforces that, and it has to, since two owners would
|
|
160
177
|
* restore each other's flipped values and leave sleep disabled for good.
|
|
161
178
|
*/
|
|
162
|
-
export async function armNoSleep(seconds) {
|
|
163
|
-
if (!(await helperReady()))
|
|
179
|
+
export async function armNoSleep(seconds, preventScreenLock = true) {
|
|
180
|
+
if (!(await helperReady()) || installedHelper() !== helperFile())
|
|
164
181
|
return unavailable();
|
|
165
182
|
// Floor of 1, not 0. The helper reads 0 as "until killed", so anything under a second
|
|
166
183
|
// truncates straight past MAX_SECONDS into a window nothing ever closes — which is the
|
|
@@ -168,7 +185,7 @@ export async function armNoSleep(seconds) {
|
|
|
168
185
|
const secs = Math.min(MAX_SECONDS, Math.max(1, Math.trunc(seconds)));
|
|
169
186
|
// Detached, own session, no stdio: it has to survive this relay's own restarts,
|
|
170
187
|
// which autoupdate performs routinely and without warning.
|
|
171
|
-
const child = spawn('sudo', ['-n', HELPER_PATH, String(secs), ''], {
|
|
188
|
+
const child = spawn('sudo', ['-n', HELPER_PATH, String(secs), '', preventScreenLock ? '1' : '0'], {
|
|
172
189
|
detached: true,
|
|
173
190
|
stdio: 'ignore'
|
|
174
191
|
});
|
|
@@ -214,7 +231,11 @@ export async function disarmNoSleep() {
|
|
|
214
231
|
sleepSoon('the keep-awake window was ended from the phone');
|
|
215
232
|
else
|
|
216
233
|
console.info('nosleep: window ended with the lid open (or unreadable) — sleep re-enabled, not forced');
|
|
217
|
-
return {
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
willSleep,
|
|
237
|
+
state: { available: true, armed: false, until: null, pid: null, preventsScreenLock: false }
|
|
238
|
+
};
|
|
218
239
|
}
|
|
219
240
|
await new Promise(r => setTimeout(r, 200));
|
|
220
241
|
}
|
|
@@ -257,7 +278,7 @@ function rescanExpiry() {
|
|
|
257
278
|
expiryKey = null;
|
|
258
279
|
void windowExpired(fireAt);
|
|
259
280
|
}, Math.max(0, fireAt - Date.now()));
|
|
260
|
-
// A
|
|
281
|
+
// A 16h timer must not hold the process open, and it survives nothing anyway — a
|
|
261
282
|
// restarted relay rebuilds it from the pidfile in watchNoSleepExpiry's first rescan.
|
|
262
283
|
expiryTimer.unref();
|
|
263
284
|
}
|
package/dist-node/src/server.js
CHANGED
|
@@ -671,7 +671,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
671
671
|
// truncates to 0 — an unbounded window from a request that looked bounded.
|
|
672
672
|
if (!Number.isInteger(seconds) || seconds < 1)
|
|
673
673
|
return json(req, res, 400, { error: 'need a whole number of seconds >= 1' });
|
|
674
|
-
const result = await armNoSleep(seconds);
|
|
674
|
+
const result = await armNoSleep(seconds, cfg.preventScreenLock);
|
|
675
675
|
return json(req, res, result.ok ? 200 : result.state.available ? 502 : 409, result);
|
|
676
676
|
}
|
|
677
677
|
// DELETE /api/nosleep — let it sleep again now, rather than at the window's end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.47.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.15.0",
|
|
6
6
|
"description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
|
|
@@ -50,12 +50,13 @@
|
|
|
50
50
|
"typecheck": "tsc -p tsconfig.json",
|
|
51
51
|
"lint": "biome check .",
|
|
52
52
|
"fix": "biome check . --fix",
|
|
53
|
-
"verify": "yarn typecheck && yarn lint && yarn check:imports && yarn check:routes && yarn check:attachments && yarn check:applescript && yarn check:model-labels && yarn check:nosleep && yarn check:uilock && yarn check:firstprompt && yarn check:sendonce && yarn check:notify",
|
|
53
|
+
"verify": "yarn typecheck && yarn lint && yarn check:imports && yarn check:routes && yarn check:attachments && yarn check:applescript && yarn check:config && yarn check:model-labels && yarn check:nosleep && yarn check:uilock && yarn check:firstprompt && yarn check:sendonce && yarn check:notify",
|
|
54
54
|
"release": "semantic-release",
|
|
55
55
|
"prepack": "yarn build && yarn build:node",
|
|
56
56
|
"postinstall": "husky || true",
|
|
57
57
|
"check:attachments": "node scripts/check-attachments.ts",
|
|
58
58
|
"check:applescript": "node scripts/check-applescript.ts",
|
|
59
|
+
"check:config": "node scripts/check-config.ts",
|
|
59
60
|
"check:firstprompt": "node scripts/check-firstprompt.ts",
|
|
60
61
|
"check:model-labels": "node scripts/check-model-labels.ts",
|
|
61
62
|
"check:sendonce": "node scripts/check-sendonce.ts",
|