@indigoai-us/hq-cli 5.345.177 → 5.345.179
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 +50 -0
- package/dist/command-catalog.generated.d.ts +79 -1
- package/dist/command-catalog.generated.js +96 -1
- package/dist/commands/bot-jobs.d.ts +11 -9
- package/dist/commands/bot-jobs.js +56 -21
- package/dist/commands/bot.d.ts +13 -0
- package/dist/commands/bot.js +230 -21
- package/dist/commands/cloud.d.ts +49 -0
- package/dist/commands/cloud.js +155 -15
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +15 -4
- package/dist/commands/dm-attach.js +15 -3
- package/dist/commands/integrations-core.d.ts +2 -0
- package/dist/commands/integrations-core.js +1 -0
- package/dist/commands/library-add.d.ts +9 -0
- package/dist/commands/library-add.js +19 -2
- package/dist/commands/run.js +9 -1
- package/dist/commands/sources.d.ts +89 -0
- package/dist/commands/sources.js +363 -1
- package/dist/lib/bot/config.d.ts +2 -0
- package/dist/lib/bot/fleet-default-flag.d.ts +31 -0
- package/dist/lib/bot/fleet-default-flag.js +127 -0
- package/dist/lib/bot/fleet-local.d.ts +30 -1
- package/dist/lib/bot/fleet-local.js +86 -11
- package/dist/lib/bot/index.d.ts +1 -0
- package/dist/lib/bot/index.js +1 -0
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
- package/dist/lib/bot/local-config.js +1 -1
- package/dist/lib/bot/local-jobs.d.ts +40 -4
- package/dist/lib/bot/local-jobs.js +72 -5
- package/dist/lib/bot/run.d.ts +15 -0
- package/dist/lib/bot/run.js +61 -0
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +7 -2
- package/dist/lib/daemon/config.d.ts +2 -1
- package/dist/lib/daemon/config.js +4 -3
- package/dist/lib/daemon/handoff.d.ts +4 -0
- package/dist/lib/daemon/handoff.js +6 -2
- package/dist/lib/daemon/install.d.ts +70 -2
- package/dist/lib/daemon/install.js +185 -27
- package/dist/lib/daemon/legacy.d.ts +9 -3
- package/dist/lib/daemon/legacy.js +11 -4
- package/dist/lib/daemon/migrate.js +1 -1
- package/dist/lib/daemon/run.d.ts +3 -2
- package/dist/lib/daemon/run.js +10 -6
- package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
- package/dist/lib/doctor/checks/anywhere.js +35 -6
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
- package/dist/lib/hot-channels/adapters/attio.js +374 -0
- package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
- package/dist/lib/hot-channels/adapters/gmail.js +413 -0
- package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
- package/dist/lib/hot-channels/adapters/index.js +39 -0
- package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
- package/dist/lib/hot-channels/adapters/linear.js +321 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
- package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
- package/dist/lib/hot-channels/adapters/meetings.js +523 -0
- package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
- package/dist/lib/hot-channels/adapters/slack.js +277 -0
- package/dist/lib/hot-channels/alert.d.ts +78 -0
- package/dist/lib/hot-channels/alert.js +179 -0
- package/dist/lib/hot-channels/audience.d.ts +15 -0
- package/dist/lib/hot-channels/audience.js +25 -0
- package/dist/lib/hot-channels/contract.d.ts +140 -0
- package/dist/lib/hot-channels/contract.js +20 -0
- package/dist/lib/hot-channels/cursors.d.ts +41 -0
- package/dist/lib/hot-channels/cursors.js +84 -0
- package/dist/lib/hot-channels/index.d.ts +10 -0
- package/dist/lib/hot-channels/index.js +10 -0
- package/dist/lib/hot-channels/landing.d.ts +83 -0
- package/dist/lib/hot-channels/landing.js +175 -0
- package/dist/lib/hot-channels/ledger.d.ts +101 -0
- package/dist/lib/hot-channels/ledger.js +186 -0
- package/dist/lib/hot-channels/loop.d.ts +74 -0
- package/dist/lib/hot-channels/loop.js +219 -0
- package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
- package/dist/lib/hot-channels/registry.d.ts +28 -0
- package/dist/lib/hot-channels/registry.js +121 -0
- package/dist/lib/hot-channels/run.d.ts +137 -0
- package/dist/lib/hot-channels/run.js +324 -0
- package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
- package/dist/lib/hot-channels/signals-pass.js +581 -0
- package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
- package/dist/lib/hot-channels/sync-lock.js +224 -0
- package/dist/lib/install/hook-target.d.ts +6 -2
- package/dist/lib/install/hook-target.js +9 -4
- package/dist/lib/lanes/loop.d.ts +10 -0
- package/dist/lib/lanes/loop.js +18 -1
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +26 -6
- package/dist/run/hq-plugin.d.ts +3 -0
- package/dist/run/hq-plugin.js +18 -0
- package/package.json +7 -7
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -134,6 +134,21 @@ export interface BotRunDeps {
|
|
|
134
134
|
inboxPollBackoffEnabled?: () => Promise<boolean>;
|
|
135
135
|
/** Default-off gate for the per-job quiet-success option. */
|
|
136
136
|
jobQuietOptionEnabled?: () => boolean | Promise<boolean>;
|
|
137
|
+
/**
|
|
138
|
+
* The built-in hot-signal-sync job (jobs/hot-signal-sync.ts), run in-process
|
|
139
|
+
* with no model turn. Unset = a fired run of that job fails with a plain
|
|
140
|
+
* report instead of handing the instruction to the model.
|
|
141
|
+
*/
|
|
142
|
+
hotSignalSync?: (input: {
|
|
143
|
+
company: string;
|
|
144
|
+
catchUp: boolean;
|
|
145
|
+
now: () => Date;
|
|
146
|
+
/** The signals-failure alert to the owner, sent even when the job is quiet (US-012). */
|
|
147
|
+
sendAlert: (text: string) => Promise<void>;
|
|
148
|
+
}) => Promise<{
|
|
149
|
+
ok: boolean;
|
|
150
|
+
text: string;
|
|
151
|
+
}>;
|
|
137
152
|
heartbeatIntervalMs?: number;
|
|
138
153
|
/** Override the rendered system prompt (tests). */
|
|
139
154
|
systemPromptOverride?: string;
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -82,6 +82,7 @@ import { isAgentSender, isDmItem, isMetaNonReply, resolveReplyTarget } from "./r
|
|
|
82
82
|
import { resolveRoomMentionsForBot, roomPostHasMentionTokens, } from "./room-mentions.js";
|
|
83
83
|
import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOutput, machineTimeZone, planJobRuns, stripJobHeader, SUPERSEDED_DISPOSITION } from "./job-run.js";
|
|
84
84
|
import { botJobQuietOptionEnabled } from "./job-quiet-flag.js";
|
|
85
|
+
import { currentHotSignalSyncCompany, hotSignalSyncJobCompany, removeStaleHotSignalSyncJobs } from "./jobs/hot-signal-sync.js";
|
|
85
86
|
import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
|
|
86
87
|
import { SIGN_IN_CHECK_FRESH_MS, SIGN_IN_CHECK_MS, SIGN_IN_RETRY_MS, signInNeededText, signedOutExcerpt, } from "./runtime-sign-in.js";
|
|
87
88
|
import { checkRuntimeSignIn } from "./runtime-sign-in-check.js";
|
|
@@ -1932,6 +1933,66 @@ export async function runBot(deps) {
|
|
|
1932
1933
|
const markReported = (status) => {
|
|
1933
1934
|
patchInflight(dir, id, { jobCompleted: status });
|
|
1934
1935
|
};
|
|
1936
|
+
// The built-in hot-signal-sync job runs here, in the bot, with no model
|
|
1937
|
+
// turn. It is marked processed as soon as the sync finishes, so a report
|
|
1938
|
+
// that fails to post (or a redelivered item) never syncs a second time.
|
|
1939
|
+
const syncCompany = hotSignalSyncJobCompany(job.context);
|
|
1940
|
+
if (syncCompany) {
|
|
1941
|
+
// The job's stored company is checked against the bot's current config
|
|
1942
|
+
// (read fresh from disk) every time it fires. A company the bot no longer
|
|
1943
|
+
// serves is never synced: the run is skipped and the job removed.
|
|
1944
|
+
const currentCompany = currentHotSignalSyncCompany(readBotConfig(dir) ?? config);
|
|
1945
|
+
if (currentCompany !== syncCompany) {
|
|
1946
|
+
let removedNote = "";
|
|
1947
|
+
try {
|
|
1948
|
+
removeStaleHotSignalSyncJobs(dir, currentCompany);
|
|
1949
|
+
removedNote = ` Removed job ${jobId}.`;
|
|
1950
|
+
}
|
|
1951
|
+
catch (err) {
|
|
1952
|
+
removedNote = ` Could not remove job ${jobId}: ${err instanceof Error ? err.message : String(err)}`;
|
|
1953
|
+
}
|
|
1954
|
+
const text = `Skipped the hot-channel sync for ${syncCompany}: this bot no longer syncs that company (current: ${currentCompany ?? "none"}). Nothing was written.${removedNote}`;
|
|
1955
|
+
markProcessed(dir, id, now().toISOString());
|
|
1956
|
+
try {
|
|
1957
|
+
await sendToPerson(text);
|
|
1958
|
+
}
|
|
1959
|
+
catch (err) {
|
|
1960
|
+
log("warn", `report for run ${id} of job ${jobId} was not delivered: ${err instanceof Error ? err.message : String(err)}`);
|
|
1961
|
+
}
|
|
1962
|
+
await completeJobRun(jobId, id, { status: "skipped-precondition", output: jobRunOutput(text) });
|
|
1963
|
+
markReported("skipped-precondition");
|
|
1964
|
+
await api.ackInbox(config.agentUid, id);
|
|
1965
|
+
clearInflight(dir, id);
|
|
1966
|
+
attempts.delete(id);
|
|
1967
|
+
setStatus({ lastActivityAt: now().toISOString() });
|
|
1968
|
+
log("info", `hot-signal-sync job ${jobId} run ${id} skipped: company ${syncCompany} no longer served (current: ${currentCompany ?? "none"})`);
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
const result = deps.hotSignalSync
|
|
1972
|
+
? await deps.hotSignalSync({ company: syncCompany, catchUp, now, sendAlert: (text) => sendToPerson(text) })
|
|
1973
|
+
: { ok: false, text: `This bot cannot run the hot-channel sync for ${syncCompany}.` };
|
|
1974
|
+
markProcessed(dir, id, now().toISOString());
|
|
1975
|
+
const status = result.ok ? "succeeded" : "failed";
|
|
1976
|
+
if (!(quietJob && result.ok)) {
|
|
1977
|
+
try {
|
|
1978
|
+
await sendToPerson(result.text);
|
|
1979
|
+
}
|
|
1980
|
+
catch (err) {
|
|
1981
|
+
log("warn", `report for run ${id} of job ${jobId} was not delivered: ${err instanceof Error ? err.message : String(err)}`);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
await completeJobRun(jobId, id, { status, output: jobRunOutput(result.text) });
|
|
1985
|
+
markReported(status);
|
|
1986
|
+
await api.ackInbox(config.agentUid, id);
|
|
1987
|
+
clearInflight(dir, id);
|
|
1988
|
+
attempts.delete(id);
|
|
1989
|
+
setStatus({
|
|
1990
|
+
...(!(quietJob && result.ok) ? { repliesSent: (readStatus()?.repliesSent ?? 0) + 1 } : {}),
|
|
1991
|
+
lastActivityAt: now().toISOString(),
|
|
1992
|
+
});
|
|
1993
|
+
log("info", `hot-signal-sync job ${jobId} run ${id} ${status} for ${syncCompany}${catchUp ? " (catch-up)" : ""}`);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1935
1996
|
const { outcome, poster } = await runTurnWithProgress({
|
|
1936
1997
|
marker: { messageId: id, target: markerTarget, jobId, jobHeader: header, agentUid: config.agentUid },
|
|
1937
1998
|
prompt: buildJobTurnPrompt(job, { now: now(), timeZone }),
|
|
@@ -21,6 +21,8 @@ export declare function isServiceName(value: unknown): value is ServiceName;
|
|
|
21
21
|
export declare const SERVICE_DESCRIPTIONS: Record<ServiceName, string>;
|
|
22
22
|
/** The services each machine type runs without being asked. */
|
|
23
23
|
export declare const DEFAULT_SERVICES: Record<MachineType, readonly ServiceName[]>;
|
|
24
|
+
/** Services another enabled service turns on; only an explicit disable keeps them off. */
|
|
25
|
+
export declare const IMPLIED_SERVICES: Partial<Record<ServiceName, ServiceName>>;
|
|
24
26
|
/** Defaults plus this host's overrides, in catalog order. Unknown names are ignored. */
|
|
25
27
|
export declare function enabledServices(machineType: MachineType, overrides: {
|
|
26
28
|
enable: readonly string[];
|
|
@@ -80,12 +80,17 @@ export const DEFAULT_SERVICES = {
|
|
|
80
80
|
"agent-box": ["mesh", "heartbeat", "inbox", "search-index", "updater", "housekeeping"],
|
|
81
81
|
"agent-kit": ["heartbeat", "inbox", "updater", "housekeeping"],
|
|
82
82
|
};
|
|
83
|
+
/** Services another enabled service turns on; only an explicit disable keeps them off. */
|
|
84
|
+
export const IMPLIED_SERVICES = { mesh: "sync-core" };
|
|
83
85
|
/** Defaults plus this host's overrides, in catalog order. Unknown names are ignored. */
|
|
84
86
|
export function enabledServices(machineType, overrides) {
|
|
85
87
|
const defaults = DEFAULT_SERVICES[machineType];
|
|
86
88
|
const wanted = (s) => (defaults.includes(s) || overrides.enable.includes(s)) && !overrides.disable.includes(s);
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
+
const implied = (s) => {
|
|
90
|
+
const by = IMPLIED_SERVICES[s];
|
|
91
|
+
return by !== undefined && wanted(by) && !overrides.disable.includes(s);
|
|
92
|
+
};
|
|
93
|
+
return SERVICE_NAMES.filter((s) => wanted(s) || implied(s));
|
|
89
94
|
}
|
|
90
95
|
export const SYNC_CORE_RECONCILE_UNIT = "sync-core";
|
|
91
96
|
export const SYNC_CORE_RECONCILE_INTERVAL_SEC = 600;
|
|
@@ -29,7 +29,8 @@ export declare function readDaemonConfig(file: string): DaemonConfig;
|
|
|
29
29
|
export declare function writeDaemonConfig(file: string, config: DaemonConfig): void;
|
|
30
30
|
/**
|
|
31
31
|
* Turn services on or off for this host, relative to the machine type's
|
|
32
|
-
* defaults: only differences from the defaults are stored.
|
|
32
|
+
* defaults: only differences from the defaults are stored. A disable is also
|
|
33
|
+
* stored for a service another service can turn on, so it stays off.
|
|
33
34
|
*/
|
|
34
35
|
export declare function applyServiceToggle(config: DaemonConfig, action: "enable" | "disable", services: readonly string[], defaults: readonly ServiceName[]): DaemonConfig;
|
|
35
36
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { valid as validSemver } from "semver";
|
|
10
|
-
import { isServiceName } from "./catalog.js";
|
|
10
|
+
import { IMPLIED_SERVICES, isServiceName } from "./catalog.js";
|
|
11
11
|
import { isMachineType } from "./machine-type.js";
|
|
12
12
|
import { ensurePrivateDir } from "./paths.js";
|
|
13
13
|
export function effectiveSyncOwner(config, machineType) {
|
|
@@ -81,7 +81,8 @@ export function writeDaemonConfig(file, config) {
|
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
83
|
* Turn services on or off for this host, relative to the machine type's
|
|
84
|
-
* defaults: only differences from the defaults are stored.
|
|
84
|
+
* defaults: only differences from the defaults are stored. A disable is also
|
|
85
|
+
* stored for a service another service can turn on, so it stays off.
|
|
85
86
|
*/
|
|
86
87
|
export function applyServiceToggle(config, action, services, defaults) {
|
|
87
88
|
for (const s of services) {
|
|
@@ -99,7 +100,7 @@ export function applyServiceToggle(config, action, services, defaults) {
|
|
|
99
100
|
}
|
|
100
101
|
else {
|
|
101
102
|
enable.delete(s);
|
|
102
|
-
if (isDefault)
|
|
103
|
+
if (isDefault || (isServiceName(s) && IMPLIED_SERVICES[s] !== undefined))
|
|
103
104
|
disable.add(s);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -32,7 +32,11 @@ export interface DaemonUnitHost {
|
|
|
32
32
|
platform: NodeJS.Platform;
|
|
33
33
|
home: string;
|
|
34
34
|
systemUnitDir?: string;
|
|
35
|
+
/** This user's own Scheduled Task name (daemonWindowsTaskName); another user's task on the PC does not count. */
|
|
36
|
+
windowsTaskName: string;
|
|
35
37
|
windowsTaskExists: (task: string) => boolean;
|
|
38
|
+
/** The shared task an older install registered runs as this user (it still counts until install moves it). */
|
|
39
|
+
windowsLegacyTaskIsMine?: () => boolean;
|
|
36
40
|
}
|
|
37
41
|
/** The `hq daemon` OS unit is installed on this machine (for this user, or as the box's system unit). */
|
|
38
42
|
export declare function daemonUnitInstalled(host: DaemonUnitHost): boolean;
|
|
@@ -10,7 +10,8 @@ import * as os from "node:os";
|
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { DEFAULT_SERVICES, enabledServices, isServiceName } from "./catalog.js";
|
|
12
12
|
import { readDaemonConfig, writeDaemonConfig } from "./config.js";
|
|
13
|
-
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT,
|
|
13
|
+
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, SYSTEM_UNIT_DIR, currentWindowsIdentity, legacyWindowsTask, windowsTaskName } from "./install.js";
|
|
14
|
+
import { defaultRunCommand } from "../service-manager/launchd.js";
|
|
14
15
|
import { isDaemonProcess, runningDaemonPid } from "./lock.js";
|
|
15
16
|
import { detectMachineType } from "./machine-type.js";
|
|
16
17
|
import { daemonPaths } from "./paths.js";
|
|
@@ -96,7 +97,7 @@ export function daemonUnitInstalled(host) {
|
|
|
96
97
|
case "darwin":
|
|
97
98
|
return fs.existsSync(path.join(host.home, "Library", "LaunchAgents", `${DAEMON_LAUNCHD_LABEL}.plist`));
|
|
98
99
|
case "win32":
|
|
99
|
-
return host.windowsTaskExists(
|
|
100
|
+
return host.windowsTaskExists(host.windowsTaskName) || (host.windowsLegacyTaskIsMine?.() ?? false);
|
|
100
101
|
default:
|
|
101
102
|
return (fs.existsSync(path.join(host.home, ".config", "systemd", "user", DAEMON_SYSTEMD_UNIT)) ||
|
|
102
103
|
fs.existsSync(path.join(host.systemUnitDir ?? SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)));
|
|
@@ -142,9 +143,12 @@ export function handOffInstallHere(services, env = process.env, opts = {}) {
|
|
|
142
143
|
if (opts.ifStale && services.every((service) => config.disable.includes(service))) {
|
|
143
144
|
return `hq daemon has ${services.join(", ")} disabled, so the older service will not be installed.`;
|
|
144
145
|
}
|
|
146
|
+
const identity = process.platform === "win32" ? currentWindowsIdentity() : undefined;
|
|
145
147
|
const installed = daemonUnitInstalled({
|
|
146
148
|
platform: process.platform,
|
|
147
149
|
home,
|
|
150
|
+
windowsTaskName: identity ? windowsTaskName(identity) : "",
|
|
151
|
+
windowsLegacyTaskIsMine: () => identity !== undefined && legacyWindowsTask(defaultRunCommand, identity).state === "mine",
|
|
148
152
|
windowsTaskExists: (task) => spawnSync("schtasks.exe", ["/Query", "/TN", task], { encoding: "utf8", windowsHide: true }).status === 0,
|
|
149
153
|
}) || hostedDaemonRunning(paths);
|
|
150
154
|
if (!installed) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Linux, per-user: systemd user unit hq-daemon.service
|
|
8
8
|
* - Linux, system (outposts and agent boxes): /etc/systemd/system/hq-daemon.service
|
|
9
9
|
* running as the box's login user; needs root
|
|
10
|
-
* - Windows: per-user logon Scheduled Task ai.getindigo.hq-daemon
|
|
10
|
+
* - Windows: per-user logon Scheduled Task ai.getindigo.hq-daemon-<user SID>
|
|
11
11
|
* - anything else: no unit; the caller prints the command to run
|
|
12
12
|
*
|
|
13
13
|
* The unit never names services. `hq daemon run` reads the machine type and
|
|
@@ -20,8 +20,34 @@ import { type PidLockRecord } from "../mesh/live/daemon/pid-lock.js";
|
|
|
20
20
|
import type { MachineType } from "./machine-type.js";
|
|
21
21
|
export declare const DAEMON_LAUNCHD_LABEL = "ai.getindigo.hq-daemon";
|
|
22
22
|
export declare const DAEMON_SYSTEMD_UNIT = "hq-daemon.service";
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The one machine-wide task name older installs registered on Windows. Two
|
|
25
|
+
* users on one PC overwrote each other's daemon under it, so each user now
|
|
26
|
+
* gets their own name (daemonWindowsTaskName). Install moves the current
|
|
27
|
+
* user's task off this name; another user's task under it is left alone.
|
|
28
|
+
*/
|
|
29
|
+
export declare const LEGACY_DAEMON_WINDOWS_TASK = "ai.getindigo.hq-daemon";
|
|
24
30
|
export declare const SYSTEM_UNIT_DIR = "/etc/systemd/system";
|
|
31
|
+
/**
|
|
32
|
+
* The Windows task name for one user: the shared prefix plus the user's SID,
|
|
33
|
+
* or the `DOMAIN\user` name with characters a task name cannot hold replaced
|
|
34
|
+
* when the SID could not be read. Never the bare legacy name.
|
|
35
|
+
*/
|
|
36
|
+
export declare function daemonWindowsTaskName(who: {
|
|
37
|
+
sid?: string;
|
|
38
|
+
user: string;
|
|
39
|
+
}): string;
|
|
40
|
+
/** The current Windows user's SID from `whoami /user`, or undefined when it cannot be read. */
|
|
41
|
+
export declare function readWindowsSid(run: RunCommand): string | undefined;
|
|
42
|
+
/** The per-user task name for the user running this process. */
|
|
43
|
+
export declare function currentDaemonWindowsTaskName(run: RunCommand, user: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* `DOMAIN\user` for the current account. Same answer as the mesh installer's
|
|
46
|
+
* resolveWindowsUser, kept here so lookups do not load the mesh installer.
|
|
47
|
+
*/
|
|
48
|
+
export declare function currentWindowsUser(env?: NodeJS.ProcessEnv): string;
|
|
49
|
+
/** The current user as a DaemonHost names them on Windows. */
|
|
50
|
+
export declare function currentWindowsIdentity(run?: RunCommand, env?: NodeJS.ProcessEnv): Pick<DaemonHost, "windowsSid" | "windowsUser">;
|
|
25
51
|
export type InstallScope = "user" | "system";
|
|
26
52
|
export interface DaemonHost {
|
|
27
53
|
platform: NodeJS.Platform;
|
|
@@ -34,6 +60,8 @@ export interface DaemonHost {
|
|
|
34
60
|
isRoot: boolean;
|
|
35
61
|
/** `DOMAIN\user` the Windows task runs as. */
|
|
36
62
|
windowsUser: string;
|
|
63
|
+
/** That user's SID, when `whoami /user` could read it. Names the per-user task. */
|
|
64
|
+
windowsSid?: string;
|
|
37
65
|
/** conhost.exe path when `--headless` is supported, else undefined. */
|
|
38
66
|
windowsHeadlessLauncher: string | undefined;
|
|
39
67
|
/** Where system units are written (tests point this at a temp dir). */
|
|
@@ -70,6 +98,8 @@ export interface DaemonUnitResult {
|
|
|
70
98
|
startPending?: true;
|
|
71
99
|
/** Windows: an older daemon may still be running; HQ could not verify it, so did not stop it. */
|
|
72
100
|
warning?: string;
|
|
101
|
+
/** Windows: what happened to the shared task from an older install, when there is one. */
|
|
102
|
+
notice?: string;
|
|
73
103
|
}
|
|
74
104
|
export interface InstallDeps {
|
|
75
105
|
run: RunCommand;
|
|
@@ -93,6 +123,43 @@ export declare function daemonRootRefusal(isRoot: boolean, machineType: MachineT
|
|
|
93
123
|
/** Boxes run the daemon as a system unit; everything else per user. */
|
|
94
124
|
export declare function installScopeFor(machineType: MachineType): InstallScope;
|
|
95
125
|
export declare function planDaemonUnit(host: DaemonHost, options: InstallOptions): DaemonUnitPlan;
|
|
126
|
+
/**
|
|
127
|
+
* The registered per-user task names for `host`: the current name, and the
|
|
128
|
+
* user-name form an install used when it could not read the SID. The fallback
|
|
129
|
+
* name is only returned when its principal is this user.
|
|
130
|
+
*/
|
|
131
|
+
export declare function windowsUserTaskNames(run: RunCommand, host: Pick<DaemonHost, "windowsSid" | "windowsUser">): string[];
|
|
132
|
+
/** The per-user task name for `host`. */
|
|
133
|
+
export declare function windowsTaskName(host: Pick<DaemonHost, "windowsSid" | "windowsUser">): string;
|
|
134
|
+
/**
|
|
135
|
+
* Whether two Windows account names are the same user, ignoring case. Both
|
|
136
|
+
* names must have the same form: `DOMAIN\user` matches only the same
|
|
137
|
+
* `DOMAIN\user`, and a bare `user` only the same bare `user`. A bare name is
|
|
138
|
+
* never matched against a domain name, because `bob` could be `PC\bob` or
|
|
139
|
+
* `CORP\bob`, and a wrong match would end and delete another user's task.
|
|
140
|
+
*/
|
|
141
|
+
export declare function sameWindowsUser(a: string, b: string): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* The shared task an older install registered, and whose it is. "mine" means
|
|
144
|
+
* its principal is the current user, so HQ may move it. "other" means another
|
|
145
|
+
* user's, or a principal HQ cannot match to the current user; "unreadable"
|
|
146
|
+
* means schtasks refused to show it. Both are left alone.
|
|
147
|
+
*/
|
|
148
|
+
export type LegacyWindowsTask = {
|
|
149
|
+
state: "none";
|
|
150
|
+
} | {
|
|
151
|
+
state: "mine";
|
|
152
|
+
owner: string;
|
|
153
|
+
} | {
|
|
154
|
+
state: "other";
|
|
155
|
+
owner: string;
|
|
156
|
+
} | {
|
|
157
|
+
state: "unreadable";
|
|
158
|
+
reason: string;
|
|
159
|
+
};
|
|
160
|
+
export declare function legacyWindowsTask(run: RunCommand, host: Pick<DaemonHost, "windowsSid" | "windowsUser">): LegacyWindowsTask;
|
|
161
|
+
/** Whose task `task` is, read from `schtasks /Query /XML`. */
|
|
162
|
+
export declare function classifyWindowsTask(run: RunCommand, task: string, host: Pick<DaemonHost, "windowsSid" | "windowsUser">): LegacyWindowsTask;
|
|
96
163
|
/** What to tell the user when HQ did not stop pid `pid` because it could not verify it. */
|
|
97
164
|
export declare function oldDaemonWarning(pid: number, reason: string): string;
|
|
98
165
|
export declare function installDaemonUnit(host: DaemonHost, options: InstallOptions, deps: InstallDeps): DaemonUnitResult;
|
|
@@ -127,5 +194,6 @@ export declare function uninstallDaemonUnit(host: DaemonHost, options: InstallOp
|
|
|
127
194
|
unitPath: string;
|
|
128
195
|
removed: boolean;
|
|
129
196
|
warning?: string;
|
|
197
|
+
notice?: string;
|
|
130
198
|
};
|
|
131
199
|
//# sourceMappingURL=install.d.ts.map
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Linux, per-user: systemd user unit hq-daemon.service
|
|
8
8
|
* - Linux, system (outposts and agent boxes): /etc/systemd/system/hq-daemon.service
|
|
9
9
|
* running as the box's login user; needs root
|
|
10
|
-
* - Windows: per-user logon Scheduled Task ai.getindigo.hq-daemon
|
|
10
|
+
* - Windows: per-user logon Scheduled Task ai.getindigo.hq-daemon-<user SID>
|
|
11
11
|
* - anything else: no unit; the caller prints the command to run
|
|
12
12
|
*
|
|
13
13
|
* The unit never names services. `hq daemon run` reads the machine type and
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* never touch launchctl, systemctl or schtasks.
|
|
17
17
|
*/
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
|
+
import * as os from "node:os";
|
|
19
20
|
import * as path from "node:path";
|
|
20
21
|
import { activateLaunchd, blockingSleep, deactivateLaunchd, defaultRunCommand, installLaunchdPlist, launchdIsLoaded, launchdPlistPath, renderLaunchdPlist, } from "../service-manager/launchd.js";
|
|
21
22
|
import { activateSystemd, deactivateSystemd, renderSystemdUserUnit, systemdUnitPath, } from "../service-manager/systemd.js";
|
|
@@ -26,8 +27,58 @@ import { liveDaemonLock, verifyDaemonForKill } from "./lock.js";
|
|
|
26
27
|
import { daemonPaths } from "./paths.js";
|
|
27
28
|
export const DAEMON_LAUNCHD_LABEL = "ai.getindigo.hq-daemon";
|
|
28
29
|
export const DAEMON_SYSTEMD_UNIT = "hq-daemon.service";
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The one machine-wide task name older installs registered on Windows. Two
|
|
32
|
+
* users on one PC overwrote each other's daemon under it, so each user now
|
|
33
|
+
* gets their own name (daemonWindowsTaskName). Install moves the current
|
|
34
|
+
* user's task off this name; another user's task under it is left alone.
|
|
35
|
+
*/
|
|
36
|
+
export const LEGACY_DAEMON_WINDOWS_TASK = "ai.getindigo.hq-daemon";
|
|
30
37
|
export const SYSTEM_UNIT_DIR = "/etc/systemd/system";
|
|
38
|
+
/**
|
|
39
|
+
* The Windows task name for one user: the shared prefix plus the user's SID,
|
|
40
|
+
* or the `DOMAIN\user` name with characters a task name cannot hold replaced
|
|
41
|
+
* when the SID could not be read. Never the bare legacy name.
|
|
42
|
+
*/
|
|
43
|
+
export function daemonWindowsTaskName(who) {
|
|
44
|
+
const sid = who.sid?.trim();
|
|
45
|
+
if (sid)
|
|
46
|
+
return `${LEGACY_DAEMON_WINDOWS_TASK}-${sid}`;
|
|
47
|
+
const user = who.user.trim().replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
48
|
+
return `${LEGACY_DAEMON_WINDOWS_TASK}-${user || "user"}`;
|
|
49
|
+
}
|
|
50
|
+
/** The current Windows user's SID from `whoami /user`, or undefined when it cannot be read. */
|
|
51
|
+
export function readWindowsSid(run) {
|
|
52
|
+
const r = run("whoami.exe", ["/user", "/FO", "CSV", "/NH"]);
|
|
53
|
+
if (r.status !== 0)
|
|
54
|
+
return undefined;
|
|
55
|
+
return /S-1-\d+(?:-\d+)+/.exec(r.stdout)?.[0];
|
|
56
|
+
}
|
|
57
|
+
/** The per-user task name for the user running this process. */
|
|
58
|
+
export function currentDaemonWindowsTaskName(run, user) {
|
|
59
|
+
return daemonWindowsTaskName({ sid: readWindowsSid(run), user });
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `DOMAIN\user` for the current account. Same answer as the mesh installer's
|
|
63
|
+
* resolveWindowsUser, kept here so lookups do not load the mesh installer.
|
|
64
|
+
*/
|
|
65
|
+
export function currentWindowsUser(env = process.env) {
|
|
66
|
+
let user = env.USERNAME?.trim() ?? "";
|
|
67
|
+
if (!user) {
|
|
68
|
+
try {
|
|
69
|
+
user = os.userInfo().username;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
user = "";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const domain = env.USERDOMAIN?.trim();
|
|
76
|
+
return domain ? `${domain}\\${user}` : user;
|
|
77
|
+
}
|
|
78
|
+
/** The current user as a DaemonHost names them on Windows. */
|
|
79
|
+
export function currentWindowsIdentity(run = defaultRunCommand, env = process.env) {
|
|
80
|
+
return { windowsUser: currentWindowsUser(env), windowsSid: readWindowsSid(run) };
|
|
81
|
+
}
|
|
31
82
|
/** How long install waits for the Windows task to start a daemon process. */
|
|
32
83
|
export const WINDOWS_START_WAIT_MS = 10_000;
|
|
33
84
|
const WINDOWS_START_POLL_MS = 500;
|
|
@@ -128,7 +179,7 @@ function renderWindowsTaskXml(host) {
|
|
|
128
179
|
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
129
180
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
130
181
|
<RegistrationInfo>
|
|
131
|
-
<URI>\\${
|
|
182
|
+
<URI>\\${windowsTaskName(host)}</URI>
|
|
132
183
|
<Description>HQ daemon: runs this machine's HQ background services</Description>
|
|
133
184
|
</RegistrationInfo>
|
|
134
185
|
<Triggers>
|
|
@@ -204,7 +255,7 @@ export function planDaemonUnit(host, options) {
|
|
|
204
255
|
if (host.platform === "win32") {
|
|
205
256
|
return {
|
|
206
257
|
kind: "schtasks",
|
|
207
|
-
unitPath: path.join(host.daemonDir, `${
|
|
258
|
+
unitPath: path.join(host.daemonDir, `${LEGACY_DAEMON_WINDOWS_TASK}.xml`),
|
|
208
259
|
rendered: renderWindowsTaskXml(host),
|
|
209
260
|
};
|
|
210
261
|
}
|
|
@@ -224,8 +275,9 @@ function commandError(what, r) {
|
|
|
224
275
|
* immediately before the kill and killed only when it is verifiably the
|
|
225
276
|
* daemon. Returns a warning when a process was left running unverified.
|
|
226
277
|
*/
|
|
227
|
-
function endWindowsDaemon(run, owner) {
|
|
228
|
-
|
|
278
|
+
function endWindowsDaemon(run, owner, tasks) {
|
|
279
|
+
for (const task of tasks)
|
|
280
|
+
run("schtasks.exe", ["/End", "/TN", task]);
|
|
229
281
|
if (!owner || owner.pid === process.pid)
|
|
230
282
|
return undefined;
|
|
231
283
|
const check = verifyDaemonForKill(owner, run);
|
|
@@ -237,6 +289,79 @@ function endWindowsDaemon(run, owner) {
|
|
|
237
289
|
return undefined;
|
|
238
290
|
return oldDaemonWarning(owner.pid, check.reason);
|
|
239
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* The registered per-user task names for `host`: the current name, and the
|
|
294
|
+
* user-name form an install used when it could not read the SID. The fallback
|
|
295
|
+
* name is only returned when its principal is this user.
|
|
296
|
+
*/
|
|
297
|
+
export function windowsUserTaskNames(run, host) {
|
|
298
|
+
const names = [];
|
|
299
|
+
const current = windowsTaskName(host);
|
|
300
|
+
if (run("schtasks.exe", ["/Query", "/TN", current]).status === 0)
|
|
301
|
+
names.push(current);
|
|
302
|
+
const byUser = daemonWindowsTaskName({ user: host.windowsUser });
|
|
303
|
+
if (byUser !== current && classifyWindowsTask(run, byUser, host).state === "mine")
|
|
304
|
+
names.push(byUser);
|
|
305
|
+
return names;
|
|
306
|
+
}
|
|
307
|
+
/** The per-user task name for `host`. */
|
|
308
|
+
export function windowsTaskName(host) {
|
|
309
|
+
return daemonWindowsTaskName({ sid: host.windowsSid, user: host.windowsUser });
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Whether two Windows account names are the same user, ignoring case. Both
|
|
313
|
+
* names must have the same form: `DOMAIN\user` matches only the same
|
|
314
|
+
* `DOMAIN\user`, and a bare `user` only the same bare `user`. A bare name is
|
|
315
|
+
* never matched against a domain name, because `bob` could be `PC\bob` or
|
|
316
|
+
* `CORP\bob`, and a wrong match would end and delete another user's task.
|
|
317
|
+
*/
|
|
318
|
+
export function sameWindowsUser(a, b) {
|
|
319
|
+
const left = a.trim().toLowerCase();
|
|
320
|
+
const right = b.trim().toLowerCase();
|
|
321
|
+
const leftBare = (left.split("\\").pop() ?? "").trim();
|
|
322
|
+
const rightBare = (right.split("\\").pop() ?? "").trim();
|
|
323
|
+
if (!leftBare || !rightBare)
|
|
324
|
+
return false;
|
|
325
|
+
if (left.includes("\\") !== right.includes("\\"))
|
|
326
|
+
return false;
|
|
327
|
+
return left === right;
|
|
328
|
+
}
|
|
329
|
+
/** The principal a task XML runs as. */
|
|
330
|
+
function windowsTaskPrincipal(xml) {
|
|
331
|
+
return /<Principal\b[^>]*>[\s\S]*?<UserId>([^<]*)<\/UserId>/.exec(xml)?.[1]?.trim() || undefined;
|
|
332
|
+
}
|
|
333
|
+
export function legacyWindowsTask(run, host) {
|
|
334
|
+
return classifyWindowsTask(run, LEGACY_DAEMON_WINDOWS_TASK, host);
|
|
335
|
+
}
|
|
336
|
+
/** Whose task `task` is, read from `schtasks /Query /XML`. */
|
|
337
|
+
export function classifyWindowsTask(run, task, host) {
|
|
338
|
+
const r = run("schtasks.exe", ["/Query", "/TN", task, "/XML"]);
|
|
339
|
+
if (r.status !== 0) {
|
|
340
|
+
const reason = (r.stderr.trim() || r.stdout.trim()).replace(/\0/g, "");
|
|
341
|
+
return /access is denied/i.test(reason) ? { state: "unreadable", reason } : { state: "none" };
|
|
342
|
+
}
|
|
343
|
+
const owner = windowsTaskPrincipal(r.stdout.replace(/\0/g, ""));
|
|
344
|
+
if (!owner)
|
|
345
|
+
return { state: "unreadable", reason: "the task does not name the user it runs as" };
|
|
346
|
+
if (/^S-1-/i.test(owner)) {
|
|
347
|
+
const sid = host.windowsSid?.trim();
|
|
348
|
+
return sid && sid.toLowerCase() === owner.toLowerCase() ? { state: "mine", owner } : { state: "other", owner };
|
|
349
|
+
}
|
|
350
|
+
return sameWindowsUser(owner, host.windowsUser) ? { state: "mine", owner } : { state: "other", owner };
|
|
351
|
+
}
|
|
352
|
+
const REMOVE_LEGACY_TASK = `schtasks /Delete /TN ${LEGACY_DAEMON_WINDOWS_TASK} /F`;
|
|
353
|
+
/** What to tell the user about a shared task HQ left alone, or undefined when there is none. */
|
|
354
|
+
function legacyLeftNotice(legacy) {
|
|
355
|
+
if (legacy.state === "other") {
|
|
356
|
+
return (`Another Windows user's HQ daemon task (\\${LEGACY_DAEMON_WINDOWS_TASK}, run as ${legacy.owner}) was left untouched. ` +
|
|
357
|
+
`If it is in fact yours, remove it with: ${REMOVE_LEGACY_TASK}`);
|
|
358
|
+
}
|
|
359
|
+
if (legacy.state === "unreadable") {
|
|
360
|
+
return (`The shared HQ daemon task \\${LEGACY_DAEMON_WINDOWS_TASK} from an older install was left untouched because HQ could not read it (${legacy.reason}). ` +
|
|
361
|
+
`It may belong to another Windows user. If it is yours, remove it with: ${REMOVE_LEGACY_TASK}`);
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
240
365
|
/** What to tell the user when HQ did not stop pid `pid` because it could not verify it. */
|
|
241
366
|
export function oldDaemonWarning(pid, reason) {
|
|
242
367
|
return [
|
|
@@ -250,10 +375,10 @@ function activate(plan, host, run, sleep, previous = null) {
|
|
|
250
375
|
switch (plan.kind) {
|
|
251
376
|
case "launchd":
|
|
252
377
|
activateLaunchd(plan.unitPath, run, host.uid, { sleep });
|
|
253
|
-
return;
|
|
378
|
+
return {};
|
|
254
379
|
case "systemd-user":
|
|
255
380
|
activateSystemd(DAEMON_SYSTEMD_UNIT, run);
|
|
256
|
-
return;
|
|
381
|
+
return {};
|
|
257
382
|
case "systemd-system": {
|
|
258
383
|
const reload = run("systemctl", ["daemon-reload"]);
|
|
259
384
|
if (reload.status !== 0)
|
|
@@ -262,23 +387,41 @@ function activate(plan, host, run, sleep, previous = null) {
|
|
|
262
387
|
if (enable.status !== 0)
|
|
263
388
|
throw commandError(`systemctl enable --now ${DAEMON_SYSTEMD_UNIT}`, enable);
|
|
264
389
|
run("systemctl", ["restart", DAEMON_SYSTEMD_UNIT]);
|
|
265
|
-
return;
|
|
390
|
+
return {};
|
|
266
391
|
}
|
|
267
392
|
case "schtasks": {
|
|
268
|
-
const
|
|
393
|
+
const task = windowsTaskName(host);
|
|
394
|
+
// Older installs registered one task name for the whole machine. Read
|
|
395
|
+
// whose it is before registering this user's own task.
|
|
396
|
+
const legacy = legacyWindowsTask(run, host);
|
|
397
|
+
const create = run("schtasks.exe", ["/Create", "/TN", task, "/XML", plan.unitPath, "/F"]);
|
|
269
398
|
if (create.status !== 0)
|
|
270
399
|
throw commandError("schtasks /Create", create);
|
|
400
|
+
const warnings = [];
|
|
401
|
+
let notice = legacyLeftNotice(legacy);
|
|
402
|
+
if (legacy.state === "mine") {
|
|
403
|
+
// Only the current user's shared task is moved; another user's is never ended or deleted.
|
|
404
|
+
run("schtasks.exe", ["/End", "/TN", LEGACY_DAEMON_WINDOWS_TASK]);
|
|
405
|
+
const del = run("schtasks.exe", ["/Delete", "/TN", LEGACY_DAEMON_WINDOWS_TASK, "/F"]);
|
|
406
|
+
if (del.status !== 0) {
|
|
407
|
+
warnings.push(`Could not remove the shared task \\${LEGACY_DAEMON_WINDOWS_TASK}: ${(del.stderr.trim() || del.stdout.trim() || `exit ${del.status}`).replace(/\0/g, "")}. ` +
|
|
408
|
+
`Remove it with: ${REMOVE_LEGACY_TASK}`);
|
|
409
|
+
}
|
|
410
|
+
notice = `Moved your HQ daemon from the shared task \\${LEGACY_DAEMON_WINDOWS_TASK} to \\${task}.`;
|
|
411
|
+
}
|
|
271
412
|
// `/Create /F` leaves a running instance alone and `/Run` is ignored
|
|
272
413
|
// while one runs, so a daemon from an earlier install would keep the old
|
|
273
414
|
// settings. Stop it first so `/Run` starts a new one.
|
|
274
|
-
const
|
|
275
|
-
|
|
415
|
+
const old = previous ? endWindowsDaemon(run, previous, [task]) : undefined;
|
|
416
|
+
if (old)
|
|
417
|
+
warnings.push(old);
|
|
418
|
+
const start = run("schtasks.exe", ["/Run", "/TN", task]);
|
|
276
419
|
if (start.status !== 0)
|
|
277
420
|
throw commandError("schtasks /Run", start);
|
|
278
|
-
return warning;
|
|
421
|
+
return { ...(warnings.length ? { warning: warnings.join("\n") } : {}), ...(notice ? { notice } : {}) };
|
|
279
422
|
}
|
|
280
423
|
case "manual":
|
|
281
|
-
return
|
|
424
|
+
return {};
|
|
282
425
|
}
|
|
283
426
|
}
|
|
284
427
|
export function installDaemonUnit(host, options, deps) {
|
|
@@ -330,8 +473,7 @@ export function installDaemonUnit(host, options, deps) {
|
|
|
330
473
|
const lockPath = daemonPaths(host.home, { HQ_DAEMON_DIR: host.daemonDir }).lockPath;
|
|
331
474
|
const runningDaemon = deps.runningDaemon ?? (() => liveDaemonLock(lockPath));
|
|
332
475
|
const previous = plan.kind === "schtasks" ? runningDaemon() : null;
|
|
333
|
-
const
|
|
334
|
-
const warn = warning ? { warning } : {};
|
|
476
|
+
const warn = plan.kind !== "launchd" ? activate(plan, host, deps.run, deps.sleep, previous) : {};
|
|
335
477
|
if (plan.kind === "schtasks") {
|
|
336
478
|
// schtasks /Run exits 0 even when the task cannot run: an
|
|
337
479
|
// InteractiveToken task with no signed-in session stays at "has not yet
|
|
@@ -478,11 +620,22 @@ export function uninstallDaemonUnit(host, options, deps) {
|
|
|
478
620
|
: planDaemonUnit(host, options);
|
|
479
621
|
if (plan.kind === "manual")
|
|
480
622
|
return { kind: plan.kind, unitPath: plan.unitPath, removed: false };
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
623
|
+
// On Windows the XML file is shared with older installs, so it does not show
|
|
624
|
+
// that this user's per-user task exists. Ask schtasks for each name this
|
|
625
|
+
// user's task may have: the SID name, and the user-name form used when the
|
|
626
|
+
// SID could not be read at install time.
|
|
627
|
+
const userTasks = plan.kind === "schtasks" ? windowsUserTaskNames(deps.run, host) : [];
|
|
628
|
+
const present = plan.kind === "schtasks" ? userTasks.length > 0 : fs.existsSync(plan.unitPath);
|
|
629
|
+
// The current user's shared task from an older install goes too; another user's stays.
|
|
630
|
+
const legacy = plan.kind === "schtasks" ? legacyWindowsTask(deps.run, host) : { state: "none" };
|
|
631
|
+
const notice = legacyLeftNotice(legacy);
|
|
632
|
+
const noted = notice ? { notice } : {};
|
|
633
|
+
if (!present && legacy.state !== "mine") {
|
|
634
|
+
// A task XML left by an older install with no task behind it is stale.
|
|
635
|
+
if (plan.kind === "schtasks")
|
|
636
|
+
fs.rmSync(plan.unitPath, { force: true });
|
|
637
|
+
return { kind: plan.kind, unitPath: plan.unitPath, removed: false, ...noted };
|
|
638
|
+
}
|
|
486
639
|
let warning;
|
|
487
640
|
switch (plan.kind) {
|
|
488
641
|
case "launchd":
|
|
@@ -503,15 +656,20 @@ export function uninstallDaemonUnit(host, options, deps) {
|
|
|
503
656
|
}
|
|
504
657
|
case "schtasks": {
|
|
505
658
|
// The lock may be stale; endWindowsDaemon kills only a verified daemon.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
659
|
+
const tasks = [...(legacy.state === "mine" ? [LEGACY_DAEMON_WINDOWS_TASK] : []), ...userTasks];
|
|
660
|
+
warning = endWindowsDaemon(deps.run, readPidLock(path.join(host.daemonDir, "daemon.pid")), tasks);
|
|
661
|
+
for (const name of tasks) {
|
|
662
|
+
const del = deps.run("schtasks.exe", ["/Delete", "/TN", name, "/F"]);
|
|
663
|
+
// A task that is already gone counts as removed. Otherwise keep the
|
|
664
|
+
// XML so a later uninstall still finds the task.
|
|
665
|
+
if (del.status !== 0 && deps.run("schtasks.exe", ["/Query", "/TN", name]).status === 0) {
|
|
666
|
+
throw commandError(`schtasks /Delete /TN ${name}`, del);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
511
669
|
break;
|
|
512
670
|
}
|
|
513
671
|
}
|
|
514
672
|
fs.rmSync(plan.unitPath, { force: true });
|
|
515
|
-
return { kind: plan.kind, unitPath: plan.unitPath, removed: true, ...(warning ? { warning } : {}) };
|
|
673
|
+
return { kind: plan.kind, unitPath: plan.unitPath, removed: true, ...(warning ? { warning } : {}), ...noted };
|
|
516
674
|
}
|
|
517
675
|
//# sourceMappingURL=install.js.map
|