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