summaryrefslogtreecommitdiff
path: root/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service')
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/action.js217
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/control.js235
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/shutdown-timer-dbus.js72
-rw-r--r--raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js286
4 files changed, 810 insertions, 0 deletions
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/action.js b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/action.js
new file mode 100644
index 0000000..4b5bf9f
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/action.js
@@ -0,0 +1,217 @@
+// SPDX-FileCopyrightText: 2023 Deminder <tremminder@gmail.com>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Gio from 'gi://Gio';
+import * as Control from './control.js';
+import { proxyPromise } from '../modules/util.js';
+import { pgettext as C_, gettext as _ } from '../modules/translation.js';
+import { logDebug } from '../modules/util.js';
+
+export const ACTIONS = {
+ PowerOff: 0,
+ Reboot: 1,
+ Suspend: 2,
+ SuspendThenHibernate: 3,
+ Hibernate: 4,
+ HybridSleep: 5,
+ Halt: 6,
+};
+
+export const WAKE_ACTIONS = { wake: 100, 'no-wake': 101 };
+
+/**
+ * Get supported actions.
+ * In order to show an error when shutdown or reboot are not supported
+ * they are always included here. */
+export async function* supportedActions() {
+ const actionDbus = new Action();
+ for await (const action of Object.keys(ACTIONS).map(async a =>
+ ['PowerOff', 'Reboot'].includes(a) ||
+ (await actionDbus.canShutdownAction(a))
+ ? a
+ : null
+ )) {
+ if (action) {
+ yield action;
+ }
+ }
+}
+
+export class UnsupportedActionError extends Error {}
+
+export class Action {
+ #cancellable = new Gio.Cancellable();
+ #cookie = null;
+
+ #loginProxy = proxyPromise(
+ 'org.freedesktop.login1.Manager',
+ Gio.DBus.system,
+ 'org.freedesktop.login1',
+ '/org/freedesktop/login1',
+ this.#cancellable
+ );
+
+ #screenSaverProxy = proxyPromise(
+ 'org.gnome.ScreenSaver',
+ Gio.DBus.session,
+ 'org.gnome.ScreenSaver',
+ '/org/gnome/ScreenSaver',
+ this.#cancellable
+ );
+
+ #sessionProxy = proxyPromise(
+ 'org.gnome.SessionManager',
+ Gio.DBus.session,
+ 'org.gnome.SessionManager',
+ '/org/gnome/SessionManager',
+ this.#cancellable
+ );
+
+ destroy() {
+ if (this.#cancellable !== null) {
+ this.#cancellable.cancel();
+ this.#cancellable = null;
+ }
+ }
+
+ #poweroffOrReboot(action) {
+ return [ACTIONS.PowerOff, ACTIONS.Reboot].includes(ACTIONS[action]);
+ }
+
+ /**
+ * Perform the shutdown action.
+ *
+ * @param {string} action the shutdown action
+ * @param {boolean} showEndSessionDialog show the end session dialog or directly shutdown
+ *
+ * @returns {Promise} resolves on action completion
+ */
+ async shutdownAction(action, showEndSessionDialog) {
+ if (!(action in ACTIONS))
+ throw new Error(`Unknown shutdown action: ${action}`);
+ logDebug('[shutdownAction]', action);
+
+ await this.uninhibitSuspend();
+
+ const screenSaverProxy = await this.#screenSaverProxy;
+ const [screenSaverActive] = await screenSaverProxy.GetActiveAsync();
+ if (
+ showEndSessionDialog &&
+ !screenSaverActive &&
+ this.#poweroffOrReboot(action)
+ ) {
+ const sessionProxy = await this.#sessionProxy;
+ if (action === 'PowerOff') {
+ await sessionProxy.ShutdownAsync();
+ } else {
+ await sessionProxy.RebootAsync();
+ }
+ } else {
+ const loginProxy = await this.#loginProxy;
+ if (await this.canShutdownAction(action)) {
+ await loginProxy[`${action}Async`](true);
+ } else if (this.#poweroffOrReboot(action)) {
+ await Control.shutdown('now', ACTIONS[action] === ACTIONS.Reboot);
+ } else {
+ throw new UnsupportedActionError();
+ }
+ }
+ }
+
+ /**
+ * Check if a shutdown action can be performed (without authentication).
+ *
+ * @returns {Promise} resolves to `true` if action can be performed, otherwise `false`.
+ */
+ async canShutdownAction(action) {
+ const loginProxy = await this.#loginProxy;
+ if (!(action in ACTIONS))
+ throw new Error(`Unknown shutdown action: ${action}`);
+ const [result] = await loginProxy[`Can${action}Async`]();
+ return result === 'yes';
+ }
+
+ /**
+ * Schedule a wake after some minutes or cancel
+ *
+ * @param {boolean} wake
+ * @param {number} minutes
+ */
+ async wakeAction(wake, minutes) {
+ if (wake) {
+ await Control.wake(minutes);
+ } else {
+ await Control.wakeCancel();
+ }
+ }
+
+ async inhibitSuspend() {
+ if (this.#cookie === null) {
+ const sessionProxy = await this.#sessionProxy;
+ const [cookie] = await sessionProxy.InhibitAsync(
+ 'user',
+ 0,
+ 'Inhibit by Shutdown Timer (GNOME-Shell extension)',
+ /* Suspend flag */ 4
+ );
+ this.#cookie = cookie;
+ }
+ }
+
+ async uninhibitSuspend() {
+ if (this.#cookie !== null) {
+ const sessionProxy = await this.#sessionProxy;
+ await sessionProxy.UninhibitAsync(this.#cookie);
+ this.#cookie = null;
+ }
+ }
+}
+
+/**
+ * Get the translated action label
+ *
+ * @param action
+ */
+export function actionLabel(action) {
+ return {
+ SuspendThenHibernate: _('Suspend then Hibernate'),
+ HybridSleep: _('Hybrid Sleep'),
+ Hibernate: _('Hibernate'),
+ Halt: _('Halt'),
+ Suspend: _('Suspend'),
+ PowerOff: _('Power Off'),
+ Reboot: _('Restart'),
+ wake: _('Wake'),
+ 'no-wake': _('No Wake'),
+ }[action];
+}
+
+export function untilText(action) {
+ return {
+ SuspendThenHibernate: C_('untiltext', 'suspend and hibernate'),
+ HybridSleep: C_('untiltext', 'hybrid sleep'),
+ Hibernate: C_('untiltext', 'hibernate'),
+ Halt: C_('untiltext', 'halt'),
+ Suspend: C_('untiltext', 'suspend'),
+ PowerOff: C_('untiltext', 'shutdown'),
+ Reboot: C_('untiltext', 'reboot'),
+ wake: C_('untiltext', 'wakeup'),
+ }[action];
+}
+
+export function mapLegacyAction(action) {
+ return action in ACTIONS || ['wake', ''].includes(action)
+ ? action
+ : {
+ poweroff: 'PowerOff',
+ shutdown: 'PowerOff',
+ reboot: 'Reboot',
+ suspend: 'Suspend',
+ }[action] ??
+ {
+ p: 'PowerOff',
+ r: 'Reboot',
+ s: 'Suspend',
+ h: 'SuspendThenHibernate',
+ }[action[0].toLowerCase()];
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/control.js b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/control.js
new file mode 100644
index 0000000..28d184a
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/control.js
@@ -0,0 +1,235 @@
+// SPDX-FileCopyrightText: 2023 Deminder <tremminder@gmail.com>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+import { gettext as _ } from '../modules/translation.js';
+
+import { logDebug } from '../modules/util.js';
+
+function readLine(stream, cancellable) {
+ return new Promise((resolve, reject) => {
+ stream.read_line_async(0, cancellable, (s, res) => {
+ try {
+ const line = s.read_line_finish_utf8(res)[0];
+
+ if (line !== null) {
+ resolve(line);
+ } else {
+ reject(new Error('No line was read!'));
+ }
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+}
+
+function quoteEscape(str) {
+ return str.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
+}
+
+/**
+ * Execute a command asynchronously and check the exit status.
+ *
+ * If given, @cancellable can be used to stop the process before it finishes.
+ *
+ * @param {string[] | string} argv - a list of string arguments or command line that will be parsed
+ * @param {Gio.Cancellable} [cancellable] - optional cancellable object
+ * @param {boolean} shell - run command as shell command
+ * @param logFunc
+ * @returns {Promise<void>} - The process success
+ */
+export function execCheck(
+ argv,
+ cancellable = null,
+ shell = true,
+ logFunc = undefined
+) {
+ if (!shell && typeof argv === 'string') {
+ argv = GLib.shell_parse_argv(argv)[1];
+ }
+
+ const isRootProc = argv[0] && argv[0].endsWith('pkexec');
+
+ if (shell && Array.isArray(argv)) {
+ argv = argv.map(c => `"${quoteEscape(c)}"`).join(' ');
+ }
+ let cancelId = 0;
+ let proc = new Gio.Subprocess({
+ argv: (shell ? ['/bin/sh', '-c'] : []).concat(argv),
+ flags:
+ logFunc !== undefined
+ ? Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
+ : Gio.SubprocessFlags.NONE,
+ });
+ proc.init(cancellable);
+
+ if (cancellable instanceof Gio.Cancellable) {
+ cancelId = cancellable.connect(() => {
+ if (logFunc !== undefined) {
+ if (isRootProc) {
+ logFunc(`# ${_('Can not cancel root process!')}`);
+ } else {
+ logFunc(`[${_('CANCEL')}]`);
+ }
+ }
+ proc.force_exit();
+ });
+ }
+ let stdoutStream = null;
+ let stderrStream = null;
+ let readLineCancellable = null;
+
+ if (logFunc !== undefined) {
+ readLineCancellable = new Gio.Cancellable();
+ stdoutStream = new Gio.DataInputStream({
+ base_stream: proc.get_stdout_pipe(),
+ close_base_stream: true,
+ });
+
+ stderrStream = new Gio.DataInputStream({
+ base_stream: proc.get_stderr_pipe(),
+ close_base_stream: true,
+ });
+ const readNextLine = async (stream, prefix) => {
+ try {
+ const line = await readLine(stream, readLineCancellable);
+ logFunc(prefix + line);
+ logDebug(line);
+ await readNextLine(stream, prefix);
+ } catch {
+ if (!stream.is_closed()) {
+ stream.close_async(0, null, (s, sRes) => {
+ try {
+ s.close_finish(sRes);
+ } catch (e) {
+ logDebug(`[StreamCloseError] ${e}`);
+ }
+ });
+ }
+ }
+ };
+ // read stdout and stderr asynchronously
+ readNextLine(stdoutStream, '');
+ readNextLine(stderrStream, '# ');
+ }
+
+ return new Promise((resolve, reject) => {
+ proc.wait_check_async(null, (p, res) => {
+ try {
+ const success = p.wait_check_finish(res);
+ if (!success) {
+ let status = p.get_exit_status();
+
+ throw new Gio.IOErrorEnum({
+ code: Gio.io_error_from_errno(status),
+ message: GLib.strerror(status),
+ });
+ }
+
+ resolve();
+ } catch (e) {
+ reject(e);
+ } finally {
+ if (readLineCancellable) readLineCancellable.cancel();
+ readLineCancellable = null;
+ if (cancelId > 0) cancellable.disconnect(cancelId);
+ }
+ });
+ });
+}
+
+export function sleepUntilDeadline(deadlineSeconds, cancellable) {
+ return new Promise((resolve, reject) => {
+ const secondsLeft = () =>
+ deadlineSeconds - GLib.DateTime.new_now_utc().to_unix();
+
+ let timeoutId = 0;
+ let handlerId = cancellable.connect(() => {
+ handlerId = 0;
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ reject(new Error('Sleep until deadline cancelled!'));
+ });
+
+ const continueSleep = () => {
+ timeoutId = setTimeout(() => {
+ timeoutId = 0;
+ if (secondsLeft() > 0) {
+ continueSleep();
+ } else {
+ if (handlerId) {
+ cancellable.disconnect(handlerId);
+ }
+ resolve();
+ }
+ }, 1000);
+ };
+ continueSleep();
+ });
+}
+
+export function installedScriptPath() {
+ for (const name of [
+ 'shutdowntimerctl',
+ `shutdowntimerctl-${GLib.get_user_name()}`,
+ ]) {
+ const standard = GLib.find_program_in_path(name);
+ if (standard !== null) {
+ return standard;
+ }
+ for (const bindir of ['/usr/local/bin/', '/usr/bin/']) {
+ const path = bindir + name;
+ logDebug(`Looking for: ${path}`);
+ if (Gio.File.new_for_path(path).query_exists(null)) {
+ return path;
+ }
+ }
+ }
+ return null;
+}
+
+function execControlScript(args, noScriptArgs) {
+ const installedScript = installedScriptPath();
+ if (installedScript !== null) {
+ return execCheck(['pkexec', installedScript].concat(args), null, false);
+ }
+ if (noScriptArgs === undefined) {
+ throw new Error(_('Privileged script installation required!'));
+ }
+ return execCheck(noScriptArgs, null, false);
+}
+
+export function shutdown(minutes, reboot = false) {
+ logDebug(`[root-shutdown] ${minutes} minutes, reboot: ${reboot}`);
+ return execControlScript(
+ [reboot ? 'reboot' : 'shutdown', `${minutes}`],
+ ['shutdown', reboot ? '-r' : '-P', `${minutes}`]
+ );
+}
+
+function shutdownCancel() {
+ logDebug('[root-shutdown] cancel');
+ return execControlScript(['shutdown-cancel'], ['shutdown', '-c']);
+}
+
+export function wake(minutes) {
+ return execControlScript(['wake', `${minutes}`]);
+}
+
+export function wakeCancel() {
+ return execControlScript(['wake-cancel']);
+}
+
+export async function stopRootModeProtection(info) {
+ if (['PowerOff', 'Reboot'].includes(info.mode)) {
+ await shutdownCancel();
+ }
+}
+export async function startRootModeProtection(info) {
+ if (['PowerOff', 'Reboot'].includes(info.mode)) {
+ await shutdown(Math.max(0, info.minutes) + 1, info.mode === 'Reboot');
+ }
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/shutdown-timer-dbus.js b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/shutdown-timer-dbus.js
new file mode 100644
index 0000000..8db37a4
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/shutdown-timer-dbus.js
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: 2023 Deminder <tremminder@gmail.com>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+import { loadInterfaceXML, logDebug } from '../modules/util.js';
+import { Timer } from './timer.js';
+
+export const ShutdownTimerName = 'org.gnome.Shell.Extensions.ShutdownTimer';
+export const ShutdownTimerObjectPath =
+ '/org/gnome/Shell/Extensions/ShutdownTimer';
+export const ShutdownTimerIface = await loadInterfaceXML(ShutdownTimerName);
+
+export class ShutdownTimerDBus {
+ constructor({ settings }) {
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(
+ ShutdownTimerIface,
+ this
+ );
+ this._dbusImpl.export(Gio.DBus.session, ShutdownTimerObjectPath);
+
+ this._timer = new Timer({ settings });
+ this._timer.connect('message', (_, msg) => {
+ logDebug('[shutdown-timer-dbus] msg', msg);
+ this._dbusImpl.emit_signal('OnMessage', new GLib.Variant('(s)', [msg]));
+ });
+ this._timer.connect('change', () => {
+ logDebug('[shutdown-timer-dbus] state', this._timer.state);
+ this._dbusImpl.emit_signal(
+ 'OnStateChange',
+ new GLib.Variant('(s)', [this._timer.state])
+ );
+ });
+ this._timer.connect('change-external', () => {
+ this._dbusImpl.emit_signal('OnExternalChange', null);
+ });
+ }
+
+ async ScheduleShutdownAsync(parameters, invocation) {
+ const [shutdown, action] = parameters;
+ logDebug(
+ '[sdt-dbus] [ScheduleShutdownAsync] shutdown',
+ shutdown,
+ 'action',
+ action
+ );
+ await this._timer.toggleShutdown(shutdown, action);
+ invocation.return_value(null);
+ }
+
+ async ScheduleWakeAsync(parameters, invocation) {
+ const [wake] = parameters;
+ logDebug('[sdt-dbus] [ScheduleWakeAsync] wake', wake);
+ await this._timer.toggleWake(wake);
+ invocation.return_value(null);
+ }
+
+ GetStateAsync(_, invocation) {
+ logDebug('[sdt-dbus] [GetStateAsync]');
+ invocation.return_value(new GLib.Variant('(s)', [this._timer.state]));
+ return Promise.resolve();
+ }
+
+ destroy() {
+ logDebug('[sdt-dbus] destroy');
+ this._dbusImpl.unexport();
+ this._dbusImpl = null;
+ this._timer.destroy();
+ this._timer = null;
+ }
+}
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js
new file mode 100644
index 0000000..6c439ab
--- /dev/null
+++ b/raveos-theme/gnome/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js
@@ -0,0 +1,286 @@
+// SPDX-FileCopyrightText: 2023 Deminder <tremminder@gmail.com>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+import {
+ gettext as _,
+ ngettext as _n,
+ pgettext as C_,
+} from '../modules/translation.js';
+
+import { logDebug, throttleTimeout } from '../modules/util.js';
+import * as Control from './control.js';
+import {
+ longDurationString,
+ ScheduleInfo,
+ getShutdownScheduleFromSettings,
+ getSliderMinutesFromSettings,
+} from '../modules/schedule-info.js';
+import {
+ actionLabel,
+ Action,
+ mapLegacyAction,
+ UnsupportedActionError,
+} from './action.js';
+
+const Signals = imports.signals;
+
+export class Timer {
+ _timerCancellable = null;
+ _updateScheduleCancel = null;
+ _action = new Action();
+ info = {
+ externalShutdown: new ScheduleInfo({
+ external: true,
+ }),
+ externalWake: new ScheduleInfo({
+ external: false,
+ mode: 'wake',
+ }),
+ internalShutdown: new ScheduleInfo({ mode: 'PowerOff' }),
+ };
+
+ constructor({ settings }) {
+ this._settings = settings;
+
+ const [updateScheduleThrottled, updateScheduleCancel] = throttleTimeout(
+ () => this.updateSchedule(),
+ 20
+ );
+ this._updateScheduleCancel = updateScheduleCancel;
+ this._settingsIds = [
+ 'root-mode-value',
+ 'shutdown-timestamp-value',
+ 'shutdown-mode-value',
+ ].map(settingName =>
+ settings.connect(`changed::${settingName}`, updateScheduleThrottled)
+ );
+
+ this.updateSchedule();
+ }
+
+ destroy() {
+ if (this._settings === null) {
+ throw new Error('should not destroy twice');
+ }
+ // Disconnect settings
+ this._settingsIds.forEach(id => this._settings.disconnect(id));
+ this._settingsIds = [];
+ this._settings = null;
+
+ // Cancel schedule updates
+ if (this._updateScheduleCancel !== null) {
+ this._updateScheduleCancel();
+ this._updateScheduleCancel = null;
+ }
+
+ // Cancel internal timer
+ if (this._timerCancellable !== null) {
+ this._timerCancellable.cancel();
+ this._timerCancellable = null;
+ }
+
+ // External schedules (for 'shutdown' and 'wake') are not stopped
+ }
+
+ updateSchedule() {
+ const oldInternal = this.info.internalShutdown;
+ const internal = getShutdownScheduleFromSettings(this._settings);
+ this.info.internalShutdown = internal;
+ logDebug(
+ `[updateSchedule] internal schedule: ${internal.label} (old internal schedule: ${oldInternal.label})`
+ );
+ this._updateRootModeProtection(oldInternal);
+ if (
+ internal.mode !== oldInternal.mode ||
+ internal.deadline !== oldInternal.deadline
+ ) {
+ this.emit('change');
+ if (internal.scheduled) {
+ if (internal.minutes > 0) {
+ // Show schedule info
+ this.emit(
+ 'message',
+ C_('StartSchedulePopup', '%s in %s').format(
+ actionLabel(internal.mode),
+ longDurationString(
+ internal.minutes,
+ h => _n('%s hour', '%s hours', h),
+ m => _n('%s minute', '%s minutes', m)
+ )
+ )
+ );
+ } else {
+ logDebug(`[updateSchedule] hidden message for '< 1 minute' schedule`);
+ }
+ if (this._timerCancellable !== null) {
+ this._timerCancellable.cancel();
+ this._timerCancellable = null;
+ }
+ this.executeActionDelayed()
+ .then(() => {
+ logDebug('[executeActionDelayed] done');
+ })
+ .catch(err => {
+ console.error('executeActionDelayed', err);
+ });
+ } else {
+ if (this._timerCancellable !== null) {
+ this._timerCancellable.cancel();
+ this._timerCancellable = null;
+ }
+ if (oldInternal.scheduled) {
+ this.emit('message', _('Shutdown Timer stopped'));
+ }
+ }
+ }
+ }
+
+ async executeAction() {
+ const internal = this.info.internalShutdown;
+ if (!internal.scheduled) {
+ logDebug(`Refusing to exectute non scheduled action! '${internal.mode}'`);
+ return;
+ }
+ logDebug(`Running '${internal.mode}' timer action...`);
+ try {
+ this.emit('change');
+ // Refresh root mode protection
+ await Promise.all([
+ this._updateRootModeProtection(),
+ // Do shutdown
+ this._action.shutdownAction(
+ internal.mode,
+ this._settings.get_boolean('show-end-session-dialog-value')
+ ),
+ ]);
+ } catch (err) {
+ if (/* destroyed */ this._settings === null) {
+ throw err;
+ }
+ const newInternal = this.info.internalShutdown;
+ if (newInternal.scheduled && newInternal.secondsLeft > 0) {
+ logDebug('[timer] Replaced by new schedule.');
+ return;
+ }
+ if (err instanceof UnsupportedActionError) {
+ this.emit(
+ 'message',
+ _('%s is not supported!').format(actionLabel(internal.mode))
+ );
+ }
+ }
+ await this.toggleShutdown(false, '');
+ }
+
+ async executeActionDelayed() {
+ const internal = this.info.internalShutdown;
+ const secs = internal.secondsLeft;
+ if (secs > 0) {
+ logDebug(`Started delayed action: ${internal.minutes}min remaining`);
+ try {
+ this._timerCancellable = new Gio.Cancellable();
+ await Control.sleepUntilDeadline(
+ internal.deadline,
+ this._timerCancellable
+ );
+ this._timerCancellable = null;
+ } catch {
+ logDebug(`Canceled delayed action: ${internal.minutes}min remaining`);
+ return;
+ }
+ }
+ await this.executeAction();
+ }
+
+ async toggleWake(wake) {
+ try {
+ logDebug('[toggleWake] wake', wake);
+ await this._action.wakeAction(
+ wake,
+ getSliderMinutesFromSettings(this._settings, 'wake')
+ );
+ this.emit('change-external');
+ } catch (err) {
+ this.emit(
+ 'message',
+ C_('Error', '%s\n%s').format(_('Wake action failed!'), err)
+ );
+ this._settings.set_int('shutdown-timestamp-value', -1);
+ }
+ }
+
+ async toggleShutdown(shutdown, legacyAction) {
+ // Update shutdown action
+ const action = mapLegacyAction(legacyAction);
+ if (action === undefined) {
+ this.emit(
+ 'message',
+ _('Unknown shutdown action: "%s"!').format(legacyAction)
+ );
+ return;
+ }
+ logDebug('[toggleShutdown] shutdown', shutdown, 'action', action);
+ if (action !== '') {
+ this._settings.set_string('shutdown-mode-value', action);
+ }
+
+ // Update shutdown timestamp
+ this._settings.set_int(
+ 'shutdown-timestamp-value',
+ shutdown
+ ? GLib.DateTime.new_now_utc().to_unix() +
+ Math.max(
+ 1,
+ getSliderMinutesFromSettings(this._settings, 'shutdown') * 60
+ )
+ : -1
+ );
+ if (shutdown) {
+ await this._action.inhibitSuspend();
+ } else {
+ await this._action.uninhibitSuspend();
+ }
+ if (this._settings.get_boolean('auto-wake-value')) {
+ await this.toggleWake(shutdown);
+ }
+ }
+
+ get state() {
+ return this.info.internalShutdown.scheduled
+ ? this.info.internalShutdown.secondsLeft > 0
+ ? 'active'
+ : 'action'
+ : 'inactive';
+ }
+
+ /**
+ * Ensure that shutdown/reboot is executed even if the Timer fails by running
+ * the `shutdown` command delayed by 1 minute.
+ */
+ async _updateRootModeProtection(oldInternal) {
+ if (this._settings.get_boolean('root-mode-value')) {
+ const internal = this.info.internalShutdown;
+ logDebug('[updateRootModeProtection] mode ', internal.mode);
+ try {
+ if (oldInternal?.scheduled && oldInternal.mode !== internal.mode) {
+ await Control.stopRootModeProtection(oldInternal);
+ }
+ if (internal.scheduled) {
+ await Control.startRootModeProtection(internal);
+ } else {
+ await Control.stopRootModeProtection(internal);
+ }
+ } catch (err) {
+ this.emit(
+ 'message',
+ C_('Error', '%s\n%s').format(_('Root mode protection failed!'), err)
+ );
+ console.error('[updateRootModeProtection]', err);
+ }
+ this.emit('change-external');
+ }
+ }
+}
+Signals.addSignalMethods(Timer.prototype);