@lakutata/dbus 2.3.1 → 3.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +253 -25
- package/dist/cjs/adapters/DBusEntrypoint.d.ts +89 -0
- package/dist/cjs/adapters/DBusEntrypoint.js +280 -0
- package/dist/cjs/channel/DBusChannel.d.ts +36 -0
- package/dist/cjs/channel/DBusChannel.js +70 -0
- package/dist/cjs/components/DBusClientComponent.d.ts +494 -0
- package/dist/cjs/components/DBusClientComponent.js +272 -0
- package/dist/cjs/components/DBusObjectManager.d.ts +164 -0
- package/dist/cjs/components/DBusObjectManager.js +200 -0
- package/dist/cjs/context/DBusContext.d.ts +52 -0
- package/dist/cjs/context/DBusContext.js +68 -0
- package/dist/cjs/decorators/DBusInterface.d.ts +74 -0
- package/dist/cjs/decorators/DBusInterface.js +63 -0
- package/dist/cjs/decorators/DBusMethod.d.ts +115 -0
- package/dist/cjs/decorators/DBusMethod.js +20 -0
- package/dist/cjs/exports/DBus.d.ts +11 -0
- package/dist/cjs/exports/DBus.js +42 -0
- package/dist/cjs/lib/DBusClientOptions.d.ts +30 -0
- package/dist/cjs/lib/Declarations.d.ts +187 -0
- package/dist/cjs/lib/Declarations.js +113 -0
- package/dist/cjs/lib/Names.d.ts +35 -0
- package/dist/cjs/lib/Names.js +80 -0
- package/dist/cjs/lib/Signature.d.ts +14 -0
- package/dist/cjs/lib/Signature.js +53 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types/DBusContextParams.d.ts +22 -0
- package/dist/cjs/types/DBusContextParams.js +2 -0
- package/dist/esm/adapters/DBusEntrypoint.js +274 -0
- package/dist/esm/channel/DBusChannel.js +67 -0
- package/dist/esm/components/DBusClientComponent.js +267 -0
- package/dist/esm/components/DBusObjectManager.js +197 -0
- package/dist/esm/context/DBusContext.js +64 -0
- package/dist/esm/decorators/DBusInterface.js +60 -0
- package/dist/esm/decorators/DBusMethod.js +17 -0
- package/dist/esm/exports/DBus.js +12 -0
- package/dist/esm/lib/DBusClientOptions.js +1 -0
- package/dist/esm/lib/Declarations.js +107 -0
- package/dist/esm/lib/Names.js +73 -0
- package/dist/esm/lib/Signature.js +49 -0
- package/dist/esm/types/DBusContextParams.js +1 -0
- package/dist/types/adapters/DBusEntrypoint.d.ts +89 -0
- package/dist/types/channel/DBusChannel.d.ts +36 -0
- package/dist/types/components/DBusClientComponent.d.ts +494 -0
- package/dist/types/components/DBusObjectManager.d.ts +164 -0
- package/dist/types/context/DBusContext.d.ts +52 -0
- package/dist/types/decorators/DBusInterface.d.ts +74 -0
- package/dist/types/decorators/DBusMethod.d.ts +115 -0
- package/dist/types/exports/DBus.d.ts +11 -0
- package/dist/types/lib/DBusClientOptions.d.ts +30 -0
- package/dist/types/lib/Declarations.d.ts +187 -0
- package/dist/types/lib/Names.d.ts +35 -0
- package/dist/types/lib/Signature.d.ts +14 -0
- package/dist/types/types/DBusContextParams.d.ts +22 -0
- package/package.json +38 -39
- package/CHANGELOG.md +0 -153
- package/dist/CommonExports.d.ts +0 -2
- package/dist/CommonExports.d.ts.map +0 -1
- package/dist/CommonExports.js +0 -44
- package/dist/ComponentExports.d.ts +0 -2
- package/dist/ComponentExports.d.ts.map +0 -1
- package/dist/ComponentExports.js +0 -5
- package/dist/ProviderExports.d.ts +0 -2
- package/dist/ProviderExports.d.ts.map +0 -1
- package/dist/ProviderExports.js +0 -5
- package/dist/components/DBusClientComponent.d.ts +0 -75
- package/dist/components/DBusClientComponent.d.ts.map +0 -1
- package/dist/components/DBusClientComponent.js +0 -125
- package/dist/interfaces/DBusClientOptions.d.ts +0 -7
- package/dist/interfaces/DBusClientOptions.d.ts.map +0 -1
- package/dist/providers/DBusClientProvider.d.ts +0 -75
- package/dist/providers/DBusClientProvider.d.ts.map +0 -1
- package/dist/providers/DBusClientProvider.js +0 -125
- package/dist/tests/DBusTest.spec.d.ts +0 -2
- package/dist/tests/DBusTest.spec.d.ts.map +0 -1
- package/dist/tests/DBusTest.spec.js +0 -26
- package/dist/tests/Test.d.ts +0 -9
- package/dist/tests/Test.d.ts.map +0 -1
- package/dist/tests/Test.js +0 -80
- package/tsconfig.json +0 -7
- /package/dist/{interfaces → cjs/lib}/DBusClientOptions.js +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
2
|
+
import { FlexibleDTO } from '@lakutata/core/dtos';
|
|
3
|
+
import { LocalInterface, LocalObject, LocalService, RequestNameFlags, RequestNameResultCode } from 'dbus-sdk';
|
|
4
|
+
import { DBusChannel } from '../channel/DBusChannel.js';
|
|
5
|
+
import { DBusContext } from '../context/DBusContext.js';
|
|
6
|
+
import { ATTACH, DBusObjectManager, STORE } from '../components/DBusObjectManager.js';
|
|
7
|
+
import { getDBusInterfaceDeclaration } from '../lib/Declarations.js';
|
|
8
|
+
import { checkInterfaceName, compilePathTemplate, errorName } from '../lib/Names.js';
|
|
9
|
+
import { toEncodable, schemaSignature } from '../lib/Signature.js';
|
|
10
|
+
/**
|
|
11
|
+
* The connection options of a bus
|
|
12
|
+
* @param bus
|
|
13
|
+
*/
|
|
14
|
+
function connectOptions(bus) {
|
|
15
|
+
if (bus === 'session')
|
|
16
|
+
return { busAddress: process.env.DBUS_SESSION_BUS_ADDRESS };
|
|
17
|
+
if (bus === 'system')
|
|
18
|
+
return { busAddress: process.env.DBUS_SYSTEM_BUS_ADDRESS ?? 'unix:path=/var/run/dbus/system_bus_socket' };
|
|
19
|
+
if (/^[a-z]+:/.test(bus))
|
|
20
|
+
return { busAddress: bus };
|
|
21
|
+
return { socket: bus };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The error given to the caller of a method
|
|
25
|
+
* @param error
|
|
26
|
+
*/
|
|
27
|
+
function toMethodError(error) {
|
|
28
|
+
const { errMsg, message } = (error && typeof error === 'object' ? error : {});
|
|
29
|
+
const methodError = new Error(typeof errMsg === 'string' && errMsg ? errMsg : typeof message === 'string' ? message : String(error));
|
|
30
|
+
methodError.name = errorName(error);
|
|
31
|
+
return methodError;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the D-Bus adapter of the entrypoint (its `dbus` option): the application owns a service name on a bus and
|
|
35
|
+
* serves the controllers declared with `@DBusInterface` as the interfaces of its objects, their `@DBusMethod` actions
|
|
36
|
+
* as the methods. The objects of the paths without variables are exported when the adapter starts, until it stops
|
|
37
|
+
* ({@link DBusObjectManager.removeObject} does not remove them); the objects of the path templates are exported when
|
|
38
|
+
* they are added with {@link DBusObjectManager.addObject}, before or after the start.
|
|
39
|
+
*
|
|
40
|
+
* Starting connects to the bus and requests the name. The adapter stops gracefully with the entrypoint: it releases the
|
|
41
|
+
* name (no new call reaches it), the calls in progress end, then the connection closes. It needs a D-Bus daemon, as on
|
|
42
|
+
* Linux: the system bus, a session bus, or a private `dbus-daemon`.
|
|
43
|
+
* @param options The bus, the service name and how it is owned, the adapter's middlewares.
|
|
44
|
+
* @returns The adapter, to give to the `dbus` option of `buildEntrypoints()`.
|
|
45
|
+
* @throws {TypeError} At once, when the service name is invalid; when the adapter starts, when an action declared with
|
|
46
|
+
* `@DBusMethod` belongs to a controller without `@DBusInterface`.
|
|
47
|
+
* @throws {Error} When the adapter starts, if the bus cannot be reached, or if another connection owns the name
|
|
48
|
+
* (`The DBus name … is owned by another service`): the entrypoint fails to start.
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* import {Application} from 'lakutata'
|
|
52
|
+
* import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
53
|
+
* import {dbusEntrypoint, DBusInterface, DBusMethod} from '@lakutata/dbus'
|
|
54
|
+
*
|
|
55
|
+
* @DBusInterface('com.example.Manager', {path: '/com/example/Manager'})
|
|
56
|
+
* class ManagerController extends Controller {
|
|
57
|
+
* @DBusMethod('Ping', {returns: 's'})
|
|
58
|
+
* public async ping(): Promise<string> {
|
|
59
|
+
* return 'pong'
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* //busctl --system call com.example.DeviceService /com/example/Manager com.example.Manager Ping
|
|
64
|
+
* Application.run(() => ({
|
|
65
|
+
* id: 'devices.app',
|
|
66
|
+
* name: 'Devices',
|
|
67
|
+
* components: {
|
|
68
|
+
* entrypoint: buildEntrypoints({
|
|
69
|
+
* controllers: [ManagerController],
|
|
70
|
+
* dbus: dbusEntrypoint({bus: 'system', service: 'com.example.DeviceService'})
|
|
71
|
+
* })
|
|
72
|
+
* },
|
|
73
|
+
* bootstrap: ['entrypoint']
|
|
74
|
+
* }))
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function dbusEntrypoint(options) {
|
|
78
|
+
checkInterfaceName(options.service);
|
|
79
|
+
let service;
|
|
80
|
+
let detach;
|
|
81
|
+
const exported = new Map();
|
|
82
|
+
/**
|
|
83
|
+
* The interfaces of the controllers and of the actions, by path template
|
|
84
|
+
* @param runtime
|
|
85
|
+
*/
|
|
86
|
+
const collect = (runtime) => {
|
|
87
|
+
const templates = new Map();
|
|
88
|
+
const interfacesAt = (path, name) => {
|
|
89
|
+
let template = templates.get(path);
|
|
90
|
+
if (!template) {
|
|
91
|
+
template = { template: compilePathTemplate(path), interfaces: new Map() };
|
|
92
|
+
templates.set(path, template);
|
|
93
|
+
}
|
|
94
|
+
let interfaces = template.interfaces.get(name);
|
|
95
|
+
if (!interfaces) {
|
|
96
|
+
interfaces = { methods: [] };
|
|
97
|
+
template.interfaces.set(name, interfaces);
|
|
98
|
+
}
|
|
99
|
+
return interfaces;
|
|
100
|
+
};
|
|
101
|
+
for (const controller of runtime.controllers) {
|
|
102
|
+
const declaration = getDBusInterfaceDeclaration(controller);
|
|
103
|
+
if (declaration)
|
|
104
|
+
interfacesAt(declaration.path, declaration.interface).declaration = declaration;
|
|
105
|
+
}
|
|
106
|
+
for (const action of runtime.actions) {
|
|
107
|
+
const { path, interface: interfaceName, member } = action.address;
|
|
108
|
+
if (path === undefined || interfaceName === undefined)
|
|
109
|
+
throw new TypeError(`The DBus method ${member} of ${action.controller} has no interface: declare its controller with @DBusInterface`);
|
|
110
|
+
interfacesAt(path, interfaceName).methods.push(action);
|
|
111
|
+
}
|
|
112
|
+
return templates;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The signature of the result of a method: its returns option, or the schema of @Return
|
|
116
|
+
* @param action
|
|
117
|
+
*/
|
|
118
|
+
const resultSignature = (action) => {
|
|
119
|
+
if (action.address.returns !== undefined)
|
|
120
|
+
return action.address.returns;
|
|
121
|
+
return action.returnSchema ? schemaSignature(action.returnSchema) : undefined;
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
channel: 'dbus',
|
|
125
|
+
definition: DBusChannel,
|
|
126
|
+
name: options.name,
|
|
127
|
+
middlewares: options.middlewares,
|
|
128
|
+
start: async (runtime) => {
|
|
129
|
+
const templates = collect(runtime);
|
|
130
|
+
const manager = await runtime.getObject(DBusObjectManager);
|
|
131
|
+
const localService = new LocalService(options.service);
|
|
132
|
+
service = localService;
|
|
133
|
+
/**
|
|
134
|
+
* Build the interface of an object
|
|
135
|
+
* @param path
|
|
136
|
+
* @param name
|
|
137
|
+
* @param served
|
|
138
|
+
* @param variables
|
|
139
|
+
* @param object
|
|
140
|
+
*/
|
|
141
|
+
const buildInterface = (path, name, served, variables, object) => {
|
|
142
|
+
const localInterface = new LocalInterface(name);
|
|
143
|
+
for (const action of served.methods) {
|
|
144
|
+
const returns = resultSignature(action);
|
|
145
|
+
const hasInput = action.dto !== FlexibleDTO;
|
|
146
|
+
localInterface.defineMethod({
|
|
147
|
+
name: action.address.member,
|
|
148
|
+
inputArgs: hasInput ? [{ name: 'input', type: 'a{sv}' }] : [],
|
|
149
|
+
outputArgs: returns ? [{ name: 'output', type: returns }] : [],
|
|
150
|
+
method: async (input) => {
|
|
151
|
+
const context = new DBusContext({ path: path, interface: name, member: action.address.member, data: input && typeof input === 'object' ? input : {} });
|
|
152
|
+
let result;
|
|
153
|
+
try {
|
|
154
|
+
result = await runtime.dispatchTo(action, context, { params: variables });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw toMethodError(error);
|
|
158
|
+
}
|
|
159
|
+
if (!returns)
|
|
160
|
+
return undefined;
|
|
161
|
+
return toEncodable(returns === 'a{sv}' && (result === undefined || result === null) ? {} : result);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const property of served.declaration?.properties ?? []) {
|
|
166
|
+
localInterface.defineProperty({
|
|
167
|
+
name: property.name,
|
|
168
|
+
type: property.type,
|
|
169
|
+
getter: () => toEncodable(manager.getPropertyValue(path, property)),
|
|
170
|
+
//A value written by a client is validated synchronously: its errors are given to the client
|
|
171
|
+
setter: property.writable ? (value) => {
|
|
172
|
+
let validated;
|
|
173
|
+
try {
|
|
174
|
+
validated = property.validate(value);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
//The errors given to DBus are plain errors (the name of an exception is read only)
|
|
178
|
+
throw toMethodError(error);
|
|
179
|
+
}
|
|
180
|
+
manager[STORE]({ path: path, interface: name, property: property, value: validated });
|
|
181
|
+
} : undefined,
|
|
182
|
+
emitPropertiesChanged: property.writable
|
|
183
|
+
});
|
|
184
|
+
object.properties.set(property, [...(object.properties.get(property) ?? []), name]);
|
|
185
|
+
}
|
|
186
|
+
for (const signal of served.declaration?.signals ?? []) {
|
|
187
|
+
const emitter = new EventEmitter();
|
|
188
|
+
localInterface.defineSignal({ name: signal.name, args: [{ name: 'data', type: signal.type }], eventEmitter: emitter });
|
|
189
|
+
const signals = object.signals.get(signal.name) ?? [];
|
|
190
|
+
signals.push({ signal: signal, emitter: emitter });
|
|
191
|
+
object.signals.set(signal.name, signals);
|
|
192
|
+
}
|
|
193
|
+
return localInterface;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Export an object
|
|
197
|
+
* @param path
|
|
198
|
+
*/
|
|
199
|
+
const addObject = (path) => {
|
|
200
|
+
if (exported.has(path))
|
|
201
|
+
return true;
|
|
202
|
+
const object = { object: new LocalObject(path), signals: new Map(), properties: new Map() };
|
|
203
|
+
let served = false;
|
|
204
|
+
for (const { template, interfaces } of templates.values()) {
|
|
205
|
+
const variables = template.match(path);
|
|
206
|
+
if (!variables)
|
|
207
|
+
continue;
|
|
208
|
+
interfaces.forEach((interfaceServed, name) => object.object.addInterface(buildInterface(path, name, interfaceServed, variables, object)));
|
|
209
|
+
served = true;
|
|
210
|
+
}
|
|
211
|
+
if (!served)
|
|
212
|
+
return false;
|
|
213
|
+
exported.set(path, object);
|
|
214
|
+
localService.addObject(object.object);
|
|
215
|
+
return true;
|
|
216
|
+
};
|
|
217
|
+
//The objects of the paths without variables are exported at once, and kept until the adapter closes
|
|
218
|
+
const fixed = new Set();
|
|
219
|
+
for (const { template } of templates.values()) {
|
|
220
|
+
if (!template.static)
|
|
221
|
+
continue;
|
|
222
|
+
fixed.add(template.template);
|
|
223
|
+
addObject(template.template);
|
|
224
|
+
}
|
|
225
|
+
const connect = connectOptions(options.bus ?? 'session');
|
|
226
|
+
let flags = RequestNameFlags.DBUS_NAME_FLAG_DO_NOT_QUEUE;
|
|
227
|
+
if (options.replaceExisting)
|
|
228
|
+
flags |= RequestNameFlags.DBUS_NAME_FLAG_REPLACE_EXISTING;
|
|
229
|
+
if (options.allowReplacement)
|
|
230
|
+
flags |= RequestNameFlags.DBUS_NAME_FLAG_ALLOW_REPLACEMENT;
|
|
231
|
+
const result = await localService.run({ ...connect, flags: flags });
|
|
232
|
+
if (result !== RequestNameResultCode.DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER && result !== RequestNameResultCode.DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER) {
|
|
233
|
+
await localService.stop().catch(() => undefined);
|
|
234
|
+
throw new Error(`The DBus name ${options.service} is owned by another service`);
|
|
235
|
+
}
|
|
236
|
+
const sink = {
|
|
237
|
+
addObject: addObject,
|
|
238
|
+
removeObject: (path) => {
|
|
239
|
+
if (fixed.has(path) || !exported.delete(path))
|
|
240
|
+
return false;
|
|
241
|
+
return localService.removeObject(path);
|
|
242
|
+
},
|
|
243
|
+
isFixed: (path) => fixed.has(path),
|
|
244
|
+
emitSignal: (path, signal, data) => {
|
|
245
|
+
const emitters = exported.get(path)?.signals.get(signal.name)?.filter((candidate) => candidate.signal === signal);
|
|
246
|
+
if (!emitters?.length)
|
|
247
|
+
return false;
|
|
248
|
+
emitters.forEach(({ emitter }) => emitter.emit(signal.name, toEncodable(data)));
|
|
249
|
+
return true;
|
|
250
|
+
},
|
|
251
|
+
propertyChanged: (path, property) => {
|
|
252
|
+
const object = exported.get(path);
|
|
253
|
+
const interfaces = object?.properties.get(property);
|
|
254
|
+
if (!object || !interfaces)
|
|
255
|
+
return;
|
|
256
|
+
//The interfaces read the new value with their getters (the manager stored it), typed by the property
|
|
257
|
+
interfaces.forEach((name) => object.object.getInterface(name)?.emitPropertiesChanged(property.name));
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
detach = manager[ATTACH](sink);
|
|
261
|
+
},
|
|
262
|
+
stop: async () => {
|
|
263
|
+
//The name is released: no new call reaches the service, the calls in progress end
|
|
264
|
+
detach?.();
|
|
265
|
+
detach = undefined;
|
|
266
|
+
await service?.dbus?.releaseName(options.service).catch(() => undefined);
|
|
267
|
+
},
|
|
268
|
+
close: async () => {
|
|
269
|
+
await service?.dbus?.disconnect().catch(() => undefined);
|
|
270
|
+
exported.clear();
|
|
271
|
+
service = undefined;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ControllerActionNotFoundException } from '@lakutata/entrypoint';
|
|
2
|
+
import { defineChannel, getChannel, hasChannel } from '@lakutata/entrypoint/channel';
|
|
3
|
+
import { DBUS_CONTEXT_TYPE } from '../context/DBusContext.js';
|
|
4
|
+
import { checkInterfaceName, checkMemberName, checkPathTemplate, compilePathTemplate } from '../lib/Names.js';
|
|
5
|
+
/**
|
|
6
|
+
* The key of the address of a method
|
|
7
|
+
* @param interfaceName
|
|
8
|
+
* @param member
|
|
9
|
+
* @param path
|
|
10
|
+
*/
|
|
11
|
+
function addressKey(interfaceName, member, path) {
|
|
12
|
+
return `${interfaceName}.${member}@${path}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The definition of the D-Bus channel of the entrypoint, named `dbus`: its actions are the methods of D-Bus interfaces,
|
|
16
|
+
* addressed by `{member, interface, path}` (the method name from `@DBusMethod`, the interface and the path template from
|
|
17
|
+
* `@DBusInterface`). A call matches an action by its interface, its method and its object path; a call matching none
|
|
18
|
+
* fails with `ControllerActionNotFoundException`. `@DBusMethod` and {@link dbusEntrypoint} use it: an application only
|
|
19
|
+
* needs it to write an adapter of its own.
|
|
20
|
+
*/
|
|
21
|
+
export const DBusChannel = hasChannel('dbus') ? getChannel('dbus') : defineChannel({
|
|
22
|
+
name: 'dbus',
|
|
23
|
+
kind: 'request',
|
|
24
|
+
contextType: DBUS_CONTEXT_TYPE,
|
|
25
|
+
validate: (pattern) => {
|
|
26
|
+
checkMemberName(pattern.member, 'method');
|
|
27
|
+
if (pattern.interface !== undefined)
|
|
28
|
+
checkInterfaceName(pattern.interface);
|
|
29
|
+
if (pattern.path !== undefined)
|
|
30
|
+
checkPathTemplate(pattern.path);
|
|
31
|
+
return pattern;
|
|
32
|
+
},
|
|
33
|
+
identify: (pattern) => addressKey(pattern.interface ?? '?', pattern.member, pattern.path ?? '?'),
|
|
34
|
+
describe: (pattern) => {
|
|
35
|
+
const { returns, ...address } = pattern;
|
|
36
|
+
return returns === undefined ? address : { ...address, returns: returns };
|
|
37
|
+
},
|
|
38
|
+
prefix: (prefix, pattern) => {
|
|
39
|
+
const dbus = prefix.dbus;
|
|
40
|
+
if (!dbus || (pattern.path !== undefined && pattern.interface !== undefined))
|
|
41
|
+
return undefined;
|
|
42
|
+
return { ...pattern, path: pattern.path ?? dbus.path, interface: pattern.interface ?? dbus.interface };
|
|
43
|
+
},
|
|
44
|
+
createMatcher: (entries) => {
|
|
45
|
+
const templates = entries
|
|
46
|
+
.filter((entry) => entry.details.pattern.path !== undefined)
|
|
47
|
+
.map((entry) => ({ entry: entry, template: compilePathTemplate(entry.details.pattern.path) }));
|
|
48
|
+
return {
|
|
49
|
+
match: (context) => {
|
|
50
|
+
const { path, interface: interfaceName, member } = context;
|
|
51
|
+
for (const { entry, template } of templates) {
|
|
52
|
+
if (entry.details.pattern.interface !== interfaceName || entry.details.pattern.member !== member)
|
|
53
|
+
continue;
|
|
54
|
+
const params = template.match(path);
|
|
55
|
+
if (params)
|
|
56
|
+
return { entry: entry, params: params };
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
notFound: (context) => new ControllerActionNotFoundException('The DBus method {member} of the interface {interface} of {path} is not found', {
|
|
63
|
+
member: context.member,
|
|
64
|
+
interface: context.interface,
|
|
65
|
+
path: context.path
|
|
66
|
+
})
|
|
67
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { DBus } from 'dbus-sdk';
|
|
11
|
+
import { Component, DTO } from '@lakutata/core';
|
|
12
|
+
import { Configurable } from '@lakutata/core/decorator/di';
|
|
13
|
+
/**
|
|
14
|
+
* Build the declaration of a {@link DBusClientComponent} for the `components` of an application or a module, from its
|
|
15
|
+
* typed options.
|
|
16
|
+
* @param options The bus address and the connection options.
|
|
17
|
+
* @returns The declaration: the options with `class: DBusClientComponent`.
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import {Application} from 'lakutata'
|
|
21
|
+
* import {buildDBusClientOptions, DBusClientComponent} from '@lakutata/dbus'
|
|
22
|
+
*
|
|
23
|
+
* Application
|
|
24
|
+
* .run(() => ({
|
|
25
|
+
* id: 'client.app',
|
|
26
|
+
* name: 'Client',
|
|
27
|
+
* components: {
|
|
28
|
+
* dbus: buildDBusClientOptions({busAddress: 'unix:path=/var/run/dbus/system_bus_socket', timeout: 5000})
|
|
29
|
+
* }
|
|
30
|
+
* }))
|
|
31
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
32
|
+
* const dbus: DBusClientComponent = await app.getObject('dbus')
|
|
33
|
+
* console.log(await dbus.listNames())
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const buildDBusClientOptions = (options) => ({
|
|
38
|
+
class: DBusClientComponent,
|
|
39
|
+
busAddress: options.busAddress,
|
|
40
|
+
timeout: options.timeout,
|
|
41
|
+
callTimeout: options.callTimeout,
|
|
42
|
+
decode: options.decode
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* A D-Bus client: a connection to a bus, to call the services on it, read their objects, properties and introspection,
|
|
46
|
+
* and listen to their signals. Declare it in the `components` of an application with {@link buildDBusClientOptions}
|
|
47
|
+
* (or `{class: DBusClientComponent, busAddress: …}`) and get it with `getObject()` or `@Inject`; it is a singleton
|
|
48
|
+
* component. It connects when it is created, so a bus that cannot be reached fails its creation; it disconnects when it
|
|
49
|
+
* is destroyed (the application stopping). It is a thin layer over the `DBus` connection of `dbus-sdk`, whose classes
|
|
50
|
+
* (`DBusService`, `DBusObject`, `DBusRemoteInterface`…) its methods return.
|
|
51
|
+
*
|
|
52
|
+
* Its `on`, `once`, `off` and `removeListener` listen to the connection's events (`'nameOwnerChanged'`,
|
|
53
|
+
* `'connectionClose'`…), not to events of the component.
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import {Application} from 'lakutata'
|
|
57
|
+
* import {DBusClientComponent, type DBusRemoteInterface} from '@lakutata/dbus'
|
|
58
|
+
*
|
|
59
|
+
* Application
|
|
60
|
+
* .run(() => ({
|
|
61
|
+
* id: 'client.app',
|
|
62
|
+
* name: 'Client',
|
|
63
|
+
* components: {
|
|
64
|
+
* dbus: {class: DBusClientComponent, busAddress: 'unix:path=/var/run/dbus/system_bus_socket'}
|
|
65
|
+
* }
|
|
66
|
+
* }))
|
|
67
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
68
|
+
* const dbus: DBusClientComponent = await app.getObject('dbus')
|
|
69
|
+
* //A method call: the output arguments, as an array
|
|
70
|
+
* const [owner] = await dbus.invoke({
|
|
71
|
+
* service: 'org.freedesktop.DBus',
|
|
72
|
+
* objectPath: '/org/freedesktop/DBus',
|
|
73
|
+
* interface: 'org.freedesktop.DBus',
|
|
74
|
+
* method: 'GetNameOwner',
|
|
75
|
+
* signature: 's',
|
|
76
|
+
* args: ['org.freedesktop.DBus']
|
|
77
|
+
* })
|
|
78
|
+
* console.log(owner)
|
|
79
|
+
* //An interface, from the introspection of its object: its methods, properties and signals
|
|
80
|
+
* const device: DBusRemoteInterface = await dbus.interface('com.example.DeviceService', '/com/example/Devices/A1', 'com.example.Device')
|
|
81
|
+
* console.log(await device.method.Status({sn: 'A1'}))
|
|
82
|
+
* console.log(await device.property.State.get())
|
|
83
|
+
* device.signal.on('Changed', (data: unknown): void => console.log(data))
|
|
84
|
+
* dbus.on('nameOwnerChanged', (name: string, oldOwner: string, newOwner: string): void => console.log(name, oldOwner, newOwner))
|
|
85
|
+
* })
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export class DBusClientComponent extends Component {
|
|
89
|
+
/**
|
|
90
|
+
* The unique name the bus gave this connection (`':1.42'`).
|
|
91
|
+
*/
|
|
92
|
+
get uniqueName() {
|
|
93
|
+
return this.dbus.uniqueName;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Connect to the bus, when the component is created.
|
|
97
|
+
* @throws {Error} When the bus cannot be reached, or the authentication fails.
|
|
98
|
+
*/
|
|
99
|
+
async init() {
|
|
100
|
+
this.dbus = await DBus.connect({
|
|
101
|
+
busAddress: this.busAddress,
|
|
102
|
+
timeout: this.timeout,
|
|
103
|
+
callTimeout: this.callTimeout,
|
|
104
|
+
//The 64-bit integers are numbers unless the decoding says otherwise (as the client of lakutata 2.x)
|
|
105
|
+
decode: { int64: 'number', ...this.decode }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Disconnect from the bus, when the component is destroyed.
|
|
110
|
+
*/
|
|
111
|
+
async destroy() {
|
|
112
|
+
await this.dbus.disconnect();
|
|
113
|
+
}
|
|
114
|
+
invoke(opts) {
|
|
115
|
+
return this.dbus.invoke(opts);
|
|
116
|
+
}
|
|
117
|
+
on(eventName, listener) {
|
|
118
|
+
this.dbus.on(eventName, listener);
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
once(eventName, listener) {
|
|
122
|
+
this.dbus.once(eventName, listener);
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
off(eventName, listener) {
|
|
126
|
+
this.dbus.off(eventName, listener);
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
removeListener(eventName, listener) {
|
|
130
|
+
this.dbus.removeListener(eventName, listener);
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Ask the bus to route the messages matching a rule to this connection, without waiting for the bus's answer (the
|
|
135
|
+
* signal emitters of {@link interface} and {@link interfaceSignal} add their own rules).
|
|
136
|
+
* @param rule The match rule (`"type='signal',interface='com.example.Device'"`).
|
|
137
|
+
*/
|
|
138
|
+
addMatch(rule) {
|
|
139
|
+
return this.dbus.addMatch(rule);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Remove a match rule added with {@link addMatch}, without waiting for the bus's answer.
|
|
143
|
+
* @param rule The match rule, as it was added.
|
|
144
|
+
*/
|
|
145
|
+
removeMatch(rule) {
|
|
146
|
+
return this.dbus.removeMatch(rule);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get the connection owning a name.
|
|
150
|
+
* @param name The bus name (`'com.example.DeviceService'`).
|
|
151
|
+
* @returns The unique name of its owner (`':1.42'`), or `undefined` when nobody owns it or the query fails.
|
|
152
|
+
*/
|
|
153
|
+
async getNameOwner(name) {
|
|
154
|
+
return this.dbus.getNameOwner(name);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* List the names the bus can start a service for (D-Bus activation), running or not.
|
|
158
|
+
* @returns The names.
|
|
159
|
+
*/
|
|
160
|
+
async listActivatableNames() {
|
|
161
|
+
return this.dbus.listActivatableNames();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* List the names owned on the bus now: the well-known names and the unique names of the connections.
|
|
165
|
+
* @returns The names.
|
|
166
|
+
*/
|
|
167
|
+
async listNames() {
|
|
168
|
+
return this.dbus.listNames();
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Tell whether a name is owned on the bus now.
|
|
172
|
+
* @param name The bus name.
|
|
173
|
+
* @returns `true` when a connection owns it.
|
|
174
|
+
*/
|
|
175
|
+
async nameHasOwner(name) {
|
|
176
|
+
return this.dbus.nameHasOwner(name);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* List the names of the bus, owned or activatable, with their owner, whether they are active or activatable, and the
|
|
180
|
+
* process id of their owner.
|
|
181
|
+
* @returns The names and their details.
|
|
182
|
+
*/
|
|
183
|
+
async listBusNames() {
|
|
184
|
+
return this.dbus.listBusNames();
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* List the services of the bus: its well-known names (the unique names `:1.42` left out), with their details.
|
|
188
|
+
* @returns The services and their details.
|
|
189
|
+
*/
|
|
190
|
+
async listServices() {
|
|
191
|
+
return this.dbus.listServices();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get the services of the bus which are running, to browse their objects (the activatable services which are not
|
|
195
|
+
* running are left out, not started: get one with {@link service} to start it).
|
|
196
|
+
* @returns The services.
|
|
197
|
+
*/
|
|
198
|
+
async services() {
|
|
199
|
+
return this.dbus.getServices();
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get a service, starting it when it is activatable and not running.
|
|
203
|
+
* @param service The service's well-known name (`'org.freedesktop.NetworkManager'`).
|
|
204
|
+
* @returns The service.
|
|
205
|
+
* @throws {Error} A `ServiceNotFoundError` of `dbus-sdk` when the name is neither owned nor activatable.
|
|
206
|
+
*/
|
|
207
|
+
async service(service) {
|
|
208
|
+
return this.dbus.getService(service);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get an object of a service, to read its introspection and its interfaces.
|
|
212
|
+
* @param service The service's well-known name.
|
|
213
|
+
* @param objectPath The object path (`'/org/freedesktop/NetworkManager'`).
|
|
214
|
+
* @returns The object.
|
|
215
|
+
* @throws {Error} A `ServiceNotFoundError` of `dbus-sdk` when the service is neither running nor activatable.
|
|
216
|
+
*/
|
|
217
|
+
async object(service, objectPath) {
|
|
218
|
+
return this.dbus.getObject(service, objectPath);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get an interface of an object, built from the object's introspection: its methods (`method.Name(...args)`, or
|
|
222
|
+
* `noReplyMethod`), its properties (`property.Name.get()`, `.set(value)`) and its signals (`signal.on('Name', …)`).
|
|
223
|
+
* @param service The service's well-known name.
|
|
224
|
+
* @param objectPath The object path.
|
|
225
|
+
* @param iface The interface name (`'org.freedesktop.NetworkManager'`).
|
|
226
|
+
* @returns The interface.
|
|
227
|
+
* @throws {Error} When the service is not found, or the object does not implement the interface
|
|
228
|
+
* (`InterfaceNotFoundError` of `dbus-sdk`).
|
|
229
|
+
*/
|
|
230
|
+
async interface(service, objectPath, iface) {
|
|
231
|
+
return this.dbus.getInterface(service, objectPath, iface);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get the signals of an interface of an object: an event emitter emitting each signal under its name, with its
|
|
235
|
+
* arguments.
|
|
236
|
+
* @param service The service's well-known name.
|
|
237
|
+
* @param objectPath The object path.
|
|
238
|
+
* @param iface The interface name.
|
|
239
|
+
* @returns The emitter of the interface's signals.
|
|
240
|
+
* @throws {Error} When the service is not found, or the object does not implement the interface
|
|
241
|
+
* (`InterfaceNotFoundError` of `dbus-sdk`).
|
|
242
|
+
*/
|
|
243
|
+
async interfaceSignal(service, objectPath, iface) {
|
|
244
|
+
const dbusIface = await this.dbus.getInterface(service, objectPath, iface);
|
|
245
|
+
return dbusIface.signal;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
__decorate([
|
|
249
|
+
Configurable(DTO.String().required()),
|
|
250
|
+
__metadata("design:type", String)
|
|
251
|
+
], DBusClientComponent.prototype, "busAddress", void 0);
|
|
252
|
+
__decorate([
|
|
253
|
+
Configurable(DTO.Number().positive().integer().optional()),
|
|
254
|
+
__metadata("design:type", Number)
|
|
255
|
+
], DBusClientComponent.prototype, "timeout", void 0);
|
|
256
|
+
__decorate([
|
|
257
|
+
Configurable(DTO.Number().positive().integer().optional()),
|
|
258
|
+
__metadata("design:type", Number)
|
|
259
|
+
], DBusClientComponent.prototype, "callTimeout", void 0);
|
|
260
|
+
__decorate([
|
|
261
|
+
Configurable(DTO.Object({
|
|
262
|
+
types: DTO.String().valid('none', 'variants', 'all').optional(),
|
|
263
|
+
dictionaries: DTO.String().valid('object', 'map').optional(),
|
|
264
|
+
int64: DTO.String().valid('bigint', 'number').optional()
|
|
265
|
+
}).optional()),
|
|
266
|
+
__metadata("design:type", Object)
|
|
267
|
+
], DBusClientComponent.prototype, "decode", void 0);
|