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.
Files changed (203) hide show
  1. package/dist/chunks/353DMT35.js +3 -0
  2. package/dist/chunks/42KARAL4.js +31 -0
  3. package/dist/chunks/7AUQHB3O.js +10 -0
  4. package/dist/chunks/7SMZ43LI.js +9 -0
  5. package/dist/chunks/AM3OEMFK.js +16 -0
  6. package/dist/chunks/ANNJESOL.js +10 -0
  7. package/dist/chunks/AWB4A65P.js +63 -0
  8. package/dist/chunks/C2UUZY6J.js +2 -0
  9. package/dist/chunks/DGUZCGQN.js +3 -0
  10. package/dist/chunks/H24ZP5D5.js +16 -0
  11. package/dist/chunks/I6NRCYLI.js +3 -0
  12. package/dist/chunks/JJGEV4EZ.js +3 -0
  13. package/dist/chunks/KRSURB5L.js +5 -0
  14. package/dist/chunks/KRVF37OW.js +3 -0
  15. package/dist/chunks/KUDCC7ZO.js +6 -0
  16. package/dist/chunks/M3NRPJK2.js +77 -0
  17. package/dist/chunks/M4AFZHKH.js +3 -0
  18. package/dist/chunks/M52LJEES.js +6 -0
  19. package/dist/chunks/O2LNVFUT.js +3 -0
  20. package/dist/chunks/OJPBI7KB.js +3 -0
  21. package/dist/chunks/OMN6C7J6.js +3 -0
  22. package/dist/chunks/PBMBFWLQ.js +3 -0
  23. package/dist/chunks/PBRQ3HEG.js +3 -0
  24. package/dist/chunks/PFFVGFPL.js +45 -0
  25. package/dist/chunks/QRGNEKME.js +5 -0
  26. package/dist/chunks/TNLCNVA2.js +3 -0
  27. package/dist/chunks/U27JJKJ6.js +3 -0
  28. package/dist/chunks/U6QAYG3E.js +3 -0
  29. package/dist/chunks/UEOQE77N.js +4 -0
  30. package/dist/chunks/WSACSE45.js +4 -0
  31. package/dist/chunks/WY7WHEJ4.js +3 -0
  32. package/dist/chunks/XJKWLK43.js +4 -0
  33. package/dist/chunks/XN6D5S55.js +3 -0
  34. package/dist/chunks/YD3MIP7B.js +5 -0
  35. package/dist/chunks/YXDQ62BB.js +4 -0
  36. package/dist/cli.js +21 -294
  37. package/dist/connector/admin-broker.js +3 -303
  38. package/dist/connector/main.js +20 -18
  39. package/dist/harfbuzz-subset.wasm +0 -0
  40. package/dist/postinstall.js +17 -0
  41. package/herdr-plugin.toml +1 -1
  42. package/node_modules/herdr-remote-relay/bin/herdr-remote-relay.js +33 -92
  43. package/node_modules/herdr-remote-relay/package.json +3 -46
  44. package/package.json +8 -27
  45. package/dist/agent-status.js +0 -130
  46. package/dist/binary-plist.js +0 -157
  47. package/dist/config.js +0 -278
  48. package/dist/connector/admin-broker-setup.js +0 -40
  49. package/dist/connector/admin-shell.js +0 -67
  50. package/dist/connector/admin-tab.js +0 -90
  51. package/dist/connector/adminBrokerEndpoint.js +0 -68
  52. package/dist/connector/agent-watch.js +0 -123
  53. package/dist/connector/elevatedPty.js +0 -141
  54. package/dist/connector/font-server.js +0 -165
  55. package/dist/connector/herdr-prefix-keys.js +0 -28
  56. package/dist/connector/host-connector.js +0 -433
  57. package/dist/connector/host-hello.js +0 -32
  58. package/dist/connector/lock.js +0 -73
  59. package/dist/connector/sessions.js +0 -381
  60. package/dist/connector/update-report.js +0 -70
  61. package/dist/connector/wire.js +0 -42
  62. package/dist/exit-codes.js +0 -13
  63. package/dist/font-subset.js +0 -116
  64. package/dist/herdr-api.js +0 -254
  65. package/dist/herdr-command.js +0 -245
  66. package/dist/herdr-plugin.js +0 -120
  67. package/dist/herdr-server.js +0 -190
  68. package/dist/i18n/en.js +0 -257
  69. package/dist/i18n/index.js +0 -48
  70. package/dist/i18n/zh.js +0 -256
  71. package/dist/keepalive/detached.js +0 -97
  72. package/dist/keepalive/environment.js +0 -44
  73. package/dist/keepalive/index.js +0 -129
  74. package/dist/keepalive/launchd.js +0 -117
  75. package/dist/keepalive/systemd.js +0 -112
  76. package/dist/keepalive/types.js +0 -1
  77. package/dist/keepalive/windows-service-setup.js +0 -138
  78. package/dist/keepalive/windows-service.js +0 -272
  79. package/dist/keepalive/windows-task.js +0 -191
  80. package/dist/lib/process.js +0 -132
  81. package/dist/lifecycle.js +0 -79
  82. package/dist/net-interfaces.js +0 -82
  83. package/dist/node-pty-loader.js +0 -80
  84. package/dist/pasted-files.js +0 -115
  85. package/dist/paths.js +0 -74
  86. package/dist/pty-session.js +0 -133
  87. package/dist/relay-client.js +0 -106
  88. package/dist/relay-urls.js +0 -99
  89. package/dist/runtime.js +0 -107
  90. package/dist/service.js +0 -267
  91. package/dist/settings-model.js +0 -298
  92. package/dist/socket-discovery.js +0 -70
  93. package/dist/supervisor.js +0 -323
  94. package/dist/terminal-font/deps.js +0 -83
  95. package/dist/terminal-font/files.js +0 -357
  96. package/dist/terminal-font/formats.js +0 -355
  97. package/dist/terminal-font/index.js +0 -187
  98. package/dist/terminal-font/readers.js +0 -373
  99. package/dist/terminal-font/terminals.js +0 -147
  100. package/dist/terminal-palette.js +0 -399
  101. package/dist/tui/App.js +0 -199
  102. package/dist/tui/api.js +0 -91
  103. package/dist/tui/components/TextField.js +0 -50
  104. package/dist/tui/components/common.js +0 -80
  105. package/dist/tui/index.js +0 -36
  106. package/dist/tui/mouse/index.js +0 -164
  107. package/dist/tui/mouse/protocol.js +0 -96
  108. package/dist/tui/mouse/source.js +0 -79
  109. package/dist/tui/screens/About.js +0 -172
  110. package/dist/tui/screens/Herdr.js +0 -75
  111. package/dist/tui/screens/Keepalive.js +0 -141
  112. package/dist/tui/screens/Overview.js +0 -36
  113. package/dist/tui/screens/Pair.js +0 -74
  114. package/dist/tui/screens/Relay.js +0 -243
  115. package/dist/tui/screens/Services.js +0 -63
  116. package/dist/tui/screens/Wizard.js +0 -241
  117. package/dist/tui/terminal.js +0 -39
  118. package/dist/tui/theme.js +0 -33
  119. package/dist/update-command.js +0 -118
  120. package/dist/updater/in-place.js +0 -83
  121. package/dist/updater/npm.js +0 -62
  122. package/dist/updater.js +0 -401
  123. package/node_modules/herdr-remote-relay/README.md +0 -76
  124. package/node_modules/herdr-remote-relay/README.zh-CN.md +0 -76
  125. package/node_modules/herdr-remote-relay/config.example.json +0 -18
  126. package/node_modules/herdr-remote-relay/deploy/cloudflared/config.yml.example +0 -9
  127. package/node_modules/herdr-remote-relay/deploy/docker/compose.yaml +0 -23
  128. package/node_modules/herdr-remote-relay/deploy/nginx/herdr-remote-relay.conf.example +0 -49
  129. package/node_modules/herdr-remote-relay/deploy/systemd/herdr-remote-relay.service +0 -41
  130. package/node_modules/herdr-remote-relay/deploy/systemd/relay.env.example +0 -25
  131. package/node_modules/herdr-remote-relay/dist/auth-store.d.ts +0 -114
  132. package/node_modules/herdr-remote-relay/dist/auth-store.js +0 -289
  133. package/node_modules/herdr-remote-relay/dist/metrics.d.ts +0 -76
  134. package/node_modules/herdr-remote-relay/dist/metrics.js +0 -249
  135. package/node_modules/herdr-remote-relay/dist/protocol/frames.d.ts +0 -27
  136. package/node_modules/herdr-remote-relay/dist/protocol/frames.js +0 -110
  137. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.d.ts +0 -8
  138. package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.js +0 -34
  139. package/node_modules/herdr-remote-relay/dist/protocol/http.d.ts +0 -165
  140. package/node_modules/herdr-remote-relay/dist/protocol/http.js +0 -3
  141. package/node_modules/herdr-remote-relay/dist/protocol/index.d.ts +0 -7
  142. package/node_modules/herdr-remote-relay/dist/protocol/index.js +0 -9
  143. package/node_modules/herdr-remote-relay/dist/protocol/messages.d.ts +0 -362
  144. package/node_modules/herdr-remote-relay/dist/protocol/messages.js +0 -20
  145. package/node_modules/herdr-remote-relay/dist/protocol/paste.d.ts +0 -14
  146. package/node_modules/herdr-remote-relay/dist/protocol/paste.js +0 -50
  147. package/node_modules/herdr-remote-relay/dist/protocol/release.d.ts +0 -8
  148. package/node_modules/herdr-remote-relay/dist/protocol/release.js +0 -17
  149. package/node_modules/herdr-remote-relay/dist/protocol/terminal.d.ts +0 -79
  150. package/node_modules/herdr-remote-relay/dist/protocol/terminal.js +0 -150
  151. package/node_modules/herdr-remote-relay/dist/relay-config.d.ts +0 -74
  152. package/node_modules/herdr-remote-relay/dist/relay-config.js +0 -316
  153. package/node_modules/herdr-remote-relay/dist/relay-server.d.ts +0 -67
  154. package/node_modules/herdr-remote-relay/dist/relay-server.js +0 -196
  155. package/node_modules/herdr-remote-relay/dist/scroll-input.d.ts +0 -17
  156. package/node_modules/herdr-remote-relay/dist/scroll-input.js +0 -124
  157. package/node_modules/herdr-remote-relay/dist/server/client-channel.d.ts +0 -3
  158. package/node_modules/herdr-remote-relay/dist/server/client-channel.js +0 -396
  159. package/node_modules/herdr-remote-relay/dist/server/font-proxy.d.ts +0 -19
  160. package/node_modules/herdr-remote-relay/dist/server/font-proxy.js +0 -155
  161. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.d.ts +0 -27
  162. package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.js +0 -83
  163. package/node_modules/herdr-remote-relay/dist/server/host-channel.d.ts +0 -3
  164. package/node_modules/herdr-remote-relay/dist/server/host-channel.js +0 -271
  165. package/node_modules/herdr-remote-relay/dist/server/http-api.d.ts +0 -4
  166. package/node_modules/herdr-remote-relay/dist/server/http-api.js +0 -219
  167. package/node_modules/herdr-remote-relay/dist/server/maintenance.d.ts +0 -5
  168. package/node_modules/herdr-remote-relay/dist/server/maintenance.js +0 -64
  169. package/node_modules/herdr-remote-relay/dist/server/requests.d.ts +0 -34
  170. package/node_modules/herdr-remote-relay/dist/server/requests.js +0 -142
  171. package/node_modules/herdr-remote-relay/dist/server/sessions.d.ts +0 -36
  172. package/node_modules/herdr-remote-relay/dist/server/sessions.js +0 -230
  173. package/node_modules/herdr-remote-relay/dist/server/sockets.d.ts +0 -12
  174. package/node_modules/herdr-remote-relay/dist/server/sockets.js +0 -64
  175. package/node_modules/herdr-remote-relay/dist/server/status.d.ts +0 -11
  176. package/node_modules/herdr-remote-relay/dist/server/status.js +0 -99
  177. package/node_modules/herdr-remote-relay/dist/server/transport.d.ts +0 -16
  178. package/node_modules/herdr-remote-relay/dist/server/transport.js +0 -110
  179. package/node_modules/herdr-remote-relay/dist/server/types.d.ts +0 -133
  180. package/node_modules/herdr-remote-relay/dist/server/types.js +0 -3
  181. package/node_modules/herdr-remote-relay/dist/server/version.d.ts +0 -2
  182. package/node_modules/herdr-remote-relay/dist/server/version.js +0 -5
  183. package/node_modules/herdr-remote-relay/dist/state.d.ts +0 -6
  184. package/node_modules/herdr-remote-relay/dist/state.js +0 -47
  185. package/node_modules/ws/LICENSE +0 -20
  186. package/node_modules/ws/README.md +0 -548
  187. package/node_modules/ws/browser.js +0 -8
  188. package/node_modules/ws/index.js +0 -22
  189. package/node_modules/ws/lib/buffer-util.js +0 -131
  190. package/node_modules/ws/lib/constants.js +0 -19
  191. package/node_modules/ws/lib/event-target.js +0 -292
  192. package/node_modules/ws/lib/extension.js +0 -203
  193. package/node_modules/ws/lib/limiter.js +0 -55
  194. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  195. package/node_modules/ws/lib/receiver.js +0 -743
  196. package/node_modules/ws/lib/sender.js +0 -607
  197. package/node_modules/ws/lib/stream.js +0 -161
  198. package/node_modules/ws/lib/subprotocol.js +0 -62
  199. package/node_modules/ws/lib/validation.js +0 -152
  200. package/node_modules/ws/lib/websocket-server.js +0 -562
  201. package/node_modules/ws/lib/websocket.js +0 -1407
  202. package/node_modules/ws/package.json +0 -74
  203. 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
- };
@@ -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
- };