use crate::cmd::{Command, CommandError};
use crate::Workspace;
use failure::Error;
use log::{error, info};
use serde::Deserialize;
use std::fmt;
use std::path::{Path, PathBuf};
use std::time::Duration;
pub struct SandboxImage {
name: String,
}
impl SandboxImage {
pub fn local(name: &str) -> Result<Self, Error> {
let image = SandboxImage { name: name.into() };
info!("sandbox image is local, skipping pull");
image.ensure_exists_locally()?;
Ok(image)
}
pub fn remote(name: &str) -> Result<Self, Error> {
let image = SandboxImage { name: name.into() };
info!("pulling image {} from Docker Hub", name);
Command::new_workspaceless("docker")
.args(&["pull", &name])
.run()?;
image.ensure_exists_locally()?;
Ok(image)
}
fn ensure_exists_locally(&self) -> Result<(), Error> {
info!("checking the image {} is available locally", self.name);
Command::new_workspaceless("docker")
.args(&["image", "inspect", &self.name])
.log_output(false)
.run()?;
Ok(())
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum MountKind {
ReadWrite,
ReadOnly,
#[doc(hidden)]
__NonExaustive,
}
#[derive(Clone)]
struct MountConfig {
host_path: PathBuf,
sandbox_path: PathBuf,
perm: MountKind,
}
impl MountConfig {
fn host_path(&self, workspace: &Workspace) -> Result<PathBuf, Error> {
if let Some(container) = workspace.current_container() {
let inside_container_path = crate::utils::normalize_path(&self.host_path);
for mount in container.mounts() {
let dest = crate::utils::normalize_path(Path::new(mount.destination()));
if let Ok(shared) = inside_container_path.strip_prefix(&dest) {
return Ok(Path::new(mount.source()).join(shared));
}
}
failure::bail!("the workspace is not mounted from outside the container");
} else {
Ok(crate::utils::normalize_path(&self.host_path))
}
}
fn to_volume_arg(&self, workspace: &Workspace) -> Result<String, Error> {
let perm = match self.perm {
MountKind::ReadWrite => "rw",
MountKind::ReadOnly => "ro",
MountKind::__NonExaustive => panic!("do not create __NonExaustive variants manually"),
};
Ok(format!(
"{}:{}:{},Z",
self.host_path(workspace)?.to_string_lossy(),
self.sandbox_path.to_string_lossy(),
perm
))
}
fn to_mount_arg(&self, workspace: &Workspace) -> Result<String, Error> {
let mut opts_with_leading_comma = vec![];
if self.perm == MountKind::ReadOnly {
opts_with_leading_comma.push(",readonly");
}
Ok(format!(
"type=bind,src={},dst={}{}",
self.host_path(workspace)?.to_string_lossy(),
self.sandbox_path.to_string_lossy(),
opts_with_leading_comma.join(""),
))
}
}
#[derive(Clone)]
pub struct SandboxBuilder {
mounts: Vec<MountConfig>,
env: Vec<(String, String)>,
memory_limit: Option<usize>,
workdir: Option<String>,
cmd: Vec<String>,
enable_networking: bool,
}
impl SandboxBuilder {
pub fn new() -> Self {
Self {
mounts: Vec::new(),
env: Vec::new(),
workdir: None,
memory_limit: None,
cmd: Vec::new(),
enable_networking: true,
}
}
pub fn mount(mut self, host_path: &Path, sandbox_path: &Path, kind: MountKind) -> Self {
self.mounts.push(MountConfig {
host_path: host_path.into(),
sandbox_path: sandbox_path.into(),
perm: kind,
});
self
}
pub fn memory_limit(mut self, limit: Option<usize>) -> Self {
self.memory_limit = limit;
self
}
pub fn enable_networking(mut self, enable: bool) -> Self {
self.enable_networking = enable;
self
}
pub(super) fn env<S1: Into<String>, S2: Into<String>>(mut self, key: S1, value: S2) -> Self {
self.env.push((key.into(), value.into()));
self
}
pub(super) fn cmd(mut self, cmd: Vec<String>) -> Self {
self.cmd = cmd;
self
}
pub(super) fn workdir<S: Into<String>>(mut self, workdir: S) -> Self {
self.workdir = Some(workdir.into());
self
}
fn create(self, workspace: &Workspace) -> Result<Container<'_>, Error> {
let mut args: Vec<String> = vec!["create".into()];
for mount in &self.mounts {
std::fs::create_dir_all(&mount.host_path)?;
if cfg!(windows) {
args.push("--mount".into());
args.push(mount.to_mount_arg(workspace)?)
} else {
args.push("-v".into());
args.push(mount.to_volume_arg(workspace)?)
}
}
for &(ref var, ref value) in &self.env {
args.push("-e".into());
args.push(format! {"{}={}", var, value})
}
if let Some(workdir) = self.workdir {
args.push("-w".into());
args.push(workdir);
}
if let Some(limit) = self.memory_limit {
args.push("-m".into());
args.push(limit.to_string());
}
if !self.enable_networking {
args.push("--network".into());
args.push("none".into());
}
if cfg!(windows) {
args.push("--isolation=process".into());
}
args.push(workspace.sandbox_image().name.clone());
for arg in self.cmd {
args.push(arg);
}
let out = Command::new(workspace, "docker")
.args(&*args)
.run_capture()?;
Ok(Container {
id: out.stdout_lines()[0].clone(),
workspace,
})
}
pub(super) fn run(
self,
workspace: &Workspace,
timeout: Option<Duration>,
no_output_timeout: Option<Duration>,
) -> Result<(), Error> {
let container = self.create(workspace)?;
scopeguard::defer! {{
if let Err(err) = container.delete() {
error!("failed to delete container {}", container.id);
error!("caused by: {}", err);
for cause in err.iter_causes() {
error!("caused by: {}", cause);
}
}
}}
container.run(timeout, no_output_timeout)?;
Ok(())
}
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct InspectContainer {
state: InspectState,
}
#[derive(Deserialize)]
struct InspectState {
#[serde(rename = "OOMKilled")]
oom_killed: bool,
}
#[derive(Clone)]
struct Container<'w> {
id: String,
workspace: &'w Workspace,
}
impl fmt::Display for Container<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.id.fmt(f)
}
}
impl Container<'_> {
fn inspect(&self) -> Result<InspectContainer, Error> {
let output = Command::new(self.workspace, "docker")
.args(&["inspect", &self.id])
.log_output(false)
.run_capture()?;
let mut data: Vec<InspectContainer> =
::serde_json::from_str(&output.stdout_lines().join("\n"))?;
assert_eq!(data.len(), 1);
Ok(data.pop().unwrap())
}
fn run(
&self,
timeout: Option<Duration>,
no_output_timeout: Option<Duration>,
) -> Result<(), Error> {
let res = Command::new(self.workspace, "docker")
.args(&["start", "-a", &self.id])
.timeout(timeout)
.no_output_timeout(no_output_timeout)
.run();
let details = self.inspect()?;
if details.state.oom_killed {
if let Err(err) = res {
Err(err.context(CommandError::SandboxOOM).into())
} else {
Err(CommandError::SandboxOOM.into())
}
} else {
res
}
}
fn delete(&self) -> Result<(), Error> {
Command::new(self.workspace, "docker")
.args(&["rm", "-f", &self.id])
.run()
}
}
pub fn docker_running(workspace: &Workspace) -> bool {
info!("checking if the docker daemon is running");
Command::new(workspace, "docker")
.args(&["info"])
.log_output(false)
.run()
.is_ok()
}