smart-home-engine 1.42.0 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/assets/index-C5CNdJWG.js +289 -0
- package/dist/web/assets/index-DDpHIMRc.css +1 -0
- package/dist/web/assets/{tsMode-C0zJ2TUA.js → tsMode-IQCYIRzL.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/service/she-servicectl +131 -1
- package/src/lib/node-releases.js +65 -0
- package/src/lib/services-host.js +1 -1
- package/src/web/config-api.js +47 -0
- package/src/web/services-api.js +63 -0
- package/dist/web/assets/index-55TivCbD.css +0 -1
- package/dist/web/assets/index-fKlQBrQH.js +0 -287
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-DKJI_o9J.js";import{t as I}from"./index-
|
|
1
|
+
import{m as O}from"./monaco-langs-DKJI_o9J.js";import{t as I}from"./index-C5CNdJWG.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
package/dist/web/index.html
CHANGED
|
@@ -172,10 +172,10 @@
|
|
|
172
172
|
}
|
|
173
173
|
})();
|
|
174
174
|
</script>
|
|
175
|
-
<script type="module" crossorigin src="/assets/index-
|
|
175
|
+
<script type="module" crossorigin src="/assets/index-C5CNdJWG.js"></script>
|
|
176
176
|
<link rel="modulepreload" crossorigin href="/assets/monaco-langs-DKJI_o9J.js">
|
|
177
177
|
<link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
|
|
178
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
178
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DDpHIMRc.css">
|
|
179
179
|
</head>
|
|
180
180
|
<body>
|
|
181
181
|
<div id="app"></div>
|
package/package.json
CHANGED
package/service/she-servicectl
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
# she-servicectl files <adapter> <instance> JSON listing of /etc/<adapter>/ and /var/lib/<adapter>/<instance>/
|
|
32
32
|
# she-servicectl file <adapter> <instance> read|write <path> a file inside those two directories (write: content on stdin)
|
|
33
33
|
# she-servicectl asset <adapter> <relpath> a file shipped in the adapter package (example, schema)
|
|
34
|
+
# she-servicectl node update [--lts|--latest] install/refresh tj/n (downloaded, no npm), then n install lts (or latest) — JSON with the version before and after
|
|
35
|
+
# she-servicectl restart-all restart every running instance on this host (after a node update, say)
|
|
34
36
|
# she-servicectl self-update replace this script with the one on stdin (she ships it)
|
|
35
37
|
# she-servicectl remove-key drop the public key on stdin from the calling user's authorized_keys
|
|
36
38
|
# she-servicectl teardown [--force] remove she from this host: that key, the sudoers rule, this script,
|
|
@@ -43,10 +45,17 @@
|
|
|
43
45
|
|
|
44
46
|
set -eu
|
|
45
47
|
|
|
46
|
-
VERSION=
|
|
48
|
+
VERSION=13
|
|
47
49
|
BROKER_ENV=/etc/mqtt-interfaces/broker.env
|
|
48
50
|
UNIT_DIR=/etc/systemd/system
|
|
49
51
|
|
|
52
|
+
# tj/n, pinned: n is a self-contained bash script, so it is installed by downloading that one
|
|
53
|
+
# file — not with `npm install -g n`, which needs a node that may not be there yet and would
|
|
54
|
+
# drop n into whatever prefix the current node uses (where the node n installs then shadows it).
|
|
55
|
+
N_VERSION=10.2.0
|
|
56
|
+
N_BIN=/usr/local/bin/n
|
|
57
|
+
N_URL=https://raw.githubusercontent.com/tj/n/v$N_VERSION/bin/n
|
|
58
|
+
|
|
50
59
|
die() { printf 'she-servicectl: %s\n' "$*" >&2; exit 2; }
|
|
51
60
|
|
|
52
61
|
# ── validation ──────────────────────────────────────────────────────────────────
|
|
@@ -104,6 +113,11 @@ need_any() {
|
|
|
104
113
|
|
|
105
114
|
# ── helpers ─────────────────────────────────────────────────────────────────────
|
|
106
115
|
|
|
116
|
+
# a JSON string, or null when the value is empty
|
|
117
|
+
json_str() {
|
|
118
|
+
[ -n "$1" ] && printf '"%s"' "$(json_escape "$1")" || printf 'null'
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
json_escape() {
|
|
108
122
|
# minimal JSON string escaping for the few free-form values (timestamps, paths)
|
|
109
123
|
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | tr -d '\n\r'
|
|
@@ -651,6 +665,120 @@ cmd_teardown() {
|
|
|
651
665
|
printf 'removed %s\n' "$self"
|
|
652
666
|
}
|
|
653
667
|
|
|
668
|
+
# ── node ────────────────────────────────────────────────────────────────────────
|
|
669
|
+
# Node itself is managed with tj/n (https://github.com/tj/n). Running services keep the
|
|
670
|
+
# binary they were started with, so restart-all is what a version change needs afterwards.
|
|
671
|
+
|
|
672
|
+
# Download the pinned n and put it at $1. Checked the way self-update checks this script:
|
|
673
|
+
# interpreter line, VERSION, and a syntax check — before anything is moved into place.
|
|
674
|
+
fetch_n() {
|
|
675
|
+
tmp=$(mktemp) || die "mktemp failed"
|
|
676
|
+
if command -v curl >/dev/null 2>&1; then
|
|
677
|
+
curl -fsSL -o "$tmp" "$N_URL" || { rm -f "$tmp"; die "could not download n $N_VERSION from $N_URL"; }
|
|
678
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
679
|
+
wget -qO "$tmp" "$N_URL" || { rm -f "$tmp"; die "could not download n $N_VERSION from $N_URL"; }
|
|
680
|
+
else
|
|
681
|
+
rm -f "$tmp"
|
|
682
|
+
die "neither curl nor wget on this host — cannot fetch n (and n needs one of them itself)"
|
|
683
|
+
fi
|
|
684
|
+
head -n 1 "$tmp" | grep -q '^#!.*bash' || { rm -f "$tmp"; die "downloaded file is not the n script"; }
|
|
685
|
+
grep -q "^VERSION=\"\{0,1\}$N_VERSION" "$tmp" || { rm -f "$tmp"; die "downloaded n is not version $N_VERSION"; }
|
|
686
|
+
bash -n "$tmp" 2>/dev/null || { rm -f "$tmp"; die "downloaded n failed the syntax check"; }
|
|
687
|
+
install -m 0755 "$tmp" "$1" || { rm -f "$tmp"; die "could not install n to $1"; }
|
|
688
|
+
rm -f "$tmp"
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
cmd_node() {
|
|
692
|
+
[ $# -ge 1 ] || die "usage: node update [--lts|--stable]"
|
|
693
|
+
action=$1
|
|
694
|
+
shift
|
|
695
|
+
[ "$action" = update ] || die "unknown node action: $action"
|
|
696
|
+
# n's labels: lts = newest long-term-support release, latest = newest release of all.
|
|
697
|
+
# (n also accepts "stable", but only as its own old alias for lts — not a third channel.)
|
|
698
|
+
spec=lts
|
|
699
|
+
while [ $# -gt 0 ]; do
|
|
700
|
+
case "$1" in
|
|
701
|
+
--lts) spec=lts ;;
|
|
702
|
+
--latest) spec=latest ;;
|
|
703
|
+
--stable) spec=lts ;;
|
|
704
|
+
*) die "unknown option: $1" ;;
|
|
705
|
+
esac
|
|
706
|
+
shift
|
|
707
|
+
done
|
|
708
|
+
|
|
709
|
+
command -v bash >/dev/null 2>&1 || die "n is a bash script and this host has no bash (Alpine: apk add bash curl)"
|
|
710
|
+
N_PREFIX=${N_PREFIX:-/usr/local}
|
|
711
|
+
export N_PREFIX
|
|
712
|
+
|
|
713
|
+
before=$(node --version 2>/dev/null || true)
|
|
714
|
+
|
|
715
|
+
have=""
|
|
716
|
+
if [ -x "$N_BIN" ]; then have=$("$N_BIN" --version 2>/dev/null || true); fi
|
|
717
|
+
n_installed=false
|
|
718
|
+
if [ "$have" != "$N_VERSION" ]; then
|
|
719
|
+
fetch_n "$N_BIN"
|
|
720
|
+
n_installed=true
|
|
721
|
+
fi
|
|
722
|
+
|
|
723
|
+
"$N_BIN" install "$spec" >&2 || die "n install $spec failed"
|
|
724
|
+
|
|
725
|
+
hash -r 2>/dev/null || true
|
|
726
|
+
installed_path="$N_PREFIX/bin/node"
|
|
727
|
+
installed=$("$installed_path" --version 2>/dev/null || true)
|
|
728
|
+
active_path=$(command -v node 2>/dev/null || true)
|
|
729
|
+
after=$(node --version 2>/dev/null || true)
|
|
730
|
+
# n put a node in N_PREFIX, but another one (distro package, nvm, an /opt/nodeXX wrapper)
|
|
731
|
+
# still wins on PATH — that is the version the adapters keep running on.
|
|
732
|
+
mismatch=false
|
|
733
|
+
if [ -n "$installed" ] && [ "$after" != "$installed" ]; then mismatch=true; fi
|
|
734
|
+
|
|
735
|
+
printf '{"spec":"%s","before":%s,"after":%s,"installed":%s,"activePath":%s,"installedPath":"%s","mismatch":%s,"n":"%s","nInstalled":%s}\n' \
|
|
736
|
+
"$spec" \
|
|
737
|
+
"$(json_str "$before")" "$(json_str "$after")" "$(json_str "$installed")" "$(json_str "$active_path")" \
|
|
738
|
+
"$(json_escape "$installed_path")" "$mismatch" "$N_VERSION" "$n_installed"
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
# Restart every instance that is currently running — units that are stopped or disabled
|
|
742
|
+
# on purpose stay that way.
|
|
743
|
+
cmd_restart_all() {
|
|
744
|
+
[ $# -eq 0 ] || die "usage: restart-all"
|
|
745
|
+
first=1
|
|
746
|
+
printf '{"restarted":['
|
|
747
|
+
for unit in "$UNIT_DIR"/*@.service; do
|
|
748
|
+
[ -f "$unit" ] || continue
|
|
749
|
+
a=$(basename "$unit" @.service)
|
|
750
|
+
valid_adapter "$a" || continue
|
|
751
|
+
installed_adapter "$a" || continue
|
|
752
|
+
for envf in "/etc/$a"/*.env; do
|
|
753
|
+
[ -f "$envf" ] || continue
|
|
754
|
+
i=$(basename "$envf" .env)
|
|
755
|
+
valid_instance "$i" || continue
|
|
756
|
+
systemctl is-active --quiet "$a@$i.service" || continue
|
|
757
|
+
if err=$(systemctl restart "$a@$i.service" 2>&1); then ok=true; else ok=false; fi
|
|
758
|
+
[ $first -eq 1 ] || printf ','
|
|
759
|
+
first=0
|
|
760
|
+
printf '{"adapter":"%s","instance":"%s","ok":%s,"error":%s}' \
|
|
761
|
+
"$a" "$i" "$ok" \
|
|
762
|
+
"$([ "$ok" = true ] && printf 'null' || printf '"%s"' "$(json_escape "$err")")"
|
|
763
|
+
done
|
|
764
|
+
done
|
|
765
|
+
for unit in "$UNIT_DIR"/*.service; do
|
|
766
|
+
[ -f "$unit" ] || continue
|
|
767
|
+
a=$(basename "$unit" .service)
|
|
768
|
+
case "$a" in *@) continue ;; esac
|
|
769
|
+
valid_adapter "$a" || continue
|
|
770
|
+
legacy_unit "$a" || continue
|
|
771
|
+
systemctl is-active --quiet "$a.service" || continue
|
|
772
|
+
if err=$(systemctl restart "$a.service" 2>&1); then ok=true; else ok=false; fi
|
|
773
|
+
[ $first -eq 1 ] || printf ','
|
|
774
|
+
first=0
|
|
775
|
+
printf '{"adapter":"%s","instance":"-","ok":%s,"error":%s}' \
|
|
776
|
+
"$a" "$ok" \
|
|
777
|
+
"$([ "$ok" = true ] && printf 'null' || printf '"%s"' "$(json_escape "$err")")"
|
|
778
|
+
done
|
|
779
|
+
printf ']}\n'
|
|
780
|
+
}
|
|
781
|
+
|
|
654
782
|
cmd_npm() {
|
|
655
783
|
[ $# -ge 2 ] || die "usage: npm <adapter> version|origin|install|update|uninstall [--purge]"
|
|
656
784
|
need_adapter "$1"
|
|
@@ -697,6 +825,8 @@ case "$c" in
|
|
|
697
825
|
install) cmd_install "$@" ;;
|
|
698
826
|
uninstall) cmd_uninstall "$@" ;;
|
|
699
827
|
npm) cmd_npm "$@" ;;
|
|
828
|
+
node) cmd_node "$@" ;;
|
|
829
|
+
restart-all) cmd_restart_all "$@" ;;
|
|
700
830
|
files) cmd_files "$@" ;;
|
|
701
831
|
file) cmd_file "$@" ;;
|
|
702
832
|
asset) cmd_asset "$@" ;;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which Node.js versions a host could be updated to — the two labels tj/n understands:
|
|
5
|
+
* `lts` (newest Long Term Support release) and `latest` (newest official release).
|
|
6
|
+
*
|
|
7
|
+
* she asks nodejs.org once for all hosts and caches the answer, so the Hosts tab can show
|
|
8
|
+
* what a click would install and hide the button when the host already runs it. Best-effort:
|
|
9
|
+
* every failure yields the last good answer, or nulls — nothing throws.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const INDEX_URL = 'https://nodejs.org/dist/index.json';
|
|
13
|
+
const TTL = 6 * 60 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
let _fetch = (...args) => fetch(...args);
|
|
16
|
+
|
|
17
|
+
/** Test hook: replace the fetch implementation. */
|
|
18
|
+
function setFetch(fn) {
|
|
19
|
+
_fetch = fn;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @type {{lts: string|null, ltsName: string|null, latest: string|null, ts: number, ok: boolean}|null} */
|
|
23
|
+
let _cache = null;
|
|
24
|
+
/** @type {Promise<object>|null} */
|
|
25
|
+
let _inflight = null;
|
|
26
|
+
|
|
27
|
+
function clearCache() {
|
|
28
|
+
_cache = null;
|
|
29
|
+
_inflight = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {{force?: boolean, now?: number}} [opts]
|
|
34
|
+
* @returns {Promise<{lts: string|null, ltsName: string|null, latest: string|null, fetchedAt: number|null, stale: boolean}>}
|
|
35
|
+
*/
|
|
36
|
+
async function releases(opts = {}) {
|
|
37
|
+
const now = opts.now ?? Date.now();
|
|
38
|
+
if (_cache && !opts.force && now - _cache.ts < TTL) {
|
|
39
|
+
return { lts: _cache.lts, ltsName: _cache.ltsName, latest: _cache.latest, fetchedAt: _cache.ts, stale: !_cache.ok };
|
|
40
|
+
}
|
|
41
|
+
if (_inflight) return _inflight;
|
|
42
|
+
|
|
43
|
+
_inflight = (async () => {
|
|
44
|
+
try {
|
|
45
|
+
const res = await _fetch(INDEX_URL, { signal: AbortSignal.timeout(15000) });
|
|
46
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
47
|
+
const list = await res.json();
|
|
48
|
+
if (!Array.isArray(list) || list.length === 0) throw new Error('unexpected index.json');
|
|
49
|
+
// index.json is ordered newest first; an LTS entry carries its codename in `lts`
|
|
50
|
+
const latest = typeof list[0].version === 'string' ? list[0].version : null;
|
|
51
|
+
const ltsEntry = list.find((e) => e && e.lts && typeof e.version === 'string') || null;
|
|
52
|
+
_cache = { lts: ltsEntry ? ltsEntry.version : null, ltsName: ltsEntry ? String(ltsEntry.lts) : null, latest, ts: now, ok: true };
|
|
53
|
+
} catch {
|
|
54
|
+
// keep whatever was known before, just mark it stale
|
|
55
|
+
_cache = _cache ? { ..._cache, ts: now, ok: false } : { lts: null, ltsName: null, latest: null, ts: now, ok: false };
|
|
56
|
+
} finally {
|
|
57
|
+
_inflight = null;
|
|
58
|
+
}
|
|
59
|
+
return { lts: _cache.lts, ltsName: _cache.ltsName, latest: _cache.latest, fetchedAt: _cache.ok ? _cache.ts : null, stale: !_cache.ok };
|
|
60
|
+
})();
|
|
61
|
+
|
|
62
|
+
return _inflight;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { releases, setFetch, clearCache, INDEX_URL, TTL };
|
package/src/lib/services-host.js
CHANGED
|
@@ -26,7 +26,7 @@ const { execFile, spawn } = require('child_process');
|
|
|
26
26
|
const sshDeploy = require('./ssh-deploy');
|
|
27
27
|
|
|
28
28
|
const HELPER = '/usr/local/bin/she-servicectl';
|
|
29
|
-
const HELPER_VERSION =
|
|
29
|
+
const HELPER_VERSION = 13; // must match VERSION in service/she-servicectl (test/unit/helper-version.test.js)
|
|
30
30
|
/** Default identity for services.hosts[].ssh — one key for all managed hosts (I5). */
|
|
31
31
|
const DEFAULT_SERVICES_IDENTITY = '~/.she/ssh/services_id_ed25519';
|
|
32
32
|
/** The helper as shipped with this she version (copied to remote hosts by the deploy route). */
|
package/src/web/config-api.js
CHANGED
|
@@ -50,4 +50,51 @@ router.put('/', (req, res) => {
|
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* PATCH / — write a single subtree, leaving the rest of the file as it is on disk.
|
|
55
|
+
*
|
|
56
|
+
* Body: { path: 'broker.ssh', value: … }. A page that edits one branch of the config
|
|
57
|
+
* must not send the whole document back: it would carry a snapshot taken when the page
|
|
58
|
+
* was opened and silently revert anything another page wrote in the meantime (that is
|
|
59
|
+
* how a freshly configured services.hosts list disappeared when the broker SSH page was
|
|
60
|
+
* saved afterwards). Omitting `value` deletes the key.
|
|
61
|
+
*/
|
|
62
|
+
router.patch('/', (req, res) => {
|
|
63
|
+
const configPath = req.app.locals.configPath || DEFAULT_CONFIG_PATH;
|
|
64
|
+
try {
|
|
65
|
+
const { path: dotted, value } = req.body || {};
|
|
66
|
+
if (typeof dotted !== 'string' || !dotted) return res.status(400).json({ error: 'path required' });
|
|
67
|
+
const segments = dotted.split('.');
|
|
68
|
+
if (segments.length > 5 || !segments.every((k) => /^[A-Za-z0-9_-]+$/.test(k))) {
|
|
69
|
+
return res.status(400).json({ error: `invalid config path: ${dotted}` });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let config = {};
|
|
73
|
+
try {
|
|
74
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
75
|
+
} catch {
|
|
76
|
+
/* ok — file may not exist yet */
|
|
77
|
+
}
|
|
78
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
|
|
79
|
+
|
|
80
|
+
const before = JSON.stringify(config[segments[0]]);
|
|
81
|
+
let node = config;
|
|
82
|
+
for (const key of segments.slice(0, -1)) {
|
|
83
|
+
if (!node[key] || typeof node[key] !== 'object' || Array.isArray(node[key])) node[key] = {};
|
|
84
|
+
node = node[key];
|
|
85
|
+
}
|
|
86
|
+
const last = segments[segments.length - 1];
|
|
87
|
+
if (value === undefined) delete node[last];
|
|
88
|
+
else node[last] = value;
|
|
89
|
+
|
|
90
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
91
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
92
|
+
|
|
93
|
+
const restartRequired = !FRONTEND_ONLY_KEYS.has(segments[0]) && before !== JSON.stringify(config[segments[0]]);
|
|
94
|
+
res.json({ ok: true, restartRequired, configPath });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
res.status(500).json({ error: err.message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
53
100
|
module.exports = { router, DEFAULT_CONFIG_PATH };
|
package/src/web/services-api.js
CHANGED
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
* GET /she/services/ssh/pubkey public key of the services identity (I5)
|
|
30
30
|
* POST /she/services/ssh/keygen generate it
|
|
31
31
|
* POST /she/services/hosts/:host/test run `she-servicectl version` → ok / code
|
|
32
|
+
* GET /she/services/node/releases[?refresh=1] what a node update would install: the newest lts and latest release
|
|
33
|
+
* POST /she/services/hosts/:host/node/update { channel: lts|latest } update Node.js with tj/n (helper v13)
|
|
34
|
+
* POST /she/services/hosts/:host/instances/restart-all restart every running instance (after a node update)
|
|
32
35
|
* POST /she/services/hosts/:host/helper/deploy scp the helper to a remote host, install it, print the sudoers line
|
|
33
36
|
* POST /she/services/hosts/:host/helper/remove { mode: key|all, force? } remove she from the host (I11), drop the host entry
|
|
34
37
|
* POST /she/services/setup/token mint a one-time token + the curl | bash command (I9)
|
|
@@ -54,6 +57,7 @@ const dns = require('dns');
|
|
|
54
57
|
const { analyzeServices, wipeTopics, LOG_LEVELS } = require('../lib/services-inventory');
|
|
55
58
|
const { shapeDevices, discoverTarget } = require('../lib/device-discovery');
|
|
56
59
|
const npmRegistry = require('../lib/npm-registry');
|
|
60
|
+
const nodeReleases = require('../lib/node-releases');
|
|
57
61
|
const catalog = require('../lib/services-catalog');
|
|
58
62
|
const os = require('os');
|
|
59
63
|
const path = require('path');
|
|
@@ -1030,6 +1034,65 @@ router.post('/hosts/:host/adapters/:adapter/update', async (req, res) => {
|
|
|
1030
1034
|
}
|
|
1031
1035
|
});
|
|
1032
1036
|
|
|
1037
|
+
/** The two node commands need helper v13; older hosts answer with "unknown command". */
|
|
1038
|
+
function nodeHostError(res, err) {
|
|
1039
|
+
if (/unknown command/i.test(err.message || '')) {
|
|
1040
|
+
return res.status(400).json({
|
|
1041
|
+
error: 'the she-servicectl helper on this host is too old for Node.js management — update it on the Hosts tab',
|
|
1042
|
+
code: 'HELPER_OUTDATED',
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
return hostError(res, err);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// GET /she/services/node/releases[?refresh=1] — what the update buttons would install
|
|
1049
|
+
router.get('/node/releases', async (req, res) => {
|
|
1050
|
+
res.json(await nodeReleases.releases({ force: req.query.refresh === '1' }));
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// POST /she/services/hosts/:host/node/update { channel?: 'lts' | 'latest' }
|
|
1054
|
+
router.post('/hosts/:host/node/update', async (req, res) => {
|
|
1055
|
+
const entry = resolve(req, res);
|
|
1056
|
+
if (!entry) return;
|
|
1057
|
+
// 'stable' is n's old alias for lts — accepted so an older client keeps working
|
|
1058
|
+
const channel = (req.body && req.body.channel) === 'stable' ? 'lts' : (req.body && req.body.channel) || 'lts';
|
|
1059
|
+
if (channel !== 'lts' && channel !== 'latest') return res.status(400).json({ error: "channel must be 'lts' or 'latest'" });
|
|
1060
|
+
try {
|
|
1061
|
+
// downloading and unpacking a node build on a small host takes a while
|
|
1062
|
+
const { stdout } = await entry.driver.exec(['node', 'update', '--' + channel], { timeout: 900000 });
|
|
1063
|
+
let result = {};
|
|
1064
|
+
try {
|
|
1065
|
+
result = JSON.parse(stdout);
|
|
1066
|
+
} catch {
|
|
1067
|
+
return res.status(500).json({ error: 'helper returned unexpected output', output: stdout });
|
|
1068
|
+
}
|
|
1069
|
+
invalidateHosts();
|
|
1070
|
+
res.json({ ok: true, ...result, restartRequired: result.before !== result.after });
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
nodeHostError(res, err);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// POST /she/services/hosts/:host/instances/restart-all
|
|
1077
|
+
router.post('/hosts/:host/instances/restart-all', async (req, res) => {
|
|
1078
|
+
const entry = resolve(req, res);
|
|
1079
|
+
if (!entry) return;
|
|
1080
|
+
try {
|
|
1081
|
+
const { stdout } = await entry.driver.exec(['restart-all'], { timeout: 300000 });
|
|
1082
|
+
let result = { restarted: [] };
|
|
1083
|
+
try {
|
|
1084
|
+
result = JSON.parse(stdout);
|
|
1085
|
+
} catch {
|
|
1086
|
+
return res.status(500).json({ error: 'helper returned unexpected output', output: stdout });
|
|
1087
|
+
}
|
|
1088
|
+
const restarted = (result.restarted || []).filter((r) => r.ok);
|
|
1089
|
+
const failed = (result.restarted || []).filter((r) => !r.ok);
|
|
1090
|
+
res.json({ ok: failed.length === 0, restarted, failed });
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
nodeHostError(res, err);
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1033
1096
|
// POST /she/services/hosts/:host/units/:adapter/-/migrate { name } — legacy <adapter>.service → <adapter>@<name>
|
|
1034
1097
|
router.post('/hosts/:host/units/:adapter/' + LEGACY + '/migrate', async (req, res) => {
|
|
1035
1098
|
const entry = resolve(req, res, { adapter: true });
|