Implement hostapd terminate()

Bug: 355733880
Test: ninja
Test: build_tools.py --task runtest
Change-Id: I22b83fcd4b474870ed23da8ee6665d14436d7593
diff --git a/rust/hostapd-rs/src/hostapd.rs b/rust/hostapd-rs/src/hostapd.rs
index c4634d8..72a5e06 100644
--- a/rust/hostapd-rs/src/hostapd.rs
+++ b/rust/hostapd-rs/src/hostapd.rs
@@ -22,7 +22,7 @@
 use bytes::Bytes;
 use log::warn;
 use std::collections::HashMap;
-use std::ffi::{c_char, c_int, CString};
+use std::ffi::{c_char, c_int, CStr, CString};
 use std::fs::File;
 use std::io::{BufWriter, Write};
 #[cfg(unix)]
@@ -41,7 +41,9 @@
 use tokio::runtime::Runtime;
 use tokio::sync::Mutex;
 
-use crate::hostapd_sys::{run_hostapd_main, set_virtio_ctrl_sock, set_virtio_sock};
+use crate::hostapd_sys::{
+    run_hostapd_main, set_virtio_ctrl_sock, set_virtio_sock, VIRTIO_WIFI_CTRL_CMD_TERMINATE,
+};
 
 /// Alias for RawFd on Unix or RawSocket on Windows (converted to i32)
 type RawDescriptor = i32;
@@ -56,7 +58,7 @@
     config: HashMap<String, String>,
     config_path: PathBuf,
     data_writer: Option<Mutex<OwnedWriteHalf>>,
-    _ctrl_writer: Option<Mutex<OwnedWriteHalf>>,
+    ctrl_writer: Option<Mutex<OwnedWriteHalf>>,
     tx_bytes: mpsc::Sender<Bytes>,
 }
 
@@ -92,7 +94,7 @@
             config,
             config_path,
             data_writer: None,
-            _ctrl_writer: None,
+            ctrl_writer: None,
             tx_bytes,
         }
     }
@@ -114,7 +116,7 @@
         // Setup Sockets
         let (ctrl_listener, _ctrl_reader, ctrl_writer) =
             Self::create_pipe().expect("Failed to create ctrl pipe");
-        self._ctrl_writer = Some(Mutex::new(ctrl_writer));
+        self.ctrl_writer = Some(Mutex::new(ctrl_writer));
         let (data_listener, data_reader, data_writer) =
             Self::create_pipe().expect("Failed to create data pipe");
         self.data_writer = Some(Mutex::new(data_writer));
@@ -156,12 +158,7 @@
     pub fn input(&self, bytes: Bytes) -> anyhow::Result<()> {
         // Make sure hostapd is already running
         assert!(self.is_running(), "Failed to send input. Hostapd is not running.");
-        get_runtime().block_on(async {
-            let mut writer_guard = self.data_writer.as_ref().unwrap().lock().await;
-            writer_guard.write_all(&bytes).await?;
-            writer_guard.flush().await?;
-            Ok(())
-        })
+        get_runtime().block_on(Self::async_write(self.data_writer.as_ref().unwrap(), &bytes))
     }
 
     /// Check whether the hostapd thread is running
@@ -171,7 +168,18 @@
     }
 
     pub fn terminate(&self) {
-        todo!();
+        if !self.is_running() {
+            warn!("hostapd terminate() called when hostapd thread is not running");
+            return;
+        }
+
+        // Send terminate command to hostapd
+        if let Err(e) = get_runtime().block_on(Self::async_write(
+            self.ctrl_writer.as_ref().unwrap(),
+            c_string_to_bytes(VIRTIO_WIFI_CTRL_CMD_TERMINATE),
+        )) {
+            warn!("Failed to send VIRTIO_WIFI_CTRL_CMD_TERMINATE to hostapd to terminate: {:?}", e);
+        }
     }
 
     /// Generate hostapd.conf in discovery directory
@@ -210,6 +218,13 @@
         Ok((listener, stream))
     }
 
+    async fn async_write(writer: &Mutex<OwnedWriteHalf>, data: &[u8]) -> anyhow::Result<()> {
+        let mut writer_guard = writer.lock().await;
+        writer_guard.write_all(data).await?;
+        writer_guard.flush().await?;
+        Ok(())
+    }
+
     /// Run the C hostapd process with run_hostapd_main
     ///
     /// This function is meant to be spawn in a separate thread.
@@ -278,6 +293,12 @@
     }
 }
 
+impl Drop for Hostapd {
+    fn drop(&mut self) {
+        self.terminate();
+    }
+}
+
 /// Convert TcpStream to RawDescriptor (i32)
 fn into_raw_descriptor(stream: TcpStream) -> RawDescriptor {
     let std_stream = stream.into_std().expect("into_raw_descriptor's into_std() failed");
@@ -293,6 +314,11 @@
     std_stream.into_raw_socket().try_into().expect("Failed to convert Raw Socket value into i32")
 }
 
+/// Convert a null terminated c-string slice into &[u8] bytes without the nul terminator
+fn c_string_to_bytes(c_string: &[u8]) -> &[u8] {
+    CStr::from_bytes_with_nul(c_string).unwrap().to_bytes()
+}
+
 /// Get or init the hostapd tokio runtime
 /// TODO:
 /// * make Runtime the responsibility of the caller.
diff --git a/rust/hostapd-rs/tests/integration_test.rs b/rust/hostapd-rs/tests/integration_test.rs
index 9b60c24..fdf36d5 100644
--- a/rust/hostapd-rs/tests/integration_test.rs
+++ b/rust/hostapd-rs/tests/integration_test.rs
@@ -14,14 +14,30 @@
 
 use bytes::Bytes;
 use hostapd_rs::hostapd::Hostapd;
-use std::sync::mpsc;
+use std::{
+    env,
+    sync::mpsc,
+    thread,
+    time::{Duration, Instant},
+};
 
-/// Test whether run() starts the hostapd thread
+/// Test whether hostapd starts and terminates successfully
 #[test]
-fn test_hostapd_run() {
+fn test_hostapd_run_and_terminate() {
     let (tx, _rx): (mpsc::Sender<Bytes>, mpsc::Receiver<Bytes>) = mpsc::channel();
-    let config_path = std::env::temp_dir().join("hostapd.conf");
+    let config_path = env::temp_dir().join("hostapd.conf");
     let mut hostapd = Hostapd::new(tx, true, config_path);
     hostapd.run();
     assert!(hostapd.is_running());
+    hostapd.terminate();
+    // Check that hostapd terminates within 30s
+    let max_wait_time = Duration::from_secs(30);
+    let start_time = Instant::now();
+    while start_time.elapsed() < max_wait_time {
+        if !hostapd.is_running() {
+            break;
+        }
+        thread::sleep(Duration::from_millis(250));
+    }
+    assert!(!hostapd.is_running());
 }