metaharness 0.4.4 → 0.4.6
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 +21 -2
- package/dist/bin.js +0 -0
- package/dist/harness-bin.js +0 -0
- package/dist/host-config.d.ts.map +1 -1
- package/dist/host-config.js +17 -1
- package/dist/host-config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/meta-proxy-service.d.ts +25 -8
- package/dist/meta-proxy-service.d.ts.map +1 -1
- package/dist/meta-proxy-service.js +522 -46
- package/dist/meta-proxy-service.js.map +1 -1
- package/dist/meta-proxy.d.ts +11 -2
- package/dist/meta-proxy.d.ts.map +1 -1
- package/dist/meta-proxy.js +137 -13
- package/dist/meta-proxy.js.map +1 -1
- package/dist/secrets.d.ts +11 -1
- package/dist/secrets.d.ts.map +1 -1
- package/dist/secrets.js +20 -7
- package/dist/secrets.js.map +1 -1
- package/package.json +6 -5
|
@@ -21,12 +21,17 @@
|
|
|
21
21
|
// without touching launchctl/systemctl/schtasks. That mirrors `installFromSource`
|
|
22
22
|
// in meta-proxy.ts, which already takes an injected `run`.
|
|
23
23
|
import { spawnSync } from 'node:child_process';
|
|
24
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { homedir } from 'node:os';
|
|
26
26
|
import { dirname, join } from 'node:path';
|
|
27
27
|
import { metaProxyBinaryPath, metaProxyDataDir } from './meta-proxy.js';
|
|
28
28
|
/** Reverse-DNS label, shared by the launchd job and the scheduled task. */
|
|
29
29
|
export const SERVICE_LABEL = 'one.cognitum.meta-proxy';
|
|
30
|
+
function assertServiceLabel(label) {
|
|
31
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{0,127}$/.test(label)) {
|
|
32
|
+
throw new Error('Invalid service label.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
30
35
|
function defaultRunner(invocation) {
|
|
31
36
|
const result = spawnSync(invocation.command, invocation.args, {
|
|
32
37
|
encoding: 'utf8',
|
|
@@ -35,27 +40,30 @@ function defaultRunner(invocation) {
|
|
|
35
40
|
});
|
|
36
41
|
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
|
37
42
|
if (result.error)
|
|
38
|
-
return { ok: false, output: result.error.message };
|
|
43
|
+
return { ok: false, output: result.error.message, error: result.error.message };
|
|
39
44
|
return { ok: result.status === 0, output };
|
|
40
45
|
}
|
|
41
46
|
// --- unit file locations ----------------------------------------------------
|
|
42
|
-
export function launchAgentPath(home = homedir()) {
|
|
43
|
-
|
|
47
|
+
export function launchAgentPath(home = homedir(), label = SERVICE_LABEL) {
|
|
48
|
+
assertServiceLabel(label);
|
|
49
|
+
return join(home, 'Library', 'LaunchAgents', `${label}.plist`);
|
|
44
50
|
}
|
|
45
51
|
export function systemdUnitPath(home = homedir()) {
|
|
46
52
|
return join(home, '.config', 'systemd', 'user', 'meta-proxy.service');
|
|
47
53
|
}
|
|
48
54
|
/** The XML definition handed to `schtasks /create /xml`. */
|
|
49
|
-
export function scheduledTaskPath(home = homedir()) {
|
|
50
|
-
|
|
55
|
+
export function scheduledTaskPath(home = homedir(), label = SERVICE_LABEL) {
|
|
56
|
+
assertServiceLabel(label);
|
|
57
|
+
const filename = label === SERVICE_LABEL ? 'meta-proxy-task.xml' : `${label}-task.xml`;
|
|
58
|
+
return join(metaProxyDataDir(home), filename);
|
|
51
59
|
}
|
|
52
|
-
export function serviceUnitPath(platform, home = homedir()) {
|
|
60
|
+
export function serviceUnitPath(platform, home = homedir(), label = SERVICE_LABEL) {
|
|
53
61
|
if (platform === 'darwin')
|
|
54
|
-
return launchAgentPath(home);
|
|
62
|
+
return launchAgentPath(home, label);
|
|
55
63
|
if (platform === 'linux')
|
|
56
64
|
return systemdUnitPath(home);
|
|
57
65
|
if (platform === 'win32')
|
|
58
|
-
return scheduledTaskPath(home);
|
|
66
|
+
return scheduledTaskPath(home, label);
|
|
59
67
|
return null;
|
|
60
68
|
}
|
|
61
69
|
// --- pure renderers ---------------------------------------------------------
|
|
@@ -74,16 +82,17 @@ function escapeXml(value) {
|
|
|
74
82
|
* `proxy stop` (SIGTERM, exit 0) stay stopped — a plain `KeepAlive: true` would
|
|
75
83
|
* fight the user's own stop command forever.
|
|
76
84
|
*/
|
|
77
|
-
export function renderLaunchAgent(binaryPath, logPath) {
|
|
85
|
+
export function renderLaunchAgent(binaryPath, logPath, label = SERVICE_LABEL) {
|
|
78
86
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
79
87
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
80
88
|
<plist version="1.0">
|
|
81
89
|
<dict>
|
|
82
90
|
<key>Label</key>
|
|
83
|
-
<string>${
|
|
91
|
+
<string>${escapeXml(label)}</string>
|
|
84
92
|
<key>ProgramArguments</key>
|
|
85
93
|
<array>
|
|
86
94
|
<string>${escapeXml(binaryPath)}</string>
|
|
95
|
+
<string>--supervised</string>
|
|
87
96
|
</array>
|
|
88
97
|
<key>RunAtLoad</key>
|
|
89
98
|
<true/>
|
|
@@ -118,7 +127,7 @@ After=network-online.target
|
|
|
118
127
|
|
|
119
128
|
[Service]
|
|
120
129
|
Type=simple
|
|
121
|
-
ExecStart=${binaryPath}
|
|
130
|
+
ExecStart=${binaryPath} --supervised
|
|
122
131
|
Restart=on-failure
|
|
123
132
|
RestartSec=10
|
|
124
133
|
|
|
@@ -128,11 +137,19 @@ WantedBy=default.target
|
|
|
128
137
|
}
|
|
129
138
|
/** Task Scheduler definition — Windows has no launchd/systemd equivalent. */
|
|
130
139
|
export function renderScheduledTask(binaryPath) {
|
|
131
|
-
|
|
140
|
+
// Task Scheduler's XML importer requires its canonical BOM-marked UTF-16
|
|
141
|
+
// representation; enableMetaProxyService persists this string as utf16le.
|
|
142
|
+
return `\uFEFF<?xml version="1.0" encoding="UTF-16"?>
|
|
132
143
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
133
144
|
<RegistrationInfo>
|
|
134
145
|
<Description>Cognitum Meta-Proxy sidecar</Description>
|
|
135
146
|
</RegistrationInfo>
|
|
147
|
+
<Principals>
|
|
148
|
+
<Principal id="Author">
|
|
149
|
+
<LogonType>InteractiveToken</LogonType>
|
|
150
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
151
|
+
</Principal>
|
|
152
|
+
</Principals>
|
|
136
153
|
<Triggers>
|
|
137
154
|
<LogonTrigger>
|
|
138
155
|
<Enabled>true</Enabled>
|
|
@@ -152,19 +169,20 @@ export function renderScheduledTask(binaryPath) {
|
|
|
152
169
|
<Actions Context="Author">
|
|
153
170
|
<Exec>
|
|
154
171
|
<Command>${escapeXml(binaryPath)}</Command>
|
|
172
|
+
<Arguments>--supervised</Arguments>
|
|
155
173
|
</Exec>
|
|
156
174
|
</Actions>
|
|
157
175
|
</Task>
|
|
158
176
|
`;
|
|
159
177
|
}
|
|
160
178
|
// --- the OS commands, as data so they can be asserted -----------------------
|
|
161
|
-
export function enableCommands(platform, unitPath) {
|
|
179
|
+
export function enableCommands(platform, unitPath, label = SERVICE_LABEL) {
|
|
162
180
|
if (platform === 'darwin') {
|
|
163
181
|
// `bootstrap` is the modern replacement for `load -w`; `kickstart` starts it
|
|
164
182
|
// now so the user does not have to log out and back in to get a daemon.
|
|
165
183
|
return [
|
|
166
184
|
{ command: 'launchctl', args: ['bootstrap', `gui/${process.getuid?.() ?? 0}`, unitPath] },
|
|
167
|
-
{ command: 'launchctl', args: ['kickstart', `gui/${process.getuid?.() ?? 0}/${
|
|
185
|
+
{ command: 'launchctl', args: ['kickstart', `gui/${process.getuid?.() ?? 0}/${label}`] },
|
|
168
186
|
];
|
|
169
187
|
}
|
|
170
188
|
if (platform === 'linux') {
|
|
@@ -175,15 +193,15 @@ export function enableCommands(platform, unitPath) {
|
|
|
175
193
|
}
|
|
176
194
|
if (platform === 'win32') {
|
|
177
195
|
return [
|
|
178
|
-
{ command: 'schtasks', args: ['/create', '/tn',
|
|
179
|
-
{ command: 'schtasks', args: ['/run', '/tn',
|
|
196
|
+
{ command: 'schtasks', args: ['/create', '/tn', label, '/xml', unitPath, '/f'] },
|
|
197
|
+
{ command: 'schtasks', args: ['/run', '/tn', label] },
|
|
180
198
|
];
|
|
181
199
|
}
|
|
182
200
|
return [];
|
|
183
201
|
}
|
|
184
|
-
export function disableCommands(platform, unitPath) {
|
|
202
|
+
export function disableCommands(platform, unitPath, label = SERVICE_LABEL) {
|
|
185
203
|
if (platform === 'darwin') {
|
|
186
|
-
return [{ command: 'launchctl', args: ['bootout', `gui/${process.getuid?.() ?? 0}
|
|
204
|
+
return [{ command: 'launchctl', args: ['bootout', `gui/${process.getuid?.() ?? 0}/${label}`] }];
|
|
187
205
|
}
|
|
188
206
|
if (platform === 'linux') {
|
|
189
207
|
return [
|
|
@@ -192,22 +210,117 @@ export function disableCommands(platform, unitPath) {
|
|
|
192
210
|
];
|
|
193
211
|
}
|
|
194
212
|
if (platform === 'win32') {
|
|
195
|
-
return [{ command: 'schtasks', args: ['/delete', '/tn',
|
|
213
|
+
return [{ command: 'schtasks', args: ['/delete', '/tn', label, '/f'] }];
|
|
196
214
|
}
|
|
197
215
|
return [];
|
|
198
216
|
}
|
|
217
|
+
export function serviceStateCommand(platform, label = SERVICE_LABEL) {
|
|
218
|
+
if (platform === 'darwin') {
|
|
219
|
+
return { command: 'launchctl', args: ['print', `gui/${process.getuid?.() ?? 0}/${label}`] };
|
|
220
|
+
}
|
|
221
|
+
if (platform === 'linux') {
|
|
222
|
+
return {
|
|
223
|
+
command: 'systemctl',
|
|
224
|
+
args: ['--user', 'show', 'meta-proxy.service', '--property=LoadState,UnitFileState,ActiveState,MainPID'],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (platform === 'win32') {
|
|
228
|
+
// schtasks renders both headings and states in the user's UI language.
|
|
229
|
+
// Query the ScheduledTasks API and serialize numeric enum/bool values so
|
|
230
|
+
// state and login enablement are authoritative and locale-neutral.
|
|
231
|
+
const script = `$task = Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskName -ceq '${label}' -and $_.TaskPath -eq '\\' } | Select-Object -First 1; if ($null -eq $task) { Write-Output 'TaskState=Missing'; exit 0 }; Write-Output ('TaskState=' + [int]$task.State)`;
|
|
232
|
+
return { command: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-Command', script] };
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
function knownMissingService(platform, output) {
|
|
237
|
+
const text = output.toLowerCase();
|
|
238
|
+
if (platform === 'darwin') {
|
|
239
|
+
return text.includes('could not find service') || text.includes('service not found');
|
|
240
|
+
}
|
|
241
|
+
if (platform === 'linux') {
|
|
242
|
+
return text.includes('loadstate=not-found') || text.includes('unit meta-proxy.service could not be found');
|
|
243
|
+
}
|
|
244
|
+
if (platform === 'win32') {
|
|
245
|
+
return /^taskstate=missing$/m.test(text);
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
function parseManagerState(platform, outcome, definitionPresent) {
|
|
250
|
+
if (outcome.error)
|
|
251
|
+
return { managerState: 'unknown', loaded: null, enabledAtLogin: false, running: null, pid: null };
|
|
252
|
+
if (!outcome.ok) {
|
|
253
|
+
return knownMissingService(platform, outcome.output)
|
|
254
|
+
? { managerState: 'disabled', loaded: false, enabledAtLogin: platform === 'darwin' && definitionPresent, running: false, pid: null }
|
|
255
|
+
: { managerState: 'unknown', loaded: null, enabledAtLogin: false, running: null, pid: null };
|
|
256
|
+
}
|
|
257
|
+
if (platform === 'darwin') {
|
|
258
|
+
const pid = outcome.output.match(/\bpid\s*=\s*(\d+)/)?.[1];
|
|
259
|
+
return {
|
|
260
|
+
managerState: 'enabled',
|
|
261
|
+
loaded: true,
|
|
262
|
+
enabledAtLogin: definitionPresent,
|
|
263
|
+
running: /\bstate\s*=\s*running\b/.test(outcome.output) || pid !== undefined,
|
|
264
|
+
pid: pid ? Number.parseInt(pid, 10) : null,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (platform === 'linux') {
|
|
268
|
+
if (/^LoadState=not-found$/m.test(outcome.output)) {
|
|
269
|
+
return { managerState: 'disabled', loaded: false, enabledAtLogin: false, running: false, pid: null };
|
|
270
|
+
}
|
|
271
|
+
const enabled = /^UnitFileState=(?:enabled|enabled-runtime|linked|linked-runtime)$/m.test(outcome.output);
|
|
272
|
+
const loaded = /^LoadState=loaded$/m.test(outcome.output);
|
|
273
|
+
const pidText = outcome.output.match(/^MainPID=(\d+)$/m)?.[1];
|
|
274
|
+
const pid = pidText && pidText !== '0' ? Number.parseInt(pidText, 10) : null;
|
|
275
|
+
return {
|
|
276
|
+
managerState: loaded ? 'enabled' : 'disabled',
|
|
277
|
+
loaded,
|
|
278
|
+
enabledAtLogin: enabled,
|
|
279
|
+
running: /^ActiveState=active$/m.test(outcome.output),
|
|
280
|
+
pid,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (platform === 'win32') {
|
|
284
|
+
if (/^TaskState=Missing$/m.test(outcome.output)) {
|
|
285
|
+
return { managerState: 'disabled', loaded: false, enabledAtLogin: false, running: false, pid: null };
|
|
286
|
+
}
|
|
287
|
+
const state = outcome.output.match(/^TaskState=(\d+)$/m)?.[1];
|
|
288
|
+
if (state === undefined || !['0', '1', '2', '3', '4'].includes(state)) {
|
|
289
|
+
return { managerState: 'unknown', loaded: null, enabledAtLogin: false, running: null, pid: null };
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
managerState: 'enabled',
|
|
293
|
+
loaded: true,
|
|
294
|
+
// TASK_STATE_DISABLED is the authoritative registered-but-disabled state;
|
|
295
|
+
// all other known registered states remain enabled for their triggers.
|
|
296
|
+
enabledAtLogin: state !== '1',
|
|
297
|
+
running: state === '4' ? true : state === '1' || state === '3' ? false : null,
|
|
298
|
+
pid: null,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return { managerState: 'unknown', loaded: null, enabledAtLogin: false, running: null, pid: null };
|
|
302
|
+
}
|
|
199
303
|
// --- public API -------------------------------------------------------------
|
|
200
|
-
export function metaProxyServiceState(platform = process.platform, home = homedir()) {
|
|
201
|
-
const unitPath = serviceUnitPath(platform, home);
|
|
304
|
+
export function metaProxyServiceState(platform = process.platform, home = homedir(), run = defaultRunner, label = SERVICE_LABEL) {
|
|
305
|
+
const unitPath = serviceUnitPath(platform, home, label);
|
|
202
306
|
if (!unitPath)
|
|
203
|
-
return { supported: false, installed: false, unitPath: null };
|
|
204
|
-
|
|
307
|
+
return { supported: false, installed: false, unitPath: null, managerState: 'unknown', definitionPresent: false, loaded: null, enabledAtLogin: false, running: null, pid: null };
|
|
308
|
+
const definitionPresent = existsSync(unitPath);
|
|
309
|
+
const command = serviceStateCommand(platform, label);
|
|
310
|
+
const outcome = command ? run(command) : { ok: false, output: '', error: 'unsupported' };
|
|
311
|
+
const manager = parseManagerState(platform, outcome, definitionPresent);
|
|
312
|
+
return { supported: true, installed: manager.loaded === true, unitPath, definitionPresent, ...manager };
|
|
313
|
+
}
|
|
314
|
+
function defaultWait(milliseconds) {
|
|
315
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
205
316
|
}
|
|
206
317
|
export function enableMetaProxyService(options = {}) {
|
|
207
318
|
const home = options.home ?? homedir();
|
|
208
319
|
const platform = options.platform ?? process.platform;
|
|
209
320
|
const run = options.run ?? defaultRunner;
|
|
210
|
-
const
|
|
321
|
+
const label = options.label ?? SERVICE_LABEL;
|
|
322
|
+
const wait = options.wait ?? defaultWait;
|
|
323
|
+
const unitPath = serviceUnitPath(platform, home, label);
|
|
211
324
|
if (!unitPath) {
|
|
212
325
|
return { ok: false, message: `Start-at-login is not supported on ${platform}.` };
|
|
213
326
|
}
|
|
@@ -220,17 +333,193 @@ export function enableMetaProxyService(options = {}) {
|
|
|
220
333
|
}
|
|
221
334
|
const logPath = join(metaProxyDataDir(home), 'meta-proxy.log');
|
|
222
335
|
const definition = platform === 'darwin'
|
|
223
|
-
? renderLaunchAgent(binaryPath, logPath)
|
|
336
|
+
? renderLaunchAgent(binaryPath, logPath, label)
|
|
224
337
|
: platform === 'linux'
|
|
225
338
|
? renderSystemdUnit(binaryPath)
|
|
226
339
|
: renderScheduledTask(binaryPath);
|
|
340
|
+
const before = metaProxyServiceState(platform, home, run, label);
|
|
341
|
+
if (before.managerState === 'unknown') {
|
|
342
|
+
return { ok: false, message: `Could not determine service-manager state; preserved ${unitPath}.`, unitPath };
|
|
343
|
+
}
|
|
344
|
+
if (before.loaded === true && !before.definitionPresent) {
|
|
345
|
+
// Something answers to our label but no owned definition exists — another
|
|
346
|
+
// installer's job, or a unit shadowing ours from a different path. On
|
|
347
|
+
// darwin/linux a failed enable from this state would be "compensated" by
|
|
348
|
+
// booting out that foreign service, so refuse up front on every platform.
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
message: `A Meta-Proxy ${platform === 'win32' ? 'task' : 'service'} is registered without the owned definition at ${unitPath}. Inspect and remove it manually before replacement.`,
|
|
352
|
+
unitPath,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const persistedEncoding = platform === 'win32' ? 'utf16le' : 'utf8';
|
|
356
|
+
if (before.definitionPresent && readFileSync(unitPath, persistedEncoding) !== definition) {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
message: `A different Meta-Proxy service definition already exists at ${unitPath}. Disable it before replacement.`,
|
|
360
|
+
unitPath,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (before.managerState === 'enabled' && before.enabledAtLogin) {
|
|
364
|
+
if (before.definitionPresent) {
|
|
365
|
+
return { ok: true, message: `Meta-Proxy is already set to start at login. Definition: ${unitPath}`, unitPath };
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
message: `A Meta-Proxy service is already registered with a different or missing definition. Disable it before replacing ${unitPath}.`,
|
|
370
|
+
unitPath,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
227
373
|
mkdirSync(dirname(unitPath), { recursive: true, mode: 0o700 });
|
|
228
|
-
writeFileSync(unitPath, definition, { encoding:
|
|
229
|
-
for (const invocation of enableCommands(platform, unitPath)) {
|
|
374
|
+
writeFileSync(unitPath, definition, { encoding: persistedEncoding, mode: 0o600 });
|
|
375
|
+
for (const invocation of enableCommands(platform, unitPath, label)) {
|
|
230
376
|
const outcome = run(invocation);
|
|
231
377
|
if (!outcome.ok) {
|
|
232
|
-
|
|
233
|
-
|
|
378
|
+
const state = metaProxyServiceState(platform, home, run, label);
|
|
379
|
+
if (before.definitionPresent) {
|
|
380
|
+
// This enable started from an existing owned definition. Never erase it
|
|
381
|
+
// or stop a pre-existing active service while compensating a failed
|
|
382
|
+
// attempt to change only its login-start state.
|
|
383
|
+
if (platform === 'linux') {
|
|
384
|
+
const restore = [];
|
|
385
|
+
if (!before.enabledAtLogin && state.enabledAtLogin) {
|
|
386
|
+
// Deliberately omit --now: a service that was already active must
|
|
387
|
+
// remain active while only its prior login-enable bit is restored.
|
|
388
|
+
restore.push({ command: 'systemctl', args: ['--user', 'disable', 'meta-proxy.service'] });
|
|
389
|
+
}
|
|
390
|
+
if (before.running === false && state.running === true) {
|
|
391
|
+
restore.push({ command: 'systemctl', args: ['--user', 'stop', 'meta-proxy.service'] });
|
|
392
|
+
}
|
|
393
|
+
const restoreFailure = restore.map((command) => run(command)).find((result) => !result.ok);
|
|
394
|
+
if (restoreFailure) {
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
message: `Could not enable start-at-login and restoring the prior service state failed: ${restoreFailure.output || 'command failed'}. Definition preserved at ${unitPath}.`,
|
|
398
|
+
unitPath,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const restored = metaProxyServiceState(platform, home, run, label);
|
|
402
|
+
if (restored.managerState === 'unknown'
|
|
403
|
+
|| restored.enabledAtLogin !== before.enabledAtLogin
|
|
404
|
+
|| restored.running !== before.running) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
message: `Could not prove the prior service state was restored. Definition preserved at ${unitPath}.`,
|
|
408
|
+
unitPath,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (platform === 'win32' && before.loaded === false && state.loaded === true) {
|
|
413
|
+
const cleanup = stopAndDeleteWindowsTask(home, unitPath, label, run, wait, false);
|
|
414
|
+
if (!cleanup.ok) {
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
message: `Could not enable start-at-login and restoring the prior absent task failed: ${cleanup.message}`,
|
|
418
|
+
unitPath,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
message: `Could not enable start-at-login (${invocation.command} ${invocation.args.join(' ')}): ${outcome.output || 'command failed'}. Prior definition preserved at ${unitPath}.`,
|
|
424
|
+
unitPath,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (platform === 'win32' && before.loaded === true && !before.enabledAtLogin) {
|
|
428
|
+
if (state.managerState === 'unknown') {
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
message: `Could not enable start-at-login and manager state is unknown. Prior definition preserved at ${unitPath}.`,
|
|
432
|
+
unitPath,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (state.loaded === false) {
|
|
436
|
+
const recreate = enableCommands('win32', unitPath, label)[0];
|
|
437
|
+
const recreated = recreate ? run(recreate) : { ok: false, output: 'create command unavailable' };
|
|
438
|
+
// `/create /f` can apply and still lose its response. Its exit
|
|
439
|
+
// status is not authoritative; re-read before deciding whether
|
|
440
|
+
// the prior task is still absent or available for restoration.
|
|
441
|
+
const afterRecreate = metaProxyServiceState('win32', home, run, label);
|
|
442
|
+
if (afterRecreate.managerState === 'unknown' || afterRecreate.loaded !== true) {
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
message: `Could not enable start-at-login and could not prove the prior disabled task was recreated: ${recreated.output || 'state mismatch'}. Definition preserved at ${unitPath}.`,
|
|
446
|
+
unitPath,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const stopped = confirmWindowsTaskStopped(home, unitPath, label, run, wait);
|
|
451
|
+
if (!stopped.ok) {
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
message: `Could not enable start-at-login and restoring the prior disabled task failed: ${stopped.message}`,
|
|
455
|
+
unitPath,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const disabled = run({ command: 'schtasks', args: ['/change', '/tn', label, '/disable'] });
|
|
459
|
+
const restored = disabled.ok ? metaProxyServiceState('win32', home, run, label) : null;
|
|
460
|
+
if (!disabled.ok || restored?.loaded !== true || restored.enabledAtLogin || restored.running !== false) {
|
|
461
|
+
return {
|
|
462
|
+
ok: false,
|
|
463
|
+
message: `Could not enable start-at-login and restoring the prior disabled task failed: ${disabled.output || 'state mismatch'}. Definition preserved at ${unitPath}.`,
|
|
464
|
+
unitPath,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
ok: false,
|
|
469
|
+
message: `Could not enable start-at-login (${invocation.command} ${invocation.args.join(' ')}): ${outcome.output || 'command failed'}. Prior disabled task and definition restored at ${unitPath}.`,
|
|
470
|
+
unitPath,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (before.loaded === false && state.managerState === 'enabled') {
|
|
474
|
+
const cleanup = disableCommands(platform, unitPath, label)[0];
|
|
475
|
+
const cleanupOutcome = cleanup ? run(cleanup) : { ok: false, output: 'cleanup command unavailable' };
|
|
476
|
+
if (!cleanupOutcome.ok) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
message: `Could not enable start-at-login and compensating cleanup failed: ${cleanupOutcome.output || 'command failed'}. Definition preserved at ${unitPath}.`,
|
|
480
|
+
unitPath,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
ok: false,
|
|
486
|
+
message: `Could not enable start-at-login (${invocation.command} ${invocation.args.join(' ')}): ${outcome.output || 'command failed'}. Definition preserved at ${unitPath}.`,
|
|
487
|
+
unitPath,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (state.managerState === 'enabled') {
|
|
491
|
+
if (platform === 'win32') {
|
|
492
|
+
const cleanup = stopAndDeleteWindowsTask(home, unitPath, label, run, wait);
|
|
493
|
+
if (!cleanup.ok) {
|
|
494
|
+
return {
|
|
495
|
+
ok: false,
|
|
496
|
+
message: `Could not enable start-at-login and compensating cleanup failed: ${cleanup.message}`,
|
|
497
|
+
unitPath,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
ok: false,
|
|
502
|
+
message: `Could not enable start-at-login (${invocation.command} ${invocation.args.join(' ')}): ${outcome.output || 'command failed'}`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const cleanupFailures = disableCommands(platform, unitPath, label)
|
|
506
|
+
.map((cleanup) => run(cleanup))
|
|
507
|
+
.filter((cleanup) => !cleanup.ok);
|
|
508
|
+
if (cleanupFailures.length > 0) {
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
message: `Could not enable start-at-login and compensating cleanup failed: ${cleanupFailures.map((failure) => failure.output || 'command failed').join('; ')}. Definition preserved at ${unitPath}.`,
|
|
512
|
+
unitPath,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else if (state.managerState === 'unknown') {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
message: `Could not enable start-at-login and manager state is unknown. Definition preserved at ${unitPath}.`,
|
|
520
|
+
unitPath,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
234
523
|
rmSync(unitPath, { force: true });
|
|
235
524
|
return {
|
|
236
525
|
ok: false,
|
|
@@ -244,34 +533,221 @@ export function enableMetaProxyService(options = {}) {
|
|
|
244
533
|
unitPath,
|
|
245
534
|
};
|
|
246
535
|
}
|
|
536
|
+
function startCommand(platform, label = SERVICE_LABEL) {
|
|
537
|
+
if (platform === 'darwin')
|
|
538
|
+
return { command: 'launchctl', args: ['kickstart', `gui/${process.getuid?.() ?? 0}/${label}`] };
|
|
539
|
+
if (platform === 'linux')
|
|
540
|
+
return { command: 'systemctl', args: ['--user', 'start', 'meta-proxy.service'] };
|
|
541
|
+
if (platform === 'win32')
|
|
542
|
+
return { command: 'schtasks', args: ['/run', '/tn', label] };
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
function stopCommand(platform, label = SERVICE_LABEL) {
|
|
546
|
+
// `launchctl kill SIGTERM` may still be classified by launchd as an
|
|
547
|
+
// unsuccessful termination and restarted under KeepAlive. Bootout unloads
|
|
548
|
+
// the current job but deliberately leaves the plist in LaunchAgents, so the
|
|
549
|
+
// explicit stop stays stopped now and remains configured for next login.
|
|
550
|
+
if (platform === 'darwin')
|
|
551
|
+
return { command: 'launchctl', args: ['bootout', `gui/${process.getuid?.() ?? 0}/${label}`] };
|
|
552
|
+
if (platform === 'linux')
|
|
553
|
+
return { command: 'systemctl', args: ['--user', 'stop', 'meta-proxy.service'] };
|
|
554
|
+
if (platform === 'win32')
|
|
555
|
+
return { command: 'schtasks', args: ['/end', '/tn', label] };
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
export function startMetaProxyService(options = {}) {
|
|
559
|
+
const home = options.home ?? homedir();
|
|
560
|
+
const platform = options.platform ?? process.platform;
|
|
561
|
+
const run = options.run ?? defaultRunner;
|
|
562
|
+
const label = options.label ?? SERVICE_LABEL;
|
|
563
|
+
const state = metaProxyServiceState(platform, home, run, label);
|
|
564
|
+
if (state.managerState === 'unknown')
|
|
565
|
+
return { ok: false, message: 'Could not determine service-manager state.' };
|
|
566
|
+
if (state.managerState !== 'enabled' && !(platform === 'darwin' && state.definitionPresent)) {
|
|
567
|
+
return { ok: false, message: 'Meta-Proxy is not enabled at login.' };
|
|
568
|
+
}
|
|
569
|
+
if (state.running)
|
|
570
|
+
return { ok: true, message: `Meta-Proxy is already running${state.pid ? ` (pid ${state.pid})` : ''}.` };
|
|
571
|
+
const commands = platform === 'darwin' && state.managerState === 'disabled' && state.unitPath
|
|
572
|
+
? enableCommands(platform, state.unitPath, label)
|
|
573
|
+
: [startCommand(platform, label)].filter((command) => command !== null);
|
|
574
|
+
if (commands.length === 0)
|
|
575
|
+
return { ok: false, message: `Managed start is not supported on ${platform}.` };
|
|
576
|
+
for (const command of commands) {
|
|
577
|
+
const outcome = run(command);
|
|
578
|
+
if (!outcome.ok)
|
|
579
|
+
return { ok: false, message: `Could not start Meta-Proxy: ${outcome.output || 'command failed'}` };
|
|
580
|
+
}
|
|
581
|
+
return { ok: true, message: 'Meta-Proxy managed start requested.' };
|
|
582
|
+
}
|
|
583
|
+
export function stopMetaProxyService(options = {}) {
|
|
584
|
+
const home = options.home ?? homedir();
|
|
585
|
+
const platform = options.platform ?? process.platform;
|
|
586
|
+
const run = options.run ?? defaultRunner;
|
|
587
|
+
const label = options.label ?? SERVICE_LABEL;
|
|
588
|
+
const state = metaProxyServiceState(platform, home, run, label);
|
|
589
|
+
if (state.managerState === 'unknown')
|
|
590
|
+
return { ok: false, message: 'Could not determine service-manager state.' };
|
|
591
|
+
if (state.managerState !== 'enabled') {
|
|
592
|
+
return state.enabledAtLogin
|
|
593
|
+
? { ok: true, message: 'Meta-Proxy managed service is already stopped.' }
|
|
594
|
+
: { ok: false, message: 'Meta-Proxy is not enabled at login.' };
|
|
595
|
+
}
|
|
596
|
+
if (state.running === false)
|
|
597
|
+
return { ok: true, message: 'Meta-Proxy managed service is already stopped.' };
|
|
598
|
+
const command = stopCommand(platform, label);
|
|
599
|
+
if (!command)
|
|
600
|
+
return { ok: false, message: `Managed stop is not supported on ${platform}.` };
|
|
601
|
+
const outcome = run(command);
|
|
602
|
+
return outcome.ok
|
|
603
|
+
? { ok: true, message: 'Meta-Proxy managed stop requested.' }
|
|
604
|
+
: { ok: false, message: `Could not stop Meta-Proxy: ${outcome.output || 'command failed'}` };
|
|
605
|
+
}
|
|
247
606
|
export function disableMetaProxyService(options = {}) {
|
|
248
607
|
const home = options.home ?? homedir();
|
|
249
608
|
const platform = options.platform ?? process.platform;
|
|
250
609
|
const run = options.run ?? defaultRunner;
|
|
251
|
-
const
|
|
610
|
+
const label = options.label ?? SERVICE_LABEL;
|
|
611
|
+
const wait = options.wait ?? defaultWait;
|
|
612
|
+
const unitPath = serviceUnitPath(platform, home, label);
|
|
252
613
|
if (!unitPath) {
|
|
253
614
|
return { ok: true, message: `Start-at-login is not supported on ${platform}; nothing to disable.` };
|
|
254
615
|
}
|
|
255
|
-
|
|
256
|
-
|
|
616
|
+
const state = metaProxyServiceState(platform, home, run, label);
|
|
617
|
+
if (state.managerState === 'unknown') {
|
|
618
|
+
return { ok: false, message: `Could not determine service-manager state; preserved ${unitPath}.`, unitPath };
|
|
257
619
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// `proxy status`. The daemon binary is untouched — disabling supervision is
|
|
261
|
-
// not uninstalling the proxy.
|
|
262
|
-
const failures = [];
|
|
263
|
-
for (const invocation of disableCommands(platform, unitPath)) {
|
|
264
|
-
const outcome = run(invocation);
|
|
265
|
-
if (!outcome.ok)
|
|
266
|
-
failures.push(outcome.output || `${invocation.command} failed`);
|
|
620
|
+
if (state.managerState === 'disabled' && !state.definitionPresent) {
|
|
621
|
+
return { ok: true, message: 'Meta-Proxy is not set to start at login.' };
|
|
267
622
|
}
|
|
268
|
-
|
|
269
|
-
|
|
623
|
+
if (state.loaded === true && !state.definitionPresent) {
|
|
624
|
+
// Same ownership rule as enable: without the owned definition on disk we
|
|
625
|
+
// cannot prove the registered job is ours, and unregistering by label
|
|
626
|
+
// alone would stop another installer's service.
|
|
270
627
|
return {
|
|
271
|
-
ok:
|
|
272
|
-
message: `
|
|
628
|
+
ok: false,
|
|
629
|
+
message: `A Meta-Proxy ${platform === 'win32' ? 'task' : 'service'} is registered without the owned definition at ${unitPath}; refusing to stop or delete it by label alone.`,
|
|
630
|
+
unitPath,
|
|
273
631
|
};
|
|
274
632
|
}
|
|
633
|
+
if (platform === 'win32' && state.managerState === 'enabled') {
|
|
634
|
+
return stopAndDeleteWindowsTask(home, unitPath, label, run, wait);
|
|
635
|
+
}
|
|
636
|
+
const unregister = state.managerState === 'enabled'
|
|
637
|
+
? disableCommands(platform, unitPath, label)[0]
|
|
638
|
+
: undefined;
|
|
639
|
+
if (unregister) {
|
|
640
|
+
const outcome = run(unregister);
|
|
641
|
+
if (!outcome.ok) {
|
|
642
|
+
if (platform === 'linux') {
|
|
643
|
+
const after = metaProxyServiceState(platform, home, run, label);
|
|
644
|
+
if (after.managerState === 'unknown') {
|
|
645
|
+
return {
|
|
646
|
+
ok: false,
|
|
647
|
+
message: `Could not disable start-at-login and could not determine whether it partially changed state: ${outcome.output || `${unregister.command} failed`}. Definition preserved at ${unitPath}.`,
|
|
648
|
+
unitPath,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const restore = [];
|
|
652
|
+
if (state.enabledAtLogin !== after.enabledAtLogin) {
|
|
653
|
+
restore.push({ command: 'systemctl', args: ['--user', state.enabledAtLogin ? 'enable' : 'disable', 'meta-proxy.service'] });
|
|
654
|
+
}
|
|
655
|
+
if (state.running !== after.running) {
|
|
656
|
+
restore.push({ command: 'systemctl', args: ['--user', state.running ? 'start' : 'stop', 'meta-proxy.service'] });
|
|
657
|
+
}
|
|
658
|
+
const restoreFailure = restore.map((command) => run(command)).find((result) => !result.ok);
|
|
659
|
+
const restored = restoreFailure ? null : metaProxyServiceState(platform, home, run, label);
|
|
660
|
+
if (restoreFailure
|
|
661
|
+
|| restored?.managerState === 'unknown'
|
|
662
|
+
|| restored?.loaded !== state.loaded
|
|
663
|
+
|| restored?.enabledAtLogin !== state.enabledAtLogin
|
|
664
|
+
|| restored?.running !== state.running) {
|
|
665
|
+
return {
|
|
666
|
+
ok: false,
|
|
667
|
+
message: `Could not disable start-at-login and restoring its possible partial changes failed: ${restoreFailure?.output || 'state mismatch'}. Definition preserved at ${unitPath}.`,
|
|
668
|
+
unitPath,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
ok: false,
|
|
673
|
+
message: `Could not disable start-at-login (${outcome.output || `${unregister.command} failed`}); prior login enablement and running state were restored. Definition preserved at ${unitPath}.`,
|
|
674
|
+
unitPath,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
ok: false,
|
|
679
|
+
message: `Could not disable start-at-login: ${outcome.output || `${unregister.command} failed`}. Definition preserved at ${unitPath}.`,
|
|
680
|
+
unitPath,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const definition = state.definitionPresent ? readFileSync(unitPath, 'utf8') : null;
|
|
685
|
+
rmSync(unitPath, { force: true });
|
|
686
|
+
if (platform === 'linux') {
|
|
687
|
+
const refresh = disableCommands(platform, unitPath, label)[1];
|
|
688
|
+
const outcome = refresh ? run(refresh) : { ok: false, output: 'daemon-reload command unavailable' };
|
|
689
|
+
if (!outcome.ok) {
|
|
690
|
+
if (definition !== null) {
|
|
691
|
+
mkdirSync(dirname(unitPath), { recursive: true, mode: 0o700 });
|
|
692
|
+
writeFileSync(unitPath, definition, { encoding: 'utf8', mode: 0o600 });
|
|
693
|
+
}
|
|
694
|
+
const restoreReload = refresh ? run(refresh) : { ok: false, output: 'daemon-reload command unavailable' };
|
|
695
|
+
const restoreCommands = [];
|
|
696
|
+
if (state.enabledAtLogin)
|
|
697
|
+
restoreCommands.push({ command: 'systemctl', args: ['--user', 'enable', 'meta-proxy.service'] });
|
|
698
|
+
if (state.running)
|
|
699
|
+
restoreCommands.push({ command: 'systemctl', args: ['--user', 'start', 'meta-proxy.service'] });
|
|
700
|
+
const restoreFailure = !restoreReload.ok
|
|
701
|
+
? restoreReload
|
|
702
|
+
: restoreCommands.map((command) => run(command)).find((result) => !result.ok);
|
|
703
|
+
const restored = restoreFailure ? null : metaProxyServiceState(platform, home, run, label);
|
|
704
|
+
if (restoreFailure
|
|
705
|
+
|| restored?.managerState === 'unknown'
|
|
706
|
+
|| restored?.enabledAtLogin !== state.enabledAtLogin
|
|
707
|
+
|| restored?.running !== state.running) {
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
message: `Disabled service and daemon-reload failed; restoring the prior manager state also failed: ${restoreFailure?.output || 'state mismatch'}. Definition preserved at ${unitPath}.`,
|
|
711
|
+
unitPath,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
ok: false,
|
|
716
|
+
message: `Disable rolled back after daemon-reload failed: ${outcome.output || 'command failed'}. Prior definition, login enablement, and running state restored at ${unitPath}.`,
|
|
717
|
+
unitPath,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
}
|
|
275
721
|
return { ok: true, message: `Meta-Proxy will no longer start at login. Removed ${unitPath}.` };
|
|
276
722
|
}
|
|
723
|
+
function stopAndDeleteWindowsTask(home, unitPath, label, run, wait, removeDefinition = true) {
|
|
724
|
+
const stopped = confirmWindowsTaskStopped(home, unitPath, label, run, wait);
|
|
725
|
+
if (!stopped.ok)
|
|
726
|
+
return stopped;
|
|
727
|
+
const removeTask = disableCommands('win32', unitPath, label)[0];
|
|
728
|
+
const removed = removeTask ? run(removeTask) : { ok: false, output: 'delete command unavailable' };
|
|
729
|
+
if (!removed.ok) {
|
|
730
|
+
return { ok: false, message: `Could not delete the stopped Meta-Proxy task: ${removed.output || 'command failed'}. Definition preserved at ${unitPath}.`, unitPath };
|
|
731
|
+
}
|
|
732
|
+
if (removeDefinition)
|
|
733
|
+
rmSync(unitPath, { force: true });
|
|
734
|
+
return { ok: true, message: removeDefinition ? `Meta-Proxy will no longer start at login. Removed ${unitPath}.` : 'Newly-created Meta-Proxy task removed; prior definition preserved.', unitPath };
|
|
735
|
+
}
|
|
736
|
+
function confirmWindowsTaskStopped(home, unitPath, label, run, wait) {
|
|
737
|
+
const end = stopCommand('win32', label);
|
|
738
|
+
if (!end)
|
|
739
|
+
return { ok: false, message: 'Could not construct the Windows stop command.', unitPath };
|
|
740
|
+
run(end);
|
|
741
|
+
let stopped = metaProxyServiceState('win32', home, run, label);
|
|
742
|
+
let consecutiveStopped = stopped.running === false ? 1 : 0;
|
|
743
|
+
for (let attempt = 0; attempt < 20 && stopped.managerState !== 'unknown' && consecutiveStopped < 2; attempt++) {
|
|
744
|
+
wait(100);
|
|
745
|
+
stopped = metaProxyServiceState('win32', home, run, label);
|
|
746
|
+
consecutiveStopped = stopped.running === false ? consecutiveStopped + 1 : 0;
|
|
747
|
+
}
|
|
748
|
+
if (stopped.managerState === 'unknown' || consecutiveStopped < 2) {
|
|
749
|
+
return { ok: false, message: `Could not confirm the Meta-Proxy task stopped; preserved task and ${unitPath}.`, unitPath };
|
|
750
|
+
}
|
|
751
|
+
return { ok: true, message: 'Meta-Proxy task is stably stopped.', unitPath };
|
|
752
|
+
}
|
|
277
753
|
//# sourceMappingURL=meta-proxy-service.js.map
|