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
This commit is contained in:
Felipe Lalanne 2023-08-10 15:18:12 -04:00
parent 8f17c30de6
commit 327dc31ef0
6 changed files with 76 additions and 179 deletions

View File

@ -1,7 +1,7 @@
ARG ARCH=%%BALENA_ARCH%% ARG ARCH=%%BALENA_ARCH%%
ARG FATRW_VERSION=0.2.9 ARG FATRW_VERSION=0.2.9
ARG NODE="nodejs>16" ARG NODE="nodejs<19"
ARG NPM="npm>8" ARG NPM="npm<10"
################################################### ###################################################
# Build the supervisor dependencies # 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 WORKDIR /usr/src/app
RUN apk add --update --no-cache \ RUN apk add --update --no-cache \
g++ \ build-base \
make \
python3 \ python3 \
curl \ curl \
$NODE \ $NODE \
$NPM \ $NPM \
libuv \ libuv \
sqlite-dev \ sqlite-dev \
dbus-dev && \ cargo \
npm install -g npm@8 rust
COPY package*.json ./ COPY package*.json ./
@ -114,7 +113,7 @@ ARG ARCH
# We want to use as close to the final image when running tests # We want to use as close to the final image when running tests
# but we need npm so we install it here again # 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 WORKDIR /usr/src/app
@ -170,8 +169,8 @@ RUN npm run build
# Run the production install here, to avoid the npm dependency on # Run the production install here, to avoid the npm dependency on
# the later stage # the later stage
RUN npm ci \ RUN npm ci \
--production \ --omit=dev \
--no-optional \ --omit=optional \
--unsafe-perm \ --unsafe-perm \
--build-from-source \ --build-from-source \
--sqlite=/usr/lib \ --sqlite=/usr/lib \

63
package-lock.json generated
View File

@ -10,7 +10,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@balena/happy-eyeballs": "0.0.6", "@balena/happy-eyeballs": "0.0.6",
"dbus": "^1.0.7", "@balena/systemd": "^0.4.1",
"got": "^12.5.3", "got": "^12.5.3",
"mdns-resolver": "^1.0.0", "mdns-resolver": "^1.0.0",
"semver": "^7.3.2", "semver": "^7.3.2",
@ -28,7 +28,6 @@
"@types/chai-things": "0.0.35", "@types/chai-things": "0.0.35",
"@types/common-tags": "^1.8.1", "@types/common-tags": "^1.8.1",
"@types/copy-webpack-plugin": "^10.1.0", "@types/copy-webpack-plugin": "^10.1.0",
"@types/dbus": "^1.0.3",
"@types/dockerode": "^2.5.34", "@types/dockerode": "^2.5.34",
"@types/event-stream": "^3.3.34", "@types/event-stream": "^3.3.34",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
@ -115,8 +114,8 @@
"yargs": "^15.4.1" "yargs": "^15.4.1"
}, },
"engines": { "engines": {
"node": "^16.17.0", "node": ">=16.17.0",
"npm": "^8.15.0" "npm": ">=8.15.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@ -875,6 +874,12 @@
"node": ">=0.10.0" "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": { "node_modules/@dabh/diagnostics": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
@ -1268,12 +1273,6 @@
"copy-webpack-plugin": "*" "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": { "node_modules/@types/debug": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
@ -3705,21 +3704,6 @@
"node": ">=0.10" "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": { "node_modules/debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -8905,11 +8889,6 @@
"thenify-all": "^1.0.0" "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": { "node_modules/nanoid": {
"version": "3.1.20", "version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", "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": { "@dabh/diagnostics": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
@ -14517,12 +14501,6 @@
"copy-webpack-plugin": "*" "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": { "@types/debug": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
@ -16577,14 +16555,6 @@
"assert-plus": "^1.0.0" "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": { "debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -20629,11 +20599,6 @@
"thenify-all": "^1.0.0" "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": { "nanoid": {
"version": "3.1.20", "version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",

View File

@ -32,7 +32,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@balena/happy-eyeballs": "0.0.6", "@balena/happy-eyeballs": "0.0.6",
"dbus": "^1.0.7", "@balena/systemd": "^0.4.1",
"got": "^12.5.3", "got": "^12.5.3",
"mdns-resolver": "^1.0.0", "mdns-resolver": "^1.0.0",
"semver": "^7.3.2", "semver": "^7.3.2",
@ -54,7 +54,6 @@
"@types/chai-things": "0.0.35", "@types/chai-things": "0.0.35",
"@types/common-tags": "^1.8.1", "@types/common-tags": "^1.8.1",
"@types/copy-webpack-plugin": "^10.1.0", "@types/copy-webpack-plugin": "^10.1.0",
"@types/dbus": "^1.0.3",
"@types/dockerode": "^2.5.34", "@types/dockerode": "^2.5.34",
"@types/event-stream": "^3.3.34", "@types/event-stream": "^3.3.34",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",

View File

@ -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 log from './supervisor-console';
import DBus = require('dbus'); import { singleton, ServiceManager, LoginManager } from '@balena/systemd';
import { setTimeout } from 'timers/promises';
export class DbusError extends TypedError {}
let bus: DBus.DBusConnection;
let getInterfaceAsync: <T = DBus.AnyInterfaceMethod>(
serviceName: string,
objectPath: string,
ifaceName: string,
) => Promise<DBus.DBusInterface<T>>;
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);
}
}
async function startUnit(unitName: string) { 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}`); log.debug(`Starting systemd unit: ${unitName}`);
try { await unit.start('fail');
systemd.StartUnit(unitName, 'fail');
} catch (e) {
throw new DbusError(e as DBusError);
}
} }
export async function restartService(serviceName: string) { 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}`); log.debug(`Restarting systemd service: ${serviceName}`);
try { await unit.restart('fail');
systemd.RestartUnit(`${serviceName}.service`, 'fail');
} catch (e) {
throw new DbusError(e as DBusError);
}
} }
export async function startService(serviceName: string) { export async function startService(serviceName: string) {
@ -75,13 +27,11 @@ export async function startSocket(socketName: string) {
} }
async function stopUnit(unitName: 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}`); log.debug(`Stopping systemd unit: ${unitName}`);
try { await unit.stop('fail');
systemd.StopUnit(unitName, 'fail');
} catch (e) {
throw new DbusError(e as DBusError);
}
} }
export async function stopService(serviceName: string) { export async function stopService(serviceName: string) {
@ -92,60 +42,44 @@ export async function stopSocket(socketName: string) {
return stopUnit(`${socketName}.socket`); return stopUnit(`${socketName}.socket`);
} }
export const reboot = async () => export async function reboot() {
setTimeout(async () => { // 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 { try {
const logind = await getLoginManagerInterface(); await logind.reboot();
logind.Reboot(false);
} catch (e) { } catch (e) {
log.error(`Unable to reboot: ${e}`); log.error(`Unable to reboot: ${e}`);
} }
}, 1000); }
export const shutdown = async () => export async function shutdown() {
setTimeout(async () => { // 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 { try {
const logind = await getLoginManagerInterface(); await logind.powerOff();
logind.PowerOff(false);
} catch (e) { } catch (e) {
log.error(`Unable to shutdown: ${e}`); log.error(`Unable to shutdown: ${e}`);
} }
}, 1000);
async function getUnitProperty(
unitName: string,
property: string,
): Promise<string> {
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 function serviceActiveState(serviceName: string) { export async function serviceActiveState(serviceName: string) {
return getUnitProperty(`${serviceName}.service`, 'ActiveState'); const bus = await singleton();
const systemd = new ServiceManager(bus);
const unit = systemd.getUnit(`${serviceName}.service`);
return await unit.activeState;
} }
export function servicePartOf(serviceName: string) { export async function servicePartOf(serviceName: string) {
return getUnitProperty(`${serviceName}.service`, 'PartOf'); const bus = await singleton();
const systemd = new ServiceManager(bus);
const unit = systemd.getUnit(`${serviceName}.service`);
return await unit.partOf;
} }

View File

@ -68,7 +68,7 @@ describe('host-config', () => {
beforeEach(async () => { beforeEach(async () => {
await tFs.enable(); await tFs.enable();
// Stub external dependencies // Stub external dependencies
stub(dbus, 'servicePartOf').resolves(''); stub(dbus, 'servicePartOf').resolves([]);
stub(dbus, 'restartService').resolves(); stub(dbus, 'restartService').resolves();
}); });
@ -153,7 +153,7 @@ describe('host-config', () => {
}); });
it('skips restarting proxy services when part of redsocks-conf.target', async () => { 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({ await hostConfig.patch({
network: { network: {
proxy: { proxy: {

View File

@ -19,9 +19,9 @@ var externalModules = [
'oracledb', 'oracledb',
'pg-query-stream', 'pg-query-stream',
'tedious', 'tedious',
'dbus',
/mssql\/.*/, /mssql\/.*/,
'osx-temperature-sensor', 'osx-temperature-sensor',
'@balena/systemd',
]; ];
let requiredModules = []; let requiredModules = [];