idevice/services/core_device/
app_service.rs1use log::warn;
4use serde::Deserialize;
5
6use crate::{IdeviceError, ReadWrite, RsdService, obf, xpc::XPCObject};
7
8use super::CoreDeviceServiceClient;
9
10impl RsdService for AppServiceClient<Box<dyn ReadWrite>> {
11 fn rsd_service_name() -> std::borrow::Cow<'static, str> {
12 obf!("com.apple.coredevice.appservice")
13 }
14
15 async fn from_stream(stream: Box<dyn ReadWrite>) -> Result<Self, IdeviceError> {
16 Ok(Self {
17 inner: CoreDeviceServiceClient::new(stream).await?,
18 })
19 }
20}
21
22pub struct AppServiceClient<R: ReadWrite> {
23 inner: CoreDeviceServiceClient<R>,
24}
25
26#[derive(Deserialize, Clone, Debug)]
27pub struct AppListEntry {
28 #[serde(rename = "isRemovable")]
29 pub is_removable: bool,
30 pub name: String,
31 #[serde(rename = "isFirstParty")]
32 pub is_first_party: bool,
33 pub path: String,
34 #[serde(rename = "bundleIdentifier")]
35 pub bundle_identifier: String,
36 #[serde(rename = "isDeveloperApp")]
37 pub is_developer_app: bool,
38 #[serde(rename = "bundleVersion")]
39 pub bundle_version: Option<String>,
40 #[serde(rename = "isInternal")]
41 pub is_internal: bool,
42 #[serde(rename = "isHidden")]
43 pub is_hidden: bool,
44 #[serde(rename = "isAppClip")]
45 pub is_app_clip: bool,
46 pub version: Option<String>,
47}
48
49#[derive(Deserialize, Clone, Debug)]
50pub struct LaunchResponse {
51 #[serde(rename = "processIdentifierVersion")]
52 pub process_identifier_version: u32,
53 #[serde(rename = "processIdentifier")]
54 pub pid: u32,
55 #[serde(rename = "executableURL")]
56 pub executable_url: ExecutableUrl,
57 #[serde(rename = "auditToken")]
58 pub audit_token: Vec<u32>,
59}
60
61#[derive(Deserialize, Clone, Debug)]
62pub struct ExecutableUrl {
63 pub relative: String,
64}
65
66#[derive(Deserialize, Clone, Debug)]
67pub struct ProcessToken {
68 #[serde(rename = "processIdentifier")]
69 pub pid: u32,
70 #[serde(rename = "executableURL")]
71 pub executable_url: Option<ExecutableUrl>,
72}
73
74#[derive(Deserialize, Clone, Debug)]
75pub struct SignalResponse {
76 pub process: ProcessToken,
77 #[serde(rename = "deviceTimestamp")]
78 pub device_timestamp: plist::Date,
79 pub signal: u32,
80}
81
82#[derive(Deserialize, Clone, Debug)]
106pub struct IconData {
107 pub data: plist::Data,
108 #[serde(rename = "iconSize.height")]
109 pub icon_height: f64,
110 #[serde(rename = "iconSize.width")]
111 pub icon_width: f64,
112 #[serde(rename = "minimumSize.height")]
113 pub minimum_height: f64,
114 #[serde(rename = "minimumSize.width")]
115 pub minimum_width: f64,
116 #[serde(rename = "$classes")]
117 pub classes: Vec<String>,
118 #[serde(rename = "validationToken")]
119 pub validation_token: plist::Data,
120 pub uuid: IconUuid,
121}
122
123#[derive(Deserialize, Clone, Debug)]
124pub struct IconUuid {
125 #[serde(rename = "NS.uuidbytes")]
126 pub bytes: plist::Data,
127 #[serde(rename = "$classes")]
128 pub classes: Vec<String>,
129}
130
131impl<R: ReadWrite> AppServiceClient<R> {
132 pub async fn new(stream: R) -> Result<Self, IdeviceError> {
133 Ok(Self {
134 inner: CoreDeviceServiceClient::new(stream).await?,
135 })
136 }
137
138 pub async fn list_apps(
139 &mut self,
140 app_clips: bool,
141 removable_apps: bool,
142 hidden_apps: bool,
143 internal_apps: bool,
144 default_apps: bool,
145 ) -> Result<Vec<AppListEntry>, IdeviceError> {
146 let options = crate::plist!(dict {
147 "includeAppClips": app_clips,
148 "includeRemovableApps": removable_apps,
149 "includeHiddenApps": hidden_apps,
150 "includeInternalApps": internal_apps,
151 "includeDefaultApps": default_apps,
152 });
153 let res = self
154 .inner
155 .invoke_with_plist("com.apple.coredevice.feature.listapps", options)
156 .await?;
157
158 let res = match res.as_array() {
159 Some(a) => a,
160 None => {
161 warn!("CoreDevice result was not an array");
162 return Err(IdeviceError::UnexpectedResponse);
163 }
164 };
165
166 let mut desd = Vec::new();
167 for r in res {
168 let r: AppListEntry = match plist::from_value(r) {
169 Ok(r) => r,
170 Err(e) => {
171 warn!("Failed to parse app entry: {e:?}");
172 return Err(IdeviceError::UnexpectedResponse);
173 }
174 };
175 desd.push(r);
176 }
177
178 Ok(desd)
179 }
180
181 #[allow(clippy::too_many_arguments)] pub async fn launch_application(
192 &mut self,
193 bundle_id: impl Into<String>,
194 arguments: &[&str],
195 kill_existing: bool,
196 start_suspended: bool,
197 environment: Option<plist::Dictionary>,
198 platform_options: Option<plist::Dictionary>,
199 stdio_uuid: Option<uuid::Uuid>,
200 ) -> Result<LaunchResponse, IdeviceError> {
201 let bundle_id = bundle_id.into();
202
203 let req = crate::plist!({
204 "applicationSpecifier": {
205 "bundleIdentifier": {
206 "_0": bundle_id
207 }
208 },
209 "options": {
210 "arguments": arguments,
211 "environmentVariables": environment.unwrap_or_default(),
212 "standardIOUsesPseudoterminals": true,
213 "startStopped": start_suspended,
214 "terminateExisting": kill_existing,
215 "user": {
216 "active": true,
217 },
218 "platformSpecificOptions": plist::Value::Data(crate::util::plist_to_xml_bytes(&platform_options.unwrap_or_default())),
219 },
220 });
221
222 let req: XPCObject = req.into();
223 let mut req = req.to_dictionary().unwrap();
224 req.insert(
225 "standardIOIdentifiers".into(),
226 match stdio_uuid {
227 Some(u) => {
228 let u = XPCObject::Uuid(u);
229 let mut d = crate::xpc::Dictionary::new();
230 d.insert("standardInput".into(), u.clone());
231 d.insert("standardOutput".into(), u.clone());
232 d.insert("standardError".into(), u.clone());
233 d.into()
234 }
235 None => crate::xpc::Dictionary::new().into(),
236 },
237 );
238
239 let res = self
240 .inner
241 .invoke("com.apple.coredevice.feature.launchapplication", Some(req))
242 .await?;
243
244 let res = match res
245 .as_dictionary()
246 .and_then(|r| r.get("processToken"))
247 .and_then(|x| plist::from_value(x).ok())
248 {
249 Some(r) => r,
250 None => {
251 warn!("CoreDevice res did not contain parsable processToken");
252 return Err(IdeviceError::UnexpectedResponse);
253 }
254 };
255
256 Ok(res)
257 }
258
259 pub async fn list_processes(&mut self) -> Result<Vec<ProcessToken>, IdeviceError> {
260 let res = self
261 .inner
262 .invoke("com.apple.coredevice.feature.listprocesses", None)
263 .await?;
264
265 let res = match res
266 .as_dictionary()
267 .and_then(|x| x.get("processTokens"))
268 .and_then(|x| plist::from_value(x).ok())
269 {
270 Some(r) => r,
271 None => {
272 warn!("CoreDevice res did not contain parsable processToken");
273 return Err(IdeviceError::UnexpectedResponse);
274 }
275 };
276
277 Ok(res)
278 }
279
280 pub async fn uninstall_app(
282 &mut self,
283 bundle_id: impl Into<String>,
284 ) -> Result<(), IdeviceError> {
285 let bundle_id = bundle_id.into();
286 self.inner
287 .invoke_with_plist(
288 "com.apple.coredevice.feature.uninstallapp",
289 crate::plist!({"bundleIdentifier": bundle_id})
290 .into_dictionary()
291 .unwrap(),
292 )
293 .await?;
294
295 Ok(())
296 }
297
298 pub async fn send_signal(
299 &mut self,
300 pid: u32,
301 signal: u32,
302 ) -> Result<SignalResponse, IdeviceError> {
303 let res = self
304 .inner
305 .invoke_with_plist(
306 "com.apple.coredevice.feature.sendsignaltoprocess",
307 crate::plist!({
308 "process": { "processIdentifier": pid as i64},
309 "signal": signal as i64,
310 })
311 .into_dictionary()
312 .unwrap(),
313 )
314 .await?;
315
316 let res = match plist::from_value(&res) {
317 Ok(r) => r,
318 Err(e) => {
319 warn!("Could not parse signal response: {e:?}");
320 return Err(IdeviceError::UnexpectedResponse);
321 }
322 };
323
324 Ok(res)
325 }
326
327 pub async fn fetch_app_icon(
328 &mut self,
329 bundle_id: impl Into<String>,
330 width: f32,
331 height: f32,
332 scale: f32,
333 allow_placeholder: bool,
334 ) -> Result<IconData, IdeviceError> {
335 let bundle_id = bundle_id.into();
336 let res = self
337 .inner
338 .invoke_with_plist(
339 "com.apple.coredevice.feature.fetchappicons",
340 crate::plist!({
341 "width": width,
342 "height": height,
343 "scale": scale,
344 "allowPlaceholder": allow_placeholder,
345 "bundleIdentifier": bundle_id
346 })
347 .into_dictionary()
348 .unwrap(),
349 )
350 .await?;
351
352 let res = match res
353 .as_dictionary()
354 .and_then(|x| x.get("appIconContainer"))
355 .and_then(|x| x.as_dictionary())
356 .and_then(|x| x.get("iconImage"))
357 .and_then(|x| x.as_data())
358 {
359 Some(r) => r.to_vec(),
360 None => {
361 warn!("Did not receive appIconContainer/iconImage data");
362 return Err(IdeviceError::UnexpectedResponse);
363 }
364 };
365
366 let res = ns_keyed_archive::decode::from_bytes(&res)?;
367 match plist::from_value(&res) {
368 Ok(r) => Ok(r),
369 Err(e) => {
370 warn!("Failed to deserialize ns keyed archive: {e:?}");
371 Err(IdeviceError::UnexpectedResponse)
372 }
373 }
374 }
375}