1 //! Generate and execute adb commands from the host.
2 use crate::fingerprint::*;
3 use crate::restart_chooser::{RestartChooser, RestartType};
4 
5 use std::collections::HashMap;
6 use std::fmt::Debug;
7 use std::path::{Path, PathBuf};
8 use tracing::debug;
9 
10 #[derive(Clone, Debug, PartialEq)]
11 pub enum AdbAction {
12     /// e.g. adb shell mkdir <device_filename>
13     Mkdir,
14     /// e.g. adb push <host_filename> <device_filename>
15     Push { host_path: String },
16     /// e.g. adb shell ln -s <target> <device_filename>
17     Symlink { target: String },
18     /// e.g. adb rm <device_filename>
19     DeleteFile,
20     /// e.g. adb rm -rf <device_filename>
21     DeleteDir,
22 }
23 
split_string(s: &str) -> Vec<String>24 pub fn split_string(s: &str) -> Vec<String> {
25     Vec::from_iter(s.split(' ').map(String::from))
26 }
27 
28 /// Given an `action` like AdbAction::Push, return a vector of
29 /// command line args to pass to `adb`.  `adb` will not be in
30 /// vector of args.
31 /// `file_path` is the device-relative file_path.
32 /// It is expected that the arguments returned arguments will not be
33 /// evaluated by a shell, but instead passed directly on to `exec`.
34 /// i.e. If a filename has spaces in it, there should be no quotes.
command_args(action: &AdbAction, file_path: &Path) -> Vec<String>35 pub fn command_args(action: &AdbAction, file_path: &Path) -> Vec<String> {
36     let path_str = file_path.to_path_buf().into_os_string().into_string().expect("already tested");
37     let add_cmd_and_path = |s| {
38         let mut args = split_string(s);
39         args.push(path_str.clone());
40         args
41     };
42     let add_cmd_and_paths = |s, path: &String| {
43         let mut args = split_string(s);
44         args.push(path.clone());
45         args.push(path_str.clone());
46         args
47     };
48     match action {
49         // [adb] mkdir device_path
50         AdbAction::Mkdir => add_cmd_and_path("shell mkdir -p"),
51         // TODO(rbraunstein): Add compression flag.
52         // [adb] push host_path device_path
53         AdbAction::Push { host_path } => add_cmd_and_paths("push", host_path),
54         // [adb] ln -s -f target device_path
55         AdbAction::Symlink { target } => add_cmd_and_paths("shell ln -sf", target),
56         // [adb] shell rm device_path
57         AdbAction::DeleteFile => add_cmd_and_path("shell rm"),
58         // [adb] shell rm -rf device_path
59         AdbAction::DeleteDir => add_cmd_and_path("shell rm -rf"),
60     }
61 }
62 
63 /// Return type for `compute_actions` so we can segregate out deletes.
64 pub struct Commands {
65     pub upserts: HashMap<PathBuf, AdbCommand>,
66     pub deletes: HashMap<PathBuf, AdbCommand>,
67 }
68 
69 impl Commands {
is_empty(&self) -> bool70     pub fn is_empty(&self) -> bool {
71         self.upserts.is_empty() && self.deletes.is_empty()
72     }
73 }
74 
75 /// Compose an adb command, i.e. an argv array, for each action listed in the diffs.
76 /// e.g. `adb push host_file device_file` or `adb mkdir /path/to/device_dir`
compose(diffs: &Diffs, product_out: &Path) -> Commands77 pub fn compose(diffs: &Diffs, product_out: &Path) -> Commands {
78     // Note we don't need to make intermediate dirs, adb push
79     // will do that for us.
80 
81     let mut commands = Commands { upserts: HashMap::new(), deletes: HashMap::new() };
82 
83     // We use the same command for updating a file on the device as we do for adding it
84     // if it doesn't exist.
85     for (file_path, metadata) in diffs.device_needs.iter().chain(diffs.device_diffs.iter()) {
86         let host_path =
87             product_out.join(file_path).into_os_string().into_string().expect("already visited");
88         let adb_cmd = |action| AdbCommand::from_action(action, file_path);
89         commands.upserts.insert(
90             file_path.to_path_buf(),
91             match metadata.file_type {
92                 FileType::File => adb_cmd(AdbAction::Push { host_path }),
93                 FileType::Symlink => {
94                     adb_cmd(AdbAction::Symlink { target: metadata.symlink.clone() })
95                 }
96                 FileType::Directory => adb_cmd(AdbAction::Mkdir),
97             },
98         );
99     }
100 
101     for (file_path, metadata) in diffs.device_extra.iter() {
102         let adb_cmd = |action| AdbCommand::from_action(action, file_path);
103         commands.deletes.insert(
104             file_path.to_path_buf(),
105             match metadata.file_type {
106                 // [adb] rm device_path
107                 FileType::File | FileType::Symlink => adb_cmd(AdbAction::DeleteFile),
108                 // TODO(rbraunstein): More efficient deletes, or change rm -rf back to rmdir
109                 FileType::Directory => adb_cmd(AdbAction::DeleteDir),
110             },
111         );
112     }
113 
114     commands
115 }
116 
117 /// Given a set of files, determine the combined set of commands we need
118 /// to execute on the device to make the device aware of the new files.
119 /// In the most conservative case we will return a single "reboot" command.
120 /// Short of that, there should be zero or one commands per installed file.
121 /// If multiple installed files are part of the same module, we will reduce
122 /// to one command for those files.  If multiple services are sync'd, there
123 /// may be multiple restart commands.
restart_type( build_system: &RestartChooser, installed_file_paths: &Vec<String>, ) -> RestartType124 pub fn restart_type(
125     build_system: &RestartChooser,
126     installed_file_paths: &Vec<String>,
127 ) -> RestartType {
128     let mut soft_restart_needed = false;
129     let mut reboot_needed = false;
130 
131     for installed_file in installed_file_paths {
132         let restart_type = build_system.restart_type(installed_file);
133         debug!(" -- Restart is {:?} for {}", restart_type.clone(), installed_file);
134         match restart_type {
135             RestartType::Reboot => reboot_needed = true,
136             RestartType::SoftRestart => soft_restart_needed = true,
137             RestartType::None => (),
138             // TODO(rbraunstein): Deal with determining the command needed. Full reboot for now.
139             //RestartType::RestartBinary => (),
140         }
141     }
142     // Note, we don't do early return so we log restart_type for each file.
143     if reboot_needed {
144         return RestartType::Reboot;
145     }
146     if soft_restart_needed {
147         return RestartType::SoftRestart;
148     }
149     RestartType::None
150 }
151 
152 #[derive(Clone, Debug, PartialEq)]
153 pub struct AdbCommand {
154     /// Args to pass to adb to do the action.
155     args: Vec<String>,
156     /// Action we are going to perform, like Push, create symlink, delete file.
157     pub action: AdbAction,
158     /// Device path for file we are operating on.
159     pub file: PathBuf,
160 }
161 
162 impl AdbCommand {
163     /// Pass the command line with spaces between args.
from_action(adb_action: AdbAction, device_path: &Path) -> Self164     pub fn from_action(adb_action: AdbAction, device_path: &Path) -> Self {
165         AdbCommand {
166             args: command_args(&adb_action, device_path),
167             action: adb_action,
168             file: device_path.to_path_buf(),
169         }
170     }
args(&self) -> Vec<String>171     pub fn args(&self) -> Vec<String> {
172         self.args.clone()
173     }
174 
is_mkdir(&self) -> bool175     pub fn is_mkdir(&self) -> bool {
176         matches!(self.action, AdbAction::Mkdir { .. })
177     }
178 
is_rm(&self) -> bool179     pub fn is_rm(&self) -> bool {
180         matches!(self.action, AdbAction::DeleteDir { .. })
181             || matches!(self.action, AdbAction::DeleteFile { .. })
182     }
183 
device_path(&self) -> &Path184     pub fn device_path(&self) -> &Path {
185         &self.file
186     }
187 }
188 
189 #[cfg(test)]
190 mod tests {
191     use super::*;
192 
193     #[test]
push_cmd_args()194     fn push_cmd_args() {
195         assert_eq!(
196             string_vec(&["push", "local/host/path", "device/path",]),
197             command_args(
198                 &AdbAction::Push { host_path: "local/host/path".to_string() },
199                 &PathBuf::from("device/path")
200             )
201         );
202     }
203 
204     #[test]
mkdir_cmd_args()205     fn mkdir_cmd_args() {
206         assert_eq!(
207             string_vec(&["shell", "mkdir", "-p", "device/new/dir",]),
208             command_args(&AdbAction::Mkdir, &PathBuf::from("device/new/dir"))
209         );
210     }
211 
212     #[test]
symlink_cmd_args()213     fn symlink_cmd_args() {
214         assert_eq!(
215             string_vec(&["shell", "ln", "-sf", "the_target", "system/tmp/p",]),
216             command_args(
217                 &AdbAction::Symlink { target: "the_target".to_string() },
218                 &PathBuf::from("system/tmp/p")
219             )
220         );
221     }
222     #[test]
delete_file_cmd_args()223     fn delete_file_cmd_args() {
224         assert_eq!(
225             string_vec(&["shell", "rm", "system/file.so",]),
226             command_args(&AdbAction::DeleteFile, &PathBuf::from("system/file.so"))
227         );
228     }
229     #[test]
delete_dir_cmd_args()230     fn delete_dir_cmd_args() {
231         assert_eq!(
232             string_vec(&["shell", "rm", "-rf", "some/dir"]),
233             command_args(&AdbAction::DeleteDir, &PathBuf::from("some/dir"))
234         );
235     }
236 
237     #[test]
cmds_on_files_spaces_utf8_chars_work()238     fn cmds_on_files_spaces_utf8_chars_work() {
239         // File with spaces in the name
240         assert_eq!(
241             string_vec(&["push", "local/host/path with space", "device/path with space",]),
242             command_args(
243                 &AdbAction::Push { host_path: "local/host/path with space".to_string() },
244                 &PathBuf::from("device/path with space")
245             )
246         );
247         // Symlink with spaces and utf8 chars
248         assert_eq!(
249             string_vec(&["shell", "ln", "-sf", "cup of water", "/tmp/ha ha/물 주세요",]),
250             command_args(
251                 &AdbAction::Symlink { target: "cup of water".to_string() },
252                 &PathBuf::from("/tmp/ha ha/물 주세요")
253             )
254         );
255     }
256 
257     // helper to gofrom vec of str -> vec of String
string_vec(v: &[&str]) -> Vec<String>258     fn string_vec(v: &[&str]) -> Vec<String> {
259         v.iter().map(|&x| x.into()).collect()
260     }
261 }
262