herdr-remote 0.2.39 → 0.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/353DMT35.js +3 -0
- package/dist/chunks/42KARAL4.js +31 -0
- package/dist/chunks/7AUQHB3O.js +10 -0
- package/dist/chunks/7SMZ43LI.js +9 -0
- package/dist/chunks/AM3OEMFK.js +16 -0
- package/dist/chunks/ANNJESOL.js +10 -0
- package/dist/chunks/AWB4A65P.js +63 -0
- package/dist/chunks/C2UUZY6J.js +2 -0
- package/dist/chunks/DGUZCGQN.js +3 -0
- package/dist/chunks/H24ZP5D5.js +16 -0
- package/dist/chunks/I6NRCYLI.js +3 -0
- package/dist/chunks/JJGEV4EZ.js +3 -0
- package/dist/chunks/KRSURB5L.js +5 -0
- package/dist/chunks/KRVF37OW.js +3 -0
- package/dist/chunks/KUDCC7ZO.js +6 -0
- package/dist/chunks/M3NRPJK2.js +77 -0
- package/dist/chunks/M4AFZHKH.js +3 -0
- package/dist/chunks/M52LJEES.js +6 -0
- package/dist/chunks/O2LNVFUT.js +3 -0
- package/dist/chunks/OJPBI7KB.js +3 -0
- package/dist/chunks/OMN6C7J6.js +3 -0
- package/dist/chunks/PBMBFWLQ.js +3 -0
- package/dist/chunks/PBRQ3HEG.js +3 -0
- package/dist/chunks/PFFVGFPL.js +45 -0
- package/dist/chunks/QRGNEKME.js +5 -0
- package/dist/chunks/TNLCNVA2.js +3 -0
- package/dist/chunks/U27JJKJ6.js +3 -0
- package/dist/chunks/U6QAYG3E.js +3 -0
- package/dist/chunks/UEOQE77N.js +4 -0
- package/dist/chunks/WSACSE45.js +4 -0
- package/dist/chunks/WY7WHEJ4.js +3 -0
- package/dist/chunks/XJKWLK43.js +4 -0
- package/dist/chunks/XN6D5S55.js +3 -0
- package/dist/chunks/YD3MIP7B.js +5 -0
- package/dist/chunks/YXDQ62BB.js +4 -0
- package/dist/cli.js +21 -294
- package/dist/connector/admin-broker.js +3 -303
- package/dist/connector/main.js +20 -18
- package/dist/harfbuzz-subset.wasm +0 -0
- package/dist/postinstall.js +17 -0
- package/herdr-plugin.toml +1 -1
- package/node_modules/herdr-remote-relay/bin/herdr-remote-relay.js +33 -92
- package/node_modules/herdr-remote-relay/package.json +3 -46
- package/package.json +8 -27
- package/dist/agent-status.js +0 -130
- package/dist/binary-plist.js +0 -157
- package/dist/config.js +0 -278
- package/dist/connector/admin-broker-setup.js +0 -40
- package/dist/connector/admin-shell.js +0 -67
- package/dist/connector/admin-tab.js +0 -90
- package/dist/connector/adminBrokerEndpoint.js +0 -68
- package/dist/connector/agent-watch.js +0 -123
- package/dist/connector/elevatedPty.js +0 -141
- package/dist/connector/font-server.js +0 -165
- package/dist/connector/herdr-prefix-keys.js +0 -28
- package/dist/connector/host-connector.js +0 -433
- package/dist/connector/host-hello.js +0 -32
- package/dist/connector/lock.js +0 -73
- package/dist/connector/sessions.js +0 -381
- package/dist/connector/update-report.js +0 -70
- package/dist/connector/wire.js +0 -42
- package/dist/exit-codes.js +0 -13
- package/dist/font-subset.js +0 -116
- package/dist/herdr-api.js +0 -254
- package/dist/herdr-command.js +0 -245
- package/dist/herdr-plugin.js +0 -120
- package/dist/herdr-server.js +0 -190
- package/dist/i18n/en.js +0 -257
- package/dist/i18n/index.js +0 -48
- package/dist/i18n/zh.js +0 -256
- package/dist/keepalive/detached.js +0 -97
- package/dist/keepalive/environment.js +0 -44
- package/dist/keepalive/index.js +0 -129
- package/dist/keepalive/launchd.js +0 -117
- package/dist/keepalive/systemd.js +0 -112
- package/dist/keepalive/types.js +0 -1
- package/dist/keepalive/windows-service-setup.js +0 -138
- package/dist/keepalive/windows-service.js +0 -272
- package/dist/keepalive/windows-task.js +0 -191
- package/dist/lib/process.js +0 -132
- package/dist/lifecycle.js +0 -79
- package/dist/net-interfaces.js +0 -82
- package/dist/node-pty-loader.js +0 -80
- package/dist/pasted-files.js +0 -115
- package/dist/paths.js +0 -74
- package/dist/pty-session.js +0 -133
- package/dist/relay-client.js +0 -106
- package/dist/relay-urls.js +0 -99
- package/dist/runtime.js +0 -107
- package/dist/service.js +0 -267
- package/dist/settings-model.js +0 -298
- package/dist/socket-discovery.js +0 -70
- package/dist/supervisor.js +0 -323
- package/dist/terminal-font/deps.js +0 -83
- package/dist/terminal-font/files.js +0 -357
- package/dist/terminal-font/formats.js +0 -355
- package/dist/terminal-font/index.js +0 -187
- package/dist/terminal-font/readers.js +0 -373
- package/dist/terminal-font/terminals.js +0 -147
- package/dist/terminal-palette.js +0 -399
- package/dist/tui/App.js +0 -199
- package/dist/tui/api.js +0 -91
- package/dist/tui/components/TextField.js +0 -50
- package/dist/tui/components/common.js +0 -80
- package/dist/tui/index.js +0 -36
- package/dist/tui/mouse/index.js +0 -164
- package/dist/tui/mouse/protocol.js +0 -96
- package/dist/tui/mouse/source.js +0 -79
- package/dist/tui/screens/About.js +0 -172
- package/dist/tui/screens/Herdr.js +0 -75
- package/dist/tui/screens/Keepalive.js +0 -141
- package/dist/tui/screens/Overview.js +0 -36
- package/dist/tui/screens/Pair.js +0 -74
- package/dist/tui/screens/Relay.js +0 -243
- package/dist/tui/screens/Services.js +0 -63
- package/dist/tui/screens/Wizard.js +0 -241
- package/dist/tui/terminal.js +0 -39
- package/dist/tui/theme.js +0 -33
- package/dist/update-command.js +0 -118
- package/dist/updater/in-place.js +0 -83
- package/dist/updater/npm.js +0 -62
- package/dist/updater.js +0 -401
- package/node_modules/herdr-remote-relay/README.md +0 -76
- package/node_modules/herdr-remote-relay/README.zh-CN.md +0 -76
- package/node_modules/herdr-remote-relay/config.example.json +0 -18
- package/node_modules/herdr-remote-relay/deploy/cloudflared/config.yml.example +0 -9
- package/node_modules/herdr-remote-relay/deploy/docker/compose.yaml +0 -23
- package/node_modules/herdr-remote-relay/deploy/nginx/herdr-remote-relay.conf.example +0 -49
- package/node_modules/herdr-remote-relay/deploy/systemd/herdr-remote-relay.service +0 -41
- package/node_modules/herdr-remote-relay/deploy/systemd/relay.env.example +0 -25
- package/node_modules/herdr-remote-relay/dist/auth-store.d.ts +0 -114
- package/node_modules/herdr-remote-relay/dist/auth-store.js +0 -289
- package/node_modules/herdr-remote-relay/dist/metrics.d.ts +0 -76
- package/node_modules/herdr-remote-relay/dist/metrics.js +0 -249
- package/node_modules/herdr-remote-relay/dist/protocol/frames.d.ts +0 -27
- package/node_modules/herdr-remote-relay/dist/protocol/frames.js +0 -110
- package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.d.ts +0 -8
- package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.js +0 -34
- package/node_modules/herdr-remote-relay/dist/protocol/http.d.ts +0 -165
- package/node_modules/herdr-remote-relay/dist/protocol/http.js +0 -3
- package/node_modules/herdr-remote-relay/dist/protocol/index.d.ts +0 -7
- package/node_modules/herdr-remote-relay/dist/protocol/index.js +0 -9
- package/node_modules/herdr-remote-relay/dist/protocol/messages.d.ts +0 -362
- package/node_modules/herdr-remote-relay/dist/protocol/messages.js +0 -20
- package/node_modules/herdr-remote-relay/dist/protocol/paste.d.ts +0 -14
- package/node_modules/herdr-remote-relay/dist/protocol/paste.js +0 -50
- package/node_modules/herdr-remote-relay/dist/protocol/release.d.ts +0 -8
- package/node_modules/herdr-remote-relay/dist/protocol/release.js +0 -17
- package/node_modules/herdr-remote-relay/dist/protocol/terminal.d.ts +0 -79
- package/node_modules/herdr-remote-relay/dist/protocol/terminal.js +0 -150
- package/node_modules/herdr-remote-relay/dist/relay-config.d.ts +0 -74
- package/node_modules/herdr-remote-relay/dist/relay-config.js +0 -316
- package/node_modules/herdr-remote-relay/dist/relay-server.d.ts +0 -67
- package/node_modules/herdr-remote-relay/dist/relay-server.js +0 -196
- package/node_modules/herdr-remote-relay/dist/scroll-input.d.ts +0 -17
- package/node_modules/herdr-remote-relay/dist/scroll-input.js +0 -124
- package/node_modules/herdr-remote-relay/dist/server/client-channel.d.ts +0 -3
- package/node_modules/herdr-remote-relay/dist/server/client-channel.js +0 -396
- package/node_modules/herdr-remote-relay/dist/server/font-proxy.d.ts +0 -19
- package/node_modules/herdr-remote-relay/dist/server/font-proxy.js +0 -155
- package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.d.ts +0 -27
- package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.js +0 -83
- package/node_modules/herdr-remote-relay/dist/server/host-channel.d.ts +0 -3
- package/node_modules/herdr-remote-relay/dist/server/host-channel.js +0 -271
- package/node_modules/herdr-remote-relay/dist/server/http-api.d.ts +0 -4
- package/node_modules/herdr-remote-relay/dist/server/http-api.js +0 -219
- package/node_modules/herdr-remote-relay/dist/server/maintenance.d.ts +0 -5
- package/node_modules/herdr-remote-relay/dist/server/maintenance.js +0 -64
- package/node_modules/herdr-remote-relay/dist/server/requests.d.ts +0 -34
- package/node_modules/herdr-remote-relay/dist/server/requests.js +0 -142
- package/node_modules/herdr-remote-relay/dist/server/sessions.d.ts +0 -36
- package/node_modules/herdr-remote-relay/dist/server/sessions.js +0 -230
- package/node_modules/herdr-remote-relay/dist/server/sockets.d.ts +0 -12
- package/node_modules/herdr-remote-relay/dist/server/sockets.js +0 -64
- package/node_modules/herdr-remote-relay/dist/server/status.d.ts +0 -11
- package/node_modules/herdr-remote-relay/dist/server/status.js +0 -99
- package/node_modules/herdr-remote-relay/dist/server/transport.d.ts +0 -16
- package/node_modules/herdr-remote-relay/dist/server/transport.js +0 -110
- package/node_modules/herdr-remote-relay/dist/server/types.d.ts +0 -133
- package/node_modules/herdr-remote-relay/dist/server/types.js +0 -3
- package/node_modules/herdr-remote-relay/dist/server/version.d.ts +0 -2
- package/node_modules/herdr-remote-relay/dist/server/version.js +0 -5
- package/node_modules/herdr-remote-relay/dist/state.d.ts +0 -6
- package/node_modules/herdr-remote-relay/dist/state.js +0 -47
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -22
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -530
- package/node_modules/ws/lib/receiver.js +0 -743
- package/node_modules/ws/lib/sender.js +0 -607
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -562
- package/node_modules/ws/lib/websocket.js +0 -1407
- package/node_modules/ws/package.json +0 -74
- package/node_modules/ws/wrapper.mjs +0 -21
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// systemd --user: a unit that runs `herdr-remote run` and restarts it.
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { ensureDir } from 'herdr-remote-relay/state';
|
|
7
|
-
import { cliEntryPoint, serviceEnvironment } from './environment.js';
|
|
8
|
-
const SYSTEMD_UNIT_NAME = 'herdr-remote.service';
|
|
9
|
-
function systemdUnitPath() {
|
|
10
|
-
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
11
|
-
return path.join(configHome, 'systemd', 'user', SYSTEMD_UNIT_NAME);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* One `KEY=value` per directive. An unquoted systemd value ends at the first
|
|
15
|
-
* space, and `%` starts a specifier, so a `PATH` with either in it would be
|
|
16
|
-
* silently truncated or rewritten.
|
|
17
|
-
*/
|
|
18
|
-
export function systemdEnvironmentLine(key, value) {
|
|
19
|
-
const text = String(value)
|
|
20
|
-
.replace(/[\r\n]+/g, ' ')
|
|
21
|
-
.replace(/%/g, '%%');
|
|
22
|
-
if (/^[\w@+=:,./-]*$/.test(text))
|
|
23
|
-
return `Environment=${key}=${text}`;
|
|
24
|
-
return `Environment="${key}=${text.replace(/([\\"])/g, '\\$1')}"`;
|
|
25
|
-
}
|
|
26
|
-
export function renderSystemdUnit({ nodePath = process.execPath, entryPoint = cliEntryPoint(), environment = {}, } = {}) {
|
|
27
|
-
const environmentLines = Object.entries(environment)
|
|
28
|
-
.map(([key, value]) => systemdEnvironmentLine(key, value))
|
|
29
|
-
.join('\n');
|
|
30
|
-
return `[Unit]
|
|
31
|
-
Description=Herdr Remote (relay and host connector)
|
|
32
|
-
Documentation=https://github.com/dibin666/herdr-remote
|
|
33
|
-
After=default.target
|
|
34
|
-
|
|
35
|
-
[Service]
|
|
36
|
-
Type=simple
|
|
37
|
-
ExecStart=${nodePath} ${entryPoint} run
|
|
38
|
-
Restart=always
|
|
39
|
-
RestartSec=3
|
|
40
|
-
# Give the relay and host connector time to close sessions cleanly.
|
|
41
|
-
TimeoutStopSec=15
|
|
42
|
-
${environmentLines}
|
|
43
|
-
|
|
44
|
-
[Install]
|
|
45
|
-
WantedBy=default.target
|
|
46
|
-
`;
|
|
47
|
-
}
|
|
48
|
-
function systemctl(args) {
|
|
49
|
-
return spawnSync('systemctl', ['--user', ...args], {
|
|
50
|
-
encoding: 'utf8',
|
|
51
|
-
stdio: 'pipe',
|
|
52
|
-
windowsHide: true,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
/** Run a systemctl command that must succeed, throwing its stderr when it does not. */
|
|
56
|
-
function systemctlOrThrow(args, failure) {
|
|
57
|
-
const result = systemctl(args);
|
|
58
|
-
if (result.status !== 0) {
|
|
59
|
-
throw new Error(`${failure}: ${String(result.stderr || '').trim()}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
export const systemd = {
|
|
63
|
-
name: 'systemd',
|
|
64
|
-
status() {
|
|
65
|
-
const installed = fs.existsSync(systemdUnitPath());
|
|
66
|
-
if (!installed)
|
|
67
|
-
return { manager: 'systemd', installed: false, active: false, enabled: false };
|
|
68
|
-
const active = systemctl(['is-active', SYSTEMD_UNIT_NAME]);
|
|
69
|
-
const enabled = systemctl(['is-enabled', SYSTEMD_UNIT_NAME]);
|
|
70
|
-
const lingering = spawnSync('loginctl', ['show-user', os.userInfo().username, '--property=Linger'], { encoding: 'utf8', windowsHide: true });
|
|
71
|
-
return {
|
|
72
|
-
manager: 'systemd',
|
|
73
|
-
installed: true,
|
|
74
|
-
active: String(active.stdout || '').trim() === 'active',
|
|
75
|
-
enabled: String(enabled.stdout || '')
|
|
76
|
-
.trim()
|
|
77
|
-
.startsWith('enabled'),
|
|
78
|
-
linger: String(lingering.stdout || '').includes('Linger=yes'),
|
|
79
|
-
unitPath: systemdUnitPath(),
|
|
80
|
-
state: String(active.stdout || active.stderr || '').trim(),
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
async install() {
|
|
84
|
-
const unitPath = systemdUnitPath();
|
|
85
|
-
ensureDir(path.dirname(unitPath));
|
|
86
|
-
fs.writeFileSync(unitPath, renderSystemdUnit({ environment: serviceEnvironment() }), {
|
|
87
|
-
mode: 0o644,
|
|
88
|
-
});
|
|
89
|
-
systemctlOrThrow(['daemon-reload'], 'systemctl --user daemon-reload failed');
|
|
90
|
-
systemctlOrThrow(['enable', '--now', SYSTEMD_UNIT_NAME], 'systemctl --user enable --now failed');
|
|
91
|
-
return { ok: true, unitPath, hint: `loginctl enable-linger ${os.userInfo().username}` };
|
|
92
|
-
},
|
|
93
|
-
async uninstall() {
|
|
94
|
-
const unitPath = systemdUnitPath();
|
|
95
|
-
systemctl(['disable', '--now', SYSTEMD_UNIT_NAME]);
|
|
96
|
-
fs.rmSync(unitPath, { force: true });
|
|
97
|
-
systemctl(['daemon-reload']);
|
|
98
|
-
return { ok: true, unitPath };
|
|
99
|
-
},
|
|
100
|
-
restart() {
|
|
101
|
-
const result = systemctl(['restart', SYSTEMD_UNIT_NAME]);
|
|
102
|
-
if (result.status !== 0)
|
|
103
|
-
throw new Error(String(result.stderr || '').trim() || 'systemctl restart failed');
|
|
104
|
-
return { ok: true };
|
|
105
|
-
},
|
|
106
|
-
stop() {
|
|
107
|
-
systemctl(['stop', SYSTEMD_UNIT_NAME]);
|
|
108
|
-
},
|
|
109
|
-
logsHint() {
|
|
110
|
-
return `journalctl --user -u ${SYSTEMD_UNIT_NAME} -f`;
|
|
111
|
-
},
|
|
112
|
-
};
|
package/dist/keepalive/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import { classifyStartResult, currentServiceAccount, encodePowerShellCommand, escapePowerShellSingleQuoted, startService, waitForState, windowsServicePaths, serviceState, windowsService, } from './windows-service.js';
|
|
4
|
-
const MAX_PASSWORD_ATTEMPTS = 3;
|
|
5
|
-
export function renderSetServiceAccountScript({ id, account }) {
|
|
6
|
-
const safeId = escapePowerShellSingleQuoted(id);
|
|
7
|
-
const safeAccount = escapePowerShellSingleQuoted(account);
|
|
8
|
-
return `$ErrorActionPreference = 'Stop'
|
|
9
|
-
$password = [Console]::In.ReadLine()
|
|
10
|
-
$service = Get-CimInstance -ClassName Win32_Service -Filter "Name='${safeId}'"
|
|
11
|
-
$result = Invoke-CimMethod -InputObject $service -MethodName Change -Arguments @{ StartName = '${safeAccount}'; StartPassword = $password }
|
|
12
|
-
exit [int]$result.ReturnValue`;
|
|
13
|
-
}
|
|
14
|
-
function setServiceAccount(id, account, password) {
|
|
15
|
-
const script = renderSetServiceAccountScript({ id, account });
|
|
16
|
-
// The password travels through stdin only; it is not part of PowerShell arguments or a file.
|
|
17
|
-
return spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encodePowerShellCommand(script)], { input: `${password}\n`, encoding: 'utf8', windowsHide: true });
|
|
18
|
-
}
|
|
19
|
-
/** Read a password with `*` feedback and remove ESC sequences such as arrow keys. */
|
|
20
|
-
function promptSecret(prompt) {
|
|
21
|
-
const input = process.stdin;
|
|
22
|
-
process.stdout.write(prompt);
|
|
23
|
-
input.setRawMode(true);
|
|
24
|
-
input.resume();
|
|
25
|
-
return new Promise((resolve) => {
|
|
26
|
-
let password = '';
|
|
27
|
-
const finish = (value) => {
|
|
28
|
-
input.removeListener('data', onData);
|
|
29
|
-
input.setRawMode(false);
|
|
30
|
-
input.pause();
|
|
31
|
-
process.stdout.write('\n');
|
|
32
|
-
resolve(value);
|
|
33
|
-
};
|
|
34
|
-
const onData = (chunk) => {
|
|
35
|
-
const clean = String(chunk).replace(/\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|O.|.)/g, '');
|
|
36
|
-
for (const character of clean) {
|
|
37
|
-
if (character === '\r' || character === '\n')
|
|
38
|
-
return finish(password);
|
|
39
|
-
if (character === '\u0003')
|
|
40
|
-
return finish(null);
|
|
41
|
-
if (character === '\u007f' || character === '\b') {
|
|
42
|
-
if (password.length > 0) {
|
|
43
|
-
password = Array.from(password).slice(0, -1).join('');
|
|
44
|
-
process.stdout.write('\b \b');
|
|
45
|
-
}
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const code = character.codePointAt(0) || 0;
|
|
49
|
-
if (code < 0x20 || (code >= 0x7f && code <= 0x9f))
|
|
50
|
-
continue;
|
|
51
|
-
password += character;
|
|
52
|
-
process.stdout.write('*');
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
input.on('data', onData);
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
function failure(label, result) {
|
|
59
|
-
const output = String(result.stdout || result.stderr || '').trim();
|
|
60
|
-
return new Error(`${label} failed (${String(result.status)}): ${output}`);
|
|
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
|
-
}
|
|
86
|
-
/** Complete service installation inside the elevated console. */
|
|
87
|
-
export async function completeServiceInstall(t) {
|
|
88
|
-
try {
|
|
89
|
-
const paths = windowsServicePaths();
|
|
90
|
-
// A different elevated account resolves a different state directory
|
|
91
|
-
// and cannot see the XML written before elevation.
|
|
92
|
-
if (!fs.existsSync(paths.unitPath)) {
|
|
93
|
-
process.stdout.write(`${t('keepalive.wrongAccount')}\n`);
|
|
94
|
-
return 1;
|
|
95
|
-
}
|
|
96
|
-
if (serviceState() !== 'NonExistent') {
|
|
97
|
-
windowsService.stop();
|
|
98
|
-
waitForState('Stopped');
|
|
99
|
-
const removed = spawnSync(paths.exePath, ['uninstall'], {
|
|
100
|
-
encoding: 'utf8',
|
|
101
|
-
windowsHide: true,
|
|
102
|
-
});
|
|
103
|
-
if (removed.error || removed.status !== 0)
|
|
104
|
-
throw failure('WinSW uninstall', removed);
|
|
105
|
-
}
|
|
106
|
-
const installed = spawnSync(paths.exePath, ['install'], {
|
|
107
|
-
stdio: 'inherit',
|
|
108
|
-
windowsHide: true,
|
|
109
|
-
});
|
|
110
|
-
if (installed.error || installed.status !== 0)
|
|
111
|
-
throw failure('WinSW install', installed);
|
|
112
|
-
const { domain, user } = currentServiceAccount();
|
|
113
|
-
const account = `${domain}\\${user}`;
|
|
114
|
-
if (await configureAndStartServiceAccount(paths.id, account, t)) {
|
|
115
|
-
process.stdout.write(`${t('keepalive.serviceInstalled', { id: paths.id })}\n`);
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
118
|
-
return 1;
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
process.stdout.write(`${error.message}\n`);
|
|
122
|
-
return 1;
|
|
123
|
-
}
|
|
124
|
-
finally {
|
|
125
|
-
process.stdout.write(`${t('keepalive.pressEnter')}\n`);
|
|
126
|
-
process.stdin.resume();
|
|
127
|
-
await new Promise((resolve) => {
|
|
128
|
-
const onData = (chunk) => {
|
|
129
|
-
if (!/[\r\n]/.test(String(chunk)))
|
|
130
|
-
return;
|
|
131
|
-
process.stdin.removeListener('data', onData);
|
|
132
|
-
process.stdin.pause();
|
|
133
|
-
resolve();
|
|
134
|
-
};
|
|
135
|
-
process.stdin.on('data', onData);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { configDir, PACKAGE_ROOT, stateDir } from '../paths.js';
|
|
7
|
-
import { cliEntryPoint, serviceEnvironment } from './environment.js';
|
|
8
|
-
import { escapeXml } from './launchd.js';
|
|
9
|
-
const DEFAULT_STATE_TIMEOUT_MS = 20_000;
|
|
10
|
-
const STATE_POLL_INTERVAL_MS = 250;
|
|
11
|
-
/** The service name is machine-wide, so include the account to isolate each user's service. */
|
|
12
|
-
export function serviceId(username) {
|
|
13
|
-
return `herdr-remote-${username.replace(/[^A-Za-z0-9._-]/g, '_')}`;
|
|
14
|
-
}
|
|
15
|
-
export function currentServiceAccount() {
|
|
16
|
-
return {
|
|
17
|
-
domain: process.env.USERDOMAIN || '',
|
|
18
|
-
user: process.env.USERNAME || os.userInfo().username,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
/** Keep WinSW outside the npm package so updates do not touch a running executable. */
|
|
22
|
-
export function windowsServicePaths() {
|
|
23
|
-
const id = serviceId(currentServiceAccount().user);
|
|
24
|
-
const directory = path.join(stateDir(), 'service');
|
|
25
|
-
return {
|
|
26
|
-
id,
|
|
27
|
-
directory,
|
|
28
|
-
exePath: path.join(directory, `${id}.exe`),
|
|
29
|
-
unitPath: path.join(directory, `${id}.xml`),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function lastNonemptyLine(output) {
|
|
33
|
-
return output.trim().split(/\r?\n/).pop()?.trim();
|
|
34
|
-
}
|
|
35
|
-
export function parseWhoamiSid(output) {
|
|
36
|
-
const sid = lastNonemptyLine(output)?.match(/"([^"]+)"\s*$/)?.[1];
|
|
37
|
-
return sid && /^S-\d(?:-\d+)+$/.test(sid) ? sid : null;
|
|
38
|
-
}
|
|
39
|
-
export function parseWinswState(output) {
|
|
40
|
-
const state = lastNonemptyLine(output);
|
|
41
|
-
return state === 'Started' || state === 'Stopped' || state === 'NonExistent'
|
|
42
|
-
? state
|
|
43
|
-
: 'NonExistent';
|
|
44
|
-
}
|
|
45
|
-
export function windowsServiceSddl(userSid) {
|
|
46
|
-
// SYSTEM, Administrators, interactive users and service users keep the default service DACL.
|
|
47
|
-
// The last ACE lets ordinary plugin hooks and the TUI start or stop this user's service.
|
|
48
|
-
const accessEntries = [
|
|
49
|
-
'(A;;CCLCSWRPWPDTLOCRRC;;;SY)',
|
|
50
|
-
'(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)',
|
|
51
|
-
'(A;;CCLCSWLOCRRC;;;IU)',
|
|
52
|
-
'(A;;CCLCSWLOCRRC;;;SU)',
|
|
53
|
-
`(A;;CCLCSWRPWPDTLOCRRC;;;${userSid})`,
|
|
54
|
-
];
|
|
55
|
-
return 'D:' + accessEntries.join('');
|
|
56
|
-
}
|
|
57
|
-
export function renderWinswConfig(options) {
|
|
58
|
-
const { id, user, domain, userSid, nodePath, entryPoint, workingDirectory, environment } = options;
|
|
59
|
-
const environmentLines = Object.entries(environment).map(([key, value]) => ` <env name="${escapeXml(key)}" value="${escapeXml(value)}"/>`);
|
|
60
|
-
// Children write their own logs; WinSW captures the supervisor in supervisor.out.log.
|
|
61
|
-
return `<service>
|
|
62
|
-
<id>${escapeXml(id)}</id>
|
|
63
|
-
<name>${escapeXml(`Herdr Remote (${user})`)}</name>
|
|
64
|
-
<description>${escapeXml(`Browser access to Herdr workspaces, running as ${domain}\\${user}.`)}</description>
|
|
65
|
-
<executable>${escapeXml(nodePath)}</executable>
|
|
66
|
-
<arguments>${escapeXml(`"${entryPoint}" run --daemon`)}</arguments>
|
|
67
|
-
<workingdirectory>${escapeXml(workingDirectory)}</workingdirectory>
|
|
68
|
-
<startmode>Automatic</startmode>
|
|
69
|
-
<delayedAutoStart/>
|
|
70
|
-
<onfailure action="restart" delay="3 sec"/>
|
|
71
|
-
<resetfailure>1 hour</resetfailure>
|
|
72
|
-
<stoptimeout>15 sec</stoptimeout>
|
|
73
|
-
<logpath>${escapeXml(workingDirectory)}</logpath>
|
|
74
|
-
<logname>supervisor</logname>
|
|
75
|
-
<log mode="append"/>
|
|
76
|
-
${environmentLines.join('\n')}
|
|
77
|
-
<serviceaccount>
|
|
78
|
-
<domain>${escapeXml(domain)}</domain>
|
|
79
|
-
<user>${escapeXml(user)}</user>
|
|
80
|
-
<allowservicelogon>true</allowservicelogon>
|
|
81
|
-
</serviceaccount>
|
|
82
|
-
<securityDescriptor>${escapeXml(windowsServiceSddl(userSid))}</securityDescriptor>
|
|
83
|
-
</service>
|
|
84
|
-
`;
|
|
85
|
-
}
|
|
86
|
-
export function escapePowerShellSingleQuoted(value) {
|
|
87
|
-
return value.replace(/'/g, "''");
|
|
88
|
-
}
|
|
89
|
-
export function renderElevationScript({ nodePath, entryPoint }) {
|
|
90
|
-
const executable = escapePowerShellSingleQuoted(nodePath);
|
|
91
|
-
const scriptPath = escapePowerShellSingleQuoted(entryPoint);
|
|
92
|
-
return `$ErrorActionPreference = 'Stop'
|
|
93
|
-
try {
|
|
94
|
-
$process = Start-Process -FilePath '${executable}' -ArgumentList '"${scriptPath}" keepalive install --elevated' -Verb RunAs -Wait -PassThru
|
|
95
|
-
} catch {
|
|
96
|
-
exit 1223
|
|
97
|
-
}
|
|
98
|
-
exit $process.ExitCode`;
|
|
99
|
-
}
|
|
100
|
-
export function encodePowerShellCommand(script) {
|
|
101
|
-
return Buffer.from(script, 'utf16le').toString('base64');
|
|
102
|
-
}
|
|
103
|
-
function text(value) {
|
|
104
|
-
return value ? String(value).trim() : '';
|
|
105
|
-
}
|
|
106
|
-
function runSc(command) {
|
|
107
|
-
const result = spawnSync('sc.exe', [command, windowsServicePaths().id], {
|
|
108
|
-
encoding: 'utf8',
|
|
109
|
-
windowsHide: true,
|
|
110
|
-
});
|
|
111
|
-
return { code: result.status, stdout: text(result.stdout), stderr: text(result.stderr) };
|
|
112
|
-
}
|
|
113
|
-
function scFailure(command, result) {
|
|
114
|
-
const output = result.stdout || result.stderr;
|
|
115
|
-
return new Error(`sc.exe ${command} failed (${result.code}): ${output}`);
|
|
116
|
-
}
|
|
117
|
-
/** sc.exe returns Win32 codes: 1056 means already running and 1069 means service logon failed. */
|
|
118
|
-
export function classifyStartResult(result) {
|
|
119
|
-
if (result.code === 0 || result.code === 1056)
|
|
120
|
-
return 'ok';
|
|
121
|
-
if (result.code === 1069)
|
|
122
|
-
return 'logon-failed';
|
|
123
|
-
return scFailure('start', result);
|
|
124
|
-
}
|
|
125
|
-
export function startService() {
|
|
126
|
-
return runSc('start');
|
|
127
|
-
}
|
|
128
|
-
export function stopService() {
|
|
129
|
-
return runSc('stop');
|
|
130
|
-
}
|
|
131
|
-
export function serviceState() {
|
|
132
|
-
const { exePath } = windowsServicePaths();
|
|
133
|
-
if (!fs.existsSync(exePath))
|
|
134
|
-
return 'NonExistent';
|
|
135
|
-
const result = spawnSync(exePath, ['status'], { encoding: 'utf8', windowsHide: true });
|
|
136
|
-
return result.error || result.status !== 0 ? 'NonExistent' : parseWinswState(text(result.stdout));
|
|
137
|
-
}
|
|
138
|
-
export function waitForState(target, timeoutMs = DEFAULT_STATE_TIMEOUT_MS) {
|
|
139
|
-
const deadlineMs = Date.now() + timeoutMs;
|
|
140
|
-
while (serviceState() !== target) {
|
|
141
|
-
if (Date.now() >= deadlineMs)
|
|
142
|
-
throw new Error(`Windows service did not reach ${target} within ${timeoutMs} ms`);
|
|
143
|
-
// Lifecycle start/restart is synchronous, so this wait must not yield to an event loop.
|
|
144
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, STATE_POLL_INTERVAL_MS);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function stop() {
|
|
148
|
-
const result = stopService();
|
|
149
|
-
if (result.code !== 0 && result.code !== 1062 && result.code !== 1060)
|
|
150
|
-
throw scFailure('stop', result);
|
|
151
|
-
}
|
|
152
|
-
function start() {
|
|
153
|
-
const commandResult = startService();
|
|
154
|
-
const result = classifyStartResult(commandResult);
|
|
155
|
-
if (result === 'logon-failed') {
|
|
156
|
-
const output = commandResult.stdout || commandResult.stderr;
|
|
157
|
-
throw new Error(`Windows service logon failed (1069): ${output}`);
|
|
158
|
-
}
|
|
159
|
-
if (result instanceof Error)
|
|
160
|
-
throw result;
|
|
161
|
-
return { ok: true };
|
|
162
|
-
}
|
|
163
|
-
function sha256(filePath) {
|
|
164
|
-
return createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
165
|
-
}
|
|
166
|
-
async function prepare() {
|
|
167
|
-
const paths = windowsServicePaths();
|
|
168
|
-
await fs.promises.mkdir(paths.directory, { recursive: true });
|
|
169
|
-
const source = path.join(PACKAGE_ROOT, 'vendor', 'winsw', 'WinSW.NET461.exe');
|
|
170
|
-
if (!fs.existsSync(paths.exePath) || sha256(paths.exePath) !== sha256(source)) {
|
|
171
|
-
try {
|
|
172
|
-
await fs.promises.copyFile(source, paths.exePath);
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
if (!['EBUSY', 'EPERM', 'EACCES'].includes(error.code || ''))
|
|
176
|
-
throw error;
|
|
177
|
-
throw new Error(`Could not replace ${paths.exePath}; stop the Windows service first.`, {
|
|
178
|
-
cause: error,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// A service may not receive reliable APPDATA, so pin both canonical directories.
|
|
183
|
-
const environment = {
|
|
184
|
-
...serviceEnvironment(),
|
|
185
|
-
HERDR_REMOTE_CONFIG_DIR: configDir(),
|
|
186
|
-
HERDR_REMOTE_STATE_DIR: stateDir(),
|
|
187
|
-
HERDR_REMOTE_SERVICE: '1',
|
|
188
|
-
};
|
|
189
|
-
const result = spawnSync('whoami', ['/user', '/fo', 'csv', '/nh'], {
|
|
190
|
-
encoding: 'utf8',
|
|
191
|
-
windowsHide: true,
|
|
192
|
-
});
|
|
193
|
-
const userSid = !result.error && result.status === 0 ? parseWhoamiSid(text(result.stdout)) : null;
|
|
194
|
-
if (!userSid)
|
|
195
|
-
throw new Error(`Could not read the current Windows user SID: ${text(result.stderr)}`);
|
|
196
|
-
const { user, domain } = currentServiceAccount();
|
|
197
|
-
await fs.promises.writeFile(paths.unitPath, renderWinswConfig({
|
|
198
|
-
id: paths.id,
|
|
199
|
-
user,
|
|
200
|
-
domain,
|
|
201
|
-
userSid,
|
|
202
|
-
nodePath: process.execPath,
|
|
203
|
-
entryPoint: cliEntryPoint(),
|
|
204
|
-
workingDirectory: stateDir(),
|
|
205
|
-
environment,
|
|
206
|
-
}), 'utf8');
|
|
207
|
-
return paths;
|
|
208
|
-
}
|
|
209
|
-
function waitForChild(child) {
|
|
210
|
-
return new Promise((resolve, reject) => {
|
|
211
|
-
child.once('error', reject);
|
|
212
|
-
child.once('close', (code) => resolve(code ?? 1));
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
/** The password is entered only in the elevated console, so wait for that window to finish. */
|
|
216
|
-
async function install() {
|
|
217
|
-
const paths = await prepare();
|
|
218
|
-
const script = renderElevationScript({ nodePath: process.execPath, entryPoint: cliEntryPoint() });
|
|
219
|
-
const child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encodePowerShellCommand(script)], { windowsHide: true, stdio: 'ignore' });
|
|
220
|
-
const exitCode = await waitForChild(child);
|
|
221
|
-
if (exitCode === 1223) {
|
|
222
|
-
const error = new Error('Administrator rights were not granted; nothing was installed.');
|
|
223
|
-
error.code = 'ELEVATION_CANCELLED';
|
|
224
|
-
throw error;
|
|
225
|
-
}
|
|
226
|
-
if (exitCode !== 0)
|
|
227
|
-
throw new Error(`Windows service installation did not complete (exit code ${exitCode}).`);
|
|
228
|
-
return { ok: true, unitPath: paths.unitPath, state: serviceState() };
|
|
229
|
-
}
|
|
230
|
-
function status() {
|
|
231
|
-
const paths = windowsServicePaths();
|
|
232
|
-
const state = serviceState();
|
|
233
|
-
const installed = state !== 'NonExistent';
|
|
234
|
-
return {
|
|
235
|
-
manager: 'windows-service',
|
|
236
|
-
installed,
|
|
237
|
-
active: state === 'Started',
|
|
238
|
-
enabled: installed,
|
|
239
|
-
unitPath: paths.unitPath,
|
|
240
|
-
state,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
/** WinSW requests elevation itself, so stopping first avoids a second UAC prompt. */
|
|
244
|
-
async function uninstall() {
|
|
245
|
-
const paths = windowsServicePaths();
|
|
246
|
-
stop();
|
|
247
|
-
waitForState('Stopped');
|
|
248
|
-
const result = spawnSync(paths.exePath, ['uninstall'], { windowsHide: true });
|
|
249
|
-
if (result.error || result.status !== 0)
|
|
250
|
-
throw new Error(`WinSW uninstall failed with exit code ${String(result.status)}: ${text(result.stdout)}`);
|
|
251
|
-
if (serviceState() !== 'NonExistent')
|
|
252
|
-
throw new Error(`Windows service still exists: ${paths.id}`);
|
|
253
|
-
fs.rmSync(paths.directory, { recursive: true, force: true });
|
|
254
|
-
return { ok: true, unitPath: paths.unitPath };
|
|
255
|
-
}
|
|
256
|
-
function restart() {
|
|
257
|
-
stop();
|
|
258
|
-
waitForState('Stopped');
|
|
259
|
-
return start();
|
|
260
|
-
}
|
|
261
|
-
function logsHint() {
|
|
262
|
-
return `Get-Content -Wait -Tail 40 "${path.join(stateDir(), 'supervisor.out.log')}"`;
|
|
263
|
-
}
|
|
264
|
-
export const windowsService = {
|
|
265
|
-
name: 'windows-service',
|
|
266
|
-
status,
|
|
267
|
-
install,
|
|
268
|
-
uninstall,
|
|
269
|
-
restart,
|
|
270
|
-
stop,
|
|
271
|
-
logsHint,
|
|
272
|
-
};
|