Merge pull request #2047 from balena-os/nodejs-18

Update to nodejs 18
This commit is contained in:
flowzone-app[bot] 2023-08-17 00:37:41 +00:00 committed by GitHub
commit f8694bc506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 179 additions and 655 deletions

View File

@ -1,12 +1,12 @@
ARG ARCH=%%BALENA_ARCH%%
ARG FATRW_VERSION=0.2.9
ARG NODE="nodejs<18"
ARG NPM="npm<9"
ARG NODE="nodejs<19"
ARG NPM="npm<10"
###################################################
# Build the supervisor dependencies
###################################################
FROM alpine:3.16 as build-base
FROM alpine:3.18 as build-base
ARG ARCH
ARG NODE
@ -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 \

View File

@ -39,7 +39,18 @@ Here's a few guidelines to make the process easier for everyone involved.
- Commits should be squashed as much as makes sense.
- Commits should be signed-off (`git commit -s`)
## Setup
## Developing the supervisor
### Requirements
These are the system requirements for developing and testing the balenaSupervisor on a local machine
- [Node.js](https://nodejs.org/en) v18 or latest
- [Rust](https://www.rust-lang.org/) v1.64 or latest for installing the [@balena/systemd](https://www.npmjs.com/package/@balena/systemd) NPM package.
- If developing on an architecture not supported by default by [node-sqlite3](https://github.com/TryGhost/node-sqlite3#prebuilt-binaries), a C++ compiler and linker are also required, plus the libsqlite development headers.
### Setup
To get the codebase setup on your development machine follow these steps. For running the supervisor on a device see [Developing the supervisor](#developing-the-supervisor) or [Using balenaOS-in-container](#using-balenaos-in-container).
@ -53,11 +64,11 @@ npm ci
We explicitly use `npm ci` over `npm install` to ensure the correct package versions are installed. More documentation for this can be found [here](https://docs.npmjs.com/cli/ci) on the npm cli docs.
You're now ready to start developing. If you get stuck at some point please reference the [troubleshooting](#troubleshooting) section before creating an issue.
You're now ready to start developing.
## Developing the supervisor
### Running your code
By far the most convenient way to develop the supervisor is
By far the most convenient way to test your supervisor code is
to download a development image of balenaOS from the
dashboard, and run it on a device you have to hand. You can
then use the local network to sync changes using
@ -69,7 +80,7 @@ a supervisor locally, using
[balenaOS-in-container](https://github.com/balena-os/balenaos-in-container).
These steps are detailed below.
### Sync
#### Sync
Example:
@ -101,7 +112,7 @@ and sync any relevant file changes to the running supervisor
container. It will then decide if the container should be
restarted, or let nodemon handle the changes.
### Using balenaOS-in-container
#### Using balenaOS-in-container
This process will allow you to run a development instance of the supervisor on your local computer. It is not recommended for production scenarios, but allows someone developing on the supervisor to test changes quickly.
The supervisor is run inside a balenaOS instance running in a container, so effectively it's a Docker-in-Docker instance (or more precisely, [balenaEngine](https://github.com/resin-os/balena-engine)-in-Docker).
@ -117,7 +128,7 @@ $ npm run sync -- d19baeb.local -a amd64
> ts-node --project tsconfig.json sync/sync.ts "d19baeb.local"
```
## Developing using a production image or device
### Developing using a production image or device
A production balena image does not have an open docker
socket, required for livepush to work. In this situation, [balena tunnel](https://www.balena.io/docs/reference/balena-cli/#tunnel-deviceorfleet)
@ -152,7 +163,7 @@ root@d19baeb.local
npm run sync -- 127.0.0.1 -a amd64
```
## Building
### Building
The supervisor is built automatically by the CI system, but a docker image can be also be built locally using the [balena CLI](https://www.balena.io/docs/reference/balena-cli/#build-source).
@ -174,7 +185,7 @@ For instance to build for raspberrypi4:
balena build -d raspberrypi4-64 -A aarch64
```
## Testing
### Testing
The codebase splits the test suite into unit and integration tests. While unit tests can be run in the local development machine,
integration tests require a containerized environment with the right dependencies to be setup.
@ -235,30 +246,7 @@ npm run test:unit -- -g "(GET|POST|PUT|DELETE)"
The `--grep` option, when specified, will trigger mocha to only run tests matching the given pattern which is internally compiled to a RegExp.
## Troubleshooting
Make sure you are running at least:
```sh
node -v # >= 12.16.2
npm -v # >= 6.14.4
git --version # >= 2.13.0
```
Also, ensure you're installing dependencies with `npm ci` as this will perform a clean install and guarantee the module versions specified are downloaded rather then installed which might attempt to upgrade!
If you have upgraded system packages and find that your tests are failing to initialize with docker network errors, a reboot may resolve this. See [this issue](https://github.com/moby/moby/issues/34575) for details.
### DBus
When developing on macOS you may need to install DBus on the development host.
1. `brew install dbus`
2. `npm ci`
On Debian-based systems, `sudo apt install libdbus-1-dev` would be the equivalent.
#### Downgrading versions
## Downgrading versions
The Supervisor will always be forwards compatible so you can just simply run newer versions. If there is data that must be normalized to a new schema such as the naming of engine resources, values in the sqlite database, etc then the new version will automatically take care of that either via [migrations](/src/migrations) or at runtime when the value is queried.

View File

@ -1,13 +1,6 @@
version: '2.3'
services:
dbus-services:
environment:
DEVELOPMENT: 1
volumes:
- './test/lib/dbus/systemd.ts:/usr/src/app/systemd.ts'
- './test/lib/dbus/login.ts:/usr/src/app/login.ts'
sut:
command: sleep infinity
volumes:

View File

@ -12,41 +12,30 @@ services:
# Use bridge networking for the tests
environment:
DOCKER_HOST: tcp://docker:2375
DBUS_SYSTEM_BUS_ADDRESS: unix:path=/run/dbus/system_bus_socket
DBUS_SYSTEM_BUS_ADDRESS: unix:path=/shared/dbus/system_bus_socket
# Required to skip device mounting in test env
TEST: 1
depends_on:
- docker
- dbus
- dbus-services
- mock-systemd
volumes:
- dbus:/run/dbus
- dbus:/shared/dbus
- ./test/data/root:/mnt/root
- ./test/data/root/mnt/boot:/mnt/boot
- ./test/lib/wait-for-it.sh:/wait-for-it.sh
tmpfs:
- /data # sqlite3 database
dbus:
image: balenablocks/dbus
stop_grace_period: 3s
environment:
DBUS_CONFIG: session.conf
DBUS_ADDRESS: unix:path=/run/dbus/system_bus_socket
# The service setup
mock-systemd:
image: ghcr.io/balena-os/mock-systemd-bus
# Necessary to run systemd in a container
privileged: true
volumes:
- dbus:/run/dbus
# Fake system service to listen for supervisor
# requests
dbus-services:
build: ./test/lib/dbus
stop_grace_period: 3s
depends_on:
- dbus
volumes:
- dbus:/run/dbus
- dbus:/shared/dbus
environment:
DBUS_SYSTEM_BUS_ADDRESS: unix:path=/run/dbus/system_bus_socket
DBUS_SYSTEM_BUS_ADDRESS: unix:path=/shared/dbus/system_bus_socket
MOCK_SYSTEMD_UNITS: openvpn.service avahi.socket
docker:
image: docker:dind
@ -77,15 +66,14 @@ services:
depends_on:
- balena-supervisor-sut
- docker
- dbus
- dbus-services
- mock-systemd
stop_grace_period: 3s
volumes:
- dbus:/run/dbus
- dbus:/shared/dbus
# Set required supervisor configuration variables here
environment:
DOCKER_HOST: tcp://docker:2375
DBUS_SYSTEM_BUS_ADDRESS: unix:path=/run/dbus/system_bus_socket
DBUS_SYSTEM_BUS_ADDRESS: unix:path=/shared/dbus/system_bus_socket
BALENA_SUPERVISOR_ADDRESS: http://balena-supervisor-sut:48484
# Required by migrations
CONFIG_MOUNT_POINT: /mnt/boot/config.json

77
package-lock.json generated
View File

@ -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",
@ -37,7 +36,7 @@
"@types/mocha": "^8.2.3",
"@types/mock-fs": "^4.13.1",
"@types/morgan": "^1.9.3",
"@types/node": "^16.11.63",
"@types/node": "^18.11.7",
"@types/request": "^2.48.8",
"@types/rewire": "^2.5.28",
"@types/rimraf": "^2.0.5",
@ -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",
@ -1485,9 +1484,9 @@
}
},
"node_modules/@types/node": {
"version": "16.11.63",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.63.tgz",
"integrity": "sha512-3OxnrEQLBz8EIIaHpg3CibmTAEGkDBcHY4fL5cnBwg2vd2yvHrUDGWxK+MlYPeXWWIoJJW79dGtU+oeBr6166Q==",
"version": "18.11.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.7.tgz",
"integrity": "sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==",
"dev": true
},
"node_modules/@types/object-hash": {
@ -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",
@ -14734,9 +14712,9 @@
}
},
"@types/node": {
"version": "16.11.63",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.63.tgz",
"integrity": "sha512-3OxnrEQLBz8EIIaHpg3CibmTAEGkDBcHY4fL5cnBwg2vd2yvHrUDGWxK+MlYPeXWWIoJJW79dGtU+oeBr6166Q==",
"version": "18.11.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.7.tgz",
"integrity": "sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==",
"dev": true
},
"@types/object-hash": {
@ -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",

View File

@ -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",
@ -40,8 +40,8 @@
"systeminformation": "^5.6.10"
},
"engines": {
"node": "^16.17.0",
"npm": "^8.15.0"
"node": ">=16.17.0",
"npm": ">=8.15.0"
},
"devDependencies": {
"@balena/contrato": "^0.6.0",
@ -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",
@ -63,7 +62,7 @@
"@types/mocha": "^8.2.3",
"@types/mock-fs": "^4.13.1",
"@types/morgan": "^1.9.3",
"@types/node": "^16.11.63",
"@types/node": "^18.11.7",
"@types/request": "^2.48.8",
"@types/rewire": "^2.5.28",
"@types/rimraf": "^2.0.5",

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 DBus = require('dbus');
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);
}
}
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<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 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;
}

View File

@ -1,5 +1,5 @@
import { expect } from 'chai';
import { stub, SinonStub, spy, SinonSpy } from 'sinon';
import { stub, SinonStub } from 'sinon';
import * as Docker from 'dockerode';
import * as request from 'supertest';
import { setTimeout } from 'timers/promises';
@ -10,9 +10,41 @@ import * as hostConfig from '~/src/host-config';
import * as deviceApi from '~/src/device-api';
import * as actions from '~/src/device-api/actions';
import * as TargetState from '~/src/device-state/target-state';
import * as dbus from '~/lib/dbus';
import { cleanupDocker } from '~/test-lib/docker-helper';
import { exec } from '~/src/lib/fs-utils';
export async function dbusSend(
dest: string,
path: string,
message: string,
...contents: string[]
) {
const { stdout, stderr } = await exec(
[
'dbus-send',
'--system',
`--dest=${dest}`,
'--print-reply',
path,
message,
...contents,
].join(' '),
{ encoding: 'utf8' },
);
if (stderr) {
throw new Error(stderr);
}
// Remove first line, trim each line, and join them back together
return stdout
.split(/\r?\n/)
.slice(1)
.map((s) => s.trim())
.join('');
}
describe('regenerates API keys', () => {
// Stub external dependency - current state report should be tested separately.
// API key related methods are tested in api-keys.spec.ts.
@ -692,24 +724,34 @@ describe('manages application lifecycle', () => {
});
describe('reboots or shuts down device', () => {
before(async () => {
spy(dbus, 'reboot');
spy(dbus, 'shutdown');
});
after(() => {
(dbus.reboot as SinonSpy).restore();
(dbus.shutdown as SinonSpy).restore();
});
it('reboots device', async () => {
await actions.executeDeviceAction({ action: 'reboot' });
expect(dbus.reboot as SinonSpy).to.have.been.called;
// The reboot method delays the call by one second
await setTimeout(1500);
await expect(
dbusSend(
'org.freedesktop.login1',
'/org/freedesktop/login1',
'org.freedesktop.DBus.Properties.Get',
'string:org.freedesktop.login1.Manager',
'string:MockState',
),
).to.eventually.equal('variant string "rebooting"');
});
it('shuts down device', async () => {
await actions.executeDeviceAction({ action: 'shutdown' });
expect(dbus.shutdown as SinonSpy).to.have.been.called;
// The shutdown method delays the call by one second
await setTimeout(1500);
await expect(
dbusSend(
'org.freedesktop.login1',
'/org/freedesktop/login1',
'org.freedesktop.DBus.Properties.Get',
'string:org.freedesktop.login1.Manager',
'string:MockState',
),
).to.eventually.equal('variant string "off"');
});
});

View File

@ -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: {

View File

@ -1 +0,0 @@
package-lock=false

View File

@ -1,10 +0,0 @@
FROM node:16-alpine
RUN apk add --update python3 dbus-dev make g++ libgcc
WORKDIR /usr/src/app
COPY package.json *.ts tsconfig.json entry.sh ./
RUN npm install && npm run build
CMD ["./entry.sh"]

View File

@ -1,13 +0,0 @@
#!/bin/sh
if [ "${DEVELOPMENT}" = "1" ]; then
# Use nodemon in development mode
npx nodemon -w systemd.ts systemd.ts &
npx nodemon -w login.ts login.ts
else
# Launch services in separate processes. node-dbus for some
# reason blocks when trying to register multiple services
# on the same process
node systemd.js &
node login.js
fi

View File

@ -1,66 +0,0 @@
import { createSystemInterface } from './utils';
// Create login interface
const login = createSystemInterface(
'org.freedesktop.login1',
'/org/freedesktop/login1',
'org.freedesktop.login1.Manager',
);
type SystemState = { status: 'ready' | 'rebooting' | 'off' };
const systemState: SystemState = { status: 'ready' };
login.addMethod(
'Reboot',
{ in: [{ type: 'b', name: 'interactive' }] } as any,
function (_interactive, callback) {
// Wait a bit before changing the runtime state
setTimeout(() => {
console.log('Rebooting');
systemState.status = 'rebooting';
}, 500);
callback(null);
},
);
login.addMethod(
'PowerOff',
{ in: [{ type: 'b', name: 'interactive' }] } as any,
function (_interactive, callback) {
// Wait a bit before changing the runtime state
setTimeout(() => {
console.log('Powering off');
systemState.status = 'off';
}, 500);
callback(null);
},
);
// This is not a real login interface method, but it will help for
// testing
login.addMethod(
'PowerOn',
{ in: [{ type: 'b', name: 'interactive' }] } as any,
function (_interactive, callback) {
// Wait a bit before changing the runtime state
setTimeout(() => {
console.log('Starting up');
systemState.status = 'ready';
}, 500);
callback(null);
},
);
// This is not a real login interface method, but it will help for
// testing
login.addMethod(
'GetState',
{ out: { type: 's', name: 'state' } } as any,
function (callback: any) {
callback(null, systemState.status);
},
);
login.update();

View File

@ -1,20 +0,0 @@
{
"name": "dbus",
"version": "0.1.0",
"description": "OS dbus service spoofing",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"dbus": "^1.0.7"
},
"devDependencies": {
"@types/dbus": "^1.0.3",
"nodemon": "^2.0.20",
"ts-node": "^10.9.1",
"typescript": "^4.8.4"
}
}

View File

@ -1,173 +0,0 @@
import * as DBus from 'dbus';
import { createSystemInterface } from './utils';
const systemdService = DBus.registerService(
'system',
'org.freedesktop.systemd1',
);
// Create the systemd
const systemd = createSystemInterface(
systemdService,
'/org/freedesktop/systemd1',
'org.freedesktop.systemd1.Manager',
);
type Unit = {
running: boolean;
path: string;
partOf?: string;
};
// Maintain the state of created units in memory
const units: { [key: string]: Unit } = {};
function createUnit(name: string, path: string, partOf?: string) {
// Each unit needs an object and a properties interface
const obj = systemdService.createObject(path);
const iface = obj.createInterface('org.freedesktop.DBus.Properties');
units[name] = { running: false, path, partOf };
// org.freedesktop.DBus.Properties needs a Get method to get the
// unit properties
iface.addMethod(
'Get',
{
in: [
{ type: 's', name: 'interface_name' },
{ type: 's', name: 'property_name' },
],
out: { type: 'v' },
} as any,
function (interfaceName, propertyName, callback: any) {
if (interfaceName !== 'org.freedesktop.systemd1.Unit') {
callback(`Unkown interface: ${interfaceName}`);
}
switch (propertyName) {
case 'ActiveState':
callback(null, units[name].running ? 'active' : 'inactive');
break;
case 'PartOf':
callback(partOf ?? 'none');
break;
default:
callback(`Unknown property: ${propertyName}`);
}
},
);
iface.update();
}
systemd.addMethod(
'StopUnit',
{
in: [
{ type: 's', name: 'unit_name' },
{ type: 's', name: 'mode' },
],
out: { type: 'o' },
} as any,
function (unitName, _mode, callback: any) {
if (!units[unitName]) {
callback(`Unit not found: ${unitName}`);
return;
}
// Wait a bit before changing the runtime state
setTimeout(() => {
units[unitName] = { ...units[unitName], running: false };
}, 500);
callback(
null,
`/org/freedesktop/systemd1/job/${String(
Math.floor(Math.random() * 10000),
)}`,
);
},
);
systemd.addMethod(
'StartUnit',
{
in: [
{ type: 's', name: 'unit_name' },
{ type: 's', name: 'mode' },
],
out: { type: 'o' },
} as any,
function (unitName, _mode, callback: any) {
if (!units[unitName]) {
callback(`Unit not found: ${unitName}`);
return;
}
// Wait a bit before changing the runtime state
setTimeout(() => {
units[unitName] = { ...units[unitName], running: true };
}, 500);
callback(
null,
// Make up a job number
`/org/freedesktop/systemd1/job/${String(
Math.floor(Math.random() * 10000),
)}`,
);
},
);
systemd.addMethod(
'RestartUnit',
{
in: [
{ type: 's', name: 'unit_name' },
{ type: 's', name: 'mode' },
],
out: { type: 'o' },
} as any,
function (unitName, _mode, callback: any) {
if (!units[unitName]) {
callback(`Unit not found: ${unitName}`);
return;
}
// Wait a bit before changing the runtime state
setTimeout(() => {
units[unitName] = { ...units[unitName], running: false };
}, 500);
setTimeout(() => {
units[unitName] = { ...units[unitName], running: true };
}, 1000);
callback(
null,
`/org/freedesktop/systemd1/job/${String(
Math.floor(Math.random() * 10000),
)}`,
);
},
);
systemd.addMethod(
'GetUnit',
{ in: [{ type: 's', name: 'unit_name' }], out: { type: 'o' } } as any,
function (unitName, callback) {
if (!units[unitName]) {
callback(`Unit not found: ${unitName}`);
return;
}
const { path } = units[unitName];
callback(null, path);
},
);
// Simulate OS units
createUnit('openvpn.service', '/org/freedesktop/systemd1/unit/openvpn');
createUnit('avahi.socket', '/org/freedesktop/systemd1/unit/avahi');
systemd.update();

View File

@ -1,14 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"noUnusedParameters": true,
"noUnusedLocals": true,
"removeComments": true,
"sourceMap": true,
"strict": true,
"target": "es2019",
"declaration": true,
"skipLibCheck": true
},
"include": ["*.ts"]
}

View File

@ -1,17 +0,0 @@
import * as DBus from 'dbus';
export function createSystemInterface(
svc: DBus.DBusService | string,
objName: string,
ifaceName: string,
) {
const service = ((s: DBus.DBusService | string) => {
if (typeof s === 'string') {
return DBus.registerService('system', s);
}
return s;
})(svc);
const obj = service.createObject(objName);
return obj.createInterface(ifaceName);
}

View File

@ -1,70 +0,0 @@
// TODO: Remove this file when all legacy tests have migrated to unit/integration.
import { stub, SinonStub } from 'sinon';
import * as dbus from 'dbus';
import { Error as DBusError, DBusInterface } from 'dbus';
import { initialized } from '~/lib/dbus';
let getBusStub: SinonStub;
export const mochaHooks = {
async beforeAll() {
getBusStub = stub(dbus, 'getBus').returns({
getInterface: (
serviceName: string,
_objectPath: string,
_interfaceName: string,
interfaceCb: (err: null | DBusError, iface: DBusInterface) => void,
) => {
if (/systemd/.test(serviceName)) {
interfaceCb(null, {
StartUnit: () => {
// noop
},
RestartUnit: () => {
// noop
},
StopUnit: () => {
// noop
},
EnableUnitFiles: () => {
// noop
},
DisableUnitFiles: () => {
// noop
},
GetUnit: (
_unitName: string,
getUnitCb: (err: null | Error, unitPath: string) => void,
) => {
getUnitCb(null, 'this is the unit path');
},
Get: (
_unitName: string,
_property: string,
getCb: (err: null | Error, value: unknown) => void,
) => {
getCb(null, 'this is the value');
},
} as any);
} else {
interfaceCb(null, {
Reboot: () => {
// noop
},
PowerOff: () => {
// noop
},
} as any);
}
},
} as dbus.DBusConnection);
// Initialize dbus module before any tests are run so any further tests
// that interface with lib/dbus use the stubbed busses above.
await initialized();
},
afterAll() {
getBusStub.restore();
},
};

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"baseUrl": "./",
"target": "ES2021",
"target": "ES2022",
"module": "commonjs",
"moduleResolution": "Node16",
"strict": true,

View File

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