thunderbolt: add policy provider and indicator

We will now basically act as "policy provider" for thunderbolt
peripherals by using org.freedesktop.bolt service: when new
devices are connect and session is a unlocked user session
we will automatically enroll (authorize and store in the database)
them.
If new devices are connected but the session is locked a message
will be shown informing the user that the device needs to be
reconnected to properly work.
The org.freedesktop.bolt service is provided by the "bolt" daemon.
This commit is contained in:
Christian Kellner 2017-12-19 13:41:14 +01:00
parent 8fdf47ea5b
commit 194040dcb2
3 changed files with 344 additions and 0 deletions

View File

@ -132,5 +132,6 @@
<file>ui/status/bluetooth.js</file>
<file>ui/status/screencast.js</file>
<file>ui/status/system.js</file>
<file>ui/status/thunderbolt.js</file>
</gresource>
</gresources>

View File

@ -720,7 +720,9 @@ var AggregateMenu = new Lang.Class({
this._screencast = new imports.ui.status.screencast.Indicator();
this._location = new imports.ui.status.location.Indicator();
this._nightLight = new imports.ui.status.nightLight.Indicator();
this._thunderbolt = new imports.ui.status.thunderbolt.Indicator();
this._indicators.add_child(this._thunderbolt.indicators);
this._indicators.add_child(this._screencast.indicators);
this._indicators.add_child(this._location.indicators);
this._indicators.add_child(this._nightLight.indicators);

341
js/ui/status/thunderbolt.js Normal file
View File

@ -0,0 +1,341 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
// the following is a modified version of bolt/contrib/js/client.js
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Lang = imports.lang;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const Main = imports.ui.main;
const MessageTray = imports.ui.messageTray;
const PanelMenu = imports.ui.panelMenu;
/* Keep in sync with data/org.freedesktop.bolt.xml */
const BoltClientInterface = '<node> \
<interface name="org.freedesktop.bolt1.Manager"> \
<property name="Probing" type="b" access="read"></property> \
<method name="EnrollDevice"> \
<arg type="s" name="uid" direction="in"> </arg> \
<arg type="u" name="policy" direction="in"> </arg> \
<arg type="u" name="flags" direction="in"> </arg> \
<arg name="device" direction="out" type="o"> </arg> \
</method> \
<signal name="DeviceAdded"> \
<arg name="device" type="o"> </arg> \
</signal> \
</interface> \
</node>';
const BoltDeviceInterface = '<node> \
<interface name="org.freedesktop.bolt1.Device"> \
<property name="Uid" type="s" access="read"></property> \
<property name="Name" type="s" access="read"></property> \
<property name="Vendor" type="s" access="read"></property> \
<property name="Status" type="u" access="read"></property> \
<property name="SysfsPath" type="s" access="read"></property> \
<property name="Security" type="u" access="read"></property> \
<property name="Parent" type="s" access="read"></property> \
<property name="Stored" type="b" access="read"></property> \
<property name="Policy" type="u" access="read"></property> \
<property name="Key" type="u" access="read"></property> \
</interface> \
</node>';
const BoltClientProxy = Gio.DBusProxy.makeProxyWrapper(BoltClientInterface);
const BoltDeviceProxy = Gio.DBusProxy.makeProxyWrapper(BoltDeviceInterface);
/* */
var Status = {
DISCONNECTED: 0,
CONNECTED: 1,
AUTHORIZING: 2,
AUTH_ERROR: 3,
AUTHORIZED: 4,
AUTHORIZED_SECURE: 5,
AUTHORIZED_NEWKY: 6
};
var Policy = {
DEFAULT: 0,
MANUAL: 1,
AUTO:2
};
var AuthFlags = {
NONE: 0,
};
const BOLT_DBUS_NAME = 'org.freedesktop.bolt';
const BOLT_DBUS_PATH = '/org/freedesktop/bolt';
var Client = new Lang.Class({
Name: 'BoltClient',
_init: function() {
this._proxy = null;
new BoltClientProxy(
Gio.DBus.system,
BOLT_DBUS_NAME,
BOLT_DBUS_PATH,
Lang.bind(this, this._onProxyReady)
);
this.probing = false;
},
_onProxyReady: function(proxy, error) {
if (error !== null) {
log('error creating bolt proxy: %s'.format(error.message));
return;
}
this._proxy = proxy;
this._propsChangedId = this._proxy.connect('g-properties-changed', Lang.bind(this, this._onPropertiesChanged));
this._deviceAddedId = this._proxy.connectSignal('DeviceAdded', Lang.bind(this, this._onDeviceAdded), true);
this.probing = this._proxy.Probing;
if (this.probing)
this.emit('probing-changed', this.probing);
},
_onPropertiesChanged: function(proxy, properties) {
let unpacked = properties.deep_unpack();
if (!('Probing' in unpacked))
return;
this.probing = this._proxy.Probing;
this.emit('probing-changed', this.probing);
},
_onDeviceAdded: function(proxy, emitter, params) {
let [path] = params;
let device = new BoltDeviceProxy(Gio.DBus.system,
BOLT_DBUS_NAME,
path);
this.emit('device-added', device);
},
/* public methods */
close: function() {
if (!this._proxy)
return;
this._proxy.disconnectSignal(this._deviceAddedId);
this._proxy.disconnect(this._propsChangedId);
this._proxy = null;
},
enrollDevice: function(id, policy, callback) {
this._proxy.EnrollDeviceRemote(id, policy, AuthFlags.NONE,
Lang.bind(this, function (res, error) {
if (error) {
callback(null, error);
return;
}
let [path] = res;
let device = new BoltDeviceProxy(Gio.DBus.system,
BOLT_DBUS_NAME,
path);
callback(device, null);
}));
}
});
Signals.addSignalMethods(Client.prototype);
/* helper class to automatically authorize new devices */
var AuthRobot = new Lang.Class({
Name: 'BoltAuthRobot',
_init: function(client) {
this._client = client;
this._devicesToEnroll = [];
this._enrolling = false;
this._client.connect('device-added', Lang.bind(this, this._onDeviceAdded));
},
close: function() {
this.disconnectAll();
this._client = null;
},
/* the "device-added" signal will be emitted by boltd for every
* device that is not currently stored in the database. We are
* only interested in those devices, because all known devices
* will be handled by the user himself */
_onDeviceAdded: function(cli, dev) {
if (dev.Status !== Status.CONNECTED)
return;
/* check if we should enroll the device */
let res = [false];
this.emit('enroll-device', dev, res);
if (res[0] !== true)
return;
/* ok, we should authorize the device, add it to the back
* of the list */
this._devicesToEnroll.push(dev);
this._enrollDevices();
},
/* The enrollment queue:
* - new devices will be added to the end of the array.
* - an idle callback will be scheduled that will keep
* calling itself as long as there a devices to be
* enrolled.
*/
_enrollDevices: function() {
if (this._enrolling)
return;
this.enrolling = true;
GLib.idle_add(GLib.PRIORITY_DEFAULT,
Lang.bind(this, this._enrollDevicesIdle));
},
_onEnrollDone: function(device, error) {
if (error)
this.emit('enroll-failed', error, device);
/* TODO: scan the list of devices to be authorized for children
* of this device and remove them (and their children and
* their children and ....) from the device queue
*/
this._enrolling = this._devicesToEnroll.length > 0;
if (this._enrolling)
GLib.idle_add(GLib.PRIORITY_DEFAULT,
Lang.bind(this, this._enrollDevicesIdle));
},
_enrollDevicesIdle: function() {
let devices = this._devicesToEnroll;
let dev = devices.shift();
if (dev === undefined)
return GLib.SOURCE_REMOVE;
this._client.enrollDevice(dev.Uid,
Policy.DEFAULT,
Lang.bind(this, this._onEnrollDone));
return GLib.SOURCE_REMOVE;
}
});
Signals.addSignalMethods(AuthRobot.prototype);
/* eof client.js */
var Indicator = new Lang.Class({
Name: 'ThunderboltIndicator',
Extends: PanelMenu.SystemIndicator,
_init: function() {
this.parent();
this._indicator = this._addIndicator();
this._indicator.icon_name = 'thunderbolt-symbolic';
this._client = new Client();
this._client.connect('probing-changed', Lang.bind(this, this._onProbing));
this._robot = new AuthRobot(this._client);
this._robot.connect('enroll-device', Lang.bind(this, this._onEnrollDevice));
this._robot.connect('enroll-failed', Lang.bind(this, this._onEnrollFailed));
Main.sessionMode.connect('updated', Lang.bind(this, this._sync));
this._sync();
this._source = null;
},
_onDestroy: function() {
this._robot.close();
this._client.close();
},
_ensureSource: function() {
if (!this._source) {
this._source = new MessageTray.Source(_("Thunderbolt"),
'thunderbolt-symbolic');
this._source.connect('destroy', Lang.bind(this, function() {
this._source = null;
}));
Main.messageTray.add(this._source);
}
return this._source;
},
_notify: function(title, body) {
if (this._notification)
this._notification.destroy();
let source = this._ensureSource();
this._notification = new MessageTray.Notification(source, title, body);
this._notification.setUrgency(MessageTray.Urgency.HIGH);
this._notification.connect('destroy', function() {
this._notification = null;
});
this._notification.connect('activated', () => {
let app = Shell.AppSystem.get_default().lookup_app('gnome-thunderbolt-panel.desktop');
if (app)
app.activate();
});
this._source.notify(this._notification);
},
/* Session callbacks */
_sync: function() {
let active = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
this._indicator.visible = active && this._client.probing;
},
/* Bolt.Client callbacks */
_onProbing: function(cli, probing) {
if (probing)
this._indicator.icon_name = 'thunderbolt-acquiring-symbolic';
else
this._indicator.icon_name = 'thunderbolt-symbolic';
this._sync();
},
/* AuthRobot callbacks */
_onEnrollDevice: function(obj, device, policy) {
let auth = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
policy[0] = auth;
log("thunderbolt: [%s] auto enrollment: %s".format(device.Name, auth ? 'yes' : 'no'));
if (auth)
return; /* we are done */
const title = _('Unknown Thunderbolt device');
const body = _('New device has been detected while you were away. Please disconnect and reconnect the device to start using it.');
this._notify(title, body);
},
_onEnrollFailed: function (obj, device, error) {
const title = _('Thunderbolt authorization error');
const body = _('Could not authorize the thunderbolt device: %s'.format(error.message));
this._notify(title, body);
}
});