blob: 590c735596ed980583ea479d6e872ba0cb9cfd8f [file] [log] [blame]
//! This is a small client program intended to pair with `remote-test-server` in
//! this repository. This client connects to the server over TCP and is used to
//! push artifacts and run tests on the server instead of locally.
//!
//! Here is also where we bake in the support to spawn the QEMU emulator as
//! well.
use std::env;
use std::fs::{self, File};
use std::io::prelude::*;
use std::io::{self, BufWriter};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
const REMOTE_ADDR_ENV: &str = "TEST_DEVICE_ADDR";
const DEFAULT_ADDR: &str = "127.0.0.1:12345";
macro_rules! t {
($e:expr) => {
match $e {
Ok(e) => e,
Err(e) => panic!("{} failed with {}", stringify!($e), e),
}
};
}
fn main() {
let mut args = env::args().skip(1);
let next = args.next();
if next.is_none() {
return help();
}
match &next.unwrap()[..] {
"spawn-emulator" => spawn_emulator(
&args.next().unwrap(),
Path::new(&args.next().unwrap()),
Path::new(&args.next().unwrap()),
args.next().map(|s| s.into()),
),
"push" => push(Path::new(&args.next().unwrap())),
"run" => run(
args.next().and_then(|count| count.parse().ok()).unwrap(),
// the last required parameter must remain the executable
// path so that the client works as a cargo runner
args.next().unwrap(),
args.collect(),
),
"help" | "-h" | "--help" => help(),
cmd => {
println!("unknown command: {}", cmd);
help();
}
}
}
fn spawn_emulator(target: &str, server: &Path, tmpdir: &Path, rootfs: Option<PathBuf>) {
let device_address = env::var(REMOTE_ADDR_ENV).unwrap_or(DEFAULT_ADDR.to_string());
if env::var(REMOTE_ADDR_ENV).is_ok() {
println!("Connecting to remote device {} ...", device_address);
} else if target.contains("android") {
start_android_emulator(server);
} else {
let rootfs = rootfs.as_ref().expect("need rootfs on non-android");
start_qemu_emulator(target, rootfs, server, tmpdir);
}
// Wait for the emulator to come online
loop {
let dur = Duration::from_millis(100);
if let Ok(mut client) = TcpStream::connect(&device_address) {
t!(client.set_read_timeout(Some(dur)));
t!(client.set_write_timeout(Some(dur)));
if client.write_all(b"ping").is_ok() {
let mut b = [0; 4];
if client.read_exact(&mut b).is_ok() {
break;
}
}
}
thread::sleep(dur);
}
}
fn start_android_emulator(server: &Path) {
println!("waiting for device to come online");
let status = Command::new("adb").arg("wait-for-device").status().unwrap();
assert!(status.success());
println!("pushing server");
let status =
Command::new("adb").arg("push").arg(server).arg("/data/local/tmp/testd").status().unwrap();
assert!(status.success());
println!("forwarding tcp");
let status =
Command::new("adb").arg("forward").arg("tcp:12345").arg("tcp:12345").status().unwrap();
assert!(status.success());
println!("executing server");
Command::new("adb").arg("shell").arg("/data/local/tmp/testd").spawn().unwrap();
}
fn prepare_rootfs(target: &str, rootfs: &Path, server: &Path, rootfs_img: &Path) {
t!(fs::copy(server, rootfs.join("testd")));
match target {
"arm-unknown-linux-gnueabihf" | "aarch64-unknown-linux-gnu" => {
prepare_rootfs_cpio(rootfs, rootfs_img)
}
"riscv64gc-unknown-linux-gnu" => prepare_rootfs_ext4(rootfs, rootfs_img),
_ => panic!("{} is not supported", target),
}
}
fn prepare_rootfs_cpio(rootfs: &Path, rootfs_img: &Path) {
// Generate a new rootfs image now that we've updated the test server
// executable. This is the equivalent of:
//
// find $rootfs -print 0 | cpio --null -o --format=newc > rootfs.img
let mut cmd = Command::new("cpio");
cmd.arg("--null")
.arg("-o")
.arg("--format=newc")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.current_dir(rootfs);
let mut child = t!(cmd.spawn());
let mut stdin = child.stdin.take().unwrap();
let rootfs = rootfs.to_path_buf();
thread::spawn(move || add_files(&mut stdin, &rootfs, &rootfs));
t!(io::copy(&mut child.stdout.take().unwrap(), &mut t!(File::create(&rootfs_img))));
assert!(t!(child.wait()).success());
fn add_files(w: &mut dyn Write, root: &Path, cur: &Path) {
for entry in t!(cur.read_dir()) {
let entry = t!(entry);
let path = entry.path();
let to_print = path.strip_prefix(root).unwrap();
t!(write!(w, "{}\u{0}", to_print.to_str().unwrap()));
if t!(entry.file_type()).is_dir() {
add_files(w, root, &path);
}
}
}
}
fn prepare_rootfs_ext4(rootfs: &Path, rootfs_img: &Path) {
let mut dd = Command::new("dd");
dd.arg("if=/dev/zero")
.arg(&format!("of={}", rootfs_img.to_string_lossy()))
.arg("bs=1M")
.arg("count=1024");
let mut dd_child = t!(dd.spawn());
assert!(t!(dd_child.wait()).success());
let mut mkfs = Command::new("mkfs.ext4");
mkfs.arg("-d").arg(rootfs).arg(rootfs_img);
let mut mkfs_child = t!(mkfs.spawn());
assert!(t!(mkfs_child.wait()).success());
}
fn start_qemu_emulator(target: &str, rootfs: &Path, server: &Path, tmpdir: &Path) {
let rootfs_img = &tmpdir.join("rootfs.img");
prepare_rootfs(target, rootfs, server, rootfs_img);
// Start up the emulator, in the background
match target {
"arm-unknown-linux-gnueabihf" => {
let mut cmd = Command::new("qemu-system-arm");
cmd.arg("-M")
.arg("vexpress-a15")
.arg("-m")
.arg("1024")
.arg("-kernel")
.arg("/tmp/zImage")
.arg("-initrd")
.arg(&rootfs_img)
.arg("-dtb")
.arg("/tmp/vexpress-v2p-ca15-tc1.dtb")
.arg("-append")
.arg("console=ttyAMA0 root=/dev/ram rdinit=/sbin/init init=/sbin/init")
.arg("-nographic")
.arg("-netdev")
.arg("user,id=net0,hostfwd=tcp::12345-:12345")
.arg("-device")
.arg("virtio-net-device,netdev=net0,mac=00:00:00:00:00:00");
t!(cmd.spawn());
}
"aarch64-unknown-linux-gnu" => {
let mut cmd = Command::new("qemu-system-aarch64");
cmd.arg("-machine")
.arg("virt")
.arg("-cpu")
.arg("cortex-a57")
.arg("-m")
.arg("1024")
.arg("-kernel")
.arg("/tmp/Image")
.arg("-initrd")
.arg(&rootfs_img)
.arg("-append")
.arg("console=ttyAMA0 root=/dev/ram rdinit=/sbin/init init=/sbin/init")
.arg("-nographic")
.arg("-netdev")
.arg("user,id=net0,hostfwd=tcp::12345-:12345")
.arg("-device")
.arg("virtio-net-device,netdev=net0,mac=00:00:00:00:00:00");
t!(cmd.spawn());
}
"riscv64gc-unknown-linux-gnu" => {
let mut cmd = Command::new("qemu-system-riscv64");
cmd.arg("-nographic")
.arg("-machine")
.arg("virt")
.arg("-m")
.arg("1024")
.arg("-bios")
.arg("none")
.arg("-kernel")
.arg("/tmp/bbl")
.arg("-append")
.arg("quiet console=ttyS0 root=/dev/vda rw")
.arg("-netdev")
.arg("user,id=net0,hostfwd=tcp::12345-:12345")
.arg("-device")
.arg("virtio-net-device,netdev=net0,mac=00:00:00:00:00:00")
.arg("-device")
.arg("virtio-blk-device,drive=hd0")
.arg("-drive")
.arg(&format!("file={},format=raw,id=hd0", &rootfs_img.to_string_lossy()));
t!(cmd.spawn());
}
_ => panic!("cannot start emulator for: {}", target),
}
}
fn push(path: &Path) {
let device_address = env::var(REMOTE_ADDR_ENV).unwrap_or(DEFAULT_ADDR.to_string());
let client = t!(TcpStream::connect(device_address));
let mut client = BufWriter::new(client);
t!(client.write_all(b"push"));
send(path, &mut client);
t!(client.flush());
// Wait for an acknowledgement that all the data was received. No idea
// why this is necessary, seems like it shouldn't be!
let mut client = client.into_inner().unwrap();
let mut buf = [0; 4];
t!(client.read_exact(&mut buf));
assert_eq!(&buf, b"ack ");
println!("done pushing {:?}", path);
}
fn run(support_lib_count: usize, exe: String, all_args: Vec<String>) {
let device_address = env::var(REMOTE_ADDR_ENV).unwrap_or(DEFAULT_ADDR.to_string());
let client = t!(TcpStream::connect(device_address));
let mut client = BufWriter::new(client);
t!(client.write_all(b"run "));
let (support_libs, args) = all_args.split_at(support_lib_count);
// Send over the args
for arg in args {
t!(client.write_all(arg.as_bytes()));
t!(client.write_all(&[0]));
}
t!(client.write_all(&[0]));
// Send over env vars
//
// Don't send over *everything* though as some env vars are set by and used
// by the client.
for (k, v) in env::vars() {
match &k[..] {
"PATH" | "LD_LIBRARY_PATH" | "PWD" | "RUST_TEST_TMPDIR" => continue,
_ => {}
}
t!(client.write_all(k.as_bytes()));
t!(client.write_all(&[0]));
t!(client.write_all(v.as_bytes()));
t!(client.write_all(&[0]));
}
t!(client.write_all(&[0]));
// Send over support libraries
for file in support_libs.iter().map(Path::new) {
send(&file, &mut client);
}
t!(client.write_all(&[0]));
// Send over the client executable as the last piece
send(exe.as_ref(), &mut client);
println!("uploaded {:?}, waiting for result", exe);
// Ok now it's time to read all the output. We're receiving "frames"
// representing stdout/stderr, so we decode all that here.
let mut header = [0; 9];
let mut stderr_done = false;
let mut stdout_done = false;
let mut client = t!(client.into_inner());
let mut stdout = io::stdout();
let mut stderr = io::stderr();
while !stdout_done || !stderr_done {
t!(client.read_exact(&mut header));
let amt = u64::from_be_bytes(header[1..9].try_into().unwrap());
if header[0] == 0 {
if amt == 0 {
stdout_done = true;
} else {
t!(io::copy(&mut (&mut client).take(amt), &mut stdout));
t!(stdout.flush());
}
} else {
if amt == 0 {
stderr_done = true;
} else {
t!(io::copy(&mut (&mut client).take(amt), &mut stderr));
t!(stderr.flush());
}
}
}
// Finally, read out the exit status
let mut status = [0; 5];
t!(client.read_exact(&mut status));
let code = ((status[1] as i32) << 24)
| ((status[2] as i32) << 16)
| ((status[3] as i32) << 8)
| ((status[4] as i32) << 0);
if status[0] == 0 {
std::process::exit(code);
} else {
println!("died due to signal {}", code);
std::process::exit(3);
}
}
fn send(path: &Path, dst: &mut dyn Write) {
t!(dst.write_all(path.file_name().unwrap().to_str().unwrap().as_bytes()));
t!(dst.write_all(&[0]));
let mut file = t!(File::open(&path));
let amt = t!(file.metadata()).len();
t!(dst.write_all(&amt.to_be_bytes()));
t!(io::copy(&mut file, dst));
}
fn help() {
println!(
"
Usage: {0} <command> [<args>]
Sub-commands:
spawn-emulator <target> <server> <tmpdir> [rootfs] See below
push <path> Copy <path> to emulator
run <support_lib_count> <file> [support_libs...] [args...]
Run program on emulator
help Display help message
Spawning an emulator:
For Android <target>s, adb will push the <server>, set up TCP forwarding and run
the <server>. Otherwise qemu emulates the target using a rootfs image created in
<tmpdir> and generated from <rootfs> plus the <server> executable.
If {1} is set in the environment, this step is skipped.
Pushing a path to a running emulator:
A running emulator or adb device is connected to at the IP address and port in
the {1} environment variable or {2} if this isn't
specified. The file at <path> is sent to this target.
Executing commands on a running emulator:
First the target emulator/adb session is connected to as for pushing files. Next
the <file> and any specified support libs are pushed to the target. Finally, the
<file> is executed in the emulator, preserving the current environment.
That command's status code is returned.
",
env::args().next().unwrap(),
REMOTE_ADDR_ENV,
DEFAULT_ADDR
);
}