mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-05-31 23:00:48 +00:00
Setup environment for dbus tests
Change-type: patch
This commit is contained in:
parent
97ec2a4151
commit
819e184095
@ -22,4 +22,5 @@ testfs:
|
|||||||
# when restoring the filesystem
|
# when restoring the filesystem
|
||||||
cleanup:
|
cleanup:
|
||||||
- /data/database.sqlite
|
- /data/database.sqlite
|
||||||
|
- /data/apps.json.preloaded
|
||||||
- /mnt/root/tmp/balena-supervisor/**/*.lock
|
- /mnt/root/tmp/balena-supervisor/**/*.lock
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
version: '2.3'
|
version: '2.3'
|
||||||
|
|
||||||
services:
|
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:
|
sut:
|
||||||
command: sleep infinity
|
command: sleep infinity
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -10,8 +10,23 @@ services:
|
|||||||
command: sleep infinity
|
command: sleep infinity
|
||||||
|
|
||||||
dbus:
|
dbus:
|
||||||
build:
|
image: balenablocks/dbus
|
||||||
context: ./test/lib/dbus/
|
environment:
|
||||||
|
DBUS_CONFIG: session.conf
|
||||||
|
DBUS_ADDRESS: unix:path=/run/dbus/system_bus_socket
|
||||||
|
volumes:
|
||||||
|
- dbus:/run/dbus
|
||||||
|
|
||||||
|
# Fake system service to listen for supervisor
|
||||||
|
# requests
|
||||||
|
dbus-services:
|
||||||
|
build: ./test/lib/dbus
|
||||||
|
depends_on:
|
||||||
|
- dbus
|
||||||
|
volumes:
|
||||||
|
- dbus:/run/dbus
|
||||||
|
environment:
|
||||||
|
DBUS_SYSTEM_BUS_ADDRESS: unix:path=/run/dbus/system_bus_socket
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
image: docker:dind
|
image: docker:dind
|
||||||
@ -28,15 +43,18 @@ services:
|
|||||||
target: test
|
target: test
|
||||||
args:
|
args:
|
||||||
# Change this if testing in another architecture
|
# Change this if testing in another architecture
|
||||||
ARCH: amd64
|
ARCH: ${ARCH:-amd64}
|
||||||
depends_on:
|
depends_on:
|
||||||
- balena-supervisor
|
- balena-supervisor
|
||||||
- docker
|
- docker
|
||||||
- dbus
|
- dbus
|
||||||
|
- dbus-services
|
||||||
|
volumes:
|
||||||
|
- dbus:/run/dbus
|
||||||
# Set required supervisor configuration variables here
|
# Set required supervisor configuration variables here
|
||||||
environment:
|
environment:
|
||||||
DOCKER_HOST: tcp://docker:2375
|
DOCKER_HOST: tcp://docker:2375
|
||||||
DBUS_SYSTEM_BUS_ADDRESS: tcp:host=dbus,port=6667,family=ipv4
|
DBUS_SYSTEM_BUS_ADDRESS: unix:path=/run/dbus/system_bus_socket
|
||||||
# Required by migrations
|
# Required by migrations
|
||||||
CONFIG_MOUNT_POINT: /mnt/root/mnt/boot/config.json
|
CONFIG_MOUNT_POINT: /mnt/root/mnt/boot/config.json
|
||||||
# Read by constants to setup `bootMountpoint`
|
# Read by constants to setup `bootMountpoint`
|
||||||
@ -48,3 +66,10 @@ services:
|
|||||||
tmpfs:
|
tmpfs:
|
||||||
- /data
|
- /data
|
||||||
- /mnt/root
|
- /mnt/root
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dbus:
|
||||||
|
driver_opts:
|
||||||
|
# Use tmpfs to avoid files remaining between runs
|
||||||
|
type: tmpfs
|
||||||
|
device: tmpfs
|
||||||
|
@ -1,134 +1,61 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
import { UpdatesLockedError } from '~/lib/errors';
|
||||||
import { StatusCodeError, UpdatesLockedError } from '~/lib/errors';
|
|
||||||
import * as dockerUtils from '~/lib/docker-utils';
|
|
||||||
import * as config from '~/src/config';
|
|
||||||
import * as imageManager from '~/src/compose/images';
|
|
||||||
import { ConfigTxt } from '~/src/config/backends/config-txt';
|
|
||||||
import * as deviceState from '~/src/device-state';
|
|
||||||
import * as deviceConfig from '~/src/device-config';
|
|
||||||
import { loadTargetFromFile, appsJsonBackup } from '~/src/device-state/preload';
|
|
||||||
import Service from '~/src/compose/service';
|
|
||||||
import { intialiseContractRequirements } from '~/lib/contracts';
|
|
||||||
import * as updateLock from '~/lib/update-lock';
|
|
||||||
import * as fsUtils from '~/lib/fs-utils';
|
import * as fsUtils from '~/lib/fs-utils';
|
||||||
|
import * as updateLock from '~/lib/update-lock';
|
||||||
|
import * as config from '~/src/config';
|
||||||
|
import * as deviceState from '~/src/device-state';
|
||||||
|
import { appsJsonBackup, loadTargetFromFile } from '~/src/device-state/preload';
|
||||||
import { TargetState } from '~/src/types';
|
import { TargetState } from '~/src/types';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { intialiseContractRequirements } from '~/lib/contracts';
|
||||||
|
|
||||||
import * as dbHelper from '~/test-lib/db-helper';
|
import { testfs } from 'mocha-pod';
|
||||||
import log from '~/lib/supervisor-console';
|
import { createDockerImage } from '~/test-lib/docker-helper';
|
||||||
|
import * as Docker from 'dockerode';
|
||||||
const mockedInitialConfig = {
|
|
||||||
RESIN_SUPERVISOR_CONNECTIVITY_CHECK: 'true',
|
|
||||||
RESIN_SUPERVISOR_DELTA: 'false',
|
|
||||||
RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT: '0',
|
|
||||||
RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT: '30000',
|
|
||||||
RESIN_SUPERVISOR_DELTA_RETRY_COUNT: '30',
|
|
||||||
RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL: '10000',
|
|
||||||
RESIN_SUPERVISOR_DELTA_VERSION: '2',
|
|
||||||
RESIN_SUPERVISOR_INSTANT_UPDATE_TRIGGER: 'true',
|
|
||||||
RESIN_SUPERVISOR_LOCAL_MODE: 'false',
|
|
||||||
RESIN_SUPERVISOR_LOG_CONTROL: 'true',
|
|
||||||
RESIN_SUPERVISOR_OVERRIDE_LOCK: 'false',
|
|
||||||
RESIN_SUPERVISOR_POLL_INTERVAL: '60000',
|
|
||||||
RESIN_SUPERVISOR_VPN_CONTROL: 'true',
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('device-state', () => {
|
describe('device-state', () => {
|
||||||
const originalImagesSave = imageManager.save;
|
const docker = new Docker();
|
||||||
const originalImagesInspect = imageManager.inspectByName;
|
|
||||||
const originalGetCurrent = deviceConfig.getCurrent;
|
|
||||||
|
|
||||||
let testDb: dbHelper.TestDatabase;
|
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
testDb = await dbHelper.createDB();
|
|
||||||
|
|
||||||
await config.initialized();
|
await config.initialized();
|
||||||
|
|
||||||
// Prevent side effects from changes in config
|
|
||||||
sinon.stub(config, 'on');
|
|
||||||
|
|
||||||
// Set the device uuid
|
// Set the device uuid
|
||||||
await config.set({ uuid: 'local' });
|
await config.set({ uuid: 'local' });
|
||||||
|
|
||||||
await deviceState.initialized();
|
|
||||||
|
|
||||||
// disable log output during testing
|
|
||||||
sinon.stub(log, 'debug');
|
|
||||||
sinon.stub(log, 'warn');
|
|
||||||
sinon.stub(log, 'info');
|
|
||||||
sinon.stub(log, 'event');
|
|
||||||
sinon.stub(log, 'success');
|
|
||||||
|
|
||||||
// TODO: all these stubs are internal implementation details of
|
|
||||||
// deviceState, we should refactor deviceState to use dependency
|
|
||||||
// injection instead of initializing everything in memory
|
|
||||||
sinon.stub(Service as any, 'extendEnvVars').callsFake((env: any) => {
|
|
||||||
env['ADDITIONAL_ENV_VAR'] = 'foo';
|
|
||||||
return env;
|
|
||||||
});
|
|
||||||
|
|
||||||
intialiseContractRequirements({
|
intialiseContractRequirements({
|
||||||
supervisorVersion: '11.0.0',
|
supervisorVersion: '11.0.0',
|
||||||
deviceType: 'intel-nuc',
|
deviceType: 'intel-nuc',
|
||||||
});
|
});
|
||||||
|
|
||||||
sinon
|
|
||||||
.stub(dockerUtils, 'getNetworkGateway')
|
|
||||||
.returns(Promise.resolve('172.17.0.1'));
|
|
||||||
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
imageManager.cleanImageData = () => {
|
|
||||||
console.log('Cleanup database called');
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
imageManager.save = () => Promise.resolve();
|
|
||||||
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
imageManager.inspectByName = () => {
|
|
||||||
const err: StatusCodeError = new Error();
|
|
||||||
err.statusCode = 404;
|
|
||||||
return Promise.reject(err);
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
deviceConfig.configBackend = new ConfigTxt();
|
|
||||||
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
deviceConfig.getCurrent = async () => mockedInitialConfig;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
(Service as any).extendEnvVars.restore();
|
await docker.pruneImages({ filters: { dangling: { false: true } } });
|
||||||
(dockerUtils.getNetworkGateway as sinon.SinonStub).restore();
|
|
||||||
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
imageManager.save = originalImagesSave;
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
imageManager.inspectByName = originalImagesInspect;
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
|
||||||
deviceConfig.getCurrent = originalGetCurrent;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await testDb.destroy();
|
|
||||||
} catch {
|
|
||||||
/* noop */
|
|
||||||
}
|
|
||||||
sinon.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await testDb.reset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads a target state from an apps.json file and saves it as target state, then returns it', async () => {
|
it('loads a target state from an apps.json file and saves it as target state, then returns it', async () => {
|
||||||
const appsJson = process.env.ROOT_MOUNTPOINT + '/apps.json';
|
const localFs = await testfs(
|
||||||
|
{ '/data/apps.json': testfs.from('test/data/apps.json') },
|
||||||
|
{ cleanup: ['/data/apps.json.preloaded'] },
|
||||||
|
).enable();
|
||||||
|
|
||||||
|
// The image needs to exist before the test
|
||||||
|
const dockerImageId = await createDockerImage(
|
||||||
|
'registry2.resin.io/superapp/abcdef:latest',
|
||||||
|
['io.balena.testing=1'],
|
||||||
|
docker,
|
||||||
|
);
|
||||||
|
|
||||||
|
const appsJson = '/data/apps.json';
|
||||||
|
await expect(
|
||||||
|
fs.access(appsJson),
|
||||||
|
'apps.json exists before loading the target',
|
||||||
|
).to.not.be.rejected;
|
||||||
await loadTargetFromFile(appsJson);
|
await loadTargetFromFile(appsJson);
|
||||||
const targetState = await deviceState.getTarget();
|
const targetState = await deviceState.getTarget();
|
||||||
expect(await fsUtils.exists(appsJsonBackup(appsJson))).to.be.true;
|
// console.log('TARGET', JSON.stringify(targetState, null, 2));
|
||||||
|
await expect(
|
||||||
|
fs.access(appsJsonBackup(appsJson)),
|
||||||
|
'apps.json.preloaded is created after loading the target',
|
||||||
|
).to.not.be.rejected;
|
||||||
expect(targetState)
|
expect(targetState)
|
||||||
.to.have.property('local')
|
.to.have.property('local')
|
||||||
.that.has.property('config')
|
.that.has.property('config')
|
||||||
@ -145,58 +72,31 @@ describe('device-state', () => {
|
|||||||
expect(app.services[0])
|
expect(app.services[0])
|
||||||
.to.have.property('config')
|
.to.have.property('config')
|
||||||
.that.has.property('image')
|
.that.has.property('image')
|
||||||
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
.that.equals(dockerImageId);
|
||||||
expect(app.services[0].config)
|
|
||||||
.to.have.property('labels')
|
|
||||||
.that.has.property('io.balena.something')
|
|
||||||
.that.equals('bar');
|
|
||||||
expect(app).to.have.property('appName').that.equals('superapp');
|
|
||||||
expect(app).to.have.property('services').that.is.an('array').with.length(1);
|
|
||||||
expect(app.services[0])
|
|
||||||
.to.have.property('config')
|
|
||||||
.that.has.property('image')
|
|
||||||
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
|
||||||
expect(app.services[0].config)
|
|
||||||
.to.have.property('labels')
|
|
||||||
.that.has.property('io.balena.something')
|
|
||||||
.that.equals('bar');
|
|
||||||
expect(app).to.have.property('appName').that.equals('superapp');
|
|
||||||
expect(app).to.have.property('services').that.is.an('array').with.length(1);
|
|
||||||
expect(app.services[0])
|
|
||||||
.to.have.property('config')
|
|
||||||
.that.has.property('image')
|
|
||||||
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
|
||||||
expect(app.services[0].config)
|
|
||||||
.to.have.property('labels')
|
|
||||||
.that.has.property('io.balena.something')
|
|
||||||
.that.equals('bar');
|
|
||||||
expect(app).to.have.property('appName').that.equals('superapp');
|
|
||||||
expect(app).to.have.property('services').that.is.an('array').with.length(1);
|
|
||||||
expect(app.services[0])
|
|
||||||
.to.have.property('config')
|
|
||||||
.that.has.property('image')
|
|
||||||
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
|
||||||
expect(app.services[0].config)
|
|
||||||
.to.have.property('labels')
|
|
||||||
.that.has.property('io.balena.something')
|
|
||||||
.that.equals('bar');
|
|
||||||
expect(app).to.have.property('appName').that.equals('superapp');
|
|
||||||
expect(app).to.have.property('services').that.is.an('array').with.length(1);
|
|
||||||
expect(app.services[0])
|
|
||||||
.to.have.property('config')
|
|
||||||
.that.has.property('image')
|
|
||||||
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
|
||||||
expect(app.services[0].config)
|
expect(app.services[0].config)
|
||||||
.to.have.property('labels')
|
.to.have.property('labels')
|
||||||
.that.has.property('io.balena.something')
|
.that.has.property('io.balena.something')
|
||||||
.that.equals('bar');
|
.that.equals('bar');
|
||||||
|
|
||||||
// Restore renamed apps.json
|
// Remove the image
|
||||||
await fsUtils.safeRename(appsJsonBackup(appsJson), appsJson);
|
await docker.getImage(dockerImageId).remove();
|
||||||
|
await localFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stores info for pinning a device after loading an apps.json with a pinDevice field', async () => {
|
it('stores info for pinning a device after loading an apps.json with a pinDevice field', async () => {
|
||||||
const appsJson = process.env.ROOT_MOUNTPOINT + '/apps-pin.json';
|
const localFs = await testfs(
|
||||||
|
{ '/data/apps.json': testfs.from('test/data/apps-pin.json') },
|
||||||
|
{ cleanup: ['/data/apps.json.preloaded'] },
|
||||||
|
).enable();
|
||||||
|
|
||||||
|
// The image needs to exist before the test
|
||||||
|
const dockerImageId = await createDockerImage(
|
||||||
|
'registry2.resin.io/superapp/abcdef:latest',
|
||||||
|
['io.balena.testing=1'],
|
||||||
|
docker,
|
||||||
|
);
|
||||||
|
|
||||||
|
const appsJson = '/data/apps.json';
|
||||||
await loadTargetFromFile(appsJson);
|
await loadTargetFromFile(appsJson);
|
||||||
|
|
||||||
const pinned = await config.get('pinDevice');
|
const pinned = await config.get('pinDevice');
|
||||||
@ -204,12 +104,12 @@ describe('device-state', () => {
|
|||||||
expect(pinned).to.have.property('commit').that.equals('abcdef');
|
expect(pinned).to.have.property('commit').that.equals('abcdef');
|
||||||
expect(await fsUtils.exists(appsJsonBackup(appsJson))).to.be.true;
|
expect(await fsUtils.exists(appsJsonBackup(appsJson))).to.be.true;
|
||||||
|
|
||||||
// Restore renamed apps.json
|
// Remove the image
|
||||||
await fsUtils.safeRename(appsJsonBackup(appsJson), appsJson);
|
await docker.getImage(dockerImageId).remove();
|
||||||
|
await localFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits a change event when a new state is reported', (done) => {
|
it('emits a change event when a new state is reported', (done) => {
|
||||||
// TODO: where is the test on this test?
|
|
||||||
deviceState.once('change', done);
|
deviceState.once('change', done);
|
||||||
deviceState.reportCurrentState({ someStateDiff: 'someValue' } as any);
|
deviceState.reportCurrentState({ someStateDiff: 'someValue' } as any);
|
||||||
});
|
});
|
1
test/lib/dbus/.npmrc
Normal file
1
test/lib/dbus/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
package-lock=false
|
@ -1,10 +1,10 @@
|
|||||||
FROM ubuntu:20.04
|
FROM node:16-alpine
|
||||||
|
|
||||||
# Install Systemd
|
RUN apk add --update python3 dbus-dev make g++ libgcc
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
dbus \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY dbus.conf /etc/dbus-1/session.d/
|
WORKDIR /usr/src/app
|
||||||
|
COPY package.json *.ts tsconfig.json entry.sh ./
|
||||||
|
|
||||||
ENTRYPOINT ["dbus-run-session", "sleep", "infinity"]
|
RUN npm install && npm run build
|
||||||
|
|
||||||
|
CMD ["./entry.sh"]
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
|
|
||||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
|
||||||
|
|
||||||
<busconfig>
|
|
||||||
<listen>tcp:host=localhost,bind=*,port=6667,family=ipv4</listen>
|
|
||||||
<listen>unix:tmpdir=/tmp</listen>
|
|
||||||
<auth>ANONYMOUS</auth>
|
|
||||||
<allow_anonymous/>
|
|
||||||
</busconfig>
|
|
13
test/lib/dbus/entry.sh
Executable file
13
test/lib/dbus/entry.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/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
|
66
test/lib/dbus/login.ts
Normal file
66
test/lib/dbus/login.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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();
|
20
test/lib/dbus/package.json
Normal file
20
test/lib/dbus/package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
173
test/lib/dbus/systemd.ts
Normal file
173
test/lib/dbus/systemd.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
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();
|
14
test/lib/dbus/tsconfig.json
Normal file
14
test/lib/dbus/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "es2019",
|
||||||
|
"declaration": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["*.ts"]
|
||||||
|
}
|
17
test/lib/dbus/utils.ts
Normal file
17
test/lib/dbus/utils.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user