You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
547 lines
13 KiB
547 lines
13 KiB
/**
|
|
* Shutdown Timer Extension for GNOME Shell
|
|
*
|
|
* @author Deminder <tremminder@gmail.com>
|
|
* @author D. Neumann <neumann89@gmail.com>
|
|
* @copyright 2014-2021
|
|
* @license GNU General Public License v3.0
|
|
*/
|
|
/* exported init, enable, disable */
|
|
|
|
const ExtensionUtils = imports.misc.extensionUtils;
|
|
const Me = ExtensionUtils.getCurrentExtension();
|
|
const {
|
|
ScheduleInfo,
|
|
MenuItem,
|
|
Textbox,
|
|
RootMode,
|
|
Timer,
|
|
Convenience,
|
|
EndSessionDialogAware,
|
|
SessionModeAware,
|
|
CheckCommand,
|
|
} = Me.imports.lib;
|
|
|
|
const {
|
|
guiIdle,
|
|
throttleTimeout,
|
|
disableGuiIdle,
|
|
modeLabel,
|
|
enableGuiIdle,
|
|
longDurationString,
|
|
logDebug,
|
|
} = Convenience;
|
|
|
|
/* IMPORTS */
|
|
const { GLib } = imports.gi;
|
|
const LoginManager = imports.misc.loginManager;
|
|
|
|
// screen and main functionality
|
|
const Main = imports.ui.main;
|
|
const PopupMenu = imports.ui.popupMenu;
|
|
|
|
// translations
|
|
const Gettext = imports.gettext.domain('ShutdownTimer');
|
|
const _ = Gettext.gettext;
|
|
const C_ = Gettext.pgettext;
|
|
const _n = Gettext.ngettext;
|
|
|
|
/* GLOBAL VARIABLES */
|
|
let shutdownTimerMenu, timer, separator, settings;
|
|
|
|
let initialized = false;
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function refreshExternalInfo() {
|
|
if (shutdownTimerMenu !== undefined) {
|
|
shutdownTimerMenu.infoFetcher.refresh();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param textmsg
|
|
*/
|
|
function maybeShowTextbox(textmsg) {
|
|
if (settings.get_boolean('show-textboxes-value')) {
|
|
guiIdle(() => {
|
|
Textbox.showTextbox(textmsg);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param info
|
|
* @param stopScheduled
|
|
*/
|
|
async function maybeStopRootModeProtection(info, stopScheduled = false) {
|
|
if (
|
|
(stopScheduled || !info.scheduled) &&
|
|
settings.get_boolean('root-mode-value')
|
|
) {
|
|
logDebug(`Stop root mode protection for: ${info.mode}`);
|
|
try {
|
|
switch (info.mode) {
|
|
case 'poweroff':
|
|
case 'reboot':
|
|
await RootMode.shutdownCancel();
|
|
refreshExternalInfo();
|
|
break;
|
|
default:
|
|
logDebug(`No root mode protection stopped for: ${info.mode}`);
|
|
}
|
|
} catch (err) {
|
|
maybeShowTextbox(
|
|
C_('Error', '%s\n%s').format(_('Root mode protection failed!'), err)
|
|
);
|
|
logError(err, 'DisableRootModeProtection');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Insure that shutdown is executed even if the GLib timer fails by running
|
|
* the `shutdown` command delayed by 1 minute. Suspend is not insured.
|
|
*
|
|
* @param info
|
|
*/
|
|
async function maybeStartRootModeProtection(info) {
|
|
if (info.scheduled && settings.get_boolean('root-mode-value')) {
|
|
logDebug(`Start root mode protection for: ${info.label}`);
|
|
try {
|
|
const minutes = Math.max(0, info.minutes) + 1;
|
|
switch (info.mode) {
|
|
case 'poweroff':
|
|
await RootMode.shutdown(minutes);
|
|
refreshExternalInfo();
|
|
break;
|
|
case 'reboot':
|
|
await RootMode.shutdown(minutes, true);
|
|
refreshExternalInfo();
|
|
break;
|
|
default:
|
|
logDebug(`No root mode protection started for: ${info.mode}`);
|
|
}
|
|
} catch (err) {
|
|
maybeShowTextbox(
|
|
C_('Error', '%s\n%s').format(_('Root mode protection failed!'), err)
|
|
);
|
|
logError(err, 'EnableRootModeProtection');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param wakeMinutes
|
|
*/
|
|
async function maybeStartWake(wakeMinutes) {
|
|
if (settings.get_boolean('auto-wake-value')) {
|
|
await wakeAction('wake', wakeMinutes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
async function maybeStopWake() {
|
|
if (settings.get_boolean('auto-wake-value')) {
|
|
await wakeAction('no-wake');
|
|
}
|
|
}
|
|
|
|
// timer action (shutdown/reboot/suspend)
|
|
/**
|
|
*
|
|
* @param mode
|
|
*/
|
|
async function serveInernalSchedule(mode) {
|
|
const checkCmd = maybeCheckCmdString();
|
|
try {
|
|
if (checkCmd !== '') {
|
|
guiIdle(() => {
|
|
shutdownTimerMenu.checkRunning = true;
|
|
shutdownTimerMenu.updateShutdownInfo();
|
|
});
|
|
maybeShowTextbox(checkCmd);
|
|
maybeShowTextbox(
|
|
_('Waiting for %s confirmation').format(modeLabel(mode))
|
|
);
|
|
await CheckCommand.doCheck(
|
|
checkCmd,
|
|
line => {
|
|
if (!line.startsWith('[')) {
|
|
maybeShowTextbox(`'${line}'`);
|
|
}
|
|
},
|
|
async () => {
|
|
// keep protection alive
|
|
await maybeStartRootModeProtection(
|
|
new ScheduleInfo.ScheduleInfo({ mode, deadline: 0 })
|
|
);
|
|
}
|
|
);
|
|
}
|
|
// check succeeded: do shutdown
|
|
shutdown(mode);
|
|
} catch (err) {
|
|
logError(err, 'CheckError');
|
|
// check failed: cancel shutdown
|
|
// stop root protection
|
|
await maybeStopRootModeProtection(
|
|
new ScheduleInfo.ScheduleInfo({ mode, deadline: 0 }),
|
|
true
|
|
);
|
|
try {
|
|
const root = settings.get_boolean('root-mode-value');
|
|
if (root) {
|
|
await RootMode.shutdownCancel();
|
|
}
|
|
const wake = settings.get_boolean('auto-wake-value');
|
|
if (wake) {
|
|
await RootMode.wakeCancel();
|
|
}
|
|
if (root || wake) {
|
|
refreshExternalInfo();
|
|
}
|
|
} catch (err2) {
|
|
// error is most likely: script not installed
|
|
logError(err2, 'CheckError');
|
|
}
|
|
// check failed: log failure
|
|
let code = '?';
|
|
if ('code' in err) {
|
|
code = `${err.code}`;
|
|
logDebug(`Check command aborted ${mode}. Code: ${code}`);
|
|
}
|
|
maybeShowTextbox(
|
|
C_('CheckCommand', '%s aborted (Code: %s)').format(modeLabel(mode), code)
|
|
);
|
|
if (parseInt(code) === 19) {
|
|
maybeShowTextbox(_('Confirmation canceled'));
|
|
}
|
|
} finally {
|
|
// update shutdownTimerMenu
|
|
guiIdle(() => {
|
|
shutdownTimerMenu.checkRunning = false;
|
|
shutdownTimerMenu.updateShutdownInfo();
|
|
});
|
|
// reset schedule timestamp
|
|
settings.set_int('shutdown-timestamp-value', -1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function foregroundActive() {
|
|
// ubuntu22.04 uses 'ubuntu' as 'user' sessionMode
|
|
return Main.sessionMode.currentMode !== 'unlock-dialog';
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param mode
|
|
*/
|
|
function shutdown(mode) {
|
|
if (foregroundActive()) {
|
|
Main.overview.hide();
|
|
Textbox.hideAll();
|
|
}
|
|
|
|
if (['reboot', 'poweroff'].includes(mode)) {
|
|
if (
|
|
foregroundActive() &&
|
|
settings.get_boolean('show-end-session-dialog-value')
|
|
) {
|
|
// show endSessionDialog
|
|
// refresh root shutdown protection
|
|
maybeStartRootModeProtection(
|
|
new ScheduleInfo.ScheduleInfo({ mode, deadline: 0 })
|
|
);
|
|
EndSessionDialogAware.register();
|
|
const session = new imports.misc.gnomeSession.SessionManager();
|
|
if (mode === 'reboot') {
|
|
session.RebootRemote(0);
|
|
} else {
|
|
session.ShutdownRemote(0);
|
|
}
|
|
} else {
|
|
imports.misc.util.spawnCommandLine(
|
|
mode === 'reboot' ? 'reboot' : 'poweroff'
|
|
);
|
|
}
|
|
} else if (mode === 'suspend') {
|
|
LoginManager.getLoginManager().suspend();
|
|
} else {
|
|
logError(new Error(`Unknown shutdown mode: ${mode}`));
|
|
}
|
|
}
|
|
|
|
/* ACTION FUNCTIONS */
|
|
/**
|
|
*
|
|
* @param mode
|
|
* @param minutes
|
|
*/
|
|
async function wakeAction(mode, minutes) {
|
|
try {
|
|
switch (mode) {
|
|
case 'wake':
|
|
await RootMode.wake(minutes);
|
|
refreshExternalInfo();
|
|
return;
|
|
case 'no-wake':
|
|
await RootMode.wakeCancel();
|
|
refreshExternalInfo();
|
|
return;
|
|
default:
|
|
logError(new Error(`Unknown wake mode: ${mode}`));
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
maybeShowTextbox(
|
|
C_('Error', '%s\n%s').format(_('Wake action failed!'), err)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param stopProtection
|
|
*/
|
|
function stopSchedule(stopProtection = true) {
|
|
EndSessionDialogAware.unregister();
|
|
const canceled = CheckCommand.maybeCancel();
|
|
if (!canceled && settings.get_int('shutdown-timestamp-value') > -1) {
|
|
settings.set_int('shutdown-timestamp-value', -1);
|
|
maybeShowTextbox(_('Shutdown Timer stopped'));
|
|
}
|
|
|
|
if (stopProtection) {
|
|
// stop root protection
|
|
const info =
|
|
timer !== undefined ? timer.info : new ScheduleInfo.ScheduleInfo();
|
|
return Promise.all([maybeStopRootModeProtection(info), maybeStopWake()]);
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param maxTimerMinutes
|
|
* @param wakeMinutes
|
|
*/
|
|
async function startSchedule(maxTimerMinutes, wakeMinutes) {
|
|
EndSessionDialogAware.unregister();
|
|
CheckCommand.maybeCancel();
|
|
|
|
const seconds = maxTimerMinutes * 60;
|
|
const info = new ScheduleInfo.ScheduleInfo({
|
|
mode: settings.get_string('shutdown-mode-value'),
|
|
deadline: GLib.DateTime.new_now_utc().to_unix() + Math.max(1, seconds),
|
|
});
|
|
settings.set_int('shutdown-timestamp-value', info.deadline);
|
|
let startPopupText = C_('StartSchedulePopup', '%s in %s').format(
|
|
modeLabel(info.mode),
|
|
longDurationString(
|
|
maxTimerMinutes,
|
|
h => _n('%s hour', '%s hours', h),
|
|
m => _n('%s minute', '%s minutes', m)
|
|
)
|
|
);
|
|
const checkCmd = maybeCheckCmdString();
|
|
if (checkCmd !== '') {
|
|
maybeShowTextbox(checkCmd);
|
|
}
|
|
maybeShowTextbox(startPopupText);
|
|
|
|
// start root protection
|
|
await Promise.all([
|
|
maybeStartRootModeProtection(info),
|
|
maybeStartWake(wakeMinutes),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function maybeCheckCmdString() {
|
|
const cmd = settings
|
|
.get_string('check-command-value')
|
|
.split('\n')
|
|
.filter(line => !line.trimLeft().startsWith('#') && line.trim())
|
|
.join('\n');
|
|
|
|
return settings.get_boolean('enable-check-command-value') ? cmd : '';
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param info
|
|
*/
|
|
function onShutdownScheduleChange(info) {
|
|
if (timer !== undefined) {
|
|
timer.adjustTo(info);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param sessionMode
|
|
*/
|
|
function onSessionModeChange(sessionMode) {
|
|
logDebug(`sessionMode: ${sessionMode}`);
|
|
switch (sessionMode) {
|
|
case 'unlock-dialog':
|
|
disableForeground();
|
|
break;
|
|
case 'user':
|
|
default:
|
|
enableForeground();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function enableForeground() {
|
|
enableGuiIdle();
|
|
// add separator line and submenu in status area menu
|
|
const statusMenu = Main.panel.statusArea['aggregateMenu'];
|
|
if (separator === undefined) {
|
|
separator = new PopupMenu.PopupSeparatorMenuItem();
|
|
statusMenu.menu.addMenuItem(separator);
|
|
}
|
|
if (shutdownTimerMenu === undefined) {
|
|
shutdownTimerMenu = new MenuItem.ShutdownTimer();
|
|
shutdownTimerMenu.checkRunning = CheckCommand.isChecking();
|
|
timer.setTickCallback(
|
|
shutdownTimerMenu.updateShutdownInfo.bind(shutdownTimerMenu)
|
|
);
|
|
statusMenu.menu.addMenuItem(shutdownTimerMenu);
|
|
}
|
|
// stop schedule if endSessionDialog cancel button is activated
|
|
EndSessionDialogAware.load(stopSchedule);
|
|
logDebug('Enabled foreground.');
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function disableForeground() {
|
|
disableGuiIdle();
|
|
Textbox.hideAll();
|
|
if (shutdownTimerMenu !== undefined) {
|
|
shutdownTimerMenu.destroy();
|
|
if (timer !== undefined) {
|
|
timer.setTickCallback(null);
|
|
// keep sleep process alive
|
|
timer.stopForeground();
|
|
}
|
|
}
|
|
shutdownTimerMenu = undefined;
|
|
if (separator !== undefined) {
|
|
separator.destroy();
|
|
}
|
|
separator = undefined;
|
|
EndSessionDialogAware.unload();
|
|
logDebug('Disabled foreground.');
|
|
}
|
|
|
|
/* EXTENSION MAIN FUNCTIONS */
|
|
/**
|
|
*
|
|
*/
|
|
function init() {
|
|
// initialize translations
|
|
ExtensionUtils.initTranslations();
|
|
}
|
|
|
|
let throttleDisable = null;
|
|
let throttleDisableCancel = null;
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function enable() {
|
|
if (!initialized) {
|
|
[throttleDisable, throttleDisableCancel] = throttleTimeout(
|
|
completeDisable,
|
|
100
|
|
);
|
|
// initialize settings
|
|
settings = ExtensionUtils.getSettings();
|
|
// ensure that no shutdown is scheduled
|
|
settings.set_int('shutdown-timestamp-value', -1);
|
|
MenuItem.init(settings, {
|
|
wakeAction,
|
|
startSchedule,
|
|
stopSchedule,
|
|
maybeStopRootModeProtection,
|
|
maybeStartRootModeProtection,
|
|
onShutdownScheduleChange,
|
|
});
|
|
Textbox.init();
|
|
|
|
// check for shutdown may run in background and can be canceled by user
|
|
// starts internal shutdown schedule if ready
|
|
timer = new Timer.Timer(serveInernalSchedule);
|
|
|
|
SessionModeAware.load(onSessionModeChange);
|
|
|
|
initialized = true;
|
|
} else {
|
|
throttleDisableCancel();
|
|
}
|
|
if (foregroundActive()) {
|
|
enableForeground();
|
|
logDebug('Completly enabled.');
|
|
} else {
|
|
logDebug('Background enabled.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function disable() {
|
|
// unlock-dialog session-mode is required such that the timer action can trigger
|
|
disableForeground();
|
|
// DELAYED DISABLE:
|
|
// Workaround for Gnome 42 weird behaviour (?unlock-dialog sessionMode bug?):
|
|
// on first screensaver activation after login gnome-shell quickly enables/disables this extension
|
|
// for each extension that is also enabled besides this extension
|
|
if (initialized) {
|
|
throttleDisable();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function completeDisable() {
|
|
if (initialized) {
|
|
if (timer !== undefined) {
|
|
timer.stopTimer();
|
|
timer = undefined;
|
|
}
|
|
Textbox.uninit();
|
|
MenuItem.uninit();
|
|
// clear internal schedule and keep root protected schedule
|
|
stopSchedule(false);
|
|
SessionModeAware.unload();
|
|
|
|
throttleDisableCancel();
|
|
throttleDisable = null;
|
|
throttleDisableCancel = null;
|
|
initialized = false;
|
|
logDebug('Completly disabled.');
|
|
}
|
|
}
|