From 327dc31ef02caa365e5a0c03597ba18d121c32a3 Mon Sep 17 00:00:00 2001 From: Felipe Lalanne <1822826+pipex@users.noreply.github.com> Date: Thu, 10 Aug 2023 15:18:12 -0400 Subject: [PATCH] Replace node-dbus with @balena/systemd The node-dbus module is unmaintained and a blocker for the update to Node 18. Switching to our own node bindings for systemd solves this issue Relates-to: Shouqun/node-dbus#241 Change-type: patch --- Dockerfile.template | 17 ++- package-lock.json | 63 +++------- package.json | 3 +- src/lib/dbus.ts | 166 ++++++++------------------- test/integration/host-config.spec.ts | 4 +- webpack.config.js | 2 +- 6 files changed, 76 insertions(+), 179 deletions(-) diff --git a/Dockerfile.template b/Dockerfile.template index a10209e5..3bf84fe4 100644 --- a/Dockerfile.template +++ b/Dockerfile.template @@ -1,7 +1,7 @@ ARG ARCH=%%BALENA_ARCH%% ARG FATRW_VERSION=0.2.9 -ARG NODE="nodejs>16" -ARG NPM="npm>8" +ARG NODE="nodejs<19" +ARG NPM="npm<10" ################################################### # Build the supervisor dependencies @@ -18,16 +18,15 @@ ARG FATRW_LOCATION="https://github.com/balena-os/fatrw/releases/download/v${FATR WORKDIR /usr/src/app RUN apk add --update --no-cache \ - g++ \ - make \ + build-base \ python3 \ curl \ $NODE \ $NPM \ libuv \ sqlite-dev \ - dbus-dev && \ - npm install -g npm@8 + cargo \ + rust COPY package*.json ./ @@ -114,7 +113,7 @@ ARG ARCH # We want to use as close to the final image when running tests # but we need npm so we install it here again -RUN apk add --update --no-cache $NPM && npm install -g npm@8 +RUN apk add --update --no-cache $NPM WORKDIR /usr/src/app @@ -170,8 +169,8 @@ RUN npm run build # Run the production install here, to avoid the npm dependency on # the later stage RUN npm ci \ - --production \ - --no-optional \ + --omit=dev \ + --omit=optional \ --unsafe-perm \ --build-from-source \ --sqlite=/usr/lib \ diff --git a/package-lock.json b/package-lock.json index d83fcc34..c1e45c98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@balena/happy-eyeballs": "0.0.6", - "dbus": "^1.0.7", + "@balena/systemd": "^0.4.1", "got": "^12.5.3", "mdns-resolver": "^1.0.0", "semver": "^7.3.2", @@ -28,7 +28,6 @@ "@types/chai-things": "0.0.35", "@types/common-tags": "^1.8.1", "@types/copy-webpack-plugin": "^10.1.0", - "@types/dbus": "^1.0.3", "@types/dockerode": "^2.5.34", "@types/event-stream": "^3.3.34", "@types/express": "^4.17.14", @@ -115,8 +114,8 @@ "yargs": "^15.4.1" }, "engines": { - "node": "^16.17.0", - "npm": "^8.15.0" + "node": ">=16.17.0", + "npm": ">=8.15.0" } }, "node_modules/@babel/code-frame": { @@ -875,6 +874,12 @@ "node": ">=0.10.0" } }, + "node_modules/@balena/systemd": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@balena/systemd/-/systemd-0.4.1.tgz", + "integrity": "sha512-N4/kiixRHPqw9ZLdQSMEzyYm7rJrpnYW2lPcLb2bHrRoHznWPoz1UlmMOYfvf1iNgpEuSd0x9ARSWI1P6J358Q==", + "hasInstallScript": true + }, "node_modules/@dabh/diagnostics": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", @@ -1268,12 +1273,6 @@ "copy-webpack-plugin": "*" } }, - "node_modules/@types/dbus": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/dbus/-/dbus-1.0.3.tgz", - "integrity": "sha512-DY8++cs1e4d7zPuNibJOhRmaEWkSIMheGo3govF2LwHREi4yN/OJbAX79V9hDJnjUqxI9BrHoBIsfekh9LX/Hg==", - "dev": true - }, "node_modules/@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -3705,21 +3704,6 @@ "node": ">=0.10" } }, - "node_modules/dbus": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/dbus/-/dbus-1.0.7.tgz", - "integrity": "sha512-qba6/ajLoqzCy3Kl3aFgLXLP4TTf0qfgNjib1qoCJG/8HbSs0lDvxkz4nJU63CURZVzxvpK/VpQpT40KA8Kr3A==", - "hasInstallScript": true, - "os": [ - "!win32" - ], - "dependencies": { - "nan": "^2.14.0" - }, - "engines": { - "node": ">= 0.12.0" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -8905,11 +8889,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" - }, "node_modules/nanoid": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", @@ -14186,6 +14165,11 @@ } } }, + "@balena/systemd": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@balena/systemd/-/systemd-0.4.1.tgz", + "integrity": "sha512-N4/kiixRHPqw9ZLdQSMEzyYm7rJrpnYW2lPcLb2bHrRoHznWPoz1UlmMOYfvf1iNgpEuSd0x9ARSWI1P6J358Q==" + }, "@dabh/diagnostics": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", @@ -14517,12 +14501,6 @@ "copy-webpack-plugin": "*" } }, - "@types/dbus": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/dbus/-/dbus-1.0.3.tgz", - "integrity": "sha512-DY8++cs1e4d7zPuNibJOhRmaEWkSIMheGo3govF2LwHREi4yN/OJbAX79V9hDJnjUqxI9BrHoBIsfekh9LX/Hg==", - "dev": true - }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -16577,14 +16555,6 @@ "assert-plus": "^1.0.0" } }, - "dbus": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/dbus/-/dbus-1.0.7.tgz", - "integrity": "sha512-qba6/ajLoqzCy3Kl3aFgLXLP4TTf0qfgNjib1qoCJG/8HbSs0lDvxkz4nJU63CURZVzxvpK/VpQpT40KA8Kr3A==", - "requires": { - "nan": "^2.14.0" - } - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -20629,11 +20599,6 @@ "thenify-all": "^1.0.0" } }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" - }, "nanoid": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", diff --git a/package.json b/package.json index 0d7420ea..f03593e4 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "private": true, "dependencies": { "@balena/happy-eyeballs": "0.0.6", - "dbus": "^1.0.7", + "@balena/systemd": "^0.4.1", "got": "^12.5.3", "mdns-resolver": "^1.0.0", "semver": "^7.3.2", @@ -54,7 +54,6 @@ "@types/chai-things": "0.0.35", "@types/common-tags": "^1.8.1", "@types/copy-webpack-plugin": "^10.1.0", - "@types/dbus": "^1.0.3", "@types/dockerode": "^2.5.34", "@types/event-stream": "^3.3.34", "@types/express": "^4.17.14", diff --git a/src/lib/dbus.ts b/src/lib/dbus.ts index 9fe1dee5..b08e13bb 100644 --- a/src/lib/dbus.ts +++ b/src/lib/dbus.ts @@ -1,69 +1,21 @@ -import { getBus, Error as DBusError } from 'dbus'; -import { promisify } from 'util'; -import { TypedError } from 'typed-error'; -import * as _ from 'lodash'; - import log from './supervisor-console'; -import DBus = require('dbus'); - -export class DbusError extends TypedError {} - -let bus: DBus.DBusConnection; -let getInterfaceAsync: ( - serviceName: string, - objectPath: string, - ifaceName: string, -) => Promise>; - -export const initialized = _.once(async () => { - bus = getBus('system'); - getInterfaceAsync = promisify(bus.getInterface.bind(bus)); -}); - -async function getSystemdInterface() { - await initialized(); - try { - return await getInterfaceAsync( - 'org.freedesktop.systemd1', - '/org/freedesktop/systemd1', - 'org.freedesktop.systemd1.Manager', - ); - } catch (e) { - throw new DbusError(e as DBusError); - } -} - -async function getLoginManagerInterface() { - await initialized(); - try { - return await getInterfaceAsync( - 'org.freedesktop.login1', - '/org/freedesktop/login1', - 'org.freedesktop.login1.Manager', - ); - } catch (e) { - throw new DbusError(e as DBusError); - } -} +import { singleton, ServiceManager, LoginManager } from '@balena/systemd'; +import { setTimeout } from 'timers/promises'; async function startUnit(unitName: string) { - const systemd = await getSystemdInterface(); + const bus = await singleton(); + const systemd = new ServiceManager(bus); + const unit = systemd.getUnit(unitName); log.debug(`Starting systemd unit: ${unitName}`); - try { - systemd.StartUnit(unitName, 'fail'); - } catch (e) { - throw new DbusError(e as DBusError); - } + await unit.start('fail'); } export async function restartService(serviceName: string) { - const systemd = await getSystemdInterface(); + const bus = await singleton(); + const systemd = new ServiceManager(bus); + const unit = systemd.getUnit(`${serviceName}.service`); log.debug(`Restarting systemd service: ${serviceName}`); - try { - systemd.RestartUnit(`${serviceName}.service`, 'fail'); - } catch (e) { - throw new DbusError(e as DBusError); - } + await unit.restart('fail'); } export async function startService(serviceName: string) { @@ -75,13 +27,11 @@ export async function startSocket(socketName: string) { } async function stopUnit(unitName: string) { - const systemd = await getSystemdInterface(); + const bus = await singleton(); + const systemd = new ServiceManager(bus); + const unit = systemd.getUnit(unitName); log.debug(`Stopping systemd unit: ${unitName}`); - try { - systemd.StopUnit(unitName, 'fail'); - } catch (e) { - throw new DbusError(e as DBusError); - } + await unit.stop('fail'); } export async function stopService(serviceName: string) { @@ -92,60 +42,44 @@ export async function stopSocket(socketName: string) { return stopUnit(`${socketName}.socket`); } -export const reboot = async () => - setTimeout(async () => { - try { - const logind = await getLoginManagerInterface(); - logind.Reboot(false); - } catch (e) { - log.error(`Unable to reboot: ${e}`); - } - }, 1000); - -export const shutdown = async () => - setTimeout(async () => { - try { - const logind = await getLoginManagerInterface(); - logind.PowerOff(false); - } catch (e) { - log.error(`Unable to shutdown: ${e}`); - } - }, 1000); - -async function getUnitProperty( - unitName: string, - property: string, -): Promise { - const systemd = await getSystemdInterface(); - return new Promise((resolve, reject) => { - systemd.GetUnit(unitName, async (err: Error, unitPath: string) => { - if (err) { - return reject(err); - } - const iface = await getInterfaceAsync( - 'org.freedesktop.systemd1', - unitPath, - 'org.freedesktop.DBus.Properties', - ); - - iface.Get( - 'org.freedesktop.systemd1.Unit', - property, - (e: Error, value: string) => { - if (e) { - return reject(new DbusError(e)); - } - resolve(value); - }, - ); - }); - }); +export async function reboot() { + // No idea why this timeout is here, my guess + // is that it is to allow the API reboot endpoint to be able + // to send a response before the event happens + await setTimeout(1000); + const bus = await singleton(); + const logind = new LoginManager(bus); + try { + await logind.reboot(); + } catch (e) { + log.error(`Unable to reboot: ${e}`); + } } -export function serviceActiveState(serviceName: string) { - return getUnitProperty(`${serviceName}.service`, 'ActiveState'); +export async function shutdown() { + // No idea why this timeout is here, my guess + // is that it is to allow the API shutdown endpoint to be able + // to send a response before the event happens + await setTimeout(1000); + const bus = await singleton(); + const logind = new LoginManager(bus); + try { + await logind.powerOff(); + } catch (e) { + log.error(`Unable to shutdown: ${e}`); + } } -export function servicePartOf(serviceName: string) { - return getUnitProperty(`${serviceName}.service`, 'PartOf'); +export async function serviceActiveState(serviceName: string) { + const bus = await singleton(); + const systemd = new ServiceManager(bus); + const unit = systemd.getUnit(`${serviceName}.service`); + return await unit.activeState; +} + +export async function servicePartOf(serviceName: string) { + const bus = await singleton(); + const systemd = new ServiceManager(bus); + const unit = systemd.getUnit(`${serviceName}.service`); + return await unit.partOf; } diff --git a/test/integration/host-config.spec.ts b/test/integration/host-config.spec.ts index 3b481b4b..929aeab0 100644 --- a/test/integration/host-config.spec.ts +++ b/test/integration/host-config.spec.ts @@ -68,7 +68,7 @@ describe('host-config', () => { beforeEach(async () => { await tFs.enable(); // Stub external dependencies - stub(dbus, 'servicePartOf').resolves(''); + stub(dbus, 'servicePartOf').resolves([]); stub(dbus, 'restartService').resolves(); }); @@ -153,7 +153,7 @@ describe('host-config', () => { }); it('skips restarting proxy services when part of redsocks-conf.target', async () => { - (dbus.servicePartOf as SinonStub).resolves('redsocks-conf.target'); + (dbus.servicePartOf as SinonStub).resolves(['redsocks-conf.target']); await hostConfig.patch({ network: { proxy: { diff --git a/webpack.config.js b/webpack.config.js index 9df2e95a..0603c84a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,9 +19,9 @@ var externalModules = [ 'oracledb', 'pg-query-stream', 'tedious', - 'dbus', /mssql\/.*/, 'osx-temperature-sensor', + '@balena/systemd', ]; let requiredModules = [];