iobroker.bee 0.0.0-stage → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -2
- package/admin/bee.svg +1 -0
- package/admin/jsonConfig.json +321 -0
- package/build/lib/commands.js +96 -0
- package/build/lib/commands.js.map +1 -0
- package/build/lib/console.js +218 -0
- package/build/lib/console.js.map +1 -0
- package/build/lib/enrollment.js +37 -0
- package/build/lib/enrollment.js.map +1 -0
- package/build/lib/inventory.js +136 -0
- package/build/lib/inventory.js.map +1 -0
- package/build/lib/jobs.js +251 -0
- package/build/lib/jobs.js.map +1 -0
- package/build/lib/packages.js +139 -0
- package/build/lib/packages.js.map +1 -0
- package/build/lib/planner.js +128 -0
- package/build/lib/planner.js.map +1 -0
- package/build/lib/preflight.js +90 -0
- package/build/lib/preflight.js.map +1 -0
- package/build/lib/protocol.js +70 -0
- package/build/lib/protocol.js.map +1 -0
- package/build/lib/remote.js +211 -0
- package/build/lib/remote.js.map +1 -0
- package/build/lib/transport.js +158 -0
- package/build/lib/transport.js.map +1 -0
- package/build/lib/tunnel.js +293 -0
- package/build/lib/tunnel.js.map +1 -0
- package/build/lib/util.js +102 -0
- package/build/lib/util.js.map +1 -0
- package/build/main.js +835 -0
- package/build/main.js.map +1 -0
- package/build/runner.js +193 -0
- package/build/runner.js.map +1 -0
- package/io-package.json +434 -0
- package/package.json +54 -4
package/build/main.js
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const utils = __importStar(require("@iobroker/adapter-core"));
|
|
40
|
+
const node_fs_1 = require("node:fs");
|
|
41
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
42
|
+
const node_path_1 = require("node:path");
|
|
43
|
+
const commands_1 = require("./lib/commands");
|
|
44
|
+
const console_1 = require("./lib/console");
|
|
45
|
+
const enrollment_1 = require("./lib/enrollment");
|
|
46
|
+
const inventory_1 = require("./lib/inventory");
|
|
47
|
+
const jobs_1 = require("./lib/jobs");
|
|
48
|
+
const packages_1 = require("./lib/packages");
|
|
49
|
+
const planner_1 = require("./lib/planner");
|
|
50
|
+
const preflight_1 = require("./lib/preflight");
|
|
51
|
+
const protocol_1 = require("./lib/protocol");
|
|
52
|
+
const remote_1 = require("./lib/remote");
|
|
53
|
+
const transport_1 = require("./lib/transport");
|
|
54
|
+
const tunnel_1 = require("./lib/tunnel");
|
|
55
|
+
const util_1 = require("./lib/util");
|
|
56
|
+
class BeeAdapter extends utils.Adapter {
|
|
57
|
+
dataDir = '';
|
|
58
|
+
creds = null;
|
|
59
|
+
transport = null;
|
|
60
|
+
jobs = null;
|
|
61
|
+
desired = null;
|
|
62
|
+
control = null;
|
|
63
|
+
persisted = { results: {}, lastResult: null, lastBackup: null, notified: {} };
|
|
64
|
+
timers = [];
|
|
65
|
+
enrollTimer = null;
|
|
66
|
+
enrollBackoff = 30_000;
|
|
67
|
+
seq = 0;
|
|
68
|
+
lastFlat = null;
|
|
69
|
+
uuid = '';
|
|
70
|
+
errorLog = [];
|
|
71
|
+
errorsSinceHeartbeat = 0;
|
|
72
|
+
reconciling = false;
|
|
73
|
+
/** A command of the fleet management is being carried out (one at a time) */
|
|
74
|
+
commandRunning = false;
|
|
75
|
+
lastWaitReason = '';
|
|
76
|
+
unloading = false;
|
|
77
|
+
/** Open remote access sessions by session id */
|
|
78
|
+
tunnels = new Map();
|
|
79
|
+
/** Running shells of the console per session */
|
|
80
|
+
shells = new Map();
|
|
81
|
+
constructor(options = {}) {
|
|
82
|
+
super({ ...options, name: 'bee' });
|
|
83
|
+
this.on('ready', () => this.onReady());
|
|
84
|
+
this.on('stateChange', (id, state) => this.onStateChange(id, state));
|
|
85
|
+
this.on('unload', callback => void this.onUnload(callback));
|
|
86
|
+
this.on('log', (msg) => this.onLog(msg));
|
|
87
|
+
}
|
|
88
|
+
// ------------------------------------------------------------------ lifecycle
|
|
89
|
+
async onReady() {
|
|
90
|
+
this.dataDir = utils.getAbsoluteInstanceDataDir(this);
|
|
91
|
+
(0, node_fs_1.mkdirSync)(this.dataDir, { recursive: true });
|
|
92
|
+
await this.setState('info.connection', false, true);
|
|
93
|
+
const meta = await this.getForeignObjectAsync('system.meta.uuid');
|
|
94
|
+
this.uuid = meta?.native?.uuid || '';
|
|
95
|
+
this.persisted = { ...this.persisted, ...(await (0, util_1.readJson)((0, node_path_1.join)(this.dataDir, 'state.json'))) };
|
|
96
|
+
this.desired = await (0, util_1.readJson)((0, node_path_1.join)(this.dataDir, 'desired.json'));
|
|
97
|
+
this.control = await (0, util_1.readJson)((0, node_path_1.join)(this.dataDir, 'control.json'));
|
|
98
|
+
this.jobs = new jobs_1.JobManager({
|
|
99
|
+
dataDir: this.dataDir,
|
|
100
|
+
iobrokerJs: (0, node_path_1.join)(utils.controllerDir, 'iobroker.js'),
|
|
101
|
+
cwd: (0, node_path_1.join)(utils.controllerDir, '..', '..'),
|
|
102
|
+
log: this.log,
|
|
103
|
+
verifySeconds: this.config.verifySeconds || 120,
|
|
104
|
+
startTimeoutSeconds: this.config.startTimeoutSeconds || 300,
|
|
105
|
+
report: job => this.reportJob(job),
|
|
106
|
+
installed: async () => (0, inventory_1.installedVersions)(await this.inventory()),
|
|
107
|
+
instancesOf: adapter => this.instancesOf(adapter),
|
|
108
|
+
errorsSince: (ts, adapters) => this.errorLog.filter(e => e.ts >= ts && adapters.some(a => e.from.startsWith(`${a}.`))),
|
|
109
|
+
onFinished: (job, result) => this.onJobFinished(job, result),
|
|
110
|
+
});
|
|
111
|
+
await this.jobs.load();
|
|
112
|
+
await this.jobs.setHalt(!!this.control?.halt);
|
|
113
|
+
// count error log lines of all instances (verification and heartbeat)
|
|
114
|
+
await this.requireLog?.(true);
|
|
115
|
+
await this.subscribeStatesAsync('control.*');
|
|
116
|
+
await this.updateInfoStates();
|
|
117
|
+
this.creds = await (0, util_1.readJson)((0, node_path_1.join)(this.dataDir, 'device.json'));
|
|
118
|
+
await this.updateAccessStates();
|
|
119
|
+
if (this.creds) {
|
|
120
|
+
this.startTransport();
|
|
121
|
+
// sessions that were open before a restart (e.g. after a change of the settings)
|
|
122
|
+
for (const t of (await (0, util_1.readJson)((0, node_path_1.join)(this.dataDir, 'tunnels.json'))) || []) {
|
|
123
|
+
await this.openTunnel(t, true);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
await this.tryEnroll();
|
|
128
|
+
}
|
|
129
|
+
this.timers.push(setInterval(() => void this.jobs?.poll(), 2000));
|
|
130
|
+
this.timers.push(setInterval(() => void this.heartbeat(), (this.config.heartbeatMinutes || 60) * 60_000));
|
|
131
|
+
this.timers.push(setInterval(() => {
|
|
132
|
+
void this.sendInventory(false);
|
|
133
|
+
void this.reconcile();
|
|
134
|
+
}, (this.config.checkMinutes || 5) * 60_000));
|
|
135
|
+
}
|
|
136
|
+
async onUnload(callback) {
|
|
137
|
+
this.unloading = true;
|
|
138
|
+
try {
|
|
139
|
+
this.timers.forEach(t => clearInterval(t));
|
|
140
|
+
if (this.enrollTimer) {
|
|
141
|
+
clearTimeout(this.enrollTimer);
|
|
142
|
+
}
|
|
143
|
+
// the sessions stay in tunnels.json and are resumed after the restart
|
|
144
|
+
for (const client of [...this.tunnels.values()]) {
|
|
145
|
+
client.stop('agent stops');
|
|
146
|
+
}
|
|
147
|
+
// js-controller kills slow adapters; the Last Will covers a disconnect that does not finish in time
|
|
148
|
+
await Promise.race([this.transport?.stop(), new Promise(resolve => setTimeout(resolve, 800))]);
|
|
149
|
+
await this.setState('info.connection', false, true);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
callback();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async savePersisted() {
|
|
156
|
+
await (0, util_1.writeJsonAtomic)((0, node_path_1.join)(this.dataDir, 'state.json'), this.persisted);
|
|
157
|
+
}
|
|
158
|
+
// ------------------------------------------------------------------ enrollment
|
|
159
|
+
async stopWithReason(reason) {
|
|
160
|
+
this.log.error(`Beekeeper rejected this device: ${reason}. The agent stops.`);
|
|
161
|
+
await this.setState('info.status', `Abgewiesen: ${reason}`, true);
|
|
162
|
+
await this.notify('rejected', `Die Flottenverwaltung hat dieses Gerät abgewiesen: ${reason}`);
|
|
163
|
+
// explicit rejection: stop instead of knocking again; js-controller does not restart us
|
|
164
|
+
this.terminate(reason, utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
|
|
165
|
+
}
|
|
166
|
+
async tryEnroll() {
|
|
167
|
+
if (!this.config.enrollUrl || !this.config.enrollToken) {
|
|
168
|
+
await this.setState('info.status', 'Nicht konfiguriert: Enrollment-URL und Token fehlen', true);
|
|
169
|
+
this.log.warn('Enrollment URL or token missing, please configure the adapter');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
await this.setState('info.status', 'Anmeldung beim Server …', true);
|
|
173
|
+
const result = await (0, enrollment_1.enroll)(this.config.enrollUrl, {
|
|
174
|
+
token: this.config.enrollToken,
|
|
175
|
+
uuid: this.uuid,
|
|
176
|
+
hostname: node_os_1.default.hostname(),
|
|
177
|
+
agentVersion: this.version,
|
|
178
|
+
comment: (0, protocol_1.normalizeComment)(this.config.comment),
|
|
179
|
+
});
|
|
180
|
+
if (result.kind === 'ok') {
|
|
181
|
+
this.creds = result.creds;
|
|
182
|
+
await (0, util_1.writeJsonAtomic)((0, node_path_1.join)(this.dataDir, 'device.json'), result.creds);
|
|
183
|
+
this.log.info(`Enrolled as ${result.creds.deviceId} (asset ${result.creds.assetId})`);
|
|
184
|
+
await this.updateInfoStates();
|
|
185
|
+
this.startTransport();
|
|
186
|
+
}
|
|
187
|
+
else if (result.kind === 'rejected') {
|
|
188
|
+
await this.stopWithReason(result.reason);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// network or server trouble is no reason to stop: retry with growing distance
|
|
192
|
+
const delay = this.enrollBackoff;
|
|
193
|
+
this.enrollBackoff = Math.min(this.enrollBackoff * 2, 24 * 3600_000);
|
|
194
|
+
this.log.warn(`Enrollment failed (${result.reason}), retry in ${Math.round(delay / 1000)} s`);
|
|
195
|
+
await this.setState('info.status', `Anmeldung fehlgeschlagen, neuer Versuch in ${Math.round(delay / 60000)} min`, true);
|
|
196
|
+
this.enrollTimer = setTimeout(() => {
|
|
197
|
+
this.enrollTimer = null;
|
|
198
|
+
void this.tryEnroll();
|
|
199
|
+
}, delay);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ------------------------------------------------------------------ transport
|
|
203
|
+
startTransport() {
|
|
204
|
+
const transport = new transport_1.Transport(this.creds, { keepalive: this.config.keepaliveSeconds || 300, log: this.log });
|
|
205
|
+
this.transport = transport;
|
|
206
|
+
transport.on('connected', () => void this.onConnected());
|
|
207
|
+
transport.on('disconnected', () => {
|
|
208
|
+
void this.setState('info.connection', false, true);
|
|
209
|
+
});
|
|
210
|
+
transport.on('desired', msg => void this.onDesired(msg));
|
|
211
|
+
transport.on('control', msg => void this.onControl(msg));
|
|
212
|
+
transport.on('job', msg => void this.onJob(msg));
|
|
213
|
+
transport.on('rejected', reason => void this.stopWithReason(reason));
|
|
214
|
+
transport.start();
|
|
215
|
+
}
|
|
216
|
+
async onConnected() {
|
|
217
|
+
this.log.info('Connected to beekeeper');
|
|
218
|
+
await this.setState('info.connection', true, true);
|
|
219
|
+
await this.setState('info.status', 'Verbunden', true);
|
|
220
|
+
await this.heartbeat();
|
|
221
|
+
await this.sendInventory(true);
|
|
222
|
+
// results produced while offline
|
|
223
|
+
if (this.jobs?.job) {
|
|
224
|
+
await this.reportJob(this.jobs.job);
|
|
225
|
+
}
|
|
226
|
+
else if (this.persisted.lastResult) {
|
|
227
|
+
await this.transport?.publish('result', this.persisted.lastResult);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async onDesired(msg) {
|
|
231
|
+
this.desired = msg;
|
|
232
|
+
await (0, util_1.writeJsonAtomic)((0, node_path_1.join)(this.dataDir, 'desired.json'), msg);
|
|
233
|
+
this.log.info(msg?.profile
|
|
234
|
+
? `Desired state: ${msg.profile.name} (${msg.mode}${msg.retry ? `, retry ${msg.retry}` : ''})`
|
|
235
|
+
: 'No desired state');
|
|
236
|
+
await this.updateInfoStates();
|
|
237
|
+
this.lastWaitReason = '';
|
|
238
|
+
await this.reconcile();
|
|
239
|
+
}
|
|
240
|
+
async onControl(msg) {
|
|
241
|
+
// log only real changes, not the retained state delivered on every connect
|
|
242
|
+
const changed = msg.halt !== !!this.control?.halt;
|
|
243
|
+
this.control = msg;
|
|
244
|
+
await (0, util_1.writeJsonAtomic)((0, node_path_1.join)(this.dataDir, 'control.json'), msg);
|
|
245
|
+
await this.jobs?.setHalt(msg.halt);
|
|
246
|
+
await this.setState('control.halted', msg.halt, true);
|
|
247
|
+
if (changed) {
|
|
248
|
+
this.log.warn(msg.halt ? `Emergency stop: ${msg.reason || 'no reason given'}` : 'Emergency stop released');
|
|
249
|
+
if (!msg.halt) {
|
|
250
|
+
await this.reconcile();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async onJob(msg) {
|
|
255
|
+
if (msg.cmd === 'resync' || msg.cmd === 'refresh') {
|
|
256
|
+
await this.sendInventory(true);
|
|
257
|
+
}
|
|
258
|
+
else if (msg.cmd === 'tunnel' && msg.tunnel) {
|
|
259
|
+
await this.openTunnel(msg.tunnel, false);
|
|
260
|
+
}
|
|
261
|
+
else if (msg.ref && (msg.cmd === 'instance' || msg.cmd === 'restart' || msg.cmd === 'osUpgrade')) {
|
|
262
|
+
await this.runCommand(msg);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ------------------------------------------------------------------ commands of the fleet management
|
|
266
|
+
async reply(ref, state, message) {
|
|
267
|
+
const msg = { v: protocol_1.PROTOCOL_VERSION, ts: Date.now(), ref, state, ...(message ? { message } : {}) };
|
|
268
|
+
await this.transport?.publish('reply', msg);
|
|
269
|
+
}
|
|
270
|
+
/** Sends the inventory again a while after a command, when the instances show its effect */
|
|
271
|
+
inventorySoon() {
|
|
272
|
+
for (const ms of [5_000, 30_000]) {
|
|
273
|
+
this.timers.push(setTimeout(() => void this.sendInventory(false), ms));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async runCommand(msg) {
|
|
277
|
+
const ref = msg.ref;
|
|
278
|
+
const by = msg.by || 'Flottenverwaltung';
|
|
279
|
+
if (this.reconciling || this.jobs?.isActive()) {
|
|
280
|
+
await this.reply(ref, 'error', 'Während eines Auftrags (Update) nicht möglich');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (this.commandRunning) {
|
|
284
|
+
await this.reply(ref, 'error', 'Es läuft schon ein Befehl');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.commandRunning = true;
|
|
288
|
+
try {
|
|
289
|
+
if (msg.cmd === 'instance') {
|
|
290
|
+
await this.instanceCommand(ref, by, msg.instance);
|
|
291
|
+
}
|
|
292
|
+
else if (msg.cmd === 'restart') {
|
|
293
|
+
this.log.warn(`Restart of ioBroker on request of ${by} (fleet management)`);
|
|
294
|
+
// the answer goes out first, the restart takes the agent down too
|
|
295
|
+
await this.reply(ref, 'done', 'ioBroker startet neu');
|
|
296
|
+
this.sendToHost(`system.host.${this.host}`, 'restartController', {});
|
|
297
|
+
}
|
|
298
|
+
else if (msg.cmd === 'osUpgrade') {
|
|
299
|
+
await this.osUpgradeCommand(ref, by, msg.osUpgrade);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
this.log.warn(`Command ${msg.cmd} of ${by} failed: ${e.message}`);
|
|
304
|
+
await this.reply(ref, 'error', e.message);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
this.commandRunning = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Start, stop or restart an instance on this host, the same way admin does it */
|
|
311
|
+
async instanceCommand(ref, by, req) {
|
|
312
|
+
const id = req?.id;
|
|
313
|
+
const action = req?.action;
|
|
314
|
+
const objId = `system.adapter.${id}`;
|
|
315
|
+
const obj = typeof id === 'string' && /^[\w.-]+$/.test(id) ? await this.getForeignObjectAsync(objId) : null;
|
|
316
|
+
const refusal = (0, commands_1.instanceRefusal)(id, action, this.namespace, obj, this.host || '');
|
|
317
|
+
if (refusal || !req) {
|
|
318
|
+
await this.reply(ref, 'error', refusal || 'Instanz fehlt');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.log.info(`Instance ${req.id}: ${req.action} on request of ${by} (fleet management)`);
|
|
322
|
+
const own = req.id === this.namespace;
|
|
323
|
+
if (own) {
|
|
324
|
+
// a restart of this instance ends this process: answer first
|
|
325
|
+
await this.reply(ref, 'done', `${req.id} startet neu`);
|
|
326
|
+
}
|
|
327
|
+
await this.extendForeignObjectAsync(objId, req.action === 'restart' ? {} : { common: { enabled: req.action === 'start' } });
|
|
328
|
+
if (!own) {
|
|
329
|
+
await this.reply(ref, 'done', `${req.id} ${commands_1.ACTION_DONE[req.action]}`);
|
|
330
|
+
this.inventorySoon();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** Upgrade OS packages through js-controller, only those it listed itself */
|
|
334
|
+
async osUpgradeCommand(ref, by, req) {
|
|
335
|
+
const host = `system.host.${this.host}`;
|
|
336
|
+
const hostObj = await this.getForeignObjectAsync(host);
|
|
337
|
+
if (!(0, commands_1.supportedCommands)(String(hostObj?.common?.installedVersion || '')).includes('osUpgrade')) {
|
|
338
|
+
await this.reply(ref, 'error', 'OS-Updates brauchen Linux und js-controller ab 6');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const listed = (0, commands_1.osUpdatesFrom)(await this.getForeignStateAsync(`${host}.osPackageUpdates`), false)?.packages ?? [];
|
|
342
|
+
const { packages, unknown } = (0, commands_1.chooseOsPackages)(listed, req?.packages);
|
|
343
|
+
if (!packages.length || unknown.length) {
|
|
344
|
+
await this.reply(ref, 'error', unknown.length ? `Nicht als aktualisierbar gelistet: ${unknown.join(', ')}` : 'Keine Pakete angegeben');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const names = packages.map(p => p.name).join(', ');
|
|
348
|
+
this.log.warn(`Upgrade of OS packages ${names} on request of ${by} (fleet management)`);
|
|
349
|
+
await this.reply(ref, 'running', `apt: ${names}`);
|
|
350
|
+
// js-controller runs apt (with sudo if it is not root) and answers when it is through
|
|
351
|
+
const answer = (await Promise.race([
|
|
352
|
+
this.sendToHostAsync(host, 'upgradeOsPackages', {
|
|
353
|
+
packages: packages.map(p => ({ name: p.name })),
|
|
354
|
+
restart: !!req?.restart,
|
|
355
|
+
}),
|
|
356
|
+
new Promise(resolve => {
|
|
357
|
+
this.timers.push(setTimeout(() => resolve({ error: 'Keine Antwort von js-controller nach 30 Minuten' }), 30 * 60_000));
|
|
358
|
+
}),
|
|
359
|
+
]));
|
|
360
|
+
if (answer?.success) {
|
|
361
|
+
this.log.info(`OS packages upgraded: ${names}`);
|
|
362
|
+
await this.reply(ref, 'done', `${packages.length} ${packages.length === 1 ? 'Paket' : 'Pakete'} aktualisiert${req?.restart ? ', ioBroker startet neu' : ''}`);
|
|
363
|
+
this.inventorySoon();
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const error = answer?.error || 'js-controller meldet einen Fehler';
|
|
367
|
+
this.log.warn(`OS upgrade failed: ${error}`);
|
|
368
|
+
await this.reply(ref, 'error', error);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// ------------------------------------------------------------------ remote access
|
|
372
|
+
/** Kinds of remote access the owner allows in the instance settings */
|
|
373
|
+
allowedAccess() {
|
|
374
|
+
const kinds = [];
|
|
375
|
+
if (this.config.allowAdmin !== false) {
|
|
376
|
+
kinds.push('admin');
|
|
377
|
+
}
|
|
378
|
+
if (this.config.allowVis !== false) {
|
|
379
|
+
kinds.push('vis');
|
|
380
|
+
}
|
|
381
|
+
if (this.config.allowConsole === true && (0, console_1.findScript)()) {
|
|
382
|
+
kinds.push('console');
|
|
383
|
+
}
|
|
384
|
+
return kinds;
|
|
385
|
+
}
|
|
386
|
+
/** One shell for a stream of a console session, at most MAX_SHELLS at the same time */
|
|
387
|
+
openShell(t, stream) {
|
|
388
|
+
const running = this.shells.get(t.id) ?? 0;
|
|
389
|
+
if (running >= console_1.MAX_SHELLS) {
|
|
390
|
+
stream.end(Buffer.concat([
|
|
391
|
+
(0, tunnel_1.consoleMessage)(tunnel_1.CONSOLE.DATA, Buffer.from(`Höchstens ${console_1.MAX_SHELLS} Shells je Sitzung.\r\n`)),
|
|
392
|
+
(0, tunnel_1.consoleMessage)(tunnel_1.CONSOLE.EXIT, Buffer.from('zu viele Shells')),
|
|
393
|
+
]));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this.shells.set(t.id, running + 1);
|
|
397
|
+
void this.updateAccessStates();
|
|
398
|
+
(0, console_1.startShell)(stream, {
|
|
399
|
+
cwd: (0, node_path_1.join)(utils.controllerDir, '..', '..'),
|
|
400
|
+
log: this.log,
|
|
401
|
+
by: t.by,
|
|
402
|
+
onEnd: () => {
|
|
403
|
+
const n = (this.shells.get(t.id) ?? 1) - 1;
|
|
404
|
+
if (n > 0) {
|
|
405
|
+
this.shells.set(t.id, n);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
this.shells.delete(t.id);
|
|
409
|
+
}
|
|
410
|
+
if (!this.unloading) {
|
|
411
|
+
void this.updateAccessStates();
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Opens the tunnel of an access session, or refuses it with a reason the operator sees.
|
|
418
|
+
*
|
|
419
|
+
* @param t request of the server
|
|
420
|
+
* @param resumed the session was open before a restart of the agent
|
|
421
|
+
*/
|
|
422
|
+
async openTunnel(t, resumed) {
|
|
423
|
+
if (this.tunnels.has(t.id) || t.expiresAt <= Date.now() || this.unloading) {
|
|
424
|
+
// repeated delivery of the job, or over
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!(0, remote_1.trustedTunnelUrl)(t.url, [this.config.enrollUrl, this.creds?.mqttUrl || ''])) {
|
|
428
|
+
this.log.warn(`Remote access: tunnel URL ${t.url} does not belong to the fleet server, ignored`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
let refusal = null;
|
|
432
|
+
let target = null;
|
|
433
|
+
const isConsole = t.kind === 'console';
|
|
434
|
+
if (!this.allowedAccess().includes(t.kind)) {
|
|
435
|
+
refusal = !isConsole
|
|
436
|
+
? `Fernzugriff auf ${remote_1.ACCESS_LABEL[t.kind]} ist am Gerät abgeschaltet`
|
|
437
|
+
: this.config.allowConsole === true
|
|
438
|
+
? 'Die Konsole braucht Linux mit script (util-linux)'
|
|
439
|
+
: 'Die Konsole ist am Gerät nicht freigegeben';
|
|
440
|
+
}
|
|
441
|
+
else if (isConsole) {
|
|
442
|
+
if (this.tunnels.size >= 4) {
|
|
443
|
+
refusal = 'Zu viele gleichzeitige Fernzugriffe auf dem Gerät';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// only an instance of this kind on this host, whatever the server asks for
|
|
448
|
+
target = (0, remote_1.chooseTarget)(await (0, remote_1.findTargets)(this, t.kind), t.instance);
|
|
449
|
+
if (!target) {
|
|
450
|
+
refusal = t.instance
|
|
451
|
+
? `Instanz ${t.instance} ist auf dem Gerät nicht aktiv`
|
|
452
|
+
: `Keine aktive ${t.kind === 'admin' ? 'admin' : 'web'}-Instanz auf dem Gerät`;
|
|
453
|
+
}
|
|
454
|
+
else if (this.tunnels.size >= 4) {
|
|
455
|
+
refusal = 'Zu viele gleichzeitige Fernzugriffe auf dem Gerät';
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const client = new remote_1.TunnelClient(t, {
|
|
459
|
+
target,
|
|
460
|
+
...(isConsole && !refusal ? { handler: (stream) => this.openShell(t, stream) } : {}),
|
|
461
|
+
refusal,
|
|
462
|
+
ca: (0, transport_1.trustedCas)(this.creds?.ca || ''),
|
|
463
|
+
log: this.log,
|
|
464
|
+
onEnd: (c, reason) => {
|
|
465
|
+
if (this.tunnels.get(t.id) === c) {
|
|
466
|
+
this.tunnels.delete(t.id);
|
|
467
|
+
this.shells.delete(t.id);
|
|
468
|
+
this.log.info(`Remote access to ${remote_1.ACCESS_LABEL[t.kind]} by ${t.by} ended (${reason})`);
|
|
469
|
+
}
|
|
470
|
+
if (!this.unloading) {
|
|
471
|
+
void this.saveTunnels();
|
|
472
|
+
void this.updateAccessStates();
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
if (refusal) {
|
|
477
|
+
this.log.warn(`Remote access to ${remote_1.ACCESS_LABEL[t.kind]} by ${t.by} refused: ${refusal}`);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
this.tunnels.set(t.id, client);
|
|
481
|
+
this.log.info(`Remote access to ${remote_1.ACCESS_LABEL[t.kind]}${target ? ` (${target.instance})` : ''} ${resumed ? 'resumed' : 'opened'} by ${t.by} until ${new Date(t.expiresAt).toLocaleTimeString()}: ${t.purpose}`);
|
|
482
|
+
}
|
|
483
|
+
client.start();
|
|
484
|
+
await this.saveTunnels();
|
|
485
|
+
await this.updateAccessStates();
|
|
486
|
+
}
|
|
487
|
+
async saveTunnels() {
|
|
488
|
+
await (0, util_1.writeJsonAtomic)((0, node_path_1.join)(this.dataDir, 'tunnels.json'), [...this.tunnels.values()].map(c => c.req));
|
|
489
|
+
}
|
|
490
|
+
/** " (2 Shells offen)" for a console */
|
|
491
|
+
shellText(t) {
|
|
492
|
+
if (t.kind !== 'console') {
|
|
493
|
+
return '';
|
|
494
|
+
}
|
|
495
|
+
const n = this.shells.get(t.id) ?? 0;
|
|
496
|
+
return n ? ` (${n} ${n === 1 ? 'Shell' : 'Shells'} offen)` : '';
|
|
497
|
+
}
|
|
498
|
+
/** Shows the owner who is connected right now, and why */
|
|
499
|
+
async updateAccessStates() {
|
|
500
|
+
const active = [...this.tunnels.values()].map(c => c.req);
|
|
501
|
+
await this.setState('info.remoteAccess', active.length > 0, true);
|
|
502
|
+
await this.setState('info.remoteAccessBy', active
|
|
503
|
+
.map(t => `${remote_1.ACCESS_LABEL[t.kind]}${t.instance ? ` (${t.instance})` : ''}${this.shellText(t)}: ${t.by}, ${t.purpose} (bis ${new Date(t.expiresAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })})`)
|
|
504
|
+
.join('; '), true);
|
|
505
|
+
}
|
|
506
|
+
// ------------------------------------------------------------------ inventory and heartbeat
|
|
507
|
+
async inventory() {
|
|
508
|
+
const inv = await (0, inventory_1.collectInventory)(this, {
|
|
509
|
+
agentVersion: this.version || '',
|
|
510
|
+
dataDir: this.dataDir,
|
|
511
|
+
lastBackup: this.persisted.lastBackup,
|
|
512
|
+
});
|
|
513
|
+
inv.uuid = this.uuid;
|
|
514
|
+
const comment = (0, protocol_1.normalizeComment)(this.config.comment);
|
|
515
|
+
if (comment) {
|
|
516
|
+
inv.comment = comment;
|
|
517
|
+
}
|
|
518
|
+
// also tells the server that this agent can open a tunnel at all
|
|
519
|
+
inv.remoteAccess = this.allowedAccess();
|
|
520
|
+
inv.accessTargets = {
|
|
521
|
+
admin: (0, remote_1.describeTargets)(await (0, remote_1.findTargets)(this, 'admin')),
|
|
522
|
+
vis: (0, remote_1.describeTargets)(await (0, remote_1.findTargets)(this, 'vis')),
|
|
523
|
+
};
|
|
524
|
+
if (inv.remoteAccess.includes('console')) {
|
|
525
|
+
inv.consoleUser = (0, console_1.consoleUser)();
|
|
526
|
+
}
|
|
527
|
+
inv.commands = (0, commands_1.supportedCommands)(inv.software.jsController);
|
|
528
|
+
if (inv.commands.includes('osUpgrade')) {
|
|
529
|
+
const updates = (0, commands_1.osUpdatesFrom)(await this.getForeignStateAsync(`system.host.${this.host}.osPackageUpdates`), (0, node_fs_1.existsSync)(commands_1.REBOOT_REQUIRED));
|
|
530
|
+
if (updates) {
|
|
531
|
+
inv.osUpdates = updates;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return inv;
|
|
535
|
+
}
|
|
536
|
+
async sendInventory(full) {
|
|
537
|
+
if (!this.transport?.connected) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const inv = await this.inventory();
|
|
541
|
+
const flat = (0, util_1.flatten)(inv);
|
|
542
|
+
if (full || !this.lastFlat) {
|
|
543
|
+
this.seq++;
|
|
544
|
+
await this.transport.publish('inventory', {
|
|
545
|
+
v: protocol_1.PROTOCOL_VERSION,
|
|
546
|
+
ts: Date.now(),
|
|
547
|
+
seq: this.seq,
|
|
548
|
+
type: 'full',
|
|
549
|
+
data: inv,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
const { set, unset } = (0, util_1.diffFlat)(this.lastFlat, flat);
|
|
554
|
+
if (!Object.keys(set).length && !unset.length) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
this.seq++;
|
|
558
|
+
await this.transport.publish('inventory', {
|
|
559
|
+
v: protocol_1.PROTOCOL_VERSION,
|
|
560
|
+
ts: Date.now(),
|
|
561
|
+
seq: this.seq,
|
|
562
|
+
type: 'diff',
|
|
563
|
+
baseSeq: this.seq - 1,
|
|
564
|
+
set,
|
|
565
|
+
unset,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
this.lastFlat = flat;
|
|
569
|
+
}
|
|
570
|
+
async heartbeat() {
|
|
571
|
+
const disk = await (0, inventory_1.diskInfo)(this.dataDir);
|
|
572
|
+
let signal = null;
|
|
573
|
+
if (this.config.signalStateId) {
|
|
574
|
+
const s = await this.getForeignStateAsync(this.config.signalStateId);
|
|
575
|
+
signal = typeof s?.val === 'number' ? s.val : null;
|
|
576
|
+
}
|
|
577
|
+
const msg = {
|
|
578
|
+
v: protocol_1.PROTOCOL_VERSION,
|
|
579
|
+
ts: Date.now(),
|
|
580
|
+
online: true,
|
|
581
|
+
agentVersion: this.version,
|
|
582
|
+
uptime: Math.round(node_os_1.default.uptime()),
|
|
583
|
+
load: Math.round(node_os_1.default.loadavg()[0] * 100) / 100,
|
|
584
|
+
memFree: node_os_1.default.freemem(),
|
|
585
|
+
diskFree: disk?.free,
|
|
586
|
+
temperature: (await (0, inventory_1.cpuTemperature)()) ?? undefined,
|
|
587
|
+
logErrors: this.errorsSinceHeartbeat,
|
|
588
|
+
jobState: this.jobs?.job?.state,
|
|
589
|
+
signal,
|
|
590
|
+
sdWrites: await (0, inventory_1.sdWrites)(),
|
|
591
|
+
};
|
|
592
|
+
this.errorsSinceHeartbeat = 0;
|
|
593
|
+
await this.transport?.publish('state', msg);
|
|
594
|
+
}
|
|
595
|
+
onLog(msg) {
|
|
596
|
+
if (msg.severity !== 'error' || msg.from?.startsWith(`${this.namespace}`)) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
this.errorsSinceHeartbeat++;
|
|
600
|
+
this.errorLog.push({ ts: Date.now(), from: msg.from, message: String(msg.message) });
|
|
601
|
+
if (this.errorLog.length > 500) {
|
|
602
|
+
this.errorLog.splice(0, this.errorLog.length - 500);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async instancesOf(adapter) {
|
|
606
|
+
const res = await this.getObjectViewAsync('system', 'instance', {
|
|
607
|
+
startkey: `system.adapter.${adapter}.`,
|
|
608
|
+
endkey: `system.adapter.${adapter}.香`,
|
|
609
|
+
});
|
|
610
|
+
const list = [];
|
|
611
|
+
for (const row of res.rows) {
|
|
612
|
+
const obj = row.value;
|
|
613
|
+
if (!obj?.common.enabled || obj.common.host !== this.host) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
// scheduled instances are not running permanently
|
|
617
|
+
if (obj.common.mode !== 'daemon') {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const alive = await this.getForeignStateAsync(`${obj._id}.alive`);
|
|
621
|
+
list.push({ id: obj._id.replace('system.adapter.', ''), alive: !!alive?.val });
|
|
622
|
+
}
|
|
623
|
+
return list;
|
|
624
|
+
}
|
|
625
|
+
// ------------------------------------------------------------------ reconciliation
|
|
626
|
+
async report(state, detail, steps) {
|
|
627
|
+
const d = this.desired;
|
|
628
|
+
if (!d?.profile) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const msg = {
|
|
632
|
+
v: protocol_1.PROTOCOL_VERSION,
|
|
633
|
+
ts: Date.now(),
|
|
634
|
+
profileId: d.profile.id,
|
|
635
|
+
profileHash: d.profile.hash,
|
|
636
|
+
retry: d.retry,
|
|
637
|
+
state,
|
|
638
|
+
detail,
|
|
639
|
+
steps,
|
|
640
|
+
};
|
|
641
|
+
this.persisted.lastResult = msg;
|
|
642
|
+
await this.savePersisted();
|
|
643
|
+
await this.setState('job.state', state, true);
|
|
644
|
+
await this.setState('job.detail', detail, true);
|
|
645
|
+
await this.transport?.publish('result', msg);
|
|
646
|
+
}
|
|
647
|
+
async reportJob(job) {
|
|
648
|
+
const msg = {
|
|
649
|
+
v: protocol_1.PROTOCOL_VERSION,
|
|
650
|
+
ts: Date.now(),
|
|
651
|
+
profileId: job.profileId,
|
|
652
|
+
profileHash: job.profileHash,
|
|
653
|
+
retry: job.retry,
|
|
654
|
+
state: job.state,
|
|
655
|
+
detail: job.detail,
|
|
656
|
+
steps: job.steps,
|
|
657
|
+
};
|
|
658
|
+
this.persisted.lastResult = msg;
|
|
659
|
+
await this.savePersisted();
|
|
660
|
+
await this.setState('job.state', job.state, true);
|
|
661
|
+
await this.setState('job.detail', job.detail, true);
|
|
662
|
+
await this.setState('job.steps', JSON.stringify(job.steps), true);
|
|
663
|
+
await this.transport?.publish('result', msg);
|
|
664
|
+
}
|
|
665
|
+
async onJobFinished(job, result) {
|
|
666
|
+
if (result !== 'waiting') {
|
|
667
|
+
this.persisted.results[job.key] = result;
|
|
668
|
+
}
|
|
669
|
+
const backup = job.steps.find(s => s.action === 'backup' && s.status === 'ok');
|
|
670
|
+
if (backup?.detail) {
|
|
671
|
+
this.persisted.lastBackup = { ts: backup.ts || Date.now(), file: backup.detail, size: 0 };
|
|
672
|
+
}
|
|
673
|
+
await this.savePersisted();
|
|
674
|
+
this.log.info(`Job ${job.profileName}: ${result} (${job.detail})`);
|
|
675
|
+
if (result === 'done') {
|
|
676
|
+
await this.notify('updated', `Das System wurde auf den Stand ${job.profileName} gebracht.`);
|
|
677
|
+
}
|
|
678
|
+
await this.sendInventory(false);
|
|
679
|
+
await this.updateInfoStates();
|
|
680
|
+
// the desired state may have changed while the job was running
|
|
681
|
+
setTimeout(() => void this.reconcile(), 1000);
|
|
682
|
+
}
|
|
683
|
+
/** Converges on the desired state if possible. Never runs two jobs at once. */
|
|
684
|
+
async reconcile() {
|
|
685
|
+
if (this.reconciling || this.unloading || !this.jobs || this.jobs.isActive()) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
this.reconciling = true;
|
|
689
|
+
try {
|
|
690
|
+
await this.reconcileInner();
|
|
691
|
+
}
|
|
692
|
+
catch (e) {
|
|
693
|
+
this.log.error(`Reconcile: ${e.stack || e.message}`);
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
696
|
+
this.reconciling = false;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async reconcileInner() {
|
|
700
|
+
const d = this.desired;
|
|
701
|
+
if (!d?.profile) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const inv = await this.inventory();
|
|
705
|
+
const installed = (0, inventory_1.installedVersions)(inv);
|
|
706
|
+
const key = `${d.profile.hash}:${d.retry}`;
|
|
707
|
+
// a package with the version number of what is installed: once per job, never in a loop
|
|
708
|
+
const from = this.persisted.results[key] === 'done'
|
|
709
|
+
? undefined
|
|
710
|
+
: Object.fromEntries(Object.entries(inv.software.adapters).map(([n, a]) => [n, a.installedFrom]));
|
|
711
|
+
const plan = (0, planner_1.computePlan)(d, installed, this.name, from);
|
|
712
|
+
await this.setState('info.onTarget', plan.length === 0, true);
|
|
713
|
+
if (d.mode === 'self') {
|
|
714
|
+
// self-managed: no automatic rollout, only a hint about the new state
|
|
715
|
+
if (plan.length && !this.persisted.notified[`update:${d.profile.hash}`]) {
|
|
716
|
+
this.persisted.notified[`update:${d.profile.hash}`] = Date.now();
|
|
717
|
+
await this.savePersisted();
|
|
718
|
+
await this.notify('updateAvailable', `Neuer Softwarestand ${d.profile.name} verfügbar (${plan.length} Änderungen). Die Anlage ist eigenverantwortlich und wird nicht automatisch aktualisiert.`);
|
|
719
|
+
}
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const foreign = (0, planner_1.foreignAdapters)(d, installed);
|
|
723
|
+
if (foreign.length) {
|
|
724
|
+
// own installations: do not touch anything; the server switches the mode and informs the dealer
|
|
725
|
+
const key = `foreign:${foreign.join(',')}`;
|
|
726
|
+
if (!this.persisted.notified[key]) {
|
|
727
|
+
this.persisted.notified[key] = Date.now();
|
|
728
|
+
await this.savePersisted();
|
|
729
|
+
await this.notify('modeChanged', `Selbst installierte Adapter erkannt (${foreign.join(', ')}). Die Anlage erhält keine automatischen Updates mehr, bis sie in den verwalteten Modus zurückgeführt wird.`);
|
|
730
|
+
}
|
|
731
|
+
await this.setState('info.status', `Eigeninstallation erkannt: ${foreign.join(', ')}`, true);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (this.persisted.results[key] === 'blocked') {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (!plan.length) {
|
|
738
|
+
if (this.persisted.results[key] !== 'done') {
|
|
739
|
+
this.persisted.results[key] = 'done';
|
|
740
|
+
await this.report('done', 'Bereits auf Sollstand');
|
|
741
|
+
}
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
// drift after an earlier success (e.g. manual change): converge again
|
|
745
|
+
delete this.persisted.results[key];
|
|
746
|
+
const pf = await (0, preflight_1.preflight)({
|
|
747
|
+
now: new Date(),
|
|
748
|
+
halted: !!this.control?.halt,
|
|
749
|
+
haltReason: this.control?.reason,
|
|
750
|
+
window: {
|
|
751
|
+
start: this.config.windowStart || '02:00',
|
|
752
|
+
end: this.config.windowEnd || '05:00',
|
|
753
|
+
ignore: !!this.config.ignoreWindow,
|
|
754
|
+
},
|
|
755
|
+
conditions: (this.config.conditions || []).filter(c => c?.stateId),
|
|
756
|
+
readState: async (id) => (await this.getForeignStateAsync(id))?.val ?? undefined,
|
|
757
|
+
diskPath: this.dataDir,
|
|
758
|
+
minFreeMb: this.config.minFreeMb || 1024,
|
|
759
|
+
dependencyErrors: (0, planner_1.checkDependencies)(d, installed, {
|
|
760
|
+
jsController: inv.software.jsController,
|
|
761
|
+
node: process.version,
|
|
762
|
+
}),
|
|
763
|
+
});
|
|
764
|
+
if (!pf.ok) {
|
|
765
|
+
if (pf.kind === 'block') {
|
|
766
|
+
this.persisted.results[key] = 'blocked';
|
|
767
|
+
await this.report('blocked', pf.reason, plan);
|
|
768
|
+
}
|
|
769
|
+
else if (pf.reason !== this.lastWaitReason) {
|
|
770
|
+
this.lastWaitReason = pf.reason;
|
|
771
|
+
await this.report('waiting', pf.reason, plan);
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// packages of the server for single assets: loaded before anything changes
|
|
776
|
+
const files = {};
|
|
777
|
+
for (const [adapter, tarball] of Object.entries((0, packages_1.packagesOf)(d, plan))) {
|
|
778
|
+
try {
|
|
779
|
+
files[adapter] = await (0, packages_1.ensurePackage)((0, node_path_1.join)(this.dataDir, 'packages'), tarball, {
|
|
780
|
+
serverUrls: [this.config.enrollUrl, this.creds?.mqttUrl || ''],
|
|
781
|
+
ca: (0, transport_1.trustedCas)(this.creds?.ca || ''),
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
catch (e) {
|
|
785
|
+
const reason = `Paket für ${adapter} nicht ladbar: ${e.message}`;
|
|
786
|
+
this.log.warn(reason);
|
|
787
|
+
if (reason !== this.lastWaitReason) {
|
|
788
|
+
this.lastWaitReason = reason;
|
|
789
|
+
await this.report('waiting', reason, plan);
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
this.lastWaitReason = '';
|
|
795
|
+
this.log.info(`Starting job for ${d.profile.name}: ${plan.map(s => `${s.action} ${s.adapter}`).join(', ')}`);
|
|
796
|
+
await this.report('preflight', `${plan.length} Schritte geplant, Vorprüfung bestanden`, plan);
|
|
797
|
+
await this.jobs.start(d, plan, this.config.backupBeforeUpdate !== false, files);
|
|
798
|
+
}
|
|
799
|
+
// ------------------------------------------------------------------ states, notifications
|
|
800
|
+
async updateInfoStates() {
|
|
801
|
+
await this.setState('info.deviceId', this.creds?.deviceId || '', true);
|
|
802
|
+
await this.setState('info.assetId', this.creds?.assetId || '', true);
|
|
803
|
+
await this.setState('info.mode', this.desired?.mode || '', true);
|
|
804
|
+
await this.setState('info.profile', this.desired?.profile?.name || '', true);
|
|
805
|
+
}
|
|
806
|
+
async notify(category, message) {
|
|
807
|
+
try {
|
|
808
|
+
await this.registerNotification('bee', category, message);
|
|
809
|
+
}
|
|
810
|
+
catch (e) {
|
|
811
|
+
this.log.debug(`Notification not possible: ${e.message}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async onStateChange(id, state) {
|
|
815
|
+
if (!state || state.ack) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (id === `${this.namespace}.control.refresh`) {
|
|
819
|
+
await this.sendInventory(true);
|
|
820
|
+
await this.setState('control.refresh', false, true);
|
|
821
|
+
}
|
|
822
|
+
else if (id === `${this.namespace}.control.check`) {
|
|
823
|
+
this.lastWaitReason = '';
|
|
824
|
+
await this.reconcile();
|
|
825
|
+
await this.setState('control.check', false, true);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (require.main !== module) {
|
|
830
|
+
module.exports = (options) => new BeeAdapter(options);
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
(() => new BeeAdapter())();
|
|
834
|
+
}
|
|
835
|
+
//# sourceMappingURL=main.js.map
|