smart-home-engine 1.53.4 → 2.0.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-Cp--IWao.css → index-Bvl2uECO.css} +1 -1
- package/dist/web/assets/index-DLP7FxoS.js +305 -0
- package/dist/web/assets/{tsMode-Ci4l56iF.js → tsMode-BJVHqGQB.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -2
- package/src/config.js +21 -1
- package/src/index.js +107 -9
- package/src/lib/crash-record.js +103 -0
- package/src/lib/secrets.js +28 -1
- package/src/matter/bindings.js +291 -0
- package/src/matter/bridge-templates.js +261 -0
- package/src/matter/controller.js +1299 -12
- package/src/matter/dcl.js +252 -0
- package/src/matter/device-health.js +337 -0
- package/src/matter/groups.js +192 -0
- package/src/matter/mqtt-bridge.js +513 -0
- package/src/matter/thread-datasets.js +285 -0
- package/src/matter/thread-health.js +278 -0
- package/src/matter/thread-mesh.js +496 -0
- package/src/matter/thread.js +267 -0
- package/src/web/ai-tools.js +369 -0
- package/src/web/matter-api.js +560 -0
- package/src/web/prompts/tools.md +2 -1
- package/src/web/server.js +4 -1
- package/dist/web/assets/index-DPRtP55L.js +0 -298
|
@@ -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-DLP7FxoS.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
|
@@ -175,10 +175,10 @@
|
|
|
175
175
|
}
|
|
176
176
|
})();
|
|
177
177
|
</script>
|
|
178
|
-
<script type="module" crossorigin src="/assets/index-
|
|
178
|
+
<script type="module" crossorigin src="/assets/index-DLP7FxoS.js"></script>
|
|
179
179
|
<link rel="modulepreload" crossorigin href="/assets/monaco-langs-DKJI_o9J.js">
|
|
180
180
|
<link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
|
|
181
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
181
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bvl2uECO.css">
|
|
182
182
|
</head>
|
|
183
183
|
<body>
|
|
184
184
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-home-engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Node.js based script runner for use in MQTT based Smart Home environments",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@elastic/elasticsearch": "^9.4.2",
|
|
34
34
|
"@influxdata/influxdb-client": "^1.35.0",
|
|
35
|
-
"@matter/
|
|
35
|
+
"@matter/dcl-data": "^2026.10.18",
|
|
36
|
+
"@matter/main": "^0.17.9",
|
|
37
|
+
"@matter/thread-br-client": "^0.17.9",
|
|
36
38
|
"bcryptjs": "^2.4.3",
|
|
37
39
|
"chokidar": "^4.0.0",
|
|
38
40
|
"express": "^5.2.1",
|
package/src/config.js
CHANGED
|
@@ -29,6 +29,11 @@ const config = require('yargs')
|
|
|
29
29
|
describe: 'enable Matter controller; pass a directory path or true to use <data-dir>/matter',
|
|
30
30
|
type: 'string',
|
|
31
31
|
})
|
|
32
|
+
.option('matter-dcl-offline', {
|
|
33
|
+
describe: 'never contact the CSA Distributed Compliance Ledger (vendor names from the bundled snapshot, product details from the cache only)',
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
default: false,
|
|
36
|
+
})
|
|
32
37
|
.option('port', {
|
|
33
38
|
alias: 'p',
|
|
34
39
|
describe: 'HTTP server port (0 = OS-assigned random port)',
|
|
@@ -127,10 +132,25 @@ const config = require('yargs')
|
|
|
127
132
|
type: 'boolean',
|
|
128
133
|
})
|
|
129
134
|
.option('safe-mode-auto-detect', {
|
|
130
|
-
describe: 'enter safe mode automatically
|
|
135
|
+
describe: 'enter safe mode automatically when the daemon crashes repeatedly shortly after start (default: true)',
|
|
131
136
|
default: true,
|
|
132
137
|
type: 'boolean',
|
|
133
138
|
})
|
|
139
|
+
.option('safe-mode-early-crash', {
|
|
140
|
+
describe: 'seconds after start within which an unclean end counts as an early crash (default: 300)',
|
|
141
|
+
default: 300,
|
|
142
|
+
type: 'number',
|
|
143
|
+
})
|
|
144
|
+
.option('safe-mode-crash-count', {
|
|
145
|
+
describe: 'how many early crashes in a row put the next start into safe mode (default: 3)',
|
|
146
|
+
default: 3,
|
|
147
|
+
type: 'number',
|
|
148
|
+
})
|
|
149
|
+
.option('safe-mode-crash-window', {
|
|
150
|
+
describe: 'seconds within which those crashes must have happened (default: 1800)',
|
|
151
|
+
default: 1800,
|
|
152
|
+
type: 'number',
|
|
153
|
+
})
|
|
134
154
|
.option('script-timeout', {
|
|
135
155
|
describe: "ms a script's synchronous top-level code may run before it is terminated, 0 = no limit (default: 5000)",
|
|
136
156
|
default: 5000,
|
package/src/index.js
CHANGED
|
@@ -102,23 +102,44 @@ const _sentinelFile = _path.join(require('./lib/storage').STORAGE_ROOT, '.she-ru
|
|
|
102
102
|
* unrelated process after a reboot is the one case this misses, which is the harmless
|
|
103
103
|
* direction to be wrong in.
|
|
104
104
|
*/
|
|
105
|
+
const _crashRecord = require('./lib/crash-record');
|
|
106
|
+
const _crashFile = _path.join(require('./lib/storage').STORAGE_ROOT, '.she-crashes.json');
|
|
107
|
+
let _deadRun = null; // the stale sentinel's { pid, startMs }
|
|
105
108
|
function _sentinelState() {
|
|
106
|
-
let
|
|
109
|
+
let parsed;
|
|
107
110
|
try {
|
|
108
|
-
|
|
111
|
+
parsed = _crashRecord.parseSentinel(_fs.readFileSync(_sentinelFile, 'utf8'));
|
|
109
112
|
} catch {
|
|
110
113
|
return null; // no marker
|
|
111
114
|
}
|
|
112
|
-
if (!pid)
|
|
115
|
+
if (!parsed.pid) {
|
|
116
|
+
_deadRun = parsed;
|
|
117
|
+
return 'stale';
|
|
118
|
+
}
|
|
113
119
|
try {
|
|
114
|
-
process.kill(pid, 0);
|
|
120
|
+
process.kill(parsed.pid, 0);
|
|
115
121
|
return 'live';
|
|
116
122
|
} catch (e) {
|
|
117
|
-
|
|
123
|
+
if (e.code === 'EPERM') return 'live'; // alive, just not ours
|
|
124
|
+
_deadRun = parsed;
|
|
125
|
+
return 'stale';
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
const _sentinelBefore = _sentinelState();
|
|
121
|
-
|
|
129
|
+
// A stale sentinel alone is no longer a reason for safe mode (roadmap A10): only a crash that
|
|
130
|
+
// repeats and happens early — the signature of a script that kills the daemon at load — is.
|
|
131
|
+
// A single crash after days of uptime restarts normally, so the house keeps its automation.
|
|
132
|
+
let _crashEval = null;
|
|
133
|
+
if (_sentinelBefore === 'stale') {
|
|
134
|
+
_crashEval = _crashRecord.evaluate(_deadRun, _crashRecord.readCrashes(_crashFile), {
|
|
135
|
+
earlyCrashSec: config.safeModeEarlyCrash,
|
|
136
|
+
crashCount: config.safeModeCrashCount,
|
|
137
|
+
windowSec: config.safeModeCrashWindow,
|
|
138
|
+
});
|
|
139
|
+
_crashRecord.writeCrashes(_crashFile, _crashEval.crashes);
|
|
140
|
+
}
|
|
141
|
+
const SAFE_MODE = config.safeMode === true || (config.safeModeAutoDetect !== false && _sentinelBefore === 'stale' && !!_crashEval?.safeMode);
|
|
142
|
+
const SAFE_MODE_REASON = !SAFE_MODE ? null : config.safeMode === true ? '--safe-mode' : _crashEval.reason;
|
|
122
143
|
let _sentinelWritten = false;
|
|
123
144
|
function _clearSentinel() {
|
|
124
145
|
if (!_sentinelWritten) return;
|
|
@@ -131,7 +152,7 @@ function _clearSentinel() {
|
|
|
131
152
|
}
|
|
132
153
|
if (_sentinelBefore !== 'live') {
|
|
133
154
|
try {
|
|
134
|
-
_fs.writeFileSync(_sentinelFile,
|
|
155
|
+
_fs.writeFileSync(_sentinelFile, _crashRecord.formatSentinel(process.pid, Date.now()));
|
|
135
156
|
_sentinelWritten = true;
|
|
136
157
|
} catch (e) {
|
|
137
158
|
// not fatal — auto-detect simply never triggers when the file cannot be written
|
|
@@ -185,8 +206,19 @@ if (SAFE_MODE) {
|
|
|
185
206
|
if (config.safeMode === true) log.warn('SAFE MODE (--safe-mode): user scripts are not loaded. Restart without the flag to run them.');
|
|
186
207
|
else
|
|
187
208
|
log.warn(
|
|
188
|
-
`SAFE MODE: ${_sentinelFile} was left behind, so the
|
|
209
|
+
`SAFE MODE: ${_crashEval.reason} — ${_sentinelFile} was left behind each time, so the daemon was killed rather than stopped shortly after start; a script may be blocking the event loop or crashing the daemon at load. User scripts are not loaded; fix the script and restart. Set "safeModeAutoDetect": false to disable this detection, or raise safeModeCrashCount.`,
|
|
189
210
|
);
|
|
211
|
+
} else if (_sentinelBefore === 'stale' && _crashEval) {
|
|
212
|
+
log.warn(`${_crashEval.message} (${_sentinelFile} was left behind${config.safeModeAutoDetect === false ? '; safe-mode detection is off' : ''})`);
|
|
213
|
+
}
|
|
214
|
+
// a run that stays up past the early-crash threshold is healthy: the crash record starts over
|
|
215
|
+
if (_sentinelBefore !== 'live' && !SAFE_MODE) {
|
|
216
|
+
setTimeout(
|
|
217
|
+
() => {
|
|
218
|
+
_crashRecord.writeCrashes(_crashFile, []);
|
|
219
|
+
},
|
|
220
|
+
(config.safeModeEarlyCrash || 300) * 1000,
|
|
221
|
+
).unref();
|
|
190
222
|
}
|
|
191
223
|
log.debug('loaded config: ', config);
|
|
192
224
|
|
|
@@ -386,6 +418,7 @@ const _SENTINEL_TIMEOUT_MS = config.sentinelTimeout; // ms to wait for sentinel
|
|
|
386
418
|
// she.matter.* at top level don't fail with "Matter controller not started".
|
|
387
419
|
let _mqttGate = false;
|
|
388
420
|
let _matterGate = !config.matterStorage; // pre-open when matter is disabled
|
|
421
|
+
let _matterStoragePath = null;
|
|
389
422
|
|
|
390
423
|
function _maybeStart() {
|
|
391
424
|
if (_started || !_mqttGate || !_matterGate) {
|
|
@@ -396,6 +429,41 @@ function _maybeStart() {
|
|
|
396
429
|
}
|
|
397
430
|
_started = true;
|
|
398
431
|
start();
|
|
432
|
+
// the Matter ↔ MQTT bridge needs both: the controller and the retained state (M19)
|
|
433
|
+
if (config.matterStorage) {
|
|
434
|
+
try {
|
|
435
|
+
require('./matter/mqtt-bridge').start();
|
|
436
|
+
} catch (err) {
|
|
437
|
+
log.warn('matter bridge start failed:', err.message);
|
|
438
|
+
}
|
|
439
|
+
// Device health (M21): batteries, last seen, offline history — published under <name>/status/matter/health
|
|
440
|
+
try {
|
|
441
|
+
require('./matter/device-health').start({
|
|
442
|
+
publish: (topic, payload, opts) => {
|
|
443
|
+
if (mqtt && connected) mqtt.publish(topic, payload, opts || {});
|
|
444
|
+
},
|
|
445
|
+
name: config.name,
|
|
446
|
+
log,
|
|
447
|
+
broadcast,
|
|
448
|
+
storageDir: _matterStoragePath,
|
|
449
|
+
});
|
|
450
|
+
} catch (err) {
|
|
451
|
+
log.warn('matter device health start failed:', err.message);
|
|
452
|
+
}
|
|
453
|
+
// Thread health alerts (M26): evaluated once a minute, published under <name>/status/thread/health
|
|
454
|
+
try {
|
|
455
|
+
require('./matter/thread-health').start({
|
|
456
|
+
publish: (topic, payload, opts) => {
|
|
457
|
+
if (mqtt && connected) mqtt.publish(topic, payload, opts || {});
|
|
458
|
+
},
|
|
459
|
+
name: config.name,
|
|
460
|
+
log,
|
|
461
|
+
broadcast,
|
|
462
|
+
});
|
|
463
|
+
} catch (err) {
|
|
464
|
+
log.warn('matter thread health start failed:', err.message);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
399
467
|
}
|
|
400
468
|
|
|
401
469
|
function matterGateOpen() {
|
|
@@ -508,6 +576,7 @@ const statsSnapshot = () => {
|
|
|
508
576
|
elMeanMs,
|
|
509
577
|
elMaxMs,
|
|
510
578
|
safeMode: SAFE_MODE,
|
|
579
|
+
safeModeReason: SAFE_MODE_REASON,
|
|
511
580
|
};
|
|
512
581
|
};
|
|
513
582
|
require('./web/server').setStatsProvider(statsSnapshot);
|
|
@@ -519,6 +588,7 @@ const healthSnapshot = () => ({
|
|
|
519
588
|
mqttConnected: connected,
|
|
520
589
|
scripts: Object.keys(scripts).length,
|
|
521
590
|
safeMode: SAFE_MODE,
|
|
591
|
+
safeModeReason: SAFE_MODE_REASON,
|
|
522
592
|
});
|
|
523
593
|
require('./web/server').setHealthProvider(healthSnapshot);
|
|
524
594
|
|
|
@@ -652,6 +722,15 @@ if (config.url) {
|
|
|
652
722
|
|
|
653
723
|
if (shedb.handleMqttMessage(topic, payload)) return;
|
|
654
724
|
|
|
725
|
+
// matter/set/… and matter/get/… go to the Matter bridge (M19); the message is still stored below
|
|
726
|
+
if (config.matterStorage && _started) {
|
|
727
|
+
try {
|
|
728
|
+
require('./matter/mqtt-bridge').onMessage(topic, payload);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
log.warn('matter bridge:', err.message);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
655
734
|
// An empty payload deletes the topic from the state store (mqtt-smarthome
|
|
656
735
|
// convention; also how a retained message is cleared per MQTT spec). The
|
|
657
736
|
// retain flag cannot be used to detect this: MQTT 3.1.1 brokers clear it
|
|
@@ -774,6 +853,18 @@ if (config.matterStorage) {
|
|
|
774
853
|
matterStoragePath = ensureStorageDir('matter');
|
|
775
854
|
}
|
|
776
855
|
log.info('matter controller starting, storage:', matterStoragePath);
|
|
856
|
+
_matterStoragePath = matterStoragePath;
|
|
857
|
+
require('./matter/mqtt-bridge').init({
|
|
858
|
+
publish: (topic, payload, opts) => {
|
|
859
|
+
if (!mqtt) throw new Error('MQTT is not connected');
|
|
860
|
+
mqtt.publish(topic, payload, opts || {});
|
|
861
|
+
},
|
|
862
|
+
isConnected: () => !!mqtt && connected,
|
|
863
|
+
store,
|
|
864
|
+
log,
|
|
865
|
+
storageDir: matterStoragePath,
|
|
866
|
+
broadcast,
|
|
867
|
+
});
|
|
777
868
|
// Scripts wait for the controller (see _maybeStart) — but never forever:
|
|
778
869
|
// a hanging start must not block the whole script engine.
|
|
779
870
|
const _matterStartTimeout = setTimeout(() => {
|
|
@@ -781,7 +872,7 @@ if (config.matterStorage) {
|
|
|
781
872
|
matterGateOpen();
|
|
782
873
|
}, 30000);
|
|
783
874
|
matterController
|
|
784
|
-
.init(matterStoragePath, log, broadcast)
|
|
875
|
+
.init(matterStoragePath, log, broadcast, { dclOffline: config.matterDclOffline === true })
|
|
785
876
|
.catch((err) => {
|
|
786
877
|
log.error('matter controller init failed:', err.message, err.stack);
|
|
787
878
|
})
|
|
@@ -1796,6 +1887,13 @@ async function gracefulShutdown(signal) {
|
|
|
1796
1887
|
_clearSentinel();
|
|
1797
1888
|
log.info(`got ${signal}. exiting.`);
|
|
1798
1889
|
if (config.matterStorage) {
|
|
1890
|
+
try {
|
|
1891
|
+
require('./matter/mqtt-bridge').stop();
|
|
1892
|
+
require('./matter/thread-health').stop();
|
|
1893
|
+
require('./matter/device-health').stop();
|
|
1894
|
+
} catch {
|
|
1895
|
+
// ignore
|
|
1896
|
+
}
|
|
1799
1897
|
try {
|
|
1800
1898
|
await require('./matter/controller').close();
|
|
1801
1899
|
} catch {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Crash record for the safe-mode decision (roadmap A10). Safe mode — starting without any user
|
|
5
|
+
* script — is the right answer to a script that kills the daemon at or shortly after load, where
|
|
6
|
+
* every restart would fail the same way; it is the wrong answer to a crash after days of normal
|
|
7
|
+
* operation, where systemd restarts the daemon and the house then runs without automation until
|
|
8
|
+
* someone notices. So: a crash on its own is not a reason. Safe mode only when the crash repeats
|
|
9
|
+
* and happens early.
|
|
10
|
+
*
|
|
11
|
+
* The sentinel `.she-running` carries `<pid> <startMs>`; a stale one at start tells when the dead
|
|
12
|
+
* run began. The crash record `.she-crashes.json` next to it keeps the last few unclean runs. Pure
|
|
13
|
+
* functions here; the daemon reads and writes the files.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
const DEFAULTS = Object.freeze({ earlyCrashSec: 300, crashCount: 3, windowSec: 1800 });
|
|
19
|
+
const KEEP = 10;
|
|
20
|
+
|
|
21
|
+
/** `<pid> <startMs>` (or the pre-A10 `<pid>` alone) → { pid, startMs }; null when unreadable. */
|
|
22
|
+
function parseSentinel(text) {
|
|
23
|
+
const parts = String(text ?? '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split(/\s+/);
|
|
26
|
+
const pid = parseInt(parts[0], 10);
|
|
27
|
+
if (!Number.isFinite(pid) || pid <= 0) return { pid: null, startMs: null };
|
|
28
|
+
const startMs = parts.length > 1 ? parseInt(parts[1], 10) : NaN;
|
|
29
|
+
return { pid, startMs: Number.isFinite(startMs) && startMs > 0 ? startMs : null };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatSentinel(pid, startMs) {
|
|
33
|
+
return `${pid} ${startMs}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readCrashes(file) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
39
|
+
const list = Array.isArray(raw) ? raw : Array.isArray(raw?.crashes) ? raw.crashes : [];
|
|
40
|
+
return list.filter((c) => c && typeof c === 'object' && Number.isFinite(c.end));
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeCrashes(file, crashes) {
|
|
47
|
+
try {
|
|
48
|
+
if (!crashes.length) {
|
|
49
|
+
fs.rmSync(file, { force: true });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
fs.writeFileSync(file, JSON.stringify({ v: 1, crashes: crashes.slice(-KEEP) }) + '\n');
|
|
53
|
+
} catch {
|
|
54
|
+
/* a record that cannot be written costs at most one safe-mode decision */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fmtUptime(ms) {
|
|
59
|
+
if (ms === null) return 'unknown uptime';
|
|
60
|
+
const s = Math.round(ms / 1000);
|
|
61
|
+
if (s < 120) return `${s} s`;
|
|
62
|
+
const m = Math.round(s / 60);
|
|
63
|
+
if (m < 120) return `${m} min`;
|
|
64
|
+
const h = Math.round(m / 60);
|
|
65
|
+
if (h < 48) return `${h} h`;
|
|
66
|
+
return `${Math.round(h / 24)} d`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The decision at a start that found a stale sentinel.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ startMs: number|null }} dead the dead run (from its sentinel; startMs null for a pre-A10 sentinel)
|
|
73
|
+
* @param {{ start: number|null, end: number, early: boolean }[]} crashes the record so far
|
|
74
|
+
* @param {{ now?: number, earlyCrashSec?: number, crashCount?: number, windowSec?: number }} [opts]
|
|
75
|
+
* @returns {{ safeMode: boolean, reason: string|null, crashes: object[], uptimeMs: number|null, early: boolean, message: string }}
|
|
76
|
+
*/
|
|
77
|
+
function evaluate(dead, crashes, opts = {}) {
|
|
78
|
+
const now = opts.now ?? Date.now();
|
|
79
|
+
const earlyMs = (opts.earlyCrashSec ?? DEFAULTS.earlyCrashSec) * 1000;
|
|
80
|
+
const count = Math.max(1, opts.crashCount ?? DEFAULTS.crashCount);
|
|
81
|
+
const windowMs = (opts.windowSec ?? DEFAULTS.windowSec) * 1000;
|
|
82
|
+
const uptimeMs = dead?.startMs ? Math.max(0, now - dead.startMs) : null;
|
|
83
|
+
// a dead run whose start is unknown (a sentinel from before this record) counts as early: that is
|
|
84
|
+
// the case the detection was built for, and it happens once at most
|
|
85
|
+
const early = uptimeMs === null || uptimeMs < earlyMs;
|
|
86
|
+
const list = [...crashes, { start: dead?.startMs ?? null, end: now, early }].slice(-KEEP);
|
|
87
|
+
const recent = list.slice(-count);
|
|
88
|
+
const safeMode = early && recent.length >= count && recent.every((c) => c.early && now - c.end <= windowMs);
|
|
89
|
+
let reason = null;
|
|
90
|
+
let message;
|
|
91
|
+
if (safeMode) {
|
|
92
|
+
reason = `${count} crashes within ${fmtUptime(earlyMs)} of start (last ${fmtUptime(now - recent[0].end)})`;
|
|
93
|
+
message = `the previous run was killed ${uptimeMs === null ? '' : 'after ' + fmtUptime(uptimeMs) + ' '}— the ${count}. early crash in a row`;
|
|
94
|
+
} else if (early) {
|
|
95
|
+
const n = list.filter((c, i) => i >= list.length - count && c.early && now - c.end <= windowMs).length;
|
|
96
|
+
message = `the previous run was killed ${uptimeMs === null ? '(uptime unknown) ' : 'after ' + fmtUptime(uptimeMs) + ' '}— early crash ${n} of ${count} within ${fmtUptime(windowMs)}; starting normally`;
|
|
97
|
+
} else {
|
|
98
|
+
message = `the previous run was killed after ${fmtUptime(uptimeMs)} of uptime — not an early crash; starting normally`;
|
|
99
|
+
}
|
|
100
|
+
return { safeMode, reason, crashes: list, uptimeMs, early, message };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { DEFAULTS, KEEP, parseSentinel, formatSentinel, readCrashes, writeCrashes, evaluate, fmtUptime };
|
package/src/lib/secrets.js
CHANGED
|
@@ -317,4 +317,31 @@ function cli(argv, io = { stdout: process.stdout, stderr: process.stderr, stdin:
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
|
|
320
|
+
/**
|
|
321
|
+
* The encryption of the secrets store for other files that must not sit in plaintext in the data
|
|
322
|
+
* directory (the Matter group keys, the Thread datasets): same key, same format, their own file.
|
|
323
|
+
*/
|
|
324
|
+
function readEncryptedFile(file) {
|
|
325
|
+
let raw;
|
|
326
|
+
try {
|
|
327
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
328
|
+
} catch (err) {
|
|
329
|
+
if (err.code === 'ENOENT') return null;
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
const key = loadKey();
|
|
333
|
+
if (!key) throw new Error(`no key to read ${file}: set SHE_SECRETS_KEY or restore ${_keyFile}`);
|
|
334
|
+
const obj = JSON.parse(raw);
|
|
335
|
+
if (!obj || obj.alg !== ALG) throw new Error(`${file}: unknown file format`);
|
|
336
|
+
return JSON.parse(decrypt(key, obj));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function writeEncryptedFile(file, data) {
|
|
340
|
+
const key = loadKey({ create: true });
|
|
341
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
342
|
+
const tmp = file + '.tmp';
|
|
343
|
+
fs.writeFileSync(tmp, JSON.stringify(encrypt(key, JSON.stringify(data))) + '\n', { mode: 0o600 });
|
|
344
|
+
fs.renameSync(tmp, file);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = { init, load, status, list, get, has, set, mark, remove, redact, cli, validName, NAME_RE, MAX_VALUE, MIN_REDACT, readEncryptedFile, writeEncryptedFile };
|