Blob Blame History Raw
/*
 * 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;
    }
}