@lakutata/dbus 2.3.0 → 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.
Files changed (81) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +253 -25
  3. package/dist/cjs/adapters/DBusEntrypoint.d.ts +89 -0
  4. package/dist/cjs/adapters/DBusEntrypoint.js +280 -0
  5. package/dist/cjs/channel/DBusChannel.d.ts +36 -0
  6. package/dist/cjs/channel/DBusChannel.js +70 -0
  7. package/dist/cjs/components/DBusClientComponent.d.ts +494 -0
  8. package/dist/cjs/components/DBusClientComponent.js +272 -0
  9. package/dist/cjs/components/DBusObjectManager.d.ts +164 -0
  10. package/dist/cjs/components/DBusObjectManager.js +200 -0
  11. package/dist/cjs/context/DBusContext.d.ts +52 -0
  12. package/dist/cjs/context/DBusContext.js +68 -0
  13. package/dist/cjs/decorators/DBusInterface.d.ts +74 -0
  14. package/dist/cjs/decorators/DBusInterface.js +63 -0
  15. package/dist/cjs/decorators/DBusMethod.d.ts +115 -0
  16. package/dist/cjs/decorators/DBusMethod.js +20 -0
  17. package/dist/cjs/exports/DBus.d.ts +11 -0
  18. package/dist/cjs/exports/DBus.js +42 -0
  19. package/dist/cjs/lib/DBusClientOptions.d.ts +30 -0
  20. package/dist/cjs/lib/Declarations.d.ts +187 -0
  21. package/dist/cjs/lib/Declarations.js +113 -0
  22. package/dist/cjs/lib/Names.d.ts +35 -0
  23. package/dist/cjs/lib/Names.js +80 -0
  24. package/dist/cjs/lib/Signature.d.ts +14 -0
  25. package/dist/cjs/lib/Signature.js +53 -0
  26. package/dist/cjs/package.json +1 -0
  27. package/dist/cjs/types/DBusContextParams.d.ts +22 -0
  28. package/dist/cjs/types/DBusContextParams.js +2 -0
  29. package/dist/esm/adapters/DBusEntrypoint.js +274 -0
  30. package/dist/esm/channel/DBusChannel.js +67 -0
  31. package/dist/esm/components/DBusClientComponent.js +267 -0
  32. package/dist/esm/components/DBusObjectManager.js +197 -0
  33. package/dist/esm/context/DBusContext.js +64 -0
  34. package/dist/esm/decorators/DBusInterface.js +60 -0
  35. package/dist/esm/decorators/DBusMethod.js +17 -0
  36. package/dist/esm/exports/DBus.js +12 -0
  37. package/dist/esm/lib/DBusClientOptions.js +1 -0
  38. package/dist/esm/lib/Declarations.js +107 -0
  39. package/dist/esm/lib/Names.js +73 -0
  40. package/dist/esm/lib/Signature.js +49 -0
  41. package/dist/esm/types/DBusContextParams.js +1 -0
  42. package/dist/types/adapters/DBusEntrypoint.d.ts +89 -0
  43. package/dist/types/channel/DBusChannel.d.ts +36 -0
  44. package/dist/types/components/DBusClientComponent.d.ts +494 -0
  45. package/dist/types/components/DBusObjectManager.d.ts +164 -0
  46. package/dist/types/context/DBusContext.d.ts +52 -0
  47. package/dist/types/decorators/DBusInterface.d.ts +74 -0
  48. package/dist/types/decorators/DBusMethod.d.ts +115 -0
  49. package/dist/types/exports/DBus.d.ts +11 -0
  50. package/dist/types/lib/DBusClientOptions.d.ts +30 -0
  51. package/dist/types/lib/Declarations.d.ts +187 -0
  52. package/dist/types/lib/Names.d.ts +35 -0
  53. package/dist/types/lib/Signature.d.ts +14 -0
  54. package/dist/types/types/DBusContextParams.d.ts +22 -0
  55. package/package.json +38 -39
  56. package/CHANGELOG.md +0 -145
  57. package/dist/CommonExports.d.ts +0 -2
  58. package/dist/CommonExports.d.ts.map +0 -1
  59. package/dist/CommonExports.js +0 -44
  60. package/dist/ComponentExports.d.ts +0 -2
  61. package/dist/ComponentExports.d.ts.map +0 -1
  62. package/dist/ComponentExports.js +0 -5
  63. package/dist/ProviderExports.d.ts +0 -2
  64. package/dist/ProviderExports.d.ts.map +0 -1
  65. package/dist/ProviderExports.js +0 -5
  66. package/dist/components/DBusClientComponent.d.ts +0 -75
  67. package/dist/components/DBusClientComponent.d.ts.map +0 -1
  68. package/dist/components/DBusClientComponent.js +0 -125
  69. package/dist/interfaces/DBusClientOptions.d.ts +0 -7
  70. package/dist/interfaces/DBusClientOptions.d.ts.map +0 -1
  71. package/dist/providers/DBusClientProvider.d.ts +0 -75
  72. package/dist/providers/DBusClientProvider.d.ts.map +0 -1
  73. package/dist/providers/DBusClientProvider.js +0 -125
  74. package/dist/tests/DBusTest.spec.d.ts +0 -2
  75. package/dist/tests/DBusTest.spec.d.ts.map +0 -1
  76. package/dist/tests/DBusTest.spec.js +0 -26
  77. package/dist/tests/Test.d.ts +0 -9
  78. package/dist/tests/Test.d.ts.map +0 -1
  79. package/dist/tests/Test.js +0 -80
  80. package/tsconfig.json +0 -7
  81. /package/dist/{interfaces → cjs/lib}/DBusClientOptions.js +0 -0
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.dbusEntrypoint = dbusEntrypoint;
7
+ const node_events_1 = __importDefault(require("node:events"));
8
+ const dtos_1 = require("@lakutata/core/dtos");
9
+ const dbus_sdk_1 = require("dbus-sdk");
10
+ const DBusChannel_js_1 = require("../channel/DBusChannel.js");
11
+ const DBusContext_js_1 = require("../context/DBusContext.js");
12
+ const DBusObjectManager_js_1 = require("../components/DBusObjectManager.js");
13
+ const Declarations_js_1 = require("../lib/Declarations.js");
14
+ const Names_js_1 = require("../lib/Names.js");
15
+ const Signature_js_1 = require("../lib/Signature.js");
16
+ /**
17
+ * The connection options of a bus
18
+ * @param bus
19
+ */
20
+ function connectOptions(bus) {
21
+ if (bus === 'session')
22
+ return { busAddress: process.env.DBUS_SESSION_BUS_ADDRESS };
23
+ if (bus === 'system')
24
+ return { busAddress: process.env.DBUS_SYSTEM_BUS_ADDRESS ?? 'unix:path=/var/run/dbus/system_bus_socket' };
25
+ if (/^[a-z]+:/.test(bus))
26
+ return { busAddress: bus };
27
+ return { socket: bus };
28
+ }
29
+ /**
30
+ * The error given to the caller of a method
31
+ * @param error
32
+ */
33
+ function toMethodError(error) {
34
+ const { errMsg, message } = (error && typeof error === 'object' ? error : {});
35
+ const methodError = new Error(typeof errMsg === 'string' && errMsg ? errMsg : typeof message === 'string' ? message : String(error));
36
+ methodError.name = (0, Names_js_1.errorName)(error);
37
+ return methodError;
38
+ }
39
+ /**
40
+ * Build the D-Bus adapter of the entrypoint (its `dbus` option): the application owns a service name on a bus and
41
+ * serves the controllers declared with `@DBusInterface` as the interfaces of its objects, their `@DBusMethod` actions
42
+ * as the methods. The objects of the paths without variables are exported when the adapter starts, until it stops
43
+ * ({@link DBusObjectManager.removeObject} does not remove them); the objects of the path templates are exported when
44
+ * they are added with {@link DBusObjectManager.addObject}, before or after the start.
45
+ *
46
+ * Starting connects to the bus and requests the name. The adapter stops gracefully with the entrypoint: it releases the
47
+ * name (no new call reaches it), the calls in progress end, then the connection closes. It needs a D-Bus daemon, as on
48
+ * Linux: the system bus, a session bus, or a private `dbus-daemon`.
49
+ * @param options The bus, the service name and how it is owned, the adapter's middlewares.
50
+ * @returns The adapter, to give to the `dbus` option of `buildEntrypoints()`.
51
+ * @throws {TypeError} At once, when the service name is invalid; when the adapter starts, when an action declared with
52
+ * `@DBusMethod` belongs to a controller without `@DBusInterface`.
53
+ * @throws {Error} When the adapter starts, if the bus cannot be reached, or if another connection owns the name
54
+ * (`The DBus name … is owned by another service`): the entrypoint fails to start.
55
+ * @example
56
+ * ```typescript
57
+ * import {Application} from 'lakutata'
58
+ * import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
59
+ * import {dbusEntrypoint, DBusInterface, DBusMethod} from '@lakutata/dbus'
60
+ *
61
+ * @DBusInterface('com.example.Manager', {path: '/com/example/Manager'})
62
+ * class ManagerController extends Controller {
63
+ * @DBusMethod('Ping', {returns: 's'})
64
+ * public async ping(): Promise<string> {
65
+ * return 'pong'
66
+ * }
67
+ * }
68
+ *
69
+ * //busctl --system call com.example.DeviceService /com/example/Manager com.example.Manager Ping
70
+ * Application.run(() => ({
71
+ * id: 'devices.app',
72
+ * name: 'Devices',
73
+ * components: {
74
+ * entrypoint: buildEntrypoints({
75
+ * controllers: [ManagerController],
76
+ * dbus: dbusEntrypoint({bus: 'system', service: 'com.example.DeviceService'})
77
+ * })
78
+ * },
79
+ * bootstrap: ['entrypoint']
80
+ * }))
81
+ * ```
82
+ */
83
+ function dbusEntrypoint(options) {
84
+ (0, Names_js_1.checkInterfaceName)(options.service);
85
+ let service;
86
+ let detach;
87
+ const exported = new Map();
88
+ /**
89
+ * The interfaces of the controllers and of the actions, by path template
90
+ * @param runtime
91
+ */
92
+ const collect = (runtime) => {
93
+ const templates = new Map();
94
+ const interfacesAt = (path, name) => {
95
+ let template = templates.get(path);
96
+ if (!template) {
97
+ template = { template: (0, Names_js_1.compilePathTemplate)(path), interfaces: new Map() };
98
+ templates.set(path, template);
99
+ }
100
+ let interfaces = template.interfaces.get(name);
101
+ if (!interfaces) {
102
+ interfaces = { methods: [] };
103
+ template.interfaces.set(name, interfaces);
104
+ }
105
+ return interfaces;
106
+ };
107
+ for (const controller of runtime.controllers) {
108
+ const declaration = (0, Declarations_js_1.getDBusInterfaceDeclaration)(controller);
109
+ if (declaration)
110
+ interfacesAt(declaration.path, declaration.interface).declaration = declaration;
111
+ }
112
+ for (const action of runtime.actions) {
113
+ const { path, interface: interfaceName, member } = action.address;
114
+ if (path === undefined || interfaceName === undefined)
115
+ throw new TypeError(`The DBus method ${member} of ${action.controller} has no interface: declare its controller with @DBusInterface`);
116
+ interfacesAt(path, interfaceName).methods.push(action);
117
+ }
118
+ return templates;
119
+ };
120
+ /**
121
+ * The signature of the result of a method: its returns option, or the schema of @Return
122
+ * @param action
123
+ */
124
+ const resultSignature = (action) => {
125
+ if (action.address.returns !== undefined)
126
+ return action.address.returns;
127
+ return action.returnSchema ? (0, Signature_js_1.schemaSignature)(action.returnSchema) : undefined;
128
+ };
129
+ return {
130
+ channel: 'dbus',
131
+ definition: DBusChannel_js_1.DBusChannel,
132
+ name: options.name,
133
+ middlewares: options.middlewares,
134
+ start: async (runtime) => {
135
+ const templates = collect(runtime);
136
+ const manager = await runtime.getObject(DBusObjectManager_js_1.DBusObjectManager);
137
+ const localService = new dbus_sdk_1.LocalService(options.service);
138
+ service = localService;
139
+ /**
140
+ * Build the interface of an object
141
+ * @param path
142
+ * @param name
143
+ * @param served
144
+ * @param variables
145
+ * @param object
146
+ */
147
+ const buildInterface = (path, name, served, variables, object) => {
148
+ const localInterface = new dbus_sdk_1.LocalInterface(name);
149
+ for (const action of served.methods) {
150
+ const returns = resultSignature(action);
151
+ const hasInput = action.dto !== dtos_1.FlexibleDTO;
152
+ localInterface.defineMethod({
153
+ name: action.address.member,
154
+ inputArgs: hasInput ? [{ name: 'input', type: 'a{sv}' }] : [],
155
+ outputArgs: returns ? [{ name: 'output', type: returns }] : [],
156
+ method: async (input) => {
157
+ const context = new DBusContext_js_1.DBusContext({ path: path, interface: name, member: action.address.member, data: input && typeof input === 'object' ? input : {} });
158
+ let result;
159
+ try {
160
+ result = await runtime.dispatchTo(action, context, { params: variables });
161
+ }
162
+ catch (error) {
163
+ throw toMethodError(error);
164
+ }
165
+ if (!returns)
166
+ return undefined;
167
+ return (0, Signature_js_1.toEncodable)(returns === 'a{sv}' && (result === undefined || result === null) ? {} : result);
168
+ }
169
+ });
170
+ }
171
+ for (const property of served.declaration?.properties ?? []) {
172
+ localInterface.defineProperty({
173
+ name: property.name,
174
+ type: property.type,
175
+ getter: () => (0, Signature_js_1.toEncodable)(manager.getPropertyValue(path, property)),
176
+ //A value written by a client is validated synchronously: its errors are given to the client
177
+ setter: property.writable ? (value) => {
178
+ let validated;
179
+ try {
180
+ validated = property.validate(value);
181
+ }
182
+ catch (error) {
183
+ //The errors given to DBus are plain errors (the name of an exception is read only)
184
+ throw toMethodError(error);
185
+ }
186
+ manager[DBusObjectManager_js_1.STORE]({ path: path, interface: name, property: property, value: validated });
187
+ } : undefined,
188
+ emitPropertiesChanged: property.writable
189
+ });
190
+ object.properties.set(property, [...(object.properties.get(property) ?? []), name]);
191
+ }
192
+ for (const signal of served.declaration?.signals ?? []) {
193
+ const emitter = new node_events_1.default();
194
+ localInterface.defineSignal({ name: signal.name, args: [{ name: 'data', type: signal.type }], eventEmitter: emitter });
195
+ const signals = object.signals.get(signal.name) ?? [];
196
+ signals.push({ signal: signal, emitter: emitter });
197
+ object.signals.set(signal.name, signals);
198
+ }
199
+ return localInterface;
200
+ };
201
+ /**
202
+ * Export an object
203
+ * @param path
204
+ */
205
+ const addObject = (path) => {
206
+ if (exported.has(path))
207
+ return true;
208
+ const object = { object: new dbus_sdk_1.LocalObject(path), signals: new Map(), properties: new Map() };
209
+ let served = false;
210
+ for (const { template, interfaces } of templates.values()) {
211
+ const variables = template.match(path);
212
+ if (!variables)
213
+ continue;
214
+ interfaces.forEach((interfaceServed, name) => object.object.addInterface(buildInterface(path, name, interfaceServed, variables, object)));
215
+ served = true;
216
+ }
217
+ if (!served)
218
+ return false;
219
+ exported.set(path, object);
220
+ localService.addObject(object.object);
221
+ return true;
222
+ };
223
+ //The objects of the paths without variables are exported at once, and kept until the adapter closes
224
+ const fixed = new Set();
225
+ for (const { template } of templates.values()) {
226
+ if (!template.static)
227
+ continue;
228
+ fixed.add(template.template);
229
+ addObject(template.template);
230
+ }
231
+ const connect = connectOptions(options.bus ?? 'session');
232
+ let flags = dbus_sdk_1.RequestNameFlags.DBUS_NAME_FLAG_DO_NOT_QUEUE;
233
+ if (options.replaceExisting)
234
+ flags |= dbus_sdk_1.RequestNameFlags.DBUS_NAME_FLAG_REPLACE_EXISTING;
235
+ if (options.allowReplacement)
236
+ flags |= dbus_sdk_1.RequestNameFlags.DBUS_NAME_FLAG_ALLOW_REPLACEMENT;
237
+ const result = await localService.run({ ...connect, flags: flags });
238
+ if (result !== dbus_sdk_1.RequestNameResultCode.DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER && result !== dbus_sdk_1.RequestNameResultCode.DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER) {
239
+ await localService.stop().catch(() => undefined);
240
+ throw new Error(`The DBus name ${options.service} is owned by another service`);
241
+ }
242
+ const sink = {
243
+ addObject: addObject,
244
+ removeObject: (path) => {
245
+ if (fixed.has(path) || !exported.delete(path))
246
+ return false;
247
+ return localService.removeObject(path);
248
+ },
249
+ isFixed: (path) => fixed.has(path),
250
+ emitSignal: (path, signal, data) => {
251
+ const emitters = exported.get(path)?.signals.get(signal.name)?.filter((candidate) => candidate.signal === signal);
252
+ if (!emitters?.length)
253
+ return false;
254
+ emitters.forEach(({ emitter }) => emitter.emit(signal.name, (0, Signature_js_1.toEncodable)(data)));
255
+ return true;
256
+ },
257
+ propertyChanged: (path, property) => {
258
+ const object = exported.get(path);
259
+ const interfaces = object?.properties.get(property);
260
+ if (!object || !interfaces)
261
+ return;
262
+ //The interfaces read the new value with their getters (the manager stored it), typed by the property
263
+ interfaces.forEach((name) => object.object.getInterface(name)?.emitPropertiesChanged(property.name));
264
+ }
265
+ };
266
+ detach = manager[DBusObjectManager_js_1.ATTACH](sink);
267
+ },
268
+ stop: async () => {
269
+ //The name is released: no new call reaches the service, the calls in progress end
270
+ detach?.();
271
+ detach = undefined;
272
+ await service?.dbus?.releaseName(options.service).catch(() => undefined);
273
+ },
274
+ close: async () => {
275
+ await service?.dbus?.disconnect().catch(() => undefined);
276
+ exported.clear();
277
+ service = undefined;
278
+ }
279
+ };
280
+ }
@@ -0,0 +1,36 @@
1
+ import { type EntrypointAdapter } from '@lakutata/entrypoint';
2
+ import { type ChannelDefinition } from '@lakutata/entrypoint/channel';
3
+ import { DBusContext } from '../context/DBusContext.js';
4
+ declare module '@lakutata/entrypoint' {
5
+ interface EntrypointOptions {
6
+ /**
7
+ * The D-Bus adapters, built by `dbusEntrypoint()`: one, or several for several buses or service names.
8
+ * @default undefined (no D-Bus adapter)
9
+ */
10
+ dbus?: EntrypointAdapter<DBusContext> | EntrypointAdapter<DBusContext>[];
11
+ }
12
+ interface PrefixOptions {
13
+ /**
14
+ * The object path (or path template) and the interface of a controller's D-Bus methods. `@DBusInterface` sets
15
+ * it; an application does not set it itself.
16
+ */
17
+ dbus?: {
18
+ /**
19
+ * The object path, or path template, of the methods.
20
+ */
21
+ path?: string;
22
+ /**
23
+ * The name of the methods' interface.
24
+ */
25
+ interface?: string;
26
+ };
27
+ }
28
+ }
29
+ /**
30
+ * The definition of the D-Bus channel of the entrypoint, named `dbus`: its actions are the methods of D-Bus interfaces,
31
+ * addressed by `{member, interface, path}` (the method name from `@DBusMethod`, the interface and the path template from
32
+ * `@DBusInterface`). A call matches an action by its interface, its method and its object path; a call matching none
33
+ * fails with `ControllerActionNotFoundException`. `@DBusMethod` and {@link dbusEntrypoint} use it: an application only
34
+ * needs it to write an adapter of its own.
35
+ */
36
+ export declare const DBusChannel: ChannelDefinition;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DBusChannel = void 0;
4
+ const entrypoint_1 = require("@lakutata/entrypoint");
5
+ const channel_1 = require("@lakutata/entrypoint/channel");
6
+ const DBusContext_js_1 = require("../context/DBusContext.js");
7
+ const Names_js_1 = require("../lib/Names.js");
8
+ /**
9
+ * The key of the address of a method
10
+ * @param interfaceName
11
+ * @param member
12
+ * @param path
13
+ */
14
+ function addressKey(interfaceName, member, path) {
15
+ return `${interfaceName}.${member}@${path}`;
16
+ }
17
+ /**
18
+ * The definition of the D-Bus channel of the entrypoint, named `dbus`: its actions are the methods of D-Bus interfaces,
19
+ * addressed by `{member, interface, path}` (the method name from `@DBusMethod`, the interface and the path template from
20
+ * `@DBusInterface`). A call matches an action by its interface, its method and its object path; a call matching none
21
+ * fails with `ControllerActionNotFoundException`. `@DBusMethod` and {@link dbusEntrypoint} use it: an application only
22
+ * needs it to write an adapter of its own.
23
+ */
24
+ exports.DBusChannel = (0, channel_1.hasChannel)('dbus') ? (0, channel_1.getChannel)('dbus') : (0, channel_1.defineChannel)({
25
+ name: 'dbus',
26
+ kind: 'request',
27
+ contextType: DBusContext_js_1.DBUS_CONTEXT_TYPE,
28
+ validate: (pattern) => {
29
+ (0, Names_js_1.checkMemberName)(pattern.member, 'method');
30
+ if (pattern.interface !== undefined)
31
+ (0, Names_js_1.checkInterfaceName)(pattern.interface);
32
+ if (pattern.path !== undefined)
33
+ (0, Names_js_1.checkPathTemplate)(pattern.path);
34
+ return pattern;
35
+ },
36
+ identify: (pattern) => addressKey(pattern.interface ?? '?', pattern.member, pattern.path ?? '?'),
37
+ describe: (pattern) => {
38
+ const { returns, ...address } = pattern;
39
+ return returns === undefined ? address : { ...address, returns: returns };
40
+ },
41
+ prefix: (prefix, pattern) => {
42
+ const dbus = prefix.dbus;
43
+ if (!dbus || (pattern.path !== undefined && pattern.interface !== undefined))
44
+ return undefined;
45
+ return { ...pattern, path: pattern.path ?? dbus.path, interface: pattern.interface ?? dbus.interface };
46
+ },
47
+ createMatcher: (entries) => {
48
+ const templates = entries
49
+ .filter((entry) => entry.details.pattern.path !== undefined)
50
+ .map((entry) => ({ entry: entry, template: (0, Names_js_1.compilePathTemplate)(entry.details.pattern.path) }));
51
+ return {
52
+ match: (context) => {
53
+ const { path, interface: interfaceName, member } = context;
54
+ for (const { entry, template } of templates) {
55
+ if (entry.details.pattern.interface !== interfaceName || entry.details.pattern.member !== member)
56
+ continue;
57
+ const params = template.match(path);
58
+ if (params)
59
+ return { entry: entry, params: params };
60
+ }
61
+ return undefined;
62
+ }
63
+ };
64
+ },
65
+ notFound: (context) => new entrypoint_1.ControllerActionNotFoundException('The DBus method {member} of the interface {interface} of {path} is not found', {
66
+ member: context.member,
67
+ interface: context.interface,
68
+ path: context.path
69
+ })
70
+ });