signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
870 lines (869 loc) • 39.7 kB
JavaScript
;
/* 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