mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Implement os_trace_relay
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1184,7 +1184,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idevice"
|
name = "idevice"
|
||||||
version = "0.1.32"
|
version = "0.1.33"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@@ -173,6 +173,31 @@ impl Idevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends a binary plist-formatted message to the device
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `message` - The plist value to send
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if serialization or transmission fails
|
||||||
|
async fn send_bplist(&mut self, message: plist::Value) -> Result<(), IdeviceError> {
|
||||||
|
if let Some(socket) = &mut self.socket {
|
||||||
|
debug!("Sending plist: {}", pretty_print_plist(&message));
|
||||||
|
|
||||||
|
let buf = Vec::new();
|
||||||
|
let mut writer = BufWriter::new(buf);
|
||||||
|
message.to_writer_binary(&mut writer)?;
|
||||||
|
let message = writer.into_inner().unwrap();
|
||||||
|
let len = message.len() as u32;
|
||||||
|
socket.write_all(&len.to_be_bytes()).await?;
|
||||||
|
socket.write_all(&message).await?;
|
||||||
|
socket.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(IdeviceError::NoEstablishedConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends raw binary data to the device
|
/// Sends raw binary data to the device
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -327,6 +352,23 @@ impl Idevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_until_byte(&mut self, stopper: u8) -> Result<Vec<u8>, IdeviceError> {
|
||||||
|
if let Some(socket) = &mut self.socket {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let b = socket.read_u8().await?;
|
||||||
|
if b == stopper {
|
||||||
|
return Ok(buf);
|
||||||
|
} else {
|
||||||
|
buf.push(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(IdeviceError::NoEstablishedConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Upgrades the connection to TLS using device pairing credentials
|
/// Upgrades the connection to TLS using device pairing credentials
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ pub mod lockdown;
|
|||||||
pub mod misagent;
|
pub mod misagent;
|
||||||
#[cfg(feature = "mobile_image_mounter")]
|
#[cfg(feature = "mobile_image_mounter")]
|
||||||
pub mod mobile_image_mounter;
|
pub mod mobile_image_mounter;
|
||||||
|
#[cfg(feature = "syslog_relay")]
|
||||||
|
pub mod os_trace_relay;
|
||||||
#[cfg(feature = "springboardservices")]
|
#[cfg(feature = "springboardservices")]
|
||||||
pub mod springboardservices;
|
pub mod springboardservices;
|
||||||
#[cfg(feature = "syslog_relay")]
|
#[cfg(feature = "syslog_relay")]
|
||||||
|
|||||||
359
idevice/src/services/os_trace_relay.rs
Normal file
359
idevice/src/services/os_trace_relay.rs
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
//! iOS Device OsTraceRelay Service Abstraction
|
||||||
|
//! Note that there are unknown fields that will hopefully be filled in the future.
|
||||||
|
//! Huge thanks to pymobiledevice3 for the struct implementation
|
||||||
|
//! https://github.com/doronz88/pymobiledevice3/blob/master/pymobiledevice3/services/os_trace.py
|
||||||
|
|
||||||
|
use chrono::{DateTime, NaiveDateTime};
|
||||||
|
use plist::Dictionary;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||||
|
|
||||||
|
/// Client for interacting with the iOS device OsTraceRelay service
|
||||||
|
pub struct OsTraceRelayClient {
|
||||||
|
/// The underlying device connection with established OsTraceRelay service
|
||||||
|
pub idevice: Idevice,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdeviceService for OsTraceRelayClient {
|
||||||
|
/// Returns the OsTraceRelay service name as registered with lockdownd
|
||||||
|
fn service_name() -> &'static str {
|
||||||
|
"com.apple.os_trace_relay"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establishes a connection to the OsTraceRelay service
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `provider` - Device connection provider
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A connected `OsTraceRelayClient` instance
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if any step of the connection process fails
|
||||||
|
///
|
||||||
|
/// # Process
|
||||||
|
/// 1. Connects to lockdownd service
|
||||||
|
/// 2. Starts a lockdown session
|
||||||
|
/// 3. Requests the OsTraceRelay service port
|
||||||
|
/// 4. Establishes connection to the OsTraceRelay port
|
||||||
|
/// 5. Optionally starts TLS if required by service
|
||||||
|
async fn connect(
|
||||||
|
provider: &dyn crate::provider::IdeviceProvider,
|
||||||
|
) -> Result<Self, IdeviceError> {
|
||||||
|
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||||
|
lockdown
|
||||||
|
.start_session(&provider.get_pairing_file().await?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (port, ssl) = lockdown.start_service(Self::service_name()).await?;
|
||||||
|
|
||||||
|
let mut idevice = provider.connect(port).await?;
|
||||||
|
if ssl {
|
||||||
|
idevice
|
||||||
|
.start_session(&provider.get_pairing_file().await?)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { idevice })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An initialized client for receiving logs
|
||||||
|
pub struct OsTraceRelayReceiver {
|
||||||
|
inner: OsTraceRelayClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct OsTraceLog {
|
||||||
|
pid: u32,
|
||||||
|
timestamp: NaiveDateTime,
|
||||||
|
level: LogLevel,
|
||||||
|
image_name: String,
|
||||||
|
filename: String,
|
||||||
|
message: String,
|
||||||
|
label: Option<SyslogLabel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SyslogLabel {
|
||||||
|
subsystem: String,
|
||||||
|
category: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum LogLevel {
|
||||||
|
Notice = 0,
|
||||||
|
Info = 1,
|
||||||
|
Debug = 2,
|
||||||
|
Error = 10,
|
||||||
|
Fault = 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OsTraceRelayClient {
|
||||||
|
/// Starts the stream of logs from the relay
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `pid` - An optional pid to stream logs from
|
||||||
|
pub async fn start_trace(
|
||||||
|
mut self,
|
||||||
|
pid: Option<u32>,
|
||||||
|
) -> Result<OsTraceRelayReceiver, IdeviceError> {
|
||||||
|
let pid = match pid {
|
||||||
|
Some(p) => p as i64,
|
||||||
|
None => -1,
|
||||||
|
};
|
||||||
|
let mut req = Dictionary::new();
|
||||||
|
req.insert("Request".into(), "StartActivity".into());
|
||||||
|
req.insert("Pid".into(), Into::into(pid));
|
||||||
|
req.insert("MessageFilter".into(), Into::into(65_535));
|
||||||
|
req.insert("StreamFlags".into(), Into::into(60));
|
||||||
|
|
||||||
|
self.idevice
|
||||||
|
.send_bplist(plist::Value::Dictionary(req))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Read a single byte
|
||||||
|
self.idevice.read_raw(1).await?;
|
||||||
|
|
||||||
|
// Result
|
||||||
|
let res = self.idevice.read_plist().await?;
|
||||||
|
|
||||||
|
match res.get("Status").and_then(|x| x.as_string()) {
|
||||||
|
Some(r) => {
|
||||||
|
if r == "RequestSuccessful" {
|
||||||
|
Ok(OsTraceRelayReceiver { inner: self })
|
||||||
|
} else {
|
||||||
|
Err(IdeviceError::UnexpectedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(IdeviceError::UnexpectedResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the list of available PIDs
|
||||||
|
pub async fn get_pid_list(&mut self) -> Result<Vec<u64>, IdeviceError> {
|
||||||
|
let mut req = Dictionary::new();
|
||||||
|
req.insert("Request".into(), "PidList".into());
|
||||||
|
|
||||||
|
self.idevice
|
||||||
|
.send_bplist(plist::Value::Dictionary(req))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Read a single byte
|
||||||
|
self.idevice.read_raw(1).await?;
|
||||||
|
|
||||||
|
// Result
|
||||||
|
let res = self.idevice.read_plist().await?;
|
||||||
|
|
||||||
|
if let Some(pids) = res.get("Pids").and_then(|x| x.as_array()) {
|
||||||
|
pids.iter()
|
||||||
|
.map(|x| {
|
||||||
|
x.as_unsigned_integer()
|
||||||
|
.ok_or(IdeviceError::UnexpectedResponse)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Err(IdeviceError::UnexpectedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a log archive and write it to the provided writer
|
||||||
|
pub async fn create_archive<W: tokio::io::AsyncWrite + Unpin>(
|
||||||
|
&mut self,
|
||||||
|
out: &mut W,
|
||||||
|
size_limit: Option<u64>,
|
||||||
|
age_limit: Option<u64>,
|
||||||
|
start_time: Option<u64>,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
let mut req = Dictionary::new();
|
||||||
|
req.insert("Request".into(), "CreateArchive".into());
|
||||||
|
|
||||||
|
if let Some(size) = size_limit {
|
||||||
|
req.insert("SizeLimit".into(), size.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(age) = age_limit {
|
||||||
|
req.insert("AgeLimit".into(), age.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(time) = start_time {
|
||||||
|
req.insert("StartTime".into(), time.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.idevice
|
||||||
|
.send_bplist(plist::Value::Dictionary(req))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Read a single byte
|
||||||
|
if self.idevice.read_raw(1).await?[0] != 1 {
|
||||||
|
return Err(IdeviceError::UnexpectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check status
|
||||||
|
let res = self.idevice.read_plist().await?;
|
||||||
|
match res.get("Status").and_then(|x| x.as_string()) {
|
||||||
|
Some("RequestSuccessful") => {}
|
||||||
|
_ => return Err(IdeviceError::UnexpectedResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read archive data
|
||||||
|
loop {
|
||||||
|
match self.idevice.read_raw(1).await {
|
||||||
|
Ok(data) if data[0] == 0x03 => {
|
||||||
|
let length_bytes = self.idevice.read_raw(4).await?;
|
||||||
|
let length = u32::from_le_bytes([
|
||||||
|
length_bytes[0],
|
||||||
|
length_bytes[1],
|
||||||
|
length_bytes[2],
|
||||||
|
length_bytes[3],
|
||||||
|
]);
|
||||||
|
let data = self.idevice.read_raw(length as usize).await?;
|
||||||
|
out.write_all(&data).await?;
|
||||||
|
}
|
||||||
|
Err(IdeviceError::Socket(_)) => break,
|
||||||
|
_ => return Err(IdeviceError::UnexpectedResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OsTraceRelayReceiver {
|
||||||
|
/// Get the next log from the relay
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A string containing the log
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// UnexpectedResponse if the service sends an EOF
|
||||||
|
pub async fn next(&mut self) -> Result<OsTraceLog, IdeviceError> {
|
||||||
|
// Read 0x02, at the beginning of each packet
|
||||||
|
if self.inner.idevice.read_raw(1).await?[0] != 0x02 {
|
||||||
|
return Err(IdeviceError::UnexpectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the len of the packet
|
||||||
|
let pl = self.inner.idevice.read_raw(4).await?;
|
||||||
|
let packet_length = u32::from_le_bytes([pl[0], pl[1], pl[2], pl[3]]);
|
||||||
|
|
||||||
|
let packet = self.inner.idevice.read_raw(packet_length as usize).await?;
|
||||||
|
|
||||||
|
// 9 bytes of padding
|
||||||
|
let packet = &packet[9..];
|
||||||
|
|
||||||
|
// Parse PID (4 bytes)
|
||||||
|
let pid = u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
||||||
|
let packet = &packet[4..];
|
||||||
|
|
||||||
|
// Skip 42 unknown bytes
|
||||||
|
let packet = &packet[42..];
|
||||||
|
|
||||||
|
// Parse timestamp (seconds + microseconds)
|
||||||
|
let seconds = u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
||||||
|
let packet = &packet[8..]; // skip 4 bytes padding after seconds
|
||||||
|
let microseconds = u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
||||||
|
let packet = &packet[4..];
|
||||||
|
|
||||||
|
// Skip 1 byte padding
|
||||||
|
let packet = &packet[1..];
|
||||||
|
|
||||||
|
// Parse log level
|
||||||
|
let log_level = packet[0];
|
||||||
|
let log_level: LogLevel = log_level.try_into()?;
|
||||||
|
let packet = &packet[1..];
|
||||||
|
|
||||||
|
// Skip 38 unknown bytes
|
||||||
|
let packet = &packet[38..];
|
||||||
|
|
||||||
|
// Parse string sizes
|
||||||
|
let image_name_size = u16::from_le_bytes([packet[0], packet[1]]) as usize;
|
||||||
|
let packet = &packet[2..];
|
||||||
|
let message_size = u16::from_le_bytes([packet[0], packet[1]]) as usize;
|
||||||
|
let packet = &packet[2..];
|
||||||
|
|
||||||
|
// Skip 6 bytes
|
||||||
|
let packet = &packet[6..];
|
||||||
|
|
||||||
|
// Parse subsystem and category sizes
|
||||||
|
let subsystem_size =
|
||||||
|
u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]) as usize;
|
||||||
|
let packet = &packet[4..];
|
||||||
|
let category_size =
|
||||||
|
u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]) as usize;
|
||||||
|
let packet = &packet[4..];
|
||||||
|
|
||||||
|
// Skip 4 bytes
|
||||||
|
let packet = &packet[4..];
|
||||||
|
|
||||||
|
// Parse filename (null-terminated string)
|
||||||
|
let filename_end = packet
|
||||||
|
.iter()
|
||||||
|
.position(|&b| b == 0)
|
||||||
|
.ok_or(IdeviceError::UnexpectedResponse)?;
|
||||||
|
let filename = String::from_utf8_lossy(&packet[..filename_end]).into_owned();
|
||||||
|
let packet = &packet[filename_end + 1..];
|
||||||
|
|
||||||
|
// Parse image name
|
||||||
|
let image_name_bytes = &packet[..image_name_size];
|
||||||
|
let image_name =
|
||||||
|
String::from_utf8_lossy(&image_name_bytes[..image_name_bytes.len() - 1]).into_owned();
|
||||||
|
let packet = &packet[image_name_size..];
|
||||||
|
|
||||||
|
// Parse message
|
||||||
|
let message_bytes = &packet[..message_size];
|
||||||
|
let message =
|
||||||
|
String::from_utf8_lossy(&message_bytes[..message_bytes.len() - 1]).into_owned();
|
||||||
|
let packet = &packet[message_size..];
|
||||||
|
|
||||||
|
// Parse label if subsystem and category exist
|
||||||
|
let label = if subsystem_size > 0 && category_size > 0 && !packet.is_empty() {
|
||||||
|
let subsystem_bytes = &packet[..subsystem_size];
|
||||||
|
let subsystem =
|
||||||
|
String::from_utf8_lossy(&subsystem_bytes[..subsystem_bytes.len() - 1]).into_owned();
|
||||||
|
let packet = &packet[subsystem_size..];
|
||||||
|
|
||||||
|
let category_bytes = &packet[..category_size];
|
||||||
|
let category =
|
||||||
|
String::from_utf8_lossy(&category_bytes[..category_bytes.len() - 1]).into_owned();
|
||||||
|
|
||||||
|
Some(SyslogLabel {
|
||||||
|
subsystem,
|
||||||
|
category,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let timestamp = match DateTime::from_timestamp(seconds as i64, microseconds) {
|
||||||
|
Some(t) => t.naive_local(),
|
||||||
|
None => return Err(IdeviceError::UnexpectedResponse),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(OsTraceLog {
|
||||||
|
pid,
|
||||||
|
timestamp,
|
||||||
|
level: log_level,
|
||||||
|
image_name,
|
||||||
|
filename,
|
||||||
|
message,
|
||||||
|
label,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for LogLevel {
|
||||||
|
type Error = IdeviceError;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, IdeviceError> {
|
||||||
|
Ok(match value {
|
||||||
|
0 => Self::Notice,
|
||||||
|
1 => Self::Info,
|
||||||
|
2 => Self::Debug,
|
||||||
|
0x10 => Self::Error,
|
||||||
|
0x11 => Self::Fault,
|
||||||
|
_ => return Err(IdeviceError::UnexpectedResponse),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,10 @@ path = "src/pair.rs"
|
|||||||
name = "syslog_relay"
|
name = "syslog_relay"
|
||||||
path = "src/syslog_relay.rs"
|
path = "src/syslog_relay.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "os_trace_relay"
|
||||||
|
path = "src/os_trace_relay.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idevice = { path = "../idevice", features = ["full"] }
|
idevice = { path = "../idevice", features = ["full"] }
|
||||||
tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] }
|
tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] }
|
||||||
|
|||||||
68
tools/src/os_trace_relay.rs
Normal file
68
tools/src/os_trace_relay.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use clap::{Arg, Command};
|
||||||
|
use idevice::{os_trace_relay::OsTraceRelayClient, IdeviceService};
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let matches = Command::new("os_trace_relay")
|
||||||
|
.about("Relay system logs")
|
||||||
|
.arg(
|
||||||
|
Arg::new("host")
|
||||||
|
.long("host")
|
||||||
|
.value_name("HOST")
|
||||||
|
.help("IP address of the device"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("pairing_file")
|
||||||
|
.long("pairing-file")
|
||||||
|
.value_name("PATH")
|
||||||
|
.help("Path to the pairing file"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("udid")
|
||||||
|
.value_name("UDID")
|
||||||
|
.help("UDID of the device (overrides host/pairing file)"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("about")
|
||||||
|
.long("about")
|
||||||
|
.help("Show about information")
|
||||||
|
.action(clap::ArgAction::SetTrue),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
if matches.get_flag("about") {
|
||||||
|
println!("Relay logs on the device");
|
||||||
|
println!("Copyright (c) 2025 Jackson Coxson");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let udid = matches.get_one::<String>("udid");
|
||||||
|
let host = matches.get_one::<String>("host");
|
||||||
|
let pairing_file = matches.get_one::<String>("pairing_file");
|
||||||
|
|
||||||
|
let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let log_client = OsTraceRelayClient::connect(&*provider)
|
||||||
|
.await
|
||||||
|
.expect("Unable to connect to misagent");
|
||||||
|
|
||||||
|
let mut relay = log_client.start_trace(None).await.expect("Start failed");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
println!(
|
||||||
|
"{:#?}",
|
||||||
|
relay.next().await.expect("Failed to read next log")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user