mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-07 06:39:10 +08:00
[fix] preserve macOS permissions and defer legacy migration to launch
This commit is contained in:
@@ -70,7 +70,7 @@ DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||
VOL_NAME="Install ${APP_NAME_UPPER}"
|
||||
|
||||
echo "delete old files"
|
||||
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp CrossDesk_dmg_temp
|
||||
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp build_pkg_scripts CrossDesk_dmg_temp
|
||||
|
||||
mkdir -p build_pkg_temp
|
||||
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
|
||||
@@ -132,9 +132,13 @@ find "${APP_BUNDLE}" -name '._*' -delete
|
||||
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp "${SCRIPT_DIR}/tcc_postinstall.sh" build_pkg_scripts/postinstall
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
PKG_SCRIPT_ARGS=()
|
||||
if [[ "${CROSSDESK_MACOS_SIGN_IDENTITY:--}" != "-" ]]; then
|
||||
mkdir -p build_pkg_scripts
|
||||
cp "${SCRIPT_DIR}/tcc_migration_preinstall.sh" build_pkg_scripts/preinstall
|
||||
chmod +x build_pkg_scripts/preinstall
|
||||
PKG_SCRIPT_ARGS=(--scripts build_pkg_scripts)
|
||||
fi
|
||||
|
||||
mkdir -p build_pkg_resources
|
||||
cat > build_pkg_resources/welcome.html <<EOF
|
||||
@@ -165,7 +169,7 @@ cat > build_pkg_resources/welcome.html <<EOF
|
||||
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||
<p>正式签名版本正常升级时会保留已有权限。从未签名或 ad-hoc 旧版升级后,请退出旧版并启动新版。新版会迁移当前用户的录屏与辅助功能授权,并引导你重新授权。遇到异常可在“设置 → 系统权限”中重试或稍后处理。</p>
|
||||
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -177,7 +181,7 @@ pkgbuild \
|
||||
--version "${APP_VERSION}" \
|
||||
--install-location "/Applications" \
|
||||
--component "${APP_BUNDLE}" \
|
||||
--scripts build_pkg_scripts \
|
||||
${PKG_SCRIPT_ARGS[@]+"${PKG_SCRIPT_ARGS[@]}"} \
|
||||
build_pkg_temp/${APP_NAME}-component.pkg
|
||||
|
||||
cat > build_pkg_temp/Distribution <<EOF
|
||||
|
||||
@@ -70,7 +70,7 @@ DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||
VOL_NAME="Install ${APP_NAME_UPPER}"
|
||||
|
||||
echo "delete old files"
|
||||
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp CrossDesk_dmg_temp
|
||||
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp build_pkg_scripts CrossDesk_dmg_temp
|
||||
|
||||
mkdir -p build_pkg_temp
|
||||
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
|
||||
@@ -132,9 +132,13 @@ find "${APP_BUNDLE}" -name '._*' -delete
|
||||
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp "${SCRIPT_DIR}/tcc_postinstall.sh" build_pkg_scripts/postinstall
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
PKG_SCRIPT_ARGS=()
|
||||
if [[ "${CROSSDESK_MACOS_SIGN_IDENTITY:--}" != "-" ]]; then
|
||||
mkdir -p build_pkg_scripts
|
||||
cp "${SCRIPT_DIR}/tcc_migration_preinstall.sh" build_pkg_scripts/preinstall
|
||||
chmod +x build_pkg_scripts/preinstall
|
||||
PKG_SCRIPT_ARGS=(--scripts build_pkg_scripts)
|
||||
fi
|
||||
|
||||
mkdir -p build_pkg_resources
|
||||
cat > build_pkg_resources/welcome.html <<EOF
|
||||
@@ -165,7 +169,7 @@ cat > build_pkg_resources/welcome.html <<EOF
|
||||
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||
<p>正式签名版本正常升级时会保留已有权限。从未签名或 ad-hoc 旧版升级后,请退出旧版并启动新版。新版会迁移当前用户的录屏与辅助功能授权,并引导你重新授权。遇到异常可在“设置 → 系统权限”中重试或稍后处理。</p>
|
||||
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -177,7 +181,7 @@ pkgbuild \
|
||||
--version "${APP_VERSION}" \
|
||||
--install-location "/Applications" \
|
||||
--component "${APP_BUNDLE}" \
|
||||
--scripts build_pkg_scripts \
|
||||
${PKG_SCRIPT_ARGS[@]+"${PKG_SCRIPT_ARGS[@]}"} \
|
||||
build_pkg_temp/${APP_NAME}-component.pkg
|
||||
|
||||
cat > build_pkg_temp/Distribution <<EOF
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
export LC_ALL=C
|
||||
|
||||
# Migration is best effort. Installing the application must remain possible
|
||||
# even when its old privacy records or the migration directory are unavailable.
|
||||
if [[ $# -lt 3 ]]; then
|
||||
echo "CrossDesk permission migration skipped because installer arguments are missing." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
APP_BUNDLE="${3%/}${2%/}/CrossDesk.app"
|
||||
if [[ ! -d "$APP_BUNDLE" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
|
||||
"$APP_BUNDLE/Contents/Info.plist" 2>/dev/null || true)"
|
||||
case "$BUNDLE_ID" in
|
||||
cn.crossdesk.app|cn.crossdesk.CrossDesk|com.crossdesk.app|com.crossdesk.CrossDesk|com.kunkundi.crossdesk|com.kunkundi.CrossDesk)
|
||||
;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
if SIGNATURE="$(/usr/bin/codesign --display --verbose=4 "$APP_BUNDLE" 2>&1)"; then
|
||||
if ! /usr/bin/grep -Fxq 'Signature=adhoc' <<< "$SIGNATURE"; then
|
||||
# Keep any unfinished migration for users who have not launched yet.
|
||||
exit 0
|
||||
fi
|
||||
elif [[ "$SIGNATURE" != *": code object is not signed at all" ]]; then
|
||||
echo "Could not identify the old CrossDesk signature. Preserving migration state." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
STATE_DIR="${3%/}/Library/Application Support/CrossDesk"
|
||||
if [[ "$EUID" -ne 0 || -L "$STATE_DIR" ]]; then
|
||||
echo "Could not safely record the CrossDesk permission migration." >&2
|
||||
exit 0
|
||||
fi
|
||||
if ! /bin/mkdir -p "$STATE_DIR" ||
|
||||
[[ "$(/usr/bin/stat -f '%u' "$STATE_DIR")" != 0 ]] ||
|
||||
! /bin/chmod 755 "$STATE_DIR"; then
|
||||
echo "CrossDesk migration directory is unavailable. Use permission repair after installation." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TEMP_FILE="$(/usr/bin/mktemp "$STATE_DIR/.permission-migration.XXXXXX")" || exit 0
|
||||
trap '/bin/rm -f "$TEMP_FILE"' EXIT
|
||||
MIGRATION_ID="$(/usr/bin/uuidgen)" || exit 0
|
||||
if /usr/bin/plutil -create xml1 "$TEMP_FILE" &&
|
||||
/usr/bin/plutil -insert schema -integer 1 "$TEMP_FILE" &&
|
||||
/usr/bin/plutil -insert migration_id -string "$MIGRATION_ID" "$TEMP_FILE" &&
|
||||
/usr/bin/plutil -insert legacy_bundle_id -string "$BUNDLE_ID" "$TEMP_FILE" &&
|
||||
/usr/bin/plutil -insert app_path -string "${2%/}/CrossDesk.app" "$TEMP_FILE" &&
|
||||
/usr/bin/plutil -convert json "$TEMP_FILE" &&
|
||||
/bin/chmod 644 "$TEMP_FILE" &&
|
||||
/bin/mv -f "$TEMP_FILE" "$STATE_DIR/permission-migration.json"; then
|
||||
echo "Recorded CrossDesk permission migration. Quit the old app and launch the new version to continue."
|
||||
else
|
||||
echo "Could not record CrossDesk permission migration. Use permission repair after installation." >&2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
APP_IDENTIFIER="cn.crossdesk.app"
|
||||
|
||||
# Keep known historical identifiers here. tccutil only resets identifiers that
|
||||
# Launch Services can currently resolve, so path/db cleanup below remains a
|
||||
# best-effort fallback for stale entries from unsigned or removed builds.
|
||||
BUNDLE_IDENTIFIERS=(
|
||||
"cn.crossdesk.app"
|
||||
"cn.crossdesk.CrossDesk"
|
||||
"com.crossdesk.app"
|
||||
"com.crossdesk.CrossDesk"
|
||||
"com.kunkundi.crossdesk"
|
||||
"com.kunkundi.CrossDesk"
|
||||
)
|
||||
|
||||
TCC_SERVICES=(
|
||||
"ScreenCapture"
|
||||
"Accessibility"
|
||||
"Microphone"
|
||||
"AudioCapture"
|
||||
)
|
||||
|
||||
run_tccutil() {
|
||||
local user_name="$1"
|
||||
local user_id="$2"
|
||||
local service="$3"
|
||||
local bundle_id="$4"
|
||||
|
||||
if [ -n "$user_name" ] && [ -n "$user_id" ]; then
|
||||
/bin/launchctl asuser "$user_id" \
|
||||
/usr/bin/sudo -u "$user_name" \
|
||||
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||
else
|
||||
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
reset_bundle_tcc() {
|
||||
local user_name="$1"
|
||||
local user_id="$2"
|
||||
local bundle_id
|
||||
local service
|
||||
|
||||
for bundle_id in "${BUNDLE_IDENTIFIERS[@]}"; do
|
||||
if run_tccutil "$user_name" "$user_id" "All" "$bundle_id"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
for service in "${TCC_SERVICES[@]}"; do
|
||||
run_tccutil "$user_name" "$user_id" "$service" "$bundle_id" || true
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
cleanup_tcc_db() {
|
||||
local db_path="$1"
|
||||
|
||||
if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
/usr/bin/sqlite3 "$db_path" <<'SQL' >/dev/null 2>&1 || true
|
||||
DELETE FROM access
|
||||
WHERE service IN (
|
||||
'kTCCServiceScreenCapture',
|
||||
'kTCCServiceAccessibility',
|
||||
'kTCCServiceMicrophone',
|
||||
'kTCCServiceAudioCapture'
|
||||
)
|
||||
AND (
|
||||
client IN (
|
||||
'cn.crossdesk.app',
|
||||
'cn.crossdesk.CrossDesk',
|
||||
'com.crossdesk.app',
|
||||
'com.crossdesk.CrossDesk',
|
||||
'com.kunkundi.crossdesk',
|
||||
'com.kunkundi.CrossDesk'
|
||||
)
|
||||
OR lower(client) LIKE '%crossdesk%'
|
||||
);
|
||||
SQL
|
||||
}
|
||||
|
||||
cleanup_user_tcc_db() {
|
||||
local user_name="$1"
|
||||
local home_dir
|
||||
|
||||
home_dir=$(/usr/bin/dscl . -read "/Users/${user_name}" NFSHomeDirectory 2>/dev/null |
|
||||
/usr/bin/awk '{print $2}')
|
||||
if [ -z "$home_dir" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
cleanup_tcc_db "${home_dir}/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
}
|
||||
|
||||
CONSOLE_USER=$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || true)
|
||||
if [ -n "$CONSOLE_USER" ] &&
|
||||
[ "$CONSOLE_USER" != "root" ] &&
|
||||
[ "$CONSOLE_USER" != "loginwindow" ]; then
|
||||
CONSOLE_UID=$(/usr/bin/id -u "$CONSOLE_USER" 2>/dev/null || true)
|
||||
reset_bundle_tcc "$CONSOLE_USER" "$CONSOLE_UID"
|
||||
cleanup_user_tcc_db "$CONSOLE_USER"
|
||||
fi
|
||||
|
||||
# Also clear any system/root-scoped decisions as a harmless fallback.
|
||||
reset_bundle_tcc "" ""
|
||||
cleanup_tcc_db "/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
|
||||
exit 0
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -656,6 +656,7 @@ function setup_targets()
|
||||
"apps/desktop/src/platform/macos/gui/window_drag_mac.mm",
|
||||
"apps/desktop/src/platform/macos/gui/metal_video_renderer.mm",
|
||||
"apps/desktop/src/platform/macos/gui/video_renderer_factory_metal.mm")
|
||||
add_frameworks("Security", {public = true})
|
||||
add_includedirs("apps/desktop/src/platform/macos/gui",
|
||||
"apps/desktop/src/platform/macos/gui/tray")
|
||||
elseif is_os("linux") then
|
||||
|
||||
Reference in New Issue
Block a user