From 34d12c68cd5f9ebc83e3adf5e1c879c5f4b9a5a6 Mon Sep 17 00:00:00 2001 From: Alfonso Ros Date: Tue, 16 Nov 2021 17:54:46 +0100 Subject: [PATCH] Use separated process for forking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spawning container processes is delegated to a separated 'forker' process. The runtime communicates with this process through a unix domain socket. With this change, northstar's runtime can now execute in multithreaded mode without the danger of the libc deadlocking issue. ┌───────────┐ ┌────────┐ ┌────────────────────────────┐ │ Northstar ├────┤ Forker │ │ Container A │ │ Runtime │ └────┬───┘ │ ┌──────┐ ┌───────────────┐ │ └───────────┘ ├───────┼►│ Init ├─┤ Application A │ │ │ │ └──────┘ └───────────────┘ │ │ └────────────────────────────┘ │ │ ┌────────────────────────────┐ │ │ Container B │ │ │ ┌──────┐ ┌───────────────┐ │ ├───────┼►│ Init ├─┤ Application B │ │ │ │ └──────┘ └───────────────┘ │ │ └────────────────────────────┘ ▼ ... The 'forker' process must consequently be single threaded. Additionally, Init processes handle requests to start new processes inside the container. This is a prerequisite to #454. Additional details ------------------ - Northstar version is bumped to 0.7.0-dev - Panic if the forker process exits unexpectedly If the forker process dies for whatever reason, it is not possible to recoverable and the runtime bails out. - Do not limit the number of threads of the runtime in demo main - Parallel loading of NPKs from disk The loading of NPKs from disk is slow and blocking. Spawn a thread for each NPK in order to speed up the boring parsing of the NPK headers. - Replace manifest IO Pipe with Log The 'pipe' option for the container output is removed from the manifest. The option 'log' is renamed to 'pipe'. A new option 'discard' is added for the output. - Refactor container IO handling When the container IO configuration in the manifest indicates that any of `stdout` or `stderr` is to be 'piped', a socket is used to receive the output from the container. On the other side, the runtime uses a `async` task to forward the incoming output from the socket to the runtime log. - Pipes are removed and replaced with sockets Co-authored-by: Felix Obenhuber Co-authored-by: Alfonso Ros --- .github/workflows/ci.yml | 2 +- Cargo.lock | 10 +- README.md | 2 +- doc/diagrams/container_startup.png | Bin 47544 -> 0 bytes doc/diagrams/container_startup.puml | 33 - examples/console/manifest.yaml | 6 +- examples/cpueater/manifest.yaml | 6 +- examples/cpueater/src/main.rs | 2 +- examples/crashing/manifest.yaml | 8 +- examples/hello-ferris/manifest.yaml | 6 +- examples/hello-resource/manifest.yaml | 6 +- examples/hello-world/manifest.yaml | 6 +- examples/hello-world/src/main.rs | 10 +- examples/inspect/manifest.yaml | 12 +- examples/memeater/manifest.yaml | 6 +- examples/persistence/manifest.yaml | 6 +- examples/seccomp/manifest.yaml | 8 +- images/container-startup.png | Bin 47544 -> 79181 bytes images/container-startup.puml | 62 +- main/Cargo.toml | 2 +- main/src/logger.rs | 62 +- main/src/main.rs | 37 +- northstar-tests/Cargo.toml | 1 + northstar-tests/src/macros.rs | 80 +-- northstar-tests/src/runtime.rs | 161 +++-- northstar-tests/test-container/manifest.yaml | 12 +- northstar-tests/test-container/src/main.rs | 37 +- northstar-tests/tests/examples.rs | 81 +-- northstar-tests/tests/tests.rs | 439 +++++++------- northstar/Cargo.toml | 14 +- northstar/src/api/client.rs | 26 +- northstar/src/api/codec.rs | 125 +--- northstar/src/common/container.rs | 24 +- northstar/src/common/name.rs | 4 +- northstar/src/common/non_null_string.rs | 14 + northstar/src/common/version.rs | 6 + northstar/src/lib.rs | 4 - northstar/src/npk/manifest.rs | 81 ++- northstar/src/runtime/cgroups.rs | 20 +- northstar/src/runtime/config.rs | 31 +- northstar/src/runtime/console.rs | 36 +- northstar/src/runtime/fork/forker/impl.rs | 291 +++++++++ northstar/src/runtime/fork/forker/messages.rs | 42 ++ northstar/src/runtime/fork/forker/mod.rs | 164 +++++ .../{process/fs.rs => fork/init/builder.rs} | 146 +++-- .../{process/init.rs => fork/init/mod.rs} | 321 +++++++--- northstar/src/runtime/fork/mod.rs | 5 + northstar/src/runtime/fork/util.rs | 143 +++++ northstar/src/runtime/io.rs | 133 ++++ northstar/src/runtime/ipc/channel.rs | 201 ------- northstar/src/runtime/ipc/condition.rs | 154 ----- northstar/src/runtime/ipc/message.rs | 423 +++++++++++++ northstar/src/runtime/ipc/mod.rs | 12 +- northstar/src/runtime/ipc/owned_fd.rs | 177 ++++++ northstar/src/runtime/ipc/pipe.rs | 345 ----------- northstar/src/runtime/ipc/raw_fd_ext.rs | 66 +- northstar/src/runtime/ipc/socket_pair.rs | 43 ++ northstar/src/runtime/mod.rs | 285 ++++++--- northstar/src/runtime/mount.rs | 23 +- northstar/src/runtime/process/io.rs | 165 ----- northstar/src/runtime/process/mod.rs | 569 ------------------ northstar/src/runtime/process/trampoline.rs | 26 - northstar/src/runtime/repository.rs | 56 +- northstar/src/runtime/state.rs | 363 ++++++----- northstar/src/seccomp/bpf.rs | 51 +- northstar/src/util.rs | 40 -- tools/nstar/src/main.rs | 19 +- tools/sextant/src/pack.rs | 2 +- tools/stress/Cargo.toml | 2 + tools/stress/src/main.rs | 248 ++++---- 70 files changed, 3238 insertions(+), 2765 deletions(-) delete mode 100644 doc/diagrams/container_startup.png delete mode 100644 doc/diagrams/container_startup.puml create mode 100644 northstar/src/runtime/fork/forker/impl.rs create mode 100644 northstar/src/runtime/fork/forker/messages.rs create mode 100644 northstar/src/runtime/fork/forker/mod.rs rename northstar/src/runtime/{process/fs.rs => fork/init/builder.rs} (67%) rename northstar/src/runtime/{process/init.rs => fork/init/mod.rs} (52%) create mode 100644 northstar/src/runtime/fork/mod.rs create mode 100644 northstar/src/runtime/fork/util.rs create mode 100644 northstar/src/runtime/io.rs delete mode 100644 northstar/src/runtime/ipc/channel.rs delete mode 100644 northstar/src/runtime/ipc/condition.rs create mode 100644 northstar/src/runtime/ipc/message.rs create mode 100644 northstar/src/runtime/ipc/owned_fd.rs delete mode 100644 northstar/src/runtime/ipc/pipe.rs create mode 100644 northstar/src/runtime/ipc/socket_pair.rs delete mode 100644 northstar/src/runtime/process/io.rs delete mode 100644 northstar/src/runtime/process/mod.rs delete mode 100644 northstar/src/runtime/process/trampoline.rs delete mode 100644 northstar/src/util.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19a1a2cd3..7c3f5ed0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,7 +106,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --all-features + args: --all-features -- --test-threads=1 doc: name: Documentation diff --git a/Cargo.lock b/Cargo.lock index 3bf8a8f5a..21202c91e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,9 +422,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" +checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" dependencies = [ "cfg-if", "lazy_static", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "northstar" -version = "0.6.4" +version = "0.7.0-dev" dependencies = [ "anyhow", "async-stream", @@ -1246,6 +1246,7 @@ dependencies = [ "futures", "hex", "humanize-rs", + "humantime", "inotify", "itertools", "lazy_static", @@ -1287,6 +1288,7 @@ dependencies = [ "futures", "lazy_static", "log", + "nanoid", "nix 0.23.1", "northstar", "regex", @@ -2083,6 +2085,8 @@ dependencies = [ "clap", "env_logger 0.9.0", "futures", + "humantime", + "itertools", "log", "northstar", "rand 0.8.5", diff --git a/README.md b/README.md index d06ec4aca..324de824d 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ kernel configuration with the `CONFIG_` entries in the `check_conf.sh` script. ### Container launch sequence -**TODO**:
+
### Manifest Format diff --git a/doc/diagrams/container_startup.png b/doc/diagrams/container_startup.png deleted file mode 100644 index cc4157636bb5165374dbe454accfb3ac26fc38a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47544 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 vP?iw+0%7$e1YBSSGYy)6{s6O(70#$ Runtime: Check and Mount container -create Trampoline -Runtime -> Trampoline: Fork -activate Trampoline -create Init -Trampoline -> Init: Fork -activate Init -Trampoline -> Runtime: Init PID -destroy Trampoline -Runtime -> Runtime: Wait for Trampoline exit (waitpid) -Init -> Init: Wait for run signal (Condition::wait) -Runtime -> Runtime: Configure cgroups -Runtime -> Init: Signal run (Condition::notify) -Runtime -> Runtime: Wait for execve (Condition::wait) -Init -> Init: Mount, Chroot, UID / GID,\ndrop privileges, file descriptors -create Container -Init -> Container: Fork -activate Container -Init -> Init: Wait for container to exit (waitpid) -Container -> Container: Set seccomp filter -Container -> : Execve(..) -Runtime -> Runtime: Condition pipe closed: Container is started -note left: Condition pipe is CLOEXEC -Container -> Init: Exit -destroy Container -Init -> Runtime: Exit -Runtime -> Runtime: Read exit status from pipe or waitpid on pid of init -destroy Init - -@enduml diff --git a/examples/console/manifest.yaml b/examples/console/manifest.yaml index 0592ddadc..e80a4dad8 100644 --- a/examples/console/manifest.yaml +++ b/examples/console/manifest.yaml @@ -5,10 +5,8 @@ console: true uid: 1000 gid: 1000 io: - stdout: - log: - level: DEBUG - tag: console + stdout: pipe + stderr: pipe mounts: /dev: type: dev diff --git a/examples/cpueater/manifest.yaml b/examples/cpueater/manifest.yaml index 07d7e716e..eceb048f6 100644 --- a/examples/cpueater/manifest.yaml +++ b/examples/cpueater/manifest.yaml @@ -24,7 +24,5 @@ mounts: type: bind host: /system io: - stdout: - log: - level: DEBUG - tag: cpueater + stdout: pipe + stderr: pipe diff --git a/examples/cpueater/src/main.rs b/examples/cpueater/src/main.rs index 3ed4fbd5e..40c0de44a 100644 --- a/examples/cpueater/src/main.rs +++ b/examples/cpueater/src/main.rs @@ -1,7 +1,7 @@ use std::env::var; fn main() { - let version = var("VERSION").expect("Failed to read VERSION"); + let version = var("NORTHSTAR_VERSION").expect("Failed to read NORTHSTAR_VERSION"); let threads = var("THREADS") .expect("Failed to read THREADS") .parse::() diff --git a/examples/crashing/manifest.yaml b/examples/crashing/manifest.yaml index f83430406..d6b5a6c65 100644 --- a/examples/crashing/manifest.yaml +++ b/examples/crashing/manifest.yaml @@ -5,6 +5,9 @@ uid: 1000 gid: 1000 env: RUST_BACKTRACE: 1 +io: + stdout: pipe + stderr: discard mounts: /dev: type: dev @@ -19,8 +22,3 @@ mounts: /system: type: bind host: /system -io: - stdout: - log: - level: DEBUG - tag: crashing diff --git a/examples/hello-ferris/manifest.yaml b/examples/hello-ferris/manifest.yaml index 6a249e20b..b8273d7a0 100644 --- a/examples/hello-ferris/manifest.yaml +++ b/examples/hello-ferris/manifest.yaml @@ -37,7 +37,5 @@ mounts: dir: / options: noexec,nodev,nosuid io: - stdout: - log: - level: DEBUG - tag: ferris + stdout: pipe + stderr: pipe diff --git a/examples/hello-resource/manifest.yaml b/examples/hello-resource/manifest.yaml index 045168037..e4533d3c7 100644 --- a/examples/hello-resource/manifest.yaml +++ b/examples/hello-resource/manifest.yaml @@ -23,7 +23,5 @@ mounts: type: bind host: /system io: - stdout: - log: - level: DEBUG - tag: hello + stdout: pipe + stderr: pipe diff --git a/examples/hello-world/manifest.yaml b/examples/hello-world/manifest.yaml index e8cfc98de..49ce0cca6 100644 --- a/examples/hello-world/manifest.yaml +++ b/examples/hello-world/manifest.yaml @@ -6,10 +6,8 @@ gid: 1000 env: HELLO: northstar io: - stdout: - log: - level: DEBUG - tag: hello + stdout: pipe + stderr: pipe mounts: /dev: type: dev diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs index 4de022188..ae428f8ca 100644 --- a/examples/hello-world/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -1,13 +1,9 @@ fn main() { - let hello = std::env::var("HELLO").unwrap_or_else(|_| "unknown".into()); - let version = std::env::var("VERSION").unwrap_or_else(|_| "unknown".into()); + let hello = std::env::var("NORTHSTAR_CONTAINER").unwrap_or_else(|_| "unknown".into()); - println!("Hello again {} from version {}!", hello, version); + println!("Hello again {}!", hello); for i in 0..u64::MAX { - println!( - "...and hello again #{} {} from version {}...", - i, hello, version - ); + println!("...and hello again #{} {} ...", i, hello); std::thread::sleep(std::time::Duration::from_secs(1)); } } diff --git a/examples/inspect/manifest.yaml b/examples/inspect/manifest.yaml index 726a2c8b9..3307b025f 100644 --- a/examples/inspect/manifest.yaml +++ b/examples/inspect/manifest.yaml @@ -1,17 +1,11 @@ -name: inspect +name: inspect version: 0.0.1 init: /inspect uid: 1000 gid: 1000 io: - stdout: - log: - level: DEBUG - tag: inspect - stderr: - log: - level: WARN - tag: inspect + stdout: pipe + stderr: discard mounts: /dev: type: dev diff --git a/examples/memeater/manifest.yaml b/examples/memeater/manifest.yaml index 5e9059ce4..7b2e27c0b 100644 --- a/examples/memeater/manifest.yaml +++ b/examples/memeater/manifest.yaml @@ -23,7 +23,5 @@ mounts: type: bind host: /system io: - stdout: - log: - level: DEBUG - tag: memeater + stdout: pipe + stderr: pipe diff --git a/examples/persistence/manifest.yaml b/examples/persistence/manifest.yaml index 2e5595de4..19dda8c4e 100644 --- a/examples/persistence/manifest.yaml +++ b/examples/persistence/manifest.yaml @@ -20,7 +20,5 @@ mounts: type: bind host: /system io: - stdout: - log: - level: DEBUG - tag: persistence + stdout: pipe + stderr: pipe diff --git a/examples/seccomp/manifest.yaml b/examples/seccomp/manifest.yaml index 3fb514724..415624770 100644 --- a/examples/seccomp/manifest.yaml +++ b/examples/seccomp/manifest.yaml @@ -18,10 +18,8 @@ mounts: type: bind host: /system io: - stdout: - log: - level: DEBUG - tag: seccomp + stdout: pipe + stderr: pipe seccomp: profile: - default \ No newline at end of file + default diff --git a/images/container-startup.png b/images/container-startup.png index cc4157636bb5165374dbe454accfb3ac26fc38a4..e698a46169f53a1a85dc27731be18ee3cef2563e 100644 GIT binary patch literal 79181 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 B2QvTw literal 47544 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 vP?iw+0%7$e1YBSSGYy)6{s6O(70#$ Runtime: Check and Mount container + +create Forker +Runtime -> Forker: Fork +activate Forker + +Client -> Runtime: Connect: Hello +Client <- Runtime: ConnectAck +Client -> Runtime: Start container +Runtime -> Runtime: Check and mount container(s) +Runtime -> Runtime: Open PTY + +Runtime -> Forker: Create container + create Trampoline -Runtime -> Trampoline: Fork +Forker -> Trampoline: Fork activate Trampoline +Trampoline -> Trampoline: Create PID namespace + create Init Trampoline -> Init: Fork activate Init -Trampoline -> Runtime: Init PID +Init -> Init: Mount, Chroot, UID / GID,\ndrop privileges, file descriptors + +Trampoline -> Forker: Forked init with PID destroy Trampoline -Runtime -> Runtime: Wait for Trampoline exit (waitpid) -Init -> Init: Wait for run signal (Condition::wait) + +Forker -> Forker: reap Trampoline + +Forker -> Runtime: Created init with PID + Runtime -> Runtime: Configure cgroups -Runtime -> Init: Signal run (Condition::notify) -Runtime -> Runtime: Wait for execve (Condition::wait) -Init -> Init: Mount, Chroot, UID / GID,\ndrop privileges, file descriptors +Runtime -> Runtime: Configure debug +Runtime -> Runtime: Configure PTY forward + +Runtime -> Forker: Exec container +Forker -> Init: Exec Container create Container Init -> Container: Fork activate Container +Forker <- Init: Exec +Runtime <- Forker: Exec +Client <- Runtime: Started +Client <- Runtime: Notification: Started + Init -> Init: Wait for container to exit (waitpid) +Container -> Container: Setup PTY Container -> Container: Set seccomp filter Container -> : Execve(..) -Runtime -> Runtime: Condition pipe closed: Container is started -note left: Condition pipe is CLOEXEC -Container -> Init: Exit +... +Container -> Init: SIGCHLD destroy Container -Init -> Runtime: Exit -Runtime -> Runtime: Read exit status from pipe or waitpid on pid of init + +Init -> Init: waitpid: Exit status of container +Init -> Forker: Container exit status destroy Init +Forker -> Runtime: Container exit status +Runtime -> Runtime: Stop PTY thread +Runtime -> Runtime: Destroy cgroups +Client <- Runtime: Notification: Exit + @enduml diff --git a/main/Cargo.toml b/main/Cargo.toml index f321b6b7a..0070c2cb8 100644 --- a/main/Cargo.toml +++ b/main/Cargo.toml @@ -16,7 +16,7 @@ clap = { version = "3.1.0", features = ["derive"] } log = "0.4.14" nix = "0.23.0" northstar = { path = "../northstar", features = ["runtime"] } -tokio = { version = "1.17.0", features = ["rt", "macros", "signal"] } +tokio = { version = "1.17.0", features = ["rt-multi-thread", "macros", "signal"] } toml = "0.5.8" [target.'cfg(not(target_os = "android"))'.dependencies] diff --git a/main/src/logger.rs b/main/src/logger.rs index a3a486779..b9ee80617 100644 --- a/main/src/logger.rs +++ b/main/src/logger.rs @@ -9,7 +9,7 @@ pub fn init() { } #[cfg(not(target_os = "android"))] -static TAG_SIZE: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(20); +static TAG_SIZE: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(28); /// Initialize the logger #[cfg(not(target_os = "android"))] @@ -17,51 +17,63 @@ pub fn init() { use env_logger::fmt::Color; use std::{io::Write, sync::atomic::Ordering}; + fn color(target: &str) -> Color { + // Some colors are hard to read on (at least) dark terminals + // and I consider some others as ugly ;-) + let hash = target.bytes().fold(42u8, |c, x| c ^ x); + Color::Ansi256(match hash { + c @ 0..=1 => c + 2, + c @ 16..=21 => c + 6, + c @ 52..=55 | c @ 126..=129 => c + 4, + c @ 163..=165 | c @ 200..=201 => c + 3, + c @ 207 => c + 1, + c @ 232..=240 => c + 9, + c => c, + }) + } + let mut builder = env_logger::Builder::new(); builder.parse_filters("northstar=debug"); builder.format(|buf, record| { - let mut style = buf.style(); + let timestamp = buf.timestamp_millis().to_string(); + let timestamp = timestamp.strip_suffix('Z').unwrap(); + + let mut level = buf.default_level_style(record.metadata().level()); + level.set_bold(true); + let level = level.value(record.metadata().level().as_str()); - let timestamp = buf.timestamp_millis(); - let level = buf.default_styled_level(record.metadata().level()); + let pid = std::process::id().to_string(); + let mut pid_style = buf.style(); + pid_style.set_color(color(&pid)); - if let Some(module_path) = record - .module_path() + if let Some(target) = Option::from(record.target().is_empty()) + .map(|_| record.target()) + .or_else(|| record.module_path()) .and_then(|module_path| module_path.find(&"::").map(|p| &module_path[p + 2..])) { - TAG_SIZE.fetch_max(module_path.len(), Ordering::SeqCst); + let mut tag_style = buf.style(); + TAG_SIZE.fetch_max(target.len(), Ordering::SeqCst); let tag_size = TAG_SIZE.load(Ordering::SeqCst); - fn hashed_color(i: &str) -> Color { - // Some colors are hard to read on (at least) dark terminals - // and I consider some others as ugly ;-) - Color::Ansi256(match i.bytes().fold(42u8, |c, x| c ^ x) { - c @ 0..=1 => c + 2, - c @ 16..=21 => c + 6, - c @ 52..=55 | c @ 126..=129 => c + 4, - c @ 163..=165 | c @ 200..=201 => c + 3, - c @ 207 => c + 1, - c @ 232..=240 => c + 9, - c => c, - }) - } - style.set_color(hashed_color(module_path)); + tag_style.set_color(color(target)); writeln!( buf, - "{}: {:>s$} {:<5}: {}", + "{} {:>s$} {} {:<5}: {}", timestamp, - style.value(module_path), + tag_style.value(target), + pid_style.value("⬤"), level, record.args(), - s = tag_size + s = tag_size, ) } else { writeln!( buf, - "{}: {} {:<5}: {}", + "{} {} {} {:<5}: {}", timestamp, " ".repeat(TAG_SIZE.load(Ordering::SeqCst)), + pid_style.value("⬤"), level, record.args(), ) diff --git a/main/src/main.rs b/main/src/main.rs index dba0c49f2..80547ae73 100644 --- a/main/src/main.rs +++ b/main/src/main.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Error}; use clap::Parser; use log::{debug, info, warn}; use nix::mount::MsFlags; -use northstar::runtime; +use northstar::{runtime, runtime::Runtime as Northstar}; use runtime::config::Config; use std::{ fs::{self, read_to_string}, @@ -32,16 +32,31 @@ struct Opt { pub disable_mount_namespace: bool, } -#[tokio::main(flavor = "current_thread")] -async fn main() -> Result<(), Error> { +fn main() -> Result<(), Error> { + // Initialize logging + logger::init(); + + // Parse command line arguments and prepare the environment + let config = init()?; + + // Create the runtime launcher. This must be done *before* spawning the tokio threadpool. + let northstar = Northstar::new(config)?; + + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .thread_name("northstar") + .build() + .context("Failed to create runtime")? + .block_on(run(northstar)) +} + +fn init() -> Result { let opt = Opt::parse(); let config = read_to_string(&opt.config) .with_context(|| format!("Failed to read configuration file {}", opt.config.display()))?; let config: Config = toml::from_str(&config) .with_context(|| format!("Failed to read configuration file {}", opt.config.display()))?; - logger::init(); - fs::create_dir_all(&config.run_dir).context("Failed to create run_dir")?; fs::create_dir_all(&config.data_dir).context("Failed to create data_dir")?; fs::create_dir_all(&config.log_dir).context("Failed to create log dir")?; @@ -64,9 +79,15 @@ async fn main() -> Result<(), Error> { debug!("Mount namespace is disabled"); } - let mut runtime = runtime::Runtime::start(config) + Ok(config) +} + +async fn run(northstar: Northstar) -> Result<(), Error> { + let mut runtime = northstar + .start() .await - .context("Failed to start runtime")?; + .context("Failed to start Northstar")?; + let mut sigint = tokio::signal::unix::signal(SignalKind::interrupt()) .context("Failed to install sigint handler")?; let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate()) @@ -87,7 +108,7 @@ async fn main() -> Result<(), Error> { info!("Received SIGHUP. Stopping Northstar runtime"); runtime.shutdown().await } - status = &mut runtime => status, + status = runtime.stopped() => status, }; match status { diff --git a/northstar-tests/Cargo.toml b/northstar-tests/Cargo.toml index c73648d94..309c2e3ac 100644 --- a/northstar-tests/Cargo.toml +++ b/northstar-tests/Cargo.toml @@ -11,6 +11,7 @@ env_logger = "0.9.0" futures = "0.3.21" lazy_static = "1.4.0" log = "0.4.14" +nanoid = "0.4.0" nix = "0.23.0" northstar = { path = "../northstar", features = ["api", "runtime"] } regex = "1.5.4" diff --git a/northstar-tests/src/macros.rs b/northstar-tests/src/macros.rs index aab9a3d8d..e5e78bb5f 100644 --- a/northstar-tests/src/macros.rs +++ b/northstar-tests/src/macros.rs @@ -1,38 +1,3 @@ -use super::logger; -use nix::{mount, sched}; -use sched::{unshare, CloneFlags}; - -pub fn init() { - logger::init(); - log::set_max_level(log::LevelFilter::Debug); - - // Enter a mount namespace. This needs to be done before spawning - // the tokio threadpool. - unshare(CloneFlags::CLONE_NEWNS).unwrap(); - - // Set the mount propagation to private on root. This ensures that *all* - // mounts get cleaned up upon process termination. The approach to bind - // mount the run_dir only (this is where the mounts from northstar happen) - // doesn't work for the tests since the run_dir is a tempdir which is a - // random dir on every run. Checking at the beginning of the tests if - // run_dir is bind mounted - a leftover from a previous crash - obviously - // doesn't work. Technically, it is only necessary set the propagation of - // the parent mount of the run_dir, but this not easy to find and the change - // of mount propagation on root is fine for the tests which are development - // only. - mount::mount( - Some("/"), - "/", - Option::<&str>::None, - mount::MsFlags::MS_PRIVATE | mount::MsFlags::MS_REC, - Option::<&'static [u8]>::None, - ) - .expect( - "Failed to set mount propagation to private on - root", - ); -} - /// Northstar integration test #[macro_export] macro_rules! test { @@ -41,15 +6,52 @@ macro_rules! test { #![rusty_fork(timeout_ms = 300000)] #[test] fn $name() { - northstar_tests::macros::init(); - match tokio::runtime::Builder::new_current_thread() + crate::logger::init(); + log::set_max_level(log::LevelFilter::Debug); + + // Enter a mount namespace. This needs to be done before spawning + // the tokio threadpool. + nix::sched::unshare(nix::sched::CloneFlags::CLONE_NEWNS).unwrap(); + + // Set the mount propagation to private on root. This ensures that *all* + // mounts get cleaned up upon process termination. The approach to bind + // mount the run_dir only (this is where the mounts from northstar happen) + // doesn't work for the tests since the run_dir is a tempdir which is a + // random dir on every run. Checking at the beginning of the tests if + // run_dir is bind mounted - a leftover from a previous crash - obviously + // doesn't work. Technically, it is only necessary set the propagation of + // the parent mount of the run_dir, but this not easy to find and the change + // of mount propagation on root is fine for the tests which are development + // only. + nix::mount::mount( + Some("/"), + "/", + Option::<&str>::None, + nix::mount::MsFlags::MS_PRIVATE | nix::mount::MsFlags::MS_REC, + Option::<&'static [u8]>::None, + ) + .expect( + "Failed to set mount propagation to private on + root", + ); + let runtime = northstar_tests::runtime::Runtime::new().expect("Failed to start runtime"); + + match tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) .enable_all() .thread_name(stringify!($name)) .build() .expect("Failed to start runtime") - .block_on(async { $e }) { + .block_on(async { + let runtime = runtime.start().await?; + $e + northstar_tests::runtime::client().shutdown().await?; + drop(runtime); + tokio::fs::remove_file(northstar_tests::runtime::console().path()).await?; + Ok(()) + }) { Ok(_) => std::process::exit(0), - Err(e) => panic!("{}", e), + anyhow::Result::<()>::Err(e) => panic!("{}", e), } } } diff --git a/northstar-tests/src/runtime.rs b/northstar-tests/src/runtime.rs index 018a4cf4c..aba9d230d 100644 --- a/northstar-tests/src/runtime.rs +++ b/northstar-tests/src/runtime.rs @@ -3,15 +3,16 @@ use super::{containers::*, logger}; use anyhow::{anyhow, Context, Result}; use futures::StreamExt; +use nanoid::nanoid; use northstar::{ api::{ - client::Client, + client, model::{Container, ExitStatus, Notification}, }, common::non_null_string::NonNullString, runtime::{ - self, config::{self, Config, RepositoryType}, + Runtime as Northstar, }, }; use std::{ @@ -21,49 +22,35 @@ use std::{ use tempfile::{NamedTempFile, TempDir}; use tokio::{fs, net::UnixStream, pin, select, time}; -pub struct Northstar { - /// Runtime configuration - pub config: Config, - /// Runtime console address (Unix socket) - pub console: String, - /// Client instance - client: northstar::api::client::Client, - /// Runtime instance - runtime: runtime::Runtime, - /// Tmpdir for NPK dumps - tmpdir: TempDir, -} +pub static mut CLIENT: Option = None; -impl std::ops::Deref for Northstar { - type Target = Client; +pub fn client() -> &'static mut Client { + unsafe { CLIENT.as_mut().unwrap() } +} - fn deref(&self) -> &Self::Target { - &self.client - } +pub fn console() -> url::Url { + let console = std::env::temp_dir().join(format!("northstar-{}", std::process::id())); + url::Url::parse(&format!("unix://{}", console.display())).unwrap() } -impl std::ops::DerefMut for Northstar { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.client - } +pub enum Runtime { + Created(Northstar, TempDir), + Started(Northstar, TempDir), } -impl Northstar { - /// Launches an instance of Northstar - pub async fn launch() -> Result { - let pid = std::process::id(); +impl Runtime { + pub fn new() -> Result { let tmpdir = tempfile::Builder::new().prefix("northstar-").tempdir()?; - let run_dir = tmpdir.path().join("run"); - fs::create_dir(&run_dir).await?; + std::fs::create_dir(&run_dir)?; let data_dir = tmpdir.path().join("data"); - fs::create_dir(&data_dir).await?; + std::fs::create_dir(&data_dir)?; let log_dir = tmpdir.path().join("log"); - fs::create_dir(&log_dir).await?; + std::fs::create_dir(&log_dir)?; let test_repository = tmpdir.path().join("test"); - fs::create_dir(&test_repository).await?; + std::fs::create_dir(&test_repository)?; let example_key = tmpdir.path().join("key.pub"); - fs::write(&example_key, include_bytes!("../../examples/northstar.pub")).await?; + std::fs::write(&example_key, include_bytes!("../../examples/northstar.pub"))?; let mut repositories = HashMap::new(); repositories.insert( @@ -77,71 +64,84 @@ impl Northstar { "test-1".into(), config::Repository { r#type: RepositoryType::Memory, - key: Some(example_key.clone()), + key: Some(example_key), }, ); - let console = format!( - "{}/northstar-{}", - tmpdir.path().display(), - std::process::id() - ); - let console_url = url::Url::parse(&format!("unix://{}", console))?; - let config = Config { - console: Some(vec![console_url.clone()]), + console: Some(vec![console()]), run_dir, - data_dir: data_dir.clone(), + data_dir, log_dir, mount_parallel: 10, - cgroup: NonNullString::try_from(format!("northstar-{}", pid)).unwrap(), + cgroup: NonNullString::try_from(format!("northstar-{}", nanoid!())).unwrap(), repositories, debug: None, }; + let b = Northstar::new(config)?; - // Start the runtime - let runtime = runtime::Runtime::start(config.clone()) - .await - .context("Failed to start runtime")?; - // Wait until the console is up and running - super::logger::assume("Started console on", 5u64).await?; + Ok(Runtime::Created(b, tmpdir)) + } + + pub async fn start(self) -> Result { + if let Runtime::Created(launcher, tmpdir) = self { + let runtime = launcher.start().await?; + logger::assume("Runtime up and running", 10u64).await?; + + unsafe { + CLIENT = Some(Client::new().await?); + } + + Ok(Runtime::Started(runtime, tmpdir)) + } else { + anyhow::bail!("Runtime is already started") + } + } +} + +pub struct Client { + /// Client instance + client: northstar::api::client::Client, +} + +impl std::ops::Deref for Client { + type Target = client::Client; + fn deref(&self) -> &Self::Target { + &self.client + } +} + +impl std::ops::DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.client + } +} + +impl Client { + /// Launches an instance of Northstar + pub async fn new() -> Result { // Connect to the runtime - let io = UnixStream::connect(&console) + let io = UnixStream::connect(console().path()) .await .expect("Failed to connect to console"); - let client = Client::new(io, Some(1000), time::Duration::from_secs(30)).await?; + let client = client::Client::new(io, Some(1000), time::Duration::from_secs(30)).await?; // Wait until a successful connection logger::assume("Client .* connected", 5u64).await?; - Ok(Northstar { - config, - console, - client, - runtime, - tmpdir, - }) + Ok(Client { client }) } /// Connect a new client instance to the runtime - pub async fn client(&self) -> Result> { - let io = UnixStream::connect(&self.console) + pub async fn client(&self) -> Result> { + let io = UnixStream::connect(console().path()) .await .context("Failed to connect to console")?; - Client::new(io, Some(1000), time::Duration::from_secs(30)) + client::Client::new(io, Some(1000), time::Duration::from_secs(30)) .await .context("Failed to create client") } - /// Launches an instance of Northstar with the test container and - /// resource installed. - pub async fn launch_install_test_container() -> Result { - let mut runtime = Self::launch().await?; - runtime.install_test_resource().await?; - runtime.install_test_container().await?; - Ok(runtime) - } - pub async fn stop(&mut self, container: &str, timeout: u64) -> Result<()> { self.client.kill(container, 15).await?; let container: Container = container.try_into()?; @@ -158,26 +158,15 @@ impl Northstar { Ok(()) } - pub async fn shutdown(self) -> Result<()> { - // Dropping the client closes the connection to the runtime - drop(self.client); - - // Stop the runtime - self.runtime - .shutdown() - .await - .context("Failed to stop the runtime")?; - - logger::assume("Closed listener", 5u64).await?; - - // Remove the tmpdir - self.tmpdir.close().expect("Failed to remove tmpdir"); + pub async fn shutdown(&mut self) -> Result<()> { + drop(self.client.shutdown().await); + logger::assume("Shutdown complete", 5u64).await?; Ok(()) } // Install a npk from a buffer pub async fn install(&mut self, npk: &[u8], repository: &str) -> Result<()> { - let f = NamedTempFile::new_in(self.tmpdir.path())?; + let f = NamedTempFile::new()?; fs::write(&f, npk).await?; self.client.install(f.path(), repository).await?; Ok(()) diff --git a/northstar-tests/test-container/manifest.yaml b/northstar-tests/test-container/manifest.yaml index c28409031..2487a077d 100644 --- a/northstar-tests/test-container/manifest.yaml +++ b/northstar-tests/test-container/manifest.yaml @@ -3,6 +3,9 @@ version: 0.0.1 init: /test-container uid: 1000 gid: 1000 +io: + stdout: pipe + stderr: pipe # cgroups: # memory: # limit_in_bytes: 10000000 @@ -35,15 +38,6 @@ mounts: version: 0.0.1 dir: test options: nosuid,nodev,noexec -io: - stdout: - log: - level: DEBUG - tag: test-container - stderr: - log: - level: DEBUG - tag: test-container rlimits: nproc: soft: 10000 diff --git a/northstar-tests/test-container/src/main.rs b/northstar-tests/test-container/src/main.rs index 4d2e68b7b..def03d394 100644 --- a/northstar-tests/test-container/src/main.rs +++ b/northstar-tests/test-container/src/main.rs @@ -18,6 +18,22 @@ struct Opt { command: Option, } +#[derive(Debug)] +enum Io { + Stdout, + Stderr, +} + +impl From<&str> for Io { + fn from(s: &str) -> Io { + match s { + "stdout" => Io::Stdout, + "stderr" => Io::Stderr, + _ => panic!("Invalid io: {}", s), + } + } +} + #[derive(Debug, Parser)] enum Command { Cat { @@ -25,13 +41,15 @@ enum Command { path: PathBuf, }, Crash, - Echo { - message: Vec, - }, Exit { code: i32, }, Inspect, + Print { + message: String, + #[structopt(short, long, parse(from_str), default_value = "stdout")] + io: Io, + }, Touch { path: PathBuf, }, @@ -49,15 +67,15 @@ fn main() -> Result<()> { let command = Opt::parse().command.unwrap_or(Command::Sleep); println!("Executing \"{:?}\"", command); match command { + Command::CallDeleteModule { flags } => call_delete_module(flags)?, Command::Cat { path } => cat(&path)?, Command::Crash => crash(), - Command::Echo { message } => echo(&message), Command::Exit { code } => exit(code), Command::Inspect => inspect(), - Command::Touch { path } => touch(&path)?, + Command::Print { message, io } => print(&message, &io), Command::Sleep => (), + Command::Touch { path } => touch(&path)?, Command::Write { message, path } => write(&message, path.as_path())?, - Command::CallDeleteModule { flags } => call_delete_module(flags)?, }; sleep(); @@ -92,8 +110,11 @@ fn crash() { panic!("witness me!"); } -fn echo(message: &[String]) { - println!("{}", message.join(" ")); +fn print(message: &str, io: &Io) { + match io { + Io::Stdout => println!("{}", message), + Io::Stderr => eprintln!("{}", message), + } } fn exit(code: i32) { diff --git a/northstar-tests/tests/examples.rs b/northstar-tests/tests/examples.rs index 836445616..718917e2a 100644 --- a/northstar-tests/tests/examples.rs +++ b/northstar-tests/tests/examples.rs @@ -1,13 +1,12 @@ use logger::assume; use northstar::api::model::{ExitStatus, Notification}; -use northstar_tests::{containers::*, logger, runtime::Northstar, test}; +use northstar_tests::{containers::*, logger, runtime::client, test}; // Start crashing example test!(crashing, { - let mut runtime = Northstar::launch().await?; - runtime.install(EXAMPLE_CRASHING_NPK, "test-0").await?; - runtime.start(EXAMPLE_CRASHING).await?; - runtime + client().install(EXAMPLE_CRASHING_NPK, "test-0").await?; + client().start(EXAMPLE_CRASHING).await?; + client() .assume_notification( |n| { matches!( @@ -21,43 +20,39 @@ test!(crashing, { 20, ) .await?; - runtime.shutdown().await }); // Start console example test!(console, { - let mut runtime = Northstar::launch().await?; - runtime.install(EXAMPLE_CONSOLE_NPK, "test-0").await?; - runtime.start(EXAMPLE_CONSOLE).await?; + client().install(EXAMPLE_CONSOLE_NPK, "test-0").await?; + client().start(EXAMPLE_CONSOLE).await?; // The console example stop itself - so wait for it... assume("Client console:0.0.1 connected", 5).await?; assume("Killing console:0.0.1 with SIGTERM", 5).await?; - runtime.shutdown().await }); // Start cpueater example and assume log message test!(cpueater, { - let mut runtime = Northstar::launch().await?; - runtime.install(EXAMPLE_CPUEATER_NPK, "test-0").await?; - runtime.start(EXAMPLE_CPUEATER).await?; + client().install(EXAMPLE_CPUEATER_NPK, "test-0").await?; + client().start(EXAMPLE_CPUEATER).await?; assume("Eating CPU", 5).await?; - runtime.stop(EXAMPLE_CPUEATER, 10).await?; - runtime.shutdown().await + client().stop(EXAMPLE_CPUEATER, 10).await?; }); // Start hello-ferris example test!(hello_ferris, { - let mut runtime = Northstar::launch().await?; - runtime.install(EXAMPLE_FERRIS_NPK, "test-0").await?; - runtime.install(EXAMPLE_MESSAGE_0_0_1_NPK, "test-0").await?; - runtime.install(EXAMPLE_HELLO_FERRIS_NPK, "test-0").await?; - runtime.start(EXAMPLE_HELLO_FERRIS).await?; + client().install(EXAMPLE_FERRIS_NPK, "test-0").await?; + client() + .install(EXAMPLE_MESSAGE_0_0_1_NPK, "test-0") + .await?; + client().install(EXAMPLE_HELLO_FERRIS_NPK, "test-0").await?; + client().start(EXAMPLE_HELLO_FERRIS).await?; assume("Hello once more from 0.0.1!", 5).await?; // The hello-ferris example terminates after printing something. - // Wait for the notification that it stopped, otherwise the runtime + // Wait for the notification that it stopped, otherwise the client() // will try to shutdown the application which is already exited. - runtime + client() .assume_notification( |n| { matches!( @@ -71,18 +66,17 @@ test!(hello_ferris, { 15, ) .await?; - - runtime.shutdown().await }); // Start hello-resource example test!(hello_resource, { - let mut runtime = Northstar::launch().await?; - runtime.install(EXAMPLE_MESSAGE_0_0_2_NPK, "test-0").await?; - runtime + client() + .install(EXAMPLE_MESSAGE_0_0_2_NPK, "test-0") + .await?; + client() .install(EXAMPLE_HELLO_RESOURCE_NPK, "test-0") .await?; - runtime.start(EXAMPLE_HELLO_RESOURCE).await?; + client().start(EXAMPLE_HELLO_RESOURCE).await?; assume( "0: Content of /message/hello: Hello once more from v0.0.2!", 5, @@ -93,42 +87,33 @@ test!(hello_resource, { 5, ) .await?; - runtime.shutdown().await }); // Start inspect example test!(inspect, { - let mut runtime = Northstar::launch().await?; - runtime.install(EXAMPLE_INSPECT_NPK, "test-0").await?; - runtime.start(EXAMPLE_INSPECT).await?; - runtime.stop(EXAMPLE_INSPECT, 5).await?; - // TODO - runtime.shutdown().await + client().install(EXAMPLE_INSPECT_NPK, "test-0").await?; + client().start(EXAMPLE_INSPECT).await?; + client().stop(EXAMPLE_INSPECT, 5).await?; }); // Start memeater example // test!(memeater, { -// let mut runtime = Northstar::launch().await?; -// runtime.install(&EXAMPLE_MEMEATER_NPK, "test-0").await?; -// runtime.start(EXAMPLE_MEMEATER).await?; -// assume("Process memeater:0.0.1 is out of memory", 20).await?; -// runtime.shutdown().await +// let mut client() = Northstar::launch().await?; +// client().install(&EXAMPLE_MEMEATER_NPK, "test-0").await?; +// client().start(EXAMPLE_MEMEATER).await?; +// assume("Process memeater:0.0.1 is out of memory", 20).await // }); // Start persistence example and check output test!(persistence, { - let mut runtime = Northstar::launch().await?; - runtime.install(EXAMPLE_PERSISTENCE_NPK, "test-0").await?; - runtime.start(EXAMPLE_PERSISTENCE).await?; + client().install(EXAMPLE_PERSISTENCE_NPK, "test-0").await?; + client().start(EXAMPLE_PERSISTENCE).await?; assume("Writing Hello! to /data/file", 5).await?; assume("Content of /data/file: Hello!", 5).await?; - runtime.shutdown().await }); // Start seccomp example test!(seccomp, { - let mut runtime = Northstar::launch().await?; - runtime.install(EXAMPLE_SECCOMP_NPK, "test-0").await?; - runtime.start(EXAMPLE_SECCOMP).await?; - runtime.shutdown().await + client().install(EXAMPLE_SECCOMP_NPK, "test-0").await?; + client().start(EXAMPLE_SECCOMP).await?; }); diff --git a/northstar-tests/tests/tests.rs b/northstar-tests/tests/tests.rs index f47403866..505c67556 100644 --- a/northstar-tests/tests/tests.rs +++ b/northstar-tests/tests/tests.rs @@ -1,4 +1,5 @@ -use anyhow::Result; +use std::path::{Path, PathBuf}; + use futures::{SinkExt, StreamExt}; use log::debug; use logger::assume; @@ -6,8 +7,7 @@ use northstar::api::{ self, model::{self, ConnectNack, ExitStatus, Notification}, }; -use northstar_tests::{containers::*, logger, runtime::Northstar, test}; -use std::path::{Path, PathBuf}; +use northstar_tests::{containers::*, logger, runtime::client, test}; use tokio::{ io::{AsyncRead, AsyncWrite}, net::UnixStream, @@ -18,175 +18,198 @@ test!(logger_smoketest, { debug!("Yippie"); assume("Yippie", 3).await?; assert!(assume("Juhuuu!", 1).await.is_err()); - Result::<()>::Ok(()) -}); - -// Smoke test the runtime startup and shutdown -test!(runtime_launch, { - Northstar::launch().await?.shutdown().await }); // Install and uninstall is a loop. After a number of installation // try to start the test container test!(install_uninstall_test_container, { - let mut runtime = Northstar::launch().await?; for _ in 0u32..10 { - runtime.install_test_container().await?; - runtime.uninstall_test_container().await?; + client().install_test_container().await?; + client().uninstall_test_container().await?; } - runtime.shutdown().await }); // Install a container that already exists with the same name and version test!(install_duplicate, { - let mut runtime = Northstar::launch().await?; - runtime.install_test_container().await?; - assert!(runtime.install_test_container().await.is_err()); - runtime.shutdown().await + client().install_test_container().await?; + client().install_test_resource().await?; + assert!(client().install_test_container().await.is_err()); }); // Install a container that already exists in another repository test!(install_duplicate_other_repository, { - let mut runtime = Northstar::launch().await?; - runtime.install(TEST_CONTAINER_NPK, "test-0").await?; - assert!(runtime.install(TEST_CONTAINER_NPK, "test-1").await.is_err()); - runtime.shutdown().await + client().install(TEST_CONTAINER_NPK, "test-0").await?; + assert!(client() + .install(TEST_CONTAINER_NPK, "test-1") + .await + .is_err()); }); // Start and stop a container multiple times test!(start_stop, { - let mut runtime = Northstar::launch_install_test_container().await?; + client().install_test_container().await?; + client().install_test_resource().await?; for _ in 0..10u32 { - runtime.start_with_args(TEST_CONTAINER, ["sleep"]).await?; + client().start_with_args(TEST_CONTAINER, ["sleep"]).await?; assume("Sleeping", 5u64).await?; - runtime.stop(TEST_CONTAINER, 5).await?; + client().stop(TEST_CONTAINER, 5).await?; assume("Process test-container:0.0.1 exited", 5).await?; } +}); - runtime.shutdown().await +// Install and uninsteall the example npks +test!(install_uninstall_examples, { + client().install(EXAMPLE_CPUEATER_NPK, "test-0").await?; + client().install(EXAMPLE_CONSOLE_NPK, "test-0").await?; + client().install(EXAMPLE_CRASHING_NPK, "test-0").await?; + client().install(EXAMPLE_FERRIS_NPK, "test-0").await?; + client().install(EXAMPLE_HELLO_FERRIS_NPK, "test-0").await?; + client() + .install(EXAMPLE_HELLO_RESOURCE_NPK, "test-0") + .await?; + client().install(EXAMPLE_INSPECT_NPK, "test-0").await?; + client().install(EXAMPLE_MEMEATER_NPK, "test-0").await?; + client() + .install(EXAMPLE_MESSAGE_0_0_1_NPK, "test-0") + .await?; + client() + .install(EXAMPLE_MESSAGE_0_0_2_NPK, "test-0") + .await?; + client().install(EXAMPLE_PERSISTENCE_NPK, "test-0").await?; + client().install(EXAMPLE_SECCOMP_NPK, "test-0").await?; + client().install(TEST_CONTAINER_NPK, "test-0").await?; + client().install(TEST_RESOURCE_NPK, "test-0").await?; + + client().uninstall(EXAMPLE_CPUEATER).await?; + client().uninstall(EXAMPLE_CONSOLE).await?; + client().uninstall(EXAMPLE_CRASHING).await?; + client().uninstall(EXAMPLE_FERRIS).await?; + client().uninstall(EXAMPLE_HELLO_FERRIS).await?; + client().uninstall(EXAMPLE_HELLO_RESOURCE).await?; + client().uninstall(EXAMPLE_INSPECT).await?; + client().uninstall(EXAMPLE_MEMEATER).await?; + client().uninstall(EXAMPLE_MESSAGE_0_0_1).await?; + client().uninstall(EXAMPLE_MESSAGE_0_0_2).await?; + client().uninstall(EXAMPLE_PERSISTENCE).await?; + client().uninstall(EXAMPLE_SECCOMP).await?; + client().uninstall(TEST_CONTAINER).await?; + client().uninstall(TEST_RESOURCE).await?; }); -// Mount and umount all containers known to the runtime +// Mount and umount all containers known to the client() test!(mount_umount, { - let mut runtime = Northstar::launch().await?; - runtime.install(EXAMPLE_CPUEATER_NPK, "test-0").await?; - runtime.install(EXAMPLE_CONSOLE_NPK, "test-0").await?; - runtime.install(EXAMPLE_CRASHING_NPK, "test-0").await?; - runtime.install(EXAMPLE_FERRIS_NPK, "test-0").await?; - runtime.install(EXAMPLE_HELLO_FERRIS_NPK, "test-0").await?; - runtime + client().install(EXAMPLE_CPUEATER_NPK, "test-0").await?; + client().install(EXAMPLE_CONSOLE_NPK, "test-0").await?; + client().install(EXAMPLE_CRASHING_NPK, "test-0").await?; + client().install(EXAMPLE_FERRIS_NPK, "test-0").await?; + client().install(EXAMPLE_HELLO_FERRIS_NPK, "test-0").await?; + client() .install(EXAMPLE_HELLO_RESOURCE_NPK, "test-0") .await?; - runtime.install(EXAMPLE_INSPECT_NPK, "test-0").await?; - runtime.install(EXAMPLE_MEMEATER_NPK, "test-0").await?; - runtime.install(EXAMPLE_MESSAGE_0_0_1_NPK, "test-0").await?; - runtime.install(EXAMPLE_MESSAGE_0_0_2_NPK, "test-0").await?; - runtime.install(EXAMPLE_PERSISTENCE_NPK, "test-0").await?; - runtime.install(EXAMPLE_SECCOMP_NPK, "test-0").await?; - runtime.install(TEST_CONTAINER_NPK, "test-0").await?; - runtime.install(TEST_RESOURCE_NPK, "test-0").await?; - - let mut containers = runtime.containers().await?; - runtime + client().install(EXAMPLE_INSPECT_NPK, "test-0").await?; + client().install(EXAMPLE_MEMEATER_NPK, "test-0").await?; + client() + .install(EXAMPLE_MESSAGE_0_0_1_NPK, "test-0") + .await?; + client() + .install(EXAMPLE_MESSAGE_0_0_2_NPK, "test-0") + .await?; + client().install(EXAMPLE_PERSISTENCE_NPK, "test-0").await?; + client().install(EXAMPLE_SECCOMP_NPK, "test-0").await?; + client().install(TEST_CONTAINER_NPK, "test-0").await?; + client().install(TEST_RESOURCE_NPK, "test-0").await?; + + let mut containers = client().containers().await?; + client() .mount(containers.drain(..).map(|c| c.container)) .await?; - let containers = &mut runtime.containers().await?; + let containers = &mut client().containers().await?; for c in containers.iter().filter(|c| c.mounted) { - runtime.umount(c.container.clone()).await?; + client().umount(c.container.clone()).await?; } - - runtime.shutdown().await }); // Try to stop a not started container and expect an Err test!(try_to_stop_unknown_container, { - let mut runtime = Northstar::launch().await?; let container = "foo:0.0.1:default"; - assert!(runtime.stop(container, 5).await.is_err()); - runtime.shutdown().await + assert!(client().stop(container, 5).await.is_err()); }); // Try to start a container which is not installed/known test!(try_to_start_unknown_container, { - let mut runtime = Northstar::launch().await?; let container = "unknown_application:0.0.12:asdf"; - assert!(runtime.start(container).await.is_err()); - runtime.shutdown().await + assert!(client().start(container).await.is_err()); }); // Try to start a container where a dependency is missing test!(try_to_start_containter_that_misses_a_resource, { - let mut runtime = Northstar::launch().await?; - runtime.install_test_container().await?; + client().install_test_container().await?; // The TEST_RESOURCE is not installed. - assert!(runtime.start(TEST_CONTAINER).await.is_err()); - runtime.shutdown().await + assert!(client().start(TEST_CONTAINER).await.is_err()); }); // Start a container that uses a resource test!(check_test_container_resource_usage, { - let mut runtime = Northstar::launch_install_test_container().await?; + client().install_test_container().await?; + client().install_test_resource().await?; // Start the test_container process - runtime + client() .start_with_args(TEST_CONTAINER, ["cat", "/resource/hello"]) .await?; assume("hello from test resource", 5).await?; // The container might have finished at this point - runtime.stop(TEST_CONTAINER, 5).await?; - - runtime.uninstall_test_container().await?; - runtime.uninstall_test_resource().await?; + client().stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().uninstall_test_container().await?; + client().uninstall_test_resource().await?; }); // Try to uninstall a started container test!(try_to_uninstall_a_started_container, { - let mut runtime = Northstar::launch_install_test_container().await?; + client().install_test_container().await?; + client().install_test_resource().await?; - runtime.start_with_args(TEST_CONTAINER, ["sleep"]).await?; - assume("test-container: Sleeping...", 5u64).await?; + client().start_with_args(TEST_CONTAINER, ["sleep"]).await?; + assume("Sleeping...", 5u64).await?; - let result = runtime.uninstall_test_container().await; + let result = client().uninstall_test_container().await; assert!(result.is_err()); - runtime.stop(TEST_CONTAINER, 5).await?; - - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); test!(start_mounted_container_with_not_mounted_resource, { - let mut runtime = Northstar::launch_install_test_container().await?; + client().install_test_container().await?; + client().install_test_resource().await?; // Start a container that depends on a resource. - runtime.start_with_args(TEST_CONTAINER, ["sleep"]).await?; - assume("test-container: Sleeping...", 5u64).await?; - runtime.stop(TEST_CONTAINER, 5).await?; + client().start_with_args(TEST_CONTAINER, ["sleep"]).await?; + assume("Sleeping...", 5u64).await?; + client().stop(TEST_CONTAINER, 5).await?; // Umount the resource and start the container again. - runtime.umount(TEST_RESOURCE).await?; + client().umount(TEST_RESOURCE).await?; - runtime.start_with_args(TEST_CONTAINER, ["sleep"]).await?; - assume("test-container: Sleeping...", 5u64).await?; + client().start_with_args(TEST_CONTAINER, ["sleep"]).await?; + assume("Sleeping...", 5u64).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // The test is flaky and needs to listen for notifications // in order to be implemented correctly test!(container_crash_exit, { - let mut runtime = Northstar::launch_install_test_container().await?; + client().install_test_container().await?; + client().install_test_resource().await?; for _ in 0..10 { - runtime.start_with_args(TEST_CONTAINER, ["crash"]).await?; - runtime + client().start_with_args(TEST_CONTAINER, ["crash"]).await?; + client() .assume_notification( |n| { matches!( @@ -202,150 +225,169 @@ test!(container_crash_exit, { .await?; } - runtime.uninstall_test_container().await?; - runtime.uninstall_test_resource().await?; - - runtime.shutdown().await + client().uninstall_test_container().await?; + client().uninstall_test_resource().await?; }); // Check uid. In the manifest of the test container the uid // is set to 1000 test!(container_uses_correct_uid, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; assume("getuid: 1000", 5).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // Check gid. In the manifest of the test container the gid // is set to 1000 test!(container_uses_correct_gid, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; assume("getgid: 1000", 5).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // Check parent pid. Northstar starts an init process which must have pid 1. test!(container_ppid_must_be_init, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; assume("getppid: 1", 5).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // Check session id which needs to be pid of init test!(container_sid_must_be_init_or_none, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; assume("getsid: 1", 5).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // The test container only gets the cap_kill capability. See the manifest test!(container_shall_only_have_configured_capabilities, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; assume("caps bounding: \\{\\}", 10).await?; assume("caps effective: \\{\\}", 10).await?; assume("caps permitted: \\{\\}", 10).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // The test container has a configured resource limit of tasks test!(container_rlimits, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; assume( "Max processes 10000 20000 processes", 10, ) .await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); -// Check whether after a runtime start, container start and shutdown +// Check whether after a client() start, container start and shutdown // any file descriptor is leaked -test!( - start_stop_runtime_and_containers_shall_not_leak_file_descriptors, - { - /// Collect a set of files in /proc/$$/fd - fn fds() -> Result, std::io::Error> { - let mut links = std::fs::read_dir("/proc/self/fd")? - .filter_map(Result::ok) - .flat_map(|entry| entry.path().read_link()) - .collect::>(); - links.sort(); - Ok(links) - } - // Collect list of fds - let before = fds()?; - - let mut runtime = Northstar::launch_install_test_container().await?; - - runtime.start_with_args(TEST_CONTAINER, ["sleep"]).await?; - assume("test-container: Sleeping", 5).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - - let result = runtime.shutdown().await; - - // Compare the list of fds before and after the RT run. - assert_eq!(before, fds()?); - - result +test!(start_stop_and_container_shall_not_leak_file_descriptors, { + /// Collect a set of files in /proc/$$/fd + fn fds() -> Result, std::io::Error> { + let mut links = std::fs::read_dir("/proc/self/fd")? + .filter_map(Result::ok) + .flat_map(|entry| entry.path().read_link()) + .collect::>(); + links.sort(); + Ok(links) } -); + + let before = fds()?; + + client().install_test_container().await?; + client().install_test_resource().await?; + + client().start_with_args(TEST_CONTAINER, ["sleep"]).await?; + assume("Sleeping", 5).await?; + client().stop(TEST_CONTAINER, 5).await?; + + client().uninstall_test_container().await?; + client().uninstall_test_resource().await?; + + // Compare the list of fds before and after the RT run. + assert_eq!(before, fds()?); + + let result = client().shutdown().await; + + assert!(result.is_ok()); +}); // Check open file descriptors in the test container that should be // stdin: /dev/null // stdout: some pipe // stderr: /dev/null test!(container_shall_only_have_configured_fds, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; assume("/proc/self/fd/0: /dev/null", 5).await?; - assume("/proc/self/fd/1: pipe:.*", 5).await?; - assume("/proc/self/fd/2: pipe:.*", 5).await?; + assume("/proc/self/fd/1: socket", 5).await?; + assume("/proc/self/fd/2: socket", 5).await?; assume("total: 3", 5).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // Check if /proc is mounted ro test!(proc_is_mounted_ro, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; assume("proc /proc proc ro,", 5).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // Check that mount flags nosuid,nodev,noexec are properly set for bind mounts // assumption: mount flags are always listed the same order (according mount.h) // note: MS_REC is not explicitly listed an cannot be checked with this test test!(mount_flags_are_set_for_bind_mounts, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; assume( "/.* /resource \\w+ ro,(\\w+,)*nosuid,(\\w+,)*nodev,(\\w+,)*noexec", 5, ) .await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // The test container only gets the cap_kill capability. See the manifest test!(selinux_mounted_squasfs_has_correct_context, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime.start_with_args(TEST_CONTAINER, ["inspect"]).await?; + client().install_test_container().await?; + client().install_test_resource().await?; + client() + .start_with_args(TEST_CONTAINER, ["inspect"]) + .await?; // Only expect selinux context if system supports it if Path::new("/sys/fs/selinux/enforce").exists() { assume( @@ -356,36 +398,36 @@ test!(selinux_mounted_squasfs_has_correct_context, { } else { assume("/.* squashfs (\\w+,)*", 5).await?; } - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // Call syscall with specifically allowed argument test!(seccomp_allowed_syscall_with_allowed_arg, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime + client().install_test_container().await?; + client().install_test_resource().await?; + client() .start_with_args(TEST_CONTAINER, ["call-delete-module", "1"]) .await?; assume("delete_module syscall was successful", 5).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // Call syscall with argument allowed by bitmask test!(seccomp_allowed_syscall_with_masked_arg, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime + client().install_test_container().await?; + client().install_test_resource().await?; + client() .start_with_args(TEST_CONTAINER, ["call-delete-module", "4"]) .await?; assume("delete_module syscall was successful", 5).await?; - runtime.stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + client().stop(TEST_CONTAINER, 5).await?; }); // Call syscall with prohibited argument test!(seccomp_allowed_syscall_with_prohibited_arg, { - let mut runtime = Northstar::launch_install_test_container().await?; - runtime + client().install_test_container().await?; + client().install_test_resource().await?; + client() .start_with_args(TEST_CONTAINER, ["call-delete-module", "7"]) .await?; @@ -396,15 +438,15 @@ test!(seccomp_allowed_syscall_with_prohibited_arg, { .. } if signal == &31) }; - runtime.assume_notification(n, 5).await?; - runtime.shutdown().await + client().assume_notification(n, 5).await?; }); // Iterate all exit codes in the u8 range test!(exitcodes, { - let mut runtime = Northstar::launch_install_test_container().await?; + client().install_test_container().await?; + client().install_test_resource().await?; for c in &[0, 1, 10, 127, 128, 255] { - runtime + client() .start_with_args(TEST_CONTAINER, ["exit".to_string(), c.to_string()]) .await?; let n = |n: &Notification| { @@ -413,39 +455,18 @@ test!(exitcodes, { .. } if code == c) }; - runtime.assume_notification(n, 5).await?; + client().assume_notification(n, 5).await?; } - runtime.shutdown().await }); -// Open many connections to the runtime -test!(open_many_connections_to_the_runtime_and_shutdown, { - let runtime = Northstar::launch().await?; - - let mut clients = Vec::new(); - for _ in 0..500 { - let client = runtime.client().await?; - clients.push(client); - } - - let result = runtime.shutdown().await; - - for client in &mut clients { - assert!(client.containers().await.is_err()); - } - clients.clear(); - - result -}); - -// Verify that the runtime reject a version mismatch in Connect +// Verify that the client() reject a version mismatch in Connect test!(check_api_version_on_connect, { - let runtime = Northstar::launch().await?; - trait AsyncReadWrite: AsyncRead + AsyncWrite + Unpin + Send {} impl AsyncReadWrite for T {} - let mut connection = api::codec::framed(UnixStream::connect(&runtime.console).await?); + let mut connection = api::codec::Framed::new( + UnixStream::connect(&northstar_tests::runtime::console().path()).await?, + ); // Send a connect with an version unequal to the one defined in the model let mut version = api::model::version(); @@ -469,6 +490,20 @@ test!(check_api_version_on_connect, { let expected_message = model::Message::new_connect(model::Connect::Nack { error }); assert_eq!(connack, expected_message); +}); + +// Check printing on stdout and stderr +test!(stdout_stderr, { + client().install_test_container().await?; + client().install_test_resource().await?; + + let args = ["print", "--io", "stdout", "hello stdout"]; + client().start_with_args(TEST_CONTAINER, args).await?; + assume("hello stdout", 10).await?; + client().stop(TEST_CONTAINER, 5).await?; - runtime.shutdown().await + let args = ["print", "--io", "stderr", "hello stderr"]; + client().start_with_args(TEST_CONTAINER, args).await?; + assume("hello stderr", 10).await?; + client().stop(TEST_CONTAINER, 5).await?; }); diff --git a/northstar/Cargo.toml b/northstar/Cargo.toml index 2bf2659ac..a964ea1d0 100644 --- a/northstar/Cargo.toml +++ b/northstar/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "northstar" -version = "0.6.4" +version = "0.7.0-dev" authors = ["ESRLabs"] edition = "2021" build = "build.rs" @@ -24,6 +24,7 @@ ed25519-dalek = { version = "1.0.1", optional = true } futures = { version = "0.3.21", features = ["thread-pool"], optional = true } hex = { version = "0.4.3", optional = true } humanize-rs = { version = "0.1.5", optional = true } +humantime = { version = "2.1.0", optional = true } inotify = { version = "0.10.0", features = ["stream"], optional = true } itertools = { version = "0.10.1", optional = true } lazy_static = { version = "1.4.0", optional = true } @@ -44,7 +45,7 @@ serde_yaml = { version = "0.8.21", optional = true } sha2 = { version = "0.10.2", optional = true } tempfile = { version = "3.3.0", optional = true } thiserror = "1.0.30" -tokio = { version = "1.17.0", features = ["fs", "io-std", "io-util", "macros", "process", "rt", "sync", "time"], optional = true } +tokio = { version = "1.17.0", features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "sync", "time"], optional = true } tokio-eventfd = { version = "0.2.0", optional = true } tokio-util = { version = "0.7.0", features = ["codec", "io"], optional = true } url = { version = "2.2.2", features = ["serde"], optional = true } @@ -92,9 +93,11 @@ runtime = [ "caps", "cgroups-rs", "devicemapper", + "derive-new", "ed25519-dalek", "futures", "hex", + "humantime", "itertools", "inotify", "lazy_static", @@ -123,14 +126,9 @@ seccomp = [ [dev-dependencies] anyhow = "1.0.54" -nix = "0.23.0" proptest = "1.0.0" -tempfile = "3.3.0" -tokio = { version = "1.17.0", features = ["rt-multi-thread"] } +serde_json = "1.0.68" [build-dependencies] anyhow = { version = "1.0.54", optional = true } bindgen = { version = "0.59.1", default-features = false, features = ["runtime"], optional = true } -nix = { version = "0.23.0", optional = true } -tokio = { version = "1.17.0", features = ["macros", "rt-multi-thread"], optional = true } - diff --git a/northstar/src/api/client.rs b/northstar/src/api/client.rs index 95b409951..4b5ca0188 100644 --- a/northstar/src/api/client.rs +++ b/northstar/src/api/client.rs @@ -1,5 +1,5 @@ use super::{ - codec::{self, framed}, + codec, model::{ self, Connect, Container, ContainerData, ContainerStats, Message, MountResult, Notification, RepositoryId, Request, Response, @@ -21,10 +21,13 @@ use std::{ use thiserror::Error; use tokio::{ fs, - io::{self, AsyncRead, AsyncWrite}, + io::{self, AsyncRead, AsyncWrite, BufWriter}, time, }; +/// Default buffer size for installation transfers +const BUFFER_SIZE: usize = 1024 * 1024; + /// API error #[allow(missing_docs)] #[derive(Error, Debug)] @@ -104,7 +107,7 @@ pub async fn connect( notifications: Option, timeout: time::Duration, ) -> Result, Error> { - let mut connection = framed(io); + let mut connection = codec::Framed::with_capacity(io, BUFFER_SIZE); // Send connect message let connect = Connect::Connect { version: model::version(), @@ -431,7 +434,7 @@ impl<'a, T: AsyncRead + AsyncWrite + Unpin> Client { /// ``` pub async fn install(&mut self, npk: &Path, repository: &str) -> Result<(), Error> { self.fused()?; - let mut file = fs::File::open(npk).await.map_err(Error::Io)?; + let file = fs::File::open(npk).await.map_err(Error::Io)?; let size = file.metadata().await.unwrap().len(); let request = Request::Install { repository: repository.into(), @@ -443,12 +446,15 @@ impl<'a, T: AsyncRead + AsyncWrite + Unpin> Client { Error::Stopped })?; - io::copy(&mut file, &mut self.connection) - .await - .map_err(|e| { - self.fuse(); - Error::Io(e) - })?; + self.connection.flush().await?; + debug_assert!(self.connection.write_buffer().is_empty()); + + let mut reader = io::BufReader::with_capacity(BUFFER_SIZE, file); + let mut writer = BufWriter::with_capacity(BUFFER_SIZE, self.connection.get_mut()); + io::copy_buf(&mut reader, &mut writer).await.map_err(|e| { + self.fuse(); + Error::Io(e) + })?; loop { match self.connection.next().await { diff --git a/northstar/src/api/codec.rs b/northstar/src/api/codec.rs index b7616e26b..b6a879cb0 100644 --- a/northstar/src/api/codec.rs +++ b/northstar/src/api/codec.rs @@ -1,43 +1,51 @@ use super::model; -use futures::Stream; -use std::{ - cmp::min, - io::ErrorKind, - pin::Pin, - task::{self, Poll}, -}; -use task::Context; +use std::io::ErrorKind; use tokio::io::{self, AsyncRead, AsyncWrite}; -use tokio_util::codec::{Decoder, Encoder, FramedParts}; +use tokio_util::codec::{Decoder, Encoder}; /// Newline delimited json codec for api::Message that on top implements AsyncRead and Write pub struct Framed { inner: tokio_util::codec::Framed, } -impl Framed { - /// Consumes the Framed, returning its underlying I/O stream, the buffer with unprocessed data, and the codec. - pub fn into_parts(self) -> FramedParts { - self.inner.into_parts() +impl Framed { + /// Provides a [`Stream`] and [`Sink`] interface for reading and writing to this + /// I/O object, using [`Decoder`] and [`Encoder`] to read and write the raw data. + pub fn new(inner: T) -> Framed { + Framed { + inner: tokio_util::codec::Framed::new(inner, Codec::default()), + } + } + + /// Provides a [`Stream`] and [`Sink`] interface for reading and writing to this + /// I/O object, using [`Decoder`] and [`Encoder`] to read and write the raw data, + /// with a specific read buffer initial capacity. + /// [`split`]: https://docs.rs/futures/0.3/futures/stream/trait.StreamExt.html#method.split + pub fn with_capacity(inner: T, capacity: usize) -> Framed { + Framed { + inner: tokio_util::codec::Framed::with_capacity(inner, Codec::default(), capacity), + } } +} + +impl std::ops::Deref for Framed { + type Target = tokio_util::codec::Framed; - /// Consumes the Framed, returning its underlying I/O stream. - pub fn into_inner(self) -> T { - self.inner.into_inner() + fn deref(&self) -> &Self::Target { + &self.inner } } -/// Constructs a new Framed with Codec from `io` -pub fn framed(io: T) -> Framed { - Framed { - inner: tokio_util::codec::Framed::new(io, Codec::default()), +impl std::ops::DerefMut for Framed { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner } } /// Newline delimited json #[derive(Default)] pub struct Codec { - lines: tokio_util::codec::LinesCodec, + inner: tokio_util::codec::LinesCodec, } impl Decoder for Codec { @@ -45,7 +53,7 @@ impl Decoder for Codec { type Error = io::Error; fn decode(&mut self, src: &mut bytes::BytesMut) -> Result, Self::Error> { - self.lines + self.inner .decode(src) .map_err(|e| io::Error::new(ErrorKind::Other, e))? // See LinesCodecError. .as_deref() @@ -63,83 +71,12 @@ impl Encoder for Codec { item: model::Message, dst: &mut bytes::BytesMut, ) -> Result<(), Self::Error> { - self.lines + self.inner .encode(serde_json::to_string(&item)?.as_str(), dst) .map_err(|e| io::Error::new(ErrorKind::Other, e)) } } -impl AsyncWrite for Framed { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - assert!(self.inner.write_buffer().is_empty()); - let t: &mut T = self.inner.get_mut(); - AsyncWrite::poll_write(Pin::new(t), cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let t: &mut T = self.inner.get_mut(); - AsyncWrite::poll_flush(Pin::new(t), cx) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - let t: &mut T = self.inner.get_mut(); - AsyncWrite::poll_shutdown(Pin::new(t), cx) - } -} - -impl AsyncRead for Framed { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut io::ReadBuf<'_>, - ) -> Poll> { - if self.inner.read_buffer().is_empty() { - let t: &mut T = self.inner.get_mut(); - AsyncRead::poll_read(Pin::new(t), cx, buf) - } else { - let n = min(buf.remaining(), self.inner.read_buffer().len()); - buf.put_slice(&self.inner.read_buffer_mut().split_to(n)); - Poll::Ready(Ok(())) - } - } -} - -impl Stream for Framed { - type Item = Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let framed = Pin::new(&mut self.inner); - framed.poll_next(cx) - } -} - -impl futures::sink::Sink for Framed { - type Error = io::Error; - - fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_ready(cx) - } - - fn start_send(mut self: Pin<&mut Self>, item: model::Message) -> Result<(), Self::Error> { - Pin::new(&mut self.inner).start_send(item) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.inner).poll_close(cx) - } -} - #[cfg(test)] mod tests { use std::convert::TryInto; diff --git a/northstar/src/common/container.rs b/northstar/src/common/container.rs index 7edacd6c5..cbe3431dd 100644 --- a/northstar/src/common/container.rs +++ b/northstar/src/common/container.rs @@ -12,9 +12,8 @@ use std::{ use thiserror::Error; /// Container identification -#[derive(Clone, Eq, PartialOrd, PartialEq, Debug, Hash, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Eq, PartialOrd, Ord, PartialEq, Debug, Hash, JsonSchema)] pub struct Container { - #[serde(flatten)] inner: Arc, } @@ -98,7 +97,26 @@ impl, N: TryInto, V: ToString> TryFrom<(N, V)> f } } -#[derive(Eq, PartialOrd, PartialEq, Debug, Hash, Serialize, Deserialize, JsonSchema)] +impl Serialize for Container { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!("{}:{}", self.inner.name, self.inner.version)) + } +} + +impl<'de> Deserialize<'de> for Container { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Container::try_from(value.as_str()).map_err(serde::de::Error::custom) + } +} + +#[derive(Eq, PartialOrd, PartialEq, Ord, Debug, Hash, Serialize, Deserialize, JsonSchema)] struct Inner { name: Name, version: Version, diff --git a/northstar/src/common/name.rs b/northstar/src/common/name.rs index be262580d..a5e1fca81 100644 --- a/northstar/src/common/name.rs +++ b/northstar/src/common/name.rs @@ -8,7 +8,9 @@ use std::{ use thiserror::Error; /// Name of a container -#[derive(Clone, Eq, PartialOrd, PartialEq, Debug, Hash, Serialize, Deserialize, JsonSchema)] +#[derive( + Clone, Eq, PartialOrd, Ord, PartialEq, Debug, Hash, Serialize, Deserialize, JsonSchema, +)] #[serde(try_from = "String")] pub struct Name(String); diff --git a/northstar/src/common/non_null_string.rs b/northstar/src/common/non_null_string.rs index d76fe8c5f..782034dd6 100644 --- a/northstar/src/common/non_null_string.rs +++ b/northstar/src/common/non_null_string.rs @@ -2,6 +2,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ convert::{TryFrom, TryInto}, + ffi::CString, fmt::{Display, Formatter}, ops::Deref, }; @@ -37,6 +38,12 @@ impl Deref for NonNullString { } } +impl From for CString { + fn from(s: NonNullString) -> Self { + CString::new(s.0.as_bytes()).unwrap() + } +} + impl TryFrom for NonNullString { type Error = InvalidNullChar; @@ -70,3 +77,10 @@ impl InvalidNullChar { self.0 } } + +#[test] +fn try_from() { + assert!(NonNullString::try_from("hel\0lo").is_err()); + assert!(NonNullString::try_from("hello").is_ok()); + assert!(NonNullString::try_from("hello\0").is_err()); +} diff --git a/northstar/src/common/version.rs b/northstar/src/common/version.rs index 171ba0449..83b128ab2 100644 --- a/northstar/src/common/version.rs +++ b/northstar/src/common/version.rs @@ -122,6 +122,12 @@ impl PartialOrd for Version { } } +impl Ord for Version { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap() + } +} + impl JsonSchema for Version { fn schema_name() -> String { "Version".to_string() diff --git a/northstar/src/lib.rs b/northstar/src/lib.rs index 85774bdd6..7cdb5c031 100644 --- a/northstar/src/lib.rs +++ b/northstar/src/lib.rs @@ -22,7 +22,3 @@ pub mod runtime; #[cfg(feature = "seccomp")] /// Support for seccomp syscall filtering pub mod seccomp; - -/// Northstar internal utilities -#[cfg(feature = "runtime")] -mod util; diff --git a/northstar/src/npk/manifest.rs b/northstar/src/npk/manifest.rs index 329d8cb0d..083c2fe7b 100644 --- a/northstar/src/npk/manifest.rs +++ b/northstar/src/npk/manifest.rs @@ -1,5 +1,5 @@ use crate::{ - common::{name::Name, non_null_string::NonNullString, version::Version}, + common::{container::Container, name::Name, non_null_string::NonNullString, version::Version}, seccomp::{Seccomp, Selinux, SyscallRule}, }; use derive_more::Deref; @@ -60,7 +60,8 @@ pub struct Manifest { /// Resource limits pub rlimits: Option>, /// IO configuration - pub io: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub io: Io, /// Optional custom data. The runtime doesnt use this. pub custom: Option, } @@ -69,6 +70,11 @@ impl Manifest { /// Manifest version supported by the runtime pub const VERSION: Version = Version::new(0, 1, 0); + /// Container that is specified in the manifest + pub fn container(&self) -> Container { + Container::new(self.name.clone(), self.version.clone()) + } + /// Read a manifest from `reader` pub fn from_reader(reader: R) -> Result { let manifest: Self = serde_yaml::from_reader(reader).map_err(Error::SerdeYaml)?; @@ -83,19 +89,24 @@ impl Manifest { fn verify(&self) -> Result<(), Error> { // Most optionals in the manifest are not valid for a resource container - if self.init.is_none() - && (self.args.is_some() - || self.env.is_some() - || self.autostart.is_some() - || self.cgroups.is_some() - || self.seccomp.is_some() - || self.capabilities.is_some() - || self.suppl_groups.is_some() - || self.io.is_some()) + + if let Some(init) = &self.init { + if NonNullString::try_from(init.display().to_string()).is_err() { + return Err(Error::Invalid( + "Init path must be a string without zero bytes".to_string(), + )); + } + } else if self.args.is_some() + || self.env.is_some() + || self.autostart.is_some() + || self.cgroups.is_some() + || self.seccomp.is_some() + || self.capabilities.is_some() + || self.suppl_groups.is_some() { return Err(Error::Invalid( "Resource containers must not define any of the following manifest entries:\ - args, env, autostart, cgroups, seccomp, capabilities, suppl_groups, io" + args, env, autostart, cgroups, seccomp, capabilities, suppl_groups, io" .to_string(), )); } @@ -108,6 +119,18 @@ impl Manifest { return Err(Error::Invalid("Invalid gid of 0".to_string())); } + // Check for reserved env variable names + if let Some(env) = &self.env { + for name in ["NAME", "VERSION", "NORTHSTAR_CONSOLE"] { + if env.keys().any(|k| name == k.as_str()) { + return Err(Error::Invalid(format!( + "Invalid env: resevered variable {}", + name + ))); + } + } + } + // Check for relative and overlapping bind mounts let mut prev_comps = vec![RootDir]; self.mounts @@ -426,31 +449,30 @@ pub enum Mount { } /// IO configuration for stdin, stdout, stderr -#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Clone, Eq, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Io { /// stdout configuration - #[serde(skip_serializing_if = "Option::is_none")] - pub stdout: Option, + pub stdout: Output, /// stderr configuration - #[serde(skip_serializing_if = "Option::is_none")] - pub stderr: Option, + pub stderr: Output, } /// Io redirection for stdout/stderr #[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] pub enum Output { - /// Inherit the runtimes stdout/stderr + /// Discard output + #[serde(rename = "discard")] + Discard, + /// Forward output to the logging system with level and optional tag #[serde(rename = "pipe")] Pipe, - /// Forward output to the logging system with level and optional tag - #[serde(rename = "log")] - Log { - /// Level - level: Level, - /// Tag - tag: String, - }, +} + +impl Default for Output { + fn default() -> Output { + Output::Discard + } } /// Log level @@ -822,7 +844,7 @@ mounts: options: noexec autostart: critical seccomp: - allow: + allow: fork: any waitpid: any cgroups: @@ -1081,10 +1103,7 @@ seccomp: capabilities: - CAP_NET_ADMIN io: - stdout: - log: - level: DEBUG - tag: test + stdout: pipe stderr: pipe cgroups: memory: diff --git a/northstar/src/runtime/cgroups.rs b/northstar/src/runtime/cgroups.rs index 905e7afc5..bd4bc4151 100644 --- a/northstar/src/runtime/cgroups.rs +++ b/northstar/src/runtime/cgroups.rs @@ -116,6 +116,7 @@ impl Hierarchy for RuntimeHierarchy { #[derive(Debug)] pub struct CGroups { + container: Container, cgroup: cgroups_rs::Cgroup, memory_monitor: MemoryMonitor, } @@ -166,14 +167,18 @@ impl CGroups { }; Ok(CGroups { + container: container.clone(), cgroup, memory_monitor, }) } pub async fn destroy(self) { + debug!("Stopping oom monitor of {}", self.container); self.memory_monitor.stop().await; - info!("Destroying cgroup"); + + info!("Destroying cgroup of {}", self.container); + assert!(self.cgroup.tasks().is_empty()); self.cgroup.delete().expect("Failed to remove cgroups"); } @@ -253,10 +258,7 @@ impl MemoryMonitor { 'outer: loop { select! { - _ = stop.cancelled() => { - debug!("Stopping oom monitor of {}", container); - break 'outer; - } + _ = stop.cancelled() => break 'outer, _ = tx.closed() => break 'outer, _ = event_fd.read(&mut buffer) => { 'inner: loop { @@ -274,6 +276,9 @@ impl MemoryMonitor { } } } + drop(event_control); + drop(oom_control); + drop(event_fd); }) }; @@ -306,10 +311,7 @@ impl MemoryMonitor { 'outer: loop { select! { - _ = stop.cancelled() => { - debug!("Stopping oom monitor of {}", container); - break 'outer; - } + _ = stop.cancelled() => break 'outer, _ = tx.closed() => break 'outer, _ = stream.next() => { let events = fs::read_to_string(&path).await.expect("Failed to read memory events"); diff --git a/northstar/src/runtime/config.rs b/northstar/src/runtime/config.rs index 4cf2abc38..facf1c4fa 100644 --- a/northstar/src/runtime/config.rs +++ b/northstar/src/runtime/config.rs @@ -1,7 +1,13 @@ use super::{Error, RepositoryId}; -use crate::{common::non_null_string::NonNullString, util::is_rw}; +use crate::common::non_null_string::NonNullString; +use nix::{sys::stat, unistd}; use serde::Deserialize; -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::HashMap, + os::unix::prelude::{MetadataExt, PermissionsExt}, + path::{Path, PathBuf}, +}; +use tokio::fs; use url::Url; /// Runtime configuration @@ -141,3 +147,24 @@ impl Config { Ok(()) } } + +/// Return true if path is read and writeable +async fn is_rw(path: &Path) -> bool { + match fs::metadata(path).await { + Ok(stat) => { + let same_uid = stat.uid() == unistd::getuid().as_raw(); + let same_gid = stat.gid() == unistd::getgid().as_raw(); + let mode = stat::Mode::from_bits_truncate(stat.permissions().mode()); + + let is_readable = (same_uid && mode.contains(stat::Mode::S_IRUSR)) + || (same_gid && mode.contains(stat::Mode::S_IRGRP)) + || mode.contains(stat::Mode::S_IROTH); + let is_writable = (same_uid && mode.contains(stat::Mode::S_IWUSR)) + || (same_gid && mode.contains(stat::Mode::S_IWGRP)) + || mode.contains(stat::Mode::S_IWOTH); + + is_readable && is_writable + } + Err(_) => false, + } +} diff --git a/northstar/src/runtime/console.rs b/northstar/src/runtime/console.rs index 416ebd51a..2ca30db4c 100644 --- a/northstar/src/runtime/console.rs +++ b/northstar/src/runtime/console.rs @@ -1,6 +1,6 @@ use super::{ContainerEvent, Event, NotificationTx, RepositoryId}; use crate::{ - api, + api::{self, codec::Framed}, common::container::Container, runtime::{EventTx, ExitStatus}, }; @@ -18,7 +18,7 @@ use std::{fmt, path::PathBuf, unreachable}; use thiserror::Error; use tokio::{ fs, - io::{self, AsyncRead, AsyncReadExt, AsyncWrite, BufReader}, + io::{self, AsyncRead, AsyncReadExt, AsyncWrite}, net::{TcpListener, UnixListener}, pin, select, sync::{broadcast, mpsc, oneshot}, @@ -28,6 +28,8 @@ use tokio::{ use tokio_util::{either::Either, io::ReaderStream, sync::CancellationToken}; use url::Url; +const BUFFER_SIZE: usize = 1024 * 1024; + // Request from the main loop to the console #[derive(Debug)] pub(crate) enum Request { @@ -114,7 +116,7 @@ impl Console { debug!("Client {} connected", peer); // Get a framed stream and sink interface. - let mut network_stream = api::codec::framed(stream); + let mut network_stream = api::codec::Framed::with_capacity(stream, BUFFER_SIZE); // Wait for a connect message within timeout let connect = network_stream.next(); @@ -253,7 +255,7 @@ impl Console { /// async fn process_request( client_id: &Peer, - stream: &mut S, + stream: &mut Framed, stop: &CancellationToken, event_loop: &EventTx, message: model::Message, @@ -263,7 +265,10 @@ where { let (reply_tx, reply_rx) = oneshot::channel(); if let model::Message::Request { - request: model::Request::Install { repository, size }, + request: model::Request::Install { + repository, + mut size, + }, } = message { debug!( @@ -281,8 +286,21 @@ where let event = Event::Console(request, reply_tx); event_loop.send(event).map_err(|_| Error::Shutdown).await?; + // The codec might have pulled bytes in the the read buffer of the connection. + if !stream.read_buffer().is_empty() { + let read_buffer = stream.read_buffer_mut().split(); + + // TODO: handle this case. The connected entity pushed the install file + // and a subsequenc request. If the codec pullen in the *full* install blob + // and some bytes from the following command the logic is screwed up. + assert!(read_buffer.len() as u64 <= size); + + size -= read_buffer.len() as u64; + tx.send(read_buffer.freeze()).await.ok(); + } + // If the connections breaks: just break. If the receiver is dropped: just break. - let mut take = ReaderStream::new(BufReader::new(stream.take(size))); + let mut take = ReaderStream::with_capacity(stream.get_mut().take(size), 1024 * 1024); while let Some(Ok(buf)) = take.next().await { if tx.send(buf).await.is_err() { break; @@ -409,6 +427,12 @@ impl From<&str> for Peer { } } +impl From for Peer { + fn from(s: String) -> Self { + Peer(s) + } +} + impl From for Peer { fn from(socket: std::net::SocketAddr) -> Self { Peer(socket.to_string()) diff --git a/northstar/src/runtime/fork/forker/impl.rs b/northstar/src/runtime/fork/forker/impl.rs new file mode 100644 index 000000000..3a9b0e41e --- /dev/null +++ b/northstar/src/runtime/fork/forker/impl.rs @@ -0,0 +1,291 @@ +use super::{ + init, + init::Init, + messages::{Message, Notification}, + util::fork, +}; +use crate::{ + common::container::Container, + debug, + runtime::{ + fork::util::{self, set_log_target}, + ipc::{self, owned_fd::OwnedFd, socket_pair, AsyncMessage, Message as IpcMessage}, + ExitStatus, Pid, + }, +}; +use futures::{ + stream::{FuturesUnordered, StreamExt}, + Future, +}; +use itertools::Itertools; +use nix::{ + errno::Errno, + sys::{signal::Signal, wait::waitpid}, + unistd, +}; +use std::{ + collections::HashMap, + os::unix::{ + io::FromRawFd, + net::UnixStream as StdUnixStream, + prelude::{IntoRawFd, RawFd}, + }, + path::PathBuf, +}; +use tokio::{net::UnixStream, select, sync::mpsc, task}; + +type Inits = HashMap; + +/// Handle the communication between the forker and the init process. +struct InitProcess { + pid: Pid, + /// Used to send messages to the init process. + stream: AsyncMessage, +} + +/// Entry point of the forker process +pub async fn run(stream: StdUnixStream, notifications: StdUnixStream) -> ! { + let mut notifications: AsyncMessage = notifications + .try_into() + .expect("Failed to create async message"); + let mut stream: AsyncMessage = + stream.try_into().expect("Failed to create async message"); + let mut inits = Inits::new(); + let mut exits = FuturesUnordered::new(); + + debug!("Entering main loop"); + + let (tx, mut rx) = mpsc::channel(1); + + // Separate tasks for notifications and messages + + task::spawn(async move { + loop { + select! { + exit = rx.recv() => { + match exit { + Some(exit) => exits.push(exit), + None => break, + } + } + exit = exits.next(), if !exits.is_empty() => { + let (container, exit_status) = exit.expect("Invalid exit status"); + debug!("Forwarding exit status notification of {}: {}", container, exit_status); + notifications.send(Notification::Exit { container, exit_status }).await.expect("Failed to send exit notification"); + } + } + } + }); + + loop { + select! { + request = recv(&mut stream) => { + match request { + Some(Message::CreateRequest { init, console }) => { + let container = init.container.clone(); + + if inits.contains_key(&container) { + let error = format!("Container {} already created", container); + log::warn!("{}", error); + stream.send(Message::Failure(error)).await.expect("Failed to send response"); + continue; + } + + debug!("Creating init process for {}", init.container); + let (pid, init_process) = create(init, console).await; + inits.insert(container, init_process); + stream.send(Message::CreateResult { init: pid }).await.expect("Failed to send response"); + } + Some(Message::ExecRequest { container, path, args, env, io }) => { + let io = io.unwrap(); + if let Some(init) = inits.remove(&container) { + let (response, exit) = exec(init, container, path, args, env, io).await; + tx.send(exit).await.ok(); + stream.send(response).await.expect("Failed to send response"); + } else { + let error = format!("Container {} not created", container); + log::warn!("{}", error); + stream.send(Message::Failure(error)).await.expect("Failed to send response"); + } + } + Some(_) => unreachable!("Unexpected message"), + None => { + debug!("Forker request channel closed. Exiting "); + std::process::exit(0); + } + } + } + } + } +} + +/// Create a new init process ("container") +async fn create(init: Init, console: Option) -> (Pid, InitProcess) { + let container = init.container.clone(); + debug!("Creating container {}", container); + let mut stream = socket_pair().expect("Failed to create socket pair"); + + let trampoline_pid = fork(|| { + set_log_target("northstar::forker-trampoline".into()); + util::set_parent_death_signal(Signal::SIGKILL); + + // Create pid namespace + debug!("Creating pid namespace"); + nix::sched::unshare(nix::sched::CloneFlags::CLONE_NEWPID) + .expect("Failed to create pid namespace"); + + // Work around the borrow checker and fork + let stream = stream.second().into_raw_fd(); + + // Fork the init process + debug!("Forking init of {}", container); + let init_pid = fork(|| { + let stream = unsafe { StdUnixStream::from_raw_fd(stream) }; + // Dive into init and never return + let stream = IpcMessage::from(stream); + init.run(stream, console); + }) + .expect("Failed to fork init"); + + let stream = unsafe { StdUnixStream::from_raw_fd(stream) }; + + // Send the pid of init to the forker process + let mut stream = ipc::Message::from(stream); + stream.send(init_pid).expect("Failed to send init pid"); + + debug!("Exiting trampoline"); + Ok(()) + }) + .expect("Failed to fork trampoline process"); + + let mut stream: AsyncMessage = stream + .first_async() + .map(Into::into) + .expect("Failed to turn socket into async UnixStream"); + + debug!("Waiting for init pid of container {}", container); + let pid = stream + .recv() + .await + .expect("Failed to receive init pid") + .unwrap(); + + // Reap the trampoline process + debug!("Waiting for trampoline process {} to exit", trampoline_pid); + let trampoline_pid = unistd::Pid::from_raw(trampoline_pid as i32); + match waitpid(Some(trampoline_pid), None) { + Ok(_) | Err(Errno::ECHILD) => (), // Ok - or reaped by the reaper thread + Err(e) => panic!("Failed to wait for the trampoline process: {}", e), + } + + debug!("Created container {} with pid {}", container, pid); + + (pid, InitProcess { pid, stream }) +} + +/// Send a exec request to a container +async fn exec( + mut init: InitProcess, + container: Container, + path: PathBuf, + args: Vec, + env: Vec, + io: [OwnedFd; 3], +) -> (Message, impl Future) { + debug_assert!(io.len() == 3); + + debug!( + "Forwarding exec request for container {}: {}", + container, + args.iter().map(ToString::to_string).join(" ") + ); + + // Send the exec request to the init process + let message = init::Message::new_exec(path, args, env); + init.stream + .send(message) + .await + .expect("Failed to send exec to init"); + + // Send io file descriptors + init.stream.send_fds(&io).await.expect("Failed to send fd"); + drop(io); + + match init.stream.recv().await.expect("Failed to receive") { + Some(init::Message::Forked { .. }) => (), + _ => panic!("Unexpected init message"), + } + + // Construct a future that waits to the init to signal a exit of it's child + // Afterwards reap the init process which should have exitted already + let exit = async move { + match init.stream.recv().await { + Ok(Some(init::Message::Exit { + pid: _, + exit_status, + })) => { + // Reap init process + debug!("Reaping init process of {} ({})", container, init.pid); + waitpid(unistd::Pid::from_raw(init.pid as i32), None) + .expect("Failed to reap init process"); + (container, exit_status) + } + Ok(None) | Err(_) => { + // Reap init process + debug!("Reaping init process of {} ({})", container, init.pid); + waitpid(unistd::Pid::from_raw(init.pid as i32), None) + .expect("Failed to reap init process"); + (container, ExitStatus::Exit(-1)) + } + Ok(_) => panic!("Unexpected message from init"), + } + }; + + (Message::ExecResult, exit) +} + +async fn recv(stream: &mut AsyncMessage) -> Option { + let request = match stream.recv().await { + Ok(request) => request, + Err(e) => { + debug!("Forker request error: {}. Breaking", e); + std::process::exit(0); + } + }; + match request { + Some(Message::CreateRequest { init, console: _ }) => { + let console = if init.console { + debug!("Console is enabled. Waiting for console stream"); + let console = stream + .recv_fds::() + .await + .expect("Failed to receive console fd"); + let console = unsafe { OwnedFd::from_raw_fd(console[0]) }; + Some(console) + } else { + None + }; + Some(Message::CreateRequest { init, console }) + } + Some(Message::ExecRequest { + container, + path, + args, + env, + .. + }) => { + let io = stream + .recv_fds::() + .await + .expect("Failed to receive io"); + Some(Message::ExecRequest { + container, + path, + args, + env, + io: Some(io), + }) + } + m => m, + } +} diff --git a/northstar/src/runtime/fork/forker/messages.rs b/northstar/src/runtime/fork/forker/messages.rs new file mode 100644 index 000000000..8bbf415a4 --- /dev/null +++ b/northstar/src/runtime/fork/forker/messages.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use super::init::Init; +use crate::{ + common::container::Container, + runtime::{ipc::owned_fd::OwnedFd, ExitStatus, Pid}, +}; +use derive_new::new; +use serde::{Deserialize, Serialize}; + +/// Request from the runtime to the forker +#[non_exhaustive] +#[derive(new, Debug, Serialize, Deserialize)] +pub enum Message { + CreateRequest { + init: Init, + #[serde(skip)] + console: Option, + }, + CreateResult { + init: Pid, + }, + ExecRequest { + container: Container, + path: PathBuf, + args: Vec, + env: Vec, + #[serde(skip)] + io: Option<[OwnedFd; 3]>, + }, + ExecResult, + Failure(String), +} + +/// Notification from the forker to the runtime +#[derive(Debug, Serialize, Deserialize)] +pub enum Notification { + Exit { + container: Container, + exit_status: ExitStatus, + }, +} diff --git a/northstar/src/runtime/fork/forker/mod.rs b/northstar/src/runtime/fork/forker/mod.rs new file mode 100644 index 000000000..6be3f2cd6 --- /dev/null +++ b/northstar/src/runtime/fork/forker/mod.rs @@ -0,0 +1,164 @@ +use super::{ + super::{error::Error, Pid}, + init, + util::{self}, +}; +use crate::{ + common::container::Container, + debug, + npk::manifest::Manifest, + runtime::{ + config::Config, + error::Context, + fork::util::set_log_target, + ipc::{owned_fd::OwnedFd, socket_pair, AsyncMessage}, + }, +}; +use futures::FutureExt; +pub use messages::{Message, Notification}; +use nix::sys::signal::{signal, SigHandler, Signal}; +use std::{os::unix::net::UnixStream as StdUnixStream, path::PathBuf}; +use tokio::{net::UnixStream, runtime}; + +mod r#impl; +mod messages; + +pub struct ForkerChannels { + pub stream: StdUnixStream, + pub notifications: StdUnixStream, +} + +/// Fork the forker process +pub fn start() -> Result<(Pid, ForkerChannels), Error> { + let mut stream_pair = socket_pair().expect("Failed to open socket pair"); + let mut notifications = socket_pair().expect("Failed to open socket pair"); + + let pid = util::fork(|| { + set_log_target("northstar::forker".into()); + util::set_child_subreaper(true); + util::set_parent_death_signal(Signal::SIGKILL); + util::set_process_name("northstar-fork"); + + let stream = stream_pair.second(); + let notifications = notifications.second(); + + debug!("Setting signal handlers for SIGINT and SIGHUP"); + unsafe { + signal(Signal::SIGINT, SigHandler::SigIgn).context("Setting SIGINT handler failed")?; + signal(Signal::SIGHUP, SigHandler::SigIgn).context("Setting SIGHUP handler failed")?; + } + + debug!("Starting async runtime"); + runtime::Builder::new_current_thread() + .thread_name("northstar-fork-runtime") + .enable_time() + .enable_io() + .build() + .expect("Failed to start runtime") + .block_on(async { + r#impl::run(stream, notifications).await; + }); + Ok(()) + }) + .expect("Failed to start Forker process"); + + let forker = ForkerChannels { + stream: stream_pair.first(), + notifications: notifications.first(), + }; + + Ok((pid, forker)) +} + +/// Handle to the forker process +#[derive(Debug)] +pub struct Forker { + /// Framed stream/sink for sending messages to the forker process + stream: AsyncMessage, +} + +impl Forker { + /// Create a new forker handle + pub fn new(stream: StdUnixStream) -> Self { + let stream = stream.try_into().expect("Failed to create AsyncMessage"); + Self { stream } + } + + /// Send a request to the forker process to create a new container + pub async fn create( + &mut self, + config: &Config, + manifest: &Manifest, + console: Option, + ) -> Result { + debug_assert_eq!(manifest.console, console.is_some()); + + let init = init::build(config, manifest).await?; + let console = console.map(Into::into); + let message = Message::new_create_request(init, console); + + match self + .request_response(message) + .await + .expect("Failed to send request") + { + Message::CreateResult { init } => Ok(init), + Message::Failure(error) => { + Err(Error::StartContainerFailed(manifest.container(), error)) + } + _ => panic!("Unexpected forker response"), + } + } + + /// Start container process in a previously created container + pub async fn exec( + &mut self, + container: Container, + path: PathBuf, + args: Vec, + env: Vec, + io: [OwnedFd; 3], + ) -> Result<(), Error> { + let message = Message::new_exec_request(container, path, args, env, Some(io)); + self.request_response(message).await.map(drop) + } + + /// Send a request to the forker process + async fn request_response(&mut self, request: Message) -> Result { + let mut request = request; + + // Remove fds from message + let fds = match &mut request { + Message::CreateRequest { init: _, console } => { + console.take().map(|console| Vec::from([console])) + } + Message::ExecRequest { io, .. } => io.take().map(Vec::from), + _ => None, + }; + + // Send it + self.stream + .send(request) + .await + .context("Failed to send request")?; + + // Send fds if any + if let Some(fds) = fds { + self.stream + .send_fds(&fds) + .await + .context("Failed to send fd")?; + drop(fds); + } + + // Receive reply + let reply = self + .stream + .recv() + .map(|s| s.map(|s| s.unwrap())) + .await + .context("Failed to receive response from forker")?; + + Ok(reply) + } +} diff --git a/northstar/src/runtime/process/fs.rs b/northstar/src/runtime/fork/init/builder.rs similarity index 67% rename from northstar/src/runtime/process/fs.rs rename to northstar/src/runtime/fork/init/builder.rs index e82c9e17f..702dc686b 100644 --- a/northstar/src/runtime/process/fs.rs +++ b/northstar/src/runtime/fork/init/builder.rs @@ -1,74 +1,95 @@ -use super::Error; +use super::{Init, Mount}; use crate::{ common::container::Container, npk::{ manifest, manifest::{Manifest, MountOption, MountOptions, Resource, Tmpfs}, }, - runtime::{config::Config, error::Context}, - util::PathExt, + runtime::{ + config::Config, + error::{Context, Error}, + }, + seccomp, }; -use log::debug; use nix::{mount::MsFlags, unistd}; -use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; +use std::{ + ffi::{c_void, CString}, + path::{Path, PathBuf}, + ptr::null, +}; use tokio::fs; -/// Instructions for mount system call done in init -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(super) struct Mount { - pub source: Option, - pub target: PathBuf, - pub fstype: Option, - pub flags: u64, - pub data: Option, - pub error_msg: String, +trait PathExt { + fn join_strip>(&self, w: T) -> PathBuf; } -impl Mount { - pub fn new( - source: Option, - target: PathBuf, - fstype: Option<&'static str>, - flags: MsFlags, - data: Option, - ) -> Mount { - let error_msg = format!( - "Failed to mount '{}' of type '{}' on '{}' with flags '{:?}' and data '{}'", - source.clone().unwrap_or_default().display(), - fstype.unwrap_or_default(), - target.display(), - flags, - data.clone().unwrap_or_default() - ); - Mount { - source, - target, - fstype: fstype.map(|s| s.to_string()), - flags: flags.bits(), - data, - error_msg, +pub async fn build(config: &Config, manifest: &Manifest) -> Result { + let container = manifest.container(); + let root = config.run_dir.join(container.to_string()); + + let capabilities = manifest.capabilities.clone(); + let console = manifest.console; + let gid = manifest.gid; + let groups = groups(manifest); + let mounts = prepare_mounts(config, &root, manifest).await?; + let rlimits = manifest.rlimits.clone(); + let seccomp = seccomp_filter(manifest); + let uid = manifest.uid; + + Ok(Init { + container, + root, + uid, + gid, + mounts, + groups, + capabilities, + rlimits, + seccomp, + console, + }) +} + +/// Generate a list of supplementary gids if the groups info can be retrieved. This +/// must happen before the init `clone` because the group information cannot be gathered +/// without `/etc` etc... +fn groups(manifest: &Manifest) -> Vec { + if let Some(groups) = manifest.suppl_groups.as_ref() { + let mut result = Vec::with_capacity(groups.len()); + for group in groups { + let cgroup = CString::new(group.as_str()).unwrap(); // Check during manifest parsing + let group_info = + unsafe { nix::libc::getgrnam(cgroup.as_ptr() as *const nix::libc::c_char) }; + if group_info == (null::() as *mut nix::libc::group) { + log::warn!("Skipping invalid supplementary group {}", group); + } else { + let gid = unsafe { (*group_info).gr_gid }; + // TODO: Are there gids cannot use? + result.push(gid) + } } + result + } else { + Vec::with_capacity(0) } +} - /// Execute this mount call - pub(super) fn mount(&self) { - nix::mount::mount( - self.source.as_ref(), - &self.target, - self.fstype.as_deref(), - // Safe because flags is private and only set in Mount::new via MsFlags::bits - unsafe { MsFlags::from_bits_unchecked(self.flags) }, - self.data.as_deref(), - ) - .expect(&self.error_msg); +/// Generate seccomp filter applied in init +fn seccomp_filter(manifest: &Manifest) -> Option { + if let Some(seccomp) = manifest.seccomp.as_ref() { + return Some(seccomp::seccomp_filter( + seccomp.profile.as_ref(), + seccomp.allow.as_ref(), + manifest.capabilities.as_ref(), + )); } + None } /// Iterate the mounts of a container and assemble a list of `mount` calls to be /// performed by init. Prepare an options persist dir. This fn fails if a resource /// is referenced that does not exist. -pub(super) async fn prepare_mounts( +async fn prepare_mounts( config: &Config, root: &Path, manifest: &Manifest, @@ -103,7 +124,7 @@ pub(super) async fn prepare_mounts( } fn proc(root: &Path, target: &Path) -> Mount { - debug!("Mounting proc on {}", target.display()); + log::debug!("Mounting proc on {}", target.display()); let source = PathBuf::from("proc"); let target = root.join_strip(target); let fstype = "proc"; @@ -115,7 +136,7 @@ fn bind(root: &Path, target: &Path, host: &Path, options: &MountOptions) -> Vec< if host.exists() { let rw = options.contains(&MountOption::Rw); let mut mounts = Vec::with_capacity(if rw { 2 } else { 1 }); - debug!( + log::debug!( "Mounting {} on {} with flags {}", host.display(), target.display(), @@ -140,7 +161,7 @@ fn bind(root: &Path, target: &Path, host: &Path, options: &MountOptions) -> Vec< } mounts } else { - debug!( + log::debug!( "Skipping bind mount of nonexistent source {} to {}", host.display(), target.display() @@ -157,13 +178,13 @@ async fn persist( gid: u16, ) -> Result { if !source.exists() { - debug!("Creating {}", source.display()); + log::debug!("Creating {}", source.display()); fs::create_dir_all(&source) .await .context(format!("Failed to create {}", source.display()))?; } - debug!("Chowning {} to {}:{}", source.display(), uid, gid); + log::debug!("Chowning {} to {}:{}", source.display(), uid, gid); unistd::chown( source.as_os_str(), Some(unistd::Uid::from_raw(uid.into())), @@ -176,7 +197,7 @@ async fn persist( gid ))?; - debug!("Mounting {} on {}", source.display(), target.display(),); + log::debug!("Mounting {} on {}", source.display(), target.display(),); let target = root.join_strip(target); let flags = MsFlags::MS_BIND | MsFlags::MS_NODEV | MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC; @@ -217,7 +238,7 @@ fn resource( dir }; - debug!( + log::debug!( "Mounting {} on {} with {}", src.display(), target.display(), @@ -236,7 +257,7 @@ fn resource( } fn tmpfs(root: &Path, target: &Path, size: u64) -> Mount { - debug!( + log::debug!( "Mounting tmpfs with size {} on {}", bytesize::ByteSize::b(size), target.display() @@ -261,3 +282,12 @@ fn options_to_flags(opt: &MountOptions) -> MsFlags { } flags } + +impl PathExt for Path { + fn join_strip>(&self, w: T) -> PathBuf { + self.join(match w.as_ref().strip_prefix("/") { + Ok(stripped) => stripped, + Err(_) => w.as_ref(), + }) + } +} diff --git a/northstar/src/runtime/process/init.rs b/northstar/src/runtime/fork/init/mod.rs similarity index 52% rename from northstar/src/runtime/process/init.rs rename to northstar/src/runtime/fork/init/mod.rs index c3a122765..c826c5775 100644 --- a/northstar/src/runtime/process/init.rs +++ b/northstar/src/runtime/fork/init/mod.rs @@ -1,68 +1,101 @@ -use super::{fs::Mount, io::Fd}; use crate::{ + common::container::Container, + debug, info, npk::manifest::{Capability, RLimitResource, RLimitValue}, runtime::{ - ipc::{channel::Channel, condition::ConditionNotify}, - ExitStatus, + fork::util::{self, fork, set_child_subreaper, set_log_target, set_process_name}, + ipc::{owned_fd::OwnedFd, Message as IpcMessage}, + ExitStatus, Pid, }, seccomp::AllowList, }; +pub use builder::build; +use derive_new::new; +use itertools::Itertools; use nix::{ errno::Errno, libc::{self, c_ulong}, + mount::MsFlags, sched::unshare, - sys::wait::{waitpid, WaitStatus}, - unistd::{self, Uid}, + sys::{ + signal::Signal, + wait::{waitpid, WaitStatus}, + }, + unistd, + unistd::Uid, }; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, env, ffi::CString, - os::unix::prelude::RawFd, + os::unix::{ + net::UnixStream, + prelude::{AsRawFd, RawFd}, + }, path::PathBuf, process::exit, }; +mod builder; + +// Message from the forker to init and response +#[derive(new, Debug, Serialize, Deserialize)] +pub enum Message { + /// The init process forked a new child with `pid` + Forked { pid: Pid }, + /// A child of init exited with `exit_status` + Exit { pid: Pid, exit_status: ExitStatus }, + /// Exec a new process + Exec { + path: PathBuf, + args: Vec, + env: Vec, + }, +} + #[derive(Clone, Debug, Serialize, Deserialize)] -pub(super) struct Init { +pub struct Init { + pub container: Container, pub root: PathBuf, - pub init: CString, - pub argv: Vec, - pub env: Vec, pub uid: u16, pub gid: u16, pub mounts: Vec, - pub fds: Vec<(RawFd, Fd)>, pub groups: Vec, pub capabilities: Option>, pub rlimits: Option>, pub seccomp: Option, + pub console: bool, } impl Init { - pub(super) fn run( - self, - condition_notify: ConditionNotify, - mut exit_status_channel: Channel, - ) -> ! { + pub fn run(self, mut stream: IpcMessage, console: Option) -> ! { + set_log_target(format!("northstar::init::{}", self.container)); + + // Become a subreaper + set_child_subreaper(true); + // Set the process name to init. This process inherited the process name // from the runtime - set_process_name(); + set_process_name(&format!("init-{}", self.container)); // Become a session group leader + debug!("Setting session id"); unistd::setsid().expect("Failed to call setsid"); // Enter mount namespace + debug!("Entering mount namespace"); unshare(nix::sched::CloneFlags::CLONE_NEWNS).expect("Failed to unshare NEWNS"); // Perform all mounts passed in mounts - mount(&self.mounts); + self.mount(); // Set the chroot to the containers root mount point + debug!("Chrooting to {}", self.root.display()); unistd::chroot(&self.root).expect("Failed to chroot"); // Set current working directory to root + debug!("Setting cwd to /"); env::set_current_dir("/").expect("Failed to set cwd to /"); // UID / GID @@ -75,65 +108,129 @@ impl Init { self.set_rlimits(); // No new privileges - set_no_new_privs(true); + Self::set_no_new_privs(true); // Capabilities self.drop_privileges(); - // Close and dup fds - self.file_descriptors(); + loop { + match stream.recv() { + Ok(Some(Message::Exec { + path, + args, + mut env, + })) => { + debug!("Execing {} {}", path.display(), args.iter().join(" ")); + + // The init process got adopted by the forker after the trampoline exited. It is + // safe to set the parent death signal now. + util::set_parent_death_signal(Signal::SIGKILL); + + if let Some(fd) = console.as_ref().map(AsRawFd::as_raw_fd) { + // Add the fd number to the environment of the application + env.push(format!("NORTHSTAR_CONSOLE={}", fd)); + } + + let io = stream.recv_fds::().expect("Failed to receive io"); + debug_assert!(io.len() == 3); + let stdin = io[0]; + let stdout = io[1]; + let stderr = io[2]; + + // Start new process inside the container + let pid = fork(|| { + set_log_target(format!("northstar::{}", self.container)); + util::set_parent_death_signal(Signal::SIGKILL); + + unistd::dup2(stdin, nix::libc::STDIN_FILENO).expect("Failed to dup2"); + unistd::dup2(stdout, nix::libc::STDOUT_FILENO).expect("Failed to dup2"); + unistd::dup2(stderr, nix::libc::STDERR_FILENO).expect("Failed to dup2"); - // Clone - match unsafe { unistd::fork() } { - Ok(result) => match result { - unistd::ForkResult::Parent { child } => { - // Drop checkpoint. The fds are cloned into the child and are closed upon execve. - drop(condition_notify); + unistd::close(stdin).expect("Failed to close stdout after dup2"); + unistd::close(stdout).expect("Failed to close stdout after dup2"); + unistd::close(stderr).expect("Failed to close stderr after dup2"); + + // Set seccomp filter + if let Some(ref filter) = self.seccomp { + filter.apply().expect("Failed to apply seccomp filter."); + } + + let path = CString::new(path.to_str().unwrap()).unwrap(); + let args = args + .iter() + .map(|s| CString::new(s.as_str()).unwrap()) + .collect::>(); + let env = env + .iter() + .map(|s| CString::new(s.as_str()).unwrap()) + .collect::>(); + + panic!( + "Execve: {:?} {:?}: {:?}", + &path, + &args, + unistd::execve(&path, &args, &env) + ) + }) + .expect("Failed to spawn child process"); + + // close fds + drop(console); + unistd::close(stdin).expect("Failed to close stdout"); + unistd::close(stdout).expect("Failed to close stdout"); + unistd::close(stderr).expect("Failed to close stderr"); + + let message = Message::Forked { pid }; + stream.send(&message).expect("Failed to send fork result"); // Wait for the child to exit loop { - match waitpid(Some(child), None) { + debug!("Waiting for child process {} to exit", pid); + match waitpid(Some(unistd::Pid::from_raw(pid as i32)), None) { Ok(WaitStatus::Exited(_pid, status)) => { + debug!("Child process {} exited with status code {}", pid, status); let exit_status = ExitStatus::Exit(status); - exit_status_channel - .send(&exit_status) - .expect("Failed to send exit status"); + stream + .send(Message::Exit { pid, exit_status }) + .expect("Channel error"); + + assert_eq!( + waitpid(Some(unistd::Pid::from_raw(pid as i32)), None), + Err(nix::Error::ECHILD) + ); + exit(0); } Ok(WaitStatus::Signaled(_pid, status, _)) => { + debug!("Child process {} exited with signal {}", pid, status); let exit_status = ExitStatus::Signalled(status as u8); - exit_status_channel - .send(&exit_status) - .expect("Failed to send exit status"); + stream + .send(Message::Exit { pid, exit_status }) + .expect("Channel error"); + + assert_eq!( + waitpid(Some(unistd::Pid::from_raw(pid as i32)), None), + Err(nix::Error::ECHILD) + ); + exit(0); } Ok(WaitStatus::Continued(_)) | Ok(WaitStatus::Stopped(_, _)) => { - continue + log::error!("Child process continued or stopped"); + continue; } Err(nix::Error::EINTR) => continue, - e => panic!("Failed to waitpid on {}: {:?}", child, e), + e => panic!("Failed to waitpid on {}: {:?}", pid, e), } } } - unistd::ForkResult::Child => { - drop(exit_status_channel); - - // Set seccomp filter - if let Some(ref filter) = self.seccomp { - filter.apply().expect("Failed to apply seccomp filter."); - } - - // Checkpoint fds are FD_CLOEXEC and act as a signal for the launcher that this child is started. - // Therefore no explicit drop (close) of _checkpoint_notify is needed here. - panic!( - "Execve: {:?} {:?}: {:?}", - &self.init, - &self.argv, - unistd::execve(&self.init, &self.argv, &self.env) - ) + Ok(None) => { + info!("Channel closed. Exiting..."); + std::process::exit(0); } - }, - Err(e) => panic!("Clone error: {}", e), + Ok(_) => unimplemented!("Unimplemented message"), + Err(e) => panic!("Failed to receive message: {}", e), + } } } @@ -149,10 +246,12 @@ impl Init { caps::securebits::set_keepcaps(true).expect("Failed to set keep caps"); } + debug!("Setting resgid {}", gid); let gid = unistd::Gid::from_raw(gid.into()); unistd::setresgid(gid, gid, gid).expect("Failed to set resgid"); let uid = unistd::Uid::from_raw(uid.into()); + debug!("Setting resuid {}", uid); unistd::setresuid(uid, uid, uid).expect("Failed to set resuid"); if rt_privileged { @@ -162,6 +261,7 @@ impl Init { } fn set_groups(&self) { + debug!("Setting groups {:?}", self.groups); let result = unsafe { nix::libc::setgroups(self.groups.len(), self.groups.as_ptr()) }; Errno::result(result) @@ -171,6 +271,7 @@ impl Init { fn set_rlimits(&self) { if let Some(limits) = self.rlimits.as_ref() { + debug!("Applying rlimits"); for (resource, limit) in limits { let resource = match resource { RLimitResource::AS => rlimit::Resource::AS, @@ -203,6 +304,7 @@ impl Init { /// Drop capabilities fn drop_privileges(&self) { + debug!("Dropping priviledges"); let mut bounded = caps::read(None, caps::CapSet::Bounding).expect("Failed to read bounding caps"); // Convert the set from the manifest to a set of caps::Capability @@ -231,54 +333,26 @@ impl Init { caps::set(None, caps::CapSet::Effective, &all).expect("Failed to reset effective caps"); } - /// Apply file descriptor configuration - fn file_descriptors(&self) { - for (fd, value) in &self.fds { - match value { - Fd::Close => { - // Ignore close errors because the fd list contains the ReadDir fd and fds from other tasks. - unistd::close(*fd).ok(); - } - Fd::Dup(n) => { - unistd::dup2(*n, *fd).expect("Failed to dup2"); - unistd::close(*n).expect("Failed to close"); - } - } + /// Execute list of mount calls + fn mount(&self) { + for mount in &self.mounts { + debug!("Mounting {:?} on {}", mount.source, mount.target.display()); + mount.mount(); } } -} -/// Execute list of mount calls -fn mount(mounts: &[Mount]) { - for mount in mounts { - mount.mount(); - } -} - -fn set_no_new_privs(value: bool) { - #[cfg(target_os = "android")] - pub const PR_SET_NO_NEW_PRIVS: libc::c_int = 38; - #[cfg(not(target_os = "android"))] - use libc::PR_SET_NO_NEW_PRIVS; + fn set_no_new_privs(value: bool) { + #[cfg(target_os = "android")] + pub const PR_SET_NO_NEW_PRIVS: libc::c_int = 38; + #[cfg(not(target_os = "android"))] + use libc::PR_SET_NO_NEW_PRIVS; - let result = unsafe { nix::libc::prctl(PR_SET_NO_NEW_PRIVS, value as c_ulong, 0, 0, 0) }; - Errno::result(result) - .map(drop) - .expect("Failed to set PR_SET_NO_NEW_PRIVS") -} - -#[cfg(target_os = "android")] -pub const PR_SET_NAME: libc::c_int = 15; -#[cfg(not(target_os = "android"))] -use libc::PR_SET_NAME; - -/// Set the name of the current process to "init" -fn set_process_name() { - let cname = "init\0"; - let result = unsafe { libc::prctl(PR_SET_NAME, cname.as_ptr() as c_ulong, 0, 0, 0) }; - Errno::result(result) - .map(drop) - .expect("Failed to set PR_SET_NAME"); + debug!("Setting no new privs"); + let result = unsafe { nix::libc::prctl(PR_SET_NO_NEW_PRIVS, value as c_ulong, 0, 0, 0) }; + Errno::result(result) + .map(drop) + .expect("Failed to set PR_SET_NO_NEW_PRIVS") + } } impl From for caps::Capability { @@ -328,3 +402,54 @@ impl From for caps::Capability { } } } + +/// Instructions for mount system call done in init +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Mount { + pub source: Option, + pub target: PathBuf, + pub fstype: Option, + pub flags: u64, + pub data: Option, + pub error_msg: String, +} + +impl Mount { + pub fn new( + source: Option, + target: PathBuf, + fstype: Option<&'static str>, + flags: MsFlags, + data: Option, + ) -> Mount { + let error_msg = format!( + "Failed to mount '{}' of type '{}' on '{}' with flags '{:?}' and data '{}'", + source.clone().unwrap_or_default().display(), + fstype.unwrap_or_default(), + target.display(), + flags, + data.clone().unwrap_or_default() + ); + Mount { + source, + target, + fstype: fstype.map(|s| s.to_string()), + flags: flags.bits(), + data, + error_msg, + } + } + + /// Execute this mount call + pub(super) fn mount(&self) { + nix::mount::mount( + self.source.as_ref(), + &self.target, + self.fstype.as_deref(), + // Safe because flags is private and only set in Mount::new via MsFlags::bits + unsafe { MsFlags::from_bits_unchecked(self.flags) }, + self.data.as_deref(), + ) + .expect(&self.error_msg); + } +} diff --git a/northstar/src/runtime/fork/mod.rs b/northstar/src/runtime/fork/mod.rs new file mode 100644 index 000000000..4f48b2c2b --- /dev/null +++ b/northstar/src/runtime/fork/mod.rs @@ -0,0 +1,5 @@ +mod forker; +mod init; +mod util; + +pub use forker::{start, Forker, ForkerChannels, Notification}; diff --git a/northstar/src/runtime/fork/util.rs b/northstar/src/runtime/fork/util.rs new file mode 100644 index 000000000..83f4d4e85 --- /dev/null +++ b/northstar/src/runtime/fork/util.rs @@ -0,0 +1,143 @@ +use crate::{ + debug, error, + runtime::{error::Error, Pid}, +}; +use nix::{ + errno::Errno, + libc::{self, c_ulong}, + sys::signal::Signal, + unistd::{self}, +}; +use std::process::exit; + +/// Set the parent death signal of the calling process +pub fn set_parent_death_signal(signal: Signal) { + #[cfg(target_os = "android")] + const PR_SET_PDEATHSIG: libc::c_int = 1; + #[cfg(not(target_os = "android"))] + use libc::PR_SET_PDEATHSIG; + + debug!("Setting parent death signal to {}", signal); + + let result = unsafe { nix::libc::prctl(PR_SET_PDEATHSIG, signal, 0, 0, 0) }; + Errno::result(result) + .map(drop) + .expect("Failed to set PR_SET_PDEATHSIG"); +} + +/// Set the name of the current process +pub fn set_process_name(name: &str) { + #[cfg(target_os = "android")] + const PR_SET_NAME: libc::c_int = 15; + #[cfg(not(target_os = "android"))] + use libc::PR_SET_NAME; + + debug!("Setting process name to {}", name); + + // PR_SET_NAME (since Linux 2.6.9) + // Set the name of the calling thread, using the value in the + // location pointed to by (char *) arg2. The name can be up + // to 16 bytes long, including the terminating null byte. + // (If the length of the string, including the terminating + // null byte, exceeds 16 bytes, the string is silently + // truncated.) This is the same attribute that can be set + // via pthread_setname_np(3) and retrieved using + // pthread_getname_np(3). The attribute is likewise + // accessible via /proc/self/task/[tid]/comm (see proc(5)), + // where [tid] is the thread ID of the calling thread, as + // returned by gettid(2). + let mut name = name.as_bytes().to_vec(); + name.truncate(15); + name.push(b'\0'); + + let result = unsafe { libc::prctl(PR_SET_NAME, name.as_ptr() as c_ulong, 0, 0, 0) }; + Errno::result(result) + .map(drop) + .expect("Failed to set PR_SET_NAME"); +} + +// Set the child subreaper flag of the calling thread +pub fn set_child_subreaper(value: bool) { + #[cfg(target_os = "android")] + const PR_SET_CHILD_SUBREAPER: nix::libc::c_int = 36; + #[cfg(not(target_os = "android"))] + use nix::libc::PR_SET_CHILD_SUBREAPER; + + debug!("Setting child subreaper flag to {}", value); + + let value = if value { 1u64 } else { 0u64 }; + let result = unsafe { nix::libc::prctl(PR_SET_CHILD_SUBREAPER, value, 0, 0, 0) }; + Errno::result(result) + .map(drop) + .expect("Failed to set child subreaper flag") +} + +/// Fork a new process. +/// +/// # Arguments: +/// +/// * `f` - The closure to run in the child process. +/// +pub fn fork(f: F) -> nix::Result +where + F: FnOnce() -> Result<(), Error>, +{ + match unsafe { unistd::fork()? } { + unistd::ForkResult::Parent { child } => Ok(child.as_raw() as Pid), + unistd::ForkResult::Child => match f() { + Ok(_) => exit(0), + Err(e) => { + error!("Failed after fork: {:?}", e); + exit(-1); + } + }, + } +} + +pub(crate) static mut LOG_TARGET: String = String::new(); + +pub(crate) fn set_log_target(tag: String) { + unsafe { + LOG_TARGET = tag; + } +} + +/// Log to debug +#[allow(unused)] +#[macro_export] +macro_rules! debug { + ($($arg:tt)+) => ( + assert!(unsafe { !crate::runtime::fork::util::LOG_TARGET.is_empty() }); + log::debug!(target: unsafe { crate::runtime::fork::util::LOG_TARGET.as_str() }, $($arg)+) + ) +} + +/// Log to info +#[allow(unused)] +#[macro_export] +macro_rules! info { + ($($arg:tt)+) => ( + assert!(unsafe { !crate::runtime::fork::util::LOG_TARGET.is_empty() }); + log::info!(target: unsafe { crate::runtime::fork::util::LOG_TARGET.as_str() }, $($arg)+) + ) +} + +/// Log to warn +#[allow(unused)] +#[macro_export] +macro_rules! warn { + ($($arg:tt)+) => ( + assert!(unsafe { !crate::runtime::fork::util::LOG_TARGET.is_empty() }); + log::warn!(target: unsafe { crate::runtime::fork::util::LOG_TARGET.as_str() }, $($arg)+) + ) +} + +/// Log to error +#[allow(unused)] +#[macro_export] +macro_rules! error { + ($($arg:tt)+) => ( + assert!(unsafe { !crate::runtime::fork::util::LOG_TARGET.is_empty() }); + log::error!(target: unsafe { crate::runtime::fork::util::LOG_TARGET.as_str() }, $($arg)+) + ) +} diff --git a/northstar/src/runtime/io.rs b/northstar/src/runtime/io.rs new file mode 100644 index 000000000..9876716a2 --- /dev/null +++ b/northstar/src/runtime/io.rs @@ -0,0 +1,133 @@ +use std::{ + os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd}, + path::{Path, PathBuf}, +}; + +use crate::{ + common::container::Container, + npk::manifest::{self, Output}, +}; +use log::debug; +use nix::{ + fcntl::OFlag, + pty, + sys::{stat::Mode, termios::SetArg}, +}; +use tokio::{ + io::{self, AsyncBufReadExt, AsyncRead}, + task::{self, JoinHandle}, +}; + +use super::ipc::owned_fd::{OwnedFd, OwnedFdRw}; + +pub struct ContainerIo { + pub io: [OwnedFd; 3], + /// A handle to the io forwarding task if stdout or stderr is set to `Output::Pipe` + pub log_task: Option>>, +} + +/// Create a new pty handle if configured in the manifest or open /dev/null instead. +pub async fn open(container: &Container, io: &manifest::Io) -> io::Result { + // Open dev null - needed in any case for stdin + let dev_null = openrw("/dev/null")?; + + // Don't start the output task if it is configured to be discarded + if io.stdout == Output::Discard && io.stderr == Output::Discard { + return Ok(ContainerIo { + io: [dev_null.clone()?, dev_null.clone()?, dev_null], + log_task: None, + }); + } + + debug!("Spawning output logging task for {}", container); + let (write, read) = output_device(OutputDevice::Socket)?; + + let log_target = format!("northstar::{}", container); + let log_task = task::spawn(log_lines(log_target, read)); + + let (stdout, stderr) = match (&io.stdout, &io.stderr) { + (Output::Discard, Output::Pipe) => (dev_null.clone()?, write), + (Output::Pipe, Output::Discard) => (write, dev_null.clone()?), + (Output::Pipe, Output::Pipe) => (write.clone()?, write), + _ => unreachable!(), + }; + + let io = [dev_null, stdout, stderr]; + + Ok(ContainerIo { + io, + log_task: Some(log_task), + }) +} + +/// Type of output device +enum OutputDevice { + Socket, + #[allow(dead_code)] + Pty, +} + +/// Open a device used to collect the container output and forward it to Northstar's log +fn output_device( + dev: OutputDevice, +) -> io::Result<(OwnedFd, Box)> { + match dev { + OutputDevice::Socket => { + let (msock, csock) = std::os::unix::net::UnixStream::pair()?; + let msock = { + msock.set_nonblocking(true)?; + tokio::net::UnixStream::from_std(msock)? + }; + Ok((csock.into(), Box::new(msock))) + } + OutputDevice::Pty => { + let (main, sec_path) = openpty(); + Ok((openrw(sec_path)?, Box::new(OwnedFdRw::new(main)?))) + } + } +} + +/// Open a path for reading and writing. +fn openrw>(f: T) -> io::Result { + nix::fcntl::open(f.as_ref(), OFlag::O_RDWR, Mode::empty()) + .map_err(|err| io::Error::from_raw_os_error(err as i32)) + .map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }) +} + +/// Create a new pty and return the main fd along with the sub name. +fn openpty() -> (OwnedFd, PathBuf) { + let main = pty::posix_openpt(OFlag::O_RDWR | OFlag::O_NOCTTY | OFlag::O_NONBLOCK) + .expect("Failed to open pty"); + + nix::sys::termios::tcgetattr(main.as_raw_fd()) + .map(|mut termios| { + nix::sys::termios::cfmakeraw(&mut termios); + termios + }) + .and_then(|termios| { + nix::sys::termios::tcsetattr(main.as_raw_fd(), SetArg::TCSANOW, &termios) + }) + .and_then(|_| pty::grantpt(&main)) + .and_then(|_| pty::unlockpt(&main)) + .expect("Failed to configure pty"); + + // Get the name of the sub + let sub = pty::ptsname_r(&main) + .map(PathBuf::from) + .expect("Failed to get PTY sub name"); + + debug!("Created PTY {}", sub.display()); + let main = unsafe { OwnedFd::from_raw_fd(main.into_raw_fd()) }; + + (main, sub) +} + +/// Pipe task: Read pty until stop is cancelled. Write linewist to `log`. +async fn log_lines(target: String, output: R) -> io::Result<()> { + let mut lines = io::BufReader::new(output).lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::debug!(target: &target, "{}", line); + } + + Ok(()) +} diff --git a/northstar/src/runtime/ipc/channel.rs b/northstar/src/runtime/ipc/channel.rs deleted file mode 100644 index f4656e85a..000000000 --- a/northstar/src/runtime/ipc/channel.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::{ - io::{ErrorKind, Read, Write}, - os::unix::prelude::{AsRawFd, RawFd}, -}; - -use bincode::Options; -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; -use serde::{de::DeserializeOwned, Serialize}; -use tokio::io::AsyncReadExt; - -use super::pipe::{pipe, AsyncPipeRead, PipeRead, PipeWrite}; - -/// Wrap a pipe to transfer bincoded structs that implement `Serialize` and `Deserialize` -pub struct Channel { - tx: PipeWrite, - rx: std::io::BufReader, -} - -impl Channel { - /// Create a new pipe channel - pub fn new() -> Channel { - let (rx, tx) = pipe().expect("Failed to create pipe"); - Channel { - tx, - rx: std::io::BufReader::new(rx), - } - } - - /// Raw fds for the rx and tx pipe - pub fn as_raw_fd(&self) -> (RawFd, RawFd) { - (self.tx.as_raw_fd(), self.rx.get_ref().as_raw_fd()) - } -} - -impl Channel { - /// Drops the tx part and returns a AsyncChannelRead - pub fn into_async_read(self) -> AsyncChannelRead { - let rx = self - .rx - .into_inner() - .try_into() - .expect("Failed to convert pipe read"); - AsyncChannelRead { - rx: tokio::io::BufReader::new(rx), - } - } - - /// Send v bincode serialized - pub fn send(&mut self, v: &T) -> std::io::Result<()> { - let buffer = bincode::DefaultOptions::new() - .with_fixint_encoding() - .serialize(v) - .map_err(|e| std::io::Error::new(ErrorKind::Other, e))?; - self.tx.write_u32::(buffer.len() as u32)?; - self.tx.write_all(&buffer) - } - - /// Receive bincode serialized - #[allow(unused)] - pub fn recv(&mut self) -> std::io::Result - where - T: DeserializeOwned, - { - let size = self.rx.read_u32::()?; - let mut buffer = vec![0; size as usize]; - self.rx.read_exact(&mut buffer)?; - bincode::DefaultOptions::new() - .with_fixint_encoding() - .deserialize(&buffer) - .map_err(|e| std::io::Error::new(ErrorKind::Other, e)) - } -} - -/// Async version of Channel -pub struct AsyncChannelRead { - rx: tokio::io::BufReader, -} - -impl AsyncChannelRead { - pub async fn recv<'de, T>(&mut self) -> std::io::Result - where - T: DeserializeOwned, - { - let size = self.rx.read_u32().await?; - let mut buffer = vec![0; size as usize]; - self.rx.read_exact(&mut buffer).await?; - bincode::DefaultOptions::new() - .with_fixint_encoding() - .deserialize(&buffer) - .map_err(|e| std::io::Error::new(ErrorKind::Other, e)) - } -} - -#[cfg(test)] -mod tests { - use nix::{sys::wait, unistd}; - - use super::*; - use crate::runtime::ExitStatus; - - #[tokio::test(flavor = "current_thread")] - async fn channel_async() { - let mut channel = Channel::new(); - - for i in 0..=255 { - channel.send(&std::time::Duration::from_secs(1)).unwrap(); - channel.send(&ExitStatus::Exit(i)).unwrap(); - channel.send(&ExitStatus::Signalled(10)).unwrap(); - } - - for i in 0..=255 { - assert_eq!( - channel.recv::().unwrap(), - std::time::Duration::from_secs(1) - ); - assert_eq!(channel.recv::().unwrap(), ExitStatus::Exit(i)); - assert_eq!( - channel.recv::().unwrap(), - ExitStatus::Signalled(10) - ); - } - - for i in 0..=255 { - channel.send(&std::time::Duration::from_secs(1)).unwrap(); - channel.send(&ExitStatus::Exit(i)).unwrap(); - channel.send(&ExitStatus::Signalled(10)).unwrap(); - } - - let mut channel = channel.into_async_read(); - for i in 0..=255 { - assert_eq!( - channel.recv::().await.unwrap(), - std::time::Duration::from_secs(1) - ); - assert_eq!( - channel.recv::().await.unwrap(), - ExitStatus::Exit(i) - ); - assert_eq!( - channel.recv::().await.unwrap(), - ExitStatus::Signalled(10) - ); - } - } - - #[test] - fn channel_fork() { - let mut channel = Channel::new(); - - match unsafe { unistd::fork().expect("Failed to fork") } { - unistd::ForkResult::Parent { child } => { - wait::waitpid(Some(child), None).expect("Failed to waitpid"); - for i in 0..=255i32 { - assert_eq!( - channel.recv::().unwrap(), - std::time::Duration::from_secs(i as u64) - ); - assert_eq!(channel.recv::().unwrap(), ExitStatus::Exit(i)); - assert_eq!( - channel.recv::().unwrap(), - ExitStatus::Signalled(10) - ); - } - } - unistd::ForkResult::Child => { - for i in 0..=255i32 { - channel - .send(&std::time::Duration::from_secs(i as u64)) - .unwrap(); - channel.send(&ExitStatus::Exit(i)).unwrap(); - channel.send(&ExitStatus::Signalled(10)).unwrap(); - } - std::process::exit(0); - } - } - } - - #[tokio::test(flavor = "current_thread")] - async fn channel_fork_close() { - let channel = Channel::new(); - match unsafe { unistd::fork().expect("Failed to fork") } { - unistd::ForkResult::Parent { child } => { - let mut channel = channel.into_async_read(); - wait::waitpid(Some(child), None).expect("Failed to waitpid"); - assert!(channel.recv::().await.is_err()); - } - unistd::ForkResult::Child => { - drop(channel); - std::process::exit(0); - } - } - } - - #[tokio::test(flavor = "current_thread")] - async fn channel_close() { - let channel = Channel::new(); - // Converting into a AsyncChannelRead closes the sending part - let mut channel = channel.into_async_read(); - assert!(channel.recv::().await.is_err()); - } -} diff --git a/northstar/src/runtime/ipc/condition.rs b/northstar/src/runtime/ipc/condition.rs deleted file mode 100644 index 2bd481711..000000000 --- a/northstar/src/runtime/ipc/condition.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::{ - io::Result, - os::unix::prelude::{AsRawFd, IntoRawFd, RawFd}, -}; - -use tokio::io::AsyncReadExt; - -use super::{ - pipe::{pipe, AsyncPipeRead, PipeRead, PipeWrite}, - raw_fd_ext::RawFdExt, -}; - -#[allow(unused)] -#[derive(Debug)] -pub struct Condition { - read: PipeRead, - write: PipeWrite, -} - -#[allow(unused)] -impl Condition { - pub fn new() -> Result { - let (rfd, wfd) = pipe()?; - - Ok(Condition { - read: rfd, - write: wfd, - }) - } - - pub fn set_cloexec(&self) { - self.read.set_cloexec(true); - self.write.set_cloexec(true); - } - - pub fn wait(mut self) { - drop(self.write); - let buf: &mut [u8] = &mut [0u8; 1]; - use std::io::Read; - loop { - match self.read.read(buf) { - Ok(n) if n == 0 => break, - Ok(_) => continue, - Err(e) => break, - } - } - } - - pub fn notify(self) {} - - pub fn split(self) -> (ConditionWait, ConditionNotify) { - ( - ConditionWait { read: self.read }, - ConditionNotify { write: self.write }, - ) - } -} - -#[derive(Debug)] -pub struct ConditionWait { - read: PipeRead, -} - -impl ConditionWait { - #[allow(unused)] - pub fn wait(mut self) { - use std::io::Read; - loop { - match self.read.read(&mut [0u8; 1]) { - Ok(n) if n == 0 => break, - Ok(_) => continue, - Err(_) => break, - } - } - } - - pub async fn async_wait(self) { - let mut read: AsyncPipeRead = self.read.try_into().unwrap(); - loop { - match read.read(&mut [0u8; 1]).await { - Ok(n) if n == 0 => break, - Ok(_) => continue, - Err(_) => break, - } - } - } -} - -impl AsRawFd for ConditionWait { - fn as_raw_fd(&self) -> RawFd { - self.read.as_raw_fd() - } -} - -impl IntoRawFd for ConditionWait { - fn into_raw_fd(self) -> RawFd { - self.read.into_raw_fd() - } -} - -#[derive(Debug)] -pub struct ConditionNotify { - write: PipeWrite, -} - -impl ConditionNotify { - #[allow(unused)] - pub fn notify(self) { - drop(self.write) - } -} - -impl AsRawFd for ConditionNotify { - fn as_raw_fd(&self) -> RawFd { - self.write.as_raw_fd() - } -} - -impl IntoRawFd for ConditionNotify { - fn into_raw_fd(self) -> RawFd { - self.write.into_raw_fd() - } -} - -#[cfg(test)] -mod tests { - use nix::unistd; - - use super::*; - - #[test] - fn condition() { - let (w0, n0) = Condition::new().unwrap().split(); - let (w1, n1) = Condition::new().unwrap().split(); - - match unsafe { unistd::fork().unwrap() } { - unistd::ForkResult::Parent { .. } => { - drop(w0); - drop(n1); - - n0.notify(); - w1.wait(); - } - unistd::ForkResult::Child => { - drop(n0); - drop(w1); - - w0.wait(); - n1.notify(); - std::process::exit(0); - } - } - } -} diff --git a/northstar/src/runtime/ipc/message.rs b/northstar/src/runtime/ipc/message.rs new file mode 100644 index 000000000..c1ee5000e --- /dev/null +++ b/northstar/src/runtime/ipc/message.rs @@ -0,0 +1,423 @@ +use bincode::Options; +use byteorder::{BigEndian, WriteBytesExt}; +use bytes::{BufMut, BytesMut}; +use lazy_static::lazy_static; +use nix::{ + cmsg_space, + sys::{ + socket::{self, ControlMessageOwned}, + uio, + }, +}; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + io::{self, ErrorKind, Read, Write}, + mem::MaybeUninit, + os::unix::prelude::{AsRawFd, FromRawFd, RawFd}, +}; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, Interest}, + net::UnixStream, +}; + +lazy_static! { + static ref OPTIONS: bincode::DefaultOptions = bincode::DefaultOptions::new(); +} + +/// Bincode encoded and length delimited message stream via Read/Write +pub struct Message { + inner: T, +} + +impl Message { + /// Send bincode encoded message with a length field + pub fn send(&mut self, v: M) -> io::Result<()> { + let size = OPTIONS + .serialized_size(&v) + .map_err(|e| io::Error::new(ErrorKind::Other, e))?; + self.inner.write_u32::(size as u32)?; + OPTIONS + .serialize_into(&mut self.inner, &v) + .map_err(|e| io::Error::new(ErrorKind::Other, e)) + } + + /// Receive a bincode encoded message with a length field + pub fn recv(&mut self) -> io::Result> { + let mut buffer = [0u8; 4]; + let mut read = 0; + while read < 4 { + match self.inner.read(&mut buffer[read..])? { + 0 => return Ok(None), + n => read += n, + } + } + let size = u32::from_be_bytes(buffer); + let mut buffer = vec![0; size as usize]; + self.inner.read_exact(&mut buffer)?; + OPTIONS + .deserialize(&buffer) + .map(Some) + .map_err(|e| io::Error::new(ErrorKind::Other, e)) + } +} + +impl Message { + /// Send a file descriptor over the socket + #[allow(unused)] + pub fn send_fds(&self, fds: &[T]) -> io::Result<()> { + let buf = &[0u8]; + let iov = &[uio::IoVec::from_slice(buf)]; + let fds = fds.iter().map(AsRawFd::as_raw_fd).collect::>(); + let cmsg = [socket::ControlMessage::ScmRights(&fds)]; + const FLAGS: socket::MsgFlags = socket::MsgFlags::empty(); + + socket::sendmsg(self.inner.as_raw_fd(), iov, &cmsg, FLAGS, None) + .map_err(os_err) + .map(drop) + } + + /// Receive a file descriptor via the socket + pub fn recv_fds(&self) -> io::Result<[T; N]> { + let mut buf = [0u8]; + let iov = &[uio::IoVec::from_mut_slice(&mut buf)]; + let mut cmsg_buffer = cmsg_space!([RawFd; N]); + const FLAGS: socket::MsgFlags = socket::MsgFlags::empty(); + + let message = socket::recvmsg(self.inner.as_raw_fd(), iov, Some(&mut cmsg_buffer), FLAGS) + .map_err(os_err)?; + + recv_control_msg::(message.cmsgs().next()) + } +} + +impl From for Message +where + T: Read + Write, +{ + fn from(inner: T) -> Self { + Message { inner } + } +} + +#[derive(Debug)] +pub struct AsyncMessage { + inner: T, + read_buffer: BytesMut, + write_buffer: BytesMut, +} + +impl AsyncMessage { + // Cancel safe send + pub async fn send(&mut self, v: M) -> io::Result<()> { + if self.write_buffer.is_empty() { + // Calculate the serialized message size + let size = OPTIONS + .serialized_size(&v) + .map_err(|e| io::Error::new(ErrorKind::Other, e))?; + self.write_buffer.reserve(4 + size as usize); + self.write_buffer.put_u32(size as u32); + + // Serialize the message + let buffer = OPTIONS + .serialize(&v) + .map_err(|e| io::Error::new(ErrorKind::Other, e))?; + self.write_buffer.extend_from_slice(&buffer); + } + + while !self.write_buffer.is_empty() { + let n = self.inner.write(&self.write_buffer).await?; + drop(self.write_buffer.split_to(n)); + } + Ok(()) + } + + // Cancel safe recv + pub async fn recv<'de, M: DeserializeOwned>(&mut self) -> io::Result> { + while self.read_buffer.len() < 4 { + let remaining = 4 - self.read_buffer.len(); + let mut buffer = BytesMut::with_capacity(remaining); + match self.inner().read_buf(&mut buffer).await? { + 0 => return Ok(None), + _ => self.read_buffer.extend_from_slice(&buffer), + } + } + + // Parse the message size + let msg_len = u32::from_be_bytes(self.read_buffer[..4].try_into().unwrap()) as usize; + + // Read until the read buffer has this length + let target_buffer_len = msg_len as usize + 4; + + while self.read_buffer.len() < target_buffer_len { + // Calculate how may bytes are missing to read the message + let remaining = target_buffer_len - self.read_buffer.len(); + let mut buffer = BytesMut::with_capacity(remaining); + match self.inner().read_buf(&mut buffer).await? { + 0 => return Ok(None), + _ => self.read_buffer.extend_from_slice(&buffer), + } + } + + let message = OPTIONS + .deserialize(&self.read_buffer[4..]) + .map(Some) + .map_err(|e| io::Error::new(ErrorKind::Other, e))?; + + self.read_buffer.clear(); + + Ok(message) + } + + pub fn inner(&mut self) -> &mut T { + &mut self.inner + } +} + +impl AsyncMessage { + /// Send a file descriptor via the stream. Ensure that fd is open until this fn returns. + pub async fn send_fds(&self, fds: &[T]) -> io::Result<()> { + assert!(self.write_buffer.is_empty()); + + loop { + self.inner.writable().await?; + + match self.inner.try_io(Interest::WRITABLE, || { + let buf = [0u8]; + let iov = &[uio::IoVec::from_slice(&buf)]; + + let fds = fds.iter().map(AsRawFd::as_raw_fd).collect::>(); + let cmsg = [socket::ControlMessage::ScmRights(&fds)]; + + let flags = socket::MsgFlags::MSG_DONTWAIT; + + socket::sendmsg(self.inner.as_raw_fd(), iov, &cmsg, flags, None).map_err(os_err) + }) { + Ok(_) => break Ok(()), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue, + Err(e) => break Err(e), + } + } + } + + /// Receive a file descriptor via the stream and convert it to T + pub async fn recv_fds(&self) -> io::Result<[T; N]> { + assert!(self.read_buffer.is_empty()); + + loop { + self.inner.readable().await?; + + let mut buf = [0u8]; + let iov = &[uio::IoVec::from_mut_slice(&mut buf)]; + let mut cmsg_buffer = cmsg_space!([RawFd; N]); + let flags = socket::MsgFlags::MSG_DONTWAIT; + + match self.inner.try_io(Interest::READABLE, || { + socket::recvmsg(self.inner.as_raw_fd(), iov, Some(&mut cmsg_buffer), flags) + .map_err(os_err) + }) { + Ok(message) => break recv_control_msg::(message.cmsgs().next()), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue, + Err(e) => break Err(e), + } + } + } +} + +#[inline] +fn os_err(err: nix::Error) -> io::Error { + io::Error::from_raw_os_error(err as i32) +} + +impl From for AsyncMessage { + fn from(inner: UnixStream) -> Self { + Self { + inner, + write_buffer: BytesMut::new(), + read_buffer: BytesMut::new(), + } + } +} + +impl TryFrom for AsyncMessage { + type Error = io::Error; + + fn try_from(inner: std::os::unix::net::UnixStream) -> io::Result { + inner.set_nonblocking(true)?; + let inner = UnixStream::from_std(inner)?; + Ok(AsyncMessage { + inner, + write_buffer: BytesMut::new(), + read_buffer: BytesMut::new(), + }) + } +} + +fn recv_control_msg( + message: Option, +) -> io::Result<[T; N]> { + match message { + Some(socket::ControlMessageOwned::ScmRights(fds)) => { + let mut result: [MaybeUninit; N] = unsafe { MaybeUninit::uninit().assume_init() }; + + for (fd, result) in fds.iter().zip(&mut result) { + result.write(unsafe { T::from_raw_fd(*fd) }); + } + + let ptr = &mut result as *mut _ as *mut [T; N]; + let res = unsafe { ptr.read() }; + core::mem::forget(result); + Ok(res) + } + Some(message) => Err(io::Error::new( + io::ErrorKind::Other, + format!( + "Failed to receive fd: unexpected control message: {:?}", + message + ), + )), + None => Err(io::Error::new( + io::ErrorKind::Other, + format!( + "Failed to receive fd: missing control message: {:?}", + message + ), + )), + } +} + +#[cfg(test)] +mod test { + use std::{io::Seek, process::exit}; + + use nix::unistd::close; + use tokio::{io::AsyncSeekExt, runtime::Builder}; + + use super::*; + + #[test] + fn send_recv_fd_async() { + let mut fd0 = nix::sys::memfd::memfd_create( + &std::ffi::CString::new("hello").unwrap(), + nix::sys::memfd::MemFdCreateFlag::empty(), + ) + .unwrap(); + let mut fd1 = nix::sys::memfd::memfd_create( + &std::ffi::CString::new("again").unwrap(), + nix::sys::memfd::MemFdCreateFlag::empty(), + ) + .unwrap(); + + let mut pair = super::super::socket_pair().unwrap(); + + const ITERATONS: usize = 100_000; + + match unsafe { nix::unistd::fork() }.unwrap() { + nix::unistd::ForkResult::Parent { child: _ } => { + let parent = pair.first(); + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + rt.block_on(async move { + let stream = AsyncMessage::try_from(parent).unwrap(); + + // Send and receive the fds a couple of times + for _ in 0..ITERATONS { + stream.send_fds(&[fd0, fd1]).await.unwrap(); + close(fd0).unwrap(); + close(fd1).unwrap(); + + let fds = stream.recv_fds::().await.unwrap(); + fd0 = fds[0]; + fd1 = fds[1]; + } + + // Done - check fd content + + let mut buf = String::new(); + let mut file0 = unsafe { tokio::fs::File::from_raw_fd(fd0) }; + file0.seek(io::SeekFrom::Start(0)).await.unwrap(); + file0.read_to_string(&mut buf).await.unwrap(); + assert_eq!(buf, "hello"); + + let mut buf = String::new(); + let mut file1 = unsafe { tokio::fs::File::from_raw_fd(fd1) }; + file1.seek(io::SeekFrom::Start(0)).await.unwrap(); + file1.read_to_string(&mut buf).await.unwrap(); + assert_eq!(buf, "again"); + }); + } + nix::unistd::ForkResult::Child => { + let child = pair.second(); + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + rt.block_on(async move { + let stream = AsyncMessage::try_from(child).unwrap(); + + // Send and receive the fds a couple of times + for _ in 0..ITERATONS { + let mut files = stream.recv_fds::().await.unwrap(); + + files[0].seek(io::SeekFrom::Start(0)).await.unwrap(); + files[0].write_all(b"hello").await.unwrap(); + files[0].flush().await.unwrap(); + + files[1].seek(io::SeekFrom::Start(0)).await.unwrap(); + files[1].write_all(b"again").await.unwrap(); + files[1].flush().await.unwrap(); + + // Send it back + stream.send_fds(&files).await.unwrap(); + } + }); + exit(0); + } + } + } + + #[test] + fn send_recv_fd_blocking() { + let mut fd = nix::sys::memfd::memfd_create( + &std::ffi::CString::new("foo").unwrap(), + nix::sys::memfd::MemFdCreateFlag::empty(), + ) + .unwrap(); + + let mut pair = super::super::socket_pair().unwrap(); + + const ITERATONS: usize = 100_000; + + match unsafe { nix::unistd::fork() }.unwrap() { + nix::unistd::ForkResult::Parent { child: _ } => { + let parent = pair.first(); + let stream = Message::try_from(parent).unwrap(); + for _ in 0..ITERATONS { + stream.send_fds(&[fd]).unwrap(); + close(fd).unwrap(); + fd = stream.recv_fds::().unwrap()[0]; + } + + // Done - check fd content + let mut file = unsafe { std::fs::File::from_raw_fd(fd) }; + file.seek(io::SeekFrom::Start(0)).unwrap(); + let mut buf = String::new(); + file.read_to_string(&mut buf).unwrap(); + assert_eq!(buf, "hello"); + } + nix::unistd::ForkResult::Child => { + let child = pair.second(); + let mut stream = Message::try_from(child).unwrap(); + for _ in 0..ITERATONS { + let mut file = stream.recv_fds::().unwrap(); + + // Write some bytes in to the fd + file[0].seek(io::SeekFrom::Start(0)).unwrap(); + file[0].write_all(b"hello").unwrap(); + file[0].flush().unwrap(); + + // Send it back + stream.send_fds(&[file[0].as_raw_fd()]).unwrap(); + drop(file); + } + stream.recv::().ok(); + exit(0); + } + } + } +} diff --git a/northstar/src/runtime/ipc/mod.rs b/northstar/src/runtime/ipc/mod.rs index 8916f9b12..9da2e7e94 100644 --- a/northstar/src/runtime/ipc/mod.rs +++ b/northstar/src/runtime/ipc/mod.rs @@ -1,4 +1,8 @@ -pub mod channel; -pub mod condition; -pub mod pipe; -pub mod raw_fd_ext; +mod message; +pub mod owned_fd; +mod raw_fd_ext; +mod socket_pair; + +pub use message::{AsyncMessage, Message}; +pub use raw_fd_ext::RawFdExt; +pub use socket_pair::socket_pair; diff --git a/northstar/src/runtime/ipc/owned_fd.rs b/northstar/src/runtime/ipc/owned_fd.rs new file mode 100644 index 000000000..57f50c3fa --- /dev/null +++ b/northstar/src/runtime/ipc/owned_fd.rs @@ -0,0 +1,177 @@ +//! Owned Unix-like file descriptors. + +use std::{ + fmt, + io::ErrorKind, + os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd, RawFd}, + pin::Pin, + task::{Context, Poll}, +}; + +use nix::{libc, unistd::dup}; +use std::io; +use tokio::io::{unix::AsyncFd, AsyncRead, AsyncWrite, ReadBuf}; + +use super::RawFdExt; + +/// Owned raw fd that closes on drop. +pub struct OwnedFd { + inner: RawFd, +} + +impl OwnedFd { + #[inline] + pub fn clone(&self) -> io::Result { + dup(self.inner) + .map(|inner| Self { inner }) + .map_err(|err| io::Error::from_raw_os_error(err as i32)) + } +} + +impl AsRawFd for OwnedFd { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.inner + } +} + +impl IntoRawFd for OwnedFd { + #[inline] + fn into_raw_fd(self) -> RawFd { + self.inner + } +} + +impl FromRawFd for OwnedFd { + /// Constructs a new instance of `Self` from the given raw file descriptor. + /// + /// # Safety + /// + /// The resource pointed to by `fd` must be open and suitable for assuming + /// ownership. The resource must not require any cleanup other than `close`. + #[inline] + unsafe fn from_raw_fd(inner: RawFd) -> Self { + assert_ne!(inner, u32::MAX as RawFd); + Self { inner } + } +} + +impl Drop for OwnedFd { + #[inline] + fn drop(&mut self) { + unsafe { + // Note that errors are ignored when closing a file descriptor. The + // reason for this is that if an error occurs we don't actually know if + // the file descriptor was closed or not, and if we retried (for + // something like EINTR), we might close another valid file descriptor + // opened after we closed ours. + let _ = libc::close(self.inner); + } + } +} + +impl fmt::Debug for OwnedFd { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OwnedFd").field("fd", &self.inner).finish() + } +} + +impl From for OwnedFd { + #[inline] + fn from(stream: std::os::unix::net::UnixStream) -> Self { + unsafe { Self::from_raw_fd(stream.into_raw_fd()) } + } +} + +pub struct OwnedFdRw { + inner: AsyncFd, +} + +impl OwnedFdRw { + pub fn new(inner: OwnedFd) -> io::Result { + inner.set_nonblocking(true)?; + AsyncFd::new(inner).map(|inner| Self { inner }) + } +} + +impl AsRawFd for OwnedFdRw { + #[inline] + fn as_raw_fd(&self) -> RawFd { + self.inner.as_raw_fd() + } +} + +impl AsyncRead for OwnedFdRw { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + loop { + let mut ready = match self.inner.poll_read_ready(cx) { + Poll::Ready(x) => x?, + Poll::Pending => return Poll::Pending, + }; + + let ret = unsafe { + nix::libc::read( + self.as_raw_fd(), + buf.unfilled_mut() as *mut _ as _, + buf.remaining(), + ) + }; + + return if ret < 0 { + let e = io::Error::last_os_error(); + if e.kind() == ErrorKind::WouldBlock { + ready.clear_ready(); + continue; + } else { + Poll::Ready(Err(e)) + } + } else { + let n = ret as usize; + unsafe { buf.assume_init(n) }; + buf.advance(n); + Poll::Ready(Ok(())) + }; + } + } +} + +impl AsyncWrite for OwnedFdRw { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + loop { + let mut ready = match self.inner.poll_write_ready(cx) { + Poll::Ready(x) => x?, + Poll::Pending => return Poll::Pending, + }; + + let ret = unsafe { nix::libc::write(self.as_raw_fd(), buf.as_ptr() as _, buf.len()) }; + + return if ret < 0 { + let e = io::Error::last_os_error(); + if e.kind() == ErrorKind::WouldBlock { + ready.clear_ready(); + continue; + } else { + Poll::Ready(Err(e)) + } + } else { + Poll::Ready(Ok(ret as usize)) + }; + } + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/northstar/src/runtime/ipc/pipe.rs b/northstar/src/runtime/ipc/pipe.rs deleted file mode 100644 index 4d6042bb1..000000000 --- a/northstar/src/runtime/ipc/pipe.rs +++ /dev/null @@ -1,345 +0,0 @@ -use futures::ready; -use nix::unistd; -use std::{ - convert::TryFrom, - io, - io::Result, - mem, - os::unix::io::{AsRawFd, IntoRawFd, RawFd}, - pin::Pin, - task::{Context, Poll}, -}; -use tokio::io::{unix::AsyncFd, AsyncRead, AsyncWrite, ReadBuf}; - -use super::raw_fd_ext::RawFdExt; - -#[derive(Debug)] -struct Inner { - fd: RawFd, -} - -impl Drop for Inner { - fn drop(&mut self) { - unistd::close(self.fd).ok(); - } -} - -impl From for Inner { - fn from(fd: RawFd) -> Self { - Inner { fd } - } -} - -/// Opens a pipe(2) with both ends blocking -pub fn pipe() -> Result<(PipeRead, PipeWrite)> { - unistd::pipe().map_err(from_nix).map(|(read, write)| { - ( - PipeRead { inner: read.into() }, - PipeWrite { - inner: write.into(), - }, - ) - }) -} - -/// Read end of a pipe(2). Last dropped clone closes the pipe -#[derive(Debug)] -pub struct PipeRead { - inner: Inner, -} - -impl io::Read for PipeRead { - fn read(&mut self, buf: &mut [u8]) -> Result { - unistd::read(self.as_raw_fd(), buf).map_err(from_nix) - } -} - -impl AsRawFd for PipeRead { - fn as_raw_fd(&self) -> RawFd { - self.inner.fd - } -} - -impl IntoRawFd for PipeRead { - fn into_raw_fd(self) -> RawFd { - let fd = self.inner.fd; - mem::forget(self); - fd - } -} - -/// Write end of a pipe(2). Last dropped clone closes the pipe -#[derive(Debug)] -pub struct PipeWrite { - inner: Inner, -} - -impl io::Write for PipeWrite { - fn write(&mut self, buf: &[u8]) -> Result { - unistd::write(self.as_raw_fd(), buf).map_err(from_nix) - } - - fn flush(&mut self) -> Result<()> { - unistd::fsync(self.as_raw_fd()).map_err(from_nix) - } -} - -impl AsRawFd for PipeWrite { - fn as_raw_fd(&self) -> RawFd { - self.inner.fd - } -} - -impl IntoRawFd for PipeWrite { - fn into_raw_fd(self) -> RawFd { - let fd = self.inner.fd; - mem::forget(self); - fd - } -} - -/// Pipe's synchronous reading end -#[derive(Debug)] -pub struct AsyncPipeRead { - inner: AsyncFd, -} - -impl TryFrom for AsyncPipeRead { - type Error = io::Error; - - fn try_from(reader: PipeRead) -> Result { - reader.set_nonblocking(); - Ok(AsyncPipeRead { - inner: AsyncFd::new(reader)?, - }) - } -} - -impl AsyncRead for AsyncPipeRead { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - loop { - let mut guard = ready!(self.inner.poll_read_ready(cx))?; - match guard.try_io(|inner| { - let fd = inner.get_ref().as_raw_fd(); - // map nix::Error to io::Error - match unistd::read(fd, buf.initialized_mut()) { - Ok(n) => Ok(n), - // read(2) on a nonblocking file (O_NONBLOCK) returns EAGAIN or EWOULDBLOCK in - // case that the read would block. That case is handled by `try_io`. - Err(e) => Err(from_nix(e)), - } - }) { - Ok(Ok(n)) => { - buf.advance(n); - return Poll::Ready(Ok(())); - } - Ok(Err(e)) if e.kind() == io::ErrorKind::WouldBlock => { - return Poll::Pending; - } - Ok(Err(e)) => { - return Poll::Ready(Err(e)); - } - Err(_would_block) => continue, - } - } - } -} - -/// Pipe's asynchronous writing end -#[derive(Debug)] -pub struct AsyncPipeWrite { - inner: AsyncFd, -} - -impl TryFrom for AsyncPipeWrite { - type Error = io::Error; - - fn try_from(write: PipeWrite) -> Result { - write.set_nonblocking(); - Ok(AsyncPipeWrite { - inner: AsyncFd::new(write)?, - }) - } -} - -impl AsyncWrite for AsyncPipeWrite { - fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - loop { - let mut guard = ready!(self.inner.poll_write_ready(cx))?; - match guard.try_io(|inner| unistd::write(inner.as_raw_fd(), buf).map_err(from_nix)) { - Ok(result) => return Poll::Ready(result), - Err(_would_block) => continue, - } - } - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - unistd::fsync(self.inner.as_raw_fd()).map_err(from_nix)?; - Poll::Ready(Ok(())) - } - - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } -} - -/// Maps an nix::Error to a io::Error -fn from_nix(error: nix::Error) -> io::Error { - match error { - nix::Error::EAGAIN => io::Error::from(io::ErrorKind::WouldBlock), - _ => io::Error::new(io::ErrorKind::Other, error), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::{ - convert::TryInto, - io::{Read, Write}, - process, thread, - }; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - #[test] - /// Smoke test - fn smoke() { - let (mut read, mut write) = pipe().unwrap(); - - write.write_all(b"Hello").unwrap(); - - let mut buf = [0u8; 5]; - read.read_exact(&mut buf).unwrap(); - - assert_eq!(&buf, b"Hello"); - } - - #[test] - /// Closing the write end must produce EOF on the read end - fn close() { - let (mut read, mut write) = pipe().unwrap(); - - write.write_all(b"Hello").unwrap(); - drop(write); - - let mut buf = String::new(); - // Read::read_to_string reads until EOF - read.read_to_string(&mut buf).unwrap(); - - assert_eq!(&buf, "Hello"); - } - - #[test] - #[should_panic] - /// Dropping the write end must reault in an EOF - fn drop_writer() { - let (mut read, write) = pipe().unwrap(); - drop(write); - assert!(matches!(read.read_exact(&mut [0u8; 1]), Ok(()))); - } - - #[test] - #[should_panic] - /// Dropping the read end must reault in an error on write - fn drop_reader() { - let (read, mut write) = pipe().unwrap(); - drop(read); - loop { - write.write_all(b"test").expect("Failed to send"); - } - } - - #[test] - /// Read and write bytes - fn read_write() { - let (mut read, mut write) = pipe().unwrap(); - - let writer = thread::spawn(move || { - for n in 0..=65535u32 { - write.write_all(&n.to_be_bytes()).unwrap(); - } - }); - - let mut buf = [0u8; 4]; - for n in 0..=65535u32 { - read.read_exact(&mut buf).unwrap(); - assert_eq!(buf, n.to_be_bytes()); - } - - writer.join().unwrap(); - } - - #[tokio::test] - /// Test async version of read and write - async fn r#async() { - let (read, write) = pipe().unwrap(); - - let mut read: AsyncPipeRead = read.try_into().unwrap(); - let mut write: AsyncPipeWrite = write.try_into().unwrap(); - - let write = tokio::spawn(async move { - for n in 0..=65535u32 { - write.write_all(&n.to_be_bytes()).await.unwrap(); - } - }); - - let mut buf = [0u8; 4]; - for n in 0..=65535u32 { - read.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, n.to_be_bytes()); - } - - write.await.unwrap() - } - - #[test] - /// Fork test - fn fork() { - let (mut read, mut write) = pipe().unwrap(); - - match unsafe { unistd::fork().unwrap() } { - unistd::ForkResult::Parent { child } => { - drop(read); - for n in 0..=65535u32 { - write.write_all(&n.to_be_bytes()).unwrap(); - } - nix::sys::wait::waitpid(child, None).ok(); - } - unistd::ForkResult::Child => { - drop(write); - let mut buf = [0u8; 4]; - for n in 0..=65535u32 { - read.read_exact(&mut buf).unwrap(); - assert_eq!(buf, n.to_be_bytes()); - } - process::exit(0); - } - } - - // And the other way round... - let (mut read, mut write) = pipe().unwrap(); - - match unsafe { unistd::fork().unwrap() } { - unistd::ForkResult::Parent { child } => { - drop(write); - let mut buf = [0u8; 4]; - for n in 0..=65535u32 { - read.read_exact(&mut buf).unwrap(); - assert_eq!(buf, n.to_be_bytes()); - } - nix::sys::wait::waitpid(child, None).ok(); - } - unistd::ForkResult::Child => { - drop(read); - for n in 0..=65535u32 { - write.write_all(&n.to_be_bytes()).unwrap(); - } - process::exit(0); - } - } - } -} diff --git a/northstar/src/runtime/ipc/raw_fd_ext.rs b/northstar/src/runtime/ipc/raw_fd_ext.rs index 2bd482da4..86792360f 100644 --- a/northstar/src/runtime/ipc/raw_fd_ext.rs +++ b/northstar/src/runtime/ipc/raw_fd_ext.rs @@ -1,35 +1,33 @@ -use std::{io, io::Result, os::unix::prelude::AsRawFd}; - use nix::fcntl; +use std::{io, io::Result, os::unix::prelude::AsRawFd}; -/// Sets O_NONBLOCK flag on self pub trait RawFdExt: AsRawFd { - fn set_nonblocking(&self); - fn set_blocking(&self); + /// Returns true of self is set to non-blocking. + fn is_nonblocking(&self) -> Result; + + /// Set non-blocking mode. + fn set_nonblocking(&self, value: bool) -> Result<()>; + + /// Set close-on-exec flag. fn set_cloexec(&self, value: bool) -> Result<()>; } impl RawFdExt for T { - fn set_nonblocking(&self) { - unsafe { - let opt = nix::libc::fcntl(self.as_raw_fd(), nix::libc::F_GETFL); - nix::libc::fcntl( - self.as_raw_fd(), - nix::libc::F_SETFL, - opt | nix::libc::O_NONBLOCK, - ); - } + fn is_nonblocking(&self) -> Result { + let flags = fcntl::fcntl(self.as_raw_fd(), fcntl::FcntlArg::F_GETFL) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + Ok(flags & fcntl::OFlag::O_NONBLOCK.bits() != 0) } - fn set_blocking(&self) { - unsafe { - let opt = nix::libc::fcntl(self.as_raw_fd(), nix::libc::F_GETFL); - nix::libc::fcntl( - self.as_raw_fd(), - nix::libc::F_SETFL, - opt & !nix::libc::O_NONBLOCK, - ); - } + fn set_nonblocking(&self, nonblocking: bool) -> Result<()> { + let flags = fcntl::fcntl(self.as_raw_fd(), fcntl::FcntlArg::F_GETFL) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let mut flags = fcntl::OFlag::from_bits_truncate(flags); + flags.set(fcntl::OFlag::O_NONBLOCK, nonblocking); + + fcntl::fcntl(self.as_raw_fd(), fcntl::FcntlArg::F_SETFL(flags)) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + .map(drop) } fn set_cloexec(&self, value: bool) -> Result<()> { @@ -44,3 +42,25 @@ impl RawFdExt for T { .map(drop) } } + +#[test] +fn non_blocking() { + let (a, b) = nix::unistd::pipe().unwrap(); + nix::unistd::close(b).unwrap(); + + let opt = unsafe { nix::libc::fcntl(a, nix::libc::F_GETFL) }; + assert!((dbg!(opt) & nix::libc::O_NONBLOCK) == 0); + assert!(!a.is_nonblocking().unwrap()); + + a.set_nonblocking(true).unwrap(); + let opt = unsafe { nix::libc::fcntl(a, nix::libc::F_GETFL) }; + assert!((dbg!(opt) & nix::libc::O_NONBLOCK) != 0); + assert!(a.is_nonblocking().unwrap()); + + a.set_nonblocking(false).unwrap(); + let opt = unsafe { nix::libc::fcntl(a, nix::libc::F_GETFL) }; + assert!((dbg!(opt) & nix::libc::O_NONBLOCK) == 0); + assert!(!a.is_nonblocking().unwrap()); + + nix::unistd::close(a).unwrap(); +} diff --git a/northstar/src/runtime/ipc/socket_pair.rs b/northstar/src/runtime/ipc/socket_pair.rs new file mode 100644 index 000000000..6bd65d680 --- /dev/null +++ b/northstar/src/runtime/ipc/socket_pair.rs @@ -0,0 +1,43 @@ +use std::os::unix::net::UnixStream; + +use tokio::net::UnixStream as TokioUnixStream; + +pub fn socket_pair() -> std::io::Result { + let (first, second) = UnixStream::pair()?; + + Ok(SocketPair { + first: Some(first), + second: Some(second), + }) +} + +#[derive(Debug)] +pub struct SocketPair { + first: Option, + second: Option, +} + +impl SocketPair { + pub fn first(&mut self) -> UnixStream { + self.second.take().unwrap(); + self.first.take().unwrap() + } + + pub fn second(&mut self) -> UnixStream { + self.first.take().unwrap(); + self.second.take().unwrap() + } + + pub fn first_async(&mut self) -> std::io::Result { + let socket = self.first(); + socket.set_nonblocking(true)?; + TokioUnixStream::from_std(socket) + } + + #[allow(dead_code)] + pub fn second_async(&mut self) -> std::io::Result { + let socket = self.second(); + socket.set_nonblocking(true)?; + TokioUnixStream::from_std(socket) + } +} diff --git a/northstar/src/runtime/mod.rs b/northstar/src/runtime/mod.rs index 92a1894f6..02d1617d1 100644 --- a/northstar/src/runtime/mod.rs +++ b/northstar/src/runtime/mod.rs @@ -1,12 +1,21 @@ -use crate::{api, api::model::Container}; +use crate::{api, api::model::Container, runtime::ipc::AsyncMessage}; +use async_stream::stream; use config::Config; use error::Error; use fmt::Debug; -use futures::{future::ready, FutureExt}; -use log::debug; +use futures::{ + future::{ready, Either}, + FutureExt, StreamExt, +}; +use log::{debug, info}; use nix::{ libc::{EXIT_FAILURE, EXIT_SUCCESS}, - sys, + sys::{ + self, + signal::Signal, + wait::{waitpid, WaitStatus}, + }, + unistd, }; use serde::{Deserialize, Serialize}; use state::State; @@ -15,30 +24,33 @@ use std::{ fmt::{self}, future::Future, path::Path, - pin::Pin, - task::{Context, Poll}, }; use sync::mpsc; use tokio::{ + pin, select, sync::{self, broadcast, oneshot}, - task, + task::{self, JoinHandle}, }; -use tokio_util::sync::CancellationToken; +use tokio_util::sync::{CancellationToken, DropGuard}; + +use self::fork::ForkerChannels; mod cgroups; -/// Runtime configuration -pub mod config; mod console; mod debug; mod error; +mod fork; +mod io; mod ipc; mod key; mod mount; -pub(crate) mod process; mod repository; mod state; pub(crate) mod stats; +/// Runtime configuration +pub mod config; + type EventTx = mpsc::Sender; type NotificationTx = broadcast::Sender<(Container, ContainerEvent)>; type RepositoryId = String; @@ -51,9 +63,11 @@ const EVENT_BUFFER_SIZE: usize = 1000; const NOTIFICATION_BUFFER_SIZE: usize = 1000; /// Environment variable name passed to the container with the containers name -const ENV_NAME: &str = "NAME"; +const ENV_NAME: &str = "NORTHSTAR_NAME"; /// Environment variable name passed to the container with the containers version -const ENV_VERSION: &str = "VERSION"; +const ENV_VERSION: &str = "NORTHSTAR_VERSION"; +/// Environment variable name passed to the container with the containers id +const ENV_CONTAINER: &str = "NORTHSTAR_CONTAINER"; #[derive(Debug)] enum Event { @@ -126,6 +140,18 @@ pub enum ExitStatus { Signalled(u8), } +impl From for ExitStatus { + fn from(signal: Signal) -> Self { + ExitStatus::Signalled(signal as u8) + } +} + +impl From for ExitStatus { + fn from(code: ExitCode) -> Self { + ExitStatus::Exit(code) + } +} + impl ExitStatus { /// Exit success pub const SUCCESS: ExitCode = EXIT_SUCCESS; @@ -150,94 +176,128 @@ impl fmt::Display for ExitStatus { } } -/// Result of a Runtime action -pub type RuntimeResult = Result<(), Error>; - -/// Handle to the Northstar runtime -pub struct Runtime { - /// Channel receive a stop signal for the runtime - /// Drop the tx part to gracefully shutdown the mail loop. - stop: CancellationToken, - // Channel to signal the runtime exit status to the caller of `start` - // When the runtime is shut down the result of shutdown is sent to this - // channel. If a error happens during normal operation the error is also - // sent to this channel. - stopped: oneshot::Receiver, - // Runtime task - task: task::JoinHandle<()>, +/// Runtime handle +#[allow(clippy::large_enum_variant)] +pub enum Runtime { + /// The runtime is created but not yet started. + Created { + /// Runtime configuration + config: Config, + /// Forker pid + forker_pid: Pid, + /// Forker channles + forker_channels: ForkerChannels, + }, + /// The runtime is started. + Running { + /// Drop guard to stop the runtime + guard: DropGuard, + /// Runtime task + task: JoinHandle>, + }, } impl Runtime { + /// Create new runtime instance + pub fn new(config: Config) -> Result { + let (forker_pid, forker_channels) = fork::start()?; + Ok(Runtime::Created { + config, + forker_pid, + forker_channels, + }) + } + /// Start runtime with configuration `config` - pub async fn start(config: Config) -> Result { + pub async fn start(self) -> Result { + let (config, forker_pid, forker_channels) = if let Runtime::Created { + config, + forker_pid, + forker_channels, + } = self + { + (config, forker_pid, forker_channels) + } else { + panic!("Runtime::start called on a running runtime"); + }; + config.check().await?; - let stop = CancellationToken::new(); - let (stopped_tx, stopped) = oneshot::channel(); + let token = CancellationToken::new(); + let guard = token.clone().drop_guard(); // Start a task that drives the main loop and wait for shutdown results - let stop_task = stop.clone(); - let task = task::spawn(async move { - match runtime_task(&config, stop_task).await { - Err(e) => { - log::error!("Runtime error: {}", e); - stopped_tx.send(Err(e)).ok(); - } - Ok(_) => drop(stopped_tx.send(Ok(()))), - }; - }); - - Ok(Runtime { - stop, - stopped, - task, - }) + let task = task::spawn(run(config, token, forker_pid, forker_channels)); + + Ok(Runtime::Running { guard, task }) } /// Stop the runtime and wait for the termination - pub fn shutdown(self) -> impl Future { - self.stop.cancel(); - let stopped = self.stopped; - self.task.then(|_| { - stopped.then(|n| match n { - Ok(n) => ready(n), - Err(_) => ready(Ok(())), + pub fn shutdown(self) -> impl Future> { + if let Runtime::Running { guard, task } = self { + drop(guard); + Either::Left({ + task.then(|n| match n { + Ok(n) => ready(n), + Err(_) => ready(Ok(())), + }) }) - }) + } else { + Either::Right(futures::future::ready(Ok(()))) + } } -} - -impl Future for Runtime { - type Output = RuntimeResult; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match Pin::new(&mut self.stopped).poll(cx) { - Poll::Ready(r) => match r { - Ok(r) => Poll::Ready(r), - // Channel error -> tx side dropped - Err(_) => Poll::Ready(Ok(())), + /// Wait for the runtime to stop + pub async fn stopped(&mut self) -> Result<(), Error> { + match self { + Runtime::Running { ref mut task, .. } => match task.await { + Ok(r) => r, + Err(_) => Ok(()), }, - Poll::Pending => Poll::Pending, + Runtime::Created { .. } => panic!("Stopped called on a stopped runtime"), } } } -async fn runtime_task(config: &'_ Config, stop: CancellationToken) -> Result<(), Error> { - let cgroup = Path::new(&*config.cgroup.as_str()); - cgroups::init(cgroup).await?; +/// Main loop +async fn run( + config: Config, + token: CancellationToken, + forker_pid: Pid, + forker_channels: ForkerChannels, +) -> Result<(), Error> { + // Setup root cgroup(s) + let cgroup = Path::new(config.cgroup.as_str()).to_owned(); + cgroups::init(&cgroup).await?; + + // Join forker + let mut join_forker = task::spawn_blocking(move || { + let pid = unistd::Pid::from_raw(forker_pid as i32); + loop { + match waitpid(Some(pid), None) { + Ok(WaitStatus::Exited(_pid, status)) => { + break ExitStatus::Exit(status); + } + Ok(WaitStatus::Signaled(_pid, status, _)) => { + break ExitStatus::Signalled(status as u8); + } + Ok(WaitStatus::Continued(_)) | Ok(WaitStatus::Stopped(_, _)) => (), + Err(nix::Error::EINTR) => (), + e => panic!("Failed to waitpid on {}: {:?}", pid, e), + } + } + }); // Northstar runs in a event loop let (event_tx, mut event_rx) = mpsc::channel::(EVENT_BUFFER_SIZE); let (notification_tx, _) = sync::broadcast::channel(NOTIFICATION_BUFFER_SIZE); - let mut state = State::new(config, event_tx.clone(), notification_tx.clone()).await?; // Initialize the console if configured - let console = if let Some(consoles) = config.console.as_ref() { if consoles.is_empty() { None } else { - let mut console = console::Console::new(event_tx.clone(), notification_tx); + let mut console = console::Console::new(event_tx.clone(), notification_tx.clone()); for url in consoles { console.listen(url).await.map_err(Error::Console)?; } @@ -247,34 +307,75 @@ async fn runtime_task(config: &'_ Config, stop: CancellationToken) -> Result<(), None }; - // Wait for a external shutdown request - task::spawn(async move { - stop.cancelled().await; - event_tx.send(Event::Shutdown).await.ok(); - }); + // Convert stream and stream_fd into Tokio UnixStream + let (forker, mut exit_notifications) = { + let ForkerChannels { + stream, + notifications, + } = forker_channels; + + let forker = fork::Forker::new(stream); + let exit_notifications: AsyncMessage<_> = notifications + .try_into() + .expect("Failed to convert exit notification handle"); + (forker, exit_notifications) + }; + + // Merge the exit notification from the forker process with other events into the main loop channel + let event_rx = stream! { + loop { + select! { + Some(event) = event_rx.recv() => yield event, + Ok(Some(fork::Notification::Exit { container, exit_status })) = exit_notifications.recv() => { + let event = ContainerEvent::Exit(exit_status); + yield Event::Container(container, event); + } + else => unimplemented!(), + } + } + }; + pin!(event_rx); + + let mut state = State::new(config, event_tx.clone(), notification_tx, forker).await?; + + info!("Runtime up and running"); // Enter main loop loop { - if let Err(e) = match event_rx.recv().await.unwrap() { - // Process console events enqueued by console::Console - Event::Console(mut msg, txr) => state.on_request(&mut msg, txr).await, - // The runtime os commanded to shut down and exit. - Event::Shutdown => { - debug!("Shutting down Northstar runtime"); - if let Some(console) = console { - debug!("Shutting down console"); - console.shutdown().await.map_err(Error::Console)?; + tokio::select! { + // External shutdown event via the token + _ = token.cancelled() => event_tx.send(Event::Shutdown).await.expect("Failed to send shutdown event"), + // Process events + event = event_rx.next() => { + if let Err(e) = match event.unwrap() { + // Process console events enqueued by console::Console + Event::Console(mut msg, response) => state.on_request(&mut msg, response).await, + // The runtime os commanded to shut down and exit. + Event::Shutdown => { + debug!("Shutting down Northstar runtime"); + if let Some(console) = console { + debug!("Shutting down console"); + console.shutdown().await.map_err(Error::Console)?; + } + break state.shutdown(event_rx).await; + } + // Container event + Event::Container(container, event) => state.on_event(&container, &event, false).await, + } { + break Err(e); } - break state.shutdown().await; } - // Container event - Event::Container(container, event) => state.on_event(&container, &event).await, - } { - break Err(e); + exit_status = &mut join_forker => panic!("Forker exited with {:?}", exit_status), } }?; - cgroups::shutdown(cgroup).await?; + // Terminate forker process + debug!("Joining forker with pid {}", forker_pid); + // signal::kill(forker_pid, Some(SIGTERM)).ok(); + join_forker.await.expect("Failed to join forker"); + + // Shutdown cgroups + cgroups::shutdown(&cgroup).await?; debug!("Shutdown complete"); diff --git a/northstar/src/runtime/mount.rs b/northstar/src/runtime/mount.rs index 4ee4af1fc..89d56dbc0 100644 --- a/northstar/src/runtime/mount.rs +++ b/northstar/src/runtime/mount.rs @@ -4,7 +4,7 @@ use crate::{ npk::{dm_verity::VerityHeader, npk::Hashes}, }; use devicemapper::{DevId, DmError, DmName, DmOptions}; -use futures::Future; +use futures::{Future, FutureExt}; use log::{debug, info, warn}; use loopdev::LoopControl; use std::{ @@ -13,10 +13,9 @@ use std::{ path::{Path, PathBuf}, str::Utf8Error, sync::Arc, - thread, }; use thiserror::Error; -use tokio::{fs, time}; +use tokio::{fs, task, time}; use crate::seccomp::Selinux; pub use nix::mount::MsFlags as MountFlags; @@ -102,7 +101,7 @@ impl MountControl { let selinux = npk.manifest().selinux.clone(); let hashes = npk.hashes().cloned(); - async move { + task::spawn_blocking(move || { let start = time::Instant::now(); debug!("Mounting {}:{}", name, version); @@ -118,8 +117,7 @@ impl MountControl { hashes, &target, key.is_some(), - ) - .await?; + )?; let duration = start.elapsed(); info!( @@ -130,7 +128,11 @@ impl MountControl { ); Ok(device) - } + }) + .map(|r| match r { + Ok(r) => r, + Err(e) => panic!("Task error: {}", e), + }) } pub(super) async fn umount(&self, target: &Path) -> Result<(), Error> { @@ -149,7 +151,7 @@ impl MountControl { } #[allow(clippy::too_many_arguments)] -async fn mount( +fn mount( dm: Arc, lc: Arc, fd: RawFd, @@ -338,9 +340,8 @@ fn dmsetup( debug!("Waiting for verity device {}", device.display(),); while !device.exists() { - // Use a std::thread::sleep because this is run on a futures - // executor and not a tokio runtime - thread::sleep(time::Duration::from_millis(1)); + // This code runs on a dedicated blocking thread + std::thread::sleep(time::Duration::from_millis(1)); if start.elapsed() > DM_DEVICE_TIMEOUT { return Err(Error::Timeout(format!( diff --git a/northstar/src/runtime/process/io.rs b/northstar/src/runtime/process/io.rs deleted file mode 100644 index d9251c10e..000000000 --- a/northstar/src/runtime/process/io.rs +++ /dev/null @@ -1,165 +0,0 @@ -use super::{ - super::ipc::pipe::{pipe, AsyncPipeRead}, - Error, -}; -use crate::{ - npk, - npk::manifest::{Level, Manifest, Output}, - runtime::{error::Context as ErrorContext, ipc::pipe::PipeWrite}, -}; -use bytes::{Buf, BufMut, BytesMut}; -use log::{debug, error, info, trace, warn}; -use nix::libc; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - convert::TryInto, - os::unix::prelude::{AsRawFd, RawFd}, - pin::Pin, - task::{Context, Poll}, -}; -use tokio::{ - io::{self, AsyncWrite, BufReader}, - task, -}; - -/// Implement AsyncWrite and forwards lines to Rust log -struct LogSink { - buffer: BytesMut, - level: Level, - tag: String, -} - -impl LogSink { - fn new(level: Level, tag: &str) -> LogSink { - LogSink { - level, - tag: tag.to_string(), - buffer: BytesMut::new(), - } - } -} - -impl LogSink { - fn log(&mut self) { - while let Some(p) = self.buffer.iter().position(|b| *b == b'\n') { - let line = self.buffer.split_to(p); - // Discard the newline - self.buffer.advance(1); - let line = String::from_utf8_lossy(&line); - match self.level { - Level::Trace => trace!("{}: {}", self.tag, line), - Level::Debug => debug!("{}: {}", self.tag, line), - Level::Info => info!("{}: {}", self.tag, line), - Level::Warn => warn!("{}: {}", self.tag, line), - Level::Error => error!("{}: {}", self.tag, line), - } - } - } -} - -impl AsyncWrite for LogSink { - fn poll_write( - mut self: Pin<&mut Self>, - _cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - self.buffer.extend(buf); - self.log(); - Poll::Ready(Ok(buf.len())) - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll> { - // Log even unfinished lines received until now by adding a newline and print - self.buffer.reserve(1); - self.buffer.put_u8(b'\n'); - self.log(); - Poll::Ready(Ok(())) - } -} - -// Writing ends for stdout/stderr -pub(super) struct Io { - _stdout: Option, - _stderr: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(super) enum Fd { - // Close the fd - Close, - // Dup2 the the fd to fd - Dup(i32), -} - -pub(super) async fn from_manifest( - manifest: &Manifest, -) -> Result<(Option, HashMap), Error> { - let mut fds = HashMap::new(); - - // The default of all fds inherited from the parent is to close it - let mut proc_self_fd = tokio::fs::read_dir("/proc/self/fd") - .await - .context("Readdir")?; - while let Ok(Some(e)) = proc_self_fd.next_entry().await { - let file = e.file_name(); - let fd: i32 = file.to_str().unwrap().parse().unwrap(); // fds are always numeric - fds.insert(fd as RawFd, Fd::Close); - } - drop(proc_self_fd); - - let mut stdout_stderr = |c: Option<&Output>, fd| { - match c { - Some(npk::manifest::Output::Pipe) => { - // Do nothing with the stdout fd - just prevent remove it from the list of fds that - // has been gathered above and instructs the init to close those fds. - fds.remove(&fd); - Result::<_, Error>::Ok(None) - } - Some(npk::manifest::Output::Log { level, ref tag }) => { - // Create a pipe: the writing end is used in the child as stdout/stderr. The reading end is used in a LogSink - let (reader, writer) = pipe().context("Failed to open pipe")?; - let reader_fd = reader.as_raw_fd(); - let reader: AsyncPipeRead = reader - .try_into() - .context("Failed to get async handler from pipe reader")?; - - let mut reader = BufReader::new(reader); - let tag = tag.to_string(); - let mut log_sink = LogSink::new(level.clone(), &tag); - task::spawn(async move { - drop(io::copy_buf(&mut reader, &mut log_sink).await); - }); - - // The read fd shall be closed in the child. It's used in the runtime only - fds.insert(reader_fd, Fd::Close); - - // Remove fd that is set to be Fd::Close by default. fd is closed by dup2 - fds.remove(&writer.as_raw_fd()); - // The writing fd shall be dupped to 2 - fds.insert(fd, Fd::Dup(writer.as_raw_fd())); - - // Return the writer: Drop (that closes) it in the parent. Forget in the child. - Ok(Some(writer)) - } - None => Ok(None), - } - }; - - if let Some(io) = manifest.io.as_ref() { - let io = Some(Io { - _stdout: stdout_stderr(io.stdout.as_ref(), libc::STDOUT_FILENO)?, - _stderr: stdout_stderr(io.stdout.as_ref(), libc::STDERR_FILENO)?, - }); - Ok((io, fds)) - } else { - Ok((None, fds)) - } -} diff --git a/northstar/src/runtime/process/mod.rs b/northstar/src/runtime/process/mod.rs deleted file mode 100644 index 03006a797..000000000 --- a/northstar/src/runtime/process/mod.rs +++ /dev/null @@ -1,569 +0,0 @@ -use super::{ - config::Config, - error::Error, - ipc::{ - channel, - condition::{self, ConditionNotify, ConditionWait}, - }, - ContainerEvent, Event, EventTx, ExitStatus, NotificationTx, Pid, ENV_NAME, ENV_VERSION, -}; -use crate::{ - common::{container::Container, non_null_string::NonNullString}, - npk::manifest::Manifest, - runtime::{ - console::{self, Peer}, - error::Context, - }, - seccomp, -}; -use async_trait::async_trait; -use futures::{future::ready, Future, FutureExt}; -use log::{debug, error, info, warn}; -use nix::{ - errno::Errno, - sys::{self, signal::Signal, socket}, - unistd, -}; -use std::{ - collections::HashMap, - ffi::{c_void, CString}, - fmt, - mem::forget, - os::unix::{ - net::UnixStream as StdUnixStream, - prelude::{AsRawFd, FromRawFd, RawFd}, - }, - path::Path, - ptr::null, -}; -use sys::wait; -use tokio::{net::UnixStream, task, time}; -use tokio_util::sync::CancellationToken; - -mod fs; -mod init; -mod io; -mod trampoline; - -#[derive(Debug)] -pub(super) struct Launcher { - tx: EventTx, - notification_tx: NotificationTx, - config: Config, -} - -pub(super) struct Process { - pid: Pid, - checkpoint: Option, - exit_status: Option + Send + Sync + Unpin>>, -} - -impl fmt::Debug for Process { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Process") - .field("pid", &self.pid) - .field("checkpoint", &self.checkpoint) - .finish() - } -} - -impl Launcher { - pub async fn start( - tx: EventTx, - config: Config, - notification_tx: NotificationTx, - ) -> Result { - set_child_subreaper(true)?; - - let launcher = Launcher { - tx, - config, - notification_tx, - }; - - Ok(launcher) - } - - pub async fn shutdown(self) -> Result<(), Error> { - Ok(()) - } - - /// Create a new container process set - pub async fn create( - &self, - root: &Path, - container: &Container, - manifest: &Manifest, - args: Option<&Vec>, - env: Option<&HashMap>, - ) -> Result { - // Token to stop the console task if any. This token is cancelled when - // the waitpid of this child process signals that the child is exited. See - // `wait`. - let stop = CancellationToken::new(); - let (init, argv) = init_argv(manifest, args); - let mut env = self::env(manifest, env); - - // Setup io and collect fd setup set - let (io, mut fds) = io::from_manifest(manifest).await?; - - // Pipe for sending the init pid from the intermediate process to the runtime - // and the exit status from init to the runtime - // - // Ensure the fds of the channel are *not* in the fds set. The list of fds that are - // closed by init is gathered above. Between the assembly of the list and the new pipes - // for the child pid and the condition variables a io task that forwards logs from containers - // can end. Those io tasks use pipes as well. If such a task ends it closes its fds. Those numbers - // can be in the list of to be closed fds but are reused when the pipe are created. - let channel = channel::Channel::new(); - fds.remove(&channel.as_raw_fd().0); - fds.remove(&channel.as_raw_fd().1); - - // Ensure that the checkpoint fds are not in the fds set and untouched - let (checkpoint_runtime, checkpoint_init) = checkpoints(); - fds.remove(&checkpoint_runtime.as_raw_fd().0); - fds.remove(&checkpoint_runtime.as_raw_fd().1); - - // Setup console if configured - let console_fd = console_fd( - self.tx.clone(), - manifest, - &mut env, - &mut fds, - stop.clone(), - &self.notification_tx, - ) - .await; - - let capabilities = manifest.capabilities.clone(); - let fds = fds.drain().collect::>(); - let uid = manifest.uid; - let gid = manifest.gid; - let groups = groups(manifest); - let mounts = fs::prepare_mounts(&self.config, root, manifest).await?; - let rlimits = manifest.rlimits.clone(); - let root = root.to_owned(); - let seccomp = seccomp_filter(manifest); - - debug!("{} init is {:?}", container, init); - debug!("{} argv is {:?}", container, argv); - debug!("{} env is {:?}", container, env); - - let init = init::Init { - root, - init, - argv, - env, - uid, - gid, - mounts, - fds, - groups, - capabilities, - rlimits, - seccomp, - }; - - // Fork trampoline process - match unsafe { unistd::fork() } { - Ok(result) => match result { - unistd::ForkResult::Parent { child } => { - let trampoline_pid = child; - // Close writing ends of log pipes (if any) - drop(io); - // Close child console socket (if any) - drop(console_fd); - // Close child checkpoint pipes - drop(checkpoint_init); - - // Receive the pid of the init process from the trampoline process - debug!("Waiting for the pid of init of {}", container); - let mut channel = channel.into_async_read(); - let pid = channel.recv::().await.expect("Failed to read pid") as Pid; - debug!("Created {} with pid {}", container, pid); - - // We're done reading the pid. The next information transferred via the - // channel is the exit status of the container process. - - // Reap the trampoline process which is (or will be) a zombie otherwise - debug!("Waiting for trampoline process {} to exit", trampoline_pid); - wait::waitpid(Some(trampoline_pid), None) - .expect("Failed to wait for trampoline process"); - - // Start a task that waits for the exit of the init process - let exit_status_fut = self.container_exit_status(container, channel, pid, stop); - - Ok(Process { - pid, - checkpoint: Some(checkpoint_runtime), - exit_status: Some(Box::new(exit_status_fut)), - }) - } - unistd::ForkResult::Child => { - // Forget writing ends of io which are stdout, stderr. The `forget` - // ensures that the file descriptors are not closed - forget(io); - - // Close checkpoint ends of the runtime - drop(checkpoint_runtime); - - trampoline::trampoline(init, channel, checkpoint_init) - } - }, - Err(e) => panic!("Fork error: {}", e), - } - } - - /// Spawn a task that waits for the containers exit status. If the receive operation - /// fails take the exit status of the init process `pid`. - fn container_exit_status( - &self, - container: &Container, - mut channel: channel::AsyncChannelRead, - pid: Pid, - stop: CancellationToken, - ) -> impl Future { - let container = container.clone(); - let tx = self.tx.clone(); - - // This task lives as long as the child process and doesn't need to be - // cancelled explicitly. - task::spawn(async move { - // Wait for an event on the channel - let status = match channel.recv::().await { - // Init sent something - Ok(exit_status) => { - debug!( - "Received exit status of {} ({}) via channel: {}", - container, pid, exit_status - ); - - // Wait for init to exit. This is needed to ensure the init process - // exited before the runtime starts to cleanup e.g remove cgroups - if let Err(e) = wait::waitpid(Some(unistd::Pid::from_raw(pid as i32)), None) { - panic!("Failed to wait for init process {}: {}", pid, e); - } - - exit_status - } - // The channel is closed before init sent something - Err(e) => { - // This is not an error. If for example the child process exited because - // of a SIGKILL the pipe is just closed and the init process cannot send - // anything there. In such a situation take the exit status of the init - // process as the exit status of the container process. - debug!( - "Failed to receive exit status of {} ({}) via channel: {}", - container, pid, e - ); - - let pid = unistd::Pid::from_raw(pid as i32); - let exit_status = loop { - match wait::waitpid(Some(pid), None) { - Ok(wait::WaitStatus::Exited(pid, code)) => { - debug!("Process {} exit code is {}", pid, code); - break ExitStatus::Exit(code); - } - Ok(wait::WaitStatus::Signaled(pid, signal, _dump)) => { - debug!("Process {} exit status is signal {}", pid, signal); - break ExitStatus::Signalled(signal as u8); - } - Ok(r) => unreachable!("Unexpected wait status of init: {:?}", r), - Err(nix::Error::EINTR) => continue, - Err(e) => panic!("Failed to waitpid on {}: {}", pid, e), - } - }; - debug!("Exit status of {} ({}): {}", container, pid, exit_status); - exit_status - } - }; - - // Stop console connection if any - stop.cancel(); - - // Send container exit event to the runtime main loop - let event = ContainerEvent::Exit(status.clone()); - tx.send(Event::Container(container, event)) - .await - .expect("Failed to send container event"); - - status - }) - .then(|r| match r { - Ok(r) => ready(r), - Err(_) => panic!("Task error"), - }) - } -} - -#[async_trait] -impl super::state::Process for Process { - fn pid(&self) -> Pid { - self.pid - } - - async fn spawn(&mut self) -> Result<(), Error> { - let checkpoint = self - .checkpoint - .take() - .expect("Attempt to start container twice. This is a bug."); - info!("Starting {}", self.pid()); - let wait = checkpoint.notify(); - - // If the child process refuses to start - kill it after 5 seconds - match time::timeout(time::Duration::from_secs(5), wait.async_wait()).await { - Ok(_) => (), - Err(_) => { - error!( - "Timeout while waiting for {} to start. Sending SIGKILL to {}", - self.pid, self.pid - ); - let process_group = unistd::Pid::from_raw(-(self.pid as i32)); - let sigkill = Some(sys::signal::SIGKILL); - sys::signal::kill(process_group, sigkill).ok(); - } - } - - Ok(()) - } - - async fn kill(&mut self, signal: Signal) -> Result<(), super::error::Error> { - debug!("Sending {} to {}", signal.as_str(), self.pid); - let process_group = unistd::Pid::from_raw(-(self.pid as i32)); - let sigterm = Some(signal); - match sys::signal::kill(process_group, sigterm) { - // The process is terminated already. Wait for the waittask to do it's job and resolve exit_status - Err(nix::Error::ESRCH) => { - debug!("Process {} already exited", self.pid); - Ok(()) - } - result => result.context(format!( - "Failed to send signal {} {}", - signal, process_group - )), - } - } - - async fn wait(&mut self) -> Result { - let exit_status = self.exit_status.take().expect("Wait called twice"); - Ok(exit_status.await) - } - - async fn destroy(&mut self) -> Result<(), Error> { - Ok(()) - } -} - -/// Construct the init and argv argument for the containers execve -fn init_argv(manifest: &Manifest, args: Option<&Vec>) -> (CString, Vec) { - // A container without an init shall not be started - // Validation of init is done in `Manifest` - let init = CString::new( - manifest - .init - .as_ref() - .expect("Attempt to use init from resource container") - .to_str() - .expect("Invalid init. This a bug in the manifest validation"), - ) - .expect("Invalid init"); - - // If optional arguments are defined, discard the values from the manifest. - // if there are no optional args - take the values from the manifest if present - // or nothing. - let args = match (manifest.args.as_ref(), args) { - (None, None) => &[], - (None, Some(a)) => a.as_slice(), - (Some(m), None) => m.as_slice(), - (Some(_), Some(a)) => a.as_slice(), - }; - - let mut argv = Vec::with_capacity(1 + args.len()); - argv.push(init.clone()); - argv.extend({ - args.iter().map(|arg| { - CString::new(arg.as_bytes()) - .expect("Invalid arg. This is a bug in the manifest or parameter validation") - }) - }); - - // argv - (init, argv) -} - -/// Construct the env argument for the containers execve. Optional args and env overwrite values from the -/// manifest. -fn env(manifest: &Manifest, env: Option<&HashMap>) -> Vec { - let mut result = Vec::with_capacity(2); - result.push( - CString::new(format!("{}={}", ENV_NAME, manifest.name)) - .expect("Invalid container name. This is a bug in the manifest validation"), - ); - result.push(CString::new(format!("{}={}", ENV_VERSION, manifest.version)).unwrap()); - - if let Some(ref e) = manifest.env { - result.extend({ - e.iter() - .filter(|(k, _)| { - // Skip the values declared in fn arguments - env.map(|env| !env.contains_key(k)).unwrap_or(true) - }) - .map(|(k, v)| { - CString::new(format!("{}={}", k, v)) - .expect("Invalid env. This is a bug in the manifest validation") - }) - }) - } - - // Add additional env variables passed - if let Some(env) = env { - result.extend( - env.iter().map(|(k, v)| { - CString::new(format!("{}={}", k, v)).expect("Invalid additional env") - }), - ); - } - - result -} - -/// Open a socket that is passed via env variable to the child. The peer of the -/// socket is a console connection handling task -async fn console_fd( - event_tx: EventTx, - manifest: &Manifest, - env: &mut Vec, - fds: &mut HashMap, - stop: CancellationToken, - notification_tx: &NotificationTx, -) -> Option { - if manifest.console { - let (runtime_socket, client_socket) = socket::socketpair( - socket::AddressFamily::Unix, - socket::SockType::Stream, - None, - socket::SockFlag::empty(), - ) - .expect("Failed to create socketpair"); - - // Add the fd number to the environment of the application - env.push(CString::new(format!("NORTHSTAR_CONSOLE={}", client_socket)).unwrap()); - - // Make sure that the server socket is closed in the child before exeve - fds.insert(runtime_socket, io::Fd::Close); - // Make sure the client socket is not included in the list to close fds - fds.remove(&client_socket.as_raw_fd()); - - // Convert std raw fd - let std = unsafe { StdUnixStream::from_raw_fd(runtime_socket) }; - std.set_nonblocking(true) - .expect("Failed to set socket into nonblocking mode"); - let io = UnixStream::from_std(std).expect("Failed to convert Unix socket"); - - let peer = Peer::from(format!("{}:{}", manifest.name, manifest.version).as_str()); - - // Start console - task::spawn(console::Console::connection( - io, - peer, - stop, - event_tx, - notification_tx.subscribe(), - None, - )); - - Some(unsafe { StdUnixStream::from_raw_fd(client_socket) }) - } else { - None - } -} - -/// Generate a list of supplementary gids if the groups info can be retrieved. This -/// must happen before the init `clone` because the group information cannot be gathered -/// without `/etc` etc... -fn groups(manifest: &Manifest) -> Vec { - if let Some(groups) = manifest.suppl_groups.as_ref() { - let mut result = Vec::with_capacity(groups.len()); - for group in groups { - let cgroup = CString::new(group.as_str()).unwrap(); // Check during manifest parsing - let group_info = - unsafe { nix::libc::getgrnam(cgroup.as_ptr() as *const nix::libc::c_char) }; - if group_info == (null::() as *mut nix::libc::group) { - warn!("Skipping invalid supplementary group {}", group); - } else { - let gid = unsafe { (*group_info).gr_gid }; - // TODO: Are there gids cannot use? - result.push(gid) - } - } - result - } else { - Vec::with_capacity(0) - } -} - -/// Generate seccomp filter applied in init -fn seccomp_filter(manifest: &Manifest) -> Option { - if let Some(seccomp) = manifest.seccomp.as_ref() { - return Some(seccomp::seccomp_filter( - seccomp.profile.as_ref(), - seccomp.allow.as_ref(), - manifest.capabilities.as_ref(), - )); - } - None -} - -// Set the child subreaper flag of the calling thread -fn set_child_subreaper(value: bool) -> Result<(), Error> { - #[cfg(target_os = "android")] - const PR_SET_CHILD_SUBREAPER: nix::libc::c_int = 36; - #[cfg(not(target_os = "android"))] - use nix::libc::PR_SET_CHILD_SUBREAPER; - - let value = if value { 1u64 } else { 0u64 }; - let result = unsafe { nix::libc::prctl(PR_SET_CHILD_SUBREAPER, value, 0, 0, 0) }; - Errno::result(result) - .map(drop) - .context("Set child subreaper flag") -} - -pub(super) struct Checkpoint(ConditionWait, ConditionNotify); - -fn checkpoints() -> (Checkpoint, Checkpoint) { - let a = condition::Condition::new().expect("Failed to create condition"); - a.set_cloexec(); - let b = condition::Condition::new().expect("Failed to create condition"); - b.set_cloexec(); - - let (aw, an) = a.split(); - let (bw, bn) = b.split(); - - (Checkpoint(aw, bn), Checkpoint(bw, an)) -} - -impl Checkpoint { - fn notify(self) -> ConditionWait { - self.1.notify(); - self.0 - } - - fn wait(self) -> ConditionNotify { - self.0.wait(); - self.1 - } - - /// Raw file descriptor number of the rx and tx pipe - fn as_raw_fd(&self) -> (RawFd, RawFd) { - (self.0.as_raw_fd(), self.1.as_raw_fd()) - } -} - -impl std::fmt::Debug for Checkpoint { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Checkpoint") - .field("wait", &self.0.as_raw_fd()) - .field("notifiy", &self.1.as_raw_fd()) - .finish() - } -} diff --git a/northstar/src/runtime/process/trampoline.rs b/northstar/src/runtime/process/trampoline.rs deleted file mode 100644 index 5e93b5895..000000000 --- a/northstar/src/runtime/process/trampoline.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::{init::Init, Checkpoint}; -use crate::runtime::ipc::channel::Channel; -use nix::{sched, unistd}; -use std::process::exit; - -pub(super) fn trampoline(init: Init, mut child_channel: Channel, checkpoint_init: Checkpoint) -> ! { - // Create pid namespace - sched::unshare(sched::CloneFlags::CLONE_NEWPID).expect("Failed to create pid namespace"); - - // Fork the init process - match unsafe { unistd::fork() }.expect("Failed to fork init") { - unistd::ForkResult::Parent { child } => { - // Send the pid of init to the runtime and exit - let pid = child.as_raw() as i32; - child_channel.send(&pid).expect("Failed to send init pid"); - exit(0); - } - unistd::ForkResult::Child => { - // Wait for the runtime to signal that init may start. - let condition_notify = checkpoint_init.wait(); - - // Dive into init and never return - init.run(condition_notify, child_channel); - } - } -} diff --git a/northstar/src/runtime/repository.rs b/northstar/src/runtime/repository.rs index be214b8ca..be515a262 100644 --- a/northstar/src/runtime/repository.rs +++ b/northstar/src/runtime/repository.rs @@ -3,17 +3,16 @@ use super::{ key::{self, PublicKey}, Container, }; -use crate::{ - npk::npk::{self}, - runtime::ipc::raw_fd_ext::RawFdExt, -}; +use crate::{npk::npk::Npk as NpkNpk, runtime::ipc::RawFdExt}; use bytes::Bytes; +use futures::{future::try_join_all, FutureExt}; use log::{debug, info}; use mpsc::Receiver; use nanoid::nanoid; use std::{ collections::HashMap, fmt, + future::ready, io::{BufReader, SeekFrom}, os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd}, path::{Path, PathBuf}, @@ -22,10 +21,11 @@ use tokio::{ fs::{self}, io::{AsyncSeekExt, AsyncWriteExt}, sync::mpsc, + task, time::Instant, }; -pub(super) type Npk = crate::npk::npk::Npk>; +pub(super) type Npk = NpkNpk>; #[async_trait::async_trait] pub(super) trait Repository: fmt::Debug { @@ -73,30 +73,40 @@ impl DirRepository { let mut readir = fs::read_dir(&dir).await.context("Repository read dir")?; let start = Instant::now(); + let mut tasks = Vec::new(); while let Ok(Some(entry)) = readir.next_entry().await { let file = entry.path(); - debug!( - "Loading {}{}", - file.display(), - if key.is_some() { " [verified]" } else { "" } - ); - let reader = std::fs::File::open(&file).context("Failed to open npk")?; - let reader = std::io::BufReader::new(reader); - let npk = crate::npk::npk::Npk::from_reader(reader, key.as_ref()) - .map_err(|e| Error::Npk(file.display().to_string(), e))?; - let name = npk.manifest().name.clone(); - let version = npk.manifest().version.clone(); - let container = Container::new(name, version); + let load_task = task::spawn_blocking(move || { + debug!( + "Loading {}{}", + file.display(), + if key.is_some() { " [verified]" } else { "" } + ); + let reader = std::fs::File::open(&file).context("Failed to open npk")?; + let reader = std::io::BufReader::new(reader); + let npk = NpkNpk::from_reader(reader, key.as_ref()) + .map_err(|e| Error::Npk(file.display().to_string(), e))?; + let name = npk.manifest().name.clone(); + let version = npk.manifest().version.clone(); + let container = Container::new(name, version); + Result::<_, Error>::Ok((container, (file, npk))) + }) + .then(|r| ready(r.expect("Task error"))); + + tasks.push(load_task); + } + + for result in try_join_all(tasks).await? { + let (container, (file, npk)) = result; containers.insert(container, (file, npk)); } let duration = start.elapsed(); info!( - "Loaded {} containers from {} in {:.03}s (avg: {:.05}s)", + "Loaded {} containers from {} in {:.03}s", containers.len(), dir.display(), duration.as_secs_f32(), - duration.as_secs_f32() / containers.len() as f32 ); Ok(DirRepository { @@ -206,7 +216,8 @@ impl<'a> Repository for MemRepository { // Write buffer to the memfd let mut file = unsafe { fs::File::from_raw_fd(fd.as_raw_fd()) }; - file.set_nonblocking(); + file.set_nonblocking(true) + .context("Failed to set nonblocking")?; while let Some(r) = rx.recv().await { file.write_all(&r).await.context("Failed stream npk")?; @@ -225,12 +236,13 @@ impl<'a> Repository for MemRepository { // Forget fd - it's owned by file fd.into_raw_fd(); - file.set_blocking(); + file.set_nonblocking(false) + .context("Failed to set blocking")?; let file = BufReader::new(file.into_std().await); // Load npk debug!("Loading memfd as npk"); - let npk = npk::Npk::from_reader(file, self.key.as_ref()) + let npk = NpkNpk::from_reader(file, self.key.as_ref()) .map_err(|e| Error::Npk("Memory".into(), e))?; let manifest = npk.manifest(); let container = Container::new(manifest.name.clone(), manifest.version.clone()); diff --git a/northstar/src/runtime/state.rs b/northstar/src/runtime/state.rs index b5fbf7331..0aa964e74 100644 --- a/northstar/src/runtime/state.rs +++ b/northstar/src/runtime/state.rs @@ -3,8 +3,9 @@ use super::{ config::{Config, RepositoryType}, console::Request, error::Error, + fork::Forker, + io, mount::MountControl, - process::Launcher, repository::{DirRepository, MemRepository, Npk}, stats::ContainerStats, Container, ContainerEvent, Event, EventTx, ExitStatus, NotificationTx, Pid, RepositoryId, @@ -13,32 +14,41 @@ use crate::{ api::{self, model}, common::non_null_string::NonNullString, npk::manifest::{Autostart, Manifest, Mount, Resource}, - runtime::{error::Context, CGroupEvent, ENV_NAME, ENV_VERSION}, + runtime::{ + console::{Console, Peer}, + io::ContainerIo, + ipc::owned_fd::OwnedFd, + CGroupEvent, ENV_CONTAINER, ENV_NAME, ENV_VERSION, + }, }; -use async_trait::async_trait; use bytes::Bytes; +use derive_new::new; use futures::{ - executor::{ThreadPool, ThreadPoolBuilder}, future::{join_all, ready, Either}, - task::SpawnExt, - Future, FutureExt, TryFutureExt, + Future, FutureExt, Stream, StreamExt, TryFutureExt, }; +use humantime::format_duration; +use itertools::Itertools; use log::{debug, error, info, warn}; use nix::sys::signal::Signal; use std::{ collections::{HashMap, HashSet}, convert::TryFrom, fmt::Debug, - iter::FromIterator, + iter::{once, FromIterator}, + os::unix::net::UnixStream as StdUnixStream, path::PathBuf, result, sync::Arc, }; use tokio::{ + net::UnixStream, + pin, sync::{mpsc, oneshot}, + task::{self, JoinHandle}, time, }; -use Signal::SIGKILL; +use tokio_util::sync::CancellationToken; /// Repository type Repository = Box; @@ -47,23 +57,13 @@ type Args<'a> = Option<&'a Vec>; /// Container environment variables set type Env<'a> = Option<&'a HashMap>; -#[async_trait] -pub(super) trait Process: Send + Sync + Debug { - fn pid(&self) -> Pid; - async fn spawn(&mut self) -> Result<(), Error>; - async fn kill(&mut self, signal: Signal) -> Result<(), Error>; - async fn wait(&mut self) -> Result; - async fn destroy(&mut self) -> Result<(), Error>; -} - #[derive(Debug)] -pub(super) struct State<'a> { - config: &'a Config, +pub(super) struct State { + config: Config, events_tx: EventTx, notification_tx: NotificationTx, mount_control: Arc, - launcher: Launcher, - executor: ThreadPool, + launcher: Forker, containers: HashMap, repositories: HashMap, } @@ -75,7 +75,7 @@ pub(super) struct ContainerState { /// Mount point of the root fs pub root: Option, /// Process information when started - pub process: Option, + pub process: Option, } impl ContainerState { @@ -84,24 +84,25 @@ impl ContainerState { } } -#[derive(Debug)] -pub(super) struct ProcessContext { - process: Box, +#[derive(new, Debug)] +pub(super) struct ContainerContext { + pid: Pid, started: time::Instant, debug: super::debug::Debug, cgroups: cgroups::CGroups, + stop: CancellationToken, + log_task: Option>>, } -impl ProcessContext { - async fn kill(&mut self, signal: Signal) -> Result<(), Error> { - self.process.kill(signal).await - } - +impl ContainerContext { async fn destroy(mut self) { - self.process - .destroy() - .await - .expect("Failed to destroy process"); + // Stop console if there's any any + self.stop.cancel(); + + if let Some(log_task) = self.log_task.take() { + // Wait for the pty to finish + drop(log_task.await); + } self.debug .destroy() @@ -112,13 +113,14 @@ impl ProcessContext { } } -impl<'a> State<'a> { +impl State { /// Create a new empty State instance pub(super) async fn new( - config: &'a Config, + config: Config, events_tx: EventTx, notification_tx: NotificationTx, - ) -> Result, Error> { + forker: Forker, + ) -> Result { let repositories = HashMap::new(); let containers = HashMap::new(); let mount_control = Arc::new( @@ -126,16 +128,6 @@ impl<'a> State<'a> { .await .expect("Failed to initialize mount control"), ); - let launcher = Launcher::start(events_tx.clone(), config.clone(), notification_tx.clone()) - .await - .expect("Failed to start launcher"); - - debug!("Initializing mount thread pool"); - let executor = ThreadPoolBuilder::new() - .name_prefix("northstar-mount-") - .pool_size(config.mount_parallel) - .create() - .expect("Failed to start mount thread pool"); let mut state = State { events_tx, @@ -143,9 +135,8 @@ impl<'a> State<'a> { repositories, containers, config, - launcher, + launcher: forker, mount_control, - executor, }; // Initialize repositories. This populates self.containers and self.repositories @@ -326,13 +317,13 @@ impl<'a> State<'a> { /// Start a container /// `container`: Container to start - /// `args`: Optional command line arguments that overwrite the values from the manifest - /// `env`: Optional env variables that overwrite the values from the manifest + /// `args_extra`: Optional command line arguments that overwrite the values from the manifest + /// `env_extra`: Optional env variables that overwrite the values from the manifest pub(super) async fn start( &mut self, container: &Container, - args: Args<'_>, - env: Env<'_>, + args_extra: Args<'_>, + env_extra: Env<'_>, ) -> Result<(), Error> { let start = time::Instant::now(); info!("Trying to start {}", container); @@ -345,11 +336,12 @@ impl<'a> State<'a> { } // Check optional env variables for reserved ENV_NAME or ENV_VERSION key which cannot be overwritten - if let Some(env) = env { - if env - .keys() - .any(|k| k.as_str() == ENV_NAME || k.as_str() == ENV_VERSION) - { + if let Some(env) = env_extra { + if env.keys().any(|k| { + k.as_str() == ENV_NAME + || k.as_str() == ENV_VERSION + || k.as_str() == "NORTHSTAR_CONSOLE" + }) { return Err(Error::InvalidArguments(format!( "env contains reserved key {} or {}", ENV_NAME, ENV_VERSION @@ -421,65 +413,110 @@ impl<'a> State<'a> { // Get a mutable reference to the container state in order to update the process field let container_state = self.containers.get_mut(container).expect("Internal error"); - // Root of container - let root = container_state - .root - .as_ref() - .map(|root| root.canonicalize().expect("Failed to canonicalize root")) - .unwrap(); - // Spawn process info!("Creating {}", container); - let mut process = match self - .launcher - .create(&root, container, &manifest, args, env) - .await - { - Ok(p) => p, - Err(e) => { - warn!("Failed to create process for {}", container); - return Err(e); - } + // Create a toke to stop tasks spawned related to this container + let stop = CancellationToken::new(); + + // We send the fd to the forker so that it can pass it to the init + let console_fd = if manifest.console { + let peer = Peer::from(container.to_string()); + let (runtime, container) = StdUnixStream::pair().expect("Failed to create socketpair"); + let container: OwnedFd = container.into(); + + let runtime = runtime + .set_nonblocking(true) + .and_then(|_| UnixStream::from_std(runtime)) + .expect("Failed to set socket into nonblocking mode"); + + let notifications = self.notification_tx.subscribe(); + let events_tx = self.events_tx.clone(); + let connection = + Console::connection(runtime, peer, stop.clone(), events_tx, notifications, None); + + // Start console task + task::spawn(connection); + + Some(container) + } else { + None }; - let pid = process.pid(); + // Create container + let config = &self.config; + let pid = self.launcher.create(config, &manifest, console_fd).await?; // Debug - let debug = super::debug::Debug::new(self.config, &manifest, pid).await?; + let debug = super::debug::Debug::new(&self.config, &manifest, pid).await?; // CGroups let cgroups = { debug!("Configuring CGroups for {}", container); let config = manifest.cgroups.clone().unwrap_or_default(); + let events_tx = self.events_tx.clone(); // Creating a cgroup is a northstar internal thing. If it fails it's not recoverable. - cgroups::CGroups::new( - &self.config.cgroup, - self.events_tx.clone(), - container, - &config, - process.pid(), - ) - .await - .expect("Failed to create cgroup") + cgroups::CGroups::new(&self.config.cgroup, events_tx, container, &config, pid) + .await + .expect("Failed to create cgroup") }; + // Open a file handle for stdin, stdout and stderr according to the manifest + let ContainerIo { io, log_task } = io::open(container, &manifest.io) + .await + .expect("IO setup error"); + // Signal the process to continue starting. This can fail because of the container content - if let Err(e) = process.spawn().await { - warn!("Failed to start {} ({}): {}", container, pid, e); + + let path = manifest.init.unwrap(); + let mut args = vec![path.display().to_string()]; + if let Some(extra_args) = args_extra { + args.extend(extra_args.iter().map(ToString::to_string)); + } else if let Some(manifest_args) = manifest.args { + args.extend(manifest_args.iter().map(ToString::to_string)); + }; + + // Prepare the environment for the container according to the manifest + let env = match (env_extra, &manifest.env) { + (Some(env), _) => env.clone(), + (None, Some(env_manifest)) => env_manifest.clone(), + (None, None) => HashMap::with_capacity(3), + }; + let env = env + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .chain(once(format!("{}={}", ENV_CONTAINER, container))) + .chain(once(format!("{}={}", ENV_NAME, container.name()))) + .chain(once(format!("{}={}", ENV_VERSION, container.version()))) + .collect::>(); + + debug!("Container {} init is {:?}", container, path.display()); + debug!("Container {} argv is {}", container, args.iter().join(" ")); + debug!("Container {} env is {}", container, env.iter().join(", ")); + + // Send exec request to launcher + if let Err(e) = self + .launcher + .exec(container.clone(), path, args, env, io) + .await + { + warn!("Failed to exec {} ({}): {}", container, pid, e); + + stop.cancel(); + + if let Some(log_task) = log_task { + drop(log_task.await); + } debug.destroy().await.expect("Failed to destroy debug"); cgroups.destroy().await; return Err(e); } // Add process context to process - container_state.process = Some(ProcessContext { - process: Box::new(process), - started: time::Instant::now(), - debug, - cgroups, - }); + let started = time::Instant::now(); + let context = ContainerContext::new(pid, started, debug, cgroups, stop, log_task); + container_state.process = Some(context); let duration = start.elapsed().as_secs_f32(); info!("Started {} ({}) in {:.03}s", container, pid, duration); @@ -499,16 +536,28 @@ impl<'a> State<'a> { let container_state = self.state_mut(container)?; match &mut container_state.process { - Some(process) => { + Some(context) => { info!("Killing {} with {}", container, signal.as_str()); - process.kill(signal).await + let pid = context.pid; + let process_group = nix::unistd::Pid::from_raw(-(pid as i32)); + match nix::sys::signal::kill(process_group, Some(signal)) { + Ok(_) => Ok(()), + Err(nix::Error::ESRCH) => { + debug!("Process {} already exited", pid); + Ok(()) + } + Err(e) => unimplemented!("Kill error {}", e), + } } None => Err(Error::StopContainerNotStarted(container.clone())), } } /// Shutdown the runtime: stop running applications and umount npks - pub(super) async fn shutdown(mut self) -> Result<(), Error> { + pub(super) async fn shutdown( + mut self, + event_rx: impl Stream, + ) -> Result<(), Error> { let to_umount = self .containers .iter() @@ -516,19 +565,39 @@ impl<'a> State<'a> { .map(|(container, _)| container.clone()) .collect::>(); - for (container, state) in &mut self.containers { - if let Some(mut context) = state.process.take() { - let pid = context.process.pid(); - info!("Sending SIGKILL to {} ({})", container, pid); - nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid as i32), SIGKILL).ok(); - - info!("Waiting for {} to exit", container); - let exit_status = - time::timeout(time::Duration::from_secs(10), context.process.wait()) - .await - .context(format!("Killing {}", container))?; - debug!("Container {} terminated with {:?}", container, exit_status); - context.destroy().await; + let to_kill = self + .containers + .iter() + .filter_map(|(container, state)| state.process.as_ref().map(|_| container.clone())) + .collect::>(); + + for container in &to_kill { + self.kill(container, Signal::SIGKILL).await?; + } + + // Wait until all processes are dead + if self + .containers + .values() + .any(|state| state.process.is_some()) + { + pin!(event_rx); + + loop { + if let Some(Event::Container(container, event)) = event_rx.next().await { + self.on_event(&container, &event, true).await?; + + // Check if all containers exited if this is a exit event + if let ContainerEvent::Exit(_) = event { + if self + .containers + .values() + .all(|state| state.process.is_none()) + { + break; + } + } + } } } @@ -536,8 +605,6 @@ impl<'a> State<'a> { self.umount(&container).await?; } - self.launcher.shutdown().await?; - Ok(()) } @@ -553,32 +620,38 @@ impl<'a> State<'a> { let container = repository.insert(rx).await?; // Check if container is already known and remove newly installed one if so - if let Ok(state) = self.state(&container) { + let already_installed = self + .state(&container) + .ok() + .map(|state| state.repository.clone()); + + if let Some(current_repository) = already_installed { warn!( "Skipping duplicate container {} which is already in repository {}", - container, state.repository + container, current_repository ); + let repository = self .repositories .get_mut(id) .ok_or_else(|| Error::InvalidRepository(id.to_string()))?; repository.remove(&container).await?; - Err(Error::InstallDuplicate(container)) - } else { - // Add the container to the state - self.containers.insert( - container.clone(), - ContainerState { - repository: id.into(), - ..Default::default() - }, - ); - info!("Successfully installed {}", container); + return Err(Error::InstallDuplicate(container)); + } - self.container_event(&container, ContainerEvent::Installed); + // Add the container to the state + self.containers.insert( + container.clone(), + ContainerState { + repository: id.into(), + ..Default::default() + }, + ); + info!("Successfully installed {}", container); - Ok(()) - } + self.container_event(&container, ContainerEvent::Installed); + + Ok(()) } /// Remove and umount a specific app @@ -632,6 +705,7 @@ impl<'a> State<'a> { &mut self, container: &Container, exit_status: &ExitStatus, + is_shutdown: bool, ) -> Result<(), Error> { let autostart = self .manifest(container) @@ -641,16 +715,21 @@ impl<'a> State<'a> { if let Ok(state) = self.state_mut(container) { if let Some(process) = state.process.take() { let is_critical = autostart == Some(Autostart::Critical); + let is_critical = is_critical && !is_shutdown; let duration = process.started.elapsed(); if is_critical { error!( - "Critical process {} exited after {:?} with status {}", - container, duration, exit_status, + "Critical process {} exited after {} with status {}", + container, + format_duration(duration), + exit_status, ); } else { info!( - "Process {} exited after {:?} with status {}", - container, duration, exit_status, + "Process {} exited after {} with status {}", + container, + format_duration(duration), + exit_status, ); } @@ -658,6 +737,8 @@ impl<'a> State<'a> { self.container_event(container, ContainerEvent::Exit(exit_status.clone())); + info!("Container {} exited with status {}", container, exit_status); + // This is a critical flagged container that exited with a error exit code. That's not good... if !exit_status.success() && is_critical { return Err(Error::CriticalContainer( @@ -675,11 +756,12 @@ impl<'a> State<'a> { &mut self, container: &Container, event: &ContainerEvent, + is_shutdown: bool, ) -> Result<(), Error> { match event { ContainerEvent::Started => (), ContainerEvent::Exit(exit_status) => { - self.on_exit(container, exit_status).await?; + self.on_exit(container, exit_status, is_shutdown).await?; } ContainerEvent::Installed => (), ContainerEvent::Uninstalled => (), @@ -695,7 +777,7 @@ impl<'a> State<'a> { pub(super) async fn on_request( &mut self, request: &mut Request, - response_tx: oneshot::Sender, + repsponse: oneshot::Sender, ) -> Result<(), Error> { match request { Request::Message(message) => { @@ -787,7 +869,7 @@ impl<'a> State<'a> { // A error on the response_tx means that the connection // was closed in the meantime. Ignore it. - response_tx.send(response).ok(); + repsponse.send(response).ok(); } else { warn!("Received message is not a request"); } @@ -800,7 +882,7 @@ impl<'a> State<'a> { // A error on the response_tx means that the connection // was closed in the meantime. Ignore it. - response_tx.send(payload).ok(); + repsponse.send(payload).ok(); } } Ok(()) @@ -829,11 +911,6 @@ impl<'a> State<'a> { } } - // Spawn mount tasks onto the executor - let mounts = mounts - .drain(..) - .map(|t| self.executor.spawn_with_handle(t).unwrap()); - // Process mount results let mut result = Vec::new(); for (container, mount_result) in containers.iter().zip(join_all(mounts).await) { @@ -855,7 +932,7 @@ impl<'a> State<'a> { warn!("Mount operation failed after {:.03}s", duration); } else { info!( - "Successfully {} container(s) in {:.03}s", + "Successfully mounted {} container(s) in {:.03}s", result.len(), duration ); @@ -869,7 +946,7 @@ impl<'a> State<'a> { for (container, state) in &self.containers { let manifest = self.manifest(container).expect("Internal error").clone(); let process = state.process.as_ref().map(|context| api::model::Process { - pid: context.process.pid(), + pid: context.pid, uptime: context.started.elapsed().as_nanos() as u64, }); let repository = state.repository.clone(); diff --git a/northstar/src/seccomp/bpf.rs b/northstar/src/seccomp/bpf.rs index 72f269087..b14fd9747 100644 --- a/northstar/src/seccomp/bpf.rs +++ b/northstar/src/seccomp/bpf.rs @@ -9,7 +9,7 @@ use bindings::{ }; use log::trace; use nix::errno::Errno; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{ collections::{HashMap, HashSet}, mem::size_of, @@ -177,7 +177,7 @@ fn check_platform_requirements() { compile_error!("Seccomp is not supported on Big Endian architectures"); } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq)] pub struct SockFilter { pub code: u16, pub jt: u8, @@ -185,6 +185,32 @@ pub struct SockFilter { pub k: u32, } +impl Serialize for SockFilter { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let a = (self.code as u32) << 16 | (self.jt as u32) << 8 | self.jf as u32; + let value = (a as u64) << 32 | self.k as u64; + serializer.serialize_u64(value) + } +} + +impl<'de> Deserialize<'de> for SockFilter { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = u64::deserialize(deserializer)?; + let a = (value >> 32) as u32; + let code = ((a & 0xFFFF0000) >> 16) as u16; + let jt = ((a & 0xFF00) >> 8) as u8; + let jf = (a & 0xFF) as u8; + let k = (value & 0xFFFFFFFF) as u32; + Ok(SockFilter { code, jt, jf, k }) + } +} + impl From<&SockFilter> for sock_filter { fn from(s: &SockFilter) -> sock_filter { sock_filter { @@ -655,3 +681,24 @@ fn bpf_jump(code: u32, k: u32, jt: u8, jf: u8) -> SockFilter { jf, } } + +#[cfg(test)] +mod test { + use super::SockFilter; + use proptest::prelude::*; + + proptest! { + #[test] + fn sock_filter_serialize_deserialize(a in 0..100, b in 0i32..10) { + let filter = SockFilter { + code: (a + b) as u16, + jt: a as u8, + jf: b as u8, + k: (a * b) as u32, + }; + let serialized = serde_json::to_string(&filter).unwrap(); + let deserialized: SockFilter = serde_json::from_str(&serialized).unwrap(); + prop_assert_eq!(filter, deserialized); + } + } +} diff --git a/northstar/src/util.rs b/northstar/src/util.rs deleted file mode 100644 index 9596ee511..000000000 --- a/northstar/src/util.rs +++ /dev/null @@ -1,40 +0,0 @@ -use nix::{sys::stat, unistd}; -use std::{ - os::unix::prelude::{MetadataExt, PermissionsExt}, - path::{Path, PathBuf}, -}; -use tokio::fs; - -/// Return true if path is read and writeable -pub(crate) async fn is_rw(path: &Path) -> bool { - match fs::metadata(path).await { - Ok(stat) => { - let same_uid = stat.uid() == unistd::getuid().as_raw(); - let same_gid = stat.gid() == unistd::getgid().as_raw(); - let mode = stat::Mode::from_bits_truncate(stat.permissions().mode()); - - let is_readable = (same_uid && mode.contains(stat::Mode::S_IRUSR)) - || (same_gid && mode.contains(stat::Mode::S_IRGRP)) - || mode.contains(stat::Mode::S_IROTH); - let is_writable = (same_uid && mode.contains(stat::Mode::S_IWUSR)) - || (same_gid && mode.contains(stat::Mode::S_IWGRP)) - || mode.contains(stat::Mode::S_IWOTH); - - is_readable && is_writable - } - Err(_) => false, - } -} - -pub(crate) trait PathExt { - fn join_strip>(&self, w: T) -> PathBuf; -} - -impl PathExt for Path { - fn join_strip>(&self, w: T) -> PathBuf { - self.join(match w.as_ref().strip_prefix("/") { - Ok(stripped) => stripped, - Err(_) => w.as_ref(), - }) - } -} diff --git a/tools/nstar/src/main.rs b/tools/nstar/src/main.rs index c5cc3f4f2..93ed43460 100644 --- a/tools/nstar/src/main.rs +++ b/tools/nstar/src/main.rs @@ -23,7 +23,7 @@ use std::{ }; use tokio::{ fs, - io::{copy, AsyncBufReadExt, AsyncRead, AsyncWrite, BufReader}, + io::{copy, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}, net::{TcpStream, UnixStream}, time, }; @@ -286,8 +286,8 @@ async fn main() -> Result<()> { clap_complete::generate( shell, - &mut Opt::into_app(), - Opt::into_app().get_name().to_string(), + &mut Opt::command(), + Opt::command().get_name().to_string(), &mut output, ); @@ -319,12 +319,12 @@ async fn main() -> Result<()> { // Subscribe to notifications and print them Subcommand::Notifications { number } => { if opt.json { - let framed = Client::new(io, Some(100), opt.timeout) + let mut framed = Client::new(io, Some(100), opt.timeout) .await .with_context(|| format!("Failed to connect to {}", opt.url))? .framed(); - let mut lines = BufReader::new(framed).lines(); + let mut lines = BufReader::new(framed.get_mut()).lines(); for _ in 0..number.unwrap_or(usize::MAX) { match lines.next_line().await.context("Failed to read stream")? { Some(line) => println!("{}", line), @@ -366,16 +366,21 @@ async fn main() -> Result<()> { // Extra file transfer for install hack if let Subcommand::Install { npk, .. } = command { + framed.flush().await.context("Failed to flush")?; + framed.get_mut().flush().await.context("Failed to flush")?; + copy( &mut fs::File::open(npk).await.context("Failed to open npk")?, - &mut framed, + &mut framed.get_mut(), ) .await .context("Failed to stream npk")?; } + framed.get_mut().flush().await.context("Failed to flush")?; + if opt.json { - let response = BufReader::new(framed) + let response = BufReader::new(framed.get_mut()) .lines() .next_line() .await diff --git a/tools/sextant/src/pack.rs b/tools/sextant/src/pack.rs index 662531fcd..3b98e47f8 100644 --- a/tools/sextant/src/pack.rs +++ b/tools/sextant/src/pack.rs @@ -29,7 +29,7 @@ pub(crate) fn pack( if manifest.init.is_some() { let tmp = tempdir().context("Failed to create temporary directory")?; let name = manifest.name.clone(); - let num = clones.to_string().chars().count() - 1; + let num = clones.to_string().chars().count(); for n in 0..clones { manifest.name = format!("{}-{:0m$}", name, n, m = num) .try_into() diff --git a/tools/stress/Cargo.toml b/tools/stress/Cargo.toml index d635b29a1..6cd135f1f 100644 --- a/tools/stress/Cargo.toml +++ b/tools/stress/Cargo.toml @@ -10,6 +10,8 @@ anyhow = "1.0.54" clap = { version = "3.1.0", features = ["derive"] } env_logger = "0.9.0" futures = "0.3.21" +humantime = "2.1.0" +itertools = "0.10.3" log = "0.4.14" northstar = { path = "../../northstar", features = ["api"], default-features = false } rand = "0.8.5" diff --git a/tools/stress/src/main.rs b/tools/stress/src/main.rs index 2b351e4c8..ecc59f88d 100644 --- a/tools/stress/src/main.rs +++ b/tools/stress/src/main.rs @@ -1,21 +1,25 @@ use anyhow::{anyhow, Context, Result}; use clap::Parser; use futures::{ - future::{self, pending, ready, try_join_all}, + future::{self, pending, ready, try_join_all, Either}, FutureExt, StreamExt, }; +use humantime::parse_duration; +use itertools::Itertools; use log::{debug, info}; use northstar::api::{ client::{self, Client}, - model::{self, ExitStatus, Notification}, + model::{self, Container, ExitStatus, Notification}, }; -use std::{path::PathBuf, str::FromStr}; +use std::{path::PathBuf, str::FromStr, sync::Arc, time::Duration}; use tokio::{ io::{AsyncRead, AsyncWrite}, net::{TcpStream, UnixStream}, - pin, select, task, time, + pin, select, + sync::Barrier, + task, time, }; -use tokio_util::{either::Either, sync::CancellationToken}; +use tokio_util::sync::CancellationToken; use url::Url; #[derive(Clone, Debug, PartialEq)] @@ -53,12 +57,16 @@ struct Opt { url: url::Url, /// Duration to run the test for in seconds - #[clap(short, long)] - duration: Option, + #[clap(short, long, parse(try_from_str = parse_duration))] + duration: Option, /// Random delay between each iteration within 0..value ms + #[clap(short, long, parse(try_from_str = parse_duration))] + random: Option, + + /// Single client #[clap(short, long)] - random: Option, + single: bool, /// Mode #[clap(short, long, default_value = "start-stop")] @@ -72,17 +80,13 @@ struct Opt { #[clap(long, required_if_eq("mode", "install-uninstall"))] repository: Option, - /// Relaxed result - #[clap(long)] - relaxed: bool, - /// Initial random delay in ms to randomize tasks #[clap(long)] initial_random_delay: Option, /// Notification timeout in seconds - #[clap(short, long, default_value = "60")] - timeout: u64, + #[clap(short, long, parse(try_from_str = parse_duration), default_value = "60s")] + timeout: Duration, } pub trait N: AsyncRead + AsyncWrite + Send + Unpin {} @@ -122,7 +126,6 @@ async fn main() -> Result<()> { debug!("address: {}", opt.url.to_string()); debug!("repository: {:?}", opt.repository); debug!("npk: {:?}", opt.npk); - debug!("relaxed: {}", opt.relaxed); debug!("random: {:?}", opt.random); debug!("timeout: {:?}", opt.timeout); @@ -140,125 +143,111 @@ async fn main() -> Result<()> { .iter() .filter(|c| c.manifest.init.is_some()) .map(|c| c.container.clone()) + .sorted() .collect::>(); drop(client); let mut tasks = Vec::new(); - let token = CancellationToken::new(); - - // Check random value that cannot be 0 - if let Some(delay) = opt.random { - assert!(delay > 0, "Invalid random value"); - } + let stop = CancellationToken::new(); - // Max string len of all containers - let len = containers - .iter() - .map(ToString::to_string) - .map(|s| s.len()) - .sum::(); - - let start = CancellationToken::new(); - - for container in &containers { - let container = container.clone(); - let initial_random_delay = opt.initial_random_delay; - let mode = opt.mode.clone(); - let random = opt.random; - let relaxed = opt.relaxed; - let start = start.clone(); - let token = token.clone(); - let url = opt.url.clone(); - let timeout = opt.timeout; - - debug!("Spawning task for {}", container); + if opt.single { + let stop = stop.clone(); let task = task::spawn(async move { - start.cancelled().await; - if let Some(initial_delay) = initial_random_delay { - time::sleep(time::Duration::from_millis( - rand::random::() % initial_delay, - )) - .await; - } - - let notifications = if relaxed { None } else { Some(100) }; let mut client = client::Client::new( - io(&url).await?, - notifications, + io(&opt.url).await?, + Some(containers.len() * 3), time::Duration::from_secs(30), ) .await?; + let mut iterations = 0; loop { - if mode == Mode::MountStartStopUmount || mode == Mode::MountUmount { - info!("{:() % delay); - info!("{: ready(r), - Err(e) => ready(Result::::Err(anyhow!("task error: {}", e))), + Err(e) => ready(Result::::Err(anyhow!("task error: {}", e))), }); - tasks.push(task); + + tasks.push(futures::future::Either::Right(task)); + } else { + // Sync the start of all tasks + let start_barrier = Arc::new(Barrier::new(containers.len())); + + for container in &containers { + let container = container.clone(); + let initial_random_delay = opt.initial_random_delay; + let mode = opt.mode.clone(); + let random = opt.random; + let start_barrier = start_barrier.clone(); + let timeout = opt.timeout; + let stop = stop.clone(); + let url = opt.url.clone(); + + debug!("Spawning task for {}", container); + let task = task::spawn(async move { + let mut client = + client::Client::new(io(&url).await?, Some(1000), time::Duration::from_secs(30)) + .await?; + + if let Some(initial_delay) = initial_random_delay { + time::sleep(time::Duration::from_millis( + rand::random::() % initial_delay, + )) + .await; + } + + let mut iterations = 0usize; + + start_barrier.wait().await; + + loop { + iteration(&mode, &container, &mut client, timeout, random).await?; + iterations += 1; + + if stop.is_cancelled() { + break Ok(iterations); + } + } + }) + .then(|r| match r { + Ok(r) => ready(r), + Err(e) => ready(Result::::Err(anyhow!("task error: {}", e))), + }); + + tasks.push(Either::Left(task)); + } } - info!("Starting {} tasks", containers.len()); - start.cancel(); + info!("Starting {} tasks", tasks.len()); let mut tasks = try_join_all(tasks); let ctrl_c = tokio::signal::ctrl_c(); let duration = opt .duration - .map(time::Duration::from_secs) .map(time::sleep) .map(Either::Left) .unwrap_or_else(|| Either::Right(future::pending::<()>())); @@ -266,18 +255,71 @@ async fn main() -> Result<()> { let result = select! { _ = duration => { info!("Stopping because test duration exceeded"); - token.cancel(); + stop.cancel(); tasks.await } _ = ctrl_c => { info!("Stopping because of ctrlc"); - token.cancel(); + stop.cancel(); tasks.await } r = &mut tasks => r, }; - info!("Total iterations: {}", result?.iter().sum::()); + info!("Total iterations: {}", result?.iter().sum::()); + Ok(()) +} + +async fn iteration( + mode: &Mode, + container: &Container, + client: &mut Client>, + timeout: Duration, + random: Option, +) -> Result<()> { + if *mode == Mode::MountStartStopUmount || *mode == Mode::MountUmount { + info!("{} mount", &container); + client.mount(vec![container.clone()]).await?; + } + + if *mode != Mode::MountUmount { + info!("{}: start", container); + client.start(container).await?; + let started = Notification::Started { + container: container.clone(), + }; + await_notification(client, started, timeout).await?; + } + + if let Some(delay) = random { + info!("{}: sleeping for {:?}", container, delay); + time::sleep(delay).await; + } + + if *mode != Mode::MountUmount { + info!("{}: stopping", container); + client + .kill(container.clone(), 15) + .await + .context("Failed to stop container")?; + + info!("{}: waiting for termination", container); + let stopped = Notification::Exit { + container: container.clone(), + status: ExitStatus::Signalled { signal: 15 }, + }; + await_notification(client, stopped, timeout).await?; + } + + // Check if we need to umount + if *mode != Mode::StartStop { + info!("{}: umounting", container); + client + .umount(container.clone()) + .await + .context("Failed to umount")?; + } + Ok(()) } @@ -285,10 +327,8 @@ async fn main() -> Result<()> { async fn await_notification( client: &mut Client, notification: Notification, - duration: u64, + duration: Duration, ) -> Result<()> { - let duration = time::Duration::from_secs(duration); - time::timeout(duration, async { loop { match client.next().await { @@ -300,7 +340,7 @@ async fn await_notification( } }) .await - .context("Failed to wait for notification")? + .with_context(|| format!("Failed to wait for notification: {:?}", notification))? } /// Install and uninstall an npk in a loop @@ -310,7 +350,7 @@ async fn install_uninstall(opt: &Opt) -> Result<()> { let timeout = opt .duration - .map(|d| Either::Left(time::sleep(time::Duration::from_secs(d)))) + .map(|d| Either::Left(time::sleep(d))) .unwrap_or_else(|| Either::Right(pending())); pin!(timeout);