[gnome-shell] thunderbolt: add policy provider and indicator



commit 194040dcb241a3da54f8d7b94ab83176d9e4f162
Author: Christian Kellner <christian kellner me>
Date:   Tue Dec 19 13:41:14 2017 +0100

    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.

 js/js-resources.gresource.xml |   1 +
 js/ui/panel.js                |   2 +
 js/ui/status/thunderbolt.js   | 341 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 344 insertions(+)
---
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 6834bf608..883b62d66 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -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>
diff --git a/js/ui/panel.js b/js/ui/panel.js
index d9b1a32ef..3eeb12622 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -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);
diff --git a/js/ui/status/thunderbolt.js b/js/ui/status/thunderbolt.js
new file mode 100644
index 000000000..5f148dac1
--- /dev/null
+++ b/js/ui/status/thunderbolt.js
@@ -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);
+    }
+
+});


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]