From 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 Mon Sep 17 00:00:00 2001 From: AlexanderCurl Date: Sat, 18 Apr 2026 17:46:06 +0100 Subject: Replaced file structures for themes in the correct format --- .../ShutdownTimer@deminder/dbus-service/action.js | 217 ++++++++++++++++ .../ShutdownTimer@deminder/dbus-service/control.js | 235 +++++++++++++++++ .../dbus-service/shutdown-timer-dbus.js | 72 ++++++ .../ShutdownTimer@deminder/dbus-service/timer.js | 286 +++++++++++++++++++++ 4 files changed, 810 insertions(+) create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/action.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/control.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/shutdown-timer-dbus.js create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js (limited to 'raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service') diff --git a/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/action.js b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/action.js new file mode 100644 index 0000000..4b5bf9f --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/action.js @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2023 Deminder +// 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-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/control.js b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/control.js new file mode 100644 index 0000000..28d184a --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/control.js @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2023 Deminder +// 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} - 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-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/shutdown-timer-dbus.js b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/shutdown-timer-dbus.js new file mode 100644 index 0000000..8db37a4 --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/shutdown-timer-dbus.js @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2023 Deminder +// 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-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js new file mode 100644 index 0000000..6c439ab --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js @@ -0,0 +1,286 @@ +// SPDX-FileCopyrightText: 2023 Deminder +// 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); -- cgit v1.3