herdr-remote 0.2.26 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/README.zh-CN.md +3 -2
- package/dist/cli.js +27 -2
- package/dist/connector/admin-broker.js +123 -29
- package/dist/connector/admin-shell.js +65 -0
- package/dist/connector/admin-tab.js +90 -0
- package/dist/connector/adminBrokerEndpoint.js +68 -0
- package/dist/connector/elevatedPty.js +31 -35
- package/dist/connector/host-connector.js +8 -1
- package/dist/connector/sessions.js +10 -39
- package/dist/connector/update-report.js +2 -0
- package/dist/i18n/en.js +13 -0
- package/dist/i18n/zh.js +13 -0
- package/dist/keepalive/detached.js +3 -2
- package/dist/lifecycle.js +6 -2
- package/dist/node-pty-loader.js +80 -0
- package/dist/pty-session.js +3 -29
- package/dist/service.js +3 -1
- package/dist/supervisor.js +2 -2
- package/dist/tui/api.js +2 -2
- package/dist/tui/screens/Keepalive.js +1 -1
- package/dist/update-command.js +120 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/dist/connector/adminBrokerPipe.js +0 -16
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// `herdr-remote update`: install the newest release and restart what was running.
|
|
2
|
+
//
|
|
3
|
+
// The TUI's update row installs and leaves the restart to a second key press;
|
|
4
|
+
// a command run over SSH or from a script has nobody to press it. The restart
|
|
5
|
+
// is run by the release just installed, not by this process: this process
|
|
6
|
+
// still runs the old code, and would start the services as the old release
|
|
7
|
+
// described them.
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { loadConfig } from './config.js';
|
|
10
|
+
import { startAdminBrokerTask, stopAdminBroker, waitForAdminBroker, } from './connector/admin-broker.js';
|
|
11
|
+
import { cliEntryPoint } from './keepalive/environment.js';
|
|
12
|
+
import { servicesRunning, startAll, stopAll } from './lifecycle.js';
|
|
13
|
+
import { checkForRelayUpdate, performRelayUpdate } from './relay-updater.js';
|
|
14
|
+
import { checkForUpdate, performUpdate } from './updater.js';
|
|
15
|
+
const RESTART_TIMEOUT_MS = 120_000;
|
|
16
|
+
function restartWithInstalledRelease() {
|
|
17
|
+
const result = spawnSync(process.execPath, [cliEntryPoint(), 'restart', '--json'], {
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
timeout: RESTART_TIMEOUT_MS,
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
});
|
|
22
|
+
if (!result.error && result.status === 0)
|
|
23
|
+
return { ok: true, message: '' };
|
|
24
|
+
const output = String(result.stderr || result.stdout || '').trim();
|
|
25
|
+
return { ok: false, message: result.error?.message || output || `exit ${result.status}` };
|
|
26
|
+
}
|
|
27
|
+
export async function runUpdate(t, options = {}) {
|
|
28
|
+
const config = () => options.config ?? loadConfig();
|
|
29
|
+
const { platform = process.platform, checkCli = () => checkForUpdate(), checkRelay = () => checkForRelayUpdate(), installCli = (check, onAttempt) => performUpdate({
|
|
30
|
+
registry: check.registry,
|
|
31
|
+
sources: check.sources,
|
|
32
|
+
version: check.latest,
|
|
33
|
+
onAttempt,
|
|
34
|
+
}), installRelay = (check, onAttempt) => performRelayUpdate({
|
|
35
|
+
registry: check.registry,
|
|
36
|
+
sources: check.sources,
|
|
37
|
+
relayVersion: check.latest ?? '',
|
|
38
|
+
onAttempt,
|
|
39
|
+
}), isRunning = () => servicesRunning(config()), stop = () => stopAll(config()), start = () => startAll(config()), restartInstalled = restartWithInstalledRelease, stopBroker = stopAdminBroker, startBroker = async () => {
|
|
40
|
+
try {
|
|
41
|
+
startAdminBrokerTask(t);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Denied or no task: the broker does not answer, which is reported next.
|
|
45
|
+
}
|
|
46
|
+
return waitForAdminBroker();
|
|
47
|
+
}, report = (line) => process.stdout.write(`${line}\n`), } = options;
|
|
48
|
+
report(t('update.checking'));
|
|
49
|
+
const [cli, relay] = await Promise.all([checkCli(), checkRelay()]);
|
|
50
|
+
const { current } = cli;
|
|
51
|
+
if (!cli.ok) {
|
|
52
|
+
report(t('update.errorNetworkDetail', { message: cli.message ?? '' }));
|
|
53
|
+
return { ok: false, current, updated: null, restarted: false, errorKey: cli.errorKey };
|
|
54
|
+
}
|
|
55
|
+
// A new herdr-remote brings the newest relay its range allows; only a
|
|
56
|
+
// current one needs the relay installed by itself.
|
|
57
|
+
const updated = cli.updateAvailable ? 'cli' : relay.ok && relay.updateAvailable ? 'relay' : null;
|
|
58
|
+
if (!updated) {
|
|
59
|
+
report(t('update.upToDate', { version: current }));
|
|
60
|
+
return { ok: true, current, updated, restarted: false };
|
|
61
|
+
}
|
|
62
|
+
const version = (updated === 'cli' ? cli.latest : relay.latest) ?? '';
|
|
63
|
+
const wasRunning = isRunning();
|
|
64
|
+
// Windows refuses to replace files a running process holds, as services
|
|
65
|
+
// started by older releases do. Elsewhere they keep serving until the restart.
|
|
66
|
+
const stopped = platform === 'win32' && wasRunning;
|
|
67
|
+
if (stopped) {
|
|
68
|
+
report(t('update.stoppingServices'));
|
|
69
|
+
stop();
|
|
70
|
+
}
|
|
71
|
+
// The admin broker stops too, so its task starts it on the new release.
|
|
72
|
+
const broker = platform === 'win32' ? await stopBroker() : 'unavailable';
|
|
73
|
+
const restoreBroker = async () => {
|
|
74
|
+
if (broker !== 'stopped')
|
|
75
|
+
return broker === 'busy' ? 'busy' : null;
|
|
76
|
+
const back = await startBroker();
|
|
77
|
+
report(t(back ? 'update.brokerRestarted' : 'update.brokerRestartFailed'));
|
|
78
|
+
return back ? 'restarted' : 'failed';
|
|
79
|
+
};
|
|
80
|
+
report(t(updated === 'cli' ? 'update.updating' : 'relayUpdate.updating', { version }));
|
|
81
|
+
const onAttempt = ({ attempt }) => {
|
|
82
|
+
if (attempt > 1)
|
|
83
|
+
report(t('update.updatingRetry', { version, attempt }));
|
|
84
|
+
};
|
|
85
|
+
const result = updated === 'cli' ? await installCli(cli, onAttempt) : await installRelay(relay, onAttempt);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
// Nothing was replaced, so the old release starts as it was.
|
|
88
|
+
if (stopped)
|
|
89
|
+
start();
|
|
90
|
+
const adminBroker = await restoreBroker();
|
|
91
|
+
report(t(result.errorKey ?? 'update.errorFailed', { version, installed: result.installed ?? '' }));
|
|
92
|
+
if (result.summary)
|
|
93
|
+
report(t('update.errorFailedDetail', { message: result.summary }));
|
|
94
|
+
if (platform === 'win32' && /\b(EBUSY|EPERM)\b/.test(result.output ?? '')) {
|
|
95
|
+
report(t('update.errorBusy'));
|
|
96
|
+
}
|
|
97
|
+
const { errorKey, summary } = result;
|
|
98
|
+
return { ok: false, current, updated, restarted: false, adminBroker, errorKey, summary };
|
|
99
|
+
}
|
|
100
|
+
const installed = result.installed ?? version;
|
|
101
|
+
report(t(updated === 'cli' ? 'update.done' : 'relayUpdate.done', { version: installed }));
|
|
102
|
+
let ok = true;
|
|
103
|
+
let restarted = false;
|
|
104
|
+
if (!wasRunning) {
|
|
105
|
+
report(t('update.notRunning'));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
report(t('update.restarting'));
|
|
109
|
+
const restart = restartInstalled();
|
|
110
|
+
ok = restart.ok;
|
|
111
|
+
restarted = restart.ok;
|
|
112
|
+
report(restart.ok
|
|
113
|
+
? t('services.restarted')
|
|
114
|
+
: t('update.restartFailed', { message: restart.message }));
|
|
115
|
+
}
|
|
116
|
+
if (broker === 'busy')
|
|
117
|
+
report(t('update.brokerBusy'));
|
|
118
|
+
const adminBroker = await restoreBroker();
|
|
119
|
+
return { ok, current, updated, installed, restarted, adminBroker };
|
|
120
|
+
}
|
package/herdr-plugin.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
const ADMIN_BROKER_TASK_NAME = 'HerdrRemoteAdminBroker';
|
|
3
|
-
export const ADMIN_BROKER_MAX_MESSAGE_BYTES = 2 * 1_024 * 1_024;
|
|
4
|
-
function adminBrokerAccountId() {
|
|
5
|
-
const account = `${process.env.USERDOMAIN || ''}\\${process.env.USERNAME || process.env.USERPROFILE || 'default'}`;
|
|
6
|
-
return createHash('sha256').update(account.toLowerCase()).digest('hex').slice(0, 24);
|
|
7
|
-
}
|
|
8
|
-
export function adminBrokerPipePath() {
|
|
9
|
-
return `\\\\.\\pipe\\herdr-remote-admin-${adminBrokerAccountId()}`;
|
|
10
|
-
}
|
|
11
|
-
export function adminBrokerTaskName() {
|
|
12
|
-
return `${ADMIN_BROKER_TASK_NAME}-${adminBrokerAccountId()}`;
|
|
13
|
-
}
|
|
14
|
-
export function sendAdminBrokerMessage(socket, message) {
|
|
15
|
-
socket.write(`${JSON.stringify(message)}\n`);
|
|
16
|
-
}
|