machine-bridge-mcp 0.11.1 → 0.12.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/CHANGELOG.md +26 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +5 -5
- package/SECURITY.md +15 -5
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +14 -8
- package/docs/AUDIT.md +110 -0
- package/docs/ENGINEERING.md +14 -4
- package/docs/LOCAL_AUTOMATION.md +3 -3
- package/docs/LOGGING.md +5 -3
- package/docs/MANAGED_JOBS.md +5 -1
- package/docs/OPERATIONS.md +12 -4
- package/docs/PRIVACY.md +5 -3
- package/docs/RELEASING.md +1 -1
- package/docs/TESTING.md +11 -8
- package/package.json +7 -8
- package/scripts/github-release.mjs +368 -0
- package/scripts/privacy-check.mjs +66 -5
- package/scripts/release-state.mjs +10 -0
- package/scripts/syntax-check.mjs +52 -0
- package/src/local/app-automation.mjs +3 -1
- package/src/local/browser-bridge.mjs +94 -53
- package/src/local/cli.mjs +165 -81
- package/src/local/daemon-process.mjs +3 -67
- package/src/local/exclusive-file.mjs +94 -0
- package/src/local/job-runner.mjs +33 -17
- package/src/local/log.mjs +16 -1
- package/src/local/managed-jobs.mjs +187 -56
- package/src/local/process-identity.mjs +143 -0
- package/src/local/resource-operations.mjs +2 -3
- package/src/local/runtime.mjs +4 -2
- package/src/local/service-lifecycle.mjs +56 -0
- package/src/local/service.mjs +53 -25
- package/src/local/shell.mjs +23 -4
- package/src/local/state.mjs +228 -66
- package/src/worker/index.ts +1 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
|
|
4
|
+
const COMMAND_TIMEOUT_MS = 3000;
|
|
5
|
+
const COMMAND_OUTPUT_BYTES = 256 * 1024;
|
|
6
|
+
const START_TIME_TOLERANCE_MS = 15_000;
|
|
7
|
+
|
|
8
|
+
export function isPidAlive(pid) {
|
|
9
|
+
const parsed = normalizePid(pid);
|
|
10
|
+
if (!parsed) return false;
|
|
11
|
+
try {
|
|
12
|
+
process.kill(parsed, 0);
|
|
13
|
+
return true;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return error?.code === "EPERM";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function currentProcessStartTimeMs() {
|
|
20
|
+
const value = Number(performance.timeOrigin);
|
|
21
|
+
return Number.isFinite(value) && value > 0 ? value : Date.now();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function processStartTimeMs(pid) {
|
|
25
|
+
const parsed = normalizePid(pid);
|
|
26
|
+
if (!parsed) return null;
|
|
27
|
+
if (parsed === process.pid) return currentProcessStartTimeMs();
|
|
28
|
+
if (process.platform === "win32") {
|
|
29
|
+
const command = `(Get-CimInstance Win32_Process -Filter "ProcessId = ${parsed}").CreationDate.ToUniversalTime().ToString('o')`;
|
|
30
|
+
const result = runBounded("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command]);
|
|
31
|
+
return result.ok ? parseTime(result.stdout) : null;
|
|
32
|
+
}
|
|
33
|
+
const result = runBounded("ps", ["-p", String(parsed), "-o", "lstart="]);
|
|
34
|
+
return result.ok ? parseTime(result.stdout) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function processCommandLine(pid) {
|
|
38
|
+
const parsed = normalizePid(pid);
|
|
39
|
+
if (!parsed) return "";
|
|
40
|
+
if (process.platform === "win32") {
|
|
41
|
+
const command = `(Get-CimInstance Win32_Process -Filter "ProcessId = ${parsed}").CommandLine`;
|
|
42
|
+
const result = runBounded("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command]);
|
|
43
|
+
return result.ok ? result.stdout.trim() : "";
|
|
44
|
+
}
|
|
45
|
+
const result = runBounded("ps", ["-ww", "-p", String(parsed), "-o", "command="]);
|
|
46
|
+
return result.ok ? result.stdout.trim() : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function splitProcessCommandLine(value) {
|
|
50
|
+
const args = [];
|
|
51
|
+
let current = "";
|
|
52
|
+
let quote = "";
|
|
53
|
+
const text = String(value || "");
|
|
54
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
55
|
+
const character = text[index];
|
|
56
|
+
if (quote) {
|
|
57
|
+
if (character === quote) quote = "";
|
|
58
|
+
else if (character === "\\" && quote === '"' && ['"', "\\"].includes(text[index + 1])) {
|
|
59
|
+
current += text[index + 1];
|
|
60
|
+
index += 1;
|
|
61
|
+
} else current += character;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (character === '"' || character === "'") {
|
|
65
|
+
quote = character;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (/\s/.test(character)) {
|
|
69
|
+
if (current) {
|
|
70
|
+
args.push(current);
|
|
71
|
+
current = "";
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (character === "\\" && /[\s'"\\]/.test(text[index + 1] || "")) {
|
|
76
|
+
current += text[index + 1];
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
current += character;
|
|
81
|
+
}
|
|
82
|
+
if (current) args.push(current);
|
|
83
|
+
return args;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function inspectProcessInstance(owner, options = {}) {
|
|
87
|
+
const pid = normalizePid(owner?.pid);
|
|
88
|
+
if (!pid) return { current: false, alive: false, reclaimable: true, reason: "invalid_pid", pid: null };
|
|
89
|
+
const alive = (options.isAlive || isPidAlive)(pid);
|
|
90
|
+
if (!alive) return { current: false, alive: false, reclaimable: true, reason: "not_running", pid };
|
|
91
|
+
|
|
92
|
+
const now = Number.isFinite(options.now) ? Number(options.now) : Date.now();
|
|
93
|
+
const lockStartedAt = parseTime(owner?.startedAt);
|
|
94
|
+
if (!lockStartedAt) return { current: false, alive: true, reclaimable: false, reason: "invalid_lock_timestamp", pid };
|
|
95
|
+
if (lockStartedAt > now + START_TIME_TOLERANCE_MS) return { current: false, alive: true, reclaimable: false, reason: "future_lock_timestamp", pid };
|
|
96
|
+
|
|
97
|
+
const observedStart = (options.getProcessStartTime || processStartTimeMs)(pid);
|
|
98
|
+
const recordedStart = parseTime(owner?.processStartedAt);
|
|
99
|
+
if (Number.isFinite(observedStart) && observedStart > 0) {
|
|
100
|
+
if (recordedStart && Math.abs(observedStart - recordedStart) > START_TIME_TOLERANCE_MS) {
|
|
101
|
+
return { current: false, alive: true, reclaimable: true, reason: "pid_reused", pid, process_started_at: observedStart };
|
|
102
|
+
}
|
|
103
|
+
if (!recordedStart && lockStartedAt + START_TIME_TOLERANCE_MS < observedStart) {
|
|
104
|
+
return { current: false, alive: true, reclaimable: true, reason: "pid_reused", pid, process_started_at: observedStart };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const maxAgeMs = Number(options.maxAgeMs);
|
|
108
|
+
if (Number.isFinite(maxAgeMs) && maxAgeMs > 0 && now - lockStartedAt > maxAgeMs) {
|
|
109
|
+
return { current: false, alive: true, reclaimable: false, reason: "lock_expired", pid, age_ms: now - lockStartedAt };
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
current: true,
|
|
113
|
+
alive: true,
|
|
114
|
+
reclaimable: false,
|
|
115
|
+
reason: "current_process",
|
|
116
|
+
pid,
|
|
117
|
+
age_ms: Math.max(0, now - lockStartedAt),
|
|
118
|
+
process_started_at: Number.isFinite(observedStart) ? observedStart : null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function runBounded(command, args) {
|
|
123
|
+
const result = spawnSync(command, args, {
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
126
|
+
maxBuffer: COMMAND_OUTPUT_BYTES,
|
|
127
|
+
windowsHide: true,
|
|
128
|
+
env: process.platform === "win32" ? process.env : { ...process.env, LC_ALL: "C", LANG: "C" },
|
|
129
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
130
|
+
});
|
|
131
|
+
return { ok: !result.error && result.status === 0, stdout: String(result.stdout || "") };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizePid(value) {
|
|
135
|
+
const parsed = Number(value);
|
|
136
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseTime(value) {
|
|
140
|
+
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value : null;
|
|
141
|
+
const parsed = Date.parse(String(value || "").trim());
|
|
142
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
143
|
+
}
|
|
@@ -3,15 +3,14 @@ import { rm } from "node:fs/promises";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { inspectResourceFile, validateResourceName } from "./managed-jobs.mjs";
|
|
5
5
|
import { generateSshKeyPair } from "./ssh-key.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { acquireStartupLockWithWait, loadState, saveState } from "./state.mjs";
|
|
7
7
|
|
|
8
8
|
export async function generateRegisteredSshKey({ workspace, stateDir, name: rawName, targetPath, comment = "" }) {
|
|
9
9
|
const name = validateResourceName(rawName);
|
|
10
10
|
if (typeof targetPath !== "string" || !targetPath.trim()) throw new Error("SSH private key target path is required");
|
|
11
11
|
const target = resolve(targetPath);
|
|
12
12
|
const state = loadState(workspace, { stateDir });
|
|
13
|
-
const lock =
|
|
14
|
-
if (!lock.acquired) throw new Error("another state-changing operation is already running for this workspace");
|
|
13
|
+
const lock = await acquireStartupLockWithWait(state, { operation: "generate-ssh-key" });
|
|
15
14
|
let key = null;
|
|
16
15
|
try {
|
|
17
16
|
state.resources ||= {};
|
package/src/local/runtime.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
3
|
+
import { constants as fsConstants, mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
4
4
|
import { chmod, link, lstat, mkdir, open, opendir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -108,6 +108,7 @@ export class LocalRuntime {
|
|
|
108
108
|
policy: this.policy,
|
|
109
109
|
resources,
|
|
110
110
|
resourceStatePath,
|
|
111
|
+
stateRoot: browserStateRoot,
|
|
111
112
|
logger: this.logger,
|
|
112
113
|
recover: recoverJobs,
|
|
113
114
|
});
|
|
@@ -1101,7 +1102,8 @@ function assertContainedPath(root, target) {
|
|
|
1101
1102
|
}
|
|
1102
1103
|
|
|
1103
1104
|
async function readBoundedFile(filePath, maxBytes, label) {
|
|
1104
|
-
const
|
|
1105
|
+
const flags = Number(fsConstants.O_RDONLY) | Number(fsConstants.O_NOFOLLOW || 0);
|
|
1106
|
+
const handle = await open(filePath, flags);
|
|
1105
1107
|
try {
|
|
1106
1108
|
const info = await handle.stat();
|
|
1107
1109
|
if (!info.isFile()) throw new Error(`${label} is not a regular file`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export async function stopAndRemoveAutostart({
|
|
2
|
+
states = [],
|
|
3
|
+
stateRoot,
|
|
4
|
+
logger = console,
|
|
5
|
+
reason = "service uninstall",
|
|
6
|
+
stopAutostart,
|
|
7
|
+
uninstallAutostart,
|
|
8
|
+
stopWorkspaceServiceDaemon,
|
|
9
|
+
} = {}) {
|
|
10
|
+
assertFunction(stopAutostart, "stopAutostart");
|
|
11
|
+
assertFunction(uninstallAutostart, "uninstallAutostart");
|
|
12
|
+
assertFunction(stopWorkspaceServiceDaemon, "stopWorkspaceServiceDaemon");
|
|
13
|
+
|
|
14
|
+
const platformStop = await stopAutostart({ logger });
|
|
15
|
+
if (platformStop?.ok !== true) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
removed: false,
|
|
19
|
+
reason: "platform_stop_failed",
|
|
20
|
+
platform_stop: platformStop,
|
|
21
|
+
workspace_daemons: [],
|
|
22
|
+
removal: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const workspaceDaemons = [];
|
|
27
|
+
for (const state of states) {
|
|
28
|
+
const result = await stopWorkspaceServiceDaemon(state, { logger, reason });
|
|
29
|
+
workspaceDaemons.push({ workspace: state?.workspace?.path || null, ...result });
|
|
30
|
+
if (result.found && !result.ok) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
removed: false,
|
|
34
|
+
reason: "workspace_daemon_stop_failed",
|
|
35
|
+
platform_stop: platformStop,
|
|
36
|
+
workspace_daemons: workspaceDaemons,
|
|
37
|
+
removal: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const removal = await uninstallAutostart({ stateRoot, logger });
|
|
43
|
+
const removed = removal?.ok === true;
|
|
44
|
+
return {
|
|
45
|
+
ok: removed,
|
|
46
|
+
removed,
|
|
47
|
+
reason: removed ? "removed" : "platform_remove_failed",
|
|
48
|
+
platform_stop: platformStop,
|
|
49
|
+
workspace_daemons: workspaceDaemons,
|
|
50
|
+
removal,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertFunction(value, name) {
|
|
55
|
+
if (typeof value !== "function") throw new Error(`${name} is required`);
|
|
56
|
+
}
|
package/src/local/service.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, rmSync, statSync,
|
|
1
|
+
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, rmSync, statSync, writeSync } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { run } from "./shell.mjs";
|
|
5
5
|
import { ensureOwnerOnlyDir, expandHome, ownerOnlyFile } from "./state.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
7
7
|
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
8
8
|
|
|
9
9
|
const LABEL = "dev.machine-bridge-mcp.daemon";
|
|
@@ -40,17 +40,34 @@ export async function autostartStatus({ logger = console } = {}) {
|
|
|
40
40
|
|
|
41
41
|
export async function startAutostart({ logger = console } = {}) {
|
|
42
42
|
if (process.platform === "darwin") return startLaunchd(logger);
|
|
43
|
-
if (process.platform === "win32")
|
|
44
|
-
|
|
43
|
+
if (process.platform === "win32") {
|
|
44
|
+
return normalizeServiceCommandResult("schtasks", await serviceRun("schtasks", ["/Run", "/TN", WINDOWS_TASK]));
|
|
45
|
+
}
|
|
46
|
+
return normalizeServiceCommandResult("systemd", await serviceRun("systemctl", ["--user", "start", "machine-bridge-mcp.service"]));
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export async function stopAutostart({ logger = console } = {}) {
|
|
48
50
|
if (process.platform === "darwin") return stopLaunchd(logger);
|
|
49
|
-
if (process.platform === "win32")
|
|
50
|
-
|
|
51
|
+
if (process.platform === "win32") {
|
|
52
|
+
return normalizeServiceCommandResult("schtasks", await serviceRun("schtasks", ["/End", "/TN", WINDOWS_TASK]), { allowAlreadyStopped: true });
|
|
53
|
+
}
|
|
54
|
+
return normalizeServiceCommandResult("systemd", await serviceRun("systemctl", ["--user", "stop", "machine-bridge-mcp.service"]), { allowAlreadyStopped: true });
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
|
|
58
|
+
|
|
59
|
+
export function normalizeServiceCommandResult(provider, result, { allowAlreadyStopped = false } = {}) {
|
|
60
|
+
const detail = `${result?.stdout || ""}
|
|
61
|
+
${result?.stderr || ""}`;
|
|
62
|
+
const alreadyStopped = allowAlreadyStopped && /(?:not loaded|not found|does not exist|cannot find|not running|inactive)/i.test(detail);
|
|
63
|
+
return {
|
|
64
|
+
...result,
|
|
65
|
+
ok: result?.code === 0 || alreadyStopped,
|
|
66
|
+
provider,
|
|
67
|
+
already_stopped: alreadyStopped,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
export function trimAutostartLogs(stateRoot, options = {}) {
|
|
55
72
|
const root = expandHome(stateRoot);
|
|
56
73
|
const maxBytes = Number.isFinite(Number(options.maxBytes)) ? Math.max(1024, Number(options.maxBytes)) : 2 * 1024 * 1024;
|
|
@@ -233,15 +250,8 @@ function writePrivateServiceFile(file, content) {
|
|
|
233
250
|
const info = lstatSync(file);
|
|
234
251
|
if (info.isSymbolicLink() || !info.isFile()) throw new Error("autostart configuration path must be a regular non-symbolic-link file");
|
|
235
252
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
239
|
-
replaceFileSync(temporary, file);
|
|
240
|
-
ownerOnlyFile(file);
|
|
241
|
-
} catch (error) {
|
|
242
|
-
try { rmSync(temporary, { force: true }); } catch {}
|
|
243
|
-
throw error;
|
|
244
|
-
}
|
|
253
|
+
replaceFileAtomicallySync(file, content, { mode: 0o600 });
|
|
254
|
+
ownerOnlyFile(file);
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
function launchdPlistPath() {
|
|
@@ -379,17 +389,28 @@ async function installSystemd(spec, logger) {
|
|
|
379
389
|
const reload = await serviceRun("systemctl", ["--user", "daemon-reload"]);
|
|
380
390
|
const enable = await serviceRun("systemctl", ["--user", "enable", "machine-bridge-mcp.service"]);
|
|
381
391
|
const linger = await serviceRun("loginctl", ["enable-linger", os.userInfo().username]);
|
|
382
|
-
|
|
383
|
-
|
|
392
|
+
const ok = reload.code === 0 && enable.code === 0;
|
|
393
|
+
if (ok) logger.info?.("Autostart installed.");
|
|
394
|
+
else logger.warn?.("Autostart installation failed; the service definition was kept for diagnosis.");
|
|
395
|
+
return { ok, provider: "systemd", path: servicePath, reload, enable, linger };
|
|
384
396
|
}
|
|
385
397
|
|
|
386
398
|
async function uninstallSystemd(logger) {
|
|
387
|
-
await serviceRun("systemctl", ["--user", "disable", "--now", "machine-bridge-mcp.service"]);
|
|
388
399
|
const servicePath = systemdPath();
|
|
400
|
+
const disable = await serviceRun("systemctl", ["--user", "disable", "--now", "machine-bridge-mcp.service"]);
|
|
401
|
+
const activeCheck = await serviceRun("systemctl", ["--user", "is-active", "machine-bridge-mcp.service"]);
|
|
402
|
+
const active = activeCheck.code === 0;
|
|
403
|
+
if (active) {
|
|
404
|
+
logger.warn?.("systemd service is still active; its definition was not removed.");
|
|
405
|
+
return { ok: false, provider: "systemd", path: servicePath, disable, active_check: activeCheck, active: true };
|
|
406
|
+
}
|
|
389
407
|
if (existsSync(servicePath)) rmSync(servicePath, { force: true });
|
|
390
|
-
await serviceRun("systemctl", ["--user", "daemon-reload"]);
|
|
391
|
-
|
|
392
|
-
|
|
408
|
+
const reload = await serviceRun("systemctl", ["--user", "daemon-reload"]);
|
|
409
|
+
const ok = reload.code === 0 && (disable.code === 0 || /not loaded|not found|does not exist/i.test(`${disable.stdout}
|
|
410
|
+
${disable.stderr}`));
|
|
411
|
+
if (ok) logger.info?.("Autostart removed.");
|
|
412
|
+
else logger.warn?.("Autostart removal reported an error.");
|
|
413
|
+
return { ok, provider: "systemd", path: servicePath, disable, active_check: activeCheck, reload, active: false };
|
|
393
414
|
}
|
|
394
415
|
|
|
395
416
|
async function statusSystemd() {
|
|
@@ -421,14 +442,21 @@ WantedBy=default.target
|
|
|
421
442
|
async function installWindowsTask(spec, logger) {
|
|
422
443
|
const command = windowsCommand(spec);
|
|
423
444
|
const result = await serviceRun("schtasks", ["/Create", "/TN", WINDOWS_TASK, "/SC", "ONLOGON", "/TR", command, "/F"]);
|
|
424
|
-
|
|
425
|
-
|
|
445
|
+
const ok = result.code === 0;
|
|
446
|
+
if (ok) logger.info?.("Windows Scheduled Task installed for logon");
|
|
447
|
+
else logger.warn?.("Windows Scheduled Task installation failed");
|
|
448
|
+
return { ok, provider: "schtasks", task: WINDOWS_TASK, result };
|
|
426
449
|
}
|
|
427
450
|
|
|
428
451
|
async function uninstallWindowsTask(logger) {
|
|
452
|
+
const end = await serviceRun("schtasks", ["/End", "/TN", WINDOWS_TASK]);
|
|
429
453
|
const result = await serviceRun("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
|
|
430
|
-
|
|
431
|
-
|
|
454
|
+
const missing = /cannot find|not exist|not found/i.test(`${result.stdout}
|
|
455
|
+
${result.stderr}`);
|
|
456
|
+
const ok = result.code === 0 || missing;
|
|
457
|
+
if (ok) logger.info?.("Windows Scheduled Task removed");
|
|
458
|
+
else logger.warn?.("Windows Scheduled Task removal failed");
|
|
459
|
+
return { ok, provider: "schtasks", task: WINDOWS_TASK, end, result };
|
|
432
460
|
}
|
|
433
461
|
|
|
434
462
|
async function statusWindowsTask() {
|
package/src/local/shell.mjs
CHANGED
|
@@ -12,6 +12,7 @@ export function run(command, args = [], options = {}) {
|
|
|
12
12
|
env: options.env || process.env,
|
|
13
13
|
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
14
14
|
shell: false,
|
|
15
|
+
detached: process.platform !== "win32",
|
|
15
16
|
windowsHide: true,
|
|
16
17
|
});
|
|
17
18
|
let stdout = "";
|
|
@@ -26,9 +27,8 @@ export function run(command, args = [], options = {}) {
|
|
|
26
27
|
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
27
28
|
timer = setTimeout(() => {
|
|
28
29
|
timedOut = true;
|
|
29
|
-
|
|
30
|
-
killTimer = setTimeout(() =>
|
|
31
|
-
killTimer.unref?.();
|
|
30
|
+
terminateCommandTree(child, false);
|
|
31
|
+
killTimer = setTimeout(() => terminateCommandTree(child, true), 2000);
|
|
32
32
|
}, timeoutMs);
|
|
33
33
|
timer.unref?.();
|
|
34
34
|
}
|
|
@@ -36,7 +36,7 @@ export function run(command, args = [], options = {}) {
|
|
|
36
36
|
if (settled) return;
|
|
37
37
|
settled = true;
|
|
38
38
|
if (timer) clearTimeout(timer);
|
|
39
|
-
if (killTimer) clearTimeout(killTimer);
|
|
39
|
+
if (killTimer && !timedOut) clearTimeout(killTimer);
|
|
40
40
|
callback();
|
|
41
41
|
};
|
|
42
42
|
if (capture) {
|
|
@@ -73,6 +73,25 @@ export function run(command, args = [], options = {}) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
|
|
77
|
+
function terminateCommandTree(child, force) {
|
|
78
|
+
if (!child?.pid) return;
|
|
79
|
+
if (process.platform === "win32") {
|
|
80
|
+
try {
|
|
81
|
+
const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", ...(force ? ["/F"] : [])], {
|
|
82
|
+
stdio: "ignore",
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
});
|
|
85
|
+
killer.unref();
|
|
86
|
+
return;
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
90
|
+
try { process.kill(-child.pid, signal); } catch {
|
|
91
|
+
try { child.kill(signal); } catch {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
function appendLimited(current, chunk, max) {
|
|
77
96
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
|
|
78
97
|
const budget = Math.max(0, max - Buffer.byteLength(current));
|