diff --git a/apps/desktop/src/platform/windows/privacy/privacy_cursor_guard.cpp b/apps/desktop/src/platform/windows/privacy/privacy_cursor_guard.cpp index 4eecdd80..0c45c08a 100644 --- a/apps/desktop/src/platform/windows/privacy/privacy_cursor_guard.cpp +++ b/apps/desktop/src/platform/windows/privacy/privacy_cursor_guard.cpp @@ -11,6 +11,7 @@ #include "../screen_capturer/dxgi_cursor_state.h" #include "privacy_cursor_api.h" +#include "privacy_cursor_process.h" #include "rd_log.h" namespace crossdesk { @@ -85,15 +86,13 @@ bool PrivacyCursorGuard::Start(std::string& error) { startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited, sizeof(inherited), nullptr, nullptr) != FALSE; PROCESS_INFORMATION child{}; + bool used_job_fallback = false; if (ok) { std::wstring command = L"\"" + helper.wstring() + L"\""; for (HANDLE handle : inherited) command += L" " + std::to_wstring(reinterpret_cast(handle)); - ok = - CreateProcessW(helper.c_str(), command.data(), nullptr, nullptr, TRUE, - CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT | - CREATE_BREAKAWAY_FROM_JOB, - nullptr, nullptr, &startup.StartupInfo, &child) != FALSE; + ok = CreatePrivacyCursorProcess(helper.c_str(), command, startup, child, + used_job_fallback); } const DWORD start_error = GetLastError(); DeleteProcThreadAttributeList(startup.lpAttributeList); @@ -102,6 +101,11 @@ bool PrivacyCursorGuard::Start(std::string& error) { return Failed("Start crossdesk_privacy_cursor_helper.exe", error, start_error); } + if (used_job_fallback) { + LOG_INFO("Privacy cursor: launcher denied job breakaway; helper process={} " + "started without breakaway flag with parent-exit monitoring", + child.dwProcessId); + } CloseHandle(child.hThread); process_ = child.hProcess; stop_event_ = stop.value; diff --git a/apps/desktop/src/platform/windows/privacy/privacy_cursor_process.h b/apps/desktop/src/platform/windows/privacy/privacy_cursor_process.h new file mode 100644 index 00000000..3a16a69a --- /dev/null +++ b/apps/desktop/src/platform/windows/privacy/privacy_cursor_process.h @@ -0,0 +1,50 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-08 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _PRIVACY_CURSOR_PROCESS_H_ +#define _PRIVACY_CURSOR_PROCESS_H_ + +#include + +#include + +namespace crossdesk { + +// Prefer an independent lifetime, so closing the launcher's job cannot kill +// the cursor helper before it restores the cursor. Some launchers (including +// scheduled tasks) forbid breakaway. In that case retain the inherited parent +// process handle/stop event and run in the job; parent-only exits still trigger +// cleanup, although terminating the entire job also terminates the helper. +inline bool CreatePrivacyCursorProcess(const wchar_t* executable, + const std::wstring& command, + STARTUPINFOEXW& startup, + PROCESS_INFORMATION& child, + bool& used_job_fallback) { + used_job_fallback = false; + const auto create = [&](DWORD flags) { + // CreateProcessW may modify its command line, including on failure. + auto mutable_command = command; + return CreateProcessW(executable, mutable_command.data(), nullptr, nullptr, + TRUE, CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT | + flags, + nullptr, nullptr, &startup.StartupInfo, &child) != FALSE; + }; + if (create(CREATE_BREAKAWAY_FROM_JOB)) return true; + const DWORD error = GetLastError(); + BOOL in_job = FALSE; + if (error != ERROR_ACCESS_DENIED || + !IsProcessInJob(GetCurrentProcess(), nullptr, &in_job) || !in_job) { + SetLastError(error); + return false; + } + if (!create(0)) return false; + used_job_fallback = true; + return true; +} + +} // namespace crossdesk + +#endif // _PRIVACY_CURSOR_PROCESS_H_ diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp index 0f0c7742..dfee133e 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp @@ -932,6 +932,9 @@ int ScreenCapturerWin::Stop() { // Drain management operations before stopping/replacing their backend. virtual_display_.RequestCancel(); StopSecureCaptureThread(); + // A recovery already in progress can publish "running" while Stop waits. + // Publish the final stopped state after that management thread has exited. + NotifyPrivacyCapture(false); JoinVirtualDisplayThread(); ResetPostSecureDesktopState(); if (impl_) { @@ -1238,6 +1241,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() { if (ret == 0) { applied_show_cursor_ = show_cursor; RestoreMonitor(current_monitor); + NotifyPrivacyCapture(true); return true; } @@ -1253,11 +1257,13 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() { if (ret == 0) { applied_show_cursor_ = show_cursor; RestoreMonitor(current_monitor); + NotifyPrivacyCapture(true); return true; } if (StartPreferredBackend(headless_compat_.load(std::memory_order_relaxed), current_monitor, usbmmidd)) { + NotifyPrivacyCapture(true); return true; }