// 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'); } }