use plist::{Dictionary, Value};
use tracing::warn;
use super::errors::DvtError;
use crate::{IdeviceError, ReadWrite, dvt::message::AuxValue, obf};
use super::remote_server::{Channel, RemoteServerClient};
#[derive(Debug)]
pub struct ProcessControlClient<'a, R: ReadWrite> {
channel: Channel<'a, R>,
}
fn parse_u64_value(value: &Value) -> Option<u64> {
match value {
Value::Integer(v) => v.as_unsigned(),
Value::String(s) => s.parse().ok(),
_ => None,
}
}
fn extract_ns_error_message(value: &Value) -> Option<String> {
let dict = match value {
Value::Dictionary(dict) => dict,
_ => return None,
};
let user_info = match dict.get("NSUserInfo") {
Some(Value::Array(items)) => items,
_ => {
return dict
.get("NSLocalizedDescription")
.and_then(Value::as_string)
.map(ToOwned::to_owned);
}
};
let mut description = dict
.get("NSLocalizedDescription")
.and_then(Value::as_string)
.map(ToOwned::to_owned);
let mut reason = dict
.get("NSLocalizedFailureReason")
.and_then(Value::as_string)
.map(ToOwned::to_owned);
for entry in user_info {
let Value::Dictionary(item) = entry else {
continue;
};
let key = item.get("key").and_then(Value::as_string);
let value = item.get("value");
match (key, value) {
(Some("NSLocalizedDescription"), Some(Value::String(s))) if description.is_none() => {
description = Some(s.clone());
}
(Some("NSLocalizedFailureReason"), Some(Value::String(s))) if reason.is_none() => {
reason = Some(s.clone());
}
(Some("NSUnderlyingError"), Some(v)) => {
if let Some(message) = extract_ns_error_message(v) {
return Some(message);
}
}
_ => {}
}
}
match (description, reason) {
(Some(description), Some(reason)) => Some(format!("{description}: {reason}")),
(Some(description), None) => Some(description),
(None, Some(reason)) => Some(reason),
(None, None) => None,
}
}
impl<'a, R: ReadWrite> ProcessControlClient<'a, R> {
pub async fn new(client: &'a mut RemoteServerClient<R>) -> Result<Self, IdeviceError> {
let channel = client
.make_channel(obf!("com.apple.instruments.server.services.processcontrol"))
.await?;
Ok(Self { channel })
}
pub async fn launch_app(
&mut self,
bundle_id: impl Into<String>,
env_vars: Option<Dictionary>,
arguments: Option<Dictionary>,
start_suspended: bool,
kill_existing: bool,
) -> Result<u64, IdeviceError> {
let method = Value::String(
"launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:"
.into(),
);
let options = crate::plist!(dict {
"StartSuspendedKey": start_suspended,
"KillExisting": kill_existing
});
let env_vars = match env_vars {
Some(e) => e,
None => Dictionary::new(),
};
let arguments = match arguments {
Some(a) => a,
None => Dictionary::new(),
};
let res = self
.channel
.call_method_with_reply(
Some(method),
Some(vec![
AuxValue::archived_value(""),
AuxValue::archived_value(bundle_id.into()),
AuxValue::archived_value(env_vars),
AuxValue::archived_value(Value::Array(
arguments
.into_iter()
.map(|(_, value)| value)
.collect::<Vec<_>>(),
)),
AuxValue::archived_value(options),
]),
)
.await?;
match res.data {
Some(Value::Integer(p)) => match p.as_unsigned() {
Some(p) => Ok(p),
None => {
warn!("PID wasn't unsigned");
Err(IdeviceError::UnexpectedResponse(
"launch response PID was not an unsigned integer".into(),
))
}
},
_ => {
warn!("Did not get integer response");
Err(IdeviceError::UnexpectedResponse(
"expected integer PID in launch app response".into(),
))
}
}
}
pub(crate) async fn launch_with_options(
&mut self,
bundle_id: impl Into<String>,
env: Dictionary,
args: Vec<Value>,
options: Dictionary,
) -> Result<u64, IdeviceError> {
let res = self
.channel
.call_method_with_reply(
Some(Value::String(
"launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:"
.into(),
)),
Some(vec![
AuxValue::archived_value(Value::String(String::new())), AuxValue::archived_value(bundle_id.into()),
AuxValue::archived_value(Value::Dictionary(env)),
AuxValue::archived_value(Value::Array(args)),
AuxValue::archived_value(Value::Dictionary(options)),
]),
)
.await?;
match res.data {
Some(v) => parse_u64_value(&v).ok_or_else(|| {
if let Some(message) = extract_ns_error_message(&v) {
warn!("Launch failed: {message}");
return IdeviceError::InternalError(message);
}
warn!("PID wasn't parseable: {v:?}");
IdeviceError::UnexpectedResponse("unexpected response".into())
}),
_ => {
warn!("Did not get integer response from launchSuspendedProcess");
Err(IdeviceError::UnexpectedResponse(
"unexpected response".into(),
))
}
}
}
pub async fn kill_app(&mut self, pid: u64) -> Result<(), IdeviceError> {
self.channel
.call_method(
"killPid:".into(),
Some(vec![AuxValue::U32(pid as u32)]),
false,
)
.await?;
Ok(())
}
pub async fn disable_memory_limit(&mut self, pid: u64) -> Result<(), IdeviceError> {
let res = self
.channel
.call_method_with_reply(
"requestDisableMemoryLimitsForPid:".into(),
Some(vec![AuxValue::U32(pid as u32)]),
)
.await?;
match res.data {
Some(Value::Boolean(b)) => {
if b {
Ok(())
} else {
warn!("Failed to disable memory limit");
Err(DvtError::DisableMemoryLimitFailed.into())
}
}
_ => {
warn!("Did not receive bool response");
Err(IdeviceError::UnexpectedResponse(
"expected boolean in disable memory limit response".into(),
))
}
}
}
}