mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-16 16:24:00 -04:00
* lock: keep the fingerprint reader usable across suspend
The reader stopped responding after the lid was closed and opened, after a long
idle period, and after suspend.
The shell held a PAM session for as long as the screen was locked, so a suspend
always landed on a running verify. fprintd then suspends the device under the
verify, pam_fprintd's ReleaseDevice fails with "device is still busy", and the
claim is never released, so every later Claim fails until fprintd restarts. The
reader is now released when the monitors blank and armed again when they come
back, so a suspend has nothing left to wedge.
The PAM profile drops to max-tries=1 and sets an explicit timeout. pam_fprintd
only counts a wrong finger, so one finished session is now one wrong scan and
maxFprintTries means the number of scans it names rather than five times that.
A timeout expiry arrives as PamResult.Error, exactly like a device fault, so a
session that ran at least as long as the configured timeout is treated as a
planned renewal instead of spending the error budget and painting an error.
A suspend that still lands while the screen is on wedges as before. Recovery now
waits out fprintd's idle timeout instead of climbing to it, which brings the
reader back about 37 seconds after resume instead of about 98.
SessionService gains lidOpened from UPower, because logind can ignore the lid on
AC and resume alone is not enough. The lock PAM state machine is fixed alongside:
PamResult.Failed was dropped, a pending retry outlived the contexts that owned
it, and an availability check reset the wrong-scan counter.
* lock: time the fingerprint session from the verify, not the claim
pam_fprintd's timeout runs from the verify, but the session age was measured
from start(), which folds the claim into it. A slow claim plus a verify that
failed before its own timeout could add up past the timeout and be taken for a
planned renewal, skipping the backoff. A session that never reached a verify had
the same problem.
The age now starts at the message pam_fprintd sends when the verify begins, so a
session that never verified can never qualify as an expiry, and a slow claim no
longer ages a fault. The message is translated and quickshell clears it before
completed, so only its timing is used, never its text.
That message arrives after pam_fprintd has already armed its deadline, because
VerifyStart is asynchronous, so the age read here runs short of the real timeout
and a slack makes it up. The bias is deliberate. A fault misread as a timeout
costs one extra retry, while a timeout misread as a fault parks the reader behind
a backoff that grows to a minute, which on an idle lock screen would leave it
unusable most of the time.
Also corrects the comment that claimed a timeout is reported without a message.
---------
(cherry picked from commit d7074a53eb)
936 lines
29 KiB
QML
936 lines
29 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.I3
|
|
import qs.Common
|
|
import qs.Services
|
|
|
|
Singleton {
|
|
id: root
|
|
readonly property var log: Log.scoped("SessionService")
|
|
|
|
// Qt.platform.os is "unix" on the BSDs (qtdeclarative qqmlplatform.cpp)
|
|
readonly property bool isBSD: Qt.platform.os === "unix"
|
|
property bool hasUwsm: false
|
|
property bool isElogind: false
|
|
property bool loginctlCommandAvailable: false
|
|
property bool systemctlCommandAvailable: false
|
|
property bool userManagerAvailable: false
|
|
property bool hibernateSupported: false
|
|
readonly property bool softRebootSupported: systemctlCommandAvailable
|
|
property bool inhibitorAvailable: true
|
|
property bool idleInhibited: false
|
|
property string inhibitReason: "Keep system awake"
|
|
property string nvidiaCommand: ""
|
|
|
|
property bool loginctlAvailable: false
|
|
property string sessionId: ""
|
|
property string sessionPath: ""
|
|
property bool locked: false
|
|
property bool active: false
|
|
property bool idleHint: false
|
|
property bool lockedHint: false
|
|
property bool preparingForSleep: false
|
|
property string sessionType: ""
|
|
property string userName: ""
|
|
property string seat: ""
|
|
property string display: ""
|
|
|
|
signal sessionLocked
|
|
signal sessionUnlocked
|
|
signal sessionResumed
|
|
signal lidOpened
|
|
signal loginctlStateChanged
|
|
|
|
property bool stateInitialized: false
|
|
property string prepareForSleepSubscriptionId: ""
|
|
property bool prepareForSleepSubscriptionPending: false
|
|
property string lidSubscriptionId: ""
|
|
property bool lidSubscriptionPending: false
|
|
property double lastResumeSignalTimestamp: 0
|
|
|
|
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
|
|
|
Timer {
|
|
id: sessionInitTimer
|
|
interval: 200
|
|
running: true
|
|
repeat: false
|
|
onTriggered: {
|
|
detectUwsmProcess.running = true;
|
|
detectElogindProcess.running = true;
|
|
detectLoginctlProcess.running = true;
|
|
detectSystemctlProcess.running = true;
|
|
detectHibernateProcess.running = true;
|
|
detectPrimeRunProcess.running = true;
|
|
if (!SettingsData.loginctlLockIntegration) {
|
|
log.debug("loginctl lock integration disabled by user");
|
|
return;
|
|
}
|
|
if (socketPath && socketPath.length > 0) {
|
|
checkDMSCapabilities();
|
|
} else {
|
|
log.debug("DMS_SOCKET not set");
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectUwsmProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v uwsm > /dev/null 2>&1 && systemctl --user is-active --quiet 'wayland-wm@*.service' 2> /dev/null"]
|
|
|
|
onExited: function (exitCode) {
|
|
hasUwsm = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectElogindProcess
|
|
running: false
|
|
command: ["sh", "-c", "ps -eo comm= | grep -E '^(elogind|elogind-daemon)$'"]
|
|
|
|
onExited: function (exitCode) {
|
|
log.debug("Elogind detection exited with code", exitCode);
|
|
isElogind = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectLoginctlProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v loginctl"]
|
|
|
|
onExited: function (exitCode) {
|
|
loginctlCommandAvailable = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectSystemctlProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v systemctl > /dev/null || exit 1; systemctl --user show-environment > /dev/null 2>&1 || exit 2"]
|
|
|
|
onExited: function (exitCode) {
|
|
systemctlCommandAvailable = (exitCode === 0 || exitCode === 2);
|
|
userManagerAvailable = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectHibernateProcess
|
|
running: false
|
|
command: ["grep", "-q", "disk", "/sys/power/state"]
|
|
|
|
onExited: function (exitCode) {
|
|
hibernateSupported = (exitCode === 0);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: hibernateProcess
|
|
running: false
|
|
|
|
property string errorOutput: ""
|
|
|
|
stderr: SplitParser {
|
|
splitMarker: "\n"
|
|
onRead: data => hibernateProcess.errorOutput += data.trim()
|
|
}
|
|
|
|
onExited: function (exitCode) {
|
|
if (exitCode === 0) {
|
|
errorOutput = "";
|
|
return;
|
|
}
|
|
ToastService.showError(I18n.tr("Hibernate failed"), errorOutput);
|
|
errorOutput = "";
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectPrimeRunProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v prime-run"]
|
|
|
|
onExited: function (exitCode) {
|
|
if (exitCode === 0) {
|
|
nvidiaCommand = "prime-run";
|
|
} else {
|
|
detectNvidiaOffloadProcess.running = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: detectNvidiaOffloadProcess
|
|
running: false
|
|
command: ["sh", "-c", "command -v nvidia-offload"]
|
|
|
|
onExited: function (exitCode) {
|
|
if (exitCode === 0) {
|
|
nvidiaCommand = "nvidia-offload";
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: uwsmLogout
|
|
property bool notRunning: false
|
|
command: ["uwsm", "stop"]
|
|
running: false
|
|
|
|
stdout: SplitParser {
|
|
splitMarker: "\n"
|
|
onRead: data => {
|
|
if (data.trim().toLowerCase().includes("not running"))
|
|
uwsmLogout.notRunning = true;
|
|
}
|
|
}
|
|
|
|
onExited: function (exitCode) {
|
|
if (exitCode === 0 && !notRunning) {
|
|
return;
|
|
}
|
|
_logout();
|
|
}
|
|
}
|
|
|
|
function escapeShellArg(arg) {
|
|
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
}
|
|
|
|
function needsShellExecution(prefix) {
|
|
if (!prefix || prefix.length === 0)
|
|
return false;
|
|
return /[;&|<>()$`\\"']/.test(prefix);
|
|
}
|
|
|
|
function parseEnvVars(envVarsStr) {
|
|
if (!envVarsStr || envVarsStr.trim().length === 0)
|
|
return {};
|
|
const envObj = {};
|
|
const pairs = envVarsStr.trim().split(/\s+/);
|
|
for (const pair of pairs) {
|
|
const eqIndex = pair.indexOf("=");
|
|
if (eqIndex > 0) {
|
|
const key = pair.substring(0, eqIndex);
|
|
const value = pair.substring(eqIndex + 1);
|
|
envObj[key] = value;
|
|
}
|
|
}
|
|
return envObj;
|
|
}
|
|
|
|
function splitShellArgs(str) {
|
|
const args = [];
|
|
let current = "";
|
|
let hasToken = false;
|
|
let quote = "";
|
|
let escaped = false;
|
|
for (const ch of str) {
|
|
if (escaped) {
|
|
current += ch;
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
switch (quote) {
|
|
case "'":
|
|
if (ch === "'") {
|
|
quote = "";
|
|
continue;
|
|
}
|
|
current += ch;
|
|
continue;
|
|
case "\"":
|
|
switch (ch) {
|
|
case "\"":
|
|
quote = "";
|
|
continue;
|
|
case "\\":
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
current += ch;
|
|
continue;
|
|
}
|
|
switch (ch) {
|
|
case "\\":
|
|
escaped = true;
|
|
continue;
|
|
case "'":
|
|
case "\"":
|
|
quote = ch;
|
|
hasToken = true;
|
|
continue;
|
|
case " ":
|
|
case "\t":
|
|
case "\n":
|
|
if (!hasToken && current.length === 0)
|
|
continue;
|
|
args.push(current);
|
|
current = "";
|
|
hasToken = false;
|
|
continue;
|
|
}
|
|
current += ch;
|
|
}
|
|
if (current.length > 0 || hasToken)
|
|
args.push(current);
|
|
return args;
|
|
}
|
|
|
|
// Restore pre-wrap Qt paths (Nix) so launched apps use their own.
|
|
function restoreWrapperEnv(env) {
|
|
const restore = (target, snapshot) => {
|
|
const orig = Quickshell.env(snapshot);
|
|
if (orig === null)
|
|
return;
|
|
env[target] = orig.length > 0 ? orig : null;
|
|
};
|
|
restore("NIXPKGS_QT6_QML_IMPORT_PATH", "DMS_ORIG_NIXPKGS_QT6_QML_IMPORT_PATH");
|
|
restore("QT_PLUGIN_PATH", "DMS_ORIG_QT_PLUGIN_PATH");
|
|
return env;
|
|
}
|
|
|
|
function launchDesktopEntry(desktopEntry, useNvidia) {
|
|
if (!desktopEntry || !desktopEntry.command)
|
|
return;
|
|
CompositorService.closeNiriOverviewOnWindowFocus();
|
|
let cmd = desktopEntry.command;
|
|
|
|
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
|
|
const override = SessionData.getAppOverride(appId);
|
|
|
|
const dgpu = useNvidia || (override?.launchOnDgpu && nvidiaCommand);
|
|
if (dgpu && nvidiaCommand)
|
|
cmd = [nvidiaCommand].concat(cmd);
|
|
|
|
if (override?.extraFlags) {
|
|
cmd = cmd.concat(splitShellArgs(override.extraFlags));
|
|
}
|
|
|
|
const userPrefix = SettingsData.launchPrefix?.trim() || "";
|
|
const defaultPrefix = Quickshell.env("DMS_DEFAULT_LAUNCH_PREFIX") || "";
|
|
const prefix = userPrefix.length > 0 ? userPrefix : defaultPrefix;
|
|
const workDir = desktopEntry.workingDirectory || Quickshell.env("HOME");
|
|
const cursorEnv = typeof SettingsData.getCursorEnvironment === "function" ? SettingsData.getCursorEnvironment() : {};
|
|
|
|
const overrideEnv = override?.envVars ? parseEnvVars(override.envVars) : {};
|
|
const finalEnv = restoreWrapperEnv(Object.assign({}, cursorEnv, overrideEnv));
|
|
|
|
if (desktopEntry.runInTerminal) {
|
|
const terminal = SessionData.resolveTerminal() || "xterm";
|
|
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
|
|
const shellCmd = prefix.length > 0 ? `${prefix} ${escapedCmd}` : escapedCmd;
|
|
Quickshell.execDetached({
|
|
command: [terminal, "-e", "sh", "-c", shellCmd],
|
|
workingDirectory: workDir,
|
|
environment: finalEnv
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (prefix.length > 0 && needsShellExecution(prefix)) {
|
|
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
|
|
Quickshell.execDetached({
|
|
command: ["sh", "-c", `${prefix} ${escapedCmd}`],
|
|
workingDirectory: workDir,
|
|
environment: finalEnv
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (prefix.length > 0)
|
|
cmd = prefix.split(" ").concat(cmd);
|
|
|
|
Quickshell.execDetached({
|
|
command: cmd,
|
|
workingDirectory: workDir,
|
|
environment: finalEnv
|
|
});
|
|
}
|
|
|
|
function launchDesktopAction(desktopEntry, action, useNvidia) {
|
|
if (!desktopEntry || !action || !action.command)
|
|
return;
|
|
CompositorService.closeNiriOverviewOnWindowFocus();
|
|
let cmd = action.command;
|
|
|
|
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
|
|
const override = SessionData.getAppOverride(appId);
|
|
const dgpu = useNvidia || (override?.launchOnDgpu && nvidiaCommand);
|
|
if (dgpu && nvidiaCommand)
|
|
cmd = [nvidiaCommand].concat(cmd);
|
|
|
|
const userPrefix = SettingsData.launchPrefix?.trim() || "";
|
|
const defaultPrefix = Quickshell.env("DMS_DEFAULT_LAUNCH_PREFIX") || "";
|
|
const prefix = userPrefix.length > 0 ? userPrefix : defaultPrefix;
|
|
const workDir = desktopEntry.workingDirectory || Quickshell.env("HOME");
|
|
const cursorEnv = typeof SettingsData.getCursorEnvironment === "function" ? SettingsData.getCursorEnvironment() : {};
|
|
const finalEnv = restoreWrapperEnv(Object.assign({}, cursorEnv));
|
|
|
|
if (prefix.length > 0 && needsShellExecution(prefix)) {
|
|
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
|
|
Quickshell.execDetached({
|
|
command: ["sh", "-c", `${prefix} ${escapedCmd}`],
|
|
workingDirectory: workDir,
|
|
environment: finalEnv
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (prefix.length > 0)
|
|
cmd = prefix.split(" ").concat(cmd);
|
|
|
|
Quickshell.execDetached({
|
|
command: cmd,
|
|
workingDirectory: workDir,
|
|
environment: finalEnv
|
|
});
|
|
}
|
|
|
|
// * Session management
|
|
function logout() {
|
|
if (hasUwsm) {
|
|
if (uwsmLogout.running)
|
|
return;
|
|
uwsmLogout.notRunning = false;
|
|
uwsmLogout.running = true;
|
|
return;
|
|
}
|
|
_logout();
|
|
}
|
|
|
|
function _logout() {
|
|
if (SettingsData.customPowerActionLogout.length === 0) {
|
|
if (CompositorService.isAqueous) {
|
|
AqueousService.quit();
|
|
return;
|
|
}
|
|
if (CompositorService.isNiri) {
|
|
NiriService.quit();
|
|
return;
|
|
}
|
|
|
|
if (CompositorService.isMango) {
|
|
MangoService.quit();
|
|
return;
|
|
}
|
|
|
|
if (CompositorService.isLabwc) {
|
|
LabwcService.quit();
|
|
return;
|
|
}
|
|
|
|
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
|
try {
|
|
I3.dispatch("exit");
|
|
} catch (_) {}
|
|
return;
|
|
}
|
|
|
|
HyprlandService.exit();
|
|
} else {
|
|
Quickshell.execDetached(customActionCommand(SettingsData.customPowerActionLogout));
|
|
}
|
|
}
|
|
|
|
// systemd-run escapes the shell's cgroup so session teardown can't kill the command mid-run (#3250)
|
|
function customActionCommand(cmd) {
|
|
if (!userManagerAvailable)
|
|
return ["sh", "-c", cmd];
|
|
return ["systemd-run", "--user", "--scope", "--collect", "--quiet", "sh", "-c", cmd];
|
|
}
|
|
|
|
function powerManagerCommand(action) {
|
|
if (isBSD)
|
|
return bsdPowerCommand(action);
|
|
const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable);
|
|
return [useLoginctl ? "loginctl" : "systemctl", action];
|
|
}
|
|
|
|
function bsdPowerCommand(action) {
|
|
switch (action) {
|
|
case "suspend":
|
|
case "suspend-then-hibernate":
|
|
return ["acpiconf", "-s", "3"];
|
|
case "hibernate":
|
|
return ["acpiconf", "-s", "4"];
|
|
case "reboot":
|
|
return ["shutdown", "-r", "now"];
|
|
default:
|
|
return ["shutdown", "-p", "now"];
|
|
}
|
|
}
|
|
|
|
function suspend() {
|
|
if (SettingsData.customPowerActionSuspend.length === 0) {
|
|
Quickshell.execDetached(powerManagerCommand("suspend"));
|
|
} else {
|
|
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]);
|
|
}
|
|
}
|
|
|
|
function hibernate() {
|
|
hibernateProcess.errorOutput = "";
|
|
if (SettingsData.customPowerActionHibernate.length > 0) {
|
|
hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate];
|
|
} else {
|
|
hibernateProcess.command = powerManagerCommand("hibernate");
|
|
}
|
|
hibernateProcess.running = true;
|
|
}
|
|
|
|
function suspendThenHibernate() {
|
|
Quickshell.execDetached(powerManagerCommand("suspend-then-hibernate"));
|
|
}
|
|
|
|
function suspendWithBehavior(behavior) {
|
|
if (behavior === SettingsData.SuspendBehavior.Hibernate) {
|
|
hibernate();
|
|
} else if (behavior === SettingsData.SuspendBehavior.SuspendThenHibernate) {
|
|
suspendThenHibernate();
|
|
} else {
|
|
suspend();
|
|
}
|
|
}
|
|
|
|
function reboot() {
|
|
if (SettingsData.customPowerActionReboot.length === 0) {
|
|
Quickshell.execDetached(powerManagerCommand("reboot"));
|
|
} else {
|
|
Quickshell.execDetached(customActionCommand(SettingsData.customPowerActionReboot));
|
|
}
|
|
}
|
|
|
|
function softReboot() {
|
|
Quickshell.execDetached(["systemctl", "soft-reboot"]);
|
|
}
|
|
|
|
function poweroff() {
|
|
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
|
Quickshell.execDetached(powerManagerCommand("poweroff"));
|
|
} else {
|
|
Quickshell.execDetached(customActionCommand(SettingsData.customPowerActionPowerOff));
|
|
}
|
|
}
|
|
|
|
function isPowerActionSupported(action) {
|
|
switch (action) {
|
|
case "hibernate":
|
|
return hibernateSupported;
|
|
case "softreboot":
|
|
return softRebootSupported;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function executePowerAction(action) {
|
|
if (action.startsWith("custom:")) {
|
|
const button = (SettingsData.customPowerButtons || [])[parseInt(action.slice(7), 10)];
|
|
if (!button?.command)
|
|
return false;
|
|
Quickshell.execDetached(customActionCommand(button.command));
|
|
return true;
|
|
}
|
|
switch (action) {
|
|
case "logout":
|
|
logout();
|
|
return true;
|
|
case "suspend":
|
|
suspend();
|
|
return true;
|
|
case "hibernate":
|
|
hibernate();
|
|
return true;
|
|
case "reboot":
|
|
reboot();
|
|
return true;
|
|
case "softreboot":
|
|
softReboot();
|
|
return true;
|
|
case "poweroff":
|
|
poweroff();
|
|
return true;
|
|
case "restart":
|
|
Quickshell.execDetached(["dms", "restart"]);
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getPowerActionData(action) {
|
|
if (action.startsWith("custom:")) {
|
|
const button = (SettingsData.customPowerButtons || [])[parseInt(action.slice(7), 10)];
|
|
return {
|
|
"icon": button?.icon || "terminal",
|
|
"label": button?.label || button?.command || "",
|
|
"key": ""
|
|
};
|
|
}
|
|
switch (action) {
|
|
case "reboot":
|
|
return {
|
|
"icon": "restart_alt",
|
|
"label": I18n.tr("Reboot"),
|
|
"key": "R"
|
|
};
|
|
case "softreboot":
|
|
return {
|
|
"icon": "autorenew",
|
|
"label": I18n.tr("Soft Reboot"),
|
|
"key": "B"
|
|
};
|
|
case "logout":
|
|
return {
|
|
"icon": "logout",
|
|
"label": I18n.tr("Log Out"),
|
|
"key": "X"
|
|
};
|
|
case "poweroff":
|
|
return {
|
|
"icon": "power_settings_new",
|
|
"label": I18n.tr("Power Off"),
|
|
"key": "P"
|
|
};
|
|
case "lock":
|
|
return {
|
|
"icon": "lock",
|
|
"label": I18n.tr("Lock"),
|
|
"key": "L"
|
|
};
|
|
case "suspend":
|
|
return {
|
|
"icon": "bedtime",
|
|
"label": I18n.tr("Suspend"),
|
|
"key": "S"
|
|
};
|
|
case "hibernate":
|
|
return {
|
|
"icon": "ac_unit",
|
|
"label": I18n.tr("Hibernate"),
|
|
"key": "H"
|
|
};
|
|
case "restart":
|
|
return {
|
|
"icon": "refresh",
|
|
"label": I18n.tr("Restart DMS"),
|
|
"key": "D"
|
|
};
|
|
case "switchuser":
|
|
return {
|
|
"icon": "switch_account",
|
|
"label": I18n.tr("Switch User"),
|
|
"key": "U"
|
|
};
|
|
default:
|
|
return {
|
|
"icon": "help",
|
|
"label": action,
|
|
"key": "?"
|
|
};
|
|
}
|
|
}
|
|
|
|
// * Idle Inhibitor
|
|
signal inhibitorChanged
|
|
|
|
// The inhibitor is a state the user turns on explicitly, so it is persisted in the session
|
|
// like the other Control Center toggles (doNotDisturb, nightModeEnabled, ...). Without this
|
|
// it lived only in memory and every shell restart silently cleared it.
|
|
//
|
|
// Both entry points are needed because the two singletons are lazily created and the order is
|
|
// not fixed: onLoaded covers "service first, session file read later", the restore below
|
|
// covers "session already loaded by the time the service is instantiated". enableIdleInhibit
|
|
// is idempotent, so whichever runs second does nothing.
|
|
function _restoreIdleInhibit() {
|
|
if (SessionData.idleInhibited && !idleInhibited)
|
|
enableIdleInhibit();
|
|
}
|
|
|
|
Connections {
|
|
target: SessionData
|
|
|
|
function onLoaded() {
|
|
root._restoreIdleInhibit();
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: _restoreIdleInhibit()
|
|
|
|
function enableIdleInhibit() {
|
|
if (idleInhibited)
|
|
return;
|
|
idleInhibited = true;
|
|
SessionData.setIdleInhibited(true);
|
|
inhibitorChanged();
|
|
}
|
|
|
|
function disableIdleInhibit() {
|
|
if (!idleInhibited)
|
|
return;
|
|
idleInhibited = false;
|
|
SessionData.setIdleInhibited(false);
|
|
inhibitorChanged();
|
|
}
|
|
|
|
function toggleIdleInhibit() {
|
|
if (idleInhibited) {
|
|
disableIdleInhibit();
|
|
} else {
|
|
enableIdleInhibit();
|
|
}
|
|
}
|
|
|
|
function setInhibitReason(reason) {
|
|
inhibitReason = reason;
|
|
}
|
|
|
|
Connections {
|
|
target: DMSService
|
|
|
|
function onConnectionStateChanged() {
|
|
if (DMSService.isConnected) {
|
|
checkDMSCapabilities();
|
|
} else {
|
|
clearPowerSubscriptionState();
|
|
}
|
|
}
|
|
|
|
function onCapabilitiesReceived() {
|
|
syncSleepInhibitor();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: DMSService
|
|
enabled: DMSService.isConnected
|
|
|
|
function onCapabilitiesChanged() {
|
|
checkDMSCapabilities();
|
|
}
|
|
|
|
function onDbusSignalReceived(subscriptionId, data) {
|
|
if (subscriptionId === prepareForSleepSubscriptionId) {
|
|
handlePrepareForSleepSignal(data);
|
|
} else if (subscriptionId === lidSubscriptionId) {
|
|
handleLidPropertiesChanged(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SettingsData
|
|
|
|
function onLoginctlLockIntegrationChanged() {
|
|
if (SettingsData.loginctlLockIntegration) {
|
|
if (socketPath && socketPath.length > 0 && loginctlAvailable) {
|
|
if (!stateInitialized) {
|
|
stateInitialized = true;
|
|
getLoginctlState();
|
|
syncLockBeforeSuspend();
|
|
}
|
|
}
|
|
} else {
|
|
stateInitialized = false;
|
|
}
|
|
syncSleepInhibitor();
|
|
}
|
|
|
|
function onLockBeforeSuspendChanged() {
|
|
if (SettingsData.loginctlLockIntegration) {
|
|
syncLockBeforeSuspend();
|
|
}
|
|
syncSleepInhibitor();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: DMSService
|
|
enabled: SettingsData.loginctlLockIntegration
|
|
|
|
function onLoginctlStateUpdate(data) {
|
|
updateLoginctlState(data);
|
|
}
|
|
}
|
|
|
|
function checkDMSCapabilities() {
|
|
if (!DMSService.isConnected) {
|
|
return;
|
|
}
|
|
|
|
if (DMSService.capabilities.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (DMSService.capabilities.includes("loginctl")) {
|
|
loginctlAvailable = true;
|
|
if (SettingsData.loginctlLockIntegration && !stateInitialized) {
|
|
stateInitialized = true;
|
|
getLoginctlState();
|
|
syncLockBeforeSuspend();
|
|
}
|
|
} else {
|
|
loginctlAvailable = false;
|
|
log.debug("loginctl capability not available in DMS");
|
|
}
|
|
|
|
if (DMSService.capabilities.includes("dbus")) {
|
|
ensurePrepareForSleepSubscription();
|
|
ensureLidSubscription();
|
|
} else {
|
|
clearPowerSubscriptionState();
|
|
}
|
|
}
|
|
|
|
function clearPowerSubscriptionState() {
|
|
prepareForSleepSubscriptionId = "";
|
|
prepareForSleepSubscriptionPending = false;
|
|
lidSubscriptionId = "";
|
|
lidSubscriptionPending = false;
|
|
}
|
|
|
|
function ensureLidSubscription() {
|
|
if (!DMSService.isConnected || !DMSService.capabilities.includes("dbus"))
|
|
return;
|
|
if (lidSubscriptionId || lidSubscriptionPending)
|
|
return;
|
|
|
|
lidSubscriptionPending = true;
|
|
DMSService.dbusSubscribe("system", "org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.DBus.Properties", "PropertiesChanged", response => {
|
|
lidSubscriptionPending = false;
|
|
if (response.error) {
|
|
log.warn("Failed to subscribe to lid changes:", response.error);
|
|
return;
|
|
}
|
|
lidSubscriptionId = response.result?.subscriptionId || "";
|
|
});
|
|
}
|
|
|
|
function handleLidPropertiesChanged(data) {
|
|
if (data?.path !== "/org/freedesktop/UPower" || data.body?.[0] !== "org.freedesktop.UPower")
|
|
return;
|
|
if (data.body?.[1]?.LidIsClosed === false)
|
|
lidOpened();
|
|
}
|
|
|
|
function ensurePrepareForSleepSubscription() {
|
|
if (!DMSService.isConnected || !DMSService.capabilities.includes("dbus")) {
|
|
return;
|
|
}
|
|
|
|
if (prepareForSleepSubscriptionId || prepareForSleepSubscriptionPending) {
|
|
return;
|
|
}
|
|
|
|
prepareForSleepSubscriptionPending = true;
|
|
DMSService.dbusSubscribe("system", "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "PrepareForSleep", response => {
|
|
prepareForSleepSubscriptionPending = false;
|
|
|
|
if (response.error) {
|
|
log.warn("Failed to subscribe to PrepareForSleep:", response.error);
|
|
return;
|
|
}
|
|
|
|
prepareForSleepSubscriptionId = response.result?.subscriptionId || "";
|
|
});
|
|
}
|
|
|
|
function emitSessionResumedOnce() {
|
|
const now = Date.now();
|
|
if ((now - lastResumeSignalTimestamp) < 1000) {
|
|
return;
|
|
}
|
|
lastResumeSignalTimestamp = now;
|
|
sessionResumed();
|
|
}
|
|
|
|
function handlePrepareForSleepSignal(data) {
|
|
if (!data?.body || data.body.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const wasSleeping = preparingForSleep;
|
|
preparingForSleep = data.body[0] === true;
|
|
|
|
if (wasSleeping && !preparingForSleep) {
|
|
emitSessionResumedOnce();
|
|
}
|
|
}
|
|
|
|
function getLoginctlState() {
|
|
if (!loginctlAvailable)
|
|
return;
|
|
DMSService.sendRequest("loginctl.getState", null, response => {
|
|
if (response.result) {
|
|
updateLoginctlState(response.result);
|
|
}
|
|
});
|
|
}
|
|
|
|
function syncLockBeforeSuspend() {
|
|
if (!loginctlAvailable)
|
|
return;
|
|
DMSService.sendRequest("loginctl.setLockBeforeSuspend", {
|
|
enabled: SettingsData.lockBeforeSuspend
|
|
}, response => {
|
|
if (response.error) {
|
|
log.warn("Failed to sync lock before suspend:", response.error);
|
|
} else {
|
|
log.debug("Synced lock before suspend:", SettingsData.lockBeforeSuspend);
|
|
}
|
|
});
|
|
}
|
|
|
|
function syncSleepInhibitor() {
|
|
if (!loginctlAvailable)
|
|
return;
|
|
if (!DMSService.apiVersion || DMSService.apiVersion < 4)
|
|
return;
|
|
DMSService.sendRequest("loginctl.setSleepInhibitorEnabled", {
|
|
enabled: SettingsData.loginctlLockIntegration && SettingsData.lockBeforeSuspend
|
|
}, response => {
|
|
if (response.error) {
|
|
log.warn("Failed to sync sleep inhibitor:", response.error);
|
|
} else {
|
|
log.debug("Synced sleep inhibitor:", SettingsData.loginctlLockIntegration);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateLoginctlState(state) {
|
|
const wasLocked = locked;
|
|
const wasSleeping = preparingForSleep;
|
|
|
|
sessionId = state.sessionId || "";
|
|
sessionPath = state.sessionPath || "";
|
|
locked = state.locked || false;
|
|
active = state.active || false;
|
|
idleHint = state.idleHint || false;
|
|
lockedHint = state.lockedHint || false;
|
|
preparingForSleep = state.preparingForSleep || false;
|
|
sessionType = state.sessionType || "";
|
|
userName = state.userName || "";
|
|
seat = state.seat || "";
|
|
display = state.display || "";
|
|
|
|
if (locked && !wasLocked) {
|
|
sessionLocked();
|
|
} else if (!locked && wasLocked) {
|
|
sessionUnlocked();
|
|
}
|
|
|
|
if (wasSleeping && !preparingForSleep) {
|
|
emitSessionResumedOnce();
|
|
}
|
|
|
|
loginctlStateChanged();
|
|
}
|
|
}
|