UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

870 lines (869 loc) 39.7 kB
"use strict"; /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-this-alias */ /* * Copyright 2014-2015 Fabian Tollenaar <fabian@starting-point.nl> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); require("./networkConfig"); require("./baconjs-compat"); const server_api_1 = require("@signalk/server-api"); const crypto_1 = require("crypto"); const express_1 = __importDefault(require("express")); const http_1 = __importDefault(require("http")); const https_1 = __importDefault(require("https")); const lodash_1 = __importDefault(require("lodash")); const path_1 = __importDefault(require("path")); const api_1 = require("./api"); const config_1 = require("./config/config"); const debug_1 = require("./debug"); const deltacache_1 = __importStar(require("./deltacache")); const deltachain_1 = __importDefault(require("./deltachain")); const deltaPriority_1 = require("./deltaPriority"); const staticDataFilter_1 = require("./staticDataFilter"); const deltastats_1 = require("./deltastats"); const modules_1 = require("./modules"); const ports_1 = require("./ports"); const security_js_1 = require("./security.js"); const cors_1 = require("./cors"); const subscriptionmanager_1 = __importDefault(require("./subscriptionmanager")); const nmea0183TalkerGroups_1 = require("./nmea0183TalkerGroups"); const pipedproviders_1 = require("./pipedproviders"); const events_1 = require("./events"); const zones_1 = require("./zones"); const version_1 = __importDefault(require("./version")); const helmet_1 = __importDefault(require("helmet")); const debug = (0, debug_1.createDebug)('signalk-server'); const sourceref_migration_1 = require("./sourceref-migration"); const streambundle_1 = require("./streambundle"); /** * Shallow-clones a delta so that an `unfilteredDelta` consumer mutating a * value or meta entry cannot corrupt the delta still travelling through * the main pipeline. The top-level delta and the `updates`/`values`/`meta` * arrays are copied; each value/meta entry is shallow-spread. * * **Contract:** consumers must not mutate nested objects within a `value` * (e.g. `value.position.latitude = …`). Deep cloning would isolate those * too but is too expensive on the per-delta hot path; if a consumer needs * to mutate nested fields it must `structuredClone` the value itself. */ function cloneDelta(delta) { return { ...delta, updates: delta.updates?.map((update) => ({ ...update, values: update.values ? update.values.map((v) => ({ ...v })) : update.values, meta: update.meta ? update.meta.map((m) => ({ ...m })) : update.meta })) }; } class Server { app; // Pending sourceRef migration timers; cleared on stop() so a deferred // migration scheduled before a restart cannot fire on a torn-down app. pendingSourceRefMigrations; constructor(opts) { (0, version_1.default)(); const FILEUPLOADSIZELIMIT = process.env.FILEUPLOADSIZELIMIT || '10mb'; const bodyParser = require('body-parser'); const app = (0, express_1.default)(); app.use(require('compression')()); app.use((0, helmet_1.default)({ // ENABLED (safe, no compatibility impact): xContentTypeOptions: true, xDnsPrefetchControl: true, xDownloadOptions: true, xPermittedCrossDomainPolicies: true, referrerPolicy: true, hsts: true, // DISABLED (would break chart plotters, plugins, webapps): frameguard: false, // Allow embedding in iframes (chart plotters, MFDs) contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, crossOriginOpenerPolicy: false, crossOriginResourcePolicy: false })); app.use(bodyParser.json({ limit: FILEUPLOADSIZELIMIT })); app.use(bodyParser.urlencoded({ extended: true })); this.app = app; app.started = false; lodash_1.default.merge(app, opts); (0, config_1.load)(app); // Apply trust proxy setting if configured if (app.config.settings.trustProxy !== undefined) { app.set('trust proxy', app.config.settings.trustProxy); } app.logging = require('./logging')(app); app.version = '0.0.1'; (0, cors_1.setupCors)(app, (0, security_js_1.getSecurityConfig)(app)); (0, security_js_1.startSecurity)(app, opts ? opts.securityConfig : null); require('./serverroutes')(app, security_js_1.saveSecurityConfig, security_js_1.getSecurityConfig); require('./put').start(app); app.signalk = new server_api_1.FullSignalK(app.selfId, app.selfType); const maxListeners = process.env.EVENT_EMITTER_MAX_LISTENERS ? parseInt(process.env.EVENT_EMITTER_MAX_LISTENERS, 10) : 50; if (maxListeners > 0) { app.signalk.setMaxListeners(maxListeners); } app.propertyValues = new server_api_1.PropertyValues(); const deltachainV1 = new deltachain_1.default(); const deltachainV2 = new deltachain_1.default(); app.registerDeltaInputHandler = (handler) => { const unRegisterHandlers = [ deltachainV1.register(handler), deltachainV2.register(handler) ]; return () => unRegisterHandlers.forEach((f) => f()); }; app.providerStatus = {}; // feature detection app.getFeatures = async (enabled) => { return { apis: enabled === false ? [] : app.apis, plugins: await app.getPluginsList(enabled) }; }; // create first temporary pluginManager to get typechecks, as // app is any and not typechecked // TODO separate app.plugins and app.pluginsMap from app const pluginManager = { setPluginOpenApi: (pluginId, openApi) => { app.pluginsMap[pluginId].openApi = openApi; }, getPluginOpenApi: (pluginId) => ({ name: `plugins/${pluginId}`, path: `/plugins/${pluginId}`, apiDoc: app.pluginsMap[pluginId].openApi }), getPluginOpenApiRecords: () => Object.keys(app.pluginsMap).reduce((acc, pluginId) => { if (app.pluginsMap[pluginId].openApi) { acc.push({ name: `plugins/${pluginId}`, path: `/plugins/${pluginId}`, apiDoc: app.pluginsMap[pluginId].openApi }); } return acc; }, []) }; Object.assign(app, pluginManager); app.setPluginStatus = (providerId, statusMessage) => { doSetProviderStatus(providerId, statusMessage, 'status', 'plugin'); }; app.setPluginError = (providerId, errorMessage) => { doSetProviderStatus(providerId, errorMessage, 'error', 'plugin'); }; app.setProviderStatus = (providerId, statusMessage) => { doSetProviderStatus(providerId, statusMessage, 'status'); }; app.setProviderError = (providerId, errorMessage) => { doSetProviderStatus(providerId, errorMessage, 'error'); }; function doSetProviderStatus(providerId, statusMessage, type, statusType = 'provider') { if (!statusMessage) { delete app.providerStatus[providerId]; return; } if (lodash_1.default.isUndefined(app.providerStatus[providerId])) { app.providerStatus[providerId] = {}; } const status = app.providerStatus[providerId]; if (status.type === 'error' && status.message !== statusMessage) { status.lastError = status.message; status.lastErrorTimeStamp = status.timeStamp; } status.type = type; status.id = providerId; status.statusType = statusType; status.timeStamp = new Date().toISOString(); status.message = statusMessage; app.emit('serverevent', { type: 'PROVIDERSTATUS', from: 'signalk-server', data: app.getProviderStatus() }); } app.getProviderStatus = () => { const providerStatus = lodash_1.default.values(app.providerStatus); if (app.plugins) { app.plugins.forEach((plugin) => { try { if (typeof plugin.statusMessage === 'function' && lodash_1.default.isUndefined(app.providerStatus[plugin.id])) { let message = plugin.statusMessage(); if (message) { message = message.trim(); if (message.length > 0) { providerStatus.push({ message, type: 'status', id: plugin.id, statusType: 'plugin' }); } } } } catch (e) { console.error(e); providerStatus.push({ message: 'Error fetching provider status, see server log for details', type: 'status', id: plugin.id }); } }); } return providerStatus; }; app.registerHistoryProvider = (provider) => { app.historyProvider = provider; }; app.unregisterHistoryProvider = () => { delete app.historyProvider; }; // Initial passthrough — replaced by activateSourcePriorities() once // the engine is built. Keeps the type-shape compatible with the // routesPath property toPreferredDelta carries. const initialPassthrough = ((delta) => delta); initialPassthrough.routesPath = () => false; let toPreferredDelta = initialPassthrough; // Terminal of the delta input handler chain: once every handler has // run on the raw delta, cache it, fan it out unfiltered, apply // source-priority filtering and dispatch the result. Defined once // (not per delta) and reads `toPreferredDelta` through the closure so // it picks up engine rebuilds. now / version are passed in by the // chain so this stays a single hoisted function on the hot path. const dispatchDelta = (handled, now, version) => { if (app.deltaCache) { app.deltaCache.ingestDelta(handled); } app.signalk.emit('unfilteredDelta', cloneDelta(handled)); const preferred = toPreferredDelta(handled, now, app.selfContext); if (version === server_api_1.SKVersion.v1) { app.signalk.addDelta(preferred); } else { app.signalk.emit('delta', preferred); } }; // Translate `<label>.<numeric>` deltas to their `<label>.<canName>` // form so a saved canName-form ranking matches incoming refs from // providers that have useCanName off. The cache is keyed by the // sources tree's last-mutation marker (Object reference comparison // would suffice if signalk-schema replaced the object on every // change, but it mutates in place, so we rebuild on each call and // rely on Map construction being cheap for typical fleets). let cachedCanonicalMap = null; let cachedCanonicalSnapshot = null; const canonicaliseSourceRef = (sourceRef) => { const sources = app.signalk?.sources; if (sources !== cachedCanonicalSnapshot || cachedCanonicalMap === null) { cachedCanonicalSnapshot = sources; cachedCanonicalMap = (0, deltacache_1.buildSrcToCanonicalMap)(sources); } return cachedCanonicalMap.get(sourceRef) ?? sourceRef; }; // Refresh the canonical cache whenever a new device is observed or // a numeric-keyed source is replaced by a canName-keyed one. This // covers the cold-boot window where an early delta arrives before // the address claim and the engine briefly treats it as unknown. app.on('sourceRefChanged', () => { cachedCanonicalSnapshot = null; }); app.activateSourcePriorities = () => { try { cachedCanonicalSnapshot = null; const s = app.config.settings; // Seed the engine's per-path publisher tracking from the // already-loaded delta cache. After a server restart, the // cache holds every source the disk-persisted snapshot knows // about, but the engine's in-memory tracking is empty — // routesPath would return false for every path until two // distinct publishers' first live deltas arrive, and the // admin UI would briefly flash "no priority configured" // warnings on every multi-publisher path. Seeding closes // that window without persisting the engine's tracking // state separately on disk. const seenPublishersByPath = app.deltaCache?.getSelfPathPublishers ? app.deltaCache.getSelfPathPublishers() : undefined; toPreferredDelta = (0, deltaPriority_1.getToPreferredDelta)({ groups: s.priorityGroups ?? [], overrides: s.priorityOverrides ?? {}, fallbackMs: s.priorityDefaults?.fallbackMs, canonicalise: canonicaliseSourceRef, seenPublishersByPath }); // Inject the engine's routes-this-path predicate into the // delta cache so onValue can avoid recording a "preferred" // winner for pass-through paths (no override, source not in // any active group, dormant override). Otherwise the admin // UI's Priority-filtered view would suppress every other // source on a path the engine isn't actually filtering. // setRoutesPathPredicate handles the bootstrap-snapshot // cleanup: it walks preferredSources and drops every entry // the new predicate no longer routes. Replaces the older // resetPreferredSourcesNotIn call which only knew about // overrides + group membership and not about path-claim // state inside the engine closure. app.deltaCache?.setRoutesPathPredicate?.(toPreferredDelta.routesPath); } catch (e) { console.error('getToPreferredDelta failed:', e); } }; app.activateSourcePriorities(); // Build a per-subscription priority engine that runs the user's // ranking with a sourceRef exclusion mask. Used by // SubscribeMessage.excludeSources / excludeSelf: a derived-data // plugin that publishes on the same path it consumes can still // receive the priority cascade across the upstream sources // without seeing its own output in the candidate set. // // Reads groups / overrides / fallbackMs from the live settings // each time so a per-subscription engine reflects whatever the // user has currently saved, and shares the canonicaliseSourceRef // closure so canName matching behaves the same as the global // engine. Seeding seenPublishersByPath is not needed — the // resulting engine is consumed by a single subscriber callback // and never feeds routesPath consumers. app.buildSubscriptionEngine = (excludeSourceRefs) => { const excludeIdentities = new Set(); for (const ref of excludeSourceRefs) { if (typeof ref === 'string' && ref.length > 0) { excludeIdentities.add((0, deltaPriority_1.sourceRefIdentity)(canonicaliseSourceRef(ref))); } } const s = app.config.settings; return (0, deltaPriority_1.getToPreferredDelta)({ groups: s.priorityGroups ?? [], overrides: s.priorityOverrides ?? {}, fallbackMs: s.priorityDefaults?.fallbackMs, canonicalise: canonicaliseSourceRef, excludeIdentities }); }; // Defer migration so that the moved device's own re-arbitration // address claim has time to land in app.signalk.sources first. // Without this delay, every legitimate address takeover would be // misclassified as a reclaim by the takeover guard, since the // arbitration loser only re-claims a fraction of a second later. // 10s is well within an admin-attention window and well past the // worst-case bus settling time observed on busy fleets. const SOURCE_REF_MIGRATION_DELAY_MS = 10_000; const pendingSourceRefMigrations = new Set(); this.pendingSourceRefMigrations = pendingSourceRefMigrations; app.on('sourceRefChanged', ({ oldRef, newRef }) => { const handle = setTimeout(() => { pendingSourceRefMigrations.delete(handle); (0, sourceref_migration_1.migrateSourceRef)(app, oldRef, newRef); }, SOURCE_REF_MIGRATION_DELAY_MS); pendingSourceRefMigrations.add(handle); }); let providerTalkerLookups = (0, nmea0183TalkerGroups_1.buildProviderTalkerLookups)(app.config.settings.pipedProviders); app.on('pipedProvidersStarted', () => { providerTalkerLookups = (0, nmea0183TalkerGroups_1.buildProviderTalkerLookups)(app.config.settings.pipedProviders); }); app.handleMessage = (providerId, data, skVersion = server_api_1.SKVersion.v1) => { if (data && Array.isArray(data.updates)) { (0, deltastats_1.incDeltaStatistics)(app, providerId); if (typeof data.context === 'undefined' || data.context === 'vessels.self') { data.context = ('vessels.' + app.selfId); } const now = new Date(); data.updates = data.updates .map((update) => { if (!isValidUpdate(update)) { console.warn(`Discarding update from ${providerId}: invalid values entry (null or missing path)`); return undefined; } if (typeof update.source !== 'undefined') { // Respect an existing label if the upstream provider has // already set one that is schema-conformant (no slashes // or other special characters that would fail Signal K // $source validation). Remote Signal K servers forward // deltas that already carry their own label (e.g. canhat, // ydwg02); preserving that lets the receiving server // recognise the same physical device across transports. // Native providers and providers that set illegal labels // (e.g. a raw device path /dev/actisense) get normalised // to providerId. const existing = update.source.label; if (typeof existing !== 'string' || existing.length === 0 || !/^[A-Za-z0-9-_.]+$/.test(existing)) { update.source.label = providerId; } if (!update.$source) { update.$source = (0, server_api_1.getSourceId)(update.source); } // Talker-group rewriting is a local-provider feature: only // apply it when this provider actually owns the label. if (update.source.type === 'NMEA0183' && update.source.talker && update.source.label === providerId) { const lookup = providerTalkerLookups.get(providerId); if (lookup) { const groupName = lookup.get(update.source.talker); if (groupName) { update.$source = (providerId + '.' + groupName); update.source.talker = groupName; } } } } else { if (typeof update.$source === 'undefined') { update.$source = providerId; } } if (!update.timestamp || app.config.overrideTimestampWithNow) { update.timestamp = now.toISOString(); } if ('meta' in update && !Array.isArray(update.meta)) { debug(`handleMessage: ignoring invalid meta`, update.meta); delete update.meta; } if ('values' in update || 'meta' in update) { return update; } }) .filter((update) => update !== undefined); // No valid updates, discarding if (data.updates.length < 1) return; try { // data.updates was just rebuilt above, so the resulting Partial<Delta> // is in practice a full Delta by the time it reaches the chain. const delta = (0, staticDataFilter_1.filterStaticSelfData)(data, app.selfContext); // Input handlers intercept the raw delta first, per the // registerDeltaInputHandler contract ("before they are // processed by the server"). Whatever a handler passes to // next() — modified or original — is what the cache, the // unfiltered bus, source-priority filtering and the full // model then see. A handler that never calls next() drops // the delta here, before any of that runs. const chain = skVersion === server_api_1.SKVersion.v1 ? deltachainV1 : deltachainV2; chain.process(delta, dispatchDelta, now, skVersion); } catch (err) { console.error(err); } } }; app.streambundle = new streambundle_1.StreamBundle(app.selfId); new zones_1.Zones(app.streambundle, (delta) => process.nextTick(() => app.handleMessage('self.notificationhandler', delta))); app.signalk.on('delta', app.streambundle.pushDelta.bind(app.streambundle)); app.signalk.on('unfilteredDelta', app.streambundle.pushUnfilteredDelta.bind(app.streambundle)); app.subscriptionmanager = new subscriptionmanager_1.default(app); app.deltaCache = new deltacache_1.default(app, app.streambundle); // Re-build the engine now that the delta cache is constructed // and has loaded its on-disk snapshot. The first call above ran // before app.deltaCache existed (so seenPublishersByPath was // undefined); rebuilding here lets the engine pick up the seed // and flip routesPath on for already-contended paths at boot. app.activateSourcePriorities(); const serverStartId = (0, crypto_1.randomUUID)(); app.getHello = () => ({ name: app.config.name, version: app.config.version, self: `vessels.${app.selfId}`, roles: ['master', 'main'], timestamp: new Date(), serverStartId }); app.isNmea2000OutAvailable = false; app.on('nmea2000OutAvailable', () => { app.isNmea2000OutAvailable = true; }); installProcessErrorHandlers(app); } start() { const self = this; const app = this.app; app.wrappedEmitter = (0, events_1.wrapEmitter)(app); app.emit = app.wrappedEmitter.emit; app.on = app.wrappedEmitter.addListener; app.addListener = app.wrappedEmitter.addListener; this.app.intervals = []; this.app.intervals.push(setInterval(app.signalk.pruneContexts.bind(app.signalk, (app.config.settings.pruneContextsMinutes || 60) * 60), 60 * 1000)); this.app.intervals.push(setInterval(app.deltaCache.pruneContexts.bind(app.deltaCache, (app.config.settings.pruneContextsMinutes || 60) * 60), 60 * 1000)); app.intervals.push(setInterval(() => { app.emit('serverevent', { type: 'PROVIDERSTATUS', from: 'signalk-server', data: app.getProviderStatus() }); }, 5 * 1000)); function serverUpgradeIsAvailable(err, newVersion) { if (err) { console.error(err); return; } const msg = `A new version (${newVersion}) of the server is available`; console.log(msg); app.handleMessage(app.config.name, { updates: [ { values: [ { path: 'notifications.server.newVersion', value: { state: 'normal', method: [], message: msg } } ] } ] }); } if (!process.env.SIGNALK_DISABLE_SERVER_UPDATES) { (0, modules_1.checkForNewServerVersion)(app.config.version, serverUpgradeIsAvailable); app.intervals.push(setInterval(() => (0, modules_1.checkForNewServerVersion)(app.config.version, serverUpgradeIsAvailable), 60 * 1000 * 60 * 24)); } this.app.providers = []; app.lastServerEvents = {}; app.on('serverevent', (event) => { if (event.type) { app.lastServerEvents[event.type] = event; } }); app.intervals.push((0, deltastats_1.startDeltaStatistics)(app)); return new Promise(async (resolve, reject) => { createServer(app, async (err, server) => { if (err) { reject(err); return; } app.server = server; app.interfaces = {}; app.clients = 0; debug('ID type: ' + app.selfType); debug('ID: ' + app.selfId); (0, config_1.sendBaseDeltas)(app); app.apis = await (0, api_1.startApis)(app); await startInterfaces(app); startMdns(app); app.pipedProviders = (0, pipedproviders_1.pipedProviders)(app); app.providers = app.pipedProviders.start(); const primaryPort = (0, ports_1.getPrimaryPort)(app); debug(`primary port:${primaryPort}`); server.listen(primaryPort, () => { console.log('signalk-server running at 0.0.0.0:' + primaryPort.toString() + '\n'); app.started = true; resolve(self); }); const secondaryPort = (0, ports_1.getSecondaryPort)(app); debug(`secondary port:${primaryPort}`); if (app.config.settings.ssl && secondaryPort) { startRedirectToSsl(secondaryPort, (0, ports_1.getExternalPort)(app), (anErr, aServer) => { if (!anErr) { app.redirectServer = aServer; } }); } }); }); } reload(mixed) { let settings; const self = this; if (typeof mixed === 'string') { try { settings = require(path_1.default.join(process.cwd(), mixed)); } catch (_e) { debug(`Settings file '${settings}' does not exist`); } } if (mixed !== null && typeof mixed === 'object') { settings = mixed; } if (settings) { this.app.config.settings = settings; } this.stop().catch((e) => console.error(e)); setTimeout(() => { self.start().catch((e) => console.error(e)); }, 1000); return this; } async stop(cb) { if (!this.app.started) { return this; } try { lodash_1.default.each(this.app.interfaces, (intf) => { if (intf !== null && typeof intf === 'object' && typeof intf.stop === 'function') { intf.stop(); } }); this.app.intervals.forEach((interval) => { clearInterval(interval); }); if (this.pendingSourceRefMigrations) { for (const handle of this.pendingSourceRefMigrations) { clearTimeout(handle); } this.pendingSourceRefMigrations.clear(); } this.app.providers.forEach((providerHolder) => { providerHolder.pipeElements[0].end(); }); debug('Closing server...'); const that = this; return new Promise((resolve, reject) => { this.app.server.close(() => { debug('Server closed'); if (that.app.redirectServer) { try { that.app.redirectServer.close(() => { debug('Redirect server closed'); delete that.app.redirectServer; that.app.started = false; cb && cb(); resolve(that); }); } catch (err) { reject(err); } } else { that.app.started = false; cb && cb(); resolve(that); } }); }); } catch (err) { throw err; } } } module.exports = Server; function identifyPluginFromStack(stack, plugins) { for (const plugin of plugins) { if (stack.includes(plugin.packageName)) { return plugin.id; } } return undefined; } function installProcessErrorHandlers(app) { process.on('uncaughtException', (err) => { console.error('Uncaught exception:', err); if (app.plugins) { const pluginId = identifyPluginFromStack(err.stack ?? '', app.plugins); if (pluginId) { app.setPluginError(pluginId, `Uncaught error: ${err.message}`); } } }); process.on('unhandledRejection', (reason) => { const err = reason instanceof Error ? reason : new Error(String(reason)); console.error('Unhandled rejection:', err); if (app.plugins) { const pluginId = identifyPluginFromStack(err.stack ?? '', app.plugins); if (pluginId) { app.setPluginError(pluginId, `Unhandled rejection: ${err.message}`); } } }); } function createServer(app, cb) { if (app.config.settings.ssl) { (0, security_js_1.getCertificateOptions)(app, (err, options) => { if (err) { cb(err); } else { debug('Starting server to serve both http and https'); cb(null, https_1.default.createServer(options, app)); } }); return; } let server; try { debug('Starting server to serve only http'); server = http_1.default.createServer(app); } catch (e) { cb(e); return; } cb(null, server); } function startRedirectToSsl(port, redirectPort, cb) { const redirectApp = (0, express_1.default)(); redirectApp.use((req, res) => { const host = req.headers.host?.split(':')[0]; res.redirect(`https://${host}:${redirectPort}${req.path}`); }); const server = http_1.default.createServer(redirectApp); server.listen(port, () => { console.log(`Redirect server running on port ${port.toString()}`); cb(null, server); }); } function startMdns(app) { if (lodash_1.default.isUndefined(app.config.settings.mdns) || app.config.settings.mdns) { debug(`Starting interface 'mDNS'`); try { app.interfaces.mdns = require('./mdns')(app); } catch (ex) { console.error('Could not start mDNS:' + ex); } } else { debug(`Interface 'mDNS' was disabled in configuration`); } } async function startInterfaces(app) { debug.enabled && debug('Interfaces config:' + JSON.stringify(app.config.settings.interfaces)); const argv = app.argv; const noPlugins = argv?.plugins === false; const noWebapps = argv?.webapps === false; const startupApp = app; if (noPlugins) { console.log('Plugins disabled by --no-plugins; skipping plugin loading'); startupApp.plugins = []; startupApp.pluginsMap = {}; startupApp.getPluginsList = async () => []; } if (noWebapps) { console.log('Webapps disabled by --no-webapps; skipping webapp loading'); startupApp.webapps = []; startupApp.addons = []; startupApp.embeddablewebapps = []; startupApp.pluginconfigurators = []; } const skippedInterfaces = new Set(); if (noPlugins) { skippedInterfaces.add('plugins').add('wasm'); } if (noWebapps) { skippedInterfaces.add('webapps').add('mfd_webapp'); } const availableInterfaces = require('./interfaces'); return await Promise.all(Object.keys(availableInterfaces).map(async (name) => { if (skippedInterfaces.has(name)) { debug.enabled && debug(`Not loading interface '${name}' (disabled by command line flag)`); return; } const theInterface = availableInterfaces[name]; if (lodash_1.default.isUndefined(app.config.settings.interfaces) || lodash_1.default.isUndefined((app.config.settings.interfaces || {})[name]) || (app.config.settings.interfaces || {})[name]) { debug(`Loading interface '${name}'`); const boundEventMethods = app.wrappedEmitter.bindMethodsById(`interface:${name}`); const appCopy = { ...app, ...boundEventMethods }; const handler = { set(obj, prop, value) { ; app[prop] = value; return true; }, get(target, prop, _receiver) { return app[prop]; } }; const _interface = (appCopy.interfaces[name] = theInterface(new Proxy(appCopy, handler))); if (_interface && lodash_1.default.isFunction(_interface.start)) { if (lodash_1.default.isUndefined(_interface.forceInactive) || !_interface.forceInactive) { debug(`Starting interface '${name}'`); _interface.data = await _interface.start(); } else { debug(`Not starting interface '${name}' by forceInactive`); } } } else { debug(`Not loading interface '${name}' because of configuration`); } })); } function isValidUpdate(update) { if (update === null || typeof update !== 'object') { return false; } const values = update.values; if ('values' in update && !Array.isArray(values)) { return false; } if (Array.isArray(values) && values.some((v) => v === null || v === undefined || typeof v.path !== 'string')) { return false; } return true; } //# sourceMappingURL=index.js.map