@join-pantheon/cli 0.32.1 → 0.33.0-rc.2
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/dist/accounts.js +1 -1
- package/dist/board.js +1417 -0
- package/dist/claude-config.js +189 -0
- package/dist/cli.js +351 -101
- package/dist/client.js +16 -0
- package/dist/command-state.js +2 -1
- package/dist/dbus.js +92 -13
- package/dist/doctor.js +36 -5
- package/dist/folder-guard.js +89 -0
- package/dist/git-safe.js +3 -3
- package/dist/host-env.js +47 -1
- package/dist/install.js +76 -10
- package/dist/installer.js +2 -1
- package/dist/ledger.js +2 -2
- package/dist/machine.js +174 -3
- package/dist/managed.js +27 -1
- package/dist/notice-linux.js +501 -0
- package/dist/notice-windows.js +323 -0
- package/dist/notifier.js +12 -3
- package/dist/quickstart.js +28 -11
- package/dist/relay.js +64 -9
- package/dist/service.js +455 -29
- package/dist/setup-guide.js +22 -5
- package/dist/store.js +18 -2
- package/dist/uninstall.js +1 -1
- package/dist/upgrade.js +249 -4
- package/dist/uri-handler.js +335 -0
- package/dist/win-host.js +993 -0
- package/dist/worker-policy.js +3 -0
- package/dist/worker.js +11 -11
- package/helper/macos/prebuilt/manifest.json +2 -2
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/windows/pantheon-host.cs +170 -0
- package/helper/windows/pantheon-notice.ps1 +127 -0
- package/helper/windows/pantheon-tray.ps1 +731 -0
- package/helper/windows/pantheon.ico +0 -0
- package/installer/install.ps1 +238 -19
- package/installer/install.sh +36 -1
- package/package.json +2 -2
package/dist/win-host.js
ADDED
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve, win32 } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { CONFIG_DIR } from "./config.js";
|
|
8
|
+
import { pidAlive } from "./file-lock.js";
|
|
9
|
+
import { VERSION } from "./version.js";
|
|
10
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
export const LEGACY_TASK_NAME = "Pantheon";
|
|
12
|
+
const SID_RE = /^S-1-\d+(?:-\d+)+$/;
|
|
13
|
+
export function parseWhoamiSid(out) {
|
|
14
|
+
const m = /"[^"]*"\s*,\s*"(S-1-[0-9-]+)"/.exec(out) ?? /\b(S-1-\d+(?:-\d+)+)\b/.exec(out);
|
|
15
|
+
const sid = m?.[1] ?? null;
|
|
16
|
+
return sid && SID_RE.test(sid) ? sid : null;
|
|
17
|
+
}
|
|
18
|
+
export function taskNameFor(sid, user = "") {
|
|
19
|
+
if (sid && SID_RE.test(sid))
|
|
20
|
+
return `${LEGACY_TASK_NAME}-${sid}`;
|
|
21
|
+
const safe = user.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64);
|
|
22
|
+
return `${LEGACY_TASK_NAME}-${safe || "user"}`;
|
|
23
|
+
}
|
|
24
|
+
const sidCache = new WeakMap();
|
|
25
|
+
export function userSid(exec) {
|
|
26
|
+
if (sidCache.has(exec))
|
|
27
|
+
return sidCache.get(exec);
|
|
28
|
+
let sid = null;
|
|
29
|
+
try {
|
|
30
|
+
sid = parseWhoamiSid(String(exec("whoami", ["/user", "/fo", "csv", "/nh"]) ?? ""));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
sid = null;
|
|
34
|
+
}
|
|
35
|
+
sidCache.set(exec, sid);
|
|
36
|
+
return sid;
|
|
37
|
+
}
|
|
38
|
+
export function legacyTaskIsUsers(exec, env = process.env) {
|
|
39
|
+
let xml;
|
|
40
|
+
try {
|
|
41
|
+
const out = exec("schtasks", ["/Query", "/TN", LEGACY_TASK_NAME, "/XML"]);
|
|
42
|
+
xml = (Buffer.isBuffer(out) ? fromUtf16le(out) : String(out ?? "")).replace(/\0/g, "");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return taskIsUsers(xml, taskUserId(env.USERNAME ?? env.USER ?? "", env), userSid(exec));
|
|
48
|
+
}
|
|
49
|
+
export function recordedTaskName(home = CONFIG_DIR) {
|
|
50
|
+
try {
|
|
51
|
+
return /<URI>\\([^<]+)<\/URI>/.exec(fromUtf16le(readFileSync(hostFiles(home).taskXml)))?.[1] ?? null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export const taskName = (exec, env = process.env) => taskNameFor(userSid(exec), env.USERNAME ?? env.USER ?? "");
|
|
58
|
+
export function taskIsUsers(xml, userId, sid) {
|
|
59
|
+
const ids = [...xml.matchAll(/<UserId>([^<]*)<\/UserId>/g)].map((m) => m[1].trim().toLowerCase());
|
|
60
|
+
if (!ids.length)
|
|
61
|
+
return false;
|
|
62
|
+
const mine = new Set([userId.toLowerCase(), ...(sid ? [sid.toLowerCase()] : [])]);
|
|
63
|
+
const bare = userId.includes("\\") ? userId.split("\\").pop().toLowerCase() : userId.toLowerCase();
|
|
64
|
+
return ids.every((id) => mine.has(id) || (!id.includes("\\") && !id.startsWith("s-1-") && id === bare));
|
|
65
|
+
}
|
|
66
|
+
export const HOST_KINDS = ["worker", "notifier"];
|
|
67
|
+
export const HOST_CHILDREN = ["worker", "notifier", "tray"];
|
|
68
|
+
export const isHostKind = (k) => HOST_KINDS.includes(k);
|
|
69
|
+
export const HOST_RESTART_CODE = 75;
|
|
70
|
+
export const hostDir = (home = CONFIG_DIR) => join(home, "host");
|
|
71
|
+
export const hostFiles = (home = CONFIG_DIR) => {
|
|
72
|
+
const dir = hostDir(home);
|
|
73
|
+
return {
|
|
74
|
+
dir,
|
|
75
|
+
definition: (k) => join(dir, `${k}.json`),
|
|
76
|
+
disabled: (c) => join(dir, `${c}.disabled`),
|
|
77
|
+
restart: (c) => join(dir, `${c}.restart`),
|
|
78
|
+
stop: join(dir, "stop"),
|
|
79
|
+
trayStop: join(dir, "tray.stop"),
|
|
80
|
+
notices: join(dir, "notices"),
|
|
81
|
+
noticesSkipped: join(dir, "notices.skipped"),
|
|
82
|
+
launch: join(dir, "launch.txt"),
|
|
83
|
+
clm: join(dir, "clm"),
|
|
84
|
+
state: join(dir, "state.json"),
|
|
85
|
+
lock: join(dir, "host.lock"),
|
|
86
|
+
taskXml: join(dir, "task.xml"),
|
|
87
|
+
log: join(home, "logs", "host.log"),
|
|
88
|
+
childLog: (c) => join(home, "logs", `${c}.log`),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export function readDefinition(kind, home = CONFIG_DIR) {
|
|
92
|
+
try {
|
|
93
|
+
const d = JSON.parse(readFileSync(hostFiles(home).definition(kind), "utf8"));
|
|
94
|
+
if (!d || typeof d.cwd !== "string" || !Array.isArray(d.args) || !d.args.every((a) => typeof a === "string"))
|
|
95
|
+
return null;
|
|
96
|
+
return { kind, cwd: d.cwd, args: d.args };
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function writeDefinition(def, home = CONFIG_DIR) {
|
|
103
|
+
const f = hostFiles(home).definition(def.kind);
|
|
104
|
+
writeAtomic(f, JSON.stringify({ kind: def.kind, cwd: def.cwd, args: def.args }, null, 2) + "\n");
|
|
105
|
+
return f;
|
|
106
|
+
}
|
|
107
|
+
function writeAtomic(f, text) {
|
|
108
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
109
|
+
const tmp = `${f}.${process.pid}.tmp`;
|
|
110
|
+
writeFileSync(tmp, text);
|
|
111
|
+
try {
|
|
112
|
+
renameSync(tmp, f);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
rmSync(tmp, { force: true });
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export const HOST_BEAT_STALE_MS = 30_000;
|
|
120
|
+
export function readHostState(home = CONFIG_DIR) {
|
|
121
|
+
try {
|
|
122
|
+
const s = JSON.parse(readFileSync(hostFiles(home).state, "utf8"));
|
|
123
|
+
return s && Number.isInteger(s.pid) && typeof s.beat === "number" ? s : null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function hostAlive(s, now = Date.now(), alive = pidAlive) {
|
|
130
|
+
return !!s && now - s.beat < HOST_BEAT_STALE_MS && s.beat - now < 60_000 && alive(s.pid);
|
|
131
|
+
}
|
|
132
|
+
export function hostChildRunning(c, home = CONFIG_DIR, now = Date.now(), alive = pidAlive) {
|
|
133
|
+
const s = readHostState(home);
|
|
134
|
+
if (!hostAlive(s, now, alive))
|
|
135
|
+
return null;
|
|
136
|
+
const r = s.children[c];
|
|
137
|
+
if (!r || r.phase !== "running" || !r.pid || !alive(r.pid))
|
|
138
|
+
return null;
|
|
139
|
+
return { ...r, pid: r.pid, hostPid: s.pid };
|
|
140
|
+
}
|
|
141
|
+
export const HOST_POLICY = { baseMs: 2_000, maxMs: 60_000, healthyMs: 10 * 60_000 };
|
|
142
|
+
export const KILL_AGAIN_MS = 10_000;
|
|
143
|
+
export const GIVE_UP_CODE = 64;
|
|
144
|
+
export const initialSup = (p = HOST_POLICY) => ({
|
|
145
|
+
children: Object.fromEntries(HOST_CHILDREN.map((c) => [c, { phase: "stopped", delayMs: p.baseMs, restarts: 0 }])),
|
|
146
|
+
exiting: null,
|
|
147
|
+
done: false,
|
|
148
|
+
});
|
|
149
|
+
const clone = (s) => ({ ...s, children: Object.fromEntries(Object.entries(s.children).map(([k, v]) => [k, { ...v }])) });
|
|
150
|
+
export function hostStep(prev, e, p = HOST_POLICY) {
|
|
151
|
+
const s = clone(prev);
|
|
152
|
+
const cmds = [];
|
|
153
|
+
if (s.done)
|
|
154
|
+
return { s, cmds };
|
|
155
|
+
const scheduleAfterExit = (c, ch, now, want, why, code = null) => {
|
|
156
|
+
const ranMs = ch.startedAt !== undefined ? now - ch.startedAt : 0;
|
|
157
|
+
if (ranMs >= p.healthyMs)
|
|
158
|
+
ch.delayMs = p.baseMs;
|
|
159
|
+
delete ch.pid;
|
|
160
|
+
delete ch.startedAt;
|
|
161
|
+
delete ch.stoppingAt;
|
|
162
|
+
delete ch.argv;
|
|
163
|
+
if (s.exiting !== null || want === null) {
|
|
164
|
+
ch.phase = "stopped";
|
|
165
|
+
delete ch.again;
|
|
166
|
+
delete ch.nextAt;
|
|
167
|
+
cmds.push({ do: "log", line: `${c}: ${why} — stays stopped` });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (code === GIVE_UP_CODE && !ch.again) {
|
|
171
|
+
ch.phase = "stopped";
|
|
172
|
+
delete ch.nextAt;
|
|
173
|
+
ch.gaveUp = "clm";
|
|
174
|
+
cmds.push({ do: "log", line: `${c}: ${why} — it cannot run in this session (PowerShell is in Constrained Language Mode); not started again until the next logon` });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (ch.again) {
|
|
178
|
+
delete ch.again;
|
|
179
|
+
delete ch.nextAt;
|
|
180
|
+
ch.delayMs = p.baseMs;
|
|
181
|
+
ch.phase = "running";
|
|
182
|
+
ch.key = want;
|
|
183
|
+
cmds.push({ do: "log", line: `${c}: ${why} — starting it again now` });
|
|
184
|
+
cmds.push({ do: "start", child: c, key: want });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
ch.restarts++;
|
|
188
|
+
ch.phase = "waiting";
|
|
189
|
+
ch.nextAt = now + ch.delayMs;
|
|
190
|
+
cmds.push({ do: "log", line: `${c}: ${why} — starting it again in ${Math.round(ch.delayMs / 1000)}s` });
|
|
191
|
+
ch.delayMs = Math.min(ch.delayMs * 2, p.maxMs);
|
|
192
|
+
};
|
|
193
|
+
switch (e.t) {
|
|
194
|
+
case "tick": {
|
|
195
|
+
if (s.exiting === null && (e.stop || e.restartHost)) {
|
|
196
|
+
s.exiting = e.stop ? 0 : HOST_RESTART_CODE;
|
|
197
|
+
cmds.push({ do: "log", line: e.stop ? "stop requested — stopping everything" : "a new Pantheon version is on disk — restarting the host on it" });
|
|
198
|
+
}
|
|
199
|
+
for (const c of HOST_CHILDREN) {
|
|
200
|
+
const ch = s.children[c];
|
|
201
|
+
const want = s.exiting !== null ? null : e.want[c];
|
|
202
|
+
if (ch.phase === "running") {
|
|
203
|
+
if (want === null || want !== ch.key || e.restart.includes(c)) {
|
|
204
|
+
if (want !== null)
|
|
205
|
+
ch.again = true;
|
|
206
|
+
ch.phase = "stopping";
|
|
207
|
+
ch.stoppingAt = e.now;
|
|
208
|
+
cmds.push({ do: "kill", child: c, pid: ch.pid });
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (ch.phase === "stopping") {
|
|
213
|
+
if (ch.pid && ch.stoppingAt !== undefined && e.now - ch.stoppingAt >= KILL_AGAIN_MS) {
|
|
214
|
+
ch.stoppingAt = e.now;
|
|
215
|
+
cmds.push({ do: "kill", child: c, pid: ch.pid });
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (want === null) {
|
|
220
|
+
if (ch.phase === "waiting") {
|
|
221
|
+
ch.phase = "stopped";
|
|
222
|
+
delete ch.nextAt;
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const restartNow = e.restart.includes(c);
|
|
227
|
+
if (ch.gaveUp && !restartNow)
|
|
228
|
+
continue;
|
|
229
|
+
if (ch.phase === "stopped" || restartNow || (ch.nextAt !== undefined && e.now >= ch.nextAt)) {
|
|
230
|
+
if (restartNow) {
|
|
231
|
+
ch.delayMs = p.baseMs;
|
|
232
|
+
delete ch.gaveUp;
|
|
233
|
+
}
|
|
234
|
+
delete ch.nextAt;
|
|
235
|
+
ch.phase = "running";
|
|
236
|
+
ch.key = want;
|
|
237
|
+
cmds.push({ do: "start", child: c, key: want });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (s.exiting !== null && HOST_CHILDREN.every((c) => s.children[c].phase === "stopped")) {
|
|
241
|
+
s.done = true;
|
|
242
|
+
cmds.push({ do: "exit", code: s.exiting });
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case "started": {
|
|
247
|
+
const ch = s.children[e.child];
|
|
248
|
+
ch.pid = e.pid;
|
|
249
|
+
ch.startedAt = e.now;
|
|
250
|
+
ch.phase = "running";
|
|
251
|
+
if (e.argv)
|
|
252
|
+
ch.argv = [...e.argv];
|
|
253
|
+
else
|
|
254
|
+
delete ch.argv;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case "spawnFailed": {
|
|
258
|
+
const ch = s.children[e.child];
|
|
259
|
+
ch.startedAt = e.now;
|
|
260
|
+
scheduleAfterExit(e.child, ch, e.now, e.want, `could not start (${e.error})`);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case "exited": {
|
|
264
|
+
const ch = s.children[e.child];
|
|
265
|
+
ch.lastExit = e.code !== null ? String(e.code) : (e.signal ?? "unknown");
|
|
266
|
+
scheduleAfterExit(e.child, ch, e.now, e.want, `exited (${e.code !== null ? `code ${e.code}` : e.signal ?? "no code"})`, e.code);
|
|
267
|
+
if (s.exiting !== null && HOST_CHILDREN.every((c) => s.children[c].phase === "stopped")) {
|
|
268
|
+
s.done = true;
|
|
269
|
+
cmds.push({ do: "exit", code: s.exiting });
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { s, cmds };
|
|
275
|
+
}
|
|
276
|
+
export function hostReport(s, pid, started, now, version = VERSION, session) {
|
|
277
|
+
const children = {};
|
|
278
|
+
for (const c of HOST_CHILDREN) {
|
|
279
|
+
const ch = s.children[c];
|
|
280
|
+
children[c] = {
|
|
281
|
+
phase: ch.phase, restarts: ch.restarts,
|
|
282
|
+
...(ch.pid ? { pid: ch.pid } : {}), ...(ch.startedAt && ch.pid ? { since: ch.startedAt } : {}),
|
|
283
|
+
...(ch.lastExit !== undefined ? { lastExit: ch.lastExit } : {}), ...(ch.nextAt !== undefined ? { nextAt: ch.nextAt } : {}),
|
|
284
|
+
...(ch.pid && ch.argv ? { argv: ch.argv } : {}), ...(ch.gaveUp ? { gaveUp: ch.gaveUp } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return { pid, version, started, beat: now, ...(session ? { session } : {}), children };
|
|
288
|
+
}
|
|
289
|
+
export class HostLoop {
|
|
290
|
+
io;
|
|
291
|
+
policy;
|
|
292
|
+
pid;
|
|
293
|
+
s;
|
|
294
|
+
started;
|
|
295
|
+
constructor(io, policy = HOST_POLICY, pid = process.pid) {
|
|
296
|
+
this.io = io;
|
|
297
|
+
this.policy = policy;
|
|
298
|
+
this.pid = pid;
|
|
299
|
+
this.s = initialSup(policy);
|
|
300
|
+
this.started = io.now();
|
|
301
|
+
}
|
|
302
|
+
feed(e) {
|
|
303
|
+
const { s, cmds } = hostStep(this.s, e, this.policy);
|
|
304
|
+
this.s = s;
|
|
305
|
+
for (const c of cmds) {
|
|
306
|
+
if (c.do === "log")
|
|
307
|
+
this.io.log(c.line);
|
|
308
|
+
else if (c.do === "kill") {
|
|
309
|
+
try {
|
|
310
|
+
this.io.kill(c.child, c.pid);
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
this.io.log(`${c.child}: could not stop pid ${c.pid} (${err.message})`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (c.do === "start") {
|
|
317
|
+
const now = this.io.now();
|
|
318
|
+
let started;
|
|
319
|
+
try {
|
|
320
|
+
started = this.io.start(c.child, (code, signal) => this.exited(c.child, code, signal));
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
this.feed({ t: "spawnFailed", child: c.child, now, error: err.message.split("\n")[0], want: this.io.want().want[c.child] });
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const pid = typeof started === "number" ? started : started.pid;
|
|
327
|
+
this.io.log(`${c.child}: started, pid ${pid}`);
|
|
328
|
+
this.feed({ t: "started", child: c.child, pid, now, ...(typeof started === "number" ? {} : { argv: started.argv }) });
|
|
329
|
+
}
|
|
330
|
+
else if (c.do === "exit") {
|
|
331
|
+
this.writeState();
|
|
332
|
+
this.io.exit(c.code);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
writeState() { try {
|
|
337
|
+
this.io.writeState(hostReport(this.s, this.pid, this.started, this.io.now(), VERSION, this.io.session));
|
|
338
|
+
}
|
|
339
|
+
catch { } }
|
|
340
|
+
tick() {
|
|
341
|
+
const { want, installed } = this.io.want();
|
|
342
|
+
const stop = this.io.stopRequested() || this.io.parentGone() || installed === 0;
|
|
343
|
+
const restartHost = !stop && this.io.newVersion() && !this.io.workerBusy();
|
|
344
|
+
this.feed({ t: "tick", now: this.io.now(), want, restart: this.io.takeRestarts(), stop, restartHost });
|
|
345
|
+
if (!this.s.done)
|
|
346
|
+
this.writeState();
|
|
347
|
+
}
|
|
348
|
+
exited(child, code, signal) {
|
|
349
|
+
this.feed({ t: "exited", child, code, signal, now: this.io.now(), want: this.io.want().want[child] });
|
|
350
|
+
if (!this.s.done)
|
|
351
|
+
this.writeState();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
export const trayScriptPath = () => resolve(HERE, "..", "helper", "windows", "pantheon-tray.ps1");
|
|
355
|
+
export const trayIconPath = () => resolve(HERE, "..", "helper", "windows", "pantheon.ico");
|
|
356
|
+
export const noticeScriptPath = () => resolve(HERE, "..", "helper", "windows", "pantheon-notice.ps1");
|
|
357
|
+
export const psQuote = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
358
|
+
export const CLM_GUARD = `if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { exit ${GIVE_UP_CODE} }`;
|
|
359
|
+
export function trayArgv(script, pantheon, home, extra = {}, powershell = windowsPowerShell()) {
|
|
360
|
+
const more = [
|
|
361
|
+
...(extra.icon ? [`-Icon ${psQuote(extra.icon)}`] : []),
|
|
362
|
+
...(extra.notices ? [`-NoticeScript ${psQuote(extra.notices)}`] : []),
|
|
363
|
+
];
|
|
364
|
+
return [powershell, "-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command",
|
|
365
|
+
`${CLM_GUARD}; & ([scriptblock]::Create([IO.File]::ReadAllText(${psQuote(script)}))) -Pantheon ${psQuote(pantheon)} -Home ${psQuote(home)}${more.map((m) => ` ${m}`).join("")}`];
|
|
366
|
+
}
|
|
367
|
+
export const trayQuitText = (session, at = new Date()) => `quit session=${session} at=${at.toISOString()}\n`;
|
|
368
|
+
export function trayQuitHolds(home, session) {
|
|
369
|
+
let text;
|
|
370
|
+
try {
|
|
371
|
+
text = readFileSync(hostFiles(home).disabled("tray"), "utf8");
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
return /\bsession=([A-Za-z0-9_.:-]+)/.exec(text)?.[1] === session;
|
|
377
|
+
}
|
|
378
|
+
export function clearTrayQuit(home = CONFIG_DIR) {
|
|
379
|
+
const f = hostFiles(home);
|
|
380
|
+
const had = existsSync(f.disabled("tray"));
|
|
381
|
+
try {
|
|
382
|
+
rmSync(f.disabled("tray"), { force: true });
|
|
383
|
+
}
|
|
384
|
+
catch { }
|
|
385
|
+
return had;
|
|
386
|
+
}
|
|
387
|
+
export function clearStaleTrayQuit(home, session) {
|
|
388
|
+
if (!existsSync(hostFiles(home).disabled("tray")) || trayQuitHolds(home, session))
|
|
389
|
+
return false;
|
|
390
|
+
return clearTrayQuit(home);
|
|
391
|
+
}
|
|
392
|
+
export function hostSession(env = process.env, pid = process.pid) {
|
|
393
|
+
const s = env.PANTHEON_HOST_SESSION;
|
|
394
|
+
return s && /^[A-Za-z0-9_.:-]{8,80}$/.test(s) ? s : `local-${pid}`;
|
|
395
|
+
}
|
|
396
|
+
export function splitWindowsCommandLine(line) {
|
|
397
|
+
const out = [];
|
|
398
|
+
let i = 0;
|
|
399
|
+
const n = line.length;
|
|
400
|
+
while (i < n && /[ \t]/.test(line[i]))
|
|
401
|
+
i++;
|
|
402
|
+
if (i >= n)
|
|
403
|
+
return out;
|
|
404
|
+
let prog = "";
|
|
405
|
+
if (line[i] === '"') {
|
|
406
|
+
i++;
|
|
407
|
+
while (i < n && line[i] !== '"')
|
|
408
|
+
prog += line[i++];
|
|
409
|
+
i++;
|
|
410
|
+
}
|
|
411
|
+
else
|
|
412
|
+
while (i < n && !/[ \t]/.test(line[i]))
|
|
413
|
+
prog += line[i++];
|
|
414
|
+
out.push(prog);
|
|
415
|
+
for (;;) {
|
|
416
|
+
while (i < n && /[ \t]/.test(line[i]))
|
|
417
|
+
i++;
|
|
418
|
+
if (i >= n)
|
|
419
|
+
return out;
|
|
420
|
+
let cur = "", inQ = false;
|
|
421
|
+
while (i < n) {
|
|
422
|
+
const c = line[i];
|
|
423
|
+
if (c === "\\") {
|
|
424
|
+
let j = i;
|
|
425
|
+
while (j < n && line[j] === "\\")
|
|
426
|
+
j++;
|
|
427
|
+
const bs = j - i;
|
|
428
|
+
if (line[j] === '"') {
|
|
429
|
+
cur += "\\".repeat(Math.floor(bs / 2));
|
|
430
|
+
if (bs % 2) {
|
|
431
|
+
cur += '"';
|
|
432
|
+
i = j + 1;
|
|
433
|
+
}
|
|
434
|
+
else
|
|
435
|
+
i = j;
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
cur += "\\".repeat(bs);
|
|
439
|
+
i = j;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else if (c === '"') {
|
|
443
|
+
if (inQ && line[i + 1] === '"') {
|
|
444
|
+
cur += '"';
|
|
445
|
+
i += 2;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
inQ = !inQ;
|
|
449
|
+
i++;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else if (!inQ && (c === " " || c === "\t"))
|
|
453
|
+
break;
|
|
454
|
+
else {
|
|
455
|
+
cur += c;
|
|
456
|
+
i++;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
out.push(cur);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
export function provenOrphan(r, info, toleranceMs = 10_000) {
|
|
463
|
+
if (!info?.cmd || !info.startedMs || !r.argv?.length || !r.since)
|
|
464
|
+
return false;
|
|
465
|
+
if (Math.abs(info.startedMs - r.since) > toleranceMs)
|
|
466
|
+
return false;
|
|
467
|
+
const got = splitWindowsCommandLine(info.cmd);
|
|
468
|
+
const want = r.argv;
|
|
469
|
+
return got.length === want.length && got.every((a, i) => (i === 0 ? a.toLowerCase() === want[0].toLowerCase() : a === want[i]));
|
|
470
|
+
}
|
|
471
|
+
export function windowsPowerShell(env = process.env) {
|
|
472
|
+
const root = env.SystemRoot ?? env.windir ?? env.WINDIR ?? "C:\\Windows";
|
|
473
|
+
return `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
474
|
+
}
|
|
475
|
+
export function childArgv(kind, args, node, flags, cli) {
|
|
476
|
+
return [node, ...flags, cli, kind, ...args];
|
|
477
|
+
}
|
|
478
|
+
const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
479
|
+
function openLog(f) {
|
|
480
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
481
|
+
try {
|
|
482
|
+
if (statSync(f).size > LOG_ROTATE_BYTES)
|
|
483
|
+
renameSync(f, `${f}.1`);
|
|
484
|
+
}
|
|
485
|
+
catch { }
|
|
486
|
+
return openSync(f, "a");
|
|
487
|
+
}
|
|
488
|
+
const RUN_RECORD = "worker-run.json";
|
|
489
|
+
export function realHostIo(o = {}) {
|
|
490
|
+
const home = o.home ?? CONFIG_DIR;
|
|
491
|
+
const f = hostFiles(home);
|
|
492
|
+
const node = o.node ?? process.execPath;
|
|
493
|
+
const flags = o.flags ?? [];
|
|
494
|
+
const cli = o.cli ?? resolve(HERE, "cli.js");
|
|
495
|
+
const pantheon = o.pantheonCommand ?? `"${node}" ${flags.join(" ")} "${cli}"`.replace(/\s+/g, " ");
|
|
496
|
+
const session = o.session ?? hostSession();
|
|
497
|
+
const parent = process.ppid;
|
|
498
|
+
let versionAt = 0, versionChanged = false;
|
|
499
|
+
const hardKill = (pid) => {
|
|
500
|
+
if (process.platform === "win32") {
|
|
501
|
+
const r = spawn("taskkill", ["/T", "/F", "/PID", String(pid)], { stdio: "ignore", windowsHide: true });
|
|
502
|
+
r.on("error", () => { try {
|
|
503
|
+
process.kill(pid);
|
|
504
|
+
}
|
|
505
|
+
catch { } });
|
|
506
|
+
}
|
|
507
|
+
else
|
|
508
|
+
process.kill(pid, "SIGTERM");
|
|
509
|
+
};
|
|
510
|
+
return {
|
|
511
|
+
session,
|
|
512
|
+
now: () => Date.now(),
|
|
513
|
+
want: () => {
|
|
514
|
+
const want = {};
|
|
515
|
+
let installed = 0;
|
|
516
|
+
for (const k of HOST_KINDS) {
|
|
517
|
+
const d = readDefinition(k, home);
|
|
518
|
+
if (d)
|
|
519
|
+
installed++;
|
|
520
|
+
want[k] = d && !existsSync(f.disabled(k)) ? JSON.stringify([d.cwd, d.args]) : null;
|
|
521
|
+
}
|
|
522
|
+
const tray = trayScriptPath();
|
|
523
|
+
want.tray = existsSync(tray) && !existsSync(f.disabled("tray")) ? tray : null;
|
|
524
|
+
return { want, installed };
|
|
525
|
+
},
|
|
526
|
+
takeRestarts: () => HOST_CHILDREN.filter((c) => {
|
|
527
|
+
if (!existsSync(f.restart(c)))
|
|
528
|
+
return false;
|
|
529
|
+
try {
|
|
530
|
+
rmSync(f.restart(c), { force: true });
|
|
531
|
+
}
|
|
532
|
+
catch { }
|
|
533
|
+
return true;
|
|
534
|
+
}),
|
|
535
|
+
stopRequested: () => existsSync(f.stop),
|
|
536
|
+
newVersion: () => {
|
|
537
|
+
if (Date.now() - versionAt < 30_000)
|
|
538
|
+
return versionChanged;
|
|
539
|
+
versionAt = Date.now();
|
|
540
|
+
try {
|
|
541
|
+
const v = JSON.parse(readFileSync(resolve(dirname(cli), "..", "package.json"), "utf8")).version;
|
|
542
|
+
versionChanged = !!v && VERSION !== "unknown" && v !== VERSION;
|
|
543
|
+
}
|
|
544
|
+
catch { }
|
|
545
|
+
return versionChanged;
|
|
546
|
+
},
|
|
547
|
+
workerBusy: () => {
|
|
548
|
+
try {
|
|
549
|
+
const r = JSON.parse(readFileSync(join(home, RUN_RECORD), "utf8"));
|
|
550
|
+
return typeof r.at === "number" && Date.now() - r.at < 3 * 3600_000 && !!r.pid && pidAlive(r.pid);
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
parentGone: () => parent > 1 && !pidAlive(parent),
|
|
557
|
+
start: (child, onExit) => {
|
|
558
|
+
const m = o.managed;
|
|
559
|
+
if (m && !existsSync(m.pkgDir) && existsSync(join(m.prevDir, "bin", "pantheon.js"))) {
|
|
560
|
+
try {
|
|
561
|
+
renameSync(m.prevDir, m.pkgDir);
|
|
562
|
+
}
|
|
563
|
+
catch { }
|
|
564
|
+
}
|
|
565
|
+
let argv;
|
|
566
|
+
let cwd = homedir();
|
|
567
|
+
if (child === "tray") {
|
|
568
|
+
const asked = Number(readFileSafe(f.trayStop));
|
|
569
|
+
if (!(Number.isInteger(asked) && asked > 0 && pidAlive(asked))) {
|
|
570
|
+
try {
|
|
571
|
+
rmSync(f.trayStop, { force: true });
|
|
572
|
+
}
|
|
573
|
+
catch { }
|
|
574
|
+
}
|
|
575
|
+
const icon = trayIconPath(), notices = noticeScriptPath();
|
|
576
|
+
argv = trayArgv(trayScriptPath(), pantheon, home, { icon: existsSync(icon) ? icon : null, notices: existsSync(notices) ? notices : null });
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
const d = readDefinition(child, home);
|
|
580
|
+
if (!d)
|
|
581
|
+
throw new Error(`no ${child} definition`);
|
|
582
|
+
argv = childArgv(child, d.args, node, flags, cli);
|
|
583
|
+
if (existsSync(d.cwd))
|
|
584
|
+
cwd = d.cwd;
|
|
585
|
+
}
|
|
586
|
+
const fd = openLog(f.childLog(child));
|
|
587
|
+
try {
|
|
588
|
+
writeSync(fd, `\n--- ${new Date().toISOString()} the Pantheon host starts the ${child}\n`);
|
|
589
|
+
const proc = spawn(argv[0], argv.slice(1), { cwd, stdio: ["ignore", fd, fd], windowsHide: true, env: { ...process.env, PANTHEON_HOST_SESSION: session } });
|
|
590
|
+
let done = false;
|
|
591
|
+
const finish = (code, signal) => { if (!done) {
|
|
592
|
+
done = true;
|
|
593
|
+
onExit(code, signal);
|
|
594
|
+
} };
|
|
595
|
+
if (!proc.pid) {
|
|
596
|
+
proc.on("error", () => { });
|
|
597
|
+
throw new Error(`could not start ${argv[0]}`);
|
|
598
|
+
}
|
|
599
|
+
proc.on("error", () => finish(null, "error"));
|
|
600
|
+
proc.on("exit", (code, signal) => finish(code, signal));
|
|
601
|
+
return { pid: proc.pid, argv };
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
closeSync(fd);
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
kill: (child, pid) => {
|
|
608
|
+
if (child !== "tray") {
|
|
609
|
+
hardKill(pid);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
mkdirSync(f.dir, { recursive: true });
|
|
614
|
+
writeFileSync(f.trayStop, String(pid));
|
|
615
|
+
}
|
|
616
|
+
catch { }
|
|
617
|
+
const t = setTimeout(() => { if (pidAlive(pid)) {
|
|
618
|
+
try {
|
|
619
|
+
hardKill(pid);
|
|
620
|
+
}
|
|
621
|
+
catch { }
|
|
622
|
+
} }, o.trayGraceMs ?? 5_000);
|
|
623
|
+
t.unref?.();
|
|
624
|
+
},
|
|
625
|
+
writeState: (s) => writeAtomic(f.state, JSON.stringify(s) + "\n"),
|
|
626
|
+
log: (line) => {
|
|
627
|
+
try {
|
|
628
|
+
mkdirSync(dirname(f.log), { recursive: true });
|
|
629
|
+
writeFileSync(f.log, `${new Date().toISOString()} ${line}\n`, { flag: "a" });
|
|
630
|
+
}
|
|
631
|
+
catch { }
|
|
632
|
+
},
|
|
633
|
+
exit: (code) => process.exit(code),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
export const HOST_LOCK_STARTUP_GRACE_MS = 2 * 60_000;
|
|
637
|
+
export function takeHostLock(home = CONFIG_DIR, pid = process.pid, now = Date.now, alive = pidAlive) {
|
|
638
|
+
const f = hostFiles(home);
|
|
639
|
+
mkdirSync(f.dir, { recursive: true });
|
|
640
|
+
let lastHolder = -1;
|
|
641
|
+
for (let i = 0; i < 4; i++) {
|
|
642
|
+
try {
|
|
643
|
+
const fd = openSync(f.lock, "wx");
|
|
644
|
+
writeSync(fd, String(pid));
|
|
645
|
+
closeSync(fd);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
if (e.code !== "EEXIST")
|
|
650
|
+
throw e;
|
|
651
|
+
}
|
|
652
|
+
const held = Number(readFileSafe(f.lock));
|
|
653
|
+
let age = 0;
|
|
654
|
+
try {
|
|
655
|
+
age = now() - statSync(f.lock).mtimeMs;
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (Number.isInteger(held) && held > 0) {
|
|
661
|
+
lastHolder = held;
|
|
662
|
+
if (held === pid)
|
|
663
|
+
return null;
|
|
664
|
+
if (alive(held) && (hostAlive(readHostState(home)) || age < HOST_LOCK_STARTUP_GRACE_MS))
|
|
665
|
+
return held;
|
|
666
|
+
}
|
|
667
|
+
else if (age < 10_000) {
|
|
668
|
+
return lastHolder;
|
|
669
|
+
}
|
|
670
|
+
const gone = `${f.lock}.stale-${pid}-${i}`;
|
|
671
|
+
try {
|
|
672
|
+
renameSync(f.lock, gone);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
rmSync(gone, { force: true });
|
|
678
|
+
}
|
|
679
|
+
return lastHolder;
|
|
680
|
+
}
|
|
681
|
+
const readFileSafe = (p) => { try {
|
|
682
|
+
return readFileSync(p, "utf8").trim();
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
return "";
|
|
686
|
+
} };
|
|
687
|
+
export function orphans(prev, isOurs, alive = pidAlive, self = process.pid) {
|
|
688
|
+
if (!prev || prev.pid === self || alive(prev.pid))
|
|
689
|
+
return [];
|
|
690
|
+
const out = [];
|
|
691
|
+
for (const c of HOST_CHILDREN) {
|
|
692
|
+
const r = prev.children[c];
|
|
693
|
+
if (r?.pid && (r.phase === "running" || r.phase === "stopping") && alive(r.pid) && isOurs(r.pid, r))
|
|
694
|
+
out.push([c, r.pid]);
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
export function runHost(o = {}) {
|
|
699
|
+
const home = o.home ?? CONFIG_DIR;
|
|
700
|
+
const f = hostFiles(home);
|
|
701
|
+
const io = realHostIo(o);
|
|
702
|
+
process.on("uncaughtException", (e) => { io.log(`host crashed: ${e.stack ?? e.message}`); process.exit(1); });
|
|
703
|
+
const other = takeHostLock(home);
|
|
704
|
+
if (other) {
|
|
705
|
+
io.log(`another host is running (pid ${other}) — this one leaves`);
|
|
706
|
+
process.exit(0);
|
|
707
|
+
}
|
|
708
|
+
rmSync(f.stop, { force: true });
|
|
709
|
+
if (clearStaleTrayQuit(home, io.session))
|
|
710
|
+
io.log("tray: it was quit in an earlier session — showing it again");
|
|
711
|
+
io.log(`host ${VERSION} starts (pid ${process.pid}, session ${io.session})`);
|
|
712
|
+
for (const [c, pid] of orphans(readHostState(home), o.isOurs ?? (() => false))) {
|
|
713
|
+
io.log(`${c}: pid ${pid} was left running by a host that is gone — stopping it before starting a new one`);
|
|
714
|
+
try {
|
|
715
|
+
io.kill(c, pid);
|
|
716
|
+
}
|
|
717
|
+
catch { }
|
|
718
|
+
}
|
|
719
|
+
const loop = new HostLoop({ ...io, exit: (code) => { rmSync(f.state, { force: true }); rmSync(f.lock, { force: true }); io.log(`host exits (${code === HOST_RESTART_CODE ? "to start again on the new version" : "stopped"})`); process.exit(code); } });
|
|
720
|
+
loop.tick();
|
|
721
|
+
setInterval(() => loop.tick(), o.tickMs ?? 2_000);
|
|
722
|
+
}
|
|
723
|
+
export function winQuote(a) {
|
|
724
|
+
if (a.length > 0 && !/[\s"]/.test(a))
|
|
725
|
+
return a;
|
|
726
|
+
let out = '"', bs = 0;
|
|
727
|
+
for (const ch of a) {
|
|
728
|
+
if (ch === "\\") {
|
|
729
|
+
bs++;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (ch === '"') {
|
|
733
|
+
out += "\\".repeat(bs * 2 + 1) + '"';
|
|
734
|
+
}
|
|
735
|
+
else
|
|
736
|
+
out += "\\".repeat(bs) + ch;
|
|
737
|
+
bs = 0;
|
|
738
|
+
}
|
|
739
|
+
return out + "\\".repeat(bs * 2) + '"';
|
|
740
|
+
}
|
|
741
|
+
export const hostArgv = (o) => [o.node, ...o.flags, o.cli, "host"];
|
|
742
|
+
export const launchFileText = (argv) => {
|
|
743
|
+
if (argv.some((a) => /[\r\n]/.test(a) || a === ""))
|
|
744
|
+
throw new Error(`a path with a line break in it cannot be started by the task: ${JSON.stringify(argv)}`);
|
|
745
|
+
return argv.join("\r\n") + "\r\n";
|
|
746
|
+
};
|
|
747
|
+
export function powershellHostLoop(o) {
|
|
748
|
+
const baked = `@(${o.argv.map(psQuote).join(", ")})`;
|
|
749
|
+
return [
|
|
750
|
+
`$env:PANTHEON_HOST_SESSION = [guid]::NewGuid().ToString('N')`,
|
|
751
|
+
...(o.home ? [`$env:PANTHEON_HOME = ${psQuote(o.home)}`] : []),
|
|
752
|
+
`$d = 2; $gone = $null`,
|
|
753
|
+
`while ($true) {` +
|
|
754
|
+
` $a = @(); try { $a = @(Get-Content -LiteralPath ${psQuote(o.launch)} -Encoding UTF8 -ErrorAction Stop | Where-Object { $_ }) } catch { $a = @() };` +
|
|
755
|
+
` if ($a.Count -lt 1 -or -not (Test-Path -LiteralPath $a[0] -PathType Leaf)) { $a = ${baked} };` +
|
|
756
|
+
` if (-not (Test-Path -LiteralPath $a[0] -PathType Leaf)) { if ($null -eq $gone) { $gone = Get-Date }; if (((Get-Date) - $gone).TotalSeconds -ge ${o.graceSeconds ?? 600}) { exit 3 }; Start-Sleep -Seconds $d; $d = [Math]::Min($d * 2, 60); continue };` +
|
|
757
|
+
` $gone = $null; $t = Get-Date;` +
|
|
758
|
+
` & $a[0] @($a | Select-Object -Skip 1); $c = $LASTEXITCODE;` +
|
|
759
|
+
` if ($c -eq 0) { exit 0 }; if ($c -eq ${HOST_RESTART_CODE}) { $d = 2; continue };` +
|
|
760
|
+
` if (((Get-Date) - $t).TotalMinutes -ge 10) { $d = 2 }; Start-Sleep -Seconds $d; $d = [Math]::Min($d * 2, 60)` +
|
|
761
|
+
` }`,
|
|
762
|
+
].join("; ");
|
|
763
|
+
}
|
|
764
|
+
export function hostLaunch(o) {
|
|
765
|
+
const argv = hostArgv(o);
|
|
766
|
+
const launch = o.launch ?? win32.join(o.home ?? CONFIG_DIR, "host", "launch.txt");
|
|
767
|
+
if (o.shim) {
|
|
768
|
+
const args = [...(o.home ? ["--env", `PANTHEON_HOME=${o.home}`] : []), "--launch", launch, "--", ...argv];
|
|
769
|
+
return { command: o.shim, arguments: args.map(winQuote).join(" "), how: "shim" };
|
|
770
|
+
}
|
|
771
|
+
const run = powershellHostLoop({ argv, launch, home: o.home });
|
|
772
|
+
return { command: o.powershell ?? windowsPowerShell(), arguments: `-NoProfile -NonInteractive -WindowStyle Hidden -Command "${run}"`, how: "powershell" };
|
|
773
|
+
}
|
|
774
|
+
const xmlText = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
775
|
+
export function taskXml(o) {
|
|
776
|
+
const user = xmlText(o.userId);
|
|
777
|
+
return [
|
|
778
|
+
`<?xml version="1.0" encoding="UTF-16"?>`,
|
|
779
|
+
`<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
|
|
780
|
+
` <RegistrationInfo>`,
|
|
781
|
+
` <Author>${user}</Author>`,
|
|
782
|
+
` <Description>${xmlText(o.description ?? "Pantheon: keeps the worker, desktop alerts and the tray icon running while you are logged on. Remove with: pantheon service uninstall worker (and notifier), or pantheon uninstall.")}</Description>`,
|
|
783
|
+
` <URI>\\${xmlText(o.name)}</URI>`,
|
|
784
|
+
` </RegistrationInfo>`,
|
|
785
|
+
` <Triggers>`,
|
|
786
|
+
` <LogonTrigger>`,
|
|
787
|
+
` <Enabled>true</Enabled>`,
|
|
788
|
+
` <UserId>${user}</UserId>`,
|
|
789
|
+
` </LogonTrigger>`,
|
|
790
|
+
` </Triggers>`,
|
|
791
|
+
` <Principals>`,
|
|
792
|
+
` <Principal id="Author">`,
|
|
793
|
+
` <UserId>${user}</UserId>`,
|
|
794
|
+
` <LogonType>InteractiveToken</LogonType>`,
|
|
795
|
+
` <RunLevel>LeastPrivilege</RunLevel>`,
|
|
796
|
+
` </Principal>`,
|
|
797
|
+
` </Principals>`,
|
|
798
|
+
` <Settings>`,
|
|
799
|
+
` <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>`,
|
|
800
|
+
` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
|
|
801
|
+
` <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>`,
|
|
802
|
+
` <AllowHardTerminate>true</AllowHardTerminate>`,
|
|
803
|
+
` <StartWhenAvailable>false</StartWhenAvailable>`,
|
|
804
|
+
` <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>`,
|
|
805
|
+
` <IdleSettings>`,
|
|
806
|
+
` <StopOnIdleEnd>false</StopOnIdleEnd>`,
|
|
807
|
+
` <RestartOnIdle>false</RestartOnIdle>`,
|
|
808
|
+
` </IdleSettings>`,
|
|
809
|
+
` <AllowStartOnDemand>true</AllowStartOnDemand>`,
|
|
810
|
+
` <Enabled>true</Enabled>`,
|
|
811
|
+
` <Hidden>false</Hidden>`,
|
|
812
|
+
` <RunOnlyIfIdle>false</RunOnlyIfIdle>`,
|
|
813
|
+
` <WakeToRun>false</WakeToRun>`,
|
|
814
|
+
` <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>`,
|
|
815
|
+
` <Priority>5</Priority>`,
|
|
816
|
+
` <RestartOnFailure>`,
|
|
817
|
+
` <Interval>PT1M</Interval>`,
|
|
818
|
+
` <Count>999</Count>`,
|
|
819
|
+
` </RestartOnFailure>`,
|
|
820
|
+
` </Settings>`,
|
|
821
|
+
` <Actions Context="Author">`,
|
|
822
|
+
` <Exec>`,
|
|
823
|
+
` <Command>${xmlText(o.launch.command)}</Command>`,
|
|
824
|
+
` <Arguments>${xmlText(o.launch.arguments)}</Arguments>`,
|
|
825
|
+
` <WorkingDirectory>${xmlText(o.workingDirectory)}</WorkingDirectory>`,
|
|
826
|
+
` </Exec>`,
|
|
827
|
+
` </Actions>`,
|
|
828
|
+
`</Task>`,
|
|
829
|
+
``,
|
|
830
|
+
].join("\r\n");
|
|
831
|
+
}
|
|
832
|
+
export const utf16le = (text) => Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(text, "utf16le")]);
|
|
833
|
+
export const fromUtf16le = (b) => (b[0] === 0xff && b[1] === 0xfe ? b.subarray(2) : b).toString("utf16le");
|
|
834
|
+
export const taskUserId = (user, env = process.env) => env.USERDOMAIN && user && !user.includes("\\") ? `${env.USERDOMAIN}\\${user}` : user;
|
|
835
|
+
export const shimSourcePath = () => resolve(HERE, "..", "helper", "windows", "pantheon-host.cs");
|
|
836
|
+
export function cscCandidates(env = process.env) {
|
|
837
|
+
const root = env.SystemRoot ?? env.windir ?? env.WINDIR;
|
|
838
|
+
if (!root)
|
|
839
|
+
return [];
|
|
840
|
+
return ["Framework64", "FrameworkArm64", "Framework"].map((fw) => `${root}\\Microsoft.NET\\${fw}\\v4.0.30319\\csc.exe`);
|
|
841
|
+
}
|
|
842
|
+
export function ensureHostShim(o) {
|
|
843
|
+
const exists = o.exists ?? existsSync;
|
|
844
|
+
const source = o.source ?? shimSourcePath();
|
|
845
|
+
let hash;
|
|
846
|
+
try {
|
|
847
|
+
hash = createHash("sha256").update(readFileSync(source)).digest("hex").slice(0, 12);
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
const out = join(o.home ?? CONFIG_DIR, "bin", `pantheon-host-${hash}.exe`);
|
|
853
|
+
if (exists(out))
|
|
854
|
+
return out;
|
|
855
|
+
const csc = cscCandidates(o.env).find((c) => exists(c));
|
|
856
|
+
if (!csc) {
|
|
857
|
+
o.log?.(" no csc.exe here (.NET Framework 4) — the task starts the host through PowerShell");
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
862
|
+
o.exec(csc, ["/nologo", "/target:winexe", "/optimize+", `/out:${out}`, source]);
|
|
863
|
+
}
|
|
864
|
+
catch (e) {
|
|
865
|
+
o.log?.(` could not compile the host launcher (${e.message.split("\n")[0]}) — the task starts the host through PowerShell`);
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
if (!exists(out))
|
|
869
|
+
return null;
|
|
870
|
+
try {
|
|
871
|
+
for (const n of readdirSync(dirname(out))) {
|
|
872
|
+
if (/^pantheon-host-[0-9a-f]{12}\.exe$/.test(n) && join(dirname(out), n) !== out) {
|
|
873
|
+
try {
|
|
874
|
+
rmSync(join(dirname(out), n), { force: true });
|
|
875
|
+
}
|
|
876
|
+
catch { }
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
catch { }
|
|
881
|
+
return out;
|
|
882
|
+
}
|
|
883
|
+
function taskQuery(exec, name) {
|
|
884
|
+
try {
|
|
885
|
+
exec("schtasks", ["/Query", "/TN", name]);
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
catch (e) {
|
|
889
|
+
return e.code === "ENOENT" ? null : false;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
export function windowsFacts(exec, home = CONFIG_DIR, now = Date.now(), trayShipped = existsSync(trayScriptPath())) {
|
|
893
|
+
const f = hostFiles(home);
|
|
894
|
+
const name = taskName(exec);
|
|
895
|
+
const taskRegistered = taskQuery(exec, name);
|
|
896
|
+
const legacyTask = legacyTaskIsUsers(exec);
|
|
897
|
+
let launch = null;
|
|
898
|
+
try {
|
|
899
|
+
const cmd = /<Command>([^<]*)<\/Command>/.exec(fromUtf16le(readFileSync(f.taskXml)))?.[1] ?? "";
|
|
900
|
+
launch = /pantheon-host-[0-9a-f]+\.exe$/i.test(cmd) ? "shim" : cmd ? "powershell" : null;
|
|
901
|
+
}
|
|
902
|
+
catch { }
|
|
903
|
+
const state = readHostState(home);
|
|
904
|
+
const hostUp = hostAlive(state, now);
|
|
905
|
+
const by = (fn) => Object.fromEntries(HOST_KINDS.map((k) => [k, fn(k)]));
|
|
906
|
+
const trayReport = hostUp ? state?.children.tray : undefined;
|
|
907
|
+
return {
|
|
908
|
+
taskName: name, taskRegistered, legacyTask, launch, state, hostUp, now,
|
|
909
|
+
installed: by((k) => existsSync(f.definition(k))),
|
|
910
|
+
disabled: by((k) => existsSync(f.disabled(k))),
|
|
911
|
+
running: by((k) => !!hostChildRunning(k, home, now)),
|
|
912
|
+
tray: { shipped: trayShipped, quit: existsSync(f.disabled("tray")), running: !!hostChildRunning("tray", home, now) },
|
|
913
|
+
clm: trayReport?.gaveUp === "clm" || existsSync(f.clm),
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
export function windowsDoctorRows(x) {
|
|
917
|
+
const rows = [];
|
|
918
|
+
const any = x.installed.worker || x.installed.notifier;
|
|
919
|
+
if (!any) {
|
|
920
|
+
rows.push({ level: "warn", name: "background", detail: "nothing runs in the background here — no worker, no desktop alerts, no tray",
|
|
921
|
+
fix: `pantheon service install worker and pantheon service install notifier (one Task Scheduler task, ${x.taskName}, starts them at logon; no admin rights needed)`, share: "no Windows background services" });
|
|
922
|
+
return rows;
|
|
923
|
+
}
|
|
924
|
+
if (x.taskRegistered === false) {
|
|
925
|
+
rows.push({ level: "warn", name: "task", detail: `the ${x.taskName} task is not registered in Task Scheduler — nothing starts the worker or alerts at logon${x.legacyTask ? ` (a "${LEGACY_TASK_NAME}" task from an older Pantheon is; installing replaces it)` : ""}`,
|
|
926
|
+
fix: `pantheon service install ${x.installed.worker ? "worker" : "notifier"} (registers it again)`, share: x.legacyTask ? "task missing, legacy task present" : "task missing" });
|
|
927
|
+
}
|
|
928
|
+
else if (x.taskRegistered) {
|
|
929
|
+
rows.push({ level: x.launch === "powershell" ? "warn" : "pass", name: "task",
|
|
930
|
+
detail: x.launch === "powershell"
|
|
931
|
+
? `${x.taskName}, at logon — through PowerShell, because no csc.exe (.NET Framework 4) was found to build pantheon-host.exe: a console may show for a moment at logon`
|
|
932
|
+
: `${x.taskName}, at logon, hidden (pantheon-host.exe), as you, without admin rights`,
|
|
933
|
+
...(x.launch === "powershell" ? { fix: "harmless; with the .NET Framework 4.8 feature on, pantheon service install worker builds the hidden launcher" } : {}),
|
|
934
|
+
share: x.launch === "powershell" ? "registered, PowerShell launch" : "registered" });
|
|
935
|
+
}
|
|
936
|
+
if (x.hostUp && x.state) {
|
|
937
|
+
const restarts = HOST_CHILDREN.map((c) => [c, x.state.children[c]?.restarts ?? 0]).filter(([, n]) => n > 0);
|
|
938
|
+
rows.push({ level: "pass", name: "host", detail: `running (pid ${x.state.pid}, ${x.state.version})${restarts.length ? ` — restarted ${restarts.map(([c, n]) => `the ${c} ${n}×`).join(", ")}` : ""}`,
|
|
939
|
+
share: "running" });
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
rows.push({ level: "warn", name: "host", detail: "not running — the worker, alerts and tray are all down until it starts",
|
|
943
|
+
fix: `pantheon service start ${x.installed.worker ? "worker" : "notifier"} (or log out and back in; its log: ~/.pantheon/logs/host.log)`, share: "not running" });
|
|
944
|
+
}
|
|
945
|
+
const one = (k, name, what, off) => {
|
|
946
|
+
if (!x.installed[k]) {
|
|
947
|
+
rows.push({ level: "warn", name, detail: `not installed — ${off}`, fix: `pantheon service install ${k}`, share: "not installed" });
|
|
948
|
+
}
|
|
949
|
+
else if (x.disabled[k]) {
|
|
950
|
+
rows.push({ level: "warn", name, detail: `paused (pantheon stop, or Pause in the tray) — ${off}`, fix: `pantheon service start ${k}`, share: "paused" });
|
|
951
|
+
}
|
|
952
|
+
else if (x.running[k]) {
|
|
953
|
+
rows.push({ level: "pass", name, detail: `running under the host — ${what}`, share: "running" });
|
|
954
|
+
}
|
|
955
|
+
else if (x.hostUp) {
|
|
956
|
+
const r = x.state?.children[k];
|
|
957
|
+
const next = r?.nextAt ? `, starting again in ${Math.max(0, Math.round((r.nextAt - x.now) / 1000))}s` : "";
|
|
958
|
+
rows.push({ level: "warn", name, detail: `not running${r?.lastExit ? ` — last exit ${r.lastExit}` : ""}${next}`, fix: `its log: pantheon logs ${k}`, share: "not running" });
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
rows.push({ level: "warn", name, detail: `not running — the host is down`, fix: `pantheon service start ${k}`, share: "host down" });
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
one("worker", "worker svc", "a teammate's request can start an agent run here while you are away", "teammates' requests wait for you");
|
|
965
|
+
one("notifier", "alerts", x.clm ? "but PowerShell runs in Constrained Language Mode here, so Windows shows none of its alerts" : "a desktop alert when a teammate needs you", "no desktop alert when a teammate needs you");
|
|
966
|
+
if (x.clm && x.installed.notifier) {
|
|
967
|
+
const last = rows[rows.length - 1];
|
|
968
|
+
if (last.level === "pass")
|
|
969
|
+
Object.assign(last, { level: "warn", fix: "your IT's policy (AppLocker or WDAC) sets it; pantheon board (in a browser) and pantheon brief show the same", share: "running, Constrained Language Mode" });
|
|
970
|
+
}
|
|
971
|
+
if (x.tray.shipped) {
|
|
972
|
+
if (x.clm) {
|
|
973
|
+
rows.push({ level: "warn", name: "tray", detail: "not shown — PowerShell runs in Constrained Language Mode here (an IT policy), which refuses what the tray icon needs; it is not retried until you log in again",
|
|
974
|
+
fix: "pantheon board (the same view in a browser)", share: "Constrained Language Mode" });
|
|
975
|
+
}
|
|
976
|
+
else if (x.tray.quit) {
|
|
977
|
+
rows.push({ level: "warn", name: "tray", detail: "you quit it from its menu — it comes back when you next log in",
|
|
978
|
+
fix: "pantheon tray (shows it again now)", share: "quit" });
|
|
979
|
+
}
|
|
980
|
+
else if (x.tray.running) {
|
|
981
|
+
rows.push({ level: "pass", name: "tray", detail: "in the notification area — what needs you, Open board, Pause", share: "running" });
|
|
982
|
+
}
|
|
983
|
+
else if (x.hostUp) {
|
|
984
|
+
const r = x.state?.children.tray;
|
|
985
|
+
rows.push({ level: "warn", name: "tray", detail: `not running${r?.lastExit ? ` — last exit ${r.lastExit}` : ""}`, fix: "its log: ~/.pantheon/logs/tray.log; pantheon tray starts it again", share: "not running" });
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
rows.push({ level: "warn", name: "tray", detail: "not running — the host is down", fix: `pantheon service start ${x.installed.worker ? "worker" : "notifier"}`, share: "host down" });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return rows;
|
|
992
|
+
}
|
|
993
|
+
//# sourceMappingURL=win-host.js.map
|