262 lines
8.3 KiB
JavaScript
262 lines
8.3 KiB
JavaScript
import { createServer } from "node:http";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import {
|
|
DEVICE_ADAPTER_CATALOG,
|
|
} from "../../packages/device-adapter-catalog/src/index.mjs";
|
|
import {
|
|
createDeviceEdgeChannelServer,
|
|
} from "../../services/device-edge-channel/src/runtime.mjs";
|
|
import {
|
|
readRuntimeConfiguration,
|
|
} from "../../services/device-edge-channel/src/server.mjs";
|
|
import {
|
|
createDeviceGatewayRuntime,
|
|
} from "../../services/device-gateway/src/runtime.mjs";
|
|
import { createHostTelemetryCollector } from "./host-telemetry-runtime.mjs";
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
await main();
|
|
}
|
|
|
|
export async function main(environment = process.env) {
|
|
const base = await readRuntimeConfiguration(environment);
|
|
const tracker = normalizeTrackerIngressConfiguration(
|
|
environment,
|
|
base.channel.edgeRegistrationId,
|
|
);
|
|
const channel = createDeviceEdgeChannelServer({
|
|
...base.channel,
|
|
commandTransport: "typed-service-ping-v1",
|
|
});
|
|
const gateway = createDeviceGatewayRuntime({
|
|
listenEnabled: true,
|
|
publicIngressEnabled: true,
|
|
coreChannelAuthenticated: true,
|
|
adapterRegistry: DEVICE_ADAPTER_CATALOG.registry,
|
|
protocolProfileRef: tracker.protocolProfileRef,
|
|
edgeRef: tracker.edgeRef,
|
|
healthHost: tracker.healthHost,
|
|
healthPort: tracker.healthPort,
|
|
tcpHost: tracker.tcpHost,
|
|
tcpPort: tracker.tcpPort,
|
|
maxBufferedBytes: tracker.maxBufferedBytes,
|
|
maxAggregateBufferedBytes: tracker.maxAggregateBufferedBytes,
|
|
maxConcurrentSessions: tracker.maxConcurrentSessions,
|
|
maxSessionsPerAddress: tracker.maxSessionsPerAddress,
|
|
maxConnectionsPerMinutePerAddress:
|
|
tracker.maxConnectionsPerMinutePerAddress,
|
|
maxTrackedSourceAddresses: tracker.maxTrackedSourceAddresses,
|
|
sessionTimeoutMs: tracker.sessionTimeoutMs,
|
|
onDiscovery: (signal) => channel.submitDiscovery(signal),
|
|
onMessage: (message) => channel.submitAdapterMessage(message),
|
|
onCommandStatus: (status) => channel.submitCommandStatus(status),
|
|
});
|
|
const telemetry = createHostTelemetryCollector({
|
|
submit: (snapshot) => channel.submitHostTelemetry(snapshot),
|
|
hostKey: environment.NODEDC_INFRASTRUCTURE_HOST_KEY
|
|
?? "robot2b-b2-edge-vps",
|
|
host: environment.NODEDC_HOST_TELEMETRY_HOST ?? "127.0.0.1",
|
|
port: environment.NODEDC_HOST_TELEMETRY_PORT ?? 18223,
|
|
agentVersion: environment.NODEDC_HOST_TELEMETRY_AGENT_VERSION ?? "1.38.4",
|
|
});
|
|
const health = createCombinedHealthServer(channel, gateway, telemetry, base.health);
|
|
let stopping = false;
|
|
|
|
try {
|
|
await channel.start();
|
|
await gateway.start();
|
|
await telemetry.start();
|
|
await listen(health, base.health.port, base.health.host);
|
|
} catch (error) {
|
|
await Promise.allSettled([
|
|
gateway.stop(),
|
|
telemetry.stop(),
|
|
channel.stop(),
|
|
closeServer(health),
|
|
]);
|
|
throw error;
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
event: "device_edge_runtime_started",
|
|
channel: `${base.channel.host}:${base.channel.port}`,
|
|
health: `${base.health.host}:${base.health.port}`,
|
|
trackerIngress: `${tracker.tcpHost}:${tracker.tcpPort}`,
|
|
hostTelemetry: `${telemetry.status().host}:${telemetry.status().port}`,
|
|
adapterProfile: tracker.protocolProfileRef,
|
|
edgeRegistrationId: base.channel.edgeRegistrationId,
|
|
channelGeneration: base.channel.channelGeneration,
|
|
trustGeneration: base.channel.trustGeneration,
|
|
commandTransport: "typed-service-ping-v1",
|
|
}));
|
|
|
|
process.on("SIGTERM", shutdown);
|
|
process.on("SIGINT", shutdown);
|
|
|
|
async function shutdown() {
|
|
if (stopping) return;
|
|
stopping = true;
|
|
await Promise.allSettled([
|
|
gateway.stop(),
|
|
telemetry.stop(),
|
|
channel.stop(),
|
|
closeServer(health),
|
|
]);
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
export function normalizeTrackerIngressConfiguration(environment = {}, edgeRef) {
|
|
return Object.freeze({
|
|
edgeRef: normalizeRef(edgeRef, "device_edge_runtime_edge_ref_invalid"),
|
|
protocolProfileRef: normalizeProfileRef(
|
|
environment.DEVICE_GATEWAY_PROTOCOL_PROFILE_REF
|
|
?? DEVICE_ADAPTER_CATALOG.defaultProfileRef,
|
|
),
|
|
healthHost: normalizeLoopbackHost(
|
|
environment.DEVICE_GATEWAY_HEALTH_HOST ?? "127.0.0.1",
|
|
),
|
|
healthPort: normalizePort(environment.DEVICE_GATEWAY_HEALTH_PORT, 18221),
|
|
tcpHost: normalizePublicHost(
|
|
environment.DEVICE_GATEWAY_TCP_HOST ?? "0.0.0.0",
|
|
),
|
|
tcpPort: normalizePort(environment.DEVICE_GATEWAY_TCP_PORT, 9921),
|
|
maxBufferedBytes: normalizeInteger(
|
|
environment.DEVICE_GATEWAY_MAX_BUFFERED_BYTES,
|
|
64 * 1024,
|
|
1024,
|
|
256 * 1024,
|
|
"device_edge_runtime_session_buffer_invalid",
|
|
),
|
|
maxAggregateBufferedBytes: normalizeInteger(
|
|
environment.DEVICE_GATEWAY_MAX_AGGREGATE_BUFFERED_BYTES,
|
|
32 * 1024 * 1024,
|
|
1024,
|
|
32 * 1024 * 1024,
|
|
"device_edge_runtime_aggregate_buffer_invalid",
|
|
),
|
|
maxConcurrentSessions: normalizeInteger(
|
|
environment.DEVICE_GATEWAY_MAX_SESSIONS,
|
|
128,
|
|
1,
|
|
128,
|
|
"device_edge_runtime_session_limit_invalid",
|
|
),
|
|
maxSessionsPerAddress: normalizeInteger(
|
|
environment.DEVICE_GATEWAY_MAX_SESSIONS_PER_ADDRESS,
|
|
16,
|
|
1,
|
|
16,
|
|
"device_edge_runtime_address_session_limit_invalid",
|
|
),
|
|
maxConnectionsPerMinutePerAddress: normalizeInteger(
|
|
environment.DEVICE_GATEWAY_MAX_CONNECTIONS_PER_MINUTE_PER_ADDRESS,
|
|
60,
|
|
1,
|
|
60,
|
|
"device_edge_runtime_address_rate_limit_invalid",
|
|
),
|
|
maxTrackedSourceAddresses: normalizeInteger(
|
|
environment.DEVICE_GATEWAY_MAX_TRACKED_SOURCE_ADDRESSES,
|
|
2048,
|
|
1,
|
|
2048,
|
|
"device_edge_runtime_source_tracking_limit_invalid",
|
|
),
|
|
sessionTimeoutMs: normalizeInteger(
|
|
environment.DEVICE_GATEWAY_SESSION_TIMEOUT_MS,
|
|
10_000,
|
|
100,
|
|
60_000,
|
|
"device_edge_runtime_session_timeout_invalid",
|
|
),
|
|
});
|
|
}
|
|
|
|
function createCombinedHealthServer(channel, gateway, telemetry, healthConfig) {
|
|
return createServer((request, response) => {
|
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
response.setHeader("Cache-Control", "no-store");
|
|
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
if (request.method !== "GET" || request.url !== "/healthz") {
|
|
response.statusCode = 404;
|
|
response.end(JSON.stringify({ ok: false, error: "not_found" }));
|
|
return;
|
|
}
|
|
response.statusCode = 200;
|
|
response.end(JSON.stringify({
|
|
ok: true,
|
|
service: "nodedc-device-edge-runtime",
|
|
health: `${healthConfig.host}:${healthConfig.port}`,
|
|
...channel.status(),
|
|
trackerIngress: "telemetry-ingest",
|
|
tracker: gateway.status(),
|
|
hostTelemetry: telemetry.status(),
|
|
commandTransport: "typed-service-ping-v1",
|
|
}));
|
|
});
|
|
}
|
|
|
|
function normalizeRef(value, errorCode) {
|
|
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
|
|
throw new TypeError(errorCode);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizeProfileRef(value) {
|
|
if (typeof value !== "string" || !/^[a-z][a-z0-9._-]{2,127}$/.test(value)) {
|
|
throw new TypeError("device_edge_runtime_profile_ref_invalid");
|
|
}
|
|
DEVICE_ADAPTER_CATALOG.registry.resolveProfile(value);
|
|
return value;
|
|
}
|
|
|
|
function normalizeLoopbackHost(value) {
|
|
if (!["127.0.0.1", "::1"].includes(value)) {
|
|
throw new TypeError("device_edge_runtime_health_host_invalid");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizePublicHost(value) {
|
|
if (!["0.0.0.0", "::"].includes(value)) {
|
|
throw new TypeError("device_edge_runtime_public_host_invalid");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizePort(value, fallback) {
|
|
return normalizeInteger(
|
|
value,
|
|
fallback,
|
|
1,
|
|
65_535,
|
|
"device_edge_runtime_port_invalid",
|
|
);
|
|
}
|
|
|
|
function normalizeInteger(value, fallback, minimum, maximum, errorCode) {
|
|
const normalized = Number(value ?? fallback);
|
|
if (!Number.isSafeInteger(normalized) || normalized < minimum || normalized > maximum) {
|
|
throw new TypeError(errorCode);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function listen(server, port, host) {
|
|
return new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(port, host, () => {
|
|
server.off("error", reject);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function closeServer(server) {
|
|
if (!server.listening) return Promise.resolve();
|
|
return new Promise((resolve) => server.close(() => resolve()));
|
|
}
|