@gjsify/node-gi 0.13.0 → 0.20.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.
@@ -0,0 +1,749 @@
1
+ // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
2
+ // SPDX-FileCopyrightText: 2011 Giovanni Campagna
3
+ //
4
+ // Adapted from GJS (refs/gjs/modules/core/overrides/Gio.js). Copyright (c) 2011
5
+ // Giovanni Campagna and GJS contributors. MIT OR LGPL-2.0-or-later.
6
+ // Modifications: the DBus surface (Gio.DBus.session/system, own_name/watch_name,
7
+ // Gio.DBusProxy.makeProxyWrapper) ported to the @gjsify/node-gi L1 model.
8
+ //
9
+ // The upstream override patches `Gio.DBusProxy.prototype` and returns a class
10
+ // extending Gio.DBusProxy. node-gi instances are Proxies over a native GObject
11
+ // handle (no live prototype to patch, and an unknown property READ returns a GI
12
+ // method thunk rather than `undefined`), so this port instead:
13
+ // • builds each proxy's convenience methods/properties as own expandos on the
14
+ // instance (node-gi's set trap stores them; the get trap surfaces them),
15
+ // • hosts the `_signals` mixin on a DEDICATED plain emitter object per proxy
16
+ // (the mixin's `this._signalConnections === undefined` bootstrap can't run on
17
+ // an instance wrapper — see above), exposed as `connectSignal`/`disconnectSignal`,
18
+ // • drives name owning via org.freedesktop.DBus RequestName/ReleaseName and name
19
+ // watching via NameOwnerChanged `signal_subscribe` — the node-gi engine can
20
+ // marshal a GDBusSignalCallback and a GSourceFunc (idle), but NOT the
21
+ // GBusAcquiredCallback/GBusNameAppearedCallback that g_bus_own_name/watch_name
22
+ // take, so those namespace functions are re-implemented in JS here.
23
+ //
24
+ // Object export (Gio.DBusExportedObject.wrapJSObject): GJS builds it on
25
+ // GjsPrivate.DBusImplementation (a C type absent on a plain Node/GI host); node-gi
26
+ // drives the INTROSPECTABLE g_dbus_connection_register_object_with_closures2
27
+ // instead — three GClosures (method call / get property / set property) the engine
28
+ // now marshals from JS functions. See wrapJSObject at the bottom of this module.
29
+ import { addSignalMethods, _connect, _disconnect, _emit } from './_signals.js';
30
+
31
+ // The `_signals` mixin's error paths (a throwing handler / a method-name clash)
32
+ // reference the GJS ambient `log`/`logError`. Under a bare `@gjsify/node-gi/gi`
33
+ // consumer (no globals.js) those may be absent — install minimal fallbacks so the
34
+ // mixin never ReferenceErrors. Idempotent; globals.js's richer versions win if
35
+ // already present.
36
+ function ensureAmbientLog() {
37
+ const g = globalThis;
38
+ if (typeof g.log === 'undefined') g.log = (...args) => console.error(args.map((a) => String(a)).join(' '));
39
+ if (typeof g.logError === 'undefined') {
40
+ g.logError = (error, prefix) => {
41
+ const head = prefix ? `${prefix}: ` : '';
42
+ console.error(head + (error && error.stack ? error.stack : String(error)));
43
+ };
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Build the L1 DBus surface bound to a set of node-gi primitives.
49
+ * @param {object} ctx
50
+ * @param {Record<string, unknown>} ctx.Gio the INTROSPECTED Gio namespace (constructible classes + namespace functions)
51
+ * @param {Record<string, unknown>} ctx.GLib the ergonomic GLib namespace (`new GLib.Variant(...)`, idle_add)
52
+ * @param {(fn: Function, ...args: unknown[]) => void} ctx.unwrap gi.js's `unwrap` (wrapped instance → native handle)
53
+ * @param {typeof import('../index.js')} ctx.native the native engine (isInstanceOf)
54
+ * @returns {{ DBus: object, DBusProxy: object, DBusExportedObject: object }}
55
+ */
56
+ export function createGioDBus(ctx) {
57
+ const { Gio, GLib, unwrap, native } = ctx;
58
+ // Scoped to actual DBus usage (first Gio.DBus* access), so a plain non-DBus
59
+ // `import { requireGi }` adds no global side effects.
60
+ ensureAmbientLog();
61
+
62
+ // DBus RequestName reply codes (org.freedesktop.DBus).
63
+ const REQUEST_NAME_PRIMARY_OWNER = 1;
64
+ const REQUEST_NAME_ALREADY_OWNER = 4;
65
+ const DBUS_NAME = 'org.freedesktop.DBus';
66
+ const DBUS_PATH = '/org/freedesktop/DBus';
67
+
68
+ function safeCallback(fn, ...args) {
69
+ try {
70
+ fn(...args);
71
+ } catch (e) {
72
+ logError(e, 'Exception in DBus name callback');
73
+ }
74
+ }
75
+
76
+ // Is `value` a wrapped node-gi instance of Gio.<typeName>? Used to classify the
77
+ // trailing flags/Cancellable/UnixFDList args a proxy method call may carry.
78
+ function isGioInstance(value, typeName) {
79
+ if (value === null || typeof value !== 'object') return false;
80
+ const handle = unwrap(value);
81
+ if (handle === value) return false; // not a wrapped instance
82
+ try {
83
+ return native.isInstanceOf(handle, 'Gio', typeName);
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ function _logReply(result, exc) {
90
+ if (exc !== null && exc !== undefined) log(`Ignored exception from dbus method: ${exc}`);
91
+ }
92
+
93
+ // Ported from refs/gjs Gio.js `_proxyInvoker`. `wrappedProxy` is captured (GJS
94
+ // uses `this`) because node-gi delivers a RAW source handle to the async ready
95
+ // callback, so the wrapper must come from the closure to reach the L1 finish.
96
+ function _proxyInvoker(wrappedProxy, methodName, sync, inSignature, args) {
97
+ let replyFunc = _logReply;
98
+ let flags = 0;
99
+ let cancellable = null;
100
+ let fdList = null;
101
+ const argArray = Array.prototype.slice.call(args);
102
+
103
+ const signatureLength = inSignature.length;
104
+ const minNumberArgs = signatureLength;
105
+ const maxNumberArgs = signatureLength + 4;
106
+ if (argArray.length < minNumberArgs) {
107
+ throw new Error(
108
+ `Not enough arguments passed for method: ${methodName}. Expected ${minNumberArgs}, got ${argArray.length}`,
109
+ );
110
+ } else if (argArray.length > maxNumberArgs) {
111
+ throw new Error(
112
+ `Too many arguments passed for method ${methodName}. Maximum is ${maxNumberArgs} ` +
113
+ 'including one callback, Gio.Cancellable, Gio.UnixFDList, and/or flags',
114
+ );
115
+ }
116
+
117
+ while (argArray.length > signatureLength) {
118
+ const argNum = argArray.length - 1;
119
+ const arg = argArray.pop();
120
+ if (typeof arg === 'function' && !sync) replyFunc = arg;
121
+ else if (typeof arg === 'number') flags = arg;
122
+ else if (isGioInstance(arg, 'Cancellable')) cancellable = arg;
123
+ else if (isGioInstance(arg, 'UnixFDList')) fdList = arg;
124
+ else {
125
+ throw new Error(
126
+ `Argument ${argNum} of method ${methodName} is ${typeof arg}. It should be a callback, ` +
127
+ 'flags, Gio.UnixFDList, or a Gio.Cancellable',
128
+ );
129
+ }
130
+ }
131
+
132
+ const inTypeString = `(${inSignature.join('')})`;
133
+ const inVariant = new GLib.Variant(inTypeString, argArray);
134
+
135
+ // GJS always routes through call_with_unix_fd_list[_sync/_finish] (a superset
136
+ // that also carries GUnixFDList fd-passing). Those live in GioUnix and are ABSENT
137
+ // from the Gio typelib on Windows, so fall back to the cross-platform
138
+ // call[_sync/_finish] there — fd-passing is a Unix concept, so there is nothing to
139
+ // carry. Detection is by method presence (node-gi's L1 wrapper returns `undefined`
140
+ // for an absent method), keeping POSIX byte-identical: where the method exists the
141
+ // fd-list path is taken exactly as before.
142
+ if (sync) {
143
+ if (typeof wrappedProxy.call_with_unix_fd_list_sync === 'function') {
144
+ const result = wrappedProxy.call_with_unix_fd_list_sync(methodName, inVariant, flags, -1, fdList, cancellable);
145
+ const outVariant = Array.isArray(result) ? result[0] : result;
146
+ const outFdList = Array.isArray(result) ? result[1] : null;
147
+ if (fdList) return [outVariant.deepUnpack(), outFdList];
148
+ return outVariant.deepUnpack();
149
+ }
150
+ if (fdList) throw new Error(`${methodName}: Gio.UnixFDList fd-passing is not supported on this platform`);
151
+ const outVariant = wrappedProxy.call_sync(methodName, inVariant, flags, -1, cancellable);
152
+ return outVariant.deepUnpack();
153
+ }
154
+
155
+ if (typeof wrappedProxy.call_with_unix_fd_list === 'function') {
156
+ const asyncCallback = (_rawSource, res) => {
157
+ try {
158
+ const result = wrappedProxy.call_with_unix_fd_list_finish(res);
159
+ const outVariant = Array.isArray(result) ? result[0] : result;
160
+ const outFdList = Array.isArray(result) ? result[1] : null;
161
+ replyFunc(outVariant.deepUnpack(), null, outFdList);
162
+ } catch (e) {
163
+ replyFunc([], e, null);
164
+ }
165
+ };
166
+ return wrappedProxy.call_with_unix_fd_list(methodName, inVariant, flags, -1, fdList, cancellable, asyncCallback);
167
+ }
168
+ if (fdList) throw new Error(`${methodName}: Gio.UnixFDList fd-passing is not supported on this platform`);
169
+ const asyncCallback = (_rawSource, res) => {
170
+ try {
171
+ const outVariant = wrappedProxy.call_finish(res);
172
+ replyFunc(outVariant.deepUnpack(), null, null);
173
+ } catch (e) {
174
+ replyFunc([], e, null);
175
+ }
176
+ };
177
+ return wrappedProxy.call(methodName, inVariant, flags, -1, cancellable, asyncCallback);
178
+ }
179
+
180
+ function _makeProxyMethod(wrappedProxy, method, sync) {
181
+ const name = method.name;
182
+ const inArgs = method.in_args;
183
+ const inSignature = [];
184
+ for (let i = 0; i < inArgs.length; i++) inSignature.push(inArgs[i].signature);
185
+ return function proxyMethod(...args) {
186
+ return _proxyInvoker(wrappedProxy, name, sync, inSignature, args);
187
+ };
188
+ }
189
+
190
+ function _propertyGetter(wrappedProxy, name) {
191
+ const value = wrappedProxy.get_cached_property(name);
192
+ return value ? value.deepUnpack() : null;
193
+ }
194
+
195
+ function _propertySetter(wrappedProxy, name, signature, value) {
196
+ const variant = new GLib.Variant(signature, value);
197
+ wrappedProxy.set_cached_property(name, variant);
198
+ wrappedProxy.call(
199
+ 'org.freedesktop.DBus.Properties.Set',
200
+ new GLib.Variant('(ssv)', [wrappedProxy.g_interface_name, name, variant]),
201
+ Gio.DBusCallFlags.NONE,
202
+ -1,
203
+ null,
204
+ (_rawSource, res) => {
205
+ try {
206
+ wrappedProxy.call_finish(res);
207
+ } catch (e) {
208
+ log(`Could not set property ${name} on remote object ${wrappedProxy.g_object_path}: ${e.message}`);
209
+ }
210
+ },
211
+ );
212
+ }
213
+
214
+ // Attach the method/property/signal convenience to a freshly-inited DBusProxy
215
+ // instance wrapper. Ported from refs/gjs Gio.js `_addDBusConvenience`.
216
+ function _addDBusConvenience(wrappedProxy, info) {
217
+ // The `_signals` mixin bookkeeping lives on a dedicated plain emitter (see the
218
+ // module header): connectSignal/disconnectSignal delegate to it.
219
+ const emitter = {};
220
+ addSignalMethods(emitter);
221
+ wrappedProxy.connectSignal = (name, callback) => _connect.call(emitter, name, callback);
222
+ wrappedProxy.disconnectSignal = (id) => _disconnect.call(emitter, id);
223
+
224
+ const signals = info.signals;
225
+ if (signals && signals.length > 0) {
226
+ wrappedProxy.connect('g-signal', (_proxy, senderName, signalName, parameters) => {
227
+ _emit.call(emitter, signalName, senderName, parameters.deepUnpack());
228
+ });
229
+ }
230
+
231
+ const methods = info.methods;
232
+ for (let i = 0; i < methods.length; i++) {
233
+ const method = methods[i];
234
+ const name = method.name;
235
+ const remoteMethod = _makeProxyMethod(wrappedProxy, method, false);
236
+ wrappedProxy[`${name}Remote`] = remoteMethod;
237
+ wrappedProxy[`${name}Sync`] = _makeProxyMethod(wrappedProxy, method, true);
238
+ wrappedProxy[`${name}Async`] = function proxyAsyncMethod(...args) {
239
+ return new Promise((resolve, reject) => {
240
+ args.push((result, error, fdList) => {
241
+ if (error) reject(error);
242
+ else if (fdList) resolve([result, fdList]);
243
+ else resolve(result);
244
+ });
245
+ remoteMethod(...args);
246
+ });
247
+ };
248
+ }
249
+
250
+ const properties = info.properties;
251
+ for (let i = 0; i < properties.length; i++) {
252
+ const propInfo = properties[i];
253
+ const name = propInfo.name;
254
+ const signature = propInfo.signature;
255
+ const propFlags = propInfo.flags;
256
+ let getter = () => {
257
+ throw new Error(`Property ${name} is not readable`);
258
+ };
259
+ let setter = () => {
260
+ throw new Error(`Property ${name} is not writable`);
261
+ };
262
+ if (propFlags & Gio.DBusPropertyInfoFlags.READABLE) getter = () => _propertyGetter(wrappedProxy, name);
263
+ if (propFlags & Gio.DBusPropertyInfoFlags.WRITABLE) {
264
+ setter = (value) => _propertySetter(wrappedProxy, name, signature, value);
265
+ }
266
+ Object.defineProperty(wrappedProxy, name, {
267
+ get: getter,
268
+ set: setter,
269
+ configurable: false,
270
+ enumerable: true,
271
+ });
272
+ }
273
+ }
274
+
275
+ function _newInterfaceInfo(xml) {
276
+ const nodeInfo = Gio.DBusNodeInfo.new_for_xml(xml);
277
+ return nodeInfo.interfaces[0];
278
+ }
279
+
280
+ // Gio.DBusProxy.makeProxyWrapper(interfaceXml). Returns a constructor:
281
+ // new ProxyClass(bus, name, objectPath[, asyncCallback[, cancellable[, flags]]])
282
+ // and a static `ProxyClass.newAsync(bus, name, objectPath[, cancellable[, flags]])`.
283
+ function makeProxyWrapper(interfaceXml) {
284
+ const info = _newInterfaceInfo(interfaceXml);
285
+ const iname = info.name;
286
+
287
+ function ProxyClass(bus, name, object, asyncCallback, cancellable = null, flags = Gio.DBusProxyFlags.NONE) {
288
+ const obj = new Gio.DBusProxy({
289
+ g_connection: bus,
290
+ g_interface_name: iname,
291
+ g_interface_info: info,
292
+ g_name: name,
293
+ g_flags: flags,
294
+ g_object_path: object,
295
+ });
296
+
297
+ if (asyncCallback) {
298
+ obj.init_async(GLib.PRIORITY_DEFAULT, cancellable, (_rawSource, res) => {
299
+ try {
300
+ obj.init_finish(res);
301
+ _addDBusConvenience(obj, info);
302
+ asyncCallback(obj, null);
303
+ } catch (e) {
304
+ asyncCallback(null, e);
305
+ }
306
+ });
307
+ } else {
308
+ obj.init(cancellable);
309
+ _addDBusConvenience(obj, info);
310
+ }
311
+ return obj;
312
+ }
313
+
314
+ ProxyClass.newAsync = function newAsync(bus, name, object, cancellable = null, flags = Gio.DBusProxyFlags.NONE) {
315
+ const obj = new Gio.DBusProxy({
316
+ g_connection: bus,
317
+ g_interface_name: info.name,
318
+ g_interface_info: info,
319
+ g_name: name,
320
+ g_flags: flags,
321
+ g_object_path: object,
322
+ });
323
+ return new Promise((resolve, reject) => {
324
+ obj.init_async(GLib.PRIORITY_DEFAULT, cancellable, (_rawSource, res) => {
325
+ try {
326
+ obj.init_finish(res);
327
+ _addDBusConvenience(obj, info);
328
+ resolve(obj);
329
+ } catch (e) {
330
+ reject(e);
331
+ }
332
+ });
333
+ });
334
+ };
335
+
336
+ return ProxyClass;
337
+ }
338
+
339
+ // ---- name owning ----
340
+ const ownedNames = new Map();
341
+ const watchedNames = new Map();
342
+ let nextToken = 1;
343
+
344
+ function busConnection(busType) {
345
+ return Gio.bus_get_sync(busType, null);
346
+ }
347
+
348
+ function requestName(connection, name, flags) {
349
+ const reply = connection.call_sync(
350
+ DBUS_NAME,
351
+ DBUS_PATH,
352
+ DBUS_NAME,
353
+ 'RequestName',
354
+ new GLib.Variant('(su)', [name, flags >>> 0]),
355
+ null,
356
+ Gio.DBusCallFlags.NONE,
357
+ -1,
358
+ null,
359
+ );
360
+ return reply.deepUnpack()[0];
361
+ }
362
+
363
+ function ownNameCore(connection, name, flags, onBusAcquired, onNameAcquired, onNameLost) {
364
+ const token = nextToken++;
365
+ ownedNames.set(token, { connection, name });
366
+ let code;
367
+ try {
368
+ code = requestName(connection, name, flags);
369
+ } catch (e) {
370
+ ownedNames.delete(token);
371
+ throw e;
372
+ }
373
+ // GJS fires the acquired/lost callbacks from the main loop, never synchronously.
374
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
375
+ if (typeof onBusAcquired === 'function') safeCallback(onBusAcquired, connection, name);
376
+ const acquired = code === REQUEST_NAME_PRIMARY_OWNER || code === REQUEST_NAME_ALREADY_OWNER;
377
+ if (acquired) {
378
+ if (typeof onNameAcquired === 'function') safeCallback(onNameAcquired, connection, name);
379
+ } else if (typeof onNameLost === 'function') {
380
+ safeCallback(onNameLost, connection, name);
381
+ }
382
+ return false; // G_SOURCE_REMOVE
383
+ });
384
+ return token;
385
+ }
386
+
387
+ function own_name(busType, name, flags, onBusAcquired, onNameAcquired, onNameLost) {
388
+ return ownNameCore(busConnection(busType), name, flags, onBusAcquired, onNameAcquired, onNameLost);
389
+ }
390
+
391
+ function own_name_on_connection(connection, name, flags, onNameAcquired, onNameLost) {
392
+ return ownNameCore(connection, name, flags, undefined, onNameAcquired, onNameLost);
393
+ }
394
+
395
+ function unown_name(token) {
396
+ const entry = ownedNames.get(token);
397
+ if (!entry) return;
398
+ ownedNames.delete(token);
399
+ try {
400
+ entry.connection.call_sync(
401
+ DBUS_NAME,
402
+ DBUS_PATH,
403
+ DBUS_NAME,
404
+ 'ReleaseName',
405
+ new GLib.Variant('(s)', [entry.name]),
406
+ null,
407
+ Gio.DBusCallFlags.NONE,
408
+ -1,
409
+ null,
410
+ );
411
+ } catch {
412
+ /* connection already gone — nothing to release */
413
+ }
414
+ }
415
+
416
+ // ---- name watching (NameOwnerChanged subscription + GetNameOwner probe) ----
417
+ function getNameOwner(connection, name) {
418
+ try {
419
+ const reply = connection.call_sync(
420
+ DBUS_NAME,
421
+ DBUS_PATH,
422
+ DBUS_NAME,
423
+ 'GetNameOwner',
424
+ new GLib.Variant('(s)', [name]),
425
+ null,
426
+ Gio.DBusCallFlags.NONE,
427
+ -1,
428
+ null,
429
+ );
430
+ return reply.deepUnpack()[0];
431
+ } catch {
432
+ return null; // NameHasNoOwner
433
+ }
434
+ }
435
+
436
+ function watchNameCore(connection, name, onNameAppeared, onNameVanished) {
437
+ const token = nextToken++;
438
+ let lastOwner = null;
439
+
440
+ const dispatch = () => {
441
+ const owner = getNameOwner(connection, name);
442
+ if (owner && owner.length > 0) {
443
+ if (owner !== lastOwner && typeof onNameAppeared === 'function') safeCallback(onNameAppeared, connection, name, owner);
444
+ lastOwner = owner;
445
+ } else {
446
+ if (lastOwner !== null && typeof onNameVanished === 'function') safeCallback(onNameVanished, connection, name);
447
+ lastOwner = null;
448
+ }
449
+ };
450
+
451
+ // arg0-filtered subscription: NameOwnerChanged whose first arg is `name`.
452
+ const subId = connection.signal_subscribe(
453
+ DBUS_NAME,
454
+ DBUS_NAME,
455
+ 'NameOwnerChanged',
456
+ DBUS_PATH,
457
+ name,
458
+ Gio.DBusSignalFlags.NONE,
459
+ () => dispatch(),
460
+ );
461
+ watchedNames.set(token, { connection, subId });
462
+
463
+ // Initial state, delivered from the loop like g_bus_watch_name's GetNameOwner.
464
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
465
+ const owner = getNameOwner(connection, name);
466
+ if (owner && owner.length > 0) {
467
+ lastOwner = owner;
468
+ if (typeof onNameAppeared === 'function') safeCallback(onNameAppeared, connection, name, owner);
469
+ } else if (typeof onNameVanished === 'function') {
470
+ safeCallback(onNameVanished, connection, name);
471
+ }
472
+ return false; // G_SOURCE_REMOVE
473
+ });
474
+ return token;
475
+ }
476
+
477
+ function watch_name(busType, name, _flags, onNameAppeared, onNameVanished) {
478
+ return watchNameCore(busConnection(busType), name, onNameAppeared, onNameVanished);
479
+ }
480
+
481
+ function watch_name_on_connection(connection, name, _flags, onNameAppeared, onNameVanished) {
482
+ return watchNameCore(connection, name, onNameAppeared, onNameVanished);
483
+ }
484
+
485
+ function unwatch_name(token) {
486
+ const entry = watchedNames.get(token);
487
+ if (!entry) return;
488
+ watchedNames.delete(token);
489
+ try {
490
+ entry.connection.signal_unsubscribe(entry.subId);
491
+ } catch {
492
+ /* connection already gone */
493
+ }
494
+ }
495
+
496
+ // ---- Gio.DBus namespace ----
497
+ const DBus = {
498
+ get: Gio.bus_get,
499
+ get_finish: Gio.bus_get_finish,
500
+ get_sync: Gio.bus_get_sync,
501
+ own_name,
502
+ own_name_on_connection,
503
+ unown_name,
504
+ watch_name,
505
+ watch_name_on_connection,
506
+ unwatch_name,
507
+ };
508
+ Object.defineProperty(DBus, 'session', {
509
+ get() {
510
+ return Gio.bus_get_sync(Gio.BusType.SESSION, null);
511
+ },
512
+ enumerable: false,
513
+ });
514
+ Object.defineProperty(DBus, 'system', {
515
+ get() {
516
+ return Gio.bus_get_sync(Gio.BusType.SYSTEM, null);
517
+ },
518
+ enumerable: false,
519
+ });
520
+
521
+ // ---- Gio.DBusProxy (adds the static makeProxyWrapper to the introspected class) ----
522
+ const DBusProxy = new Proxy(Gio.DBusProxy, {
523
+ get(target, prop) {
524
+ if (prop === 'makeProxyWrapper') return makeProxyWrapper;
525
+ return target[prop];
526
+ },
527
+ has(target, prop) {
528
+ return prop === 'makeProxyWrapper' || prop in target;
529
+ },
530
+ });
531
+
532
+ // ---- Gio.DBusExportedObject (export side) ---------------------------------
533
+ //
534
+ // Ported from refs/gjs Gio.js (_wrapJSObject + _handleMethodCall /
535
+ // _handlePropertyGet / _handlePropertySet). GJS builds this on the C type
536
+ // GjsPrivate.DBusImplementation (a GObject with handle-method-call /
537
+ // handle-property-get / handle-property-set signals); node-gi has no such type,
538
+ // so it instead drives the INTROSPECTABLE
539
+ // g_dbus_connection_register_object_with_closures2 — three GClosures whose
540
+ // marshal the engine now supports. The closure param shapes match glib's
541
+ // register_with_closures_on_* trampolines (gio/gdbusconnection.c):
542
+ // method_call: (conn, sender, path, iface, method, params:Variant, invocation)
543
+ // get_property: (conn, sender, path, iface, prop) -> Variant|null
544
+ // set_property: (conn, sender, path, iface, prop, value:Variant) -> boolean
545
+ // Ownership: register_object_data_new refs+sinks its own copy of each closure
546
+ // and frees them on unregister_object, so the JS handler (and everything it
547
+ // captures) lives exactly as long as the registration. The closures2 variant
548
+ // does NOT transfer the invocation ref to the closure — the engine's
549
+ // transfer-full-instance handling refs the invocation before a return_* call,
550
+ // matching gjs's introspection-annotation-driven behaviour.
551
+
552
+ function _makeOutSignature(args) {
553
+ let ret = '(';
554
+ for (let i = 0; i < args.length; i++) ret += args[i].signature;
555
+ return `${ret})`;
556
+ }
557
+
558
+ function _handleDBusReply(invocation, ret) {
559
+ if (ret === undefined) ret = new GLib.Variant('()', []);
560
+ try {
561
+ if (!(ret instanceof GLib.Variant)) {
562
+ const outArgs = invocation.get_method_info().out_args;
563
+ const outSignature = _makeOutSignature(outArgs);
564
+ if (outArgs.length === 1) ret = [ret];
565
+ ret = new GLib.Variant(outSignature, ret);
566
+ }
567
+ invocation.return_value(ret);
568
+ } catch (e) {
569
+ logError(e, `Exception in method call: ${invocation.get_method_name()}`);
570
+ invocation.return_dbus_error(
571
+ 'org.gnome.gjs.JSError.ValueError',
572
+ 'Service implementation returned an incorrect value type',
573
+ );
574
+ }
575
+ }
576
+
577
+ function _handleDBusError(invocation, e) {
578
+ if (e instanceof GLib.Error) {
579
+ invocation.return_gerror(e);
580
+ return;
581
+ }
582
+ let name = e && e.name ? e.name : 'Error';
583
+ if (!name.includes('.')) name = `org.gnome.gjs.JSError.${name}`;
584
+ logError(e, `Exception in method call: ${invocation.get_method_name()}`);
585
+ invocation.return_dbus_error(name, e && e.message ? e.message : String(e));
586
+ }
587
+
588
+ function _handleMethodCall(jsObj, methodName, parameters, invocation) {
589
+ // GJS appends the message's Gio.UnixFDList as a trailing argument (null when
590
+ // the call carries no fds) — mirror it so fd-aware services see the same
591
+ // shape (refs/gjs Gio.js _handleMethodCall). Gio.DBusMessage.get_unix_fd_list
592
+ // is GioUnix (fd-passing) — absent from the Gio typelib on Windows — so guard by
593
+ // method presence and pass null there (no fds to carry). POSIX byte-identical:
594
+ // where the method exists it is taken exactly as before.
595
+ const msg = invocation.get_message();
596
+ const fdList = typeof msg.get_unix_fd_list === 'function' ? msg.get_unix_fd_list() : null;
597
+ const method = jsObj[methodName];
598
+ if (typeof method === 'function') {
599
+ let retval;
600
+ try {
601
+ const args = parameters.deepUnpack();
602
+ args.push(fdList);
603
+ retval = method.apply(jsObj, args);
604
+ } catch (e) {
605
+ _handleDBusError(invocation, e);
606
+ return;
607
+ }
608
+ // A Promise-returning (async) method resolves to the reply.
609
+ if (retval && typeof retval.then === 'function') {
610
+ retval.then(
611
+ (r) => _handleDBusReply(invocation, r),
612
+ (e) => _handleDBusError(invocation, e),
613
+ );
614
+ return;
615
+ }
616
+ _handleDBusReply(invocation, retval);
617
+ return;
618
+ }
619
+
620
+ const asyncMethod = jsObj[`${methodName}Async`];
621
+ if (typeof asyncMethod === 'function') {
622
+ let ret;
623
+ try {
624
+ ret = asyncMethod.call(jsObj, parameters.deepUnpack(), invocation, fdList);
625
+ } catch (e) {
626
+ _handleDBusError(invocation, e);
627
+ return;
628
+ }
629
+ if (ret && typeof ret.catch === 'function') ret.catch((e) => _handleDBusError(invocation, e));
630
+ return;
631
+ }
632
+
633
+ logError(new Error(), `Missing handler for DBus method ${methodName}`);
634
+ invocation.return_dbus_error(
635
+ 'org.freedesktop.DBus.Error.UnknownMethod',
636
+ `Method ${methodName} is not implemented`,
637
+ );
638
+ }
639
+
640
+ function _handlePropertyGet(jsObj, info, propertyName) {
641
+ const propInfo = info.lookup_property(propertyName);
642
+ const jsval = jsObj[propertyName];
643
+ if (jsval === undefined || jsval === null) return null;
644
+ if (jsval instanceof GLib.Variant) return jsval;
645
+ return new GLib.Variant(propInfo.signature, jsval);
646
+ }
647
+
648
+ function _handlePropertySet(jsObj, propertyName, newValue) {
649
+ jsObj[propertyName] = newValue.deepUnpack();
650
+ return true;
651
+ }
652
+
653
+ function wrapJSObject(interfaceInfo, jsObj) {
654
+ let info;
655
+ if (isGioInstance(interfaceInfo, 'DBusInterfaceInfo')) info = interfaceInfo;
656
+ else info = _newInterfaceInfo(interfaceInfo);
657
+ // cache_build is required so lookup_property/method work off the info.
658
+ info.cache_build();
659
+ const ifaceName = info.name;
660
+
661
+ // Every connection this object is exported on: { connection, registrationId }.
662
+ const registrations = [];
663
+
664
+ const impl = {
665
+ get_info: () => info,
666
+ get_object_path: () => impl._objectPath,
667
+ get_connection: () => impl._connection,
668
+ _objectPath: null,
669
+ _connection: null,
670
+
671
+ export(connection, path) {
672
+ // register_object_with_closures2 (since 2.84): closures for method call /
673
+ // get property / set property. Passing JS functions marshals each to a
674
+ // real GClosure (the engine's GClosure IN-arg support). Returns the
675
+ // registration id (throws on error).
676
+ const id = connection.register_object_with_closures2(
677
+ path,
678
+ info,
679
+ (_conn, _sender, _path, _iface, method, params, invocation) =>
680
+ _handleMethodCall(jsObj, method, params, invocation),
681
+ (_conn, _sender, _path, _iface, prop) => _handlePropertyGet(jsObj, info, prop),
682
+ (_conn, _sender, _path, _iface, prop, value) => _handlePropertySet(jsObj, prop, value),
683
+ );
684
+ registrations.push({ connection, id });
685
+ impl._connection = connection;
686
+ impl._objectPath = path;
687
+ return id;
688
+ },
689
+
690
+ unexport() {
691
+ for (const { connection, id } of registrations.splice(0)) {
692
+ connection.unregister_object(id);
693
+ }
694
+ impl._connection = null;
695
+ },
696
+
697
+ unexport_from_connection(connection) {
698
+ const handle = unwrap(connection);
699
+ for (let i = registrations.length - 1; i >= 0; i--) {
700
+ if (unwrap(registrations[i].connection) === handle) {
701
+ registrations[i].connection.unregister_object(registrations[i].id);
702
+ registrations.splice(i, 1);
703
+ }
704
+ }
705
+ if (registrations.length === 0) impl._connection = null;
706
+ },
707
+
708
+ emit_signal(name, parameters = null) {
709
+ for (const { connection } of registrations) {
710
+ connection.emit_signal(null, impl._objectPath, ifaceName, name, parameters);
711
+ }
712
+ },
713
+
714
+ emit_property_changed(name, value) {
715
+ const changed = {};
716
+ changed[name] = value;
717
+ const params = new GLib.Variant('(sa{sv}as)', [ifaceName, changed, []]);
718
+ for (const { connection } of registrations) {
719
+ connection.emit_signal(
720
+ null,
721
+ impl._objectPath,
722
+ 'org.freedesktop.DBus.Properties',
723
+ 'PropertiesChanged',
724
+ params,
725
+ );
726
+ }
727
+ },
728
+
729
+ flush() {
730
+ for (const { connection } of registrations) connection.flush(null);
731
+ },
732
+ };
733
+ // camelCase aliases (GJS's DBusImplementation exposes snake_case; keep both
734
+ // so either spelling works, matching the rest of the node-gi surface).
735
+ impl.getInfo = impl.get_info;
736
+ impl.getObjectPath = impl.get_object_path;
737
+ impl.getConnection = impl.get_connection;
738
+ impl.unexportFromConnection = impl.unexport_from_connection;
739
+ impl.emitSignal = impl.emit_signal;
740
+ impl.emitPropertyChanged = impl.emit_property_changed;
741
+ return impl;
742
+ }
743
+
744
+ const DBusExportedObject = { wrapJSObject };
745
+
746
+ return { DBus, DBusProxy, DBusExportedObject };
747
+ }
748
+
749
+ export default createGioDBus;