[fix] preserve macOS permissions and defer legacy migration to launch

This commit is contained in:
dijunkun
2026-11-07 03:07:24 +08:00
parent 458f9ef58d
commit f5f8eb94d4
17 changed files with 613 additions and 203 deletions
@@ -1173,6 +1173,9 @@ int GuiApplication::Run() {
"announcement_reads");
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
InitializeTrustedDevices(path_manager_->GetConfigPath() / "trusted-devices");
#ifdef __APPLE__
StartMacPermissionMigration(false);
#endif
LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION);
@@ -2080,6 +2083,26 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
props->rejoin_ = true;
need_to_rejoin_ = true;
});
main->on_open_permissions([this, main] {
#ifdef __APPLE__
mac_permission_dialog_dismissed_ = false;
mac_permission_dialog_requested_ = true;
show_request_permission_window_ = true;
if (main->get_gallery_mode()) ShowMainWindow();
#endif
});
main->on_dismiss_permissions([this] {
#ifdef __APPLE__
mac_permission_dialog_dismissed_ = true;
mac_permission_dialog_requested_ = false;
show_request_permission_window_ = false;
#endif
});
main->on_retry_permission_migration([this] {
#ifdef __APPLE__
StartMacPermissionMigration(true);
#endif
});
main->on_request_screen_recording_permission([this] {
#ifdef __APPLE__
OpenScreenRecordingPreferences();
@@ -3617,8 +3640,16 @@ void GuiApplication::SyncPlatformDialogs() {
#endif
#ifdef __APPLE__
RefreshMacPermissionStatus(false);
show_request_permission_window_ = !mac_screen_recording_permission_granted_ ||
!mac_accessibility_permission_granted_;
const bool migration_running = mac_permission_migration_task_.valid();
const bool permissions_pending = migration_running ||
mac_permission_migration_failed_ || !mac_screen_recording_permission_granted_ ||
!mac_accessibility_permission_granted_;
show_request_permission_window_ = !mac_permission_dialog_dismissed_ &&
(mac_permission_dialog_requested_ || permissions_pending);
ui_->main->set_mac_permission_settings_visible(true);
ui_->wall->set_mac_permission_settings_visible(true);
ui_->main->set_permission_migration_running(migration_running);
ui_->main->set_permission_migration_failed(mac_permission_migration_failed_);
ui_->main->set_permission_dialog_open(show_request_permission_window_);
ui_->main->set_screen_recording_granted(
mac_screen_recording_permission_granted_);
@@ -527,6 +527,11 @@ inline constexpr TranslationRow kTranslationRows[] = {
{"打开目录", {"打开目录", "Open folder", "Открыть папку", "開啟目錄", "フォルダーを開く", "폴더 열기", "Ordner öffnen", "Ouvrir le dossier", "Abrir carpeta", "Abrir pasta"}},
{"最小化时暂停视频", {"最小化时暂停视频", "Pause video when minimized", "Приостанавливать видео при сворачивании", "最小化時暫停視訊", "最小化時に映像を一時停止", "최소화 시 영상 일시 중지", "Video beim Minimieren pausieren", "Mettre la vidéo en pause à la réduction", "Pausar el vídeo al minimizar", "Pausar vídeo ao minimizar"}},
{"控制窗口最小化时暂停接收视频,降低本机解码资源占用。", {"控制窗口最小化时暂停接收视频,降低本机解码资源占用。", "Pause video reception when the control window is minimized to reduce local decoding resource usage.", "При сворачивании окна управления приём видео приостанавливается, снижая нагрузку от декодирования на этом устройстве.", "控制視窗最小化時暫停接收視訊,降低本機解碼資源占用。", "操作ウィンドウの最小化時に映像の受信を一時停止し、この端末のデコード処理による負荷を軽減します。", "제어 창을 최소화하면 영상 수신을 일시 중지하여 이 기기의 디코딩 리소스 사용량을 줄입니다.", "Pausiert den Videoempfang, wenn das Steuerungsfenster minimiert ist, und reduziert so den lokalen Ressourcenverbrauch beim Dekodieren.", "Met en pause la réception vidéo lorsque la fenêtre de contrôle est réduite afin de diminuer les ressources de décodage utilisées sur cet appareil.", "Pausa la recepción de vídeo al minimizar la ventana de control para reducir el uso de recursos de decodificación en este dispositivo.", "Pausa a recepção de vídeo ao minimizar a janela de controle para reduzir o uso de recursos de decodificação neste dispositivo."}},
{"系统权限", {"系统权限", "System permissions", "Системные разрешения", "系統權限", "システム権限", "시스템 권한", "Systemberechtigungen", "Autorisations système", "Permisos del sistema", "Permissões do sistema"}},
{"正在迁移旧版权限…", {"正在迁移旧版权限…", "Migrating permissions…", "Перенос разрешений…", "正在遷移舊版權限…", "権限を移行中…", "권한 이전 중…", "Berechtigungen werden migriert…", "Migration des autorisations…", "Migrando permisos…", "Migrando permissões…"}},
{"权限迁移未完成。请重试,或在系统设置中移除旧 CrossDesk 后重新添加。", {"权限迁移未完成。请重试,或在系统设置中移除旧 CrossDesk 后重新添加。", "Permission migration needs attention. Retry, or remove and re-add CrossDesk in System Settings.", "Не удалось завершить перенос разрешений. Повторите попытку или удалите и заново добавьте CrossDesk в системных настройках.", "權限遷移未完成。請重試,或在系統設定中移除舊 CrossDesk 後重新加入。", "権限の移行を完了できませんでした。再試行するか、システム設定でCrossDeskを削除して追加し直してください。", "권한 이전이 완료되지 않았습니다. 다시 시도하거나 시스템 설정에서 CrossDesk를 제거한 후 다시 추가하세요.", "Die Migration der Berechtigungen ist unvollständig. Wiederholen Sie den Vorgang oder entfernen Sie CrossDesk in den Systemeinstellungen und fügen Sie es erneut hinzu.", "La migration des autorisations est incomplète. Réessayez, ou supprimez puis ajoutez à nouveau CrossDesk dans les réglages système.", "La migración de permisos está incompleta. Reinténtelo o elimine y vuelva a añadir CrossDesk en Ajustes del Sistema.", "A migração de permissões está incompleta. Tente novamente ou remova e adicione o CrossDesk novamente nos Ajustes do Sistema."}},
{"重试权限迁移", {"重试权限迁移", "Retry migration", "Повторить перенос", "重試權限遷移", "移行を再試行", "권한 이전 재시도", "Migration wiederholen", "Réessayer la migration", "Reintentar migración", "Repetir migração"}},
{"稍后", {"稍后", "Later", "Позже", "稍後", "後で", "나중에", "Später", "Plus tard", "Más tarde", "Mais tarde"}},
};
struct NamedTranslation { const char* name; const char* literal; };
@@ -817,6 +822,11 @@ inline constexpr NamedTranslation kNamedTranslations[] = {
{"shutdown_reason", "远程控制连接中,保留连接以确认或取消关机。"},
{"pause_video_when_minimized", "最小化时暂停视频"},
{"pause_video_when_minimized_hint", "控制窗口最小化时暂停接收视频,降低本机解码资源占用。"},
{"system_permissions", "系统权限"},
{"permission_migration_running", "正在迁移旧版权限…"},
{"permission_migration_failed", "权限迁移未完成。请重试,或在系统设置中移除旧 CrossDesk 后重新添加。"},
{"retry_permission_migration", "重试权限迁移"},
{"permission_later", "稍后"},
};
inline constexpr const char* kSlintLiterals[] = {
"此端口已被占用,",
@@ -1318,7 +1328,12 @@ inline constexpr const char* kSlintLiterals[] = {
X(exit_program) \
X(shutdown_reason) \
X(pause_video_when_minimized) \
X(pause_video_when_minimized_hint)
X(pause_video_when_minimized_hint) \
X(system_permissions) \
X(permission_migration_running) \
X(permission_migration_failed) \
X(retry_permission_migration) \
X(permission_later)
} // namespace localization
} // namespace crossdesk
@@ -10,6 +10,7 @@
#include <atomic>
#include <cstdint>
#include <deque>
#include <future>
#include <memory>
#include <string>
@@ -28,6 +29,9 @@
#include "runtime/peer_event_handler.h"
#include "runtime/privacy_session_policy.h"
#include "runtime/trusted_devices.h"
#ifdef __APPLE__
#include "platform/macos/gui/runtime/mac_permission_migration.h"
#endif
namespace crossdesk {
@@ -180,6 +184,11 @@ class GuiRuntime : protected gui_detail::GuiState {
#endif
#ifdef __APPLE__
void StartMacPermissionMigration(bool retry);
std::future<MacPermissionMigrationResult> mac_permission_migration_task_;
bool mac_permission_migration_failed_ = false;
bool mac_permission_dialog_dismissed_ = false;
bool mac_permission_dialog_requested_ = false;
bool CheckScreenRecordingPermission();
bool CheckAccessibilityPermission();
void OpenScreenRecordingPreferences();
+123 -67
View File
@@ -120,6 +120,11 @@ export global UiStrings {
in-out property <string> permission-required: "The application requires the following permissions:";
in-out property <string> screen-recording-permission: "Screen recording permission";
in-out property <string> accessibility-permission: "Accessibility permission";
in-out property <string> system-permissions: "System permissions";
in-out property <string> permission-migration-running: "Migrating permissions…";
in-out property <string> permission-migration-failed;
in-out property <string> retry-permission-migration: "Retry migration";
in-out property <string> permission-later: "Later";
in-out property <string> wayland-notice-title: "Wayland Session Notice";
in-out property <string> wayland-notice-subtitle;
in-out property <string> wayland-permission-title;
@@ -625,6 +630,9 @@ export component MainWindow inherits Window {
in-out property <string> connection-password: "";
in-out property <bool> connection-remember-password: false;
in property <bool> permission-dialog-open: false;
in property <bool> mac-permission-settings-visible: false;
in property <bool> permission-migration-running: false;
in property <bool> permission-migration-failed: false;
in property <bool> screen-recording-granted: true;
in property <bool> accessibility-granted: true;
in-out property <bool> wayland-notice-open: false;
@@ -717,6 +725,9 @@ export component MainWindow inherits Window {
callback reset-remote-id;
callback request-screen-recording-permission;
callback request-accessibility-permission;
callback open-permissions;
callback dismiss-permissions;
callback retry-permission-migration;
callback install-portable-service;
callback cancel-portable-service;
callback acknowledge-portable-service;
@@ -2577,82 +2588,117 @@ export component MainWindow inherits Window {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(400px, parent.width - 40px);
height: permission-content.preferred-height;
height: min(permission-content.preferred-height, parent.height - 32px);
background: #f8fafc; outline-color: UiSurface.edge;
TouchArea { clicked => { focus-sink.focus(); } }
permission-content := VerticalLayout {
padding: 24px; spacing: 20px;
VerticalLayout {
spacing: 8px;
Text {
text: UiStrings.request-permissions;
font-size: 20px; font-weight: 600; color: WorkbenchStyle.text;
wrap: word-wrap;
}
Text {
text: UiStrings.permission-required;
font-size: 12px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
}
VerticalLayout {
spacing: 12px;
for permission[index] in [
{label: UiStrings.accessibility-permission, granted: root.accessibility-granted,
description: GalleryStrings.texts[108]},
{label: UiStrings.screen-recording-permission, granted: root.screen-recording-granted,
description: GalleryStrings.texts[109]},
]: WorkbenchCard {
height: max(80px, permission-labels.preferred-height + 32px);
permission-labels := VerticalLayout {
x: 16px; y: 16px; width: parent.width - permission-action.width - 48px;
height: parent.height - 32px; spacing: 6px; alignment: center;
permission-scroll := ScrollView {
width: parent.width; height: parent.height;
viewport-width: self.visible-width;
viewport-height: permission-content.preferred-height;
horizontal-scrollbar-policy: always-off;
vertical-scrollbar-policy: as-needed;
Rectangle {
width: permission-scroll.viewport-width;
height: permission-scroll.viewport-height;
permission-content := VerticalLayout {
padding: 24px; spacing: 20px;
VerticalLayout {
spacing: 8px;
Text {
text: permission.label; font-size: 13px; font-weight: 600;
color: WorkbenchStyle.text; wrap: word-wrap;
text: UiStrings.request-permissions;
font-size: 20px; font-weight: 600; color: WorkbenchStyle.text;
wrap: word-wrap;
}
Text {
text: permission.description; font-size: 11px;
color: WorkbenchStyle.muted; wrap: word-wrap;
text: UiStrings.permission-required;
font-size: 12px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
}
permission-action := Rectangle {
x: parent.width - self.width - 16px; y: (parent.height - self.height) / 2;
width: max(96px, permission-action-metrics.preferred-width + 24px);
height: 36px;
permission-action-metrics := Text {
visible: false; font-size: 13px; font-weight: 600;
text: GalleryStrings.texts[110];
}
if !permission.granted: WorkbenchButton {
width: parent.width; height: parent.height; primary: true;
text: permission-action-metrics.text;
accessible-label: permission.label + ": " + self.text;
clicked => {
if index == 0 { root.request-accessibility-permission(); }
else { root.request-screen-recording-permission(); }
VerticalLayout {
spacing: 12px;
for permission[index] in [
{label: UiStrings.accessibility-permission, granted: root.accessibility-granted,
description: GalleryStrings.texts[108]},
{label: UiStrings.screen-recording-permission, granted: root.screen-recording-granted,
description: GalleryStrings.texts[109]},
]: WorkbenchCard {
height: max(80px, permission-labels.preferred-height + 32px);
permission-labels := VerticalLayout {
x: 16px; y: 16px; width: parent.width - permission-action.width - 48px;
height: parent.height - 32px; spacing: 6px; alignment: center;
Text {
text: permission.label; font-size: 13px; font-weight: 600;
color: WorkbenchStyle.text; wrap: word-wrap;
}
Text {
text: permission.description; font-size: 11px;
color: WorkbenchStyle.muted; wrap: word-wrap;
}
}
}
if permission.granted: HorizontalLayout {
alignment: center; spacing: 6px;
Text {
text: FontAwesomeIcons.check; font-family: "Font Awesome 6 Free";
font-weight: 900; font-size: 12px; color: WorkbenchStyle.success;
vertical-alignment: center; accessible-role: none;
}
Text {
text: GalleryStrings.texts[111];
font-size: 12px; color: WorkbenchStyle.success; vertical-alignment: center;
permission-action := Rectangle {
x: parent.width - self.width - 16px; y: (parent.height - self.height) / 2;
width: max(96px, permission-action-metrics.preferred-width + 24px);
height: 36px;
permission-action-metrics := Text {
visible: false; font-size: 13px; font-weight: 600;
text: GalleryStrings.texts[110];
}
if !permission.granted: WorkbenchButton {
width: parent.width; height: parent.height; primary: true;
enabled: !root.permission-migration-running;
text: permission-action-metrics.text;
accessible-label: permission.label + ": " + self.text;
clicked => {
if index == 0 { root.request-accessibility-permission(); }
else { root.request-screen-recording-permission(); }
}
}
if permission.granted: HorizontalLayout {
alignment: center; spacing: 6px;
Text {
text: FontAwesomeIcons.check; font-family: "Font Awesome 6 Free";
font-weight: 900; font-size: 12px; color: WorkbenchStyle.success;
vertical-alignment: center; accessible-role: none;
}
Text {
text: GalleryStrings.texts[111];
font-size: 12px; color: WorkbenchStyle.success; vertical-alignment: center;
}
}
}
}
}
VerticalLayout {
spacing: 12px;
SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; }
if root.permission-migration-running || root.permission-migration-failed: Text {
text: root.permission-migration-running
? UiStrings.permission-migration-running : UiStrings.permission-migration-failed;
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
Text {
text: GalleryStrings.texts[112];
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
HorizontalLayout {
alignment: end; spacing: 8px;
WorkbenchButton {
text: root.accessibility-granted && root.screen-recording-granted
&& !root.permission-migration-running && !root.permission-migration-failed
? UiStrings.ok : UiStrings.permission-later;
clicked => { root.dismiss-permissions(); }
}
if root.permission-migration-failed: retry-migration := WorkbenchButton {
width: max(112px, retry-migration-label.preferred-width + 24px);
primary: true; text: UiStrings.retry-permission-migration;
clicked => { root.retry-permission-migration(); }
retry-migration-label := Text {
visible: false; accessible-role: none;
text: retry-migration.text; font-size: 13px; font-weight: 600;
}
}
}
}
}
}
VerticalLayout {
spacing: 12px;
SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; }
Text {
text: GalleryStrings.texts[112];
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
}
}
@@ -2794,13 +2840,14 @@ export component MainWindow inherits Window {
}
}
SettingsGroup {
title: GalleryStrings.texts[120]; rows: 5;
title: GalleryStrings.texts[120];
rows: root.mac-permission-settings-visible ? 6 : 5;
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; }
SettingsToggleRow { label: UiStrings.privacy-local-unlock; checked <=> root.privacy-local-unlock-allowed; enabled: root.privacy-setting-available; }
SettingsToggleRow { label: UiStrings.lock-on-disconnect; checked <=> root.lock-on-disconnect-enabled; enabled: root.lock-on-disconnect-available; }
SettingsToggleRow { label: TrustStrings.only; checked <=> root.trusted-devices-only; }
SettingsRow {
label: TrustStrings.title; divider: false;
label: TrustStrings.title; divider: root.mac-permission-settings-visible;
SettingsButton {
x: parent.width - self.width - 12px; y: 4px;
width: max(104px, self.label-width); height: 28px;
@@ -2808,6 +2855,15 @@ export component MainWindow inherits Window {
clicked => { root.trusted-devices-open = true; root.trust-action("open", "", ""); }
}
}
if root.mac-permission-settings-visible: SettingsRow {
label: UiStrings.system-permissions; divider: false;
SettingsButton {
x: parent.width - self.width - 12px; y: 4px;
width: max(104px, self.label-width); height: 28px;
text: TrustStrings.manage;
clicked => { root.open-permissions(); }
}
}
}
Text {
text: GalleryStrings.texts[121];
@@ -74,6 +74,11 @@ inline int ApplyMainWindowStrings(
trust.set_remove(Text(localization::Literal("移除信任", language)));
trust.set_close(Text(localization::message_close[language]));
auto& strings = window->global<ui::UiStrings>();
strings.set_system_permissions(Text(localization::system_permissions[language]));
strings.set_permission_migration_running(Text(localization::permission_migration_running[language]));
strings.set_permission_migration_failed(Text(localization::permission_migration_failed[language]));
strings.set_retry_permission_migration(Text(localization::retry_permission_migration[language]));
strings.set_permission_later(Text(localization::permission_later[language]));
strings.set_window_minimize(Text(localization::window_minimize[language]));
strings.set_window_close(Text(localization::window_close[language]));
strings.set_save(Text(localization::save[language]));
@@ -0,0 +1,25 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-11-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MAC_PERMISSION_MIGRATION_H_
#define _MAC_PERMISSION_MIGRATION_H_
#include <string>
namespace crossdesk {
struct MacPermissionMigrationResult {
bool needs_repair = false;
std::string detail;
};
// Runs on a worker before the new app requests missing permissions. Explicit
// retries may resume failed/interrupted services, while completed ones persist.
MacPermissionMigrationResult MigrateMacPermissions(bool retry);
} // namespace crossdesk
#endif
@@ -0,0 +1,172 @@
#include "mac_permission_migration.h"
#import <Foundation/Foundation.h>
#include <ApplicationServices/ApplicationServices.h>
#include <CoreGraphics/CoreGraphics.h>
#include <Security/Security.h>
#include <nlohmann/json.hpp>
#include <sys/stat.h>
#include <signal.h>
#include <unistd.h>
#include <chrono>
#include <cerrno>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <thread>
#include "private_file.h"
#include "platform/single_instance.h"
namespace crossdesk {
namespace {
using nlohmann::json;
constexpr const char* kMigrationFile =
"/Library/Application Support/CrossDesk/permission-migration.json";
bool IsDeveloperIdProcess() {
SecCodeRef code = nullptr;
SecRequirementRef requirement = nullptr;
const auto compiled = SecRequirementCreateWithString(
CFSTR("anchor apple generic and identifier \"cn.crossdesk.app\" and "
"certificate 1[field.1.2.840.113635.100.6.2.6] exists and "
"certificate leaf[field.1.2.840.113635.100.6.1.13] exists"),
kSecCSDefaultFlags, &requirement);
const bool valid = compiled == errSecSuccess &&
SecCodeCopySelf(kSecCSDefaultFlags, &code) == errSecSuccess &&
SecCodeCheckValidity(code, kSecCSDefaultFlags, requirement) == errSecSuccess;
if (code) CFRelease(code);
if (requirement) CFRelease(requirement);
return valid;
}
bool KnownBundleId(const std::string& value) {
return value == "cn.crossdesk.app" || value == "cn.crossdesk.CrossDesk" ||
value == "com.crossdesk.app" || value == "com.crossdesk.CrossDesk" ||
value == "com.kunkundi.crossdesk" || value == "com.kunkundi.CrossDesk";
}
bool PermissionGranted(const std::string& service) {
if (service == "Accessibility") return AXIsProcessTrusted();
return CGPreflightScreenCaptureAccess();
}
bool ResetPermission(const std::string& service, const std::string& bundle_id) {
NSTask* task = [[NSTask alloc] init];
task.executableURL = [NSURL fileURLWithPath:@"/usr/bin/tccutil"];
task.arguments = @[@"reset", @(service.c_str()), @(bundle_id.c_str())];
task.standardOutput = [NSFileHandle fileHandleWithNullDevice];
task.standardError = [NSFileHandle fileHandleWithNullDevice];
NSError* error = nil;
if (![task launchAndReturnError:&error]) return false;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (task.running && std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
if (task.running) {
kill(task.processIdentifier, SIGKILL);
[task waitUntilExit];
return false;
}
[task waitUntilExit];
return task.terminationReason == NSTaskTerminationReasonExit &&
task.terminationStatus == 0;
}
MacPermissionMigrationResult Migrate(bool retry) {
if (!IsDeveloperIdProcess()) return {};
struct stat info {};
if (lstat(kMigrationFile, &info) != 0) {
if (errno == ENOENT) return {};
throw std::runtime_error("Cannot read the permission migration marker");
}
if (!S_ISREG(info.st_mode) || info.st_uid != 0 ||
(info.st_mode & 0022) != 0 || info.st_size > 16384) {
throw std::runtime_error("Invalid permission migration marker ownership or size");
}
std::ifstream input(kMigrationFile);
const json migration = json::parse(input);
const std::string generation = migration.at("migration_id");
const std::string bundle_id = migration.at("legacy_bundle_id");
const std::string app_path = migration.at("app_path");
NSUUID* uuid = [[NSUUID alloc] initWithUUIDString:@(generation.c_str())];
if (migration.at("schema") != 1 || !uuid || !KnownBundleId(bundle_id)) {
throw std::runtime_error("Invalid permission migration marker contents");
}
if (![NSBundle.mainBundle.bundlePath isEqualToString:@(app_path.c_str())]) return {};
if (geteuid() == 0) {
throw std::runtime_error("Run CrossDesk as the logged-in user to migrate permissions");
}
platform::SingleInstanceGuard guard("CrossDeskPermissionMigration");
std::string lock_error;
if (guard.TryAcquire(platform::InstanceRole::kClient, lock_error) !=
platform::InstanceResult::kAcquired) {
throw std::runtime_error("Permission migration is already running or its lock is unavailable");
}
NSURL* support = [NSFileManager.defaultManager
URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask
appropriateForURL:nil create:YES error:nullptr];
if (!support) throw std::runtime_error("Cannot find the user's application support directory");
const std::filesystem::path directory =
std::filesystem::path(support.fileSystemRepresentation) / "CrossDesk" / "PermissionMigration";
private_file::EnsureDirectory(directory, true);
const auto state_file = directory / "progress.json";
json state = {{"schema", 1}, {"migration_id", generation}, {"services", json::object()}};
if (std::filesystem::exists(state_file)) {
state = json::parse(private_file::Read(state_file));
if (state.at("schema") != 1 || !state.at("services").is_object()) {
throw std::runtime_error("Invalid per-user permission migration progress");
}
if (state.at("migration_id") != generation) {
state = {{"schema", 1}, {"migration_id", generation}, {"services", json::object()}};
}
}
MacPermissionMigrationResult result;
for (const std::string service : {"Accessibility", "ScreenCapture"}) {
const std::string progress = state["services"].value(service, "pending");
if (progress == "done") continue;
if (PermissionGranted(service)) {
// The user may have fixed this manually after a failed/interrupted reset.
state["services"][service] = "done";
private_file::Write(state_file, state.dump());
continue;
}
if (!retry && progress != "pending") {
result.needs_repair = true;
result.detail += service + " needs a manual retry. ";
continue;
}
// Persist intent before the external operation. An interrupted attempt is
// offered for explicit retry, never silently repeated at the next launch.
state["services"][service] = "attempted";
private_file::Write(state_file, state.dump());
if (ResetPermission(service, bundle_id)) {
state["services"][service] = "done";
} else {
state["services"][service] = "failed";
result.needs_repair = true;
result.detail += service + " reset failed. ";
}
private_file::Write(state_file, state.dump());
}
return result;
}
} // namespace
MacPermissionMigrationResult MigrateMacPermissions(bool retry) {
@autoreleasepool {
@try {
try {
return Migrate(retry);
} catch (const std::exception& error) {
return {true, error.what()};
}
} @catch (NSException* error) {
return {true, error.reason.UTF8String ?: "Permission migration failed"};
}
}
}
} // namespace crossdesk
@@ -28,6 +28,18 @@ void OpenPrivacyPreferences(const char *pane) {
}
} // namespace
void GuiRuntime::StartMacPermissionMigration(bool retry) {
if (mac_permission_migration_task_.valid()) return;
mac_permission_migration_failed_ = false;
try {
mac_permission_migration_task_ = std::async(
std::launch::async, [retry] { return MigrateMacPermissions(retry); });
} catch (const std::exception& error) {
mac_permission_migration_failed_ = true;
LOG_ERROR("Could not start macOS permission migration: {}", error.what());
}
}
bool GuiRuntime::CheckScreenRecordingPermission() {
// CGPreflightScreenCaptureAccess is available on macOS 10.15+
if (@available(macOS 10.15, *)) {
@@ -46,6 +58,7 @@ bool GuiRuntime::CheckAccessibilityPermission() {
}
void GuiRuntime::OpenAccessibilityPreferences() {
if (mac_permission_migration_task_.valid()) return;
if (!mac_accessibility_permission_requested_) {
NSDictionary *options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
@@ -55,6 +68,7 @@ void GuiRuntime::OpenAccessibilityPreferences() {
}
void GuiRuntime::OpenScreenRecordingPreferences() {
if (mac_permission_migration_task_.valid()) return;
if (@available(macOS 10.15, *)) {
if (!mac_screen_recording_permission_requested_) {
CGRequestScreenCaptureAccess();
@@ -67,6 +81,19 @@ void GuiRuntime::OpenScreenRecordingPreferences() {
}
void GuiRuntime::RefreshMacPermissionStatus(bool force) {
if (mac_permission_migration_task_.valid() &&
mac_permission_migration_task_.wait_for(std::chrono::seconds(0)) ==
std::future_status::ready) {
try {
const auto result = mac_permission_migration_task_.get();
mac_permission_migration_failed_ = result.needs_repair;
if (result.needs_repair) LOG_WARN("macOS permission migration: {}", result.detail);
} catch (const std::exception& error) {
mac_permission_migration_failed_ = true;
LOG_ERROR("macOS permission migration failed: {}", error.what());
}
force = true;
}
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force && mac_permission_status_initialized_ &&
now - mac_permission_last_check_tick_ <
@@ -98,7 +125,7 @@ bool GuiRuntime::EnsureMacScreenRecordingPermission() {
return true;
}
show_request_permission_window_ = true;
show_request_permission_window_ = !mac_permission_dialog_dismissed_;
return false;
}
@@ -108,7 +135,7 @@ bool GuiRuntime::EnsureMacAccessibilityPermission() {
return true;
}
show_request_permission_window_ = true;
show_request_permission_window_ = !mac_permission_dialog_dismissed_;
return false;
}