1pub mod activity_trace;
19pub mod application_listing;
20pub mod core_profile_session;
21pub mod deviceinfo;
22pub mod devicestate;
23pub mod energy;
24pub mod fps;
25pub mod graphics;
26pub mod network;
27pub mod notifications;
28pub mod process_control;
29pub mod screenshot;
30pub mod tap;
31
32pub use activity_trace::{
33 ActivityTraceClient, ActivityTraceDecoder, ActivityTraceEntry, ActivityTraceValue,
34};
35pub use application_listing::ApplicationListingClient;
36pub use core_profile_session::{
37 CoreProfileConfig, CoreProfileEvent, CoreProfileSessionClient, CORE_PROFILE_SESSION_SVC,
38};
39pub use deviceinfo::{DeviceInfoClient, RunningProcess};
40pub use devicestate::{ConditionProfile, ConditionProfileType, DeviceStateClient};
41pub use energy::EnergyMonitorClient;
42pub use fps::{parse_frame_commit_timestamps, FpsSample, FpsWindowCalculator, MachTimeInfo};
43pub use graphics::GraphicsMonitorClient;
44pub use network::{
45 ConnectionDetectionEvent, ConnectionUpdateEvent, InterfaceDetectionEvent, NetworkMonitorClient,
46 NetworkMonitorEvent, SocketAddress,
47};
48pub use notifications::{NotificationClient, NotificationEvent};
49pub use process_control::{ProcessControl, ProcessInfo};
50pub use screenshot::take_screenshot_dtx;
51pub use screenshot::take_screenshot_dtx as start_screenshot;
52pub use tap::TapClient;
53
54pub const SERVICE_LEGACY: &str = "com.apple.instruments.remoteserver";
57pub const SERVICE_IOS14: &str = "com.apple.instruments.remoteserver.DVTSecureSocketProxy";
58pub const SERVICE_IOS17: &str = "com.apple.instruments.dtservicehub"; pub const SYSMONTAP: &str = "com.apple.instruments.server.services.sysmontap";
60pub const DEVICE_INFO_SVC: &str = "com.apple.instruments.server.services.deviceinfo";
61pub const PROCESS_CTRL_SVC: &str = "com.apple.instruments.server.services.processcontrol";
62pub const SCREENSHOT_SVC: &str = "com.apple.instruments.server.services.screenshot";
63pub const APP_LISTING_SVC: &str = "com.apple.instruments.server.services.device.applictionListing";
64pub const ACTIVITY_TRACE_TAP_SVC: &str = "com.apple.instruments.server.services.activitytracetap";
65pub const CONDITION_INDUCER_SVC: &str = "com.apple.instruments.server.services.ConditionInducer";
66pub const ENERGY_MONITOR_SVC: &str = "com.apple.xcode.debug-gauge-data-providers.Energy";
67pub const GRAPHICS_MONITOR_SVC: &str = "com.apple.instruments.server.services.graphics.opengl";
68pub const MOBILE_NOTIFICATIONS_SVC: &str =
69 "com.apple.instruments.server.services.mobilenotifications";
70pub const NETWORK_MONITOR_SVC: &str = "com.apple.instruments.server.services.networking";
71
72#[derive(Debug, Clone)]
76pub struct CpuSample {
77 pub cpu_count: u64,
78 pub enabled_cpus: u64,
79 pub end_mach_abs_time: u64,
80 pub cpu_total_load: f64,
81 pub sample_type: u64,
82}
83
84#[derive(Debug, Clone)]
86pub struct MemSample {
87 pub memory_used: u64, pub memory_total: u64, }
90
91#[derive(Debug, Clone)]
93pub struct ProcessSample {
94 pub processes: Vec<serde_json::Map<String, serde_json::Value>>,
96 pub system_cpu: Option<CpuSample>,
98}
99
100pub struct SysmontapConfig {
104 pub update_rate: i32,
106 pub cpu_usage: bool,
108 pub phys_footprint: bool,
110 pub sample_interval: i64,
112}
113
114impl Default for SysmontapConfig {
115 fn default() -> Self {
116 Self {
117 update_rate: 10,
118 cpu_usage: true,
119 phys_footprint: true,
120 sample_interval: 500_000_000,
121 }
122 }
123}
124
125use crate::proto::nskeyedarchiver_encode;
128use tokio::io::{AsyncRead, AsyncWrite};
129
130use crate::services::dtx::codec::{DtxConnection, DtxError};
131use crate::services::dtx::primitive_enc::archived_object;
132use crate::services::dtx::types::{DtxMessage, DtxPayload, NSObject};
133
134pub struct SysmontapService<S> {
138 conn: DtxConnection<S>,
139 channel_code: i32,
140}
141
142impl<S: AsyncRead + AsyncWrite + Unpin + Send> SysmontapService<S> {
143 pub async fn start(
148 stream: S,
149 config: &SysmontapConfig,
150 sys_attrs: Option<Vec<plist::Value>>,
151 proc_attrs: Option<Vec<plist::Value>>,
152 ) -> Result<Self, DtxError> {
153 let mut conn = DtxConnection::new(stream);
154
155 let ch = conn.request_channel(SYSMONTAP).await?;
157
158 let mut config_dict = build_sysmontap_config(config);
160 if let Some(attrs) = sys_attrs {
161 if !attrs.is_empty() {
162 config_dict.push(("sysAttrs".to_string(), plist::Value::Array(attrs)));
163 }
164 }
165 if let Some(attrs) = proc_attrs {
166 if !attrs.is_empty() {
167 config_dict.push(("procAttrs".to_string(), plist::Value::Array(attrs)));
168 }
169 }
170 let archived = nskeyedarchiver_encode::archive_dict(config_dict);
171
172 let cfg_resp = conn
174 .method_call(ch, "setConfig:", &[archived_object(archived)])
175 .await?;
176 tracing::debug!("sysmontap setConfig: response: {:?}", cfg_resp.payload);
177
178 conn.method_call_async(ch, "start", &[]).await?;
180
181 Ok(Self {
182 conn,
183 channel_code: ch,
184 })
185 }
186
187 pub async fn next_cpu_sample(&mut self) -> Result<Option<CpuSample>, DtxError> {
190 loop {
191 let msg = self.conn.recv().await?;
192 tracing::debug!(
193 "next_cpu_sample: id={} ch={} expects_reply={} payload={:?}",
194 msg.identifier,
195 msg.channel_code,
196 msg.expects_reply,
197 std::mem::discriminant(&msg.payload)
198 );
199
200 if msg.expects_reply {
202 self.conn.send_ack(&msg).await?;
203 }
204
205 if msg.channel_code != self.channel_code && msg.channel_code != -1 {
207 continue;
208 }
209
210 tracing::debug!(
211 "sysmontap msg ch={} payload={:?}",
212 msg.channel_code,
213 &msg.payload
214 );
215
216 if let Some(sample) = parse_cpu_sample(&msg) {
217 return Ok(Some(sample));
218 }
219 }
220 }
221
222 pub async fn next_process_snapshot(
228 &mut self,
229 proc_attr_names: &[String],
230 ) -> Result<Option<ProcessSample>, DtxError> {
231 loop {
232 let msg = self.conn.recv().await?;
233 if msg.expects_reply {
234 self.conn.send_ack(&msg).await?;
235 }
236 if msg.channel_code != self.channel_code && msg.channel_code != -1 {
237 continue;
238 }
239 if let Some(sample) = parse_process_snapshot(&msg, proc_attr_names) {
240 if !sample.processes.is_empty() {
241 return Ok(Some(sample));
242 }
243 }
244 }
245 }
246
247 pub async fn stop(&mut self) -> Result<(), DtxError> {
249 self.conn
250 .method_call_async(self.channel_code, "stop", &[])
251 .await
252 }
253}
254
255fn build_sysmontap_config(cfg: &SysmontapConfig) -> Vec<(String, plist::Value)> {
258 vec![
259 (
260 "ur".to_string(),
261 plist::Value::Integer(cfg.update_rate.into()),
262 ),
263 ("bm".to_string(), plist::Value::Integer(0.into())),
264 ("cpuUsage".to_string(), plist::Value::Boolean(cfg.cpu_usage)),
265 (
266 "physFootprint".to_string(),
267 plist::Value::Boolean(cfg.phys_footprint),
268 ),
269 (
270 "sampleInterval".to_string(),
271 plist::Value::Integer(cfg.sample_interval.into()),
272 ),
273 ]
274}
275
276fn parse_cpu_sample(msg: &DtxMessage) -> Option<CpuSample> {
279 let args = match &msg.payload {
282 DtxPayload::MethodInvocation { args, .. } => args,
283 DtxPayload::Response(NSObject::Array(arr)) => {
284 return parse_from_array(arr);
285 }
286 DtxPayload::Raw(bytes) => match unarchive_raw_payload(bytes) {
287 Some(NSObject::Array(arr)) => {
288 return parse_from_array(&arr);
289 }
290 _ => return None,
291 },
292 _ => return None,
293 };
294
295 for arg in args {
297 if let NSObject::Array(arr) = arg {
298 return parse_from_array(arr);
299 }
300 }
301 None
302}
303
304fn parse_from_array(arr: &[NSObject]) -> Option<CpuSample> {
305 for item in arr {
307 if let NSObject::Dict(d) = item {
308 let sys_cpu = match d.get("SystemCPUUsage") {
310 Some(NSObject::Dict(s)) => s,
311 _ => continue,
312 };
313 let cpu_count = get_uint(d, "CPUCount").unwrap_or(0);
314 let enabled = get_uint(d, "EnabledCPUs").unwrap_or(0);
315 let end_time = get_uint(d, "EndMachAbsTime").unwrap_or(0);
316 let typ = get_uint(d, "Type").unwrap_or(0);
317 let cpu_load = get_float(sys_cpu, "CPU_TotalLoad").unwrap_or(0.0);
318
319 return Some(CpuSample {
320 cpu_count,
321 enabled_cpus: enabled,
322 end_mach_abs_time: end_time,
323 cpu_total_load: cpu_load,
324 sample_type: typ,
325 });
326 }
327 }
328 None
329}
330
331fn parse_process_snapshot(msg: &DtxMessage, attr_names: &[String]) -> Option<ProcessSample> {
343 match &msg.payload {
344 DtxPayload::MethodInvocation { args, .. } => {
345 for arg in args {
346 if let NSObject::Array(arr) = arg {
347 return parse_process_from_array(arr, attr_names);
348 }
349 }
350 None
351 }
352 DtxPayload::Response(NSObject::Array(arr)) => parse_process_from_array(arr, attr_names),
353 DtxPayload::Response(NSObject::Dict(d)) => {
354 parse_process_from_array(&[NSObject::Dict(d.clone())], attr_names)
356 }
357 DtxPayload::Raw(bytes) => match unarchive_raw_payload(bytes) {
358 Some(NSObject::Array(arr)) => parse_process_from_array(&arr, attr_names),
359 _ => None,
360 },
361 DtxPayload::RawWithAux { payload, aux } => {
362 for arg in aux {
364 if let NSObject::Array(arr) = arg {
365 if let Some(sample) = parse_process_from_array(arr, attr_names) {
366 return Some(sample);
367 }
368 }
369 }
370 match unarchive_raw_payload(payload) {
372 Some(NSObject::Array(arr)) => parse_process_from_array(&arr, attr_names),
373 _ => None,
374 }
375 }
376 _ => None,
377 }
378}
379
380fn parse_process_from_array(arr: &[NSObject], attr_names: &[String]) -> Option<ProcessSample> {
381 let mut processes = Vec::new();
384 let mut system_cpu = None;
385
386 for item in arr {
387 if let NSObject::Dict(d) = item {
388 if system_cpu.is_none() {
390 if let Some(NSObject::Dict(sys_cpu)) = d.get("SystemCPUUsage") {
391 system_cpu = Some(CpuSample {
392 cpu_count: get_uint(d, "CPUCount").unwrap_or(0),
393 enabled_cpus: get_uint(d, "EnabledCPUs").unwrap_or(0),
394 end_mach_abs_time: get_uint(d, "EndMachAbsTime").unwrap_or(0),
395 cpu_total_load: get_float(sys_cpu, "CPU_TotalLoad").unwrap_or(0.0),
396 sample_type: get_uint(d, "Type").unwrap_or(0),
397 });
398 }
399 }
400
401 if let Some(NSObject::Dict(processes_dict)) = d.get("Processes") {
404 for (_pid_key, values) in processes_dict {
405 if let NSObject::Array(vals) = values {
406 let mut proc_map = serde_json::Map::new();
407 for (i, val) in vals.iter().enumerate() {
408 let key = attr_names
409 .get(i)
410 .cloned()
411 .unwrap_or_else(|| format!("attr_{i}"));
412 proc_map.insert(key, nsobject_to_json(val));
413 }
414 processes.push(proc_map);
415 }
416 }
417 }
418 }
419 }
420
421 if !processes.is_empty() || system_cpu.is_some() {
422 Some(ProcessSample {
423 processes,
424 system_cpu,
425 })
426 } else {
427 None
428 }
429}
430
431fn get_uint(d: &indexmap::IndexMap<String, NSObject>, key: &str) -> Option<u64> {
432 match d.get(key) {
433 Some(NSObject::Uint(n)) => Some(*n),
434 Some(NSObject::Int(n)) => Some(*n as u64),
435 _ => None,
436 }
437}
438
439fn get_float(d: &indexmap::IndexMap<String, NSObject>, key: &str) -> Option<f64> {
440 match d.get(key) {
441 Some(NSObject::Double(f)) => Some(*f),
442 Some(NSObject::Int(n)) => Some(*n as f64),
443 _ => None,
444 }
445}
446
447pub(crate) fn archive_value_to_nsobject(
448 value: crate::proto::nskeyedarchiver::ArchiveValue,
449) -> NSObject {
450 use crate::proto::nskeyedarchiver::ArchiveValue;
451
452 match value {
453 ArchiveValue::Null => NSObject::Null,
454 ArchiveValue::Bool(value) => NSObject::Bool(value),
455 ArchiveValue::Int(value) => NSObject::Int(value),
456 ArchiveValue::Float(value) => NSObject::Double(value),
457 ArchiveValue::String(value) => NSObject::String(value),
458 ArchiveValue::Data(value) => NSObject::Data(value),
459 ArchiveValue::Array(values) => {
460 NSObject::Array(values.into_iter().map(archive_value_to_nsobject).collect())
461 }
462 ArchiveValue::Dict(dict) => NSObject::Dict(
463 dict.into_iter()
464 .map(|(key, value)| (key, archive_value_to_nsobject(value)))
465 .collect(),
466 ),
467 ArchiveValue::Unknown(name) => NSObject::String(format!("<{name}>")),
468 }
469}
470
471pub(crate) fn unarchive_raw_payload(payload: &bytes::Bytes) -> Option<NSObject> {
472 crate::proto::nskeyedarchiver::unarchive(payload)
473 .ok()
474 .map(archive_value_to_nsobject)
475}
476
477pub(crate) fn nsobject_to_json(value: &NSObject) -> serde_json::Value {
478 use serde_json::{Map, Number, Value};
479
480 match value {
481 NSObject::Int(value) => Value::from(*value),
482 NSObject::Uint(value) => Value::from(*value),
483 NSObject::Double(value) => Number::from_f64(*value)
484 .map(Value::Number)
485 .unwrap_or(Value::Null),
486 NSObject::Bool(value) => Value::Bool(*value),
487 NSObject::String(value) => Value::String(value.clone()),
488 NSObject::Data(value) => Value::String(
489 value
490 .iter()
491 .map(|byte| format!("{byte:02x}"))
492 .collect::<String>(),
493 ),
494 NSObject::Array(values) => Value::Array(values.iter().map(nsobject_to_json).collect()),
495 NSObject::Dict(dict) => Value::Object(
496 dict.iter()
497 .map(|(key, value)| (key.clone(), nsobject_to_json(value)))
498 .collect::<Map<_, _>>(),
499 ),
500 NSObject::Null => Value::Null,
501 }
502}