517 lines
14 KiB
JavaScript
517 lines
14 KiB
JavaScript
|
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
||
|
//
|
||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
|
||
|
import Gio from 'gi://Gio';
|
||
|
import GLib from 'gi://GLib';
|
||
|
import GObject from 'gi://GObject';
|
||
|
|
||
|
const SERVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect';
|
||
|
const SERVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect';
|
||
|
const DEVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect.Device';
|
||
|
|
||
|
|
||
|
const _PROPERTIES = {
|
||
|
'Connected': 'connected',
|
||
|
'EncryptionInfo': 'encryption-info',
|
||
|
'IconName': 'icon-name',
|
||
|
'Id': 'id',
|
||
|
'Name': 'name',
|
||
|
'Paired': 'paired',
|
||
|
'Type': 'type',
|
||
|
};
|
||
|
|
||
|
|
||
|
function _proxyInit(proxy, cancellable = null) {
|
||
|
if (proxy.__initialized !== undefined)
|
||
|
return Promise.resolve();
|
||
|
|
||
|
return new Promise((resolve, reject) => {
|
||
|
proxy.init_async(
|
||
|
GLib.PRIORITY_DEFAULT,
|
||
|
cancellable,
|
||
|
(proxy, res) => {
|
||
|
try {
|
||
|
proxy.init_finish(res);
|
||
|
proxy.__initialized = true;
|
||
|
resolve();
|
||
|
} catch (e) {
|
||
|
Gio.DBusError.strip_remote_error(e);
|
||
|
reject(e);
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* A simple proxy wrapper for devices exported over DBus.
|
||
|
*/
|
||
|
export const Device = GObject.registerClass({
|
||
|
GTypeName: 'GSConnectRemoteDevice',
|
||
|
Implements: [Gio.DBusInterface],
|
||
|
Properties: {
|
||
|
'connected': GObject.ParamSpec.boolean(
|
||
|
'connected',
|
||
|
'Connected',
|
||
|
'Whether the device is connected',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
null
|
||
|
),
|
||
|
'encryption-info': GObject.ParamSpec.string(
|
||
|
'encryption-info',
|
||
|
'Encryption Info',
|
||
|
'A formatted string with the local and remote fingerprints',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
null
|
||
|
),
|
||
|
'icon-name': GObject.ParamSpec.string(
|
||
|
'icon-name',
|
||
|
'Icon Name',
|
||
|
'Icon name representing the device',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
null
|
||
|
),
|
||
|
'id': GObject.ParamSpec.string(
|
||
|
'id',
|
||
|
'deviceId',
|
||
|
'The device hostname or other unique id',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
''
|
||
|
),
|
||
|
'name': GObject.ParamSpec.string(
|
||
|
'name',
|
||
|
'deviceName',
|
||
|
'The device name',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
null
|
||
|
),
|
||
|
'paired': GObject.ParamSpec.boolean(
|
||
|
'paired',
|
||
|
'Paired',
|
||
|
'Whether the device is paired',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
null
|
||
|
),
|
||
|
'type': GObject.ParamSpec.string(
|
||
|
'type',
|
||
|
'deviceType',
|
||
|
'The device type',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
null
|
||
|
),
|
||
|
},
|
||
|
}, class Device extends Gio.DBusProxy {
|
||
|
|
||
|
_init(service, object_path) {
|
||
|
this._service = service;
|
||
|
|
||
|
super._init({
|
||
|
g_connection: service.g_connection,
|
||
|
g_name: SERVICE_NAME,
|
||
|
g_object_path: object_path,
|
||
|
g_interface_name: DEVICE_NAME,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
vfunc_g_properties_changed(changed, invalidated) {
|
||
|
try {
|
||
|
for (const name in changed.deepUnpack())
|
||
|
this.notify(_PROPERTIES[name]);
|
||
|
} catch (e) {
|
||
|
logError(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_get(name, fallback = null) {
|
||
|
try {
|
||
|
return this.get_cached_property(name).unpack();
|
||
|
} catch (e) {
|
||
|
return fallback;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
get connected() {
|
||
|
return this._get('Connected', false);
|
||
|
}
|
||
|
|
||
|
get encryption_info() {
|
||
|
return this._get('EncryptionInfo', '');
|
||
|
}
|
||
|
|
||
|
get icon_name() {
|
||
|
return this._get('IconName', 'computer');
|
||
|
}
|
||
|
|
||
|
get id() {
|
||
|
return this._get('Id', '0');
|
||
|
}
|
||
|
|
||
|
get name() {
|
||
|
return this._get('Name', 'Unknown');
|
||
|
}
|
||
|
|
||
|
get paired() {
|
||
|
return this._get('Paired', false);
|
||
|
}
|
||
|
|
||
|
get service() {
|
||
|
return this._service;
|
||
|
}
|
||
|
|
||
|
get type() {
|
||
|
return this._get('Type', 'desktop');
|
||
|
}
|
||
|
|
||
|
async start() {
|
||
|
try {
|
||
|
await _proxyInit(this);
|
||
|
|
||
|
// For GActions & GMenu we pass the service's name owner to avoid
|
||
|
// any mixup with instances.
|
||
|
this.action_group = Gio.DBusActionGroup.get(
|
||
|
this.g_connection,
|
||
|
this.service.g_name_owner,
|
||
|
this.g_object_path
|
||
|
);
|
||
|
|
||
|
this.menu = Gio.DBusMenuModel.get(
|
||
|
this.g_connection,
|
||
|
this.service.g_name_owner,
|
||
|
this.g_object_path
|
||
|
);
|
||
|
|
||
|
// Poke the GMenu to ensure it's ready for us
|
||
|
await new Promise((resolve, reject) => {
|
||
|
this.g_connection.call(
|
||
|
SERVICE_NAME,
|
||
|
this.g_object_path,
|
||
|
'org.gtk.Menus',
|
||
|
'Start',
|
||
|
new GLib.Variant('(au)', [[0]]),
|
||
|
null,
|
||
|
Gio.DBusCallFlags.NONE,
|
||
|
-1,
|
||
|
null,
|
||
|
(proxy, res) => {
|
||
|
try {
|
||
|
resolve(proxy.call_finish(res));
|
||
|
} catch (e) {
|
||
|
Gio.DBusError.strip_remote_error(e);
|
||
|
reject(e);
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
});
|
||
|
} catch (e) {
|
||
|
this.destroy();
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
destroy() {
|
||
|
GObject.signal_handlers_destroy(this);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
|
||
|
/**
|
||
|
* A simple proxy wrapper for the GSConnect service.
|
||
|
*/
|
||
|
export const Service = GObject.registerClass({
|
||
|
GTypeName: 'GSConnectRemoteService',
|
||
|
Implements: [Gio.DBusInterface],
|
||
|
Properties: {
|
||
|
'active': GObject.ParamSpec.boolean(
|
||
|
'active',
|
||
|
'Active',
|
||
|
'Whether the service is active',
|
||
|
GObject.ParamFlags.READABLE,
|
||
|
false
|
||
|
),
|
||
|
},
|
||
|
Signals: {
|
||
|
'device-added': {
|
||
|
flags: GObject.SignalFlags.RUN_FIRST,
|
||
|
param_types: [Device.$gtype],
|
||
|
},
|
||
|
'device-removed': {
|
||
|
flags: GObject.SignalFlags.RUN_FIRST,
|
||
|
param_types: [Device.$gtype],
|
||
|
},
|
||
|
},
|
||
|
}, class Service extends Gio.DBusProxy {
|
||
|
|
||
|
_init() {
|
||
|
super._init({
|
||
|
g_bus_type: Gio.BusType.SESSION,
|
||
|
g_name: SERVICE_NAME,
|
||
|
g_object_path: SERVICE_PATH,
|
||
|
g_interface_name: 'org.freedesktop.DBus.ObjectManager',
|
||
|
g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION,
|
||
|
});
|
||
|
|
||
|
this._active = false;
|
||
|
this._devices = new Map();
|
||
|
this._starting = false;
|
||
|
|
||
|
// Watch the service
|
||
|
this._nameOwnerChangedId = this.connect(
|
||
|
'notify::g-name-owner',
|
||
|
this._onNameOwnerChanged.bind(this)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
get active() {
|
||
|
return this._active;
|
||
|
}
|
||
|
|
||
|
get devices() {
|
||
|
return Array.from(this._devices.values());
|
||
|
}
|
||
|
|
||
|
vfunc_g_signal(sender_name, signal_name, parameters) {
|
||
|
try {
|
||
|
// Don't emit signals until the ObjectManager has started
|
||
|
if (!this.active)
|
||
|
return;
|
||
|
|
||
|
parameters = parameters.deepUnpack();
|
||
|
|
||
|
switch (true) {
|
||
|
case (signal_name === 'InterfacesAdded'):
|
||
|
this._onInterfacesAdded(...parameters);
|
||
|
break;
|
||
|
|
||
|
case (signal_name === 'InterfacesRemoved'):
|
||
|
this._onInterfacesRemoved(...parameters);
|
||
|
break;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
logError(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* org.freedesktop.DBus.ObjectManager.InterfacesAdded
|
||
|
*
|
||
|
* @param {string} object_path - Path interfaces have been added to
|
||
|
* @param {Object} interfaces - A dictionary of interface objects
|
||
|
*/
|
||
|
async _onInterfacesAdded(object_path, interfaces) {
|
||
|
try {
|
||
|
// An empty list means only the object has been added
|
||
|
if (Object.values(interfaces).length === 0)
|
||
|
return;
|
||
|
|
||
|
// Skip existing proxies
|
||
|
if (this._devices.has(object_path))
|
||
|
return;
|
||
|
|
||
|
// Create a proxy
|
||
|
const device = new Device(this, object_path);
|
||
|
await device.start();
|
||
|
|
||
|
// Hold the proxy and emit ::device-added
|
||
|
this._devices.set(object_path, device);
|
||
|
this.emit('device-added', device);
|
||
|
} catch (e) {
|
||
|
logError(e, object_path);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* org.freedesktop.DBus.ObjectManager.InterfacesRemoved
|
||
|
*
|
||
|
* @param {string} object_path - Path interfaces have been removed from
|
||
|
* @param {string[]} interfaces - List of interface names removed
|
||
|
*/
|
||
|
_onInterfacesRemoved(object_path, interfaces) {
|
||
|
try {
|
||
|
// An empty interface list means the object is being removed
|
||
|
if (interfaces.length === 0)
|
||
|
return;
|
||
|
|
||
|
// Get the proxy
|
||
|
const device = this._devices.get(object_path);
|
||
|
|
||
|
if (device === undefined)
|
||
|
return;
|
||
|
|
||
|
// Release the proxy and emit ::device-removed
|
||
|
this._devices.delete(object_path);
|
||
|
this.emit('device-removed', device);
|
||
|
|
||
|
// Destroy the device and force disposal
|
||
|
device.destroy();
|
||
|
} catch (e) {
|
||
|
logError(e, object_path);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async _addDevices() {
|
||
|
const objects = await new Promise((resolve, reject) => {
|
||
|
this.call(
|
||
|
'GetManagedObjects',
|
||
|
null,
|
||
|
Gio.DBusCallFlags.NONE,
|
||
|
-1,
|
||
|
null,
|
||
|
(proxy, res) => {
|
||
|
try {
|
||
|
const variant = proxy.call_finish(res);
|
||
|
resolve(variant.deepUnpack()[0]);
|
||
|
} catch (e) {
|
||
|
Gio.DBusError.strip_remote_error(e);
|
||
|
reject(e);
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
});
|
||
|
|
||
|
for (const [object_path, object] of Object.entries(objects))
|
||
|
await this._onInterfacesAdded(object_path, object);
|
||
|
}
|
||
|
|
||
|
_clearDevices() {
|
||
|
for (const [object_path, device] of this._devices) {
|
||
|
this._devices.delete(object_path);
|
||
|
this.emit('device-removed', device);
|
||
|
device.destroy();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async _onNameOwnerChanged() {
|
||
|
try {
|
||
|
// If the service stopped, remove each device and mark it inactive
|
||
|
if (this.g_name_owner === null) {
|
||
|
this._clearDevices();
|
||
|
|
||
|
this._active = false;
|
||
|
this.notify('active');
|
||
|
|
||
|
// If the service started, mark it active and add each device
|
||
|
} else {
|
||
|
this._active = true;
|
||
|
this.notify('active');
|
||
|
|
||
|
await this._addDevices();
|
||
|
}
|
||
|
} catch (e) {
|
||
|
logError(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reload all devices without affecting the remote service. This amounts to
|
||
|
* removing and adding each device while emitting the appropriate signals.
|
||
|
*/
|
||
|
async reload() {
|
||
|
try {
|
||
|
if (this._starting === false) {
|
||
|
this._starting = true;
|
||
|
|
||
|
this._clearDevices();
|
||
|
await _proxyInit(this);
|
||
|
await this._onNameOwnerChanged();
|
||
|
|
||
|
this._starting = false;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
this._starting = false;
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Start the service
|
||
|
*/
|
||
|
async start() {
|
||
|
try {
|
||
|
if (this._starting === false && this.active === false) {
|
||
|
this._starting = true;
|
||
|
|
||
|
await _proxyInit(this);
|
||
|
await this._onNameOwnerChanged();
|
||
|
|
||
|
// Activate the service if it's not already running
|
||
|
if (!this.active) {
|
||
|
await new Promise((resolve, reject) => {
|
||
|
this.g_connection.call(
|
||
|
SERVICE_NAME,
|
||
|
SERVICE_PATH,
|
||
|
'org.freedesktop.Application',
|
||
|
'Activate',
|
||
|
GLib.Variant.new('(a{sv})', [{}]),
|
||
|
null,
|
||
|
Gio.DBusCallFlags.NONE,
|
||
|
-1,
|
||
|
null,
|
||
|
(proxy, res) => {
|
||
|
try {
|
||
|
resolve(proxy.call_finish(res));
|
||
|
} catch (e) {
|
||
|
Gio.DBusError.strip_remote_error(e);
|
||
|
reject(e);
|
||
|
}
|
||
|
}
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
this._starting = false;
|
||
|
}
|
||
|
} catch (e) {
|
||
|
this._starting = false;
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stop the service
|
||
|
*/
|
||
|
stop() {
|
||
|
if (this.active)
|
||
|
this.activate_action('quit');
|
||
|
}
|
||
|
|
||
|
activate_action(name, parameter = null) {
|
||
|
try {
|
||
|
const paramArray = [];
|
||
|
|
||
|
if (parameter instanceof GLib.Variant)
|
||
|
paramArray[0] = parameter;
|
||
|
|
||
|
const connection = this.g_connection || Gio.DBus.session;
|
||
|
|
||
|
connection.call(
|
||
|
SERVICE_NAME,
|
||
|
SERVICE_PATH,
|
||
|
'org.freedesktop.Application',
|
||
|
'ActivateAction',
|
||
|
GLib.Variant.new('(sava{sv})', [name, paramArray, {}]),
|
||
|
null,
|
||
|
Gio.DBusCallFlags.NONE,
|
||
|
-1,
|
||
|
null,
|
||
|
null
|
||
|
);
|
||
|
} catch (e) {
|
||
|
logError(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
destroy() {
|
||
|
if (this._nameOwnerChangedId > 0) {
|
||
|
this.disconnect(this._nameOwnerChangedId);
|
||
|
this._nameOwnerChangedId = 0;
|
||
|
|
||
|
this._clearDevices();
|
||
|
this._active = false;
|
||
|
|
||
|
GObject.signal_handlers_destroy(this);
|
||
|
}
|
||
|
}
|
||
|
});
|