blob: 9449cf586e37fe42244a1c9f26b8e99a9e6a7e53 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Implementation of the `ffx uart disconnect` subtool.
//!
//! Handles terminating driver processes and cleaning up socket and metadata files.
use crate::args::DisconnectCommand;
use crate::metadata::ConnectionMetadata;
use crate::sys::{get_peer_pid, is_driver_running};
use async_trait::async_trait;
use ffx_config::EnvironmentContext;
use ffx_writer::{SimpleWriter, ToolIO};
use fho::{FfxMain, FfxTool, Result, user_error};
use std::path::Path;
fn remove_file_if_exists(path: &Path, label: &str) {
if let Err(e) = std::fs::remove_file(path) {
if e.kind() != std::io::ErrorKind::NotFound {
log::warn!("Failed to remove {label} file at {}: {:?}", path.display(), e);
}
}
}
#[derive(Debug, FfxTool)]
#[target(None)]
pub struct DisconnectTool {
#[command]
pub(crate) cmd: DisconnectCommand,
pub(crate) context: EnvironmentContext,
}
impl DisconnectTool {
pub(crate) async fn disconnect(&self, target: &str, writer: &mut SimpleWriter) -> Result<()> {
let resolved = crate::resolve_target(&self.context, target).await?;
match resolved {
crate::ResolvedTarget::Active { meta_path, meta } => {
self.terminate_and_clean_target(&meta_path, &meta).await?;
writer.line(format!("Disconnect called for {}", meta.target))?;
Ok(())
}
crate::ResolvedTarget::Inactive { target_path } => {
Err(user_error!("Not connected to target {target_path}"))
}
}
}
pub(crate) async fn terminate_and_clean_target(
&self,
meta_path: &Path,
meta: &ConnectionMetadata,
) -> Result<()> {
let socket_path = uart_driver_api::get_client_socket_path(meta_path);
// `is_driver_running` verifies both process liveness and that the command line / binary
// matches `ffx-uart-driver` via /proc, preventing accidental termination if the PID was reused.
if is_driver_running(meta.pid) {
if let Err(e) = crate::driver::terminate_process(meta.pid).await {
log::warn!("Failed to terminate driver process {}: {:?}", meta.pid, e);
}
} else if socket_path.exists() {
if let Ok(Ok(stream)) = tokio::time::timeout(
crate::driver::SOCKET_PROBE_TIMEOUT,
tokio::net::UnixStream::connect(&socket_path),
)
.await
{
if let Ok(pid) = get_peer_pid(&stream) {
if is_driver_running(pid) {
if let Err(e) = crate::driver::terminate_process(pid).await {
log::warn!("Failed to terminate peer driver process {pid}: {e:?}");
}
}
}
}
}
let control_socket_path = uart_driver_api::get_control_socket_path(&socket_path);
remove_file_if_exists(meta_path, "metadata");
remove_file_if_exists(&socket_path, "socket");
remove_file_if_exists(&control_socket_path, "control socket");
Ok(())
}
}
#[async_trait(?Send)]
impl FfxMain for DisconnectTool {
type Writer = SimpleWriter;
type Error = fho::Error;
async fn main(self, mut writer: Self::Writer) -> Result<()> {
let _ = self.cmd;
let spec = crate::get_spec(&self.context, false, &mut writer).await?;
self.disconnect(&spec, &mut writer).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::{
ConnectionStatus, UartProtocol, find_active_connection, get_socket_path, write_metadata,
};
use std::num::NonZeroU32;
use tempfile::tempdir;
fn create_test_tool(temp_dir: &Path) -> DisconnectTool {
let env = ffx_config::test_env()
.runtime_config("shared_data", temp_dir.to_str().unwrap())
.build()
.expect("test env");
DisconnectTool { cmd: DisconnectCommand {}, context: env.context }
}
#[fuchsia::test]
async fn test_disconnect_not_connected() {
let temp = tempdir().unwrap();
let tool = create_test_tool(temp.path());
let buffers = ffx_writer::TestBuffers::default();
let mut writer = SimpleWriter::new_test(&buffers);
let res = tool.disconnect("/dev/ttyUSB99", &mut writer).await;
assert!(res.is_err());
let err_msg = format!("{}", res.unwrap_err());
assert!(err_msg.contains("Not connected to target /dev/ttyUSB99"));
}
#[fuchsia::test]
async fn test_disconnect_success() {
let temp = tempdir().unwrap();
let tool = create_test_tool(temp.path());
let buffers = ffx_writer::TestBuffers::default();
let mut writer = SimpleWriter::new_test(&buffers);
let target = "/dev/ttyUSB0";
let socket_path = get_socket_path(&tool.context, target).unwrap();
write_metadata(&tool.context, &socket_path, target, 999999, NonZeroU32::new(115200))
.unwrap();
let meta_path = socket_path.with_extension(uart_driver_api::METADATA_FILE_EXTENSION);
assert!(meta_path.exists());
let control_path = socket_path.with_extension(uart_driver_api::CONTROL_SOCKET_EXTENSION);
std::fs::write(&control_path, b"").unwrap();
assert!(control_path.exists());
let res = tool.disconnect(target, &mut writer).await;
assert!(res.is_ok());
assert!(!meta_path.exists());
assert!(!control_path.exists());
assert_eq!(buffers.into_stdout_str(), "Disconnect called for /dev/ttyUSB0\n");
}
#[fuchsia::test]
async fn test_disconnect_find_active_connection() {
let temp = tempdir().unwrap();
let tool = create_test_tool(temp.path());
let shared_path = temp.path().join("ffx_uart");
std::fs::create_dir_all(&shared_path).unwrap();
let target_id = "0123456789abcdef";
let meta = ConnectionMetadata {
pid: 1234,
target: "/dev/ttyUSB2".to_string(),
status: ConnectionStatus::Connected,
id: Some(target_id.to_string()),
baud: NonZeroU32::new(115200),
protocol: UartProtocol::ResendSP,
log_level: None,
nodename: Some("fuchsia-node-disc".to_string()),
serial: None,
};
let file_path = shared_path.join(format!("ffx_uart_{target_id}.json"));
std::fs::write(&file_path, serde_json::to_string(&meta).unwrap()).unwrap();
let found = find_active_connection(&tool.context, target_id).unwrap();
assert!(found.is_some());
let (path, found_meta) = found.unwrap();
assert_eq!(path, file_path);
assert_eq!(found_meta.pid, 1234);
let found_by_nodename = find_active_connection(&tool.context, "fuchsia-node-disc").unwrap();
assert!(found_by_nodename.is_some());
assert_eq!(found_by_nodename.unwrap().1.pid, 1234);
}
#[fuchsia::test]
async fn test_disconnect_main_with_target_id() {
let temp = tempdir().unwrap();
let target_id = "0123456789abcdef";
let mut env = ffx_config::test_env()
.runtime_config("shared_data", temp.path().to_str().unwrap())
.build()
.expect("test env");
let shared_path = temp.path().join("ffx_uart");
std::fs::create_dir_all(&shared_path).unwrap();
let meta = ConnectionMetadata {
pid: 999999,
target: "/dev/ttyUSB0".to_string(),
status: ConnectionStatus::Connected,
id: Some(target_id.to_string()),
baud: NonZeroU32::new(115200),
protocol: UartProtocol::ResendSP,
log_level: None,
nodename: None,
serial: None,
};
let file_path = shared_path.join(format!("ffx_uart_{target_id}.json"));
std::fs::write(&file_path, serde_json::to_string(&meta).unwrap()).unwrap();
assert!(file_path.exists());
env.context.override_target_specifier(&Some(format!("uart:{target_id}")));
let tool = DisconnectTool { cmd: DisconnectCommand {}, context: env.context };
let buffers = ffx_writer::TestBuffers::default();
let writer = SimpleWriter::new_test(&buffers);
let res = tool.main(writer).await;
assert!(res.is_ok());
assert!(!file_path.exists());
}
}