[gnome-shell-extensions/wip/rstrode/heads-up-display: 5/62] Add updates-dialog extension




commit f2d94ea367bfdcdcc9ed0346feab328c9abc5099
Author: Florian Müllner <fmuellner gnome org>
Date:   Fri Mar 4 17:07:21 2016 +0100

    Add updates-dialog extension

 extensions/updates-dialog/extension.js             | 503 +++++++++++++++++++++
 extensions/updates-dialog/meson.build              |   7 +
 extensions/updates-dialog/metadata.json.in         |  10 +
 ...ome.shell.extensions.updates-dialog.gschema.xml |  30 ++
 extensions/updates-dialog/stylesheet.css           |   1 +
 meson.build                                        |   1 +
 po/POTFILES.in                                     |   2 +
 7 files changed, 554 insertions(+)
---
diff --git a/extensions/updates-dialog/extension.js b/extensions/updates-dialog/extension.js
new file mode 100644
index 0000000..59f6dcf
--- /dev/null
+++ b/extensions/updates-dialog/extension.js
@@ -0,0 +1,503 @@
+/*
+ * Copyright (c) 2015 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* exported enable disable */
+
+const { Clutter, Gio, GLib, PackageKitGlib: PkgKit, Pango, Polkit, St } = imports.gi;
+const Signals = imports.signals;
+
+const EndSessionDialog = imports.ui.endSessionDialog;
+const ModalDialog = imports.ui.modalDialog;
+
+const ExtensionUtils = imports.misc.extensionUtils;
+const Me = ExtensionUtils.getCurrentExtension();
+
+const PkIface = '<node> \
+<interface name="org.freedesktop.PackageKit"> \
+    <method name="CreateTransaction"> \
+        <arg type="o" name="object_path" direction="out"/> \
+    </method> \
+    <signal name="UpdatesChanged"/> \
+</interface> \
+</node>';
+
+const PkOfflineIface = '<node> \
+<interface name="org.freedesktop.PackageKit.Offline"> \
+    <property name="UpdatePrepared" type="b" access="read"/> \
+    <property name="TriggerAction" type="s" access="read"/> \
+    <method name="Trigger"> \
+        <arg type="s" name="action" direction="in"/> \
+    </method> \
+    <method name="Cancel"/> \
+</interface> \
+</node>';
+
+const PkTransactionIface = '<node> \
+<interface name="org.freedesktop.PackageKit.Transaction"> \
+    <method name="SetHints"> \
+        <arg type="as" name="hints" direction="in"/> \
+    </method> \
+    <method name="GetUpdates"> \
+        <arg type="t" name="filter" direction="in"/> \
+    </method> \
+    <method name="UpdatePackages"> \
+        <arg type="t" name="transaction_flags" direction="in"/> \
+        <arg type="as" name="package_ids" direction="in"/> \
+    </method> \
+    <signal name="Package"> \
+        <arg type="u" name="info" direction="out"/> \
+        <arg type="s" name="package_id" direction="out"/> \
+        <arg type="s" name="summary" direction="out"/> \
+    </signal> \
+    <signal name="Finished"> \
+        <arg type="u" name="exit" direction="out"/> \
+        <arg type="u" name="runtime" direction="out"/> \
+    </signal> \
+</interface> \
+</node>';
+
+const LoginManagerIface = '<node> \
+<interface name="org.freedesktop.login1.Manager"> \
+<method name="Reboot"> \
+    <arg type="b" direction="in"/> \
+</method> \
+<method name="CanReboot"> \
+    <arg type="s" direction="out"/> \
+</method> \
+</interface> \
+</node>';
+
+const PkProxy = Gio.DBusProxy.makeProxyWrapper(PkIface);
+const PkOfflineProxy = Gio.DBusProxy.makeProxyWrapper(PkOfflineIface);
+const PkTransactionProxy = Gio.DBusProxy.makeProxyWrapper(PkTransactionIface);
+const LoginManagerProxy = Gio.DBusProxy.makeProxyWrapper(LoginManagerIface);
+
+let pkProxy = null;
+let pkOfflineProxy = null;
+let loginManagerProxy = null;
+let updatesDialog = null;
+let extensionSettings = null;
+let cancellable = null;
+
+let updatesCheckInProgress = false;
+let updatesCheckRequested = false;
+let securityUpdates = [];
+
+function getDetailText(period) {
+    let text = _('Important security updates need to be installed.\n');
+    if (period < 60) {
+        text += ngettext(
+            'You can close this dialog and get %d minute to finish your work.',
+            'You can close this dialog and get %d minutes to finish your work.',
+            period)
+        .format(period);
+    } else {
+        text += ngettext(
+            'You can close this dialog and get %d hour to finish your work.',
+            'You can close this dialog and get %d hours to finish your work.',
+            Math.floor(period / 60))
+        .format(Math.floor(period / 60));
+    }
+    return text;
+}
+
+const UpdatesDialog = class extends ModalDialog.ModalDialog {
+    constructor(settings) {
+        super({
+            styleClass: 'end-session-dialog',
+            destroyOnClose: false
+        });
+
+        this._gracePeriod = settings.get_uint('grace-period');
+        this._gracePeriod = Math.min(Math.max(10, this._gracePeriod), 24 * 60);
+        this._lastWarningPeriod = settings.get_uint('last-warning-period');
+        this._lastWarningPeriod = Math.min(
+            Math.max(1, this._lastWarningPeriod),
+            this._gracePeriod - 1);
+        this._lastWarnings = settings.get_uint('last-warnings');
+        this._lastWarnings = Math.min(
+            Math.max(1, this._lastWarnings),
+            Math.floor((this._gracePeriod - 1) / this._lastWarningPeriod));
+
+        let messageLayout = new St.BoxLayout({
+            vertical: true,
+            style_class: 'end-session-dialog-layout'
+        });
+        this.contentLayout.add(messageLayout, {
+            x_fill: true,
+            y_fill: true,
+            y_expand: true
+        });
+
+        let subjectLabel = new St.Label({
+            style_class: 'end-session-dialog-subject',
+            style: 'padding-bottom: 1em;',
+            text: _('Important security updates')
+        });
+        messageLayout.add(subjectLabel, {
+            x_fill: false,
+            y_fill: false,
+            x_align: St.Align.START,
+            y_align: St.Align.START
+        });
+
+        this._detailLabel = new St.Label({
+            style_class: 'end-session-dialog-description',
+            style: 'padding-bottom: 0em;',
+            text: getDetailText(this._gracePeriod)
+        });
+        this._detailLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        this._detailLabel.clutter_text.line_wrap = true;
+
+        messageLayout.add(this._detailLabel, {
+            y_fill: true,
+            y_align: St.Align.START
+        });
+
+        let buttons = [{
+            action: this.close.bind(this),
+            label: _('Close'),
+            key: Clutter.Escape
+        }, {
+            action: this._done.bind(this),
+            label: _('Restart &amp; Install')
+        }];
+
+        this.setButtons(buttons);
+
+        this._openTimeoutId = 0;
+        this.connect('destroy', this._clearOpenTimeout.bind(this));
+
+        this._startTimer();
+    }
+
+    _clearOpenTimeout() {
+        if (this._openTimeoutId > 0) {
+            GLib.source_remove(this._openTimeoutId);
+            this._openTimeoutId = 0;
+        }
+    }
+
+    tryOpen() {
+        if (this._openTimeoutId > 0 || this.open())
+            return;
+
+        this._openTimeoutId = GLib.timeout_add_seconds(
+            GLib.PRIORITY_DEFAULT, 1, () => {
+                if (!this.open())
+                    return GLib.SOURCE_CONTINUE;
+
+                this._clearOpenTimeout();
+                return GLib.SOURCE_REMOVE;
+            });
+    }
+
+    _startTimer() {
+        this._secondsLeft = this._gracePeriod * 60;
+
+        this._timerId = GLib.timeout_add_seconds(
+            GLib.PRIORITY_DEFAULT, 1, () => {
+                this._secondsLeft -= 1;
+                let minutesLeft = this._secondsLeft / 60;
+                let periodLeft = Math.floor(minutesLeft);
+
+                if (this._secondsLeft == 60 ||
+                    (periodLeft > 0 && periodLeft <= this._lastWarningPeriod * this._lastWarnings &&
+                     minutesLeft % this._lastWarningPeriod == 0)) {
+                    this.tryOpen();
+                    this._detailLabel.text = getDetailText(periodLeft);
+                }
+
+                if (this._secondsLeft > 0) {
+                    if (this._secondsLeft < 60) {
+                        let seconds = EndSessionDialog._roundSecondsToInterval(
+                            this._gracePeriod * 60, this._secondsLeft, 10);
+                        this._detailLabel.text =
+                            _('Important security updates need to be installed now.\n') +
+                            ngettext(
+                                'This computer will restart in %d second.',
+                                'This computer will restart in %d seconds.',
+                                seconds).format(seconds);
+                    }
+                    return GLib.SOURCE_CONTINUE;
+                }
+
+                this._done();
+                return GLib.SOURCE_REMOVE;
+            });
+        this.connect('destroy', () => {
+            if (this._timerId > 0) {
+                GLib.source_remove(this._timerId);
+                this._timerId = 0;
+            }
+        });
+    }
+
+    _done() {
+        this.emit('done');
+        this.destroy();
+    }
+
+    getState() {
+        return [this._gracePeriod, this._lastWarningPeriod, this._lastWarnings, this._secondsLeft];
+    }
+
+    setState(state) {
+        [this._gracePeriod, this._lastWarningPeriod, this._lastWarnings, this._secondsLeft] = state;
+    }
+};
+Signals.addSignalMethods(UpdatesDialog.prototype);
+
+function showDialog() {
+    if (updatesDialog)
+        return;
+
+    updatesDialog = new UpdatesDialog(extensionSettings);
+    updatesDialog.tryOpen();
+    updatesDialog.connect('destroy', () => updatesDialog = null);
+    updatesDialog.connect('done', () => {
+        if (pkOfflineProxy.TriggerAction == 'power-off' ||
+            pkOfflineProxy.TriggerAction == 'reboot') {
+            loginManagerProxy.RebootRemote(false);
+        } else {
+            pkOfflineProxy.TriggerRemote('reboot', (result, error) => {
+                if (!error)
+                    loginManagerProxy.RebootRemote(false);
+                else
+                    log('Failed to trigger offline update: %s'.format(error.message));
+            });
+        }
+    });
+}
+
+function cancelDialog(save) {
+    if (!updatesDialog)
+        return;
+
+    if (save) {
+        let state = GLib.Variant.new('(uuuu)', updatesDialog.getState());
+        global.set_runtime_state(Me.uuid, state);
+    }
+    updatesDialog.destroy();
+}
+
+function restoreExistingState() {
+    let state = global.get_runtime_state('(uuuu)', Me.uuid);
+    if (state === null)
+        return false;
+
+    global.set_runtime_state(Me.uuid, null);
+    showDialog();
+    updatesDialog.setState(state.deep_unpack());
+    return true;
+}
+
+function syncState() {
+    if (!pkOfflineProxy || !loginManagerProxy)
+        return;
+
+    if (restoreExistingState())
+        return;
+
+    if (!updatesCheckInProgress &&
+        securityUpdates.length > 0 &&
+        pkOfflineProxy.UpdatePrepared)
+        showDialog();
+    else
+        cancelDialog();
+}
+
+function doPkTransaction(callback) {
+    if (!pkProxy)
+        return;
+
+    pkProxy.CreateTransactionRemote((result, error) => {
+        if (error) {
+            log('Error creating PackageKit transaction: %s'.format(error.message));
+            checkUpdatesDone();
+            return;
+        }
+
+        new PkTransactionProxy(Gio.DBus.system,
+            'org.freedesktop.PackageKit',
+            String(result),
+            (proxy, error) => {
+                if (!error) {
+                    proxy.SetHintsRemote(
+                        ['background=true', 'interactive=false'],
+                        (result, error) => {
+                            if (error) {
+                                log('Error connecting to PackageKit: %s'.format(error.message));
+                                checkUpdatesDone();
+                                return;
+                            }
+                            callback(proxy);
+                        });
+                } else {
+                    log('Error connecting to PackageKit: %s'.format(error.message));
+                }
+            });
+    });
+}
+
+function pkUpdatePackages(proxy) {
+    proxy.connectSignal('Finished', (p, e, params) => {
+        let [exit, runtime_] = params;
+
+        if (exit == PkgKit.ExitEnum.CANCELLED_PRIORITY) {
+            // try again
+            checkUpdates();
+        } else if (exit != PkgKit.ExitEnum.SUCCESS) {
+            log('UpdatePackages failed: %s'.format(PkgKit.ExitEnum.to_string(exit)));
+        }
+
+        checkUpdatesDone();
+    });
+    proxy.UpdatePackagesRemote(1 << PkgKit.TransactionFlagEnum.ONLY_DOWNLOAD, securityUpdates);
+}
+
+function pkGetUpdates(proxy) {
+    proxy.connectSignal('Package', (p, e, params) => {
+        let [info, packageId, summary_] = params;
+
+        if (info == PkgKit.InfoEnum.SECURITY)
+            securityUpdates.push(packageId);
+    });
+    proxy.connectSignal('Finished', (p, e, params) => {
+        let [exit, runtime_] = params;
+
+        if (exit == PkgKit.ExitEnum.SUCCESS) {
+            if (securityUpdates.length > 0) {
+                doPkTransaction(pkUpdatePackages);
+                return;
+            }
+        } else if (exit == PkgKit.ExitEnum.CANCELLED_PRIORITY) {
+            // try again
+            checkUpdates();
+        } else {
+            log('GetUpdates failed: %s'.format(PkgKit.ExitEnum.to_string(exit)));
+        }
+
+        checkUpdatesDone();
+    });
+    proxy.GetUpdatesRemote(0);
+}
+
+function checkUpdatesDone() {
+    updatesCheckInProgress = false;
+    if (updatesCheckRequested) {
+        updatesCheckRequested = false;
+        checkUpdates();
+    } else {
+        syncState();
+    }
+}
+
+function checkUpdates() {
+    if (updatesCheckInProgress) {
+        updatesCheckRequested = true;
+        return;
+    }
+    updatesCheckInProgress = true;
+    securityUpdates = [];
+    doPkTransaction(pkGetUpdates);
+}
+
+function initSystemProxies() {
+    new PkProxy(Gio.DBus.system,
+        'org.freedesktop.PackageKit',
+        '/org/freedesktop/PackageKit',
+        (proxy, error) => {
+            if (!error) {
+                pkProxy = proxy;
+                let id = pkProxy.connectSignal('UpdatesChanged', checkUpdates);
+                pkProxy._signalId = id;
+                checkUpdates();
+            } else {
+                log('Error connecting to PackageKit: %s'.format(error.message));
+            }
+        },
+        cancellable);
+    new PkOfflineProxy(Gio.DBus.system,
+        'org.freedesktop.PackageKit',
+        '/org/freedesktop/PackageKit',
+        (proxy, error) => {
+            if (!error) {
+                pkOfflineProxy = proxy;
+                let id = pkOfflineProxy.connect('g-properties-changed', syncState);
+                pkOfflineProxy._signalId = id;
+                syncState();
+            } else {
+                log('Error connecting to PackageKit: %s'.format(error.message));
+            }
+        },
+        cancellable);
+    new LoginManagerProxy(Gio.DBus.system,
+        'org.freedesktop.login1',
+        '/org/freedesktop/login1',
+        (proxy, error) => {
+            if (!error) {
+                proxy.CanRebootRemote(cancellable, (result, error) => {
+                    if (!error && result == 'yes') {
+                        loginManagerProxy = proxy;
+                        syncState();
+                    } else {
+                        log('Reboot is not available');
+                    }
+                });
+            } else {
+                log('Error connecting to Login manager: %s'.format(error.message));
+            }
+        },
+        cancellable);
+}
+
+function enable() {
+    cancellable = new Gio.Cancellable();
+    extensionSettings = ExtensionUtils.getSettings();
+    Polkit.Permission.new('org.freedesktop.packagekit.trigger-offline-update',
+        null,
+        cancellable,
+        (p, result) => {
+            try {
+                let permission = Polkit.Permission.new_finish(result);
+                if (permission && permission.allowed)
+                    initSystemProxies();
+                else
+                    throw (new Error('not allowed'));
+            } catch (e) {
+                log('No permission to trigger offline updates: %s'.format(e.toString()));
+            }
+        });
+}
+
+function disable() {
+    cancelDialog(true);
+    cancellable.cancel();
+    cancellable = null;
+    extensionSettings = null;
+    updatesDialog = null;
+    loginManagerProxy = null;
+    if (pkOfflineProxy) {
+        pkOfflineProxy.disconnect(pkOfflineProxy._signalId);
+        pkOfflineProxy = null;
+    }
+    if (pkProxy) {
+        pkProxy.disconnectSignal(pkProxy._signalId);
+        pkProxy = null;
+    }
+}
diff --git a/extensions/updates-dialog/meson.build b/extensions/updates-dialog/meson.build
new file mode 100644
index 0000000..585c02d
--- /dev/null
+++ b/extensions/updates-dialog/meson.build
@@ -0,0 +1,7 @@
+extension_data += configure_file(
+  input: metadata_name + '.in',
+  output: metadata_name,
+  configuration: metadata_conf
+)
+
+extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')
diff --git a/extensions/updates-dialog/metadata.json.in b/extensions/updates-dialog/metadata.json.in
new file mode 100644
index 0000000..9946abb
--- /dev/null
+++ b/extensions/updates-dialog/metadata.json.in
@@ -0,0 +1,10 @@
+{
+"extension-id": "@extension_id@",
+"uuid": "@uuid@",
+"settings-schema": "@gschemaname@",
+"gettext-domain": "@gettext_domain@",
+"name": "Updates Dialog",
+"description": "Shows a modal dialog when there are software updates.",
+"shell-version": [ "@shell_current@" ],
+"url": "http://rtcm.fedorapeople.org/updates-dialog";
+}
diff --git a/extensions/updates-dialog/org.gnome.shell.extensions.updates-dialog.gschema.xml 
b/extensions/updates-dialog/org.gnome.shell.extensions.updates-dialog.gschema.xml
new file mode 100644
index 0000000..c08d33c
--- /dev/null
+++ b/extensions/updates-dialog/org.gnome.shell.extensions.updates-dialog.gschema.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+  <schema path="/org/gnome/shell/extensions/updates-dialog/"
+          id="org.gnome.shell.extensions.updates-dialog">
+    <key name="grace-period" type="u">
+      <default>300</default>
+      <summary>Grace period in minutes</summary>
+      <description>
+        When the grace period is over, the computer will automatically
+        reboot and install security updates.
+      </description>
+    </key>
+    <key name="last-warning-period" type="u">
+      <default>10</default>
+      <summary>Last warning dialog period</summary>
+      <description>
+        A last warning dialog is displayed this many minutes before
+        the automatic reboot.
+      </description>
+    </key>
+    <key name="last-warnings" type="u">
+      <default>1</default>
+      <summary>Number of last warning dialogs</summary>
+      <description>
+        How many warning dialogs are displayed. Each is displayed at
+        'last-warning-period' minute intervals.
+      </description>
+    </key>
+  </schema>
+</schemalist>
diff --git a/extensions/updates-dialog/stylesheet.css b/extensions/updates-dialog/stylesheet.css
new file mode 100644
index 0000000..25134b6
--- /dev/null
+++ b/extensions/updates-dialog/stylesheet.css
@@ -0,0 +1 @@
+/* This extensions requires no special styling */
diff --git a/meson.build b/meson.build
index e8e00dc..d129e6c 100644
--- a/meson.build
+++ b/meson.build
@@ -53,6 +53,7 @@ all_extensions += [
   'native-window-placement',
   'panel-favorites',
   'top-icons',
+  'updates-dialog',
   'user-theme'
 ]
 
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 9c1438a..55f0e9a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -10,6 +10,8 @@ extensions/native-window-placement/org.gnome.shell.extensions.native-window-plac
 extensions/places-menu/extension.js
 extensions/places-menu/placeDisplay.js
 extensions/screenshot-window-sizer/org.gnome.shell.extensions.screenshot-window-sizer.gschema.xml
+extensions/updates-dialog/extension.js
+extensions/updates-dialog/org.gnome.shell.extensions.updates-dialog.gschema.xml
 extensions/user-theme/extension.js
 extensions/user-theme/org.gnome.shell.extensions.user-theme.gschema.xml
 extensions/window-list/extension.js


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