@addai/node 0.5.1 → 0.6.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/dist/autostart-mac.d.ts +62 -0
- package/dist/autostart-mac.js +306 -0
- package/dist/autostart-win.d.ts +46 -0
- package/dist/autostart-win.js +290 -0
- package/dist/autostart.d.ts +81 -0
- package/dist/autostart.js +224 -0
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +87 -1
- package/dist/command-runner.js +37 -0
- package/dist/index.js +23 -10
- package/dist/tui/dashboard.d.ts +14 -0
- package/dist/tui/dashboard.js +67 -1
- package/dist/tui/data.d.ts +9 -0
- package/dist/tui/request-row.d.ts +1 -1
- package/dist/tui/request-row.js +22 -8
- package/dist/tui/run.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AutostartBackend, AutostartEntry } from './autostart';
|
|
2
|
+
export declare const PLIST_PATH: string;
|
|
3
|
+
export interface PlistSpec {
|
|
4
|
+
label: string;
|
|
5
|
+
entry: AutostartEntry;
|
|
6
|
+
env: Record<string, string>;
|
|
7
|
+
logPath: string;
|
|
8
|
+
workingDirectory: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The plist, as a string. Pure — this is what the tests assert on.
|
|
12
|
+
*
|
|
13
|
+
* KeepAlive is `true`, not `{SuccessfulExit: false}`: the node is meant to be
|
|
14
|
+
* always-on, and a remote roll relies on it. A supervised `update_runtime`
|
|
15
|
+
* installs the new version and simply EXITS; launchd starting the replacement
|
|
16
|
+
* is what makes that safe (see index.ts). ThrottleInterval keeps a crash loop
|
|
17
|
+
* to one launch per 10s instead of a spin.
|
|
18
|
+
*
|
|
19
|
+
* ProcessType is Interactive because Background jobs get throttled CPU
|
|
20
|
+
* scheduling — a daemon that supervises agent runs is not a batch job.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildPlist(spec: PlistSpec): string;
|
|
23
|
+
/** Pull ProgramArguments back out of a plist we wrote, for `status`. */
|
|
24
|
+
export declare function parsePlistEntry(xml: string): AutostartEntry | undefined;
|
|
25
|
+
/** Directories macOS gates behind user consent no background agent can give. */
|
|
26
|
+
export declare function protectedPathReason(scriptPath: string, home?: string): string | null;
|
|
27
|
+
export declare function protectedPathError(label: string, scriptPath: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Hand the node over to launchd.
|
|
30
|
+
*
|
|
31
|
+
* A daemon started by hand holds the lockfile, and `acquireLockfile()` throws
|
|
32
|
+
* for anyone else. Bootstrapping the agent underneath it would therefore mean
|
|
33
|
+
* launchd relaunching a job that exits 1 every ten seconds until the human
|
|
34
|
+
* closes their terminal. So we ask the incumbent to stand down first — SIGTERM
|
|
35
|
+
* runs its normal drain, so live entity turns finish rather than being cut.
|
|
36
|
+
*
|
|
37
|
+
* Except when the incumbent is US. `ainode startup enable` can be pressed from
|
|
38
|
+
* inside the console of the very daemon it is arming, and signalling ourselves
|
|
39
|
+
* there would kill the node the user is looking at. That case writes the plist
|
|
40
|
+
* and stops: ~/Library/LaunchAgents is read at every login, so the change
|
|
41
|
+
* lands at the next one without anybody being killed for it.
|
|
42
|
+
*/
|
|
43
|
+
export type Handover =
|
|
44
|
+
/** Nothing was running — safe to start the agent immediately. */
|
|
45
|
+
{
|
|
46
|
+
kind: 'none';
|
|
47
|
+
}
|
|
48
|
+
/** The running daemon is this very process. Write only, don't bootstrap. */
|
|
49
|
+
| {
|
|
50
|
+
kind: 'self';
|
|
51
|
+
}
|
|
52
|
+
/** Someone else's daemon stood down; the agent can take over now. */
|
|
53
|
+
| {
|
|
54
|
+
kind: 'stopped';
|
|
55
|
+
}
|
|
56
|
+
/** It was asked to stop and didn't. */
|
|
57
|
+
| {
|
|
58
|
+
kind: 'stuck';
|
|
59
|
+
pid: number;
|
|
60
|
+
};
|
|
61
|
+
export declare function stopRunningDaemon(timeoutMs?: number, selfPid?: number): Handover;
|
|
62
|
+
export declare const backend: AutostartBackend;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// macOS backend: a per-user LaunchAgent.
|
|
3
|
+
//
|
|
4
|
+
// ~/Library/LaunchAgents is loaded by launchd at every login, so the plist
|
|
5
|
+
// existing IS the answer to "does this node start at login?" — no daemon, no
|
|
6
|
+
// admin rights, no stored password.
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.backend = exports.PLIST_PATH = void 0;
|
|
42
|
+
exports.buildPlist = buildPlist;
|
|
43
|
+
exports.parsePlistEntry = parsePlistEntry;
|
|
44
|
+
exports.protectedPathReason = protectedPathReason;
|
|
45
|
+
exports.protectedPathError = protectedPathError;
|
|
46
|
+
exports.stopRunningDaemon = stopRunningDaemon;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const child_process_1 = require("child_process");
|
|
51
|
+
const autostart_1 = require("./autostart");
|
|
52
|
+
const lockfile_1 = require("./lockfile");
|
|
53
|
+
exports.PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${autostart_1.LABEL}.plist`);
|
|
54
|
+
/** `<string>` bodies are the only place user data lands in the plist, and a
|
|
55
|
+
* PATH entry with an `&` in it would otherwise produce invalid XML that
|
|
56
|
+
* launchd rejects at login — silently, from the user's point of view. */
|
|
57
|
+
function esc(s) {
|
|
58
|
+
return s
|
|
59
|
+
.replace(/&/g, '&')
|
|
60
|
+
.replace(/</g, '<')
|
|
61
|
+
.replace(/>/g, '>');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The plist, as a string. Pure — this is what the tests assert on.
|
|
65
|
+
*
|
|
66
|
+
* KeepAlive is `true`, not `{SuccessfulExit: false}`: the node is meant to be
|
|
67
|
+
* always-on, and a remote roll relies on it. A supervised `update_runtime`
|
|
68
|
+
* installs the new version and simply EXITS; launchd starting the replacement
|
|
69
|
+
* is what makes that safe (see index.ts). ThrottleInterval keeps a crash loop
|
|
70
|
+
* to one launch per 10s instead of a spin.
|
|
71
|
+
*
|
|
72
|
+
* ProcessType is Interactive because Background jobs get throttled CPU
|
|
73
|
+
* scheduling — a daemon that supervises agent runs is not a batch job.
|
|
74
|
+
*/
|
|
75
|
+
function buildPlist(spec) {
|
|
76
|
+
const args = [spec.entry.file, ...spec.entry.args]
|
|
77
|
+
.map(a => ` <string>${esc(a)}</string>`)
|
|
78
|
+
.join('\n');
|
|
79
|
+
const env = Object.entries(spec.env)
|
|
80
|
+
.map(([k, v]) => ` <key>${esc(k)}</key>\n <string>${esc(v)}</string>`)
|
|
81
|
+
.join('\n');
|
|
82
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
83
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
84
|
+
<plist version="1.0">
|
|
85
|
+
<dict>
|
|
86
|
+
<key>Label</key>
|
|
87
|
+
<string>${esc(spec.label)}</string>
|
|
88
|
+
<key>ProgramArguments</key>
|
|
89
|
+
<array>
|
|
90
|
+
${args}
|
|
91
|
+
</array>
|
|
92
|
+
<key>RunAtLoad</key>
|
|
93
|
+
<true/>
|
|
94
|
+
<key>KeepAlive</key>
|
|
95
|
+
<true/>
|
|
96
|
+
<key>ThrottleInterval</key>
|
|
97
|
+
<integer>10</integer>
|
|
98
|
+
<key>ProcessType</key>
|
|
99
|
+
<string>Interactive</string>
|
|
100
|
+
<key>WorkingDirectory</key>
|
|
101
|
+
<string>${esc(spec.workingDirectory)}</string>
|
|
102
|
+
<key>StandardOutPath</key>
|
|
103
|
+
<string>${esc(spec.logPath)}</string>
|
|
104
|
+
<key>StandardErrorPath</key>
|
|
105
|
+
<string>${esc(spec.logPath)}</string>
|
|
106
|
+
<key>EnvironmentVariables</key>
|
|
107
|
+
<dict>
|
|
108
|
+
${env}
|
|
109
|
+
</dict>
|
|
110
|
+
</dict>
|
|
111
|
+
</plist>
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
/** Pull ProgramArguments back out of a plist we wrote, for `status`. */
|
|
115
|
+
function parsePlistEntry(xml) {
|
|
116
|
+
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(xml);
|
|
117
|
+
if (!block)
|
|
118
|
+
return undefined;
|
|
119
|
+
const items = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)]
|
|
120
|
+
.map(m => m[1].replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'));
|
|
121
|
+
if (!items.length)
|
|
122
|
+
return undefined;
|
|
123
|
+
return { file: items[0], args: items.slice(1) };
|
|
124
|
+
}
|
|
125
|
+
/* ── TCC: the failure that looks like success ────────────────────────────
|
|
126
|
+
* Found by running this for real. A LaunchAgent pinned to a script under
|
|
127
|
+
* ~/Documents starts, reports `state = running` with a healthy pid, writes
|
|
128
|
+
* nothing, and serves nothing — because macOS privacy protection blocks the
|
|
129
|
+
* open() and the process HANGS in module resolution rather than failing.
|
|
130
|
+
* `launchctl print` says the job is fine. It is not.
|
|
131
|
+
*
|
|
132
|
+
* That is the worst possible failure for this feature: the node looks alive
|
|
133
|
+
* to every tool including our own, so refuse to arm it instead. Our fleet's
|
|
134
|
+
* checkouts live in ~/Documents/GitHub, so this is the default case here, not
|
|
135
|
+
* an edge one.
|
|
136
|
+
*/
|
|
137
|
+
/** Directories macOS gates behind user consent no background agent can give. */
|
|
138
|
+
function protectedPathReason(scriptPath, home = os.homedir()) {
|
|
139
|
+
const p = scriptPath.replace(/\/+$/, '');
|
|
140
|
+
const under = (dir) => p === dir || p.startsWith(dir + path.sep);
|
|
141
|
+
const guarded = [
|
|
142
|
+
[path.join(home, 'Documents'), '~/Documents'],
|
|
143
|
+
[path.join(home, 'Desktop'), '~/Desktop'],
|
|
144
|
+
[path.join(home, 'Downloads'), '~/Downloads'],
|
|
145
|
+
[path.join(home, 'Library', 'Mobile Documents'), 'iCloud Drive'],
|
|
146
|
+
];
|
|
147
|
+
for (const [dir, label] of guarded)
|
|
148
|
+
if (under(dir))
|
|
149
|
+
return label;
|
|
150
|
+
if (under('/Volumes'))
|
|
151
|
+
return 'an external volume';
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function protectedPathError(label, scriptPath) {
|
|
155
|
+
return [
|
|
156
|
+
`macOS will not let a login agent read ${label}, and this node runs from there:`,
|
|
157
|
+
` ${scriptPath}`,
|
|
158
|
+
'',
|
|
159
|
+
'It would not fail loudly — the agent starts, reports itself healthy, and hangs',
|
|
160
|
+
'forever on the first file it tries to read. So this refuses instead.',
|
|
161
|
+
'',
|
|
162
|
+
'Fix it either way:',
|
|
163
|
+
' npm i -g @addai/node install outside the protected folder, then re-run this',
|
|
164
|
+
' — or move the checkout somewhere like ~/src and start the node from there',
|
|
165
|
+
'',
|
|
166
|
+
'If you have already granted Full Disk Access to node, re-run with --force.',
|
|
167
|
+
].join('\n');
|
|
168
|
+
}
|
|
169
|
+
function domain() {
|
|
170
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
|
171
|
+
return `gui/${uid}`;
|
|
172
|
+
}
|
|
173
|
+
function launchctl(args) {
|
|
174
|
+
try {
|
|
175
|
+
// stderr is piped, not inherited: `bootout` on a label that isn't loaded
|
|
176
|
+
// prints "Boot-out failed: 3: No such process", and that expected no-op
|
|
177
|
+
// was landing in the user's terminal above our own success message.
|
|
178
|
+
const out = (0, child_process_1.execFileSync)('launchctl', args, {
|
|
179
|
+
encoding: 'utf8', timeout: 20_000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
180
|
+
});
|
|
181
|
+
return { ok: true, output: out.toString().trim() };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const e = err;
|
|
185
|
+
const stderr = e.stderr ? e.stderr.toString().trim() : '';
|
|
186
|
+
return { ok: false, output: stderr || e.message || 'launchctl failed' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function stopRunningDaemon(timeoutMs = 40_000, selfPid = process.pid) {
|
|
190
|
+
const lock = (0, lockfile_1.readLockfile)();
|
|
191
|
+
if (!lock || !(0, lockfile_1.lockfileAlive)(lock))
|
|
192
|
+
return { kind: 'none' };
|
|
193
|
+
if (lock.pid === selfPid)
|
|
194
|
+
return { kind: 'self' };
|
|
195
|
+
try {
|
|
196
|
+
process.kill(lock.pid, 'SIGTERM');
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return { kind: 'stopped' };
|
|
200
|
+
}
|
|
201
|
+
// Synchronous poll: `startup enable` is a one-shot CLI command, and making
|
|
202
|
+
// the whole backend async to sleep 250ms would buy nothing.
|
|
203
|
+
const deadline = Date.now() + timeoutMs;
|
|
204
|
+
const nap = new Int32Array(new SharedArrayBuffer(4));
|
|
205
|
+
while (Date.now() < deadline) {
|
|
206
|
+
Atomics.wait(nap, 0, 0, 250);
|
|
207
|
+
if (!(0, lockfile_1.lockfileAlive)((0, lockfile_1.readLockfile)()))
|
|
208
|
+
return { kind: 'stopped' };
|
|
209
|
+
}
|
|
210
|
+
return { kind: 'stuck', pid: lock.pid };
|
|
211
|
+
}
|
|
212
|
+
function enable(entry, env, logPath, opts = {}) {
|
|
213
|
+
const warnings = [];
|
|
214
|
+
const script = entry.args[0] ?? entry.file;
|
|
215
|
+
const guarded = protectedPathReason(script);
|
|
216
|
+
if (guarded && !opts.force) {
|
|
217
|
+
return { ok: false, warnings, error: protectedPathError(guarded, script) };
|
|
218
|
+
}
|
|
219
|
+
if (guarded)
|
|
220
|
+
warnings.push(`forced past macOS protection on ${guarded} — if the node hangs at login, that is why`);
|
|
221
|
+
fs.mkdirSync(path.dirname(exports.PLIST_PATH), { recursive: true });
|
|
222
|
+
fs.writeFileSync(exports.PLIST_PATH, buildPlist({
|
|
223
|
+
label: autostart_1.LABEL,
|
|
224
|
+
entry,
|
|
225
|
+
env,
|
|
226
|
+
logPath,
|
|
227
|
+
workingDirectory: os.homedir(),
|
|
228
|
+
}), { mode: 0o644 });
|
|
229
|
+
// Unload any previous copy of the agent BEFORE looking for a daemon to hand
|
|
230
|
+
// over from. Order matters: an already-supervised job that we SIGTERM while
|
|
231
|
+
// it is still loaded gets restarted by KeepAlive within the second, and the
|
|
232
|
+
// handover would sit there watching a lockfile that keeps coming back and
|
|
233
|
+
// conclude the node is stuck. Booting out first makes the stop stick.
|
|
234
|
+
launchctl(['bootout', `${domain()}/${autostart_1.LABEL}`]);
|
|
235
|
+
const handover = stopRunningDaemon();
|
|
236
|
+
if (handover.kind === 'stuck') {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
path: exports.PLIST_PATH,
|
|
240
|
+
warnings,
|
|
241
|
+
error: `a node is already running here (pid ${handover.pid}) and did not stop within 40s — quit it, then run \`ainode startup enable\` again`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (handover.kind === 'self') {
|
|
245
|
+
// Deliberately no bootstrap: launchd would start a second daemon, which
|
|
246
|
+
// would fail to take the lockfile and be relaunched every 10s for as long
|
|
247
|
+
// as this one lives. The plist on disk is enough.
|
|
248
|
+
warnings.push('this node is running right now, so launchd takes over at your next login (or when you restart it)');
|
|
249
|
+
return { ok: true, path: exports.PLIST_PATH, warnings };
|
|
250
|
+
}
|
|
251
|
+
if (handover.kind === 'stopped') {
|
|
252
|
+
warnings.push('stopped the node that was already running; launchd owns it now');
|
|
253
|
+
}
|
|
254
|
+
let load = launchctl(['bootstrap', domain(), exports.PLIST_PATH]);
|
|
255
|
+
if (!load.ok) {
|
|
256
|
+
// Pre-Yosemite verbs still work on every macOS we support and are the
|
|
257
|
+
// documented fallback when bootstrap is unavailable or refuses.
|
|
258
|
+
const legacy = launchctl(['load', '-w', exports.PLIST_PATH]);
|
|
259
|
+
if (!legacy.ok) {
|
|
260
|
+
return { ok: false, path: exports.PLIST_PATH, warnings, error: `launchctl could not load the agent: ${load.output}` };
|
|
261
|
+
}
|
|
262
|
+
load = legacy;
|
|
263
|
+
}
|
|
264
|
+
return { ok: true, path: exports.PLIST_PATH, warnings };
|
|
265
|
+
}
|
|
266
|
+
function disable() {
|
|
267
|
+
// Order matters: bootout while the plist still exists, or launchd has
|
|
268
|
+
// nothing to look up. This also STOPS a running supervised daemon — the
|
|
269
|
+
// node goes offline, which is what "stop starting at login" has to mean
|
|
270
|
+
// when the thing is running right now.
|
|
271
|
+
const out = launchctl(['bootout', `${domain()}/${autostart_1.LABEL}`]);
|
|
272
|
+
// Only fall back to the legacy verb if the modern one didn't do the job —
|
|
273
|
+
// running both unconditionally printed "Unload failed: 5" after a perfectly
|
|
274
|
+
// successful bootout.
|
|
275
|
+
if (!out.ok && fs.existsSync(exports.PLIST_PATH))
|
|
276
|
+
launchctl(['unload', '-w', exports.PLIST_PATH]);
|
|
277
|
+
try {
|
|
278
|
+
fs.unlinkSync(exports.PLIST_PATH);
|
|
279
|
+
}
|
|
280
|
+
catch { /* already gone */ }
|
|
281
|
+
return { ok: true };
|
|
282
|
+
}
|
|
283
|
+
function status() {
|
|
284
|
+
if (!fs.existsSync(exports.PLIST_PATH)) {
|
|
285
|
+
return { supported: true, enabled: false, supervisor: 'launchd' };
|
|
286
|
+
}
|
|
287
|
+
// Deliberately no `launchctl print` here: status() rides every heartbeat,
|
|
288
|
+
// and the plist sitting in ~/Library/LaunchAgents is already the whole
|
|
289
|
+
// answer to "will this start at login?" — launchd loads that directory at
|
|
290
|
+
// every login regardless of what is loaded right now.
|
|
291
|
+
const xml = fs.readFileSync(exports.PLIST_PATH, 'utf8');
|
|
292
|
+
const entry = parsePlistEntry(xml);
|
|
293
|
+
const issues = [];
|
|
294
|
+
if (entry && !fs.existsSync(entry.args[0] ?? entry.file)) {
|
|
295
|
+
issues.push(`the pinned program is gone (${entry.args[0] ?? entry.file}) — re-run \`ainode startup enable\``);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
supported: true,
|
|
299
|
+
enabled: true,
|
|
300
|
+
supervisor: 'launchd',
|
|
301
|
+
entry,
|
|
302
|
+
path: exports.PLIST_PATH,
|
|
303
|
+
...(issues.length ? { issues } : {}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
exports.backend = { supervisor: 'launchd', status, enable, disable };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AutostartBackend, AutostartEntry } from './autostart';
|
|
2
|
+
export declare const STARTUP_DIR: string;
|
|
3
|
+
export declare const CMD_PATH: string;
|
|
4
|
+
export declare const VBS_PATH: string;
|
|
5
|
+
export declare const XML_PATH: string;
|
|
6
|
+
/** The account the task runs as. `DOMAIN\User` when we have a domain — a bare
|
|
7
|
+
* username is ambiguous on a machine joined to one. */
|
|
8
|
+
export declare function currentUserId(env?: NodeJS.ProcessEnv): string;
|
|
9
|
+
/**
|
|
10
|
+
* The batch file that actually starts the node.
|
|
11
|
+
*
|
|
12
|
+
* Environment and output redirection live here, in a file where batch quoting
|
|
13
|
+
* behaves, instead of being wedged into a VBS string or a `schtasks` argument.
|
|
14
|
+
* The redirect matters: headless stdout on Windows has nowhere else to go, and
|
|
15
|
+
* a node whose only account of itself is missing is a node nobody can debug.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildCmd(entry: AutostartEntry, env: Record<string, string>, logPath: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* The window-hiding shim.
|
|
20
|
+
*
|
|
21
|
+
* A task running under an InteractiveToken shows a console window for every
|
|
22
|
+
* launch; with the logon trigger repeating, that would be a window popping up
|
|
23
|
+
* on the user's desktop every five minutes. `WScript.Shell.Run(…, 0, False)`
|
|
24
|
+
* starts the batch file with no window and does not wait for it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildVbs(cmdPath: string): string;
|
|
27
|
+
export interface TaskSpec {
|
|
28
|
+
userId: string;
|
|
29
|
+
vbsPath: string;
|
|
30
|
+
wscriptPath: string;
|
|
31
|
+
/** ISO-8601 local timestamp for the task's Date field. */
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The task definition.
|
|
36
|
+
*
|
|
37
|
+
* The repetition is what stands in for launchd's KeepAlive. Task Scheduler
|
|
38
|
+
* cannot supervise this process — the shim returns instantly, so the task is
|
|
39
|
+
* "finished" a second after it starts and RestartOnFailure never applies. So
|
|
40
|
+
* the logon trigger repeats every five minutes and the action is
|
|
41
|
+
* `run --ensure`, which exits immediately when a live daemon already owns the
|
|
42
|
+
* node. Crash recovery therefore costs at most five minutes and never stacks
|
|
43
|
+
* a second daemon.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildTaskXml(spec: TaskSpec): string;
|
|
46
|
+
export declare const backend: AutostartBackend;
|