From b9b18b571b43aea3c6efbba1916f3aee78524a4c Mon Sep 17 00:00:00 2001 From: Sungbin Jo Date: Fri, 6 Mar 2026 20:50:15 +0900 Subject: [PATCH] fix: use external binary for permission detection --- Cargo.lock | 1 + apps/desktop/src-tauri/binaries/.gitignore | 1 + apps/desktop/src-tauri/build.rs | 34 ++++++++ apps/desktop/src-tauri/tauri.conf.json | 3 + plugins/permissions/Cargo.toml | 1 + plugins/permissions/src/ext.rs | 80 +++++++++++++++++++ .../permissions/swift/check-permissions.swift | 44 ++++++++++ 7 files changed, 164 insertions(+) create mode 100644 plugins/permissions/swift/check-permissions.swift diff --git a/Cargo.lock b/Cargo.lock index 552a229bd0..1535e87a6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18503,6 +18503,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-shell", + "tauri-plugin-sidecar2", "tauri-specta", "tcc", "thiserror 2.0.18", diff --git a/apps/desktop/src-tauri/binaries/.gitignore b/apps/desktop/src-tauri/binaries/.gitignore index b87cfb1fa1..ff92e4c8e5 100644 --- a/apps/desktop/src-tauri/binaries/.gitignore +++ b/apps/desktop/src-tauri/binaries/.gitignore @@ -1 +1,2 @@ char-chrome-native-host-* +check-permissions-* diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs index feee9de32d..7e94e737a7 100644 --- a/apps/desktop/src-tauri/build.rs +++ b/apps/desktop/src-tauri/build.rs @@ -2,5 +2,39 @@ fn main() { #[cfg(target_os = "macos")] println!("cargo:rustc-link-arg=-fapple-link-rtlib"); + #[cfg(target_os = "macos")] + build_check_permissions(); + tauri_build::build() } + +#[cfg(target_os = "macos")] +fn build_check_permissions() { + let triple = std::env::var("TAURI_ENV_TARGET_TRIPLE") + .unwrap_or_else(|_| "aarch64-apple-darwin".to_string()); + + let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let swift_src = manifest_dir.join("../../../plugins/permissions/swift/check-permissions.swift"); + let binaries_dir = manifest_dir.join("binaries"); + let dst = binaries_dir.join(format!("check-permissions-{triple}")); + + println!("cargo:rerun-if-changed={}", swift_src.display()); + + if dst.exists() { + return; + } + + std::fs::create_dir_all(&binaries_dir).expect("create binaries/"); + + let status = std::process::Command::new("swiftc") + .args(["-O", "-o"]) + .arg(&dst) + .arg(&swift_src) + .status() + .expect("failed to run swiftc"); + + assert!( + status.success(), + "swiftc failed to compile check-permissions" + ); +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 94d2fcf2bc..bb7bd60c22 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -25,6 +25,9 @@ "active": true, "createUpdaterArtifacts": true, "targets": "all", + "externalBin": [ + "binaries/check-permissions" + ], "icon": [ "icons/stable/32x32.png", "icons/stable/128x128.png", diff --git a/plugins/permissions/Cargo.toml b/plugins/permissions/Cargo.toml index 98126d4d46..5031a716b7 100644 --- a/plugins/permissions/Cargo.toml +++ b/plugins/permissions/Cargo.toml @@ -19,6 +19,7 @@ hypr-audio = { workspace = true } tauri = { workspace = true, features = ["specta", "test"] } tauri-plugin-shell = { workspace = true } +tauri-plugin-sidecar2 = { workspace = true } specta = { workspace = true } tauri-specta = { workspace = true, features = ["derive", "typescript"] } diff --git a/plugins/permissions/src/ext.rs b/plugins/permissions/src/ext.rs index facfeb21ec..892aaa79c7 100644 --- a/plugins/permissions/src/ext.rs +++ b/plugins/permissions/src/ext.rs @@ -47,6 +47,22 @@ impl<'a, R: tauri::Runtime, M: tauri::Manager> Permissions<'a, R, M> { } pub async fn check(&self, permission: Permission) -> Result { + #[cfg(target_os = "macos")] + { + if matches!(permission, Permission::SystemAudio) { + return self.check_system_audio().await; + } + + if let Some(status) = self.check_sidecar(permission).await { + return Ok(status); + } + + tracing::warn!( + ?permission, + "sidecar unavailable, falling back to in-process check" + ); + } + match permission { Permission::Calendar => self.check_calendar().await, Permission::Contacts => self.check_contacts().await, @@ -56,6 +72,70 @@ impl<'a, R: tauri::Runtime, M: tauri::Manager> Permissions<'a, R, M> { } } + #[cfg(target_os = "macos")] + async fn check_sidecar(&self, permission: Permission) -> Option { + use tauri_plugin_sidecar2::Sidecar2PluginExt; + + let arg = match permission { + Permission::Calendar => "calendar", + Permission::Contacts => "contacts", + Permission::Microphone => "microphone", + Permission::Accessibility => "accessibility", + Permission::SystemAudio => unreachable!(), + }; + + let cmd = self + .manager + .sidecar2() + .sidecar("check-permissions") + .ok()? + .args([arg]); + + let output = cmd.output().await.ok()?; + + if !output.status.success() { + tracing::warn!( + status = ?output.status, + stderr = %String::from_utf8_lossy(&output.stderr), + "check-permissions binary failed" + ); + return None; + } + + let value = String::from_utf8(output.stdout).ok()?; + let value = value.trim(); + + let status = match permission { + Permission::Calendar => match value { + "notDetermined" => PermissionStatus::NeverRequested, + "fullAccess" => PermissionStatus::Authorized, + // "restricted" | "denied" | "writeOnly" | _ + _ => PermissionStatus::Denied, + }, + Permission::Contacts => match value { + "notDetermined" => PermissionStatus::NeverRequested, + "authorized" => PermissionStatus::Authorized, + // "restricted" | "denied" | _ + _ => PermissionStatus::Denied, + }, + Permission::Microphone => match value { + "notDetermined" => PermissionStatus::NeverRequested, + "authorized" => PermissionStatus::Authorized, + // "restricted" | "denied" | _ + _ => PermissionStatus::Denied, + }, + Permission::Accessibility => match value { + "trusted" => PermissionStatus::Authorized, + // "untrusted" | _ + _ => PermissionStatus::Denied, + }, + Permission::SystemAudio => unreachable!(), + }; + + tracing::debug!(permission = arg, %value, ?status, "check via sidecar"); + Some(status) + } + pub async fn request(&self, permission: Permission) -> Result<(), crate::Error> { match permission { Permission::Calendar => self.request_calendar().await, diff --git a/plugins/permissions/swift/check-permissions.swift b/plugins/permissions/swift/check-permissions.swift new file mode 100644 index 0000000000..6336c4fdf4 --- /dev/null +++ b/plugins/permissions/swift/check-permissions.swift @@ -0,0 +1,44 @@ +import AVFoundation +import Contacts +import EventKit +import Foundation + +guard CommandLine.arguments.count > 1 else { + fputs("Usage: check-permissions \n", stderr) + exit(1) +} + +let permissionType = CommandLine.arguments[1] + +switch permissionType { +case "calendar": + switch EKEventStore.authorizationStatus(for: .event) { + case .notDetermined: print("notDetermined") + case .restricted: print("restricted") + case .denied: print("denied") + case .fullAccess: print("fullAccess") + case .writeOnly: print("writeOnly") + @unknown default: print("unknown") + } +case "contacts": + switch CNContactStore.authorizationStatus(for: .contacts) { + case .notDetermined: print("notDetermined") + case .restricted: print("restricted") + case .denied: print("denied") + case .authorized: print("authorized") + @unknown default: print("unknown") + } +case "microphone": + switch AVCaptureDevice.authorizationStatus(for: .audio) { + case .notDetermined: print("notDetermined") + case .restricted: print("restricted") + case .denied: print("denied") + case .authorized: print("authorized") + @unknown default: print("unknown") + } +case "accessibility": + print(AXIsProcessTrusted() ? "trusted" : "untrusted") +default: + fputs("Unknown permission type: \(permissionType)\n", stderr) + exit(1) +}