Skip to main content

imessage_private_api/
injection.rs

1/// Helper dylib injection into macOS apps (Messages.app, FaceTime.app, FindMy.app).
2///
3/// Kills any running instance of the target app, then relaunches it with
4/// DYLD_INSERT_LIBRARIES pointing to the embedded helper dylib so it connects
5/// back to our TCP server.
6use imessage_core::config::AppPaths;
7use tracing::{error, info, warn};
8
9use crate::HELPER_DYLIB;
10use crate::service::PrivateApiService;
11
12/// Relaunch an app with the helper dylib injected.
13/// Used by refresh endpoints that need to restart an app and wait for reconnection.
14pub async fn relaunch_app_with_dylib(app_name: &str) -> Result<(), String> {
15    let dylib_dir = AppPaths::user_data().join("private-api");
16    let dylib_path = dylib_dir.join("imessage-helper.dylib");
17
18    if !dylib_path.exists() {
19        return Err(format!(
20            "Helper dylib not found at {}",
21            dylib_path.display()
22        ));
23    }
24
25    let app_path = [
26        format!("/System/Applications/{app_name}.app/Contents/MacOS/{app_name}"),
27        format!("/Applications/{app_name}.app/Contents/MacOS/{app_name}"),
28    ]
29    .iter()
30    .find(|p| std::path::Path::new(p).exists())
31    .cloned()
32    .ok_or_else(|| format!("{app_name}.app binary not found"))?;
33
34    info!("Relaunching {app_name}.app with helper dylib...");
35    let child = tokio::process::Command::new(&app_path)
36        .env("DYLD_INSERT_LIBRARIES", &dylib_path)
37        .stdout(std::process::Stdio::null())
38        .stderr(std::process::Stdio::piped())
39        .spawn()
40        .map_err(|e| format!("Failed to spawn {app_name}.app: {e}"))?;
41
42    // Log if the process exits quickly (crash or rejection)
43    let log_name = app_name.to_string();
44    tokio::spawn(async move {
45        match child.wait_with_output().await {
46            Ok(output) => {
47                if !output.status.success() {
48                    let stderr = String::from_utf8_lossy(&output.stderr);
49                    warn!(
50                        "{}.app exited with status {} after relaunch. stderr: {}",
51                        log_name, output.status, stderr
52                    );
53                }
54            }
55            Err(e) => warn!("Error waiting for {}.app after relaunch: {e}", log_name),
56        }
57    });
58
59    // Hide the app after 5 seconds
60    let hide_name = app_name.to_string();
61    tokio::spawn(async move {
62        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
63        let script = format!(
64            "tell application \"System Events\" to set visible of process \"{}\" to false",
65            hide_name
66        );
67        let _ = tokio::process::Command::new("osascript")
68            .arg("-e")
69            .arg(&script)
70            .output()
71            .await;
72    });
73
74    Ok(())
75}
76
77pub async fn inject_app_dylib(service: &PrivateApiService, app_name: &str) {
78    // Write embedded dylib to disk (DYLD_INSERT_LIBRARIES requires a file path)
79    let dylib_dir = AppPaths::user_data().join("private-api");
80    if let Err(e) = std::fs::create_dir_all(&dylib_dir) {
81        error!("Failed to create dylib directory: {e}");
82        return;
83    }
84    let dylib_path = dylib_dir.join("imessage-helper.dylib");
85    if let Err(e) = std::fs::write(&dylib_path, HELPER_DYLIB) {
86        error!("Failed to write helper dylib: {e}");
87        return;
88    }
89
90    // Find the app binary
91    let app_bin = [
92        format!("/System/Applications/{app_name}.app/Contents/MacOS/{app_name}"),
93        format!("/Applications/{app_name}.app/Contents/MacOS/{app_name}"),
94    ]
95    .iter()
96    .find(|p| std::path::Path::new(p).exists())
97    .cloned();
98
99    let Some(app_path) = app_bin else {
100        warn!("{app_name}.app binary not found!");
101        return;
102    };
103
104    info!("Injecting helper dylib into {}.app", app_name);
105
106    let mut failure_count = 0u32;
107    let mut last_error_time = std::time::Instant::now();
108
109    while failure_count < 5 {
110        // Kill existing process
111        let _ = tokio::process::Command::new("killall")
112            .arg(app_name)
113            .output()
114            .await;
115        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
116
117        // Check if we should stop (service might be shut down)
118        if service.is_connected().await {
119            info!("{app_name} helper already connected, skipping injection");
120            return;
121        }
122
123        info!("Launching {app_name}.app with DYLD_INSERT_LIBRARIES...");
124        let result = tokio::process::Command::new(&app_path)
125            .env("DYLD_INSERT_LIBRARIES", &dylib_path)
126            .stdout(std::process::Stdio::null())
127            .stderr(std::process::Stdio::piped())
128            .spawn();
129
130        match result {
131            Ok(mut child) => {
132                // Hide the app after 5 seconds
133                let hide_name = app_name.to_string();
134                tokio::spawn(async move {
135                    tokio::time::sleep(std::time::Duration::from_secs(5)).await;
136                    let script = format!(
137                        "tell application \"System Events\" to set visible of process \"{}\" to false",
138                        hide_name
139                    );
140                    let _ = tokio::process::Command::new("osascript")
141                        .arg("-e")
142                        .arg(&script)
143                        .output()
144                        .await;
145                });
146
147                // Wait for process to exit
148                match child.wait().await {
149                    Ok(status) if status.success() => {
150                        info!("{app_name}.app exited cleanly, restarting dylib...");
151                        failure_count = 0;
152                    }
153                    Ok(status) => {
154                        warn!("{app_name}.app exited with status: {status}");
155                        if last_error_time.elapsed().as_secs() > 15 {
156                            failure_count = 0;
157                        }
158                        failure_count += 1;
159                        last_error_time = std::time::Instant::now();
160                    }
161                    Err(e) => {
162                        warn!("Error waiting for {app_name}.app: {e}");
163                        if last_error_time.elapsed().as_secs() > 15 {
164                            failure_count = 0;
165                        }
166                        failure_count += 1;
167                        last_error_time = std::time::Instant::now();
168                    }
169                }
170            }
171            Err(e) => {
172                error!("Failed to spawn {app_name}.app: {e}");
173                failure_count += 1;
174                last_error_time = std::time::Instant::now();
175            }
176        }
177    }
178
179    if failure_count >= 5 {
180        error!("Failed to start {app_name}.app with dylib after 5 attempts, giving up");
181    }
182}