From 0a3c1b9c0332143a687553571ef678eee0168d36 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 19 Jul 2025 11:30:29 -0600 Subject: [PATCH] Implement core device app launching --- .../src/services/core_device/app_service.rs | 74 ++++++++++++++++++- tools/src/app_service.rs | 24 ++++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/idevice/src/services/core_device/app_service.rs b/idevice/src/services/core_device/app_service.rs index e36aa66..c3ddce1 100644 --- a/idevice/src/services/core_device/app_service.rs +++ b/idevice/src/services/core_device/app_service.rs @@ -3,7 +3,7 @@ use log::warn; use serde::Deserialize; -use crate::{obf, IdeviceError, ReadWrite, RsdService}; +use crate::{obf, pretty_print_plist, IdeviceError, ReadWrite, RsdService}; use super::CoreDeviceServiceClient; @@ -25,7 +25,7 @@ pub struct AppServiceClient { inner: CoreDeviceServiceClient, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Clone, Debug)] pub struct AppListEntry { #[serde(rename = "isRemovable")] pub is_removable: bool, @@ -48,6 +48,23 @@ pub struct AppListEntry { pub version: Option, } +#[derive(Deserialize, Clone, Debug)] +pub struct LaunchResponse { + #[serde(rename = "processIdentifierVersion")] + pub process_identifier_version: u32, + #[serde(rename = "processIdentifier")] + pub pid: u32, + #[serde(rename = "executableURL")] + pub executable_url: ExecutableUrl, + #[serde(rename = "auditToken")] + pub audit_token: Vec, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct ExecutableUrl { + pub relative: String, +} + impl AppServiceClient { pub async fn new(stream: R) -> Result { Ok(Self { @@ -96,4 +113,57 @@ impl AppServiceClient { Ok(desd) } + + pub async fn launch_application( + &mut self, + bundle_id: impl Into, + arguments: &[&str], + kill_existing: bool, + start_suspended: bool, + environment: Option, + platform_options: Option, + ) -> Result { + let bundle_id = bundle_id.into(); + + let req = crate::plist!({ + "applicationSpecifier": { + "bundleIdentifier": { + "_0": bundle_id + } + }, + "options": { + "arguments": arguments, // Now this will work directly + "environmentVariables": environment.unwrap_or_default(), + "standardIOUsesPseudoterminals": true, + "startStopped": start_suspended, + "terminateExisting": kill_existing, + "user": { + "shortName": "mobile" + }, + "platformSpecificOptions": plist::Value::Data(crate::util::plist_to_xml_bytes(&platform_options.unwrap_or_default())), + }, + "standardIOIdentifiers": {} + }) + .into_dictionary() + .unwrap(); + + let res = self + .inner + .invoke("com.apple.coredevice.feature.launchapplication", Some(req)) + .await?; + + let res = match res + .as_dictionary() + .and_then(|r| r.get("processToken")) + .and_then(|x| plist::from_value(x).ok()) + { + Some(r) => r, + None => { + warn!("CoreDevice res did not contain parsable processToken"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + Ok(res) + } } diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index 28f11d0..0d24210 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -48,6 +48,15 @@ async fn main() { .action(clap::ArgAction::SetTrue), ) .subcommand(Command::new("list").about("Lists the images mounted on the device")) + .subcommand( + Command::new("launch") + .about("Launch the app on the device") + .arg( + Arg::new("bundle_id") + .required(true) + .help("The bundle ID to launch"), + ), + ) .get_matches(); if matches.get_flag("about") { @@ -97,6 +106,21 @@ async fn main() { .await .expect("Failed to get apps"); println!("{apps:#?}"); + } else if let Some(matches) = matches.subcommand_matches("launch") { + let bundle_id: &String = match matches.get_one("bundle_id") { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + + let res = asc + .launch_application(bundle_id, &[], false, false, None, None) + .await + .expect("no launch"); + + println!("{res:#?}"); } else { eprintln!("Invalid usage, pass -h for help"); }