herdr-remote 0.2.23 → 0.2.25
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 +3 -2
- package/README.zh-CN.md +2 -2
- package/dist/config.js +1 -0
- package/dist/i18n/en.js +4 -3
- package/dist/i18n/zh.js +4 -3
- package/dist/keepalive/index.js +18 -7
- package/dist/keepalive/windows-service-setup.js +27 -18
- package/dist/keepalive/windows-task.js +191 -0
- package/dist/lifecycle.js +1 -1
- package/dist/tui/screens/Keepalive.js +1 -1
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,8 +57,9 @@ herdr-remote plugin link
|
|
|
57
57
|
The `herdr` binary is looked up in `HERDR_BIN_PATH`, then `PATH`, then the usual
|
|
58
58
|
install directories (`~/.local/bin`, `~/.cargo/bin`, `~/bin`, `/opt/homebrew/bin`,
|
|
59
59
|
`/usr/local/bin`, …). `herdr-remote keepalive install` writes the binary it found
|
|
60
|
-
and the current `PATH` into the systemd unit, launchd agent,
|
|
61
|
-
(`windows-
|
|
60
|
+
and the current `PATH` into the systemd unit, launchd agent, Windows logon task
|
|
61
|
+
(`windows-task`), or Windows service (`windows-service`), because managed processes
|
|
62
|
+
do not inherit your shell's environment.
|
|
62
63
|
|
|
63
64
|
On Windows, only `herdr.exe` is searched for, including `%LOCALAPPDATA%\Programs\Herdr\bin`,
|
|
64
65
|
`~\.cargo\bin`, and `~\scoop\shims`.
|
package/README.zh-CN.md
CHANGED
|
@@ -55,9 +55,9 @@ herdr-remote plugin link
|
|
|
55
55
|
## herdr 命令的查找方式
|
|
56
56
|
|
|
57
57
|
依次查找 `HERDR_BIN_PATH`、`PATH`,以及常见安装目录(`~/.local/bin`、`~/.cargo/bin`、
|
|
58
|
-
`~/bin`、`/opt/homebrew/bin`、`/usr/local/bin`
|
|
58
|
+
`~/bin`、`/opt/homebrew/bin`、`/usr/local/bin` 等)。后台管理器不会继承 shell 环境,
|
|
59
59
|
因此 `herdr-remote keepalive install` 会把找到的可执行文件路径和当前 `PATH` 写入
|
|
60
|
-
systemd unit、launchd plist
|
|
60
|
+
systemd unit、launchd plist、Windows 登录任务(`windows-task`)或 Windows 服务(`windows-service`)。
|
|
61
61
|
|
|
62
62
|
Windows 上只查找 `herdr.exe`,还会搜索 `%LOCALAPPDATA%\Programs\Herdr\bin`、
|
|
63
63
|
`~\.cargo\bin` 和 `~\scoop\shims`。
|
package/dist/config.js
CHANGED
package/dist/i18n/en.js
CHANGED
|
@@ -131,11 +131,12 @@ const en = {
|
|
|
131
131
|
'keepalive.enableLinger': 'Enable start at boot',
|
|
132
132
|
'keepalive.lingerDone': 'Start at boot enabled for {username}.',
|
|
133
133
|
'keepalive.fallbackNote': 'No system service manager found; background process will not survive reboot.',
|
|
134
|
-
'keepalive.
|
|
134
|
+
'keepalive.windowsTaskNote': 'Starts after you sign in as your account. It needs no password or administrator rights and does not run before sign-in.',
|
|
135
|
+
'keepalive.serviceNote': 'Runs as a Windows service under your account and starts before sign-in. Installing or removing it asks for administrator rights, and Windows may require your account password.',
|
|
135
136
|
'keepalive.serviceWindowHint': 'Finish the installation in the administrator window that opens.',
|
|
136
137
|
'keepalive.elevationCancelled': 'Administrator rights were not granted; nothing was installed.',
|
|
137
|
-
'keepalive.passwordPrompt': 'Windows password for {account} (
|
|
138
|
-
'keepalive.passwordRejected': 'Windows
|
|
138
|
+
'keepalive.passwordPrompt': 'Windows account password for {account} (not a PIN; press Enter if there is no password):',
|
|
139
|
+
'keepalive.passwordRejected': 'Windows could not start the service with those credentials. Check the account password and service logon policy.',
|
|
139
140
|
'keepalive.serviceInstalled': 'Service {id} is installed and running.',
|
|
140
141
|
'keepalive.wrongAccount': 'Run this as the account that will use herdr-remote, not another administrator account.',
|
|
141
142
|
'keepalive.pressEnter': 'Press Enter to close this window.',
|
package/dist/i18n/zh.js
CHANGED
|
@@ -130,11 +130,12 @@ const zh = {
|
|
|
130
130
|
'keepalive.enableLinger': '开启开机自启',
|
|
131
131
|
'keepalive.lingerDone': '已为 {username} 开启开机自启。',
|
|
132
132
|
'keepalive.fallbackNote': '无系统服务管理器,使用后台守护进程,重启后不自动恢复。',
|
|
133
|
-
'keepalive.
|
|
133
|
+
'keepalive.windowsTaskNote': '在你的账户登录后启动,无需密码或管理员权限;登录前不会运行。',
|
|
134
|
+
'keepalive.serviceNote': '以你的账户作为 Windows 服务运行,在登录前启动。安装或移除需要管理员权限,Windows 也可能要求账户密码。',
|
|
134
135
|
'keepalive.serviceWindowHint': '请在随后打开的管理员窗口中完成安装。',
|
|
135
136
|
'keepalive.elevationCancelled': '未授予管理员权限,未安装服务。',
|
|
136
|
-
'keepalive.passwordPrompt': '请输入 {account} 的 Windows
|
|
137
|
-
'keepalive.passwordRejected': 'Windows
|
|
137
|
+
'keepalive.passwordPrompt': '请输入 {account} 的 Windows 账户密码(不是 PIN;账户无密码时直接按 Enter):',
|
|
138
|
+
'keepalive.passwordRejected': 'Windows 未能使用这些凭据启动服务。请检查账户密码和“作为服务登录”策略。',
|
|
138
139
|
'keepalive.serviceInstalled': '服务 {id} 已安装并正在运行。',
|
|
139
140
|
'keepalive.wrongAccount': '请使用将运行 herdr-remote 的账户执行此操作,不要换用其他管理员账户。',
|
|
140
141
|
'keepalive.pressEnter': '按 Enter 关闭此窗口。',
|
package/dist/keepalive/index.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// "Start the services" and "keep the services running" are different problems:
|
|
4
4
|
// a detached spawn dies with the first crash and never comes back. This module
|
|
5
|
-
// hands supervision to the platform's
|
|
6
|
-
//
|
|
7
|
-
// of our own supervisor where neither does.
|
|
5
|
+
// hands supervision to the platform's manager where one exists (systemd --user,
|
|
6
|
+
// launchd, or Task Scheduler) and falls back to a detached supervisor elsewhere.
|
|
8
7
|
import { spawnSync } from 'node:child_process';
|
|
9
8
|
import fs from 'node:fs';
|
|
10
9
|
import os from 'node:os';
|
|
@@ -14,6 +13,7 @@ import { detached } from './detached.js';
|
|
|
14
13
|
import { launchd } from './launchd.js';
|
|
15
14
|
import { systemd } from './systemd.js';
|
|
16
15
|
import { windowsService } from './windows-service.js';
|
|
16
|
+
import { windowsTask } from './windows-task.js';
|
|
17
17
|
export { serviceEnvironment, servicePath } from './environment.js';
|
|
18
18
|
export { escapeXml, renderLaunchdPlist } from './launchd.js';
|
|
19
19
|
export { renderSystemdUnit, systemdEnvironmentLine } from './systemd.js';
|
|
@@ -41,7 +41,7 @@ export function detectManager(preference = 'auto', platform = process.platform)
|
|
|
41
41
|
if (preference && preference !== 'auto')
|
|
42
42
|
return preference;
|
|
43
43
|
if (platform === 'win32')
|
|
44
|
-
return 'windows-
|
|
44
|
+
return 'windows-task';
|
|
45
45
|
if (platform === 'linux') {
|
|
46
46
|
// `systemctl --user` needs a user bus; containers and bare TTY logins often
|
|
47
47
|
// have systemd installed but no session bus, where it would fail at runtime.
|
|
@@ -60,16 +60,27 @@ function managerOf(config) {
|
|
|
60
60
|
return detectManager(config.keepalive?.manager || 'auto');
|
|
61
61
|
}
|
|
62
62
|
/** The backend for `config`. With keep-alive off, uninstalling still cleans up the fallback. */
|
|
63
|
-
function
|
|
64
|
-
const manager = managerOf(config);
|
|
63
|
+
function configuredBackend(manager) {
|
|
65
64
|
if (manager === 'systemd')
|
|
66
65
|
return systemd;
|
|
67
66
|
if (manager === 'launchd')
|
|
68
67
|
return launchd;
|
|
68
|
+
if (manager === 'windows-task')
|
|
69
|
+
return windowsTask;
|
|
69
70
|
if (manager === 'windows-service')
|
|
70
71
|
return windowsService;
|
|
71
72
|
return detached;
|
|
72
73
|
}
|
|
74
|
+
function backendOf(config) {
|
|
75
|
+
const manager = managerOf(config);
|
|
76
|
+
if (manager === 'windows-task') {
|
|
77
|
+
if (windowsTask.status().installed)
|
|
78
|
+
return windowsTask;
|
|
79
|
+
if (windowsService.status().installed)
|
|
80
|
+
return windowsService;
|
|
81
|
+
}
|
|
82
|
+
return configuredBackend(manager);
|
|
83
|
+
}
|
|
73
84
|
export function status(config = loadConfig()) {
|
|
74
85
|
if (managerOf(config) === 'none')
|
|
75
86
|
return { manager: 'none', installed: false, active: false, enabled: false };
|
|
@@ -78,7 +89,7 @@ export function status(config = loadConfig()) {
|
|
|
78
89
|
export async function install(config = loadConfig()) {
|
|
79
90
|
if (managerOf(config) === 'none')
|
|
80
91
|
throw new Error('keep-alive is disabled in the configuration');
|
|
81
|
-
const backend =
|
|
92
|
+
const backend = configuredBackend(managerOf(config));
|
|
82
93
|
return { manager: backend.name, ...(await backend.install()) };
|
|
83
94
|
}
|
|
84
95
|
export async function uninstall(config = loadConfig()) {
|
|
@@ -59,6 +59,30 @@ function failure(label, result) {
|
|
|
59
59
|
const output = String(result.stdout || result.stderr || '').trim();
|
|
60
60
|
return new Error(`${label} failed (${String(result.status)}): ${output}`);
|
|
61
61
|
}
|
|
62
|
+
export async function configureAndStartServiceAccount(id, account, t, dependencies = {}) {
|
|
63
|
+
const setAccount = dependencies.setAccount || setServiceAccount;
|
|
64
|
+
const start = dependencies.start || startService;
|
|
65
|
+
const prompt = dependencies.prompt ||
|
|
66
|
+
((name) => promptSecret(t('keepalive.passwordPrompt', { account: name })));
|
|
67
|
+
const write = dependencies.write || ((message) => process.stdout.write(message));
|
|
68
|
+
for (let attempt = 0; attempt <= MAX_PASSWORD_ATTEMPTS; attempt += 1) {
|
|
69
|
+
const password = attempt === 0 ? '' : await prompt(account);
|
|
70
|
+
if (password === null)
|
|
71
|
+
return false;
|
|
72
|
+
const changed = setAccount(id, account, password);
|
|
73
|
+
if (changed.error || changed.status !== 0)
|
|
74
|
+
throw failure('Setting the Windows service account', changed);
|
|
75
|
+
const started = classifyStartResult(start());
|
|
76
|
+
if (started === 'ok')
|
|
77
|
+
return true;
|
|
78
|
+
if (started !== 'logon-failed') {
|
|
79
|
+
write(`${started.message}\n`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
write(`${t('keepalive.passwordRejected')}\n`);
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
62
86
|
/** Complete service installation inside the elevated console. */
|
|
63
87
|
export async function completeServiceInstall(t) {
|
|
64
88
|
try {
|
|
@@ -87,24 +111,9 @@ export async function completeServiceInstall(t) {
|
|
|
87
111
|
throw failure('WinSW install', installed);
|
|
88
112
|
const { domain, user } = currentServiceAccount();
|
|
89
113
|
const account = `${domain}\\${user}`;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return 1;
|
|
94
|
-
const changed = setServiceAccount(paths.id, account, password);
|
|
95
|
-
if (changed.error || changed.status !== 0)
|
|
96
|
-
throw failure('Setting the Windows service account', changed);
|
|
97
|
-
const started = classifyStartResult(startService());
|
|
98
|
-
if (started === 'ok') {
|
|
99
|
-
process.stdout.write(`${t('keepalive.serviceInstalled', { id: paths.id })}\n`);
|
|
100
|
-
return 0;
|
|
101
|
-
}
|
|
102
|
-
if (started === 'logon-failed') {
|
|
103
|
-
process.stdout.write(`${t('keepalive.passwordRejected')}\n`);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
process.stdout.write(`${started.message}\n`);
|
|
107
|
-
return 1;
|
|
114
|
+
if (await configureAndStartServiceAccount(paths.id, account, t)) {
|
|
115
|
+
process.stdout.write(`${t('keepalive.serviceInstalled', { id: paths.id })}\n`);
|
|
116
|
+
return 0;
|
|
108
117
|
}
|
|
109
118
|
return 1;
|
|
110
119
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { configDir, stateDir } from '../paths.js';
|
|
5
|
+
import { detachedLogsHint } from './detached.js';
|
|
6
|
+
import { cliEntryPoint, serviceEnvironment } from './environment.js';
|
|
7
|
+
import { currentServiceAccount, encodePowerShellCommand, escapePowerShellSingleQuoted, serviceId, windowsService, } from './windows-service.js';
|
|
8
|
+
const TASK_START_TIMEOUT_MS = 15_000;
|
|
9
|
+
const TASK_STOP_TIMEOUT_MS = 15_000;
|
|
10
|
+
const TASK_POLL_INTERVAL_MS = 250;
|
|
11
|
+
export function windowsTaskName(username = currentServiceAccount().user) {
|
|
12
|
+
return serviceId(username);
|
|
13
|
+
}
|
|
14
|
+
export function renderWindowsTaskLaunchScript(options) {
|
|
15
|
+
const environment = {
|
|
16
|
+
...options.environment,
|
|
17
|
+
HERDR_REMOTE_CONFIG_DIR: options.configDirectory,
|
|
18
|
+
HERDR_REMOTE_STATE_DIR: options.stateDirectory,
|
|
19
|
+
HERDR_REMOTE_SERVICE: '1',
|
|
20
|
+
};
|
|
21
|
+
const environmentLines = Object.entries(environment).map(([key, value]) => `$env:${key} = '${escapePowerShellSingleQuoted(value)}'`);
|
|
22
|
+
return `$ErrorActionPreference = 'Stop'
|
|
23
|
+
${environmentLines.join('\n')}
|
|
24
|
+
Set-Location -LiteralPath '${escapePowerShellSingleQuoted(options.workingDirectory)}'
|
|
25
|
+
& '${escapePowerShellSingleQuoted(options.nodePath)}' '${escapePowerShellSingleQuoted(options.entryPoint)}' run --daemon
|
|
26
|
+
exit $LASTEXITCODE`;
|
|
27
|
+
}
|
|
28
|
+
export function renderWindowsTaskRegistrationScript(options) {
|
|
29
|
+
const taskName = escapePowerShellSingleQuoted(options.taskName);
|
|
30
|
+
const account = escapePowerShellSingleQuoted(options.account);
|
|
31
|
+
const powershellPath = escapePowerShellSingleQuoted(options.powershellPath);
|
|
32
|
+
const workingDirectory = escapePowerShellSingleQuoted(options.workingDirectory);
|
|
33
|
+
const launchArguments = `-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${encodePowerShellCommand(options.launchScript)}`;
|
|
34
|
+
return `$ErrorActionPreference = 'Stop'
|
|
35
|
+
$action = New-ScheduledTaskAction -Execute '${powershellPath}' -Argument '${launchArguments}' -WorkingDirectory '${workingDirectory}'
|
|
36
|
+
$trigger = New-ScheduledTaskTrigger -AtLogOn -User '${account}'
|
|
37
|
+
# Interactive logon uses the signed-in token and avoids storing account credentials.
|
|
38
|
+
$principal = New-ScheduledTaskPrincipal -UserId '${account}' -LogonType Interactive -RunLevel Limited
|
|
39
|
+
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
|
|
40
|
+
Register-ScheduledTask -TaskName '${taskName}' -Description 'Herdr Remote keep-alive' -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
|
|
41
|
+
Enable-ScheduledTask -TaskName '${taskName}' | Out-Null
|
|
42
|
+
Start-ScheduledTask -TaskName '${taskName}'`;
|
|
43
|
+
}
|
|
44
|
+
export function parseWindowsTaskState(output) {
|
|
45
|
+
try {
|
|
46
|
+
const state = JSON.parse(output.replace(/^\uFEFF/, '').trim()).State;
|
|
47
|
+
return state === 'Ready' || state === 'Queued' || state === 'Running' || state === 'Disabled'
|
|
48
|
+
? state
|
|
49
|
+
: 'Absent';
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Task status is best-effort; malformed scheduler output means no known task.
|
|
53
|
+
return 'Absent';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function windowsPowerShellPath() {
|
|
57
|
+
const root = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
|
|
58
|
+
return path.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
59
|
+
}
|
|
60
|
+
function taskStateScript(taskName) {
|
|
61
|
+
const safeName = escapePowerShellSingleQuoted(taskName);
|
|
62
|
+
return `$ErrorActionPreference = 'Stop'
|
|
63
|
+
$task = Get-ScheduledTask -TaskName '${safeName}' -ErrorAction SilentlyContinue
|
|
64
|
+
if ($null -eq $task) { '{"State":"Absent"}'; exit 0 }
|
|
65
|
+
[pscustomobject]@{ State = [string]$task.State } | ConvertTo-Json -Compress`;
|
|
66
|
+
}
|
|
67
|
+
function invokePowerShell(script, label) {
|
|
68
|
+
const result = spawnSync(windowsPowerShellPath(), [
|
|
69
|
+
'-NoLogo',
|
|
70
|
+
'-NoProfile',
|
|
71
|
+
'-NonInteractive',
|
|
72
|
+
'-EncodedCommand',
|
|
73
|
+
encodePowerShellCommand(script),
|
|
74
|
+
], { encoding: 'utf8', windowsHide: true });
|
|
75
|
+
if (result.error || result.status !== 0) {
|
|
76
|
+
const output = String(result.stderr || result.stdout || '').trim();
|
|
77
|
+
throw new Error(`${label} failed (${String(result.status)}): ${output}`);
|
|
78
|
+
}
|
|
79
|
+
return String(result.stdout || '').trim();
|
|
80
|
+
}
|
|
81
|
+
function taskState() {
|
|
82
|
+
const result = spawnSync(windowsPowerShellPath(), [
|
|
83
|
+
'-NoProfile',
|
|
84
|
+
'-NoLogo',
|
|
85
|
+
'-NonInteractive',
|
|
86
|
+
'-EncodedCommand',
|
|
87
|
+
encodePowerShellCommand(taskStateScript(windowsTaskName())),
|
|
88
|
+
], { encoding: 'utf8', windowsHide: true });
|
|
89
|
+
if (result.error || result.status !== 0) {
|
|
90
|
+
// A status query must not make unrelated CLI commands fail on an unavailable scheduler.
|
|
91
|
+
return 'Absent';
|
|
92
|
+
}
|
|
93
|
+
return parseWindowsTaskState(String(result.stdout || ''));
|
|
94
|
+
}
|
|
95
|
+
function accountName() {
|
|
96
|
+
const { domain, user } = currentServiceAccount();
|
|
97
|
+
return domain ? `${domain}\\${user}` : user;
|
|
98
|
+
}
|
|
99
|
+
function stopTaskScript(taskName) {
|
|
100
|
+
const safeName = escapePowerShellSingleQuoted(taskName);
|
|
101
|
+
return `$ErrorActionPreference = 'Stop'
|
|
102
|
+
$task = Get-ScheduledTask -TaskName '${safeName}' -ErrorAction SilentlyContinue
|
|
103
|
+
if ($null -eq $task -or ($task.State -ne 'Running' -and $task.State -ne 'Queued')) { exit 0 }
|
|
104
|
+
Stop-ScheduledTask -TaskName '${safeName}'
|
|
105
|
+
$deadline = [DateTime]::UtcNow.AddMilliseconds(${TASK_STOP_TIMEOUT_MS})
|
|
106
|
+
do {
|
|
107
|
+
Start-Sleep -Milliseconds ${TASK_POLL_INTERVAL_MS}
|
|
108
|
+
$task = Get-ScheduledTask -TaskName '${safeName}' -ErrorAction SilentlyContinue
|
|
109
|
+
} while ($task -and ($task.State -eq 'Running' -or $task.State -eq 'Queued') -and [DateTime]::UtcNow -lt $deadline)
|
|
110
|
+
if ($task -and ($task.State -eq 'Running' -or $task.State -eq 'Queued')) { throw 'Scheduled task did not stop in time.' }`;
|
|
111
|
+
}
|
|
112
|
+
function stopTask() {
|
|
113
|
+
const state = taskState();
|
|
114
|
+
if (state === 'Running' || state === 'Queued')
|
|
115
|
+
invokePowerShell(stopTaskScript(windowsTaskName()), 'Stopping the Windows scheduled task');
|
|
116
|
+
}
|
|
117
|
+
function launchOptions() {
|
|
118
|
+
const workingDirectory = stateDir();
|
|
119
|
+
return {
|
|
120
|
+
nodePath: process.execPath,
|
|
121
|
+
entryPoint: cliEntryPoint(),
|
|
122
|
+
workingDirectory,
|
|
123
|
+
configDirectory: configDir(),
|
|
124
|
+
stateDirectory: workingDirectory,
|
|
125
|
+
environment: serviceEnvironment(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function status() {
|
|
129
|
+
const state = taskState();
|
|
130
|
+
const installed = state !== 'Absent';
|
|
131
|
+
return {
|
|
132
|
+
manager: 'windows-task',
|
|
133
|
+
installed,
|
|
134
|
+
active: state === 'Running',
|
|
135
|
+
enabled: installed && state !== 'Disabled',
|
|
136
|
+
state: state.toLowerCase(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function install() {
|
|
140
|
+
if (taskState() !== 'Absent')
|
|
141
|
+
stopTask();
|
|
142
|
+
// Remove the earlier service before starting a second supervisor for this user.
|
|
143
|
+
if (windowsService.status().installed)
|
|
144
|
+
await windowsService.uninstall();
|
|
145
|
+
const options = launchOptions();
|
|
146
|
+
await fs.promises.mkdir(options.workingDirectory, { recursive: true });
|
|
147
|
+
const launchScript = renderWindowsTaskLaunchScript(options);
|
|
148
|
+
const script = renderWindowsTaskRegistrationScript({
|
|
149
|
+
taskName: windowsTaskName(),
|
|
150
|
+
account: accountName(),
|
|
151
|
+
powershellPath: windowsPowerShellPath(),
|
|
152
|
+
workingDirectory: options.workingDirectory,
|
|
153
|
+
launchScript,
|
|
154
|
+
});
|
|
155
|
+
invokePowerShell(script, 'Installing the Windows scheduled task');
|
|
156
|
+
const deadline = Date.now() + TASK_START_TIMEOUT_MS;
|
|
157
|
+
let state = taskState();
|
|
158
|
+
while (state !== 'Running' && state !== 'Absent' && Date.now() < deadline) {
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, TASK_POLL_INTERVAL_MS));
|
|
160
|
+
state = taskState();
|
|
161
|
+
}
|
|
162
|
+
if (state !== 'Running')
|
|
163
|
+
throw new Error(`Windows scheduled task did not start (state: ${state.toLowerCase()}).`);
|
|
164
|
+
return { ok: true, taskName: windowsTaskName(), state };
|
|
165
|
+
}
|
|
166
|
+
async function uninstall() {
|
|
167
|
+
const name = windowsTaskName();
|
|
168
|
+
if (taskState() !== 'Absent') {
|
|
169
|
+
stopTask();
|
|
170
|
+
const safeName = escapePowerShellSingleQuoted(name);
|
|
171
|
+
invokePowerShell(`Unregister-ScheduledTask -TaskName '${safeName}' -Confirm:$false -ErrorAction Stop`, 'Removing the Windows scheduled task');
|
|
172
|
+
}
|
|
173
|
+
if (windowsService.status().installed)
|
|
174
|
+
await windowsService.uninstall();
|
|
175
|
+
return { ok: true, taskName: name };
|
|
176
|
+
}
|
|
177
|
+
function restart() {
|
|
178
|
+
stopTask();
|
|
179
|
+
const safeName = escapePowerShellSingleQuoted(windowsTaskName());
|
|
180
|
+
invokePowerShell(`Start-ScheduledTask -TaskName '${safeName}'`, 'Starting the Windows scheduled task');
|
|
181
|
+
return { ok: true };
|
|
182
|
+
}
|
|
183
|
+
export const windowsTask = {
|
|
184
|
+
name: 'windows-task',
|
|
185
|
+
status,
|
|
186
|
+
install,
|
|
187
|
+
uninstall,
|
|
188
|
+
restart,
|
|
189
|
+
stop: stopTask,
|
|
190
|
+
logsHint: () => detachedLogsHint('win32'),
|
|
191
|
+
};
|
package/dist/lifecycle.js
CHANGED
|
@@ -49,7 +49,7 @@ function restartAll(config = loadConfig()) {
|
|
|
49
49
|
}
|
|
50
50
|
return { ...restartServices(), managed: false };
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
52
|
+
/** Stop Windows keep-alive processes while npm replaces their package files. */
|
|
53
53
|
async function whileServicesStopped(config, task, { platform = process.platform, isRunning = () => Boolean(managerInUse(config)?.active || pidAlive(readRuntime().hostPid)), stop = stopAll, start = startAll, } = {}) {
|
|
54
54
|
if (platform !== 'win32' || !isRunning())
|
|
55
55
|
return task();
|
|
@@ -91,5 +91,5 @@ export function Keepalive({ ctx }) {
|
|
|
91
91
|
: t('common.notInstalled')] })] }) }), current?.unitPath ? (_jsx(Row, { label: t('keepalive.unitPath'), children: _jsx(Text, { color: theme.muted, children: current.unitPath }) })) : null, _jsx(Box, { marginTop: 1, flexDirection: "column", children: entries.map((entry) => (_jsx(Selectable, { selected: selected === entry.id, onSelect: () => {
|
|
92
92
|
setSelected(entry.id);
|
|
93
93
|
activate(entry.id);
|
|
94
|
-
}, onHover: () => setSelected(entry.id), children: entry.label }, entry.id))) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [current?.manager === 'systemd' ? (_jsx(Text, { color: current.linger ? theme.muted : theme.warn, children: current.linger ? t('keepalive.lingerEnabled') : t('keepalive.lingerDisabled') })) : null, current?.manager === 'supervisor' ? (_jsx(Text, { color: theme.warn, children: t('keepalive.fallbackNote') })) : null, current?.manager === 'windows-service' ? (_jsx(Text, { color: theme.warn, children: t('keepalive.serviceNote') })) : null, _jsx(Text, { color: theme.muted, children: t('keepalive.logsHint', { command: status?.logsHint ?? '' }) })] }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
|
|
94
|
+
}, onHover: () => setSelected(entry.id), children: entry.label }, entry.id))) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [current?.manager === 'systemd' ? (_jsx(Text, { color: current.linger ? theme.muted : theme.warn, children: current.linger ? t('keepalive.lingerEnabled') : t('keepalive.lingerDisabled') })) : null, current?.manager === 'supervisor' ? (_jsx(Text, { color: theme.warn, children: t('keepalive.fallbackNote') })) : null, current?.manager === 'windows-task' ? (_jsx(Text, { color: theme.muted, children: t('keepalive.windowsTaskNote') })) : null, current?.manager === 'windows-service' ? (_jsx(Text, { color: theme.warn, children: t('keepalive.serviceNote') })) : null, _jsx(Text, { color: theme.muted, children: t('keepalive.logsHint', { command: status?.logsHint ?? '' }) })] }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
|
|
95
95
|
}
|
package/herdr-plugin.toml
CHANGED