herdr-remote 0.2.14 → 0.2.16
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/config.example.json +2 -1
- package/dist/tui.mjs +63 -184
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/agent-status.js +14 -6
- package/src/config.js +4 -0
- package/src/herdr-server.js +201 -0
- package/src/host-connector.js +117 -6
- package/src/i18n/en.js +2 -8
- package/src/i18n/zh.js +2 -8
- package/src/lifecycle.js +6 -2
- package/src/settings-model.js +11 -0
- package/src/socket-discovery.js +6 -1
package/config.example.json
CHANGED
package/dist/tui.mjs
CHANGED
|
@@ -90,7 +90,10 @@ var require_config = __commonJS({
|
|
|
90
90
|
herdr: {
|
|
91
91
|
socketPath: null,
|
|
92
92
|
args: [],
|
|
93
|
-
cwd: os.homedir()
|
|
93
|
+
cwd: os.homedir(),
|
|
94
|
+
// Start Herdr's server when herdr-remote starts (at boot, under keep-alive).
|
|
95
|
+
// Off: Herdr starts when the user runs it, or confirms it from the WebUI.
|
|
96
|
+
autoStart: false
|
|
94
97
|
},
|
|
95
98
|
auth: {
|
|
96
99
|
pairingTtlMs: 10 * 60 * 1e3,
|
|
@@ -251,6 +254,7 @@ var require_config = __commonJS({
|
|
|
251
254
|
if (typeof config.herdr.cwd !== "string" || config.herdr.cwd.length === 0) {
|
|
252
255
|
config.herdr.cwd = os.homedir();
|
|
253
256
|
}
|
|
257
|
+
config.herdr.autoStart = config.herdr.autoStart === true;
|
|
254
258
|
if (config.relay.mode === "remote" && !config.relay.remoteUrl) {
|
|
255
259
|
config.relay.mode = "local";
|
|
256
260
|
}
|
|
@@ -659,7 +663,7 @@ var require_herdr_command = __commonJS({
|
|
|
659
663
|
if (!version) return true;
|
|
660
664
|
return compareVersions(version, minimum) >= 0;
|
|
661
665
|
}
|
|
662
|
-
function
|
|
666
|
+
function herdrVersion2({ command, timeout = 1e4, ...lookup } = {}) {
|
|
663
667
|
const resolved = command || resolveHerdrCommand(lookup);
|
|
664
668
|
const unknown = { command: resolved, version: null, raw: null, supported: true, ok: false };
|
|
665
669
|
let result;
|
|
@@ -692,7 +696,7 @@ var require_herdr_command = __commonJS({
|
|
|
692
696
|
findHerdrCommand,
|
|
693
697
|
herdrNotFoundMessage,
|
|
694
698
|
herdrOutdatedMessage,
|
|
695
|
-
herdrVersion,
|
|
699
|
+
herdrVersion: herdrVersion2,
|
|
696
700
|
meetsMinimum,
|
|
697
701
|
parseHerdrVersion,
|
|
698
702
|
resolveHerdrCommand,
|
|
@@ -839,14 +843,7 @@ var require_en = __commonJS({
|
|
|
839
843
|
"herdr.title": "Herdr integration",
|
|
840
844
|
"herdr.socketPath": "Socket path",
|
|
841
845
|
"herdr.args": "Extra arguments",
|
|
842
|
-
"herdr.
|
|
843
|
-
"herdr.pluginRegistered": "Registered with Herdr.",
|
|
844
|
-
"herdr.pluginMissing": "Not registered with Herdr.",
|
|
845
|
-
"herdr.register": "Register as Herdr plugin",
|
|
846
|
-
"herdr.unregister": "Unregister plugin",
|
|
847
|
-
"herdr.registerDone": "Registered: {path}",
|
|
848
|
-
"herdr.unregisterDone": "Plugin unregistered.",
|
|
849
|
-
"herdr.registerFailed": "Registration failed: {message}",
|
|
846
|
+
"herdr.autoStartHint": "Off: Herdr starts only when you run it, or when you confirm it from a browser. On: herdr-remote also starts it every time it starts, at boot included. Takes effect the next time herdr-remote starts.",
|
|
850
847
|
"herdr.cliMissing": "Command herdr not found. Set HERDR_BIN_PATH to its full path.",
|
|
851
848
|
"herdr.version": "Herdr version",
|
|
852
849
|
"herdr.versionOutdated": 'Herdr {version} is older than {minimum}. Run "herdr update": window titles, background machine activation and large pastes misbehave in browser windows before that release.',
|
|
@@ -883,6 +880,7 @@ var require_en = __commonJS({
|
|
|
883
880
|
"field.publicUrl": "Browser URL override",
|
|
884
881
|
"field.socketPath": "Herdr socket path",
|
|
885
882
|
"field.herdrArgs": "Herdr arguments",
|
|
883
|
+
"field.herdrAutoStart": "Auto-start Herdr",
|
|
886
884
|
"field.language": "Language",
|
|
887
885
|
"field.keepalive": "Keep-alive manager",
|
|
888
886
|
"placeholder.autoDiscovered": "auto-discovered",
|
|
@@ -1066,14 +1064,7 @@ var require_zh = __commonJS({
|
|
|
1066
1064
|
"herdr.title": "Herdr \u96C6\u6210",
|
|
1067
1065
|
"herdr.socketPath": "\u5957\u63A5\u5B57\u8DEF\u5F84",
|
|
1068
1066
|
"herdr.args": "\u9644\u52A0\u53C2\u6570",
|
|
1069
|
-
"herdr.
|
|
1070
|
-
"herdr.pluginRegistered": "\u5DF2\u6CE8\u518C\u5230 Herdr\u3002",
|
|
1071
|
-
"herdr.pluginMissing": "\u5C1A\u672A\u6CE8\u518C\u5230 Herdr\u3002",
|
|
1072
|
-
"herdr.register": "\u6CE8\u518C\u4E3A Herdr \u63D2\u4EF6",
|
|
1073
|
-
"herdr.unregister": "\u53D6\u6D88\u6CE8\u518C",
|
|
1074
|
-
"herdr.registerDone": "\u5DF2\u6CE8\u518C\uFF1A{path}",
|
|
1075
|
-
"herdr.unregisterDone": "\u5DF2\u53D6\u6D88\u6CE8\u518C\u3002",
|
|
1076
|
-
"herdr.registerFailed": "\u6CE8\u518C\u5931\u8D25\uFF1A{message}",
|
|
1067
|
+
"herdr.autoStartHint": "\u5173\uFF1AHerdr \u53EA\u5728\u4F60\u624B\u52A8\u8FD0\u884C\uFF0C\u6216\u5728\u6D4F\u89C8\u5668\u91CC\u786E\u8BA4\u540E\u624D\u542F\u52A8\u3002\u5F00\uFF1Aherdr-remote \u6BCF\u6B21\u542F\u52A8\uFF08\u5305\u62EC\u5F00\u673A\uFF09\u65F6\u4E00\u5E76\u542F\u52A8 Herdr\u3002\u4E0B\u6B21 herdr-remote \u542F\u52A8\u65F6\u751F\u6548\u3002",
|
|
1077
1068
|
"herdr.cliMissing": "\u672A\u627E\u5230 herdr \u547D\u4EE4\uFF0C\u53EF\u5C06 HERDR_BIN_PATH \u8BBE\u4E3A\u5176\u5B8C\u6574\u8DEF\u5F84\u3002",
|
|
1078
1069
|
"herdr.version": "Herdr \u7248\u672C",
|
|
1079
1070
|
"herdr.versionOutdated": "Herdr {version} \u4F4E\u4E8E {minimum}\uFF0C\u8BF7\u8FD0\u884C \u201Cherdr update\u201D\uFF1A\u5728\u8BE5\u7248\u672C\u4E4B\u524D\uFF0C\u6D4F\u89C8\u5668\u7A97\u53E3\u7684\u6807\u9898\u3001\u540E\u53F0\u673A\u5668\u6FC0\u6D3B\u4E0E\u5927\u6BB5\u7C98\u8D34\u90FD\u4F1A\u51FA\u95EE\u9898\u3002",
|
|
@@ -1110,6 +1101,7 @@ var require_zh = __commonJS({
|
|
|
1110
1101
|
"field.publicUrl": "\u6D4F\u89C8\u5668\u5730\u5740\uFF08\u8986\u76D6\uFF09",
|
|
1111
1102
|
"field.socketPath": "Herdr \u5957\u63A5\u5B57\u8DEF\u5F84",
|
|
1112
1103
|
"field.herdrArgs": "Herdr \u53C2\u6570",
|
|
1104
|
+
"field.herdrAutoStart": "\u968F\u670D\u52A1\u542F\u52A8 Herdr",
|
|
1113
1105
|
"field.language": "\u8BED\u8A00",
|
|
1114
1106
|
"field.keepalive": "\u4FDD\u6D3B\u7BA1\u7406\u65B9\u5F0F",
|
|
1115
1107
|
"placeholder.autoDiscovered": "\u81EA\u52A8\u63A2\u6D4B",
|
|
@@ -1334,6 +1326,7 @@ var require_settings_model = __commonJS({
|
|
|
1334
1326
|
{ id: "publicUrl", kind: "text", labelKey: "field.publicUrl" },
|
|
1335
1327
|
{ id: "socketPath", kind: "text", labelKey: "field.socketPath" },
|
|
1336
1328
|
{ id: "herdrArgs", kind: "text", labelKey: "field.herdrArgs" },
|
|
1329
|
+
{ id: "herdrAutoStart", kind: "toggle", labelKey: "field.herdrAutoStart" },
|
|
1337
1330
|
{ id: "language", kind: "choice", choices: LANGUAGES, labelKey: "field.language" },
|
|
1338
1331
|
{ id: "keepaliveManager", kind: "choice", choices: KEEPALIVE_MANAGERS, labelKey: "field.keepalive" }
|
|
1339
1332
|
];
|
|
@@ -1371,6 +1364,8 @@ var require_settings_model = __commonJS({
|
|
|
1371
1364
|
return draft.herdr.socketPath || EMPTY;
|
|
1372
1365
|
case "herdrArgs":
|
|
1373
1366
|
return (draft.herdr.args || []).join(" ");
|
|
1367
|
+
case "herdrAutoStart":
|
|
1368
|
+
return draft.herdr.autoStart ? "on" : "off";
|
|
1374
1369
|
case "language":
|
|
1375
1370
|
return draft.ui.language;
|
|
1376
1371
|
case "keepaliveManager":
|
|
@@ -1464,6 +1459,13 @@ var require_settings_model = __commonJS({
|
|
|
1464
1459
|
next.herdr.args = args;
|
|
1465
1460
|
break;
|
|
1466
1461
|
}
|
|
1462
|
+
case "herdrAutoStart": {
|
|
1463
|
+
if (value !== true && value !== false && value !== "on" && value !== "off") {
|
|
1464
|
+
return { draft, errorKey: "error.unknownField" };
|
|
1465
|
+
}
|
|
1466
|
+
next.herdr.autoStart = value === true || value === "on";
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1467
1469
|
case "language": {
|
|
1468
1470
|
if (!LANGUAGES.includes(value)) return { draft, errorKey: "error.invalidLanguage" };
|
|
1469
1471
|
next.ui.language = value;
|
|
@@ -1509,7 +1511,8 @@ var require_settings_model = __commonJS({
|
|
|
1509
1511
|
merged.herdr = {
|
|
1510
1512
|
...current.herdr || {},
|
|
1511
1513
|
socketPath: draft.herdr.socketPath,
|
|
1512
|
-
args: draft.herdr.args
|
|
1514
|
+
args: draft.herdr.args,
|
|
1515
|
+
autoStart: draft.herdr.autoStart === true
|
|
1513
1516
|
};
|
|
1514
1517
|
if (!merged.herdr.socketPath) delete merged.herdr.socketPath;
|
|
1515
1518
|
merged.keepalive = { ...current.keepalive || {}, manager: draft.keepalive.manager };
|
|
@@ -1758,7 +1761,12 @@ var require_socket_discovery = __commonJS({
|
|
|
1758
1761
|
}
|
|
1759
1762
|
return { ok: true, path: socketPath, uid: stat.uid, mode: stat.mode };
|
|
1760
1763
|
} catch (error) {
|
|
1761
|
-
return {
|
|
1764
|
+
return {
|
|
1765
|
+
ok: false,
|
|
1766
|
+
missing: error.code === "ENOENT",
|
|
1767
|
+
reason: error.code === "ENOENT" ? "socket does not exist" : error.message,
|
|
1768
|
+
path: socketPath
|
|
1769
|
+
};
|
|
1762
1770
|
}
|
|
1763
1771
|
}
|
|
1764
1772
|
function assertSocket(socketPath, options = {}) {
|
|
@@ -1810,7 +1818,7 @@ var require_service = __commonJS({
|
|
|
1810
1818
|
} = require_terminal_palette();
|
|
1811
1819
|
var { resolveSocketPath } = require_socket_discovery();
|
|
1812
1820
|
var { preferredLanAddress: preferredLanAddress2 } = require_net_interfaces();
|
|
1813
|
-
var { MIN_HERDR_VERSION: MIN_HERDR_VERSION2, findHerdrCommand, herdrVersion, resolveHerdrCommand } = require_herdr_command();
|
|
1821
|
+
var { MIN_HERDR_VERSION: MIN_HERDR_VERSION2, findHerdrCommand, herdrVersion: herdrVersion2, resolveHerdrCommand } = require_herdr_command();
|
|
1814
1822
|
var RUNTIME_VERSION = 2;
|
|
1815
1823
|
function pidAlive(pid) {
|
|
1816
1824
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -2117,7 +2125,7 @@ var require_service = __commonJS({
|
|
|
2117
2125
|
}
|
|
2118
2126
|
const socketPath = resolveSocketPath(config.herdr.socketPath);
|
|
2119
2127
|
const herdr = findHerdrCommand();
|
|
2120
|
-
const installed = herdr.found ?
|
|
2128
|
+
const installed = herdr.found ? herdrVersion2({ command: herdr.command }) : { version: null, supported: true };
|
|
2121
2129
|
return {
|
|
2122
2130
|
ok: true,
|
|
2123
2131
|
mode: config.relay.mode,
|
|
@@ -2580,8 +2588,8 @@ var require_lifecycle = __commonJS({
|
|
|
2580
2588
|
function startAll2(config = loadConfig2()) {
|
|
2581
2589
|
const managed = managerInUse(config);
|
|
2582
2590
|
if (managed) {
|
|
2583
|
-
keepalive2.restart(config);
|
|
2584
|
-
return { ok: true, managed: true, manager: managed.manager };
|
|
2591
|
+
if (!managed.active) keepalive2.restart(config);
|
|
2592
|
+
return { ok: true, managed: true, manager: managed.manager, alreadyRunning: Boolean(managed.active) };
|
|
2585
2593
|
}
|
|
2586
2594
|
return { ...startServices(), managed: false };
|
|
2587
2595
|
}
|
|
@@ -2613,111 +2621,6 @@ var require_lifecycle = __commonJS({
|
|
|
2613
2621
|
}
|
|
2614
2622
|
});
|
|
2615
2623
|
|
|
2616
|
-
// src/herdr-plugin.js
|
|
2617
|
-
var require_herdr_plugin = __commonJS({
|
|
2618
|
-
"src/herdr-plugin.js"(exports, module) {
|
|
2619
|
-
"use strict";
|
|
2620
|
-
var fs = __require("node:fs");
|
|
2621
|
-
var path = __require("node:path");
|
|
2622
|
-
var { spawnSync } = __require("node:child_process");
|
|
2623
|
-
var { PACKAGE_ROOT } = require_config();
|
|
2624
|
-
var { herdrVersion, resolveHerdrCommand } = require_herdr_command();
|
|
2625
|
-
var PLUGIN_ID = "herdr.remote.web";
|
|
2626
|
-
var MANIFEST_NAME = "herdr-plugin.toml";
|
|
2627
|
-
function manifestPath() {
|
|
2628
|
-
return path.join(PACKAGE_ROOT, MANIFEST_NAME);
|
|
2629
|
-
}
|
|
2630
|
-
function herdrAvailable() {
|
|
2631
|
-
return herdrVersion().ok;
|
|
2632
|
-
}
|
|
2633
|
-
function runHerdr(args, { timeout = 15e3 } = {}) {
|
|
2634
|
-
const result = spawnSync(resolveHerdrCommand(), args, { encoding: "utf8", timeout });
|
|
2635
|
-
if (result.error && result.error.code === "ENOENT") {
|
|
2636
|
-
const error = new Error("herdr command not found");
|
|
2637
|
-
error.code = "HERDR_NOT_FOUND";
|
|
2638
|
-
throw error;
|
|
2639
|
-
}
|
|
2640
|
-
return result;
|
|
2641
|
-
}
|
|
2642
|
-
function parsePluginList(output) {
|
|
2643
|
-
const plugins = [];
|
|
2644
|
-
for (const line of String(output || "").split("\n")) {
|
|
2645
|
-
const match = /^-\s+(\S+)\s+\((.*?)\)\s+(\S+)(?:\s+\[(.*)\])?/.exec(line.trim());
|
|
2646
|
-
if (!match) continue;
|
|
2647
|
-
const [, id, name, state, rawSource] = match;
|
|
2648
|
-
const source = rawSource ? rawSource.split(";")[0].trim() : null;
|
|
2649
|
-
plugins.push({
|
|
2650
|
-
id,
|
|
2651
|
-
name,
|
|
2652
|
-
enabled: state === "enabled",
|
|
2653
|
-
source,
|
|
2654
|
-
warnings: rawSource && rawSource.includes(";") ? rawSource.slice(rawSource.indexOf(";") + 1).trim() : null,
|
|
2655
|
-
localPath: source && source.startsWith("local:") ? source.slice("local:".length) : null
|
|
2656
|
-
});
|
|
2657
|
-
}
|
|
2658
|
-
return plugins;
|
|
2659
|
-
}
|
|
2660
|
-
function registrationStatus() {
|
|
2661
|
-
if (!fs.existsSync(manifestPath())) {
|
|
2662
|
-
return { available: false, registered: false, reason: "manifest missing" };
|
|
2663
|
-
}
|
|
2664
|
-
const { version, supported } = herdrVersion();
|
|
2665
|
-
const installed = { version, versionSupported: supported };
|
|
2666
|
-
let result;
|
|
2667
|
-
try {
|
|
2668
|
-
result = runHerdr(["plugin", "list"]);
|
|
2669
|
-
} catch (error) {
|
|
2670
|
-
if (error.code === "HERDR_NOT_FOUND") return { available: false, registered: false, reason: "herdr not found" };
|
|
2671
|
-
throw error;
|
|
2672
|
-
}
|
|
2673
|
-
if (result.status !== 0) {
|
|
2674
|
-
return { available: true, registered: false, reason: String(result.stderr || "").trim(), ...installed };
|
|
2675
|
-
}
|
|
2676
|
-
const plugins = parsePluginList(result.stdout);
|
|
2677
|
-
const entry = plugins.find((plugin) => plugin.id === PLUGIN_ID);
|
|
2678
|
-
if (!entry) return { available: true, registered: false, packageRoot: PACKAGE_ROOT, ...installed };
|
|
2679
|
-
return {
|
|
2680
|
-
available: true,
|
|
2681
|
-
registered: true,
|
|
2682
|
-
enabled: entry.enabled,
|
|
2683
|
-
linkedPath: entry.localPath,
|
|
2684
|
-
// A stale link pointing at an old checkout is the main failure mode after
|
|
2685
|
-
// switching from a source install to npm.
|
|
2686
|
-
stale: Boolean(entry.localPath) && path.resolve(entry.localPath) !== path.resolve(PACKAGE_ROOT),
|
|
2687
|
-
packageRoot: PACKAGE_ROOT,
|
|
2688
|
-
...installed
|
|
2689
|
-
};
|
|
2690
|
-
}
|
|
2691
|
-
function register() {
|
|
2692
|
-
const current = registrationStatus();
|
|
2693
|
-
if (current.registered && current.stale) {
|
|
2694
|
-
runHerdr(["plugin", "unlink", PLUGIN_ID]);
|
|
2695
|
-
}
|
|
2696
|
-
const result = runHerdr(["plugin", "link", PACKAGE_ROOT]);
|
|
2697
|
-
if (result.status !== 0) {
|
|
2698
|
-
throw new Error(String(result.stderr || result.stdout || "").trim() || "herdr plugin link failed");
|
|
2699
|
-
}
|
|
2700
|
-
return { ok: true, path: PACKAGE_ROOT, output: String(result.stdout || "").trim() };
|
|
2701
|
-
}
|
|
2702
|
-
function unregister() {
|
|
2703
|
-
const result = runHerdr(["plugin", "unlink", PLUGIN_ID]);
|
|
2704
|
-
if (result.status !== 0) {
|
|
2705
|
-
throw new Error(String(result.stderr || result.stdout || "").trim() || "herdr plugin unlink failed");
|
|
2706
|
-
}
|
|
2707
|
-
return { ok: true, output: String(result.stdout || "").trim() };
|
|
2708
|
-
}
|
|
2709
|
-
module.exports = {
|
|
2710
|
-
PLUGIN_ID,
|
|
2711
|
-
manifestPath,
|
|
2712
|
-
herdrAvailable,
|
|
2713
|
-
parsePluginList,
|
|
2714
|
-
registrationStatus,
|
|
2715
|
-
register,
|
|
2716
|
-
unregister
|
|
2717
|
-
};
|
|
2718
|
-
}
|
|
2719
|
-
});
|
|
2720
|
-
|
|
2721
2624
|
// tui/src/index.tsx
|
|
2722
2625
|
import { render } from "ink";
|
|
2723
2626
|
|
|
@@ -3026,7 +2929,6 @@ var import_settings_model = __toESM(require_settings_model());
|
|
|
3026
2929
|
var import_service = __toESM(require_service());
|
|
3027
2930
|
var import_lifecycle = __toESM(require_lifecycle());
|
|
3028
2931
|
var keepalive = __toESM(require_keepalive());
|
|
3029
|
-
var herdrPlugin = __toESM(require_herdr_plugin());
|
|
3030
2932
|
var import_updater = __toESM(require_updater());
|
|
3031
2933
|
function detectLocale(options = {}) {
|
|
3032
2934
|
return (0, import_i18n.detectLocale)(options);
|
|
@@ -3808,24 +3710,15 @@ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
|
3808
3710
|
function HerdrScreen({ ctx }) {
|
|
3809
3711
|
const { t, draft, editingId } = ctx;
|
|
3810
3712
|
const [selected, setSelected] = useState7("socketPath");
|
|
3811
|
-
const [
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
} catch (error) {
|
|
3816
|
-
ctx.notify(error.message, "error");
|
|
3817
|
-
}
|
|
3818
|
-
};
|
|
3819
|
-
useEffect5(reloadRegistration, []);
|
|
3713
|
+
const [installed, setInstalled] = useState7(null);
|
|
3714
|
+
useEffect5(() => {
|
|
3715
|
+
setInstalled((0, import_herdr_command.herdrVersion)());
|
|
3716
|
+
}, []);
|
|
3820
3717
|
const entries = [
|
|
3821
3718
|
{ id: "socketPath", kind: "field", label: t("herdr.socketPath") },
|
|
3822
3719
|
{ id: "herdrArgs", kind: "field", label: t("herdr.args") },
|
|
3823
|
-
{ id: "
|
|
3824
|
-
{
|
|
3825
|
-
id: registration?.registered && !registration?.stale ? "unregister" : "register",
|
|
3826
|
-
kind: "action",
|
|
3827
|
-
label: registration?.registered && !registration?.stale ? t("herdr.unregister") : t("herdr.register")
|
|
3828
|
-
}
|
|
3720
|
+
{ id: "herdrAutoStart", kind: "toggle", label: t("field.herdrAutoStart") },
|
|
3721
|
+
{ id: "save", kind: "action", label: t("common.save") }
|
|
3829
3722
|
];
|
|
3830
3723
|
const applyField = (id, value) => {
|
|
3831
3724
|
const result = (0, import_settings_model.setField)(draft, id, value);
|
|
@@ -3850,26 +3743,11 @@ function HerdrScreen({ ctx }) {
|
|
|
3850
3743
|
ctx.setEditing(id);
|
|
3851
3744
|
return;
|
|
3852
3745
|
}
|
|
3853
|
-
if (id === "
|
|
3854
|
-
|
|
3746
|
+
if (id === "herdrAutoStart") {
|
|
3747
|
+
applyField(id, draft.herdr.autoStart ? "off" : "on");
|
|
3855
3748
|
return;
|
|
3856
3749
|
}
|
|
3857
|
-
|
|
3858
|
-
try {
|
|
3859
|
-
if (id === "register") {
|
|
3860
|
-
const result = herdrPlugin.register();
|
|
3861
|
-
ctx.notify(t("herdr.registerDone", { path: result.path }), "success");
|
|
3862
|
-
} else {
|
|
3863
|
-
herdrPlugin.unregister();
|
|
3864
|
-
ctx.notify(t("herdr.unregisterDone"), "success");
|
|
3865
|
-
}
|
|
3866
|
-
} catch (error) {
|
|
3867
|
-
const failure = error;
|
|
3868
|
-
throw new Error(failure.code === "HERDR_NOT_FOUND" ? t("herdr.cliMissing") : t("herdr.registerFailed", { message: failure.message }));
|
|
3869
|
-
} finally {
|
|
3870
|
-
reloadRegistration();
|
|
3871
|
-
}
|
|
3872
|
-
});
|
|
3750
|
+
if (id === "save") save();
|
|
3873
3751
|
};
|
|
3874
3752
|
useInput6((input, key) => {
|
|
3875
3753
|
const index = entries.findIndex((entry) => entry.id === selected);
|
|
@@ -3880,7 +3758,20 @@ function HerdrScreen({ ctx }) {
|
|
|
3880
3758
|
else if (input === "s") save();
|
|
3881
3759
|
}, { isActive: editingId === null });
|
|
3882
3760
|
return /* @__PURE__ */ jsxs8(Panel, { title: t("herdr.title"), children: [
|
|
3883
|
-
entries.map((entry) => entry.kind === "
|
|
3761
|
+
entries.map((entry) => entry.kind === "toggle" ? /* @__PURE__ */ jsx9(
|
|
3762
|
+
FieldRow,
|
|
3763
|
+
{
|
|
3764
|
+
label: entry.label,
|
|
3765
|
+
selected: selected === entry.id,
|
|
3766
|
+
onSelect: () => {
|
|
3767
|
+
setSelected(entry.id);
|
|
3768
|
+
activate(entry.id);
|
|
3769
|
+
},
|
|
3770
|
+
onHover: () => setSelected(entry.id),
|
|
3771
|
+
children: /* @__PURE__ */ jsx9(Text8, { color: draft.herdr.autoStart ? theme.ok : theme.muted, children: draft.herdr.autoStart ? `[\xD7] ${t("common.on")}` : `[ ] ${t("common.off")}` })
|
|
3772
|
+
},
|
|
3773
|
+
entry.id
|
|
3774
|
+
) : entry.kind === "field" ? /* @__PURE__ */ jsx9(
|
|
3884
3775
|
FieldRow,
|
|
3885
3776
|
{
|
|
3886
3777
|
label: entry.label,
|
|
@@ -3913,27 +3804,15 @@ function HerdrScreen({ ctx }) {
|
|
|
3913
3804
|
children: entry.label
|
|
3914
3805
|
}
|
|
3915
3806
|
) }, entry.id)),
|
|
3807
|
+
selected === "herdrAutoStart" ? /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text8, { color: theme.muted, children: t("herdr.autoStartHint") }) }) : null,
|
|
3916
3808
|
ctx.dirty ? /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("hint.unsavedChanges") }) }) : null,
|
|
3917
3809
|
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
|
|
3918
|
-
/* @__PURE__ */ jsx9(Row, { label: t("herdr.
|
|
3919
|
-
/* @__PURE__ */ jsx9(StatusDot, { level:
|
|
3920
|
-
/* @__PURE__ */
|
|
3921
|
-
" ",
|
|
3922
|
-
registration?.registered ? t("herdr.pluginRegistered") : t("herdr.pluginMissing")
|
|
3923
|
-
] })
|
|
3924
|
-
] }) }),
|
|
3925
|
-
registration?.packageRoot ? /* @__PURE__ */ jsx9(Row, { label: "", children: /* @__PURE__ */ jsx9(Text8, { color: theme.muted, children: registration.packageRoot }) }) : null,
|
|
3926
|
-
registration?.version ? /* @__PURE__ */ jsx9(Row, { label: t("herdr.version"), children: /* @__PURE__ */ jsxs8(Box8, { children: [
|
|
3927
|
-
/* @__PURE__ */ jsx9(StatusDot, { level: registration.versionSupported === false ? "warn" : "ok" }),
|
|
3928
|
-
/* @__PURE__ */ jsx9(Text8, { children: ` ${registration.version}` })
|
|
3810
|
+
installed?.version ? /* @__PURE__ */ jsx9(Row, { label: t("herdr.version"), children: /* @__PURE__ */ jsxs8(Box8, { children: [
|
|
3811
|
+
/* @__PURE__ */ jsx9(StatusDot, { level: installed.supported ? "ok" : "warn" }),
|
|
3812
|
+
/* @__PURE__ */ jsx9(Text8, { children: ` ${installed.version}` })
|
|
3929
3813
|
] }) }) : null,
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
registration?.stale && registration.linkedPath ? (
|
|
3933
|
-
// A leftover link to an old source checkout is the usual state after
|
|
3934
|
-
// switching to the npm install; re-registering repoints it.
|
|
3935
|
-
/* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: registration.linkedPath })
|
|
3936
|
-
) : null
|
|
3814
|
+
installed?.version && !installed.supported ? /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("herdr.versionOutdated", { version: installed.version, minimum: import_herdr_command.MIN_HERDR_VERSION }) }) : null,
|
|
3815
|
+
installed && !installed.ok ? /* @__PURE__ */ jsx9(Text8, { color: theme.warn, children: t("herdr.cliMissing") }) : null
|
|
3937
3816
|
] }),
|
|
3938
3817
|
/* @__PURE__ */ jsx9(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? "info" })
|
|
3939
3818
|
] });
|
|
@@ -4064,7 +3943,7 @@ function About({ ctx }) {
|
|
|
4064
3943
|
children: updateLabel
|
|
4065
3944
|
}
|
|
4066
3945
|
) }),
|
|
4067
|
-
/* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.
|
|
3946
|
+
/* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.16" }) }),
|
|
4068
3947
|
/* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
|
|
4069
3948
|
/* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
|
|
4070
3949
|
/* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),
|
package/herdr-plugin.toml
CHANGED
package/package.json
CHANGED
package/src/agent-status.js
CHANGED
|
@@ -56,13 +56,21 @@ function emptyCounts() {
|
|
|
56
56
|
*/
|
|
57
57
|
function summarizeAgents(snapshot) {
|
|
58
58
|
const entries = Array.isArray(snapshot?.agents) ? snapshot.agents : [];
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
? snapshot.
|
|
62
|
-
: null;
|
|
63
|
-
const focusedAgentEntry = focusedPaneId
|
|
64
|
-
? entries.find((entry) => entry?.pane_id === focusedPaneId)
|
|
59
|
+
const panes = Array.isArray(snapshot?.panes) ? snapshot.panes : [];
|
|
60
|
+
const reportedFocusId = typeof snapshot?.focused_pane_id === 'string' && snapshot.focused_pane_id
|
|
61
|
+
? snapshot.focused_pane_id
|
|
65
62
|
: null;
|
|
63
|
+
// `focused_pane_id` is the primary signal in Herdr snapshots. The focused
|
|
64
|
+
// flags are a fallback for snapshots where that top-level field is absent.
|
|
65
|
+
const focusedPane = (reportedFocusId
|
|
66
|
+
? panes.find((pane) => pane?.pane_id === reportedFocusId)
|
|
67
|
+
: panes.find((pane) => pane?.focused === true)) || null;
|
|
68
|
+
const focusedAgentEntry = (reportedFocusId
|
|
69
|
+
? entries.find((entry) => entry?.pane_id === reportedFocusId)
|
|
70
|
+
: entries.find((entry) => entry?.focused === true)) || null;
|
|
71
|
+
const focusedPaneId = reportedFocusId
|
|
72
|
+
|| (typeof focusedPane?.pane_id === 'string' ? focusedPane.pane_id : null)
|
|
73
|
+
|| (typeof focusedAgentEntry?.pane_id === 'string' ? focusedAgentEntry.pane_id : null);
|
|
66
74
|
const focusedAgent = typeof focusedPane?.agent === 'string'
|
|
67
75
|
? focusedPane.agent
|
|
68
76
|
: typeof focusedAgentEntry?.agent === 'string'
|
package/src/config.js
CHANGED
|
@@ -77,6 +77,9 @@ const DEFAULTS = {
|
|
|
77
77
|
socketPath: null,
|
|
78
78
|
args: [],
|
|
79
79
|
cwd: os.homedir(),
|
|
80
|
+
// Start Herdr's server when herdr-remote starts (at boot, under keep-alive).
|
|
81
|
+
// Off: Herdr starts when the user runs it, or confirms it from the WebUI.
|
|
82
|
+
autoStart: false,
|
|
80
83
|
},
|
|
81
84
|
auth: {
|
|
82
85
|
pairingTtlMs: 10 * 60 * 1000,
|
|
@@ -288,6 +291,7 @@ function validate(config) {
|
|
|
288
291
|
if (typeof config.herdr.cwd !== 'string' || config.herdr.cwd.length === 0) {
|
|
289
292
|
config.herdr.cwd = os.homedir();
|
|
290
293
|
}
|
|
294
|
+
config.herdr.autoStart = config.herdr.autoStart === true;
|
|
291
295
|
|
|
292
296
|
// A "remote" config without a relay URL cannot reach anything; fall back to
|
|
293
297
|
// local rather than silently starting a host connector that dials nowhere.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Whether Herdr's server is running on this workstation, and starting it when
|
|
4
|
+
// the user asked for that.
|
|
5
|
+
//
|
|
6
|
+
// herdr-remote never starts Herdr on its own initiative. It starts one when a
|
|
7
|
+
// browser paired to this workstation asks for it, or at service start when the
|
|
8
|
+
// user switched that on — and both go through here.
|
|
9
|
+
//
|
|
10
|
+
// Two traps shape this file:
|
|
11
|
+
//
|
|
12
|
+
// - A socket file is not a server. Herdr leaves `herdr.sock` behind when it dies
|
|
13
|
+
// without cleaning up (a crash, a power cut), and a `herdr` client pointed at
|
|
14
|
+
// a dead socket quietly starts a server of its own. So "running" means a
|
|
15
|
+
// connection was accepted, not that the path exists.
|
|
16
|
+
// - A server started from inside herdr-remote's systemd service lives in that
|
|
17
|
+
// service's cgroup, and `systemctl restart herdr-remote` — an update, a keep-
|
|
18
|
+
// alive restart, Herdr's own plugin startup hook — would kill it along with
|
|
19
|
+
// every agent in it. Under a service it is therefore started in a transient
|
|
20
|
+
// scope of its own, the same place it would live had the user started it
|
|
21
|
+
// from a terminal.
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const net = require('node:net');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const { spawn } = require('node:child_process');
|
|
27
|
+
const { inspectSocket } = require('./socket-discovery');
|
|
28
|
+
const { ensureDir } = require('./state');
|
|
29
|
+
|
|
30
|
+
/** A Unix socket either accepts at once or is dead; this only guards a hang. */
|
|
31
|
+
const PROBE_TIMEOUT_MS = 1_000;
|
|
32
|
+
/** Herdr restores the saved session before its API socket opens. */
|
|
33
|
+
const START_TIMEOUT_MS = 20_000;
|
|
34
|
+
const START_POLL_MS = 200;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Is Herdr's server answering on `socketPath`?
|
|
38
|
+
*
|
|
39
|
+
* Resolves `{ state }`: `running`, `stopped` (no socket, or a socket nothing is
|
|
40
|
+
* listening on), or `unavailable` with a `reason` — a path that is not a socket
|
|
41
|
+
* or belongs to another user. That last one is never "stopped": the socket may
|
|
42
|
+
* well be live, just not ours to use, and starting a second server beside it
|
|
43
|
+
* would be starting somebody else's Herdr.
|
|
44
|
+
*/
|
|
45
|
+
function probeHerdrServer(socketPath, { connect = net.connect, timeout = PROBE_TIMEOUT_MS } = {}) {
|
|
46
|
+
const info = inspectSocket(socketPath);
|
|
47
|
+
if (!info.ok) {
|
|
48
|
+
return Promise.resolve(info.missing ? { state: 'stopped' } : { state: 'unavailable', reason: info.reason });
|
|
49
|
+
}
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
let socket;
|
|
52
|
+
const finish = (result) => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
if (socket) {
|
|
55
|
+
socket.removeAllListeners();
|
|
56
|
+
socket.on('error', () => {});
|
|
57
|
+
socket.destroy();
|
|
58
|
+
}
|
|
59
|
+
resolve(result);
|
|
60
|
+
};
|
|
61
|
+
// A socket that neither accepts nor refuses has a listener behind it that
|
|
62
|
+
// is slow, not absent; starting another server would only fight it.
|
|
63
|
+
const timer = setTimeout(() => finish({ state: 'running' }), timeout);
|
|
64
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
65
|
+
socket = connect(socketPath);
|
|
66
|
+
socket.once('connect', () => finish({ state: 'running' }));
|
|
67
|
+
socket.once('error', (error) => finish(
|
|
68
|
+
error.code === 'ECONNREFUSED' || error.code === 'ENOENT'
|
|
69
|
+
? { state: 'stopped', stale: true }
|
|
70
|
+
: { state: 'unavailable', reason: error.message },
|
|
71
|
+
));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* True when this process is a systemd service (herdr-remote's keep-alive), so
|
|
77
|
+
* anything it starts would be killed with it.
|
|
78
|
+
*/
|
|
79
|
+
function runningInSystemdService({ platform = process.platform, readCgroup = () => fs.readFileSync('/proc/self/cgroup', 'utf8') } = {}) {
|
|
80
|
+
if (platform !== 'linux') return false;
|
|
81
|
+
try {
|
|
82
|
+
const unified = readCgroup().split('\n').find((line) => line.startsWith('0::'));
|
|
83
|
+
return Boolean(unified) && /\.service$/.test(unified.trim());
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The server's environment: the one a Herdr pane would give its client. */
|
|
90
|
+
function serverEnv(socketPath, env = process.env) {
|
|
91
|
+
const next = { ...env };
|
|
92
|
+
for (const key of Object.keys(next)) {
|
|
93
|
+
if (key.startsWith('HERDR_')) delete next[key];
|
|
94
|
+
}
|
|
95
|
+
if (socketPath) next.HERDR_SOCKET_PATH = socketPath;
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The argv that starts Herdr's server: Herdr's own global arguments (a
|
|
101
|
+
* `--session`, say) followed by `server`, wrapped in `systemd-run --scope` when
|
|
102
|
+
* the caller is a systemd service.
|
|
103
|
+
*/
|
|
104
|
+
function serverCommand({ command, args = [], inService = runningInSystemdService() }) {
|
|
105
|
+
const herdr = [command, ...args, 'server'];
|
|
106
|
+
if (!inService) return { file: herdr[0], args: herdr.slice(1) };
|
|
107
|
+
return {
|
|
108
|
+
file: 'systemd-run',
|
|
109
|
+
args: ['--user', '--scope', '--quiet', '--collect', '--description=Herdr server started by herdr-remote', '--', ...herdr],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Start Herdr's server in the background. Detached from this process, with its
|
|
115
|
+
* output appended to `logPath` rather than piped here: a pipe would break the
|
|
116
|
+
* moment herdr-remote exits and take the server with it.
|
|
117
|
+
*/
|
|
118
|
+
function launchHerdrServer({ command, args = [], socketPath, cwd, logPath, spawnProcess = spawn, inService }) {
|
|
119
|
+
const { file, args: argv } = serverCommand({ command, args, inService });
|
|
120
|
+
ensureDir(path.dirname(logPath));
|
|
121
|
+
const out = fs.openSync(logPath, 'a');
|
|
122
|
+
let child;
|
|
123
|
+
try {
|
|
124
|
+
child = spawnProcess(file, argv, {
|
|
125
|
+
cwd,
|
|
126
|
+
env: serverEnv(socketPath),
|
|
127
|
+
detached: true,
|
|
128
|
+
stdio: ['ignore', out, out],
|
|
129
|
+
});
|
|
130
|
+
} finally {
|
|
131
|
+
fs.closeSync(out);
|
|
132
|
+
}
|
|
133
|
+
child.on('error', () => {});
|
|
134
|
+
child.unref();
|
|
135
|
+
return child;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readLogTail(logPath, bytes = 2_000) {
|
|
139
|
+
try {
|
|
140
|
+
const text = fs.readFileSync(logPath, 'utf8');
|
|
141
|
+
return text.slice(-bytes).trim();
|
|
142
|
+
} catch {
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Make sure Herdr's server is running, starting it if it is not.
|
|
149
|
+
*
|
|
150
|
+
* Resolves `{ started }`, or rejects with a message that says what happened:
|
|
151
|
+
* the socket belongs to someone else, the server exited, or it never opened
|
|
152
|
+
* its socket. The last lines Herdr printed ride along, because "did not start"
|
|
153
|
+
* with no reason is a message nobody can act on.
|
|
154
|
+
*/
|
|
155
|
+
async function ensureHerdrServer({
|
|
156
|
+
command,
|
|
157
|
+
args = [],
|
|
158
|
+
socketPath,
|
|
159
|
+
cwd,
|
|
160
|
+
logPath,
|
|
161
|
+
probe = probeHerdrServer,
|
|
162
|
+
launch = launchHerdrServer,
|
|
163
|
+
timeout = START_TIMEOUT_MS,
|
|
164
|
+
interval = START_POLL_MS,
|
|
165
|
+
}) {
|
|
166
|
+
const before = await probe(socketPath);
|
|
167
|
+
if (before.state === 'running') return { started: false };
|
|
168
|
+
if (before.state === 'unavailable') {
|
|
169
|
+
throw new Error(`Herdr socket at ${socketPath} cannot be used: ${before.reason}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const child = launch({ command, args, socketPath, cwd, logPath });
|
|
173
|
+
let exit = null;
|
|
174
|
+
child.once('exit', (code, signal) => { exit = { code, signal }; });
|
|
175
|
+
|
|
176
|
+
const deadline = Date.now() + timeout;
|
|
177
|
+
for (;;) {
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
179
|
+
const now = await probe(socketPath);
|
|
180
|
+
if (now.state === 'running') return { started: true };
|
|
181
|
+
const tail = readLogTail(logPath);
|
|
182
|
+
const detail = tail ? `\n${tail}` : '';
|
|
183
|
+
if (exit) {
|
|
184
|
+
throw new Error(`Herdr server exited (${exit.signal || `code ${exit.code}`}) before opening ${socketPath}.${detail}`);
|
|
185
|
+
}
|
|
186
|
+
if (Date.now() >= deadline) {
|
|
187
|
+
throw new Error(`Herdr server did not open ${socketPath} within ${Math.round(timeout / 1000)}s.${detail}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
PROBE_TIMEOUT_MS,
|
|
194
|
+
START_TIMEOUT_MS,
|
|
195
|
+
probeHerdrServer,
|
|
196
|
+
runningInSystemdService,
|
|
197
|
+
serverEnv,
|
|
198
|
+
serverCommand,
|
|
199
|
+
launchHerdrServer,
|
|
200
|
+
ensureHerdrServer,
|
|
201
|
+
};
|
package/src/host-connector.js
CHANGED
|
@@ -30,6 +30,7 @@ const { requestHerdr, subscribeHerdr } = require('./herdr-api');
|
|
|
30
30
|
const { sameSummary, summarizeAgents } = require('./agent-status');
|
|
31
31
|
const { EXIT_REPLACED, EXIT_AUTH_FAILED } = require('./exit-codes');
|
|
32
32
|
const { savePastedFile, cleanPastedDir } = require('./pasted-files');
|
|
33
|
+
const { ensureHerdrServer, probeHerdrServer } = require('./herdr-server');
|
|
33
34
|
|
|
34
35
|
/** How often the agent summary is re-read while a browser is watching. */
|
|
35
36
|
const AGENT_STATUS_POLL_MS = 5_000;
|
|
@@ -118,6 +119,16 @@ class HostConnector {
|
|
|
118
119
|
this.sessions = new Map();
|
|
119
120
|
this.streamIndexToId = new Map();
|
|
120
121
|
this.PtySession = options.PtySession || PtySession;
|
|
122
|
+
/** Is Herdr's server answering, and how to start it; injectable for tests. */
|
|
123
|
+
this.probeHerdr = options.probeHerdr || probeHerdrServer;
|
|
124
|
+
this.ensureHerdr = options.ensureHerdr || ensureHerdrServer;
|
|
125
|
+
this.herdrLogPath = options.herdrLogPath || path.join(stateDir(), 'herdr-server.log');
|
|
126
|
+
/** Windows whose session waits on a Herdr that is not running, by stream. */
|
|
127
|
+
this.waitingForHerdr = new Map();
|
|
128
|
+
/** Session starts between their liveness check and their PTY, by stream. */
|
|
129
|
+
this.probingStarts = new Map();
|
|
130
|
+
/** One start at a time, however many windows asked for it. */
|
|
131
|
+
this.herdrLaunch = null;
|
|
121
132
|
/** The side channel that answers "does anything need me?"; see startAgentStatus. */
|
|
122
133
|
this.requestHerdr = options.requestHerdr || requestHerdr;
|
|
123
134
|
this.subscribeHerdr = options.subscribeHerdr || subscribeHerdr;
|
|
@@ -190,6 +201,12 @@ class HostConnector {
|
|
|
190
201
|
this.acquireLock();
|
|
191
202
|
this.stopping = false;
|
|
192
203
|
this.connect();
|
|
204
|
+
// Off unless the user switched it on: Herdr is theirs to start.
|
|
205
|
+
if (this.config.herdr?.autoStart) {
|
|
206
|
+
this.startHerdr().catch((error) => {
|
|
207
|
+
process.stderr.write(`herdr-remote host connector: could not start Herdr: ${error.message}\n`);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
193
210
|
}
|
|
194
211
|
|
|
195
212
|
stop() {
|
|
@@ -342,8 +359,9 @@ class HostConnector {
|
|
|
342
359
|
this.setClientCount(message.clientCount);
|
|
343
360
|
return;
|
|
344
361
|
}
|
|
345
|
-
if (message.type === 'session_start') this.startSession(message);
|
|
346
|
-
|
|
362
|
+
if (message.type === 'session_start') return this.startSession(message);
|
|
363
|
+
if (message.type === 'herdr_start') return this.handleHerdrStart(message);
|
|
364
|
+
if (message.type === 'session_stop') this.stopSession(message.clientId || message.streamId);
|
|
347
365
|
else if (message.type === 'resize') this.resizeSession(message);
|
|
348
366
|
else if (message.type === 'paste_file') this.handlePasteFile(message);
|
|
349
367
|
}
|
|
@@ -394,20 +412,103 @@ class HostConnector {
|
|
|
394
412
|
|
|
395
413
|
startSession(message) {
|
|
396
414
|
const streamId = typeof message.streamId === 'string' ? message.streamId : message.clientId;
|
|
397
|
-
if (!streamId) return;
|
|
415
|
+
if (!streamId) return undefined;
|
|
398
416
|
this.stopSession(streamId);
|
|
399
|
-
const streamIndex = typeof message.streamIndex === 'number' ? message.streamIndex : null;
|
|
400
417
|
const missingHerdr = this.ensureHerdrCommand();
|
|
401
418
|
if (missingHerdr) {
|
|
402
419
|
process.stderr.write(`herdr-remote host connector: ${missingHerdr}\n`);
|
|
403
420
|
sendJson(this.ws, { type: 'error', clientId: streamId, code: 'herdr_not_found', message: missingHerdr });
|
|
404
|
-
return;
|
|
421
|
+
return undefined;
|
|
405
422
|
}
|
|
406
423
|
const socketInfo = inspectSocket(this.socketPath);
|
|
407
|
-
if (!socketInfo.ok) {
|
|
424
|
+
if (!socketInfo.ok && !socketInfo.missing) {
|
|
408
425
|
sendJson(this.ws, { type: 'error', clientId: streamId, code: 'herdr_socket_unavailable', message: socketInfo.reason });
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
if (!socketInfo.ok) {
|
|
429
|
+
this.waitForHerdr(streamId, message);
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
// The socket file alone proves nothing: a `herdr` client pointed at a dead
|
|
433
|
+
// one starts a server of its own, inside this process's service, where the
|
|
434
|
+
// next restart of herdr-remote would kill it. Only a server that accepts a
|
|
435
|
+
// connection gets a client.
|
|
436
|
+
const pending = { message };
|
|
437
|
+
this.probingStarts.set(streamId, pending);
|
|
438
|
+
return this.probeHerdr(this.socketPath).then((probe) => {
|
|
439
|
+
// Stopped, or started again, while the answer was in flight.
|
|
440
|
+
if (this.probingStarts.get(streamId) !== pending) return;
|
|
441
|
+
this.probingStarts.delete(streamId);
|
|
442
|
+
if (probe.state === 'running') this.spawnSession(pending.message);
|
|
443
|
+
else if (probe.state === 'stopped') this.waitForHerdr(streamId, pending.message);
|
|
444
|
+
else sendJson(this.ws, { type: 'error', clientId: streamId, code: 'herdr_socket_unavailable', message: probe.reason });
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Hold a window's session until Herdr runs, and ask the browser whether it
|
|
450
|
+
* should be started. Nothing starts until somebody says yes.
|
|
451
|
+
*/
|
|
452
|
+
waitForHerdr(streamId, message) {
|
|
453
|
+
this.waitingForHerdr.set(streamId, message);
|
|
454
|
+
sendJson(this.ws, {
|
|
455
|
+
type: 'error',
|
|
456
|
+
clientId: streamId,
|
|
457
|
+
code: 'herdr_not_running',
|
|
458
|
+
message: `Herdr is not running on this workstation (${this.socketPath}).`,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Start this workstation's Herdr server, once, however many windows or
|
|
464
|
+
* callers ask at the same time.
|
|
465
|
+
*/
|
|
466
|
+
startHerdr() {
|
|
467
|
+
if (!this.herdrLaunch) {
|
|
468
|
+
this.herdrLaunch = Promise.resolve()
|
|
469
|
+
.then(() => {
|
|
470
|
+
const missing = this.ensureHerdrCommand();
|
|
471
|
+
if (missing) throw new Error(missing);
|
|
472
|
+
return this.ensureHerdr({
|
|
473
|
+
command: this.herdrCommand,
|
|
474
|
+
args: this.herdrArgs,
|
|
475
|
+
socketPath: this.socketPath,
|
|
476
|
+
cwd: this.cwd,
|
|
477
|
+
logPath: this.herdrLogPath,
|
|
478
|
+
});
|
|
479
|
+
})
|
|
480
|
+
.finally(() => { this.herdrLaunch = null; });
|
|
481
|
+
}
|
|
482
|
+
return this.herdrLaunch;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* A browser said yes. The relay only ever routes this from a window paired
|
|
487
|
+
* to *this* workstation, so what starts is this user's own Herdr, at the
|
|
488
|
+
* socket this connector is configured for — never one named by the message.
|
|
489
|
+
* Once it runs, every window waiting on it gets its session.
|
|
490
|
+
*/
|
|
491
|
+
async handleHerdrStart(message) {
|
|
492
|
+
const streamId = typeof message.streamId === 'string' ? message.streamId : message.clientId;
|
|
493
|
+
try {
|
|
494
|
+
const result = await this.startHerdr();
|
|
495
|
+
if (result.started) process.stderr.write('herdr-remote host connector: started Herdr at a browser\'s request\n');
|
|
496
|
+
} catch (error) {
|
|
497
|
+
process.stderr.write(`herdr-remote host connector: could not start Herdr: ${error.message}\n`);
|
|
498
|
+
if (streamId) {
|
|
499
|
+
sendJson(this.ws, { type: 'error', clientId: streamId, code: 'herdr_start_failed', message: error.message });
|
|
500
|
+
}
|
|
409
501
|
return;
|
|
410
502
|
}
|
|
503
|
+
const waiting = [...this.waitingForHerdr.values()];
|
|
504
|
+
this.waitingForHerdr.clear();
|
|
505
|
+
for (const held of waiting) this.spawnSession(held);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Start the PTY for a window once Herdr is known to be running. */
|
|
509
|
+
spawnSession(message) {
|
|
510
|
+
const streamId = typeof message.streamId === 'string' ? message.streamId : message.clientId;
|
|
511
|
+
const streamIndex = typeof message.streamIndex === 'number' ? message.streamIndex : null;
|
|
411
512
|
const pty = new this.PtySession({
|
|
412
513
|
command: this.herdrCommand,
|
|
413
514
|
args: this.herdrArgs,
|
|
@@ -516,6 +617,8 @@ class HostConnector {
|
|
|
516
617
|
}
|
|
517
618
|
|
|
518
619
|
stopSession(streamId) {
|
|
620
|
+
this.waitingForHerdr.delete(streamId);
|
|
621
|
+
this.probingStarts.delete(streamId);
|
|
519
622
|
const session = this.sessions.get(streamId);
|
|
520
623
|
if (!session) return;
|
|
521
624
|
this.sessions.delete(streamId);
|
|
@@ -676,6 +779,12 @@ class HostConnector {
|
|
|
676
779
|
|
|
677
780
|
resizeSession(message) {
|
|
678
781
|
const id = message.clientId || message.streamId;
|
|
782
|
+
// A window still waiting on Herdr starts at the size it has by then.
|
|
783
|
+
const held = this.waitingForHerdr.get(id) || this.probingStarts.get(id)?.message;
|
|
784
|
+
if (held) {
|
|
785
|
+
if (Number.isInteger(message.cols)) held.cols = message.cols;
|
|
786
|
+
if (Number.isInteger(message.rows)) held.rows = message.rows;
|
|
787
|
+
}
|
|
679
788
|
const session = this.sessions.get(id);
|
|
680
789
|
if (!session) return;
|
|
681
790
|
session.cols = Number.isInteger(message.cols) ? Math.min(500, Math.max(2, message.cols)) : session.cols;
|
|
@@ -716,6 +825,8 @@ class HostConnector {
|
|
|
716
825
|
}
|
|
717
826
|
this.sessions.clear();
|
|
718
827
|
this.streamIndexToId.clear();
|
|
828
|
+
this.waitingForHerdr.clear();
|
|
829
|
+
this.probingStarts.clear();
|
|
719
830
|
}
|
|
720
831
|
|
|
721
832
|
sendHeartbeat(force = false) {
|
package/src/i18n/en.js
CHANGED
|
@@ -146,14 +146,7 @@ module.exports = {
|
|
|
146
146
|
'herdr.title': 'Herdr integration',
|
|
147
147
|
'herdr.socketPath': 'Socket path',
|
|
148
148
|
'herdr.args': 'Extra arguments',
|
|
149
|
-
'herdr.
|
|
150
|
-
'herdr.pluginRegistered': 'Registered with Herdr.',
|
|
151
|
-
'herdr.pluginMissing': 'Not registered with Herdr.',
|
|
152
|
-
'herdr.register': 'Register as Herdr plugin',
|
|
153
|
-
'herdr.unregister': 'Unregister plugin',
|
|
154
|
-
'herdr.registerDone': 'Registered: {path}',
|
|
155
|
-
'herdr.unregisterDone': 'Plugin unregistered.',
|
|
156
|
-
'herdr.registerFailed': 'Registration failed: {message}',
|
|
149
|
+
'herdr.autoStartHint': 'Off: Herdr starts only when you run it, or when you confirm it from a browser. On: herdr-remote also starts it every time it starts, at boot included. Takes effect the next time herdr-remote starts.',
|
|
157
150
|
'herdr.cliMissing': 'Command herdr not found. Set HERDR_BIN_PATH to its full path.',
|
|
158
151
|
'herdr.version': 'Herdr version',
|
|
159
152
|
'herdr.versionOutdated': 'Herdr {version} is older than {minimum}. Run "herdr update": window titles, background machine activation and large pastes misbehave in browser windows before that release.',
|
|
@@ -193,6 +186,7 @@ module.exports = {
|
|
|
193
186
|
'field.publicUrl': 'Browser URL override',
|
|
194
187
|
'field.socketPath': 'Herdr socket path',
|
|
195
188
|
'field.herdrArgs': 'Herdr arguments',
|
|
189
|
+
'field.herdrAutoStart': 'Auto-start Herdr',
|
|
196
190
|
'field.language': 'Language',
|
|
197
191
|
'field.keepalive': 'Keep-alive manager',
|
|
198
192
|
|
package/src/i18n/zh.js
CHANGED
|
@@ -145,14 +145,7 @@ module.exports = {
|
|
|
145
145
|
'herdr.title': 'Herdr 集成',
|
|
146
146
|
'herdr.socketPath': '套接字路径',
|
|
147
147
|
'herdr.args': '附加参数',
|
|
148
|
-
'herdr.
|
|
149
|
-
'herdr.pluginRegistered': '已注册到 Herdr。',
|
|
150
|
-
'herdr.pluginMissing': '尚未注册到 Herdr。',
|
|
151
|
-
'herdr.register': '注册为 Herdr 插件',
|
|
152
|
-
'herdr.unregister': '取消注册',
|
|
153
|
-
'herdr.registerDone': '已注册:{path}',
|
|
154
|
-
'herdr.unregisterDone': '已取消注册。',
|
|
155
|
-
'herdr.registerFailed': '注册失败:{message}',
|
|
148
|
+
'herdr.autoStartHint': '关:Herdr 只在你手动运行,或在浏览器里确认后才启动。开:herdr-remote 每次启动(包括开机)时一并启动 Herdr。下次 herdr-remote 启动时生效。',
|
|
156
149
|
'herdr.cliMissing': '未找到 herdr 命令,可将 HERDR_BIN_PATH 设为其完整路径。',
|
|
157
150
|
'herdr.version': 'Herdr 版本',
|
|
158
151
|
'herdr.versionOutdated': 'Herdr {version} 低于 {minimum},请运行 “herdr update”:在该版本之前,浏览器窗口的标题、后台机器激活与大段粘贴都会出问题。',
|
|
@@ -192,6 +185,7 @@ module.exports = {
|
|
|
192
185
|
'field.publicUrl': '浏览器地址(覆盖)',
|
|
193
186
|
'field.socketPath': 'Herdr 套接字路径',
|
|
194
187
|
'field.herdrArgs': 'Herdr 参数',
|
|
188
|
+
'field.herdrAutoStart': '随服务启动 Herdr',
|
|
195
189
|
'field.language': '语言',
|
|
196
190
|
'field.keepalive': '保活管理方式',
|
|
197
191
|
|
package/src/lifecycle.js
CHANGED
|
@@ -19,8 +19,12 @@ function managerInUse(config) {
|
|
|
19
19
|
function startAll(config = loadConfig()) {
|
|
20
20
|
const managed = managerInUse(config);
|
|
21
21
|
if (managed) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Start means "make it run", not "restart it". Herdr runs this through the
|
|
23
|
+
// plugin's startup hook every time a Herdr server starts, and a restart
|
|
24
|
+
// there dropped every browser attached to the workstation — including the
|
|
25
|
+
// one that had just asked for that Herdr to be started.
|
|
26
|
+
if (!managed.active) keepalive.restart(config);
|
|
27
|
+
return { ok: true, managed: true, manager: managed.manager, alreadyRunning: Boolean(managed.active) };
|
|
24
28
|
}
|
|
25
29
|
return { ...startServices(), managed: false };
|
|
26
30
|
}
|
package/src/settings-model.js
CHANGED
|
@@ -35,6 +35,7 @@ const FIELDS = [
|
|
|
35
35
|
{ id: 'publicUrl', kind: 'text', labelKey: 'field.publicUrl' },
|
|
36
36
|
{ id: 'socketPath', kind: 'text', labelKey: 'field.socketPath' },
|
|
37
37
|
{ id: 'herdrArgs', kind: 'text', labelKey: 'field.herdrArgs' },
|
|
38
|
+
{ id: 'herdrAutoStart', kind: 'toggle', labelKey: 'field.herdrAutoStart' },
|
|
38
39
|
{ id: 'language', kind: 'choice', choices: LANGUAGES, labelKey: 'field.language' },
|
|
39
40
|
{ id: 'keepaliveManager', kind: 'choice', choices: KEEPALIVE_MANAGERS, labelKey: 'field.keepalive' },
|
|
40
41
|
];
|
|
@@ -94,6 +95,8 @@ function getField(draft, id) {
|
|
|
94
95
|
return draft.herdr.socketPath || EMPTY;
|
|
95
96
|
case 'herdrArgs':
|
|
96
97
|
return (draft.herdr.args || []).join(' ');
|
|
98
|
+
case 'herdrAutoStart':
|
|
99
|
+
return draft.herdr.autoStart ? 'on' : 'off';
|
|
97
100
|
case 'language':
|
|
98
101
|
return draft.ui.language;
|
|
99
102
|
case 'keepaliveManager':
|
|
@@ -211,6 +214,13 @@ function setField(draft, id, rawValue) {
|
|
|
211
214
|
next.herdr.args = args;
|
|
212
215
|
break;
|
|
213
216
|
}
|
|
217
|
+
case 'herdrAutoStart': {
|
|
218
|
+
if (value !== true && value !== false && value !== 'on' && value !== 'off') {
|
|
219
|
+
return { draft, errorKey: 'error.unknownField' };
|
|
220
|
+
}
|
|
221
|
+
next.herdr.autoStart = value === true || value === 'on';
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
214
224
|
case 'language': {
|
|
215
225
|
if (!LANGUAGES.includes(value)) return { draft, errorKey: 'error.invalidLanguage' };
|
|
216
226
|
next.ui.language = value;
|
|
@@ -273,6 +283,7 @@ function saveDraft(draft) {
|
|
|
273
283
|
...(current.herdr || {}),
|
|
274
284
|
socketPath: draft.herdr.socketPath,
|
|
275
285
|
args: draft.herdr.args,
|
|
286
|
+
autoStart: draft.herdr.autoStart === true,
|
|
276
287
|
};
|
|
277
288
|
if (!merged.herdr.socketPath) delete merged.herdr.socketPath;
|
|
278
289
|
merged.keepalive = { ...(current.keepalive || {}), manager: draft.keepalive.manager };
|
package/src/socket-discovery.js
CHANGED
|
@@ -32,7 +32,12 @@ function inspectSocket(socketPath, options = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
return { ok: true, path: socketPath, uid: stat.uid, mode: stat.mode };
|
|
34
34
|
} catch (error) {
|
|
35
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
missing: error.code === 'ENOENT',
|
|
38
|
+
reason: error.code === 'ENOENT' ? 'socket does not exist' : error.message,
|
|
39
|
+
path: socketPath,
|
|
40
|
+
};
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
|