From 38b1ab5b05e1fbed725983abec6555f254fcccb2 Mon Sep 17 00:00:00 2001 From: matt-greathouse Date: Mon, 10 Aug 2026 09:37:11 -0400 Subject: [PATCH] Force desktop wake for ambiguous macOS lock states --- src/cli/LuaPrelude.cpp | 13 +++++++++++ src/daemon/DaemonDesktop.cpp | 41 ++++++++++++++++++++++++++++++----- src/daemon/DaemonDesktop.h | 1 + src/daemon/DaemonMetadata.cpp | 2 +- tests/DaemonDispatchTests.cpp | 3 +++ 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/cli/LuaPrelude.cpp b/src/cli/LuaPrelude.cpp index d98cb27..334baef 100644 --- a/src/cli/LuaPrelude.cpp +++ b/src/cli/LuaPrelude.cpp @@ -784,11 +784,24 @@ function ac.batch(steps, opts) local results = response.data and response.data.results or {} for i, result in ipairs(results) do local step = normalized_steps[i] or {} + local diagnostic = type(result.data) == "table" and result.data or {} + local desktop = type(diagnostic.after) == "table" and diagnostic.after + or (type(diagnostic.before) == "table" and diagnostic.before or {}) + local frontmost = type(diagnostic.frontmostApp) == "table" + and diagnostic.frontmostApp or {} log_line("action", result.ok and "ok" or "failed", { id = result.id or step.id, method = step.method, code = result.code, error = result.error, + desktopStatus = desktop.status, + screenLocked = desktop.screenLocked, + screenSaverActive = desktop.screenSaverActive, + displayAsleep = desktop.displayAsleep, + wakeForced = diagnostic.forced, + wakeSignalSent = diagnostic.wakeSignalSent, + frontmostApp = frontmost.name, + frontmostBundle = frontmost.bundleId, }) end if not opts.allow_error then diff --git a/src/daemon/DaemonDesktop.cpp b/src/daemon/DaemonDesktop.cpp index d5d097b..bb49753 100644 --- a/src/daemon/DaemonDesktop.cpp +++ b/src/daemon/DaemonDesktop.cpp @@ -166,6 +166,14 @@ bool CanAttemptDesktopWake(const Platform::DesktopSessionState& state, bool forc (!state.screenLocked || state.screenSaverActive || force); } +bool ShouldForceAutomaticDesktopWake(const Platform::DesktopSessionState& state) { + // Recent macOS versions can report a password-protected screensaver as + // locked without exposing ScreenSaverEngine to the current GUI session. + // A forced wake only posts user activity; readiness is still verified + // afterward and a real lock screen remains locked. + return state.screenLocked && !state.screenSaverActive; +} + bool IsPermissionsPane(const std::string& pane) { return pane == "accessibility" || pane == "screen" || @@ -256,7 +264,16 @@ json RunDesktopWakeCommand(const json& params) { return Error("desktop GUI session is unavailable", "desktop_session_unavailable"); } if (!CanAttemptDesktopWake(before, force)) { - return Error("desktop session is locked and requires manual unlock", "desktop_locked"); + return ErrorWithData( + "desktop session is locked and requires manual unlock", + "desktop_locked", + { + {"forced", force}, + {"wakeSignalSent", false}, + {"before", DesktopSessionToJson(before)}, + {"after", DesktopSessionToJson(before)}, + {"frontmostApp", AppToJson(Platform::GetFrontmostApp())} + }); } const bool alreadyReady = !force && IsDesktopSessionReady(before); @@ -273,7 +290,16 @@ json RunDesktopWakeCommand(const json& params) { } while (std::chrono::steady_clock::now() < deadline); } if (after.screenLocked) { - return Error("desktop woke to a lock screen and requires manual unlock", "desktop_locked"); + return ErrorWithData( + "desktop woke to a lock screen and requires manual unlock", + "desktop_locked", + { + {"forced", force}, + {"wakeSignalSent", wakeSignalSent}, + {"before", DesktopSessionToJson(before)}, + {"after", DesktopSessionToJson(after)}, + {"frontmostApp", AppToJson(Platform::GetFrontmostApp())} + }); } return Ok({ {"wakeRequested", !alreadyReady}, @@ -304,12 +330,17 @@ json EnsureDesktopSessionReadyForNativeControl() { }); } - json wake = RunDesktopWakeCommand(json::object()); + json wakeParams = json::object(); + if (ShouldForceAutomaticDesktopWake(state)) { + wakeParams["force"] = true; + } + json wake = RunDesktopWakeCommand(wakeParams); if (!wake.value("ok", false)) return wake; if (!wake.value("data", json::object()).value("ready", false)) { - return Error( + return ErrorWithData( "native user activity did not make the desktop session ready", - "desktop_wake_failed"); + "desktop_wake_failed", + wake.value("data", json::object())); } return wake; } diff --git a/src/daemon/DaemonDesktop.h b/src/daemon/DaemonDesktop.h index dd016ba..afff8f7 100644 --- a/src/daemon/DaemonDesktop.h +++ b/src/daemon/DaemonDesktop.h @@ -13,6 +13,7 @@ namespace ComputerCpp { std::set VisibleWindowIds(const std::vector& windows); bool IsDesktopSessionReady(const Platform::DesktopSessionState& state); bool CanAttemptDesktopWake(const Platform::DesktopSessionState& state, bool force = false); +bool ShouldForceAutomaticDesktopWake(const Platform::DesktopSessionState& state); nlohmann::json RunPermissionsCommand(const nlohmann::json& params); nlohmann::json RunOpenPermissionsCommand(const nlohmann::json& params); nlohmann::json RunStateCommand(const std::string& session); diff --git a/src/daemon/DaemonMetadata.cpp b/src/daemon/DaemonMetadata.cpp index e51b0a8..69b363f 100644 --- a/src/daemon/DaemonMetadata.cpp +++ b/src/daemon/DaemonMetadata.cpp @@ -217,7 +217,7 @@ json SchemaJson() { {"desktop", { {"sessionState", "method desktop_session_state; reports status ready, screensaver, display_asleep, locked, unavailable, or unsupported"}, {"wake", "method desktop_wake; optional force boolean sends native user activity despite a false-ready state, then conditionally polls for readiness"}, - {"automaticWake", "managed-browser, window, and PID-based app activation wake an unlocked screensaver or sleeping display before native control"}, + {"automaticWake", "managed-browser, window, and PID-based app activation wake a screensaver or sleeping display before native control; ambiguous macOS lock reports receive a forced user-activity probe"}, {"safety", "desktop_wake never attempts to authenticate or bypass a lock screen; desktop_locked requires manual unlock"}, {"sessionResponse", "detectionSupported, available, onConsole, loginDone, screenLocked, screenSaverActive, displayAsleep, ready, and status"}, {"wakeResponse", "wakeRequested, forced, wakeSignalSent, ready, before/after session state, and frontmostApp"} diff --git a/tests/DaemonDispatchTests.cpp b/tests/DaemonDispatchTests.cpp index 6657bbf..882b53c 100644 --- a/tests/DaemonDispatchTests.cpp +++ b/tests/DaemonDispatchTests.cpp @@ -34,14 +34,17 @@ void TestDesktopSessionReadiness() { assert(!ComputerCpp::IsDesktopSessionReady(state)); assert(!ComputerCpp::CanAttemptDesktopWake(state)); assert(ComputerCpp::CanAttemptDesktopWake(state, true)); + assert(ComputerCpp::ShouldForceAutomaticDesktopWake(state)); state.screenSaverActive = true; assert(ComputerCpp::CanAttemptDesktopWake(state)); + assert(!ComputerCpp::ShouldForceAutomaticDesktopWake(state)); state.screenSaverActive = false; state.screenLocked = false; state.displayAsleep = true; assert(ComputerCpp::CanAttemptDesktopWake(state)); + assert(!ComputerCpp::ShouldForceAutomaticDesktopWake(state)); } void TestManagedBrowserCompatibilityPortMarker() {