use std::io::{IsTerminal, Write};
use std::time::Duration;
use restorekit::progress::Event;
use restorekit::{device, Device, DfuOutcome, DfuVia, Error, Result, Target};
use super::render;
pub(crate) fn emit_stage(json: bool, event: Event) {
if json {
render::emit_json(&event);
} else if let Event::DfuTriggerStage { stage } = event {
println!(" {stage}");
}
}
fn dfu_devices() -> Result<Vec<Device>> {
Ok(device::list()?
.into_iter()
.filter(|d| d.restorable())
.collect())
}
pub(crate) fn select_device(ecid: Option<u64>, json: bool) -> Result<Device> {
match ecid {
Some(e) => device::find(Target::Ecid(e)),
None => select_from(dfu_devices()?, json),
}
}
fn select_from(mut devices: Vec<Device>, json: bool) -> Result<Device> {
match devices.len() {
0 => Err(Error::NoDeviceFound),
1 => Ok(devices.pop().unwrap()),
n => {
if json || !std::io::stdin().is_terminal() {
return Err(Error::MultipleDevices(n));
}
println!("Found {n} Macs in DFU mode:\n");
for (i, d) in devices.iter().enumerate() {
println!(
" [{}] {} (ECID {})",
i + 1,
d.display_name(),
d.ecid_hex().unwrap_or_default()
);
}
loop {
print!("\nSelect a device [1-{n}]: ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if input.is_empty() {
return Err(Error::MultipleDevices(n));
}
match input.trim().parse::<usize>() {
Ok(i) if (1..=n).contains(&i) => return Ok(devices.swap_remove(i - 1)),
_ => println!(" Enter a number between 1 and {n}."),
}
}
}
}
}
fn via_from(dongle: Option<String>, ecid: Option<u64>, port: Option<i32>) -> DfuVia {
if let Some(id) = dongle {
DfuVia::Dongle(id)
} else if let Some(e) = ecid {
DfuVia::Ecid(e)
} else if let Some(p) = port {
DfuVia::Host(Some(p))
} else {
DfuVia::Auto
}
}
pub fn enter(
json: bool,
dongle: Option<String>,
ecid: Option<u64>,
port: Option<i32>,
) -> Result<()> {
let via = via_from(dongle, ecid, port);
match restorekit::trigger_dfu(via, Duration::from_secs(30), &mut |e| emit_stage(json, e)) {
Ok(DfuOutcome::Entered(device)) => {
if json {
emit_stage(true, Event::DeviceDetected { device });
} else {
println!("\nTarget is now in DFU mode: {}", device.display_name());
println!(" ECID: {}", device.ecid_hex().unwrap_or_default());
}
Ok(())
}
Ok(DfuOutcome::Sent) => {
if json {
emit_stage(true, Event::Done);
} else {
println!(
"DFU trigger sent via dongle. No DFU device appeared on this host — if the \
target's USB data isn't cabled here, confirm on the Mac's screen."
);
}
Ok(())
}
Err(Error::UnsupportedHost(_)) => {
if !json {
eprintln!("{}", restorekit::manual_dfu_instructions());
}
Err(Error::UnsupportedHost(
"cannot trigger DFU on this host".into(),
))
}
Err(e) => Err(e),
}
}
pub fn reboot(
json: bool,
dongle: Option<String>,
ecid: Option<u64>,
port: Option<i32>,
) -> Result<()> {
let via = via_from(dongle, ecid, port);
if !json {
println!("Rebooting the target...");
}
match restorekit::dfu::reboot(via, &mut |e| emit_stage(json, e)) {
Ok(()) => {
if json {
emit_stage(true, Event::Done);
} else {
println!("Done. The target should be booting normally.");
}
Ok(())
}
Err(Error::UnsupportedHost(_)) => {
if !json {
eprintln!("{}", restorekit::manual_dfu_instructions());
}
Err(Error::UnsupportedHost(
"cannot control the target from this host".into(),
))
}
Err(e) => Err(e),
}
}
pub fn serial(
json: bool,
dongle: Option<String>,
ecid: Option<u64>,
port: Option<i32>,
) -> Result<()> {
let enter = |quiet: bool| {
restorekit::dfu::serial(via_from(dongle.clone(), ecid, port), &mut |e| {
if !quiet {
emit_stage(json, e)
}
})
};
let console = match enter(false) {
Ok(c) => c,
Err(Error::UnsupportedHost(_)) => {
if !json {
eprintln!(
"Serial needs a dongle, or an Apple Silicon macOS host cabled to the target's \
DFU port with a SuperSpeed USB-C cable (USB-2/charge cables lack the SBU lines)."
);
}
return Err(Error::UnsupportedHost("cannot enter serial mode".into()));
}
Err(e) => return Err(e),
};
let (path, host_rearm) = match &console {
restorekit::SerialConsole::Host => ("/dev/cu.debug-console".to_string(), true),
restorekit::SerialConsole::Dongle(d) => {
let ttys = super::dongle::serial_ttys(d);
let ts = ttys.get(1).cloned().ok_or_else(|| {
Error::Dongle(format!(
"no target-serial tty for dongle {} — is its USB data on this host?",
d.serial
))
})?;
(ts, false)
}
};
if !json {
println!("\nStreaming {path} (115200 8N1). Ctrl-C to stop.");
println!(
"{}\n",
if host_rearm {
"Re-arms serial across target reboots — leave it running through the restore."
} else {
"The dongle keeps serial live across target reboots — leave it running."
}
);
}
let cpath = std::ffi::CString::new(path).unwrap();
let mut buf = [0u8; 4096];
let mut out = std::io::stdout();
loop {
let mut fd = -1;
for _ in 0..15 {
fd = unsafe { libc::open(cpath.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
if fd >= 0 {
break;
}
std::thread::sleep(Duration::from_millis(200));
}
if fd < 0 {
if host_rearm {
let _ = enter(true); }
continue;
}
unsafe {
let mut tio: libc::termios = std::mem::zeroed();
if libc::tcgetattr(fd, &mut tio) == 0 {
libc::cfmakeraw(&mut tio);
libc::cfsetspeed(&mut tio, libc::B115200);
libc::tcsetattr(fd, libc::TCSANOW, &tio);
}
}
loop {
let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
if n > 0 {
let _ = out.write_all(&buf[..n as usize]);
let _ = out.flush();
} else {
break; }
}
unsafe { libc::close(fd) };
if host_rearm {
let _ = enter(true); }
}
}
pub fn probe_ports(json: bool) -> Result<()> {
let probes = restorekit::dfu::probe_ports(&mut |e| {
if !json {
emit_stage(json, e)
}
})?;
if json {
for p in &probes {
println!(
"{}",
serde_json::json!({
"event": "port_probe",
"rid": p.rid,
"location": p.location,
"connected": p.connected,
"dfu_capable": matches!(&p.dbma, Ok(s) if s == "DBMa"),
"status": p.dbma.as_ref().ok(),
"error": p.dbma.as_ref().err(),
})
);
}
return Ok(());
}
println!("\nHost USB-C ports (DFU/VDM capability):\n");
for p in &probes {
let loc = p.location.as_deref().unwrap_or("?");
let conn = if p.connected {
"device attached"
} else {
"empty"
};
let verdict = match &p.dbma {
Ok(s) if s == "DBMa" => "DFU-capable".to_string(),
Ok(s) => format!("reached debug (status {s})"),
Err(e) => format!("not capable — {e}"),
};
println!(" RID {:<2} {:<12} {:<16} {verdict}", p.rid, loc, conn);
}
println!();
Ok(())
}
pub(crate) fn ensure_present(
json: bool,
timeout: Duration,
dongle: Option<String>,
ecid: Option<u64>,
) -> Result<Device> {
if let Some(id) = dongle {
return finish_trigger(json, DfuVia::Dongle(id), timeout, ecid);
}
if let Some(e) = ecid {
let mut devices = device::list()?;
device::identify(&mut devices);
match devices.into_iter().find(|d| d.ecid == Some(e)) {
Some(dev) if dev.in_dfu() => return Ok(dev),
Some(dev) if !json => {
println!(
"{} is in {} mode; putting it into DFU...",
dev.display_name(),
dev.mode
);
}
_ => {}
}
} else {
let present = dfu_devices()?;
if !present.is_empty() {
return select_from(present, json);
}
if !json {
if let Some(d) = device::list()?.iter().find(|d| {
matches!(
d.mode,
restorekit::UsbMode::Booted | restorekit::UsbMode::Recovery
)
}) {
println!("Detected {} ({} mode).", d.display_name(), d.mode);
}
}
}
let via = ecid.map(DfuVia::Ecid).unwrap_or(DfuVia::Auto);
finish_trigger(json, via, timeout, ecid)
}
fn finish_trigger(json: bool, via: DfuVia, timeout: Duration, ecid: Option<u64>) -> Result<Device> {
match restorekit::trigger_dfu(via, timeout, &mut |e| emit_stage(json, e)) {
Ok(DfuOutcome::Entered(dev)) => Ok(dev),
Ok(DfuOutcome::Sent) => Err(Error::Dongle(
"triggered via dongle, but the target's USB data isn't cabled to this host; \
connect its USB-C data here to restore"
.into(),
)),
Err(Error::UnsupportedHost(_)) => {
if !json {
eprintln!("{}\n", restorekit::manual_dfu_instructions());
println!("Waiting for the Mac to enter DFU mode...");
}
restorekit::dfu::wait_manual(ecid, timeout)
}
Err(e) => Err(e),
}
}