Skip to main content

waterui_cli/android/
adb.rs

1//! The `adb` client, with its server running.
2//!
3//! `adb`'s first client command launches the server daemon, and on Windows
4//! that daemon inherits every inheritable handle the client held — which is
5//! every one this process held, including the pipe whoever ran `water` is
6//! reading. The server outlives us, so that reader never sees end-of-file.
7//! An [`Adb`] therefore exists only once `adb start-server` has run through
8//! [`Host::run_detached`], where the launcher gets no handle of ours at all;
9//! every later client command finds the server already up and spawns
10//! nothing. Code that needs the server takes an `&Adb`, never a bare path.
11
12use std::path::{Path, PathBuf};
13
14use crate::{android::toolchain::AndroidSdk, toolchain::Host, utils::CommandError};
15
16/// Why no [`Adb`] could be produced.
17#[derive(Debug, thiserror::Error)]
18pub enum AdbError {
19    /// No Android SDK, or its platform-tools carry no `adb`.
20    #[error("Android SDK not found or adb not installed")]
21    NotFound,
22    /// `adb start-server` could not be run.
23    #[error("failed to start the adb server: {0}")]
24    ServerLauncher(#[from] CommandError),
25    /// `adb start-server` ran and reported failure.
26    #[error("`{adb} start-server` failed with status {status}", adb = .adb.display())]
27    ServerStart {
28        /// The client that was run.
29        adb: PathBuf,
30        /// Its exit status.
31        status: std::process::ExitStatus,
32    },
33}
34
35/// `adb` from the platform-tools on a host, its server already running.
36#[derive(Debug, Clone)]
37pub struct Adb {
38    path: PathBuf,
39}
40
41impl Adb {
42    /// Locate `adb` on `host` and make sure its server is up.
43    ///
44    /// # Errors
45    /// [`AdbError::NotFound`] when the SDK has no `adb`; the other variants
46    /// when the server could not be started.
47    pub async fn locate(host: &Host) -> Result<Self, AdbError> {
48        let path = AndroidSdk::adb_path(host).ok_or(AdbError::NotFound)?;
49        let status = host.run_detached(&path, ["start-server"]).await?;
50        if !status.success() {
51            return Err(AdbError::ServerStart { adb: path, status });
52        }
53        Ok(Self { path })
54    }
55
56    /// The client executable.
57    #[must_use]
58    pub fn path(&self) -> &Path {
59        &self.path
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use std::ffi::OsString;
66
67    use super::{Adb, AdbError};
68    use crate::toolchain::testing::TestMachine;
69
70    #[test]
71    fn without_platform_tools_there_is_no_adb() {
72        let machine = TestMachine::new();
73        let sdk = machine.install_android_sdk();
74        let host = machine.host([(
75            OsString::from("ANDROID_SDK_ROOT"),
76            sdk.as_os_str().to_os_string(),
77        )]);
78        let error = smol::block_on(Adb::locate(&host)).expect_err("no adb staged");
79        assert!(matches!(error, AdbError::NotFound), "{error:?}");
80    }
81
82    /// The staged `adb.exe` on Windows carries shell text that `CreateProcess`
83    /// cannot run, so the launcher tests are Unix-only.
84    #[test]
85    #[cfg(unix)]
86    fn locating_adb_starts_its_server_first() {
87        let machine = TestMachine::new();
88        let sdk = machine.install_android_sdk();
89        let staged = machine.install_adb();
90        let host = machine.host([(
91            OsString::from("ANDROID_SDK_ROOT"),
92            sdk.as_os_str().to_os_string(),
93        )]);
94        let adb = smol::block_on(Adb::locate(&host)).expect("the fake adb starts its server");
95        assert_eq!(adb.path(), staged);
96    }
97
98    #[test]
99    #[cfg(unix)]
100    fn a_failing_server_launch_is_an_error() {
101        let machine = TestMachine::new();
102        let sdk = machine.install_android_sdk();
103        machine.install_adb();
104        let host = machine.host([
105            (
106                OsString::from("ANDROID_SDK_ROOT"),
107                sdk.as_os_str().to_os_string(),
108            ),
109            (
110                OsString::from("WATERUI_FAKE_ADB_START_SERVER_STATUS"),
111                OsString::from("3"),
112            ),
113        ]);
114        let error = smol::block_on(Adb::locate(&host)).expect_err("the launcher exits 3");
115        match error {
116            AdbError::ServerStart { status, .. } => assert_eq!(status.code(), Some(3)),
117            other => panic!("expected ServerStart, got {other:?}"),
118        }
119    }
120}