From 7376910610a93812f15f0ac5e4844e416bf6228f Mon Sep 17 00:00:00 2001 From: Federico Fissore Date: Fri, 15 Feb 2019 11:30:38 +0100 Subject: [PATCH 1/2] Sped up the compilation/execution of snippets by using previously started and idle docker containers, rather than starting a new one for each request. By sleeping for a long time in entrypoint.sh, containers stay idle. Cargo is called through cargo.sh, which cares of exporting env vars and killing the container once it's done. Thus, there's no need to drop a container that has been used. However idle containers need to me manually terminated: DockerContainers Drop impl takes care of that. --- compiler/base/Dockerfile | 1 + compiler/base/cargo.sh | 28 ++++ compiler/base/entrypoint.sh | 7 +- ui/src/main.rs | 34 +++-- ui/src/sandbox.rs | 288 +++++++++++++++++++++++++++++++----- 5 files changed, 299 insertions(+), 59 deletions(-) create mode 100644 compiler/base/cargo.sh diff --git a/compiler/base/Dockerfile b/compiler/base/Dockerfile index 8ca1ba57c..7eb8af03e 100644 --- a/compiler/base/Dockerfile +++ b/compiler/base/Dockerfile @@ -41,6 +41,7 @@ RUN cd / && \ WORKDIR /playground ADD Cargo.toml /playground/Cargo.toml +ADD cargo.sh /playground/cargo.sh ADD crate-information.json /playground/crate-information.json RUN cargo fetch diff --git a/compiler/base/cargo.sh b/compiler/base/cargo.sh new file mode 100644 index 000000000..533a75fbb --- /dev/null +++ b/compiler/base/cargo.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu + +POSITIONAL=() + +while [[ $# -gt 0 ]]; do + case $1 in + --env) + export $2 + shift 2 + ;; + *) + POSITIONAL+=("$1") + shift + ;; + esac +done + +eval set -- "${POSITIONAL[@]}" + +timeout=${PLAYGROUND_TIMEOUT:-10} + +modify-cargo-toml + +timeout --signal=KILL ${timeout} cargo "$@" || pkill sleep + +pkill sleep diff --git a/compiler/base/entrypoint.sh b/compiler/base/entrypoint.sh index 9f7a06074..28546ba98 100755 --- a/compiler/base/entrypoint.sh +++ b/compiler/base/entrypoint.sh @@ -1,8 +1,3 @@ #!/bin/bash -set -eu - -timeout=${PLAYGROUND_TIMEOUT:-10} - -modify-cargo-toml -timeout --signal=KILL ${timeout} "$@" +sleep 365d diff --git a/ui/src/main.rs b/ui/src/main.rs index a00cb1ec3..5b16a2c43 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -26,11 +26,12 @@ use std::{ time::{Duration, Instant}, }; -use crate::sandbox::Sandbox; +use crate::sandbox::{Channel, DockerContainers, Sandbox}; const DEFAULT_ADDRESS: &str = "127.0.0.1"; const DEFAULT_PORT: u16 = 5000; const DEFAULT_LOG_FILE: &str = "access-log.csv"; +const DEFAULT_DOCKER_CONTAINER_POOL_SIZE: usize = 10; mod asm_cleanup; mod gist; @@ -55,6 +56,9 @@ fn main() { let port = env::var("PLAYGROUND_UI_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(DEFAULT_PORT); let logfile = env::var("PLAYGROUND_LOG_FILE").unwrap_or_else(|_| DEFAULT_LOG_FILE.to_string()); let cors_enabled = env::var_os("PLAYGROUND_CORS_ENABLED").is_some(); + let docker_containers_pool_size = env::var("DOCKER_CONTAINER_POOL_SIZE").ok().and_then(|v| v.parse().ok()).unwrap_or(DEFAULT_DOCKER_CONTAINER_POOL_SIZE); + + let containers = Arc::new(DockerContainers::new(docker_containers_pool_size)); let files = Staticfile::new(&root).expect("Unable to open root directory"); let mut files = Chain::new(files); @@ -71,8 +75,10 @@ fn main() { let mut mount = Mount::new(); mount.mount("/", files); - mount.mount("/compile", compile); - mount.mount("/execute", execute); + let containers_clone = containers.clone(); + mount.mount("/compile", move |req: &mut Request<'_, '_>| compile(req, &containers_clone)); + let containers_clone = containers.clone(); + mount.mount("/execute", move |req: &mut Request<'_, '_>| handle(req, &containers_clone)); mount.mount("/format", format); mount.mount("/clippy", clippy); mount.mount("/miri", miri); @@ -84,7 +90,8 @@ fn main() { mount.mount("/meta/version/clippy", meta_version_clippy); mount.mount("/meta/version/miri", meta_version_miri); mount.mount("/meta/gist", gist_router); - mount.mount("/evaluate.json", evaluate); + let containers_clone = containers.clone(); + mount.mount("/evaluate.json", move |req: &mut Request<'_, '_>| evaluate(req, &containers_clone)); let mut chain = Chain::new(mount); let file_logger = FileLogger::new(logfile).expect("Unable to create file logger"); @@ -135,21 +142,23 @@ impl iron::typemap::Key for GhToken { type Value = Self; } -fn compile(req: &mut Request<'_, '_>) -> IronResult { +fn compile(req: &mut Request<'_, '_>, containers: &DockerContainers) -> IronResult { with_sandbox(req, |sandbox, req: CompileRequest| { - let req = req.try_into()?; + let req: sandbox::CompileRequest = req.try_into()?; + let container = containers.pop(req.channel).unwrap(); sandbox - .compile(&req) + .compile(&req, &container) .map(CompileResponse::from) .eager_context(Compilation) }) } -fn execute(req: &mut Request<'_, '_>) -> IronResult { +fn handle(req: &mut Request<'_, '_>, containers: &DockerContainers) -> IronResult { with_sandbox(req, |sandbox, req: ExecuteRequest| { - let req = req.try_into()?; + let req: sandbox::ExecuteRequest = req.try_into()?; + let container = containers.pop(req.channel).unwrap(); sandbox - .execute(&req) + .execute(&req, &container) .map(ExecuteResponse::from) .eager_context(Execution) }) @@ -262,11 +271,12 @@ fn meta_gist_get(req: &mut Request<'_, '_>) -> IronResult { // This is a backwards compatibilty shim. The Rust homepage and the // documentation use this to run code in place. -fn evaluate(req: &mut Request<'_, '_>) -> IronResult { +fn evaluate(req: &mut Request<'_, '_>, containers: &DockerContainers) -> IronResult { with_sandbox(req, |sandbox, req: EvaluateRequest| { let req = req.try_into()?; + let container = containers.pop(Channel::Stable).unwrap(); sandbox - .execute(&req) + .execute(&req, &container) .map(EvaluateResponse::from) .eager_context(Evaluation) }) diff --git a/ui/src/sandbox.rs b/ui/src/sandbox.rs index 1dfe837a8..183d59779 100644 --- a/ui/src/sandbox.rs +++ b/ui/src/sandbox.rs @@ -1,14 +1,18 @@ use serde_derive::Deserialize; use snafu::{ResultExt, Snafu}; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, ffi::OsStr, fmt, fs::{self, File}, io::{self, prelude::*, BufReader, BufWriter, ErrorKind}, path::{Path, PathBuf}, - process::Command, + process::{Command, ChildStdout, Stdio}, string, + sync::{Mutex, Arc}, + sync::mpsc::{self, Receiver, RecvError}, + time::Duration, + thread }; use tempdir::TempDir; @@ -90,10 +94,10 @@ impl Sandbox { }) } - pub fn compile(&self, req: &CompileRequest) -> Result { - self.write_source_code(&req.code)?; + pub fn compile(&self, req: &CompileRequest, container: &DockerContainer) -> Result { + self.write_source_code_to_path(&container.src_dir, &req.crate_type, &req.code)?; - let mut command = self.compile_command(req.target, req.channel, req.mode, req.tests, req); + let mut command = self.compile_command(req.target, req.channel, req.mode, req.tests, req, container); let output = command.output().context(UnableToExecuteCompiler)?; @@ -101,7 +105,7 @@ impl Sandbox { // `compilation-3b75174cac3d47fb.ll`, so we just find the // first with the right extension. let file = - fs::read_dir(&self.output_dir) + fs::read_dir(&container.output_dir) .context(UnableToReadOutput)? .flat_map(|entry| entry) .map(|entry| entry.path()) @@ -143,9 +147,9 @@ impl Sandbox { }) } - pub fn execute(&self, req: &ExecuteRequest) -> Result { - self.write_source_code(&req.code)?; - let mut command = self.execute_command(req.channel, req.mode, req.tests, req); + pub fn execute(&self, req: &ExecuteRequest, container: &DockerContainer) -> Result { + self.write_source_code_to_path(&container.src_dir, &req.crate_type, &req.code)?; + let mut command = self.execute_command(req.channel, req.mode, req.tests, req, &container); let output = command.output().context(UnableToExecuteCompiler)?; @@ -279,26 +283,39 @@ impl Sandbox { Ok(()) } - fn compile_command(&self, target: CompileTarget, channel: Channel, mode: Mode, tests: bool, req: impl CrateTypeRequest + EditionRequest + BacktraceRequest) -> Command { - let mut cmd = self.docker_command(Some(req.crate_type())); - set_execution_environment(&mut cmd, Some(target), &req); + fn write_source_code_to_path(&self, src_dir: &PathBuf, crate_type: &CrateType, code: &str) -> Result<()> { + let data = code.as_bytes(); + + let input_file = src_dir.clone().join(crate_type.file_name()); + let file = File::create(&input_file).context(UnableToCreateSourceFile)?; + let mut file = BufWriter::new(file); + + file.write_all(data).context(UnableToCreateSourceFile)?; + + log::debug!("Wrote {} bytes of source to {:?}", data.len(), input_file.display()); + Ok(()) + } + + fn compile_command(&self, target: CompileTarget, channel: Channel, mode: Mode, tests: bool, req: impl CrateTypeRequest + EditionRequest + BacktraceRequest, container: &DockerContainer) -> Command { + let mut cmd = self.docker_exec_command(&container.id); let execution_cmd = build_execution_command(Some(target), channel, mode, &req, tests); + cmd.args(&execution_cmd); - cmd.arg(&channel.container_name()).args(&execution_cmd); + set_execution_environment(&mut cmd, Some(target), &req); log::debug!("Compilation command is {:?}", cmd); cmd } - fn execute_command(&self, channel: Channel, mode: Mode, tests: bool, req: impl CrateTypeRequest + EditionRequest + BacktraceRequest) -> Command { - let mut cmd = self.docker_command(Some(req.crate_type())); - set_execution_environment(&mut cmd, None, &req); + fn execute_command(&self, channel: Channel, mode: Mode, tests: bool, req: impl CrateTypeRequest + EditionRequest + BacktraceRequest, container: &DockerContainer) -> Command { + let mut cmd = self.docker_exec_command(&container.id); let execution_cmd = build_execution_command(None, channel, mode, &req, tests); + cmd.args(&execution_cmd); - cmd.arg(&channel.container_name()).args(&execution_cmd); + set_execution_environment(&mut cmd, None, &req); log::debug!("Execution command is {:?}", cmd); @@ -346,7 +363,7 @@ impl Sandbox { let mut mount_input_file = self.input_file.as_os_str().to_os_string(); mount_input_file.push(":"); - mount_input_file.push("/playground/"); + mount_input_file.push("/playground/src/"); mount_input_file.push(crate_type.file_name()); let mut mount_output_dir = self.output_dir.as_os_str().to_os_string(); @@ -361,6 +378,126 @@ impl Sandbox { cmd } + + fn docker_exec_command(&self, container_id: &str) -> Command { + let mut cmd = Command::new("docker"); + + cmd + .arg("exec") + .arg(container_id); + + cmd + } + +} + +pub struct DockerContainer { + pub id: String, + _temp_dir: TempDir, //we need to keep this, or it's dropped (= deleted) + pub src_dir: PathBuf, + pub output_dir: PathBuf, +} + +impl DockerContainer { + + pub fn terminate(&self) { + let mut cmd = Command::new("docker"); + cmd + .arg("exec") + .arg(&self.id) + .args(&["pkill", "sleep"]) + .status(); + } + +} + +pub struct DockerContainers { + receivers: HashMap>>>, + active: Arc> +} + +impl DockerContainers { + pub fn new(pool_size: usize) -> DockerContainers { + let mut receivers = HashMap::new(); + + let active = Arc::new(Mutex::new(true)); + + for channel in [Channel::Stable, Channel::Beta, Channel::Nightly].iter() { + let (sender, receiver) = mpsc::sync_channel(pool_size); + + receivers.insert(channel.to_owned(), Arc::new(Mutex::new(receiver))); + + let active_mutex = active.clone(); + thread::spawn(move || { + let mut active = true; + while active { + sender.send(DockerContainers::start_container(channel)).unwrap_or(()); + active = *active_mutex.lock().unwrap(); + } + }); + } + + DockerContainers { receivers, active } + } + + fn container_id_from_stdout(stdout: ChildStdout) -> String { + let mut stdout = BufReader::new(stdout); + let mut container_id = String::new(); + stdout.read_line(&mut container_id).unwrap(); + + container_id.trim().to_string() + } + + fn start_container(channel: &Channel) -> DockerContainer { + let temp_dir = TempDir::new("playground_container").unwrap(); + let src_dir = temp_dir.path().to_owned(); + let output_dir = temp_dir.path().join("output"); + + let mut mount_src_dir = src_dir.as_os_str().to_os_string(); + mount_src_dir.push(":"); + mount_src_dir.push("/playground/src"); + + let mut mount_output_dir = output_dir.as_os_str().to_os_string(); + mount_output_dir.push(":"); + mount_output_dir.push("/playground-result"); + + let mut cmd = basic_secure_docker_command(); + cmd + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .arg("--volume").arg(&mount_src_dir) + .arg("--volume").arg(&mount_output_dir) + .arg("-d") + .arg(channel.container_name()); + + match cmd.spawn() { + Ok(child) => { + let id = Self::container_id_from_stdout(child.stdout.unwrap()); + + DockerContainer { id, _temp_dir: temp_dir, src_dir, output_dir } + } + Err(err) => panic!("Error starting container {}: {}", channel.container_name(), err) + } + } + + pub fn pop(&self, channel: Channel) -> Result { + self.receivers.get(&channel).unwrap().lock().unwrap().recv() + } +} + +impl Drop for DockerContainers { + + fn drop(&mut self) { + let mut active = self.active.lock().unwrap(); + *active = false; + + for (_, receiver) in self.receivers.iter() { + while let Ok(container) = receiver.lock().unwrap().recv_timeout(Duration::new(1, 0)) { + container.terminate(); + } + } + } + } fn basic_secure_docker_command() -> Command { @@ -390,7 +527,7 @@ fn build_execution_command(target: Option, channel: Channel, mode use self::CrateType::*; use self::Mode::*; - let mut cmd = vec!["cargo"]; + let mut cmd = vec!["bash", "cargo.sh"]; match (target, req.crate_type(), tests) { (Some(Wasm), _, _) => cmd.push("wasm"), @@ -510,7 +647,7 @@ impl fmt::Display for CompileTarget { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Channel { Stable, Beta, @@ -563,8 +700,8 @@ impl CrateType { use self::CrateType::*; match *self { - Binary => "src/main.rs", - Library(_) => "src/lib.rs", + Binary => "main.rs", + Library(_) => "lib.rs", } } } @@ -760,6 +897,8 @@ pub struct MiriResponse { #[cfg(test)] mod test { + use std::time::Duration; + use super::*; const HELLO_WORLD_CODE: &'static str = r#" @@ -812,7 +951,10 @@ mod test { let req = ExecuteRequest::default(); let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stdout.contains("Hello, world!")); } @@ -837,7 +979,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stdout.contains("debug mode")); } @@ -851,7 +996,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stdout.contains("release mode")); } @@ -875,7 +1023,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stdout.contains("rustc")); assert!(!resp.stdout.contains("beta")); @@ -891,7 +1042,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stdout.contains("rustc")); assert!(resp.stdout.contains("beta")); @@ -907,7 +1061,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stdout.contains("rustc")); assert!(!resp.stdout.contains("beta")); @@ -932,7 +1089,10 @@ mod test { }; let sb = Sandbox::new()?; - let resp = sb.execute(&req)?; + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container)?; assert!(resp.stderr.contains("`?` is not a macro repetition operator")); Ok(()) @@ -948,7 +1108,10 @@ mod test { }; let sb = Sandbox::new()?; - let resp = sb.execute(&req)?; + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container)?; assert!(resp.stderr.contains("`?` is not a macro repetition operator")); Ok(()) @@ -964,7 +1127,10 @@ mod test { }; let sb = Sandbox::new()?; - let resp = sb.execute(&req)?; + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container)?; assert!(!resp.stderr.contains("`crate` in paths is experimental")); Ok(()) @@ -989,7 +1155,10 @@ mod test { }; let sb = Sandbox::new()?; - let resp = sb.execute(&req)?; + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container)?; assert!(resp.stderr.contains("Run with `RUST_BACKTRACE=1` for a backtrace")); assert!(!resp.stderr.contains("stack backtrace:")); @@ -1006,7 +1175,10 @@ mod test { }; let sb = Sandbox::new()?; - let resp = sb.execute(&req)?; + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container)?; assert!(!resp.stderr.contains("Run with `RUST_BACKTRACE=1` for a backtrace")); assert!(resp.stderr.contains("stack backtrace:")); @@ -1022,7 +1194,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.compile(&req).expect("Unable to compile code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.compile(&req, &container).expect("Unable to compile code"); assert!(resp.code.contains("ModuleID")); assert!(resp.code.contains("target datalayout")); @@ -1037,7 +1212,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.compile(&req).expect("Unable to compile code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.compile(&req, &container).expect("Unable to compile code"); assert!(resp.code.contains(".text")); assert!(resp.code.contains(".file")); @@ -1053,7 +1231,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.compile(&req).expect("Unable to compile code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.compile(&req, &container).expect("Unable to compile code"); assert!(resp.code.contains("core::fmt::Arguments::new_v1")); assert!(resp.code.contains("std::io::stdio::_print@GOTPCREL")); @@ -1068,7 +1249,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.compile(&req).expect("Unable to compile code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.compile(&req, &container).expect("Unable to compile code"); assert!(resp.code.contains(".text")); assert!(resp.code.contains(".file")); @@ -1175,7 +1359,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stdout.contains("Failed to connect")); } @@ -1196,7 +1383,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stderr.contains("Killed")); } @@ -1216,7 +1406,10 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stderr.contains("Killed")); } @@ -1241,8 +1434,21 @@ mod test { }; let sb = Sandbox::new().expect("Unable to create sandbox"); - let resp = sb.execute(&req).expect("Unable to execute code"); + let containers = DockerContainers::new(0); + let container = containers.pop(req.channel).unwrap(); + + let resp = sb.execute(&req, &container).expect("Unable to execute code"); assert!(resp.stderr.contains("Cannot fork")); } + + #[test] + fn docker_containers() { + let containers = DockerContainers::new(0); + thread::sleep(Duration::new(2, 0)); + + let container = containers.pop(Channel::Stable).unwrap(); + assert_eq!(false, container.id.is_empty()); + container.terminate(); + } } From 61f8369a9633ed93a410dbec300c4da8841cc456 Mon Sep 17 00:00:00 2001 From: Federico Fissore Date: Fri, 15 Feb 2019 16:37:23 +0100 Subject: [PATCH 2/2] Added shutdown hook so we stop idle docker containers when stopping the playground Also containers are now stopped in parallel --- ui/Cargo.lock | 30 +++++++++++++++++++++++ ui/Cargo.toml | 1 + ui/src/main.rs | 24 +++++++++++++------ ui/src/sandbox.rs | 61 +++++++++++++++++++++++++++++++++-------------- 4 files changed, 91 insertions(+), 25 deletions(-) diff --git a/ui/Cargo.lock b/ui/Cargo.lock index 8cd7842ee..ad93444fd 100644 --- a/ui/Cargo.lock +++ b/ui/Cargo.lock @@ -204,6 +204,15 @@ dependencies = [ "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ctrlc" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "dotenv" version = "0.13.0" @@ -673,6 +682,18 @@ dependencies = [ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "nix" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "nodrop" version = "0.1.13" @@ -1461,6 +1482,7 @@ version = "0.1.0" dependencies = [ "bodyparser 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "corsware 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "hubcaps 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1555,6 +1577,11 @@ name = "version_check" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "want" version = "0.0.6" @@ -1645,6 +1672,7 @@ dependencies = [ "checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" "checksum crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f8306fcef4a7b563b76b7dd949ca48f52bc1141aa067d2ea09565f3e2652aa5c" "checksum csv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7ef22b37c7a51c564a365892c012dc0271221fdcc64c69b19ba4d6fa8bd96d9c" +"checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e" "checksum dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d0a1279c96732bc6800ce6337b6a614697b0e74ae058dc03c62ebeb78b4d86" "checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a" "checksum error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02" @@ -1695,6 +1723,7 @@ dependencies = [ "checksum mount 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e25c06012941aaf8c75f2eaf7ec5c48cf69f9fc489ab3eb3589edc107e386f0b" "checksum native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff8e08de0070bbf4c31f452ea2a70db092f36f6f2e4d897adf5674477d488fb2" "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" +"checksum nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d37e713a259ff641624b6cb20e3b12b2952313ba36b6823c0f16e6cfd9e5de17" "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" "checksum num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a23f0ed30a54abaa0c7e83b1d2d87ada7c3c23078d1d87815af3e3b6385fbba" "checksum openssl 0.10.19 (registry+https://github.com/rust-lang/crates.io-index)" = "84321fb9004c3bce5611188a644d6171f895fa2889d155927d528782edb21c5d" @@ -1794,6 +1823,7 @@ dependencies = [ "checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" "checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "797464475f30ddb8830cc529aaaae648d581f99e2036a928877dfde027ddf6b3" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" diff --git a/ui/Cargo.toml b/ui/Cargo.toml index cffe835d1..3436b62c6 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -32,6 +32,7 @@ router = "0.6.0" openssl-probe = "0.1.2" dotenv = "0.13.0" snafu = "0.2.0" +ctrlc = { version = "3.1.1", features = ["termination"] } [dependencies.playground-middleware] git = "https://github.com/integer32llc/playground-middleware" diff --git a/ui/src/main.rs b/ui/src/main.rs index 5b16a2c43..02a56436d 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -22,6 +22,7 @@ use std::{ convert::{TryFrom, TryInto}, env, path::PathBuf, + process, sync::{Arc, Mutex}, time::{Duration, Instant}, }; @@ -43,6 +44,13 @@ const ONE_YEAR_IN_SECONDS: u64 = 60 * 60 * 24 * 365; const SANDBOX_CACHE_TIME_TO_LIVE_IN_SECONDS: u64 = ONE_HOUR_IN_SECONDS as u64; +fn set_graceful_shutdown_hook(containers: Arc>) { + ctrlc::set_handler(move || { + containers.lock().unwrap().terminate(); + process::exit(0); + }).expect("Error setting Ctrl-C handler"); +} + fn main() { // Dotenv may be unable to load environment variables, but that's ok in production let _ = dotenv::dotenv(); @@ -58,7 +66,9 @@ fn main() { let cors_enabled = env::var_os("PLAYGROUND_CORS_ENABLED").is_some(); let docker_containers_pool_size = env::var("DOCKER_CONTAINER_POOL_SIZE").ok().and_then(|v| v.parse().ok()).unwrap_or(DEFAULT_DOCKER_CONTAINER_POOL_SIZE); - let containers = Arc::new(DockerContainers::new(docker_containers_pool_size)); + let containers = Arc::new(Mutex::new(DockerContainers::new(docker_containers_pool_size))); + + set_graceful_shutdown_hook(containers.clone()); let files = Staticfile::new(&root).expect("Unable to open root directory"); let mut files = Chain::new(files); @@ -142,10 +152,10 @@ impl iron::typemap::Key for GhToken { type Value = Self; } -fn compile(req: &mut Request<'_, '_>, containers: &DockerContainers) -> IronResult { +fn compile(req: &mut Request<'_, '_>, containers: &Mutex) -> IronResult { with_sandbox(req, |sandbox, req: CompileRequest| { let req: sandbox::CompileRequest = req.try_into()?; - let container = containers.pop(req.channel).unwrap(); + let container = containers.lock().unwrap().pop(req.channel).unwrap(); sandbox .compile(&req, &container) .map(CompileResponse::from) @@ -153,10 +163,10 @@ fn compile(req: &mut Request<'_, '_>, containers: &DockerContainers) -> IronResu }) } -fn handle(req: &mut Request<'_, '_>, containers: &DockerContainers) -> IronResult { +fn handle(req: &mut Request<'_, '_>, containers: &Mutex) -> IronResult { with_sandbox(req, |sandbox, req: ExecuteRequest| { let req: sandbox::ExecuteRequest = req.try_into()?; - let container = containers.pop(req.channel).unwrap(); + let container = containers.lock().unwrap().pop(req.channel).unwrap(); sandbox .execute(&req, &container) .map(ExecuteResponse::from) @@ -271,10 +281,10 @@ fn meta_gist_get(req: &mut Request<'_, '_>) -> IronResult { // This is a backwards compatibilty shim. The Rust homepage and the // documentation use this to run code in place. -fn evaluate(req: &mut Request<'_, '_>, containers: &DockerContainers) -> IronResult { +fn evaluate(req: &mut Request<'_, '_>, containers: &Mutex) -> IronResult { with_sandbox(req, |sandbox, req: EvaluateRequest| { let req = req.try_into()?; - let container = containers.pop(Channel::Stable).unwrap(); + let container = containers.lock().unwrap().pop(Channel::Stable).unwrap(); sandbox .execute(&req, &container) .map(EvaluateResponse::from) diff --git a/ui/src/sandbox.rs b/ui/src/sandbox.rs index 183d59779..b8c8c954d 100644 --- a/ui/src/sandbox.rs +++ b/ui/src/sandbox.rs @@ -12,7 +12,7 @@ use std::{ sync::{Mutex, Arc}, sync::mpsc::{self, Receiver, RecvError}, time::Duration, - thread + thread::{self, JoinHandle} }; use tempdir::TempDir; @@ -396,17 +396,33 @@ pub struct DockerContainer { _temp_dir: TempDir, //we need to keep this, or it's dropped (= deleted) pub src_dir: PathBuf, pub output_dir: PathBuf, + terminated: Arc> } impl DockerContainer { - pub fn terminate(&self) { - let mut cmd = Command::new("docker"); - cmd - .arg("exec") - .arg(&self.id) - .args(&["pkill", "sleep"]) - .status(); + pub fn terminate(&mut self) -> JoinHandle<()> { + let mut terminated = self.terminated.lock().unwrap(); + *terminated = true; + + let container_id = self.id.clone(); + thread::spawn(move || { + let mut cmd = Command::new("docker"); + cmd + .arg("exec") + .arg(&container_id) + .args(&["pkill", "sleep"]) + .status() + .expect(&format!("Unable to kill container with id {}", container_id)); + }) + } + + // this was initially used to implement the Drop trait, keeping it for future reference + #[allow(dead_code)] + fn is_terminated(&self) -> bool { + let terminated = &self.terminated.clone(); + let terminated = terminated.lock().unwrap(); + *terminated } } @@ -431,7 +447,7 @@ impl DockerContainers { thread::spawn(move || { let mut active = true; while active { - sender.send(DockerContainers::start_container(channel)).unwrap_or(()); + sender.send(Self::start_container(channel)).unwrap_or(()); active = *active_mutex.lock().unwrap(); } }); @@ -474,7 +490,7 @@ impl DockerContainers { Ok(child) => { let id = Self::container_id_from_stdout(child.stdout.unwrap()); - DockerContainer { id, _temp_dir: temp_dir, src_dir, output_dir } + DockerContainer { id, _temp_dir: temp_dir, src_dir, output_dir, terminated: Arc::new(Mutex::new(false)) } } Err(err) => panic!("Error starting container {}: {}", channel.container_name(), err) } @@ -483,19 +499,28 @@ impl DockerContainers { pub fn pop(&self, channel: Channel) -> Result { self.receivers.get(&channel).unwrap().lock().unwrap().recv() } -} - -impl Drop for DockerContainers { - fn drop(&mut self) { + pub fn terminate(&self) { + log::info!("Shutting down docker containers pool..."); let mut active = self.active.lock().unwrap(); *active = false; + let mut terminate_handles = Vec::new(); for (_, receiver) in self.receivers.iter() { - while let Ok(container) = receiver.lock().unwrap().recv_timeout(Duration::new(1, 0)) { - container.terminate(); + while let Ok(mut container) = receiver.lock().unwrap().recv_timeout(Duration::new(1, 0)) { + terminate_handles.push(container.terminate()); } } + for handle in terminate_handles.into_iter() { + handle.join().expect("error when joining"); + } + } +} + +impl Drop for DockerContainers { + + fn drop(&mut self) { + self.terminate(); } } @@ -1447,8 +1472,8 @@ mod test { let containers = DockerContainers::new(0); thread::sleep(Duration::new(2, 0)); - let container = containers.pop(Channel::Stable).unwrap(); + let mut container = containers.pop(Channel::Stable).unwrap(); assert_eq!(false, container.id.is_empty()); - container.terminate(); + container.terminate().join().expect("error when joining"); } }