ios_core/services/instruments/
process_control.rs1use std::collections::HashMap;
7
8use crate::proto::nskeyedarchiver_encode;
9use tokio::io::{AsyncRead, AsyncWrite};
10
11use crate::services::dtx::codec::{DtxConnection, DtxError};
12use crate::services::dtx::primitive_enc::archived_object;
13use crate::services::dtx::types::{DtxPayload, NSObject};
14
15#[derive(Debug, Clone)]
17pub struct ProcessInfo {
18 pub pid: u64,
19 pub name: String,
20 pub real_app_name: String,
21 pub is_application: bool,
22}
23
24pub struct ProcessControl<S> {
27 conn: DtxConnection<S>,
28 channel_code: i32,
29}
30
31impl<S: AsyncRead + AsyncWrite + Unpin + Send> ProcessControl<S> {
32 pub async fn connect(stream: S) -> Result<Self, DtxError> {
34 let mut conn = DtxConnection::new(stream);
35 let ch = conn.request_channel(super::PROCESS_CTRL_SVC).await?;
36 Ok(Self {
37 conn,
38 channel_code: ch,
39 })
40 }
41
42 pub async fn launch(
44 &mut self,
45 bundle_id: &str,
46 args: &[&str],
47 env: &HashMap<String, String>,
48 ) -> Result<u64, DtxError> {
49 self.launch_with_options(
50 bundle_id,
51 args,
52 env,
53 &[
54 (
55 "StartSuspendedKey".to_string(),
56 plist::Value::Boolean(false),
57 ),
58 ("KillExisting".to_string(), plist::Value::Boolean(false)),
59 ],
60 )
61 .await
62 }
63
64 pub async fn launch_with_options(
66 &mut self,
67 bundle_id: &str,
68 args: &[&str],
69 env: &HashMap<String, String>,
70 options: &[(String, plist::Value)],
71 ) -> Result<u64, DtxError> {
72 let path_enc = archived_object(nskeyedarchiver_encode::archive_string("/private/"));
74 let bid_enc = archived_object(nskeyedarchiver_encode::archive_string(bundle_id));
75
76 let mut full_env: Vec<(String, plist::Value)> = vec![(
78 "NSUnbufferedIO".to_string(),
79 plist::Value::String("YES".to_string()),
80 )];
81 for (k, v) in env {
82 full_env.push((k.clone(), plist::Value::String(v.clone())));
83 }
84 let env_enc = archived_object(nskeyedarchiver_encode::archive_dict(full_env));
85
86 let args_enc = archived_object(nskeyedarchiver_encode::archive_array(
87 args.iter()
88 .map(|s| plist::Value::String(s.to_string()))
89 .collect(),
90 ));
91
92 let opts_enc = archived_object(nskeyedarchiver_encode::archive_dict(options.to_vec()));
93
94 let msg = self.conn.method_call(
95 self.channel_code,
96 "launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:",
97 &[path_enc, bid_enc, env_enc, args_enc, opts_enc],
98 ).await?;
99
100 if let DtxPayload::Response(NSObject::Int(pid)) = msg.payload {
101 return Ok(pid as u64);
102 }
103 if let DtxPayload::Response(NSObject::Uint(pid)) = msg.payload {
104 return Ok(pid);
105 }
106 Err(DtxError::Protocol(format!(
107 "unexpected launch response: {:?}",
108 msg.payload
109 )))
110 }
111
112 pub async fn kill(&mut self, pid: u64) -> Result<(), DtxError> {
114 let pid_enc = archived_object(nskeyedarchiver_encode::archive_int(pid as i64));
115 self.conn
116 .method_call_async(self.channel_code, "killPid:", &[pid_enc])
117 .await
118 }
119
120 pub async fn disable_memory_limit(&mut self, pid: u64) -> Result<bool, DtxError> {
122 let msg = self
123 .conn
124 .method_call(
125 self.channel_code,
126 "requestDisableMemoryLimitsForPid:",
127 &[crate::services::dtx::primitive_enc::PrimArg::Int32(
128 pid as i32,
129 )],
130 )
131 .await?;
132
133 match msg.payload {
134 DtxPayload::Response(NSObject::Bool(disabled)) => Ok(disabled),
135 DtxPayload::Response(NSObject::Int(disabled)) => Ok(disabled != 0),
136 DtxPayload::Response(NSObject::Uint(disabled)) => Ok(disabled != 0),
137 other => Err(DtxError::Protocol(format!(
138 "unexpected disableMemoryLimit response: {other:?}"
139 ))),
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use plist::Value;
147 use tokio::io::{duplex, AsyncWriteExt};
148
149 use super::*;
150 use crate::services::dtx::{encode_dtx, read_dtx_frame, DtxPayload, NSObject};
151
152 const MSG_RESPONSE: u32 = 3;
153
154 #[tokio::test]
155 async fn launch_with_options_sends_expected_archived_arguments() {
156 let (client, mut server) = duplex(4096);
157 let task = tokio::spawn(async move {
158 let mut env = HashMap::new();
159 env.insert("TERM".to_string(), "xterm-256color".to_string());
160
161 let mut client = ProcessControl::connect(client).await.unwrap();
162 client
163 .launch_with_options(
164 "com.example.demo",
165 &["--flag"],
166 &env,
167 &[("KillExisting".to_string(), Value::Boolean(true))],
168 )
169 .await
170 .unwrap()
171 });
172
173 let channel_request = read_dtx_frame(&mut server).await.unwrap();
174 match channel_request.payload {
175 DtxPayload::MethodInvocation { selector, args } => {
176 assert_eq!(selector, "_requestChannelWithCode:identifier:");
177 assert!(
178 matches!(args.get(1), Some(NSObject::String(name)) if name == super::super::PROCESS_CTRL_SVC)
179 );
180 }
181 other => panic!("unexpected channel request: {other:?}"),
182 }
183 server
184 .write_all(&encode_dtx(
185 channel_request.identifier,
186 1,
187 0,
188 false,
189 MSG_RESPONSE,
190 &[],
191 &[],
192 ))
193 .await
194 .unwrap();
195
196 let launch = read_dtx_frame(&mut server).await.unwrap();
197 match launch.payload {
198 DtxPayload::MethodInvocation { selector, args } => {
199 assert_eq!(
200 selector,
201 "launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:"
202 );
203 assert!(
204 matches!(args.first(), Some(NSObject::String(path)) if path == "/private/")
205 );
206 assert!(
207 matches!(args.get(1), Some(NSObject::String(bundle)) if bundle == "com.example.demo")
208 );
209 match args.get(2) {
210 Some(NSObject::Dict(env)) => {
211 assert_eq!(
212 env.get("NSUnbufferedIO"),
213 Some(&NSObject::String("YES".into()))
214 );
215 assert_eq!(
216 env.get("TERM"),
217 Some(&NSObject::String("xterm-256color".into()))
218 );
219 }
220 other => panic!("unexpected env payload: {other:?}"),
221 }
222 assert!(matches!(
223 args.get(3),
224 Some(NSObject::Array(values))
225 if values == &vec![NSObject::String("--flag".into())]
226 ));
227 assert!(matches!(
228 args.get(4),
229 Some(NSObject::Dict(options))
230 if options.get("KillExisting") == Some(&NSObject::Bool(true))
231 ));
232 }
233 other => panic!("unexpected launch request: {other:?}"),
234 }
235 server
236 .write_all(&encode_dtx(
237 launch.identifier,
238 1,
239 launch.channel_code,
240 false,
241 MSG_RESPONSE,
242 &crate::proto::nskeyedarchiver_encode::archive_int(4242),
243 &[],
244 ))
245 .await
246 .unwrap();
247
248 assert_eq!(task.await.unwrap(), 4242);
249 }
250
251 #[tokio::test]
252 async fn disable_memory_limit_treats_integer_response_as_boolean() {
253 let (client, mut server) = duplex(4096);
254 let task = tokio::spawn(async move {
255 let mut client = ProcessControl::connect(client).await.unwrap();
256 client.disable_memory_limit(99).await.unwrap()
257 });
258
259 let channel_request = read_dtx_frame(&mut server).await.unwrap();
260 server
261 .write_all(&encode_dtx(
262 channel_request.identifier,
263 1,
264 0,
265 false,
266 MSG_RESPONSE,
267 &[],
268 &[],
269 ))
270 .await
271 .unwrap();
272
273 let request = read_dtx_frame(&mut server).await.unwrap();
274 match request.payload {
275 DtxPayload::MethodInvocation { selector, args } => {
276 assert_eq!(selector, "requestDisableMemoryLimitsForPid:");
277 assert!(matches!(args.first(), Some(NSObject::Int(99))));
278 }
279 other => panic!("unexpected disable request: {other:?}"),
280 }
281 server
282 .write_all(&encode_dtx(
283 request.identifier,
284 1,
285 request.channel_code,
286 false,
287 MSG_RESPONSE,
288 &crate::proto::nskeyedarchiver_encode::archive_int(1),
289 &[],
290 ))
291 .await
292 .unwrap();
293
294 assert!(task.await.unwrap());
295 }
296}