]> git.lizzy.rs Git - rust.git/blob - src/tools/remote-test-client/src/main.rs
Auto merge of #81762 - pietroalbini:fix-install-msys2, r=m-ou-se
[rust.git] / src / tools / remote-test-client / src / main.rs
1 //! This is a small client program intended to pair with `remote-test-server` in
2 //! this repository. This client connects to the server over TCP and is used to
3 //! push artifacts and run tests on the server instead of locally.
4 //!
5 //! Here is also where we bake in the support to spawn the QEMU emulator as
6 //! well.
7
8 use std::env;
9 use std::fs::{self, File};
10 use std::io::prelude::*;
11 use std::io::{self, BufWriter};
12 use std::net::TcpStream;
13 use std::path::{Path, PathBuf};
14 use std::process::{Command, Stdio};
15 use std::thread;
16 use std::time::Duration;
17
18 const REMOTE_ADDR_ENV: &str = "TEST_DEVICE_ADDR";
19 const DEFAULT_ADDR: &str = "127.0.0.1:12345";
20
21 macro_rules! t {
22     ($e:expr) => {
23         match $e {
24             Ok(e) => e,
25             Err(e) => panic!("{} failed with {}", stringify!($e), e),
26         }
27     };
28 }
29
30 fn main() {
31     let mut args = env::args().skip(1);
32     let next = args.next();
33     if next.is_none() {
34         return help();
35     }
36
37     match &next.unwrap()[..] {
38         "spawn-emulator" => spawn_emulator(
39             &args.next().unwrap(),
40             Path::new(&args.next().unwrap()),
41             Path::new(&args.next().unwrap()),
42             args.next().map(|s| s.into()),
43         ),
44         "push" => push(Path::new(&args.next().unwrap())),
45         "run" => run(
46             args.next().and_then(|count| count.parse().ok()).unwrap(),
47             // the last required parameter must remain the executable
48             // path so that the client works as a cargo runner
49             args.next().unwrap(),
50             args.collect(),
51         ),
52         "help" | "-h" | "--help" => help(),
53         cmd => {
54             println!("unknown command: {}", cmd);
55             help();
56         }
57     }
58 }
59
60 fn spawn_emulator(target: &str, server: &Path, tmpdir: &Path, rootfs: Option<PathBuf>) {
61     let device_address = env::var(REMOTE_ADDR_ENV).unwrap_or(DEFAULT_ADDR.to_string());
62
63     if env::var(REMOTE_ADDR_ENV).is_ok() {
64         println!("Connecting to remote device {} ...", device_address);
65     } else if target.contains("android") {
66         start_android_emulator(server);
67     } else {
68         let rootfs = rootfs.as_ref().expect("need rootfs on non-android");
69         start_qemu_emulator(target, rootfs, server, tmpdir);
70     }
71
72     // Wait for the emulator to come online
73     loop {
74         let dur = Duration::from_millis(100);
75         if let Ok(mut client) = TcpStream::connect(&device_address) {
76             t!(client.set_read_timeout(Some(dur)));
77             t!(client.set_write_timeout(Some(dur)));
78             if client.write_all(b"ping").is_ok() {
79                 let mut b = [0; 4];
80                 if client.read_exact(&mut b).is_ok() {
81                     break;
82                 }
83             }
84         }
85         thread::sleep(dur);
86     }
87 }
88
89 fn start_android_emulator(server: &Path) {
90     println!("waiting for device to come online");
91     let status = Command::new("adb").arg("wait-for-device").status().unwrap();
92     assert!(status.success());
93
94     println!("pushing server");
95     let status =
96         Command::new("adb").arg("push").arg(server).arg("/data/tmp/testd").status().unwrap();
97     assert!(status.success());
98
99     println!("forwarding tcp");
100     let status =
101         Command::new("adb").arg("forward").arg("tcp:12345").arg("tcp:12345").status().unwrap();
102     assert!(status.success());
103
104     println!("executing server");
105     Command::new("adb").arg("shell").arg("/data/tmp/testd").spawn().unwrap();
106 }
107
108 fn prepare_rootfs(target: &str, rootfs: &Path, server: &Path, rootfs_img: &Path) {
109     t!(fs::copy(server, rootfs.join("testd")));
110
111     match target {
112         "arm-unknown-linux-gnueabihf" | "aarch64-unknown-linux-gnu" => {
113             prepare_rootfs_cpio(rootfs, rootfs_img)
114         }
115         "riscv64gc-unknown-linux-gnu" => prepare_rootfs_ext4(rootfs, rootfs_img),
116         _ => panic!("{} is not supported", target),
117     }
118 }
119
120 fn prepare_rootfs_cpio(rootfs: &Path, rootfs_img: &Path) {
121     // Generate a new rootfs image now that we've updated the test server
122     // executable. This is the equivalent of:
123     //
124     //      find $rootfs -print 0 | cpio --null -o --format=newc > rootfs.img
125     let mut cmd = Command::new("cpio");
126     cmd.arg("--null")
127         .arg("-o")
128         .arg("--format=newc")
129         .stdin(Stdio::piped())
130         .stdout(Stdio::piped())
131         .current_dir(rootfs);
132     let mut child = t!(cmd.spawn());
133     let mut stdin = child.stdin.take().unwrap();
134     let rootfs = rootfs.to_path_buf();
135     thread::spawn(move || add_files(&mut stdin, &rootfs, &rootfs));
136     t!(io::copy(&mut child.stdout.take().unwrap(), &mut t!(File::create(&rootfs_img))));
137     assert!(t!(child.wait()).success());
138
139     fn add_files(w: &mut dyn Write, root: &Path, cur: &Path) {
140         for entry in t!(cur.read_dir()) {
141             let entry = t!(entry);
142             let path = entry.path();
143             let to_print = path.strip_prefix(root).unwrap();
144             t!(write!(w, "{}\u{0}", to_print.to_str().unwrap()));
145             if t!(entry.file_type()).is_dir() {
146                 add_files(w, root, &path);
147             }
148         }
149     }
150 }
151
152 fn prepare_rootfs_ext4(rootfs: &Path, rootfs_img: &Path) {
153     let mut dd = Command::new("dd");
154     dd.arg("if=/dev/zero")
155         .arg(&format!("of={}", rootfs_img.to_string_lossy()))
156         .arg("bs=1M")
157         .arg("count=1024");
158     let mut dd_child = t!(dd.spawn());
159     assert!(t!(dd_child.wait()).success());
160
161     let mut mkfs = Command::new("mkfs.ext4");
162     mkfs.arg("-d").arg(rootfs).arg(rootfs_img);
163     let mut mkfs_child = t!(mkfs.spawn());
164     assert!(t!(mkfs_child.wait()).success());
165 }
166
167 fn start_qemu_emulator(target: &str, rootfs: &Path, server: &Path, tmpdir: &Path) {
168     let rootfs_img = &tmpdir.join("rootfs.img");
169     prepare_rootfs(target, rootfs, server, rootfs_img);
170
171     // Start up the emulator, in the background
172     match target {
173         "arm-unknown-linux-gnueabihf" => {
174             let mut cmd = Command::new("qemu-system-arm");
175             cmd.arg("-M")
176                 .arg("vexpress-a15")
177                 .arg("-m")
178                 .arg("1024")
179                 .arg("-kernel")
180                 .arg("/tmp/zImage")
181                 .arg("-initrd")
182                 .arg(&rootfs_img)
183                 .arg("-dtb")
184                 .arg("/tmp/vexpress-v2p-ca15-tc1.dtb")
185                 .arg("-append")
186                 .arg("console=ttyAMA0 root=/dev/ram rdinit=/sbin/init init=/sbin/init")
187                 .arg("-nographic")
188                 .arg("-netdev")
189                 .arg("user,id=net0,hostfwd=tcp::12345-:12345")
190                 .arg("-device")
191                 .arg("virtio-net-device,netdev=net0,mac=00:00:00:00:00:00");
192             t!(cmd.spawn());
193         }
194         "aarch64-unknown-linux-gnu" => {
195             let mut cmd = Command::new("qemu-system-aarch64");
196             cmd.arg("-machine")
197                 .arg("virt")
198                 .arg("-cpu")
199                 .arg("cortex-a57")
200                 .arg("-m")
201                 .arg("1024")
202                 .arg("-kernel")
203                 .arg("/tmp/Image")
204                 .arg("-initrd")
205                 .arg(&rootfs_img)
206                 .arg("-append")
207                 .arg("console=ttyAMA0 root=/dev/ram rdinit=/sbin/init init=/sbin/init")
208                 .arg("-nographic")
209                 .arg("-netdev")
210                 .arg("user,id=net0,hostfwd=tcp::12345-:12345")
211                 .arg("-device")
212                 .arg("virtio-net-device,netdev=net0,mac=00:00:00:00:00:00");
213             t!(cmd.spawn());
214         }
215         "riscv64gc-unknown-linux-gnu" => {
216             let mut cmd = Command::new("qemu-system-riscv64");
217             cmd.arg("-nographic")
218                 .arg("-machine")
219                 .arg("virt")
220                 .arg("-m")
221                 .arg("1024")
222                 .arg("-bios")
223                 .arg("none")
224                 .arg("-kernel")
225                 .arg("/tmp/bbl")
226                 .arg("-append")
227                 .arg("quiet console=ttyS0 root=/dev/vda rw")
228                 .arg("-netdev")
229                 .arg("user,id=net0,hostfwd=tcp::12345-:12345")
230                 .arg("-device")
231                 .arg("virtio-net-device,netdev=net0,mac=00:00:00:00:00:00")
232                 .arg("-device")
233                 .arg("virtio-blk-device,drive=hd0")
234                 .arg("-drive")
235                 .arg(&format!("file={},format=raw,id=hd0", &rootfs_img.to_string_lossy()));
236             t!(cmd.spawn());
237         }
238         _ => panic!("cannot start emulator for: {}", target),
239     }
240 }
241
242 fn push(path: &Path) {
243     let device_address = env::var(REMOTE_ADDR_ENV).unwrap_or(DEFAULT_ADDR.to_string());
244     let client = t!(TcpStream::connect(device_address));
245     let mut client = BufWriter::new(client);
246     t!(client.write_all(b"push"));
247     send(path, &mut client);
248     t!(client.flush());
249
250     // Wait for an acknowledgement that all the data was received. No idea
251     // why this is necessary, seems like it shouldn't be!
252     let mut client = client.into_inner().unwrap();
253     let mut buf = [0; 4];
254     t!(client.read_exact(&mut buf));
255     assert_eq!(&buf, b"ack ");
256     println!("done pushing {:?}", path);
257 }
258
259 fn run(support_lib_count: usize, exe: String, all_args: Vec<String>) {
260     let device_address = env::var(REMOTE_ADDR_ENV).unwrap_or(DEFAULT_ADDR.to_string());
261     let client = t!(TcpStream::connect(device_address));
262     let mut client = BufWriter::new(client);
263     t!(client.write_all(b"run "));
264
265     let (support_libs, args) = all_args.split_at(support_lib_count);
266
267     // Send over the args
268     for arg in args {
269         t!(client.write_all(arg.as_bytes()));
270         t!(client.write_all(&[0]));
271     }
272     t!(client.write_all(&[0]));
273
274     // Send over env vars
275     //
276     // Don't send over *everything* though as some env vars are set by and used
277     // by the client.
278     for (k, v) in env::vars() {
279         match &k[..] {
280             "PATH" | "LD_LIBRARY_PATH" | "PWD" | "RUST_TEST_TMPDIR" => continue,
281             _ => {}
282         }
283         t!(client.write_all(k.as_bytes()));
284         t!(client.write_all(&[0]));
285         t!(client.write_all(v.as_bytes()));
286         t!(client.write_all(&[0]));
287     }
288     t!(client.write_all(&[0]));
289
290     // Send over support libraries
291     for file in support_libs.iter().map(Path::new) {
292         send(&file, &mut client);
293     }
294     t!(client.write_all(&[0]));
295
296     // Send over the client executable as the last piece
297     send(exe.as_ref(), &mut client);
298
299     println!("uploaded {:?}, waiting for result", exe);
300
301     // Ok now it's time to read all the output. We're receiving "frames"
302     // representing stdout/stderr, so we decode all that here.
303     let mut header = [0; 5];
304     let mut stderr_done = false;
305     let mut stdout_done = false;
306     let mut client = t!(client.into_inner());
307     let mut stdout = io::stdout();
308     let mut stderr = io::stderr();
309     while !stdout_done || !stderr_done {
310         t!(client.read_exact(&mut header));
311         let amt = ((header[1] as u64) << 24)
312             | ((header[2] as u64) << 16)
313             | ((header[3] as u64) << 8)
314             | ((header[4] as u64) << 0);
315         if header[0] == 0 {
316             if amt == 0 {
317                 stdout_done = true;
318             } else {
319                 t!(io::copy(&mut (&mut client).take(amt), &mut stdout));
320                 t!(stdout.flush());
321             }
322         } else {
323             if amt == 0 {
324                 stderr_done = true;
325             } else {
326                 t!(io::copy(&mut (&mut client).take(amt), &mut stderr));
327                 t!(stderr.flush());
328             }
329         }
330     }
331
332     // Finally, read out the exit status
333     let mut status = [0; 5];
334     t!(client.read_exact(&mut status));
335     let code = ((status[1] as i32) << 24)
336         | ((status[2] as i32) << 16)
337         | ((status[3] as i32) << 8)
338         | ((status[4] as i32) << 0);
339     if status[0] == 0 {
340         std::process::exit(code);
341     } else {
342         println!("died due to signal {}", code);
343         std::process::exit(3);
344     }
345 }
346
347 fn send(path: &Path, dst: &mut dyn Write) {
348     t!(dst.write_all(path.file_name().unwrap().to_str().unwrap().as_bytes()));
349     t!(dst.write_all(&[0]));
350     let mut file = t!(File::open(&path));
351     let amt = t!(file.metadata()).len();
352     t!(dst.write_all(&[(amt >> 24) as u8, (amt >> 16) as u8, (amt >> 8) as u8, (amt >> 0) as u8,]));
353     t!(io::copy(&mut file, dst));
354 }
355
356 fn help() {
357     println!(
358         "
359 Usage: {0} <command> [<args>]
360
361 Sub-commands:
362     spawn-emulator <target> <server> <tmpdir> [rootfs]   See below
363     push <path>                                          Copy <path> to emulator
364     run <support_lib_count> <file> [support_libs...] [args...]
365                                                          Run program on emulator
366     help                                                 Display help message
367
368 Spawning an emulator:
369
370 For Android <target>s, adb will push the <server>, set up TCP forwarding and run
371 the <server>. Otherwise qemu emulates the target using a rootfs image created in
372 <tmpdir> and generated from <rootfs> plus the <server> executable.
373 If {1} is set in the environment, this step is skipped.
374
375 Pushing a path to a running emulator:
376
377 A running emulator or adb device is connected to at the IP address and port in
378 the {1} environment variable or {2} if this isn't
379 specified. The file at <path> is sent to this target.
380
381 Executing commands on a running emulator:
382
383 First the target emulator/adb session is connected to as for pushing files. Next
384 the <file> and any specified support libs are pushed to the target. Finally, the
385 <file> is executed in the emulator, preserving the current environment.
386 That command's status code is returned.
387 ",
388         env::args().next().unwrap(),
389         REMOTE_ADDR_ENV,
390         DEFAULT_ADDR
391     );
392 }