herdr-remote 0.2.39 → 0.3.32

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