mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-03-22 20:16:00 +00:00
commit
f8694bc506
@ -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 \
|
||||
|
54
README.md
54
README.md
@ -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.
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
77
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
166
src/lib/dbus.ts
166
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: <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;
|
||||
}
|
||||
|
@ -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"');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -1 +0,0 @@
|
||||
package-lock=false
|
@ -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"]
|
@ -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
|
@ -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();
|
@ -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"
|
||||
}
|
||||
}
|
@ -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();
|
@ -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"]
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
},
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"target": "ES2021",
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node16",
|
||||
"strict": true,
|
||||
|
@ -19,9 +19,9 @@ var externalModules = [
|
||||
'oracledb',
|
||||
'pg-query-stream',
|
||||
'tedious',
|
||||
'dbus',
|
||||
/mssql\/.*/,
|
||||
'osx-temperature-sensor',
|
||||
'@balena/systemd',
|
||||
];
|
||||
|
||||
let requiredModules = [];
|
||||
|
Loading…
x
Reference in New Issue
Block a user