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.
5 //! Here is also where we bake in the support to spawn the QEMU emulator as
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};
16 use std::time::Duration;
18 const REMOTE_ADDR_ENV: &str = "TEST_DEVICE_ADDR";
19 const DEFAULT_ADDR: &str = "127.0.0.1:12345";
25 Err(e) => panic!("{} failed with {}", stringify!($e), e),
31 let mut args = env::args().skip(1);
32 let next = args.next();
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()),
44 "push" => push(Path::new(&args.next().unwrap())),
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
52 "help" | "-h" | "--help" => help(),
54 println!("unknown command: {}", cmd);
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());
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);
68 let rootfs = rootfs.as_ref().expect("need rootfs on non-android");
69 start_qemu_emulator(target, rootfs, server, tmpdir);
72 // Wait for the emulator to come online
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() {
80 if client.read_exact(&mut b).is_ok() {
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());
94 println!("pushing server");
96 Command::new("adb").arg("push").arg(server).arg("/data/tmp/testd").status().unwrap();
97 assert!(status.success());
99 println!("forwarding tcp");
101 Command::new("adb").arg("forward").arg("tcp:12345").arg("tcp:12345").status().unwrap();
102 assert!(status.success());
104 println!("executing server");
105 Command::new("adb").arg("shell").arg("/data/tmp/testd").spawn().unwrap();
108 fn prepare_rootfs(target: &str, rootfs: &Path, server: &Path, rootfs_img: &Path) {
109 t!(fs::copy(server, rootfs.join("testd")));
112 "arm-unknown-linux-gnueabihf" | "aarch64-unknown-linux-gnu" => {
113 prepare_rootfs_cpio(rootfs, rootfs_img)
115 "riscv64gc-unknown-linux-gnu" => prepare_rootfs_ext4(rootfs, rootfs_img),
116 _ => panic!("{} is not supported", target),
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:
124 // find $rootfs -print 0 | cpio --null -o --format=newc > rootfs.img
125 let mut cmd = Command::new("cpio");
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());
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);
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()))
158 let mut dd_child = t!(dd.spawn());
159 assert!(t!(dd_child.wait()).success());
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());
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);
171 // Start up the emulator, in the background
173 "arm-unknown-linux-gnueabihf" => {
174 let mut cmd = Command::new("qemu-system-arm");
184 .arg("/tmp/vexpress-v2p-ca15-tc1.dtb")
186 .arg("console=ttyAMA0 root=/dev/ram rdinit=/sbin/init init=/sbin/init")
189 .arg("tcp:12345::12345");
192 "aarch64-unknown-linux-gnu" => {
193 let mut cmd = Command::new("qemu-system-aarch64");
205 .arg("console=ttyAMA0 root=/dev/ram rdinit=/sbin/init init=/sbin/init")
208 .arg("user,id=net0,hostfwd=tcp::12345-:12345")
210 .arg("virtio-net-device,netdev=net0,mac=00:00:00:00:00:00");
213 "riscv64gc-unknown-linux-gnu" => {
214 let mut cmd = Command::new("qemu-system-riscv64");
215 cmd.arg("-nographic")
225 .arg("quiet console=ttyS0 root=/dev/vda rw")
227 .arg("user,id=net0,hostfwd=tcp::12345-:12345")
229 .arg("virtio-net-device,netdev=net0,mac=00:00:00:00:00:00")
231 .arg("virtio-blk-device,drive=hd0")
233 .arg(&format!("file={},format=raw,id=hd0", &rootfs_img.to_string_lossy()));
236 _ => panic!("cannot start emulator for: {}", target),
240 fn push(path: &Path) {
241 let device_address = env::var(REMOTE_ADDR_ENV).unwrap_or(DEFAULT_ADDR.to_string());
242 let client = t!(TcpStream::connect(device_address));
243 let mut client = BufWriter::new(client);
244 t!(client.write_all(b"push"));
245 send(path, &mut client);
248 // Wait for an acknowledgement that all the data was received. No idea
249 // why this is necessary, seems like it shouldn't be!
250 let mut client = client.into_inner().unwrap();
251 let mut buf = [0; 4];
252 t!(client.read_exact(&mut buf));
253 assert_eq!(&buf, b"ack ");
254 println!("done pushing {:?}", path);
257 fn run(support_lib_count: usize, exe: String, all_args: Vec<String>) {
258 let device_address = env::var(REMOTE_ADDR_ENV).unwrap_or(DEFAULT_ADDR.to_string());
259 let client = t!(TcpStream::connect(device_address));
260 let mut client = BufWriter::new(client);
261 t!(client.write_all(b"run "));
263 let (support_libs, args) = all_args.split_at(support_lib_count);
265 // Send over the args
267 t!(client.write_all(arg.as_bytes()));
268 t!(client.write_all(&[0]));
270 t!(client.write_all(&[0]));
272 // Send over env vars
274 // Don't send over *everything* though as some env vars are set by and used
276 for (k, v) in env::vars() {
278 "PATH" | "LD_LIBRARY_PATH" | "PWD" | "RUST_TEST_TMPDIR" => continue,
281 t!(client.write_all(k.as_bytes()));
282 t!(client.write_all(&[0]));
283 t!(client.write_all(v.as_bytes()));
284 t!(client.write_all(&[0]));
286 t!(client.write_all(&[0]));
288 // Send over support libraries
289 for file in support_libs.iter().map(Path::new) {
290 send(&file, &mut client);
292 t!(client.write_all(&[0]));
294 // Send over the client executable as the last piece
295 send(exe.as_ref(), &mut client);
297 println!("uploaded {:?}, waiting for result", exe);
299 // Ok now it's time to read all the output. We're receiving "frames"
300 // representing stdout/stderr, so we decode all that here.
301 let mut header = [0; 5];
302 let mut stderr_done = false;
303 let mut stdout_done = false;
304 let mut client = t!(client.into_inner());
305 let mut stdout = io::stdout();
306 let mut stderr = io::stderr();
307 while !stdout_done || !stderr_done {
308 t!(client.read_exact(&mut header));
309 let amt = ((header[1] as u64) << 24)
310 | ((header[2] as u64) << 16)
311 | ((header[3] as u64) << 8)
312 | ((header[4] as u64) << 0);
317 t!(io::copy(&mut (&mut client).take(amt), &mut stdout));
324 t!(io::copy(&mut (&mut client).take(amt), &mut stderr));
330 // Finally, read out the exit status
331 let mut status = [0; 5];
332 t!(client.read_exact(&mut status));
333 let code = ((status[1] as i32) << 24)
334 | ((status[2] as i32) << 16)
335 | ((status[3] as i32) << 8)
336 | ((status[4] as i32) << 0);
338 std::process::exit(code);
340 println!("died due to signal {}", code);
341 std::process::exit(3);
345 fn send(path: &Path, dst: &mut dyn Write) {
346 t!(dst.write_all(path.file_name().unwrap().to_str().unwrap().as_bytes()));
347 t!(dst.write_all(&[0]));
348 let mut file = t!(File::open(&path));
349 let amt = t!(file.metadata()).len();
350 t!(dst.write_all(&[(amt >> 24) as u8, (amt >> 16) as u8, (amt >> 8) as u8, (amt >> 0) as u8,]));
351 t!(io::copy(&mut file, dst));
357 Usage: {0} <command> [<args>]
360 spawn-emulator <target> <server> <tmpdir> [rootfs] See below
361 push <path> Copy <path> to emulator
362 run <support_lib_count> <file> [support_libs...] [args...]
363 Run program on emulator
364 help Display help message
366 Spawning an emulator:
368 For Android <target>s, adb will push the <server>, set up TCP forwarding and run
369 the <server>. Otherwise qemu emulates the target using a rootfs image created in
370 <tmpdir> and generated from <rootfs> plus the <server> executable.
371 If {1} is set in the environment, this step is skipped.
373 Pushing a path to a running emulator:
375 A running emulator or adb device is connected to at the IP address and port in
376 the {1} environment variable or {2} if this isn't
377 specified. The file at <path> is sent to this target.
379 Executing commands on a running emulator:
381 First the target emulator/adb session is connected to as for pushing files. Next
382 the <file> and any specified support libs are pushed to the target. Finally, the
383 <file> is executed in the emulator, preserving the current environment.
384 That command's status code is returned.
386 env::args().next().unwrap(),