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
|
||||
|
||||
+6
-2
@@ -59,7 +59,11 @@ xmake r crossdesk
|
||||
./apps/desktop/scripts/macos/pkg_x64.sh 1.4.3
|
||||
```
|
||||
|
||||
打包脚本会生成包含 Slint 运行库的 `.app` 和 `.pkg`。屏幕录制、辅助功能授权与应用路径及签名有关;测试被控能力时请使用固定路径下的应用包。
|
||||
打包脚本会生成包含 Slint 运行库的 `.app` 和 `.pkg`。正式版本持续使用同一团队的 Developer ID 签名、相同 Bundle ID 及兼容的签名要求时,正常升级通常可以沿用录屏、辅助功能等授权。测试被控能力时请使用固定路径下的应用包。
|
||||
|
||||
Developer ID 安装包通过 `preinstall` 检查覆盖前的旧应用。检测到未签名或 ad-hoc 的 CrossDesk 时,原子保存迁移标记到目标卷的 `/Library/Application Support/CrossDesk/permission-migration.json`。安装完成后退出旧版并启动新版,正式签名的新版会在后台按当前用户分别迁移辅助功能与录屏授权。迁移标记支持离线卷安装和稍后登录的其他用户。
|
||||
|
||||
每项权限的处理进度保存在当前用户的 `~/Library/Application Support/CrossDesk/PermissionMigration/progress.json`。已处理或已有效授权的项目会跳过,失败或中断的项目可在“设置 → 系统权限”中点击“重试权限迁移”。迁移处理中及失败时均可选择“稍后”,受影响的被控功能在获得授权后启用。安装器记录迁移状态失败时会写入安装日志并继续安装。已完成安装后遗留的异常授权、更换证书签名身份等情况,参照 [macOS 权限排查](FAQ.md#macos-有连接但黑屏或无法操作键鼠)。
|
||||
|
||||
### Developer ID 签名与公证
|
||||
|
||||
@@ -120,7 +124,7 @@ base64 -i "$HOME/Downloads/crossdesk-installer.p12" | pbcopy
|
||||
|
||||
配置后先手动运行工作流:填写 `desktop_version`,勾选 `sign_macos`,确认 ARM64、Intel 的 `Notarize and staple macOS installer` 步骤均成功,再推送发布标签。需要先将工作流和配套脚本提交、推送到 GitHub,新的开关和步骤才会生效。
|
||||
|
||||
CI 最多等待公证 60 分钟;若 Apple 仍在处理,任务失败并阻止发布,但会保留 `macos-pending-notarization-*` 安装包及 `macos-notarization-*` JSON 诊断产物 14 天。下载产物后,按上一节用已保存的提交 ID 查询并附加票据,无需重复上传同一个包。不要将 pending 产物当作已经完成公证的发布包。自动签名流程不改变安装器当前的权限重置逻辑;辅助功能授权是否保留还需单独调整安装脚本。
|
||||
CI 最多等待公证 60 分钟;若 Apple 仍在处理,任务失败并阻止发布,但会保留 `macos-pending-notarization-*` 安装包及 `macos-notarization-*` JSON 诊断产物 14 天。下载产物后,按上一节用已保存的提交 ID 查询并附加票据,无需重复上传同一个包。不要将 pending 产物当作已经完成公证的发布包。
|
||||
|
||||
## Linux
|
||||
|
||||
|
||||
+6
-2
@@ -56,7 +56,11 @@ The current CI sets macOS 14.0 as the minimum target for both Intel and Apple Si
|
||||
./apps/desktop/scripts/macos/pkg_x64.sh 1.4.3
|
||||
```
|
||||
|
||||
The scripts create an `.app` and `.pkg` containing the Slint runtime. Screen Recording and Accessibility grants depend on the app path and signature; use a consistent application bundle when testing host functionality.
|
||||
The scripts create an `.app` and `.pkg` containing the Slint runtime. Release updates can normally retain Screen Recording, Accessibility and other grants when they use Developer ID signing from the same team, the same bundle ID and compatible code signing requirements. Use a consistent application path when testing host functionality.
|
||||
|
||||
Developer ID installers inspect the old app in `preinstall`. For unsigned or ad-hoc builds, they atomically record a migration marker at `/Library/Application Support/CrossDesk/permission-migration.json` on the target volume. After installation, quit the old app and launch the new version. The Developer ID app migrates Accessibility and Screen Recording in the background for the current user. The marker also supports offline target volumes and users who log in later.
|
||||
|
||||
Per-service progress is stored in `~/Library/Application Support/CrossDesk/PermissionMigration/progress.json` for each user. Completed services and valid grants are preserved. Failed or interrupted services can be retried from **Settings → System permissions → Retry migration**. Choose **Later** to continue using the app while migration is running or needs attention. Affected host features become available after authorization. Failure to record the installer marker is logged and installation continues. For stale grants from earlier installations or changes between certificate signing identities, see [macOS permission repair](FAQ.md#repair-macos-permissions).
|
||||
|
||||
### Developer ID signing and notarization
|
||||
|
||||
@@ -117,7 +121,7 @@ Your local `crossdesk-notary` profile does not synchronize to GitHub runners. Th
|
||||
|
||||
After committing and pushing the workflow and supporting scripts, first run it manually with `desktop_version` set and `sign_macos` enabled. Confirm that `Notarize and staple macOS installer` succeeds for both ARM64 and Intel before pushing a release tag.
|
||||
|
||||
CI waits up to 60 minutes for notarization. If Apple is still processing, the job fails and blocks publication, while preserving `macos-pending-notarization-*` installer artifacts and `macos-notarization-*` JSON diagnostics for 14 days. Download them and use the saved submission ID as described above to resume checking and staple the package without uploading it again. Pending artifacts are not completed notarized releases. CI signing does not change the installer's existing permission-reset behavior; preserving Accessibility grants requires a separate installer change.
|
||||
CI waits up to 60 minutes for notarization. If Apple is still processing, the job fails and blocks publication, while preserving `macos-pending-notarization-*` installer artifacts and `macos-notarization-*` JSON diagnostics for 14 days. Download them and use the saved submission ID as described above to resume checking and staple the package without uploading it again. Pending artifacts are not completed notarized releases.
|
||||
|
||||
## Linux
|
||||
|
||||
|
||||
+33
-1
@@ -20,6 +20,21 @@
|
||||
|
||||
在“系统设置 → 隐私与安全性”中检查 CrossDesk 的屏幕录制与辅助功能权限。更换应用位置或签名、更新开发构建后,可能需要重新授权并重启应用。
|
||||
|
||||
同一团队正式签名且应用标识与签名要求兼容的版本,正常升级通常可以沿用权限。从未签名或 ad-hoc 旧版升级后,退出旧版并启动新版,新版会在后台处理当前用户的辅助功能和录屏权限迁移,再引导重新授权。每项权限的完成状态会单独保存。
|
||||
|
||||
迁移失败或中断时,打开“设置 → 系统权限”,点击“重试权限迁移”。也可选择“稍后”继续使用客户端,受影响的被控功能在授权后启用。若系统设置仍显示已授权但功能异常,彻底退出 CrossDesk,在对应权限列表中移除旧条目后重新添加 `/Applications/CrossDesk.app`,再启动应用。
|
||||
|
||||
问题仍存在时,再次退出 CrossDesk,在当前登录用户的终端中仅执行与故障对应的一条命令,重置 CrossDesk 的该项授权。
|
||||
|
||||
```bash
|
||||
# 录屏权限异常
|
||||
tccutil reset ScreenCapture cn.crossdesk.app
|
||||
# 辅助功能权限异常
|
||||
tccutil reset Accessibility cn.crossdesk.app
|
||||
```
|
||||
|
||||
执行后重新启动 CrossDesk,按提示在系统设置中授权。仍有旧路径或旧版本条目时,可在对应权限列表中移除旧条目后重新添加当前应用。
|
||||
|
||||
## Linux Wayland 无法捕获画面
|
||||
|
||||
Wayland 画面捕获依赖启用了 Wayland 支持的构建、宿主系统 PipeWire 0.3 和桌面门户授权;无法捕获时也可在 X11 会话中对比验证。构建选项见 [Linux 构建说明](BUILD.md#linux)。
|
||||
@@ -98,7 +113,7 @@ Windows 安装包和便携包内置了 Amyuni 的 `usbmmidd_v2` 虚拟显示器
|
||||
| Server disconnected / TLS error | Network access, signaling endpoint, certificate dates, hostname, and system trust. See [self-hosting](SELF_HOSTING_EN.md). |
|
||||
| Remote device offline | Keep the host running, connect both sides to the same signaling server, and copy its current ID again. |
|
||||
| P2P connection failed | The default connection policy already allows TURN relay fallback. Enable Force Relay in Settings, save, and reconnect to troubleshoot direct connections. For self-hosting, check Coturn and relay/media firewall ports. Review [release compatibility notes](https://github.com/kunkundi/crossdesk/releases). |
|
||||
| Black screen / no input on macOS | Grant Screen Recording and Accessibility, then reopen the app. A changed app path or signature may require fresh permission grants. |
|
||||
| Black screen / no input on macOS | Grant Screen Recording and Accessibility, then reopen the app. Release updates with compatible signing identities normally retain permissions. For migration or stale grants, see [macOS permission repair](#repair-macos-permissions). |
|
||||
| No capture on Linux Wayland | Check the Wayland build option, host PipeWire 0.3, and desktop portal permission. Compare with an X11 session. See [Linux build instructions](BUILD_EN.md#linux). |
|
||||
| Linux host has no monitor or graphical session | Start CrossDesk normally to automatically discover and select your existing desktop, preserving its Dock and apps; no display number is needed. A bare Xvfb shows only CrossDesk on a black background; use an existing session or install Xfce and specify `--headless-session startxfce4`. See [Linux headless operation](HEADLESS_EN.md). |
|
||||
| Cannot control a Windows lock screen | Install/start CrossDesk Service and keep the host client running. See [service commands](../README_EN.md#windows-service). |
|
||||
@@ -112,4 +127,21 @@ Windows 安装包和便携包内置了 Amyuni 的 `usbmmidd_v2` 虚拟显示器
|
||||
| Windows build without CUDA | Build with `--USE_CUDA=false`, or follow the [CUDA build instructions](BUILD_EN.md). Use `$env:CUDA_PATH` in PowerShell. |
|
||||
| iOS artifact cannot be installed directly | CI exports an unsigned app. Sign it yourself or build and run from Xcode on a physical device. See [iOS requirements](../apps/ios/README.md). |
|
||||
|
||||
### Repair macOS permissions
|
||||
|
||||
Release upgrades with compatible signing identities normally retain permissions. After upgrading an unsigned or ad-hoc build, quit the old app and launch the new version. It migrates Accessibility and Screen Recording in the background for the current user, then guides you through authorization. Completion is saved separately for each permission.
|
||||
|
||||
For failed or interrupted migrations, open **Settings → System permissions** and choose **Retry migration**. Choose **Later** to continue using the client. Affected host features become available after authorization. If access appears enabled while capture or input still fails, quit CrossDesk, remove the old entry from the affected privacy list, add `/Applications/CrossDesk.app` again, and reopen the app.
|
||||
|
||||
If the issue persists, quit CrossDesk and run only the command for the affected permission in the logged-in user's terminal.
|
||||
|
||||
```bash
|
||||
# Screen Recording permission issue
|
||||
tccutil reset ScreenCapture cn.crossdesk.app
|
||||
# Accessibility permission issue
|
||||
tccutil reset Accessibility cn.crossdesk.app
|
||||
```
|
||||
|
||||
Reopen CrossDesk and grant the requested permission in System Settings. If entries for older paths or builds remain, remove the old entry from the affected permission list and add the current app again.
|
||||
|
||||
If the issue persists, [open an issue](https://github.com/kunkundi/crossdesk/issues) with both OS versions, client versions, connection mode, and reproduction steps. Remove passwords and private device information from logs or screenshots before sharing them.
|
||||
|
||||
@@ -10667,6 +10667,66 @@
|
||||
"fr": "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.",
|
||||
"es": "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.",
|
||||
"pt-BR": "Pausa a recepção de vídeo ao minimizar a janela de controle para reduzir o uso de recursos de decodificação neste dispositivo."
|
||||
},
|
||||
"系统权限": {
|
||||
"zh-Hans": "系统权限",
|
||||
"en": "System permissions",
|
||||
"ru": "Системные разрешения",
|
||||
"zh-Hant": "系統權限",
|
||||
"ja": "システム権限",
|
||||
"ko": "시스템 권한",
|
||||
"de": "Systemberechtigungen",
|
||||
"fr": "Autorisations système",
|
||||
"es": "Permisos del sistema",
|
||||
"pt-BR": "Permissões do sistema"
|
||||
},
|
||||
"正在迁移旧版权限…": {
|
||||
"zh-Hans": "正在迁移旧版权限…",
|
||||
"en": "Migrating permissions…",
|
||||
"ru": "Перенос разрешений…",
|
||||
"zh-Hant": "正在遷移舊版權限…",
|
||||
"ja": "権限を移行中…",
|
||||
"ko": "권한 이전 중…",
|
||||
"de": "Berechtigungen werden migriert…",
|
||||
"fr": "Migration des autorisations…",
|
||||
"es": "Migrando permisos…",
|
||||
"pt-BR": "Migrando permissões…"
|
||||
},
|
||||
"权限迁移未完成。请重试,或在系统设置中移除旧 CrossDesk 后重新添加。": {
|
||||
"zh-Hans": "权限迁移未完成。请重试,或在系统设置中移除旧 CrossDesk 后重新添加。",
|
||||
"en": "Permission migration needs attention. Retry, or remove and re-add CrossDesk in System Settings.",
|
||||
"ru": "Не удалось завершить перенос разрешений. Повторите попытку или удалите и заново добавьте CrossDesk в системных настройках.",
|
||||
"zh-Hant": "權限遷移未完成。請重試,或在系統設定中移除舊 CrossDesk 後重新加入。",
|
||||
"ja": "権限の移行を完了できませんでした。再試行するか、システム設定でCrossDeskを削除して追加し直してください。",
|
||||
"ko": "권한 이전이 완료되지 않았습니다. 다시 시도하거나 시스템 설정에서 CrossDesk를 제거한 후 다시 추가하세요.",
|
||||
"de": "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.",
|
||||
"fr": "La migration des autorisations est incomplète. Réessayez, ou supprimez puis ajoutez à nouveau CrossDesk dans les réglages système.",
|
||||
"es": "La migración de permisos está incompleta. Reinténtelo o elimine y vuelva a añadir CrossDesk en Ajustes del Sistema.",
|
||||
"pt-BR": "A migração de permissões está incompleta. Tente novamente ou remova e adicione o CrossDesk novamente nos Ajustes do Sistema."
|
||||
},
|
||||
"重试权限迁移": {
|
||||
"zh-Hans": "重试权限迁移",
|
||||
"en": "Retry migration",
|
||||
"ru": "Повторить перенос",
|
||||
"zh-Hant": "重試權限遷移",
|
||||
"ja": "移行を再試行",
|
||||
"ko": "권한 이전 재시도",
|
||||
"de": "Migration wiederholen",
|
||||
"fr": "Réessayer la migration",
|
||||
"es": "Reintentar migración",
|
||||
"pt-BR": "Repetir migração"
|
||||
},
|
||||
"稍后": {
|
||||
"zh-Hans": "稍后",
|
||||
"en": "Later",
|
||||
"ru": "Позже",
|
||||
"zh-Hant": "稍後",
|
||||
"ja": "後で",
|
||||
"ko": "나중에",
|
||||
"de": "Später",
|
||||
"fr": "Plus tard",
|
||||
"es": "Más tarde",
|
||||
"pt-BR": "Mais tarde"
|
||||
}
|
||||
},
|
||||
"desktop": {
|
||||
@@ -10955,7 +11015,12 @@
|
||||
"exit_program": "退出",
|
||||
"shutdown_reason": "远程控制连接中,保留连接以确认或取消关机。",
|
||||
"pause_video_when_minimized": "最小化时暂停视频",
|
||||
"pause_video_when_minimized_hint": "控制窗口最小化时暂停接收视频,降低本机解码资源占用。"
|
||||
"pause_video_when_minimized_hint": "控制窗口最小化时暂停接收视频,降低本机解码资源占用。",
|
||||
"system_permissions": "系统权限",
|
||||
"permission_migration_running": "正在迁移旧版权限…",
|
||||
"permission_migration_failed": "权限迁移未完成。请重试,或在系统设置中移除旧 CrossDesk 后重新添加。",
|
||||
"retry_permission_migration": "重试权限迁移",
|
||||
"permission_later": "稍后"
|
||||
},
|
||||
"slint": [
|
||||
"此端口已被占用,",
|
||||
@@ -11684,7 +11749,12 @@
|
||||
"清除已完成",
|
||||
"打开目录",
|
||||
"最小化时暂停视频",
|
||||
"控制窗口最小化时暂停接收视频,降低本机解码资源占用。"
|
||||
"控制窗口最小化时暂停接收视频,降低本机解码资源占用。",
|
||||
"系统权限",
|
||||
"正在迁移旧版权限…",
|
||||
"权限迁移未完成。请重试,或在系统设置中移除旧 CrossDesk 后重新添加。",
|
||||
"重试权限迁移",
|
||||
"稍后"
|
||||
],
|
||||
"android": [
|
||||
" · 已完成",
|
||||
|
||||
Reference in New Issue
Block a user