diff --git a/Cargo.lock b/Cargo.lock index fe5e75b0..f8256413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,15 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "assert_fs" version = "1.0.6" @@ -129,13 +120,39 @@ version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "ansi_term", + "bitflags", + "textwrap 0.11.0", + "unicode-width", +] + +[[package]] +name = "clap" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b34190c12bd1d613deba77e1cc13e68eaf4a0d51e389dbd485b7bfe15a47c0" +dependencies = [ "atty", "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", "strsim", - "textwrap", - "unicode-width", - "vec_map", + "termcolor", + "textwrap 0.14.2", +] + +[[package]] +name = "clap_derive" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -158,7 +175,7 @@ checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10" dependencies = [ "atty", "cast", - "clap", + "clap 2.34.0", "criterion-plot", "csv", "itertools", @@ -394,6 +411,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -606,6 +629,15 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "paste" version = "1.0.6" @@ -679,6 +711,30 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -929,9 +985,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" @@ -982,6 +1038,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + [[package]] name = "thiserror" version = "1.0.30" @@ -1039,7 +1101,7 @@ dependencies = [ "burst", "byte-unit", "byteorder", - "clap", + "clap 3.0.8", "core_affinity", "criterion", "either", @@ -1086,10 +1148,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" [[package]] -name = "vec_map" -version = "0.8.2" +name = "version_check" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "virtio-bindings" diff --git a/Cargo.toml b/Cargo.toml index 257c3c0d..5dba48a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,8 +45,8 @@ xhypervisor = { git = "https://github.com/RWTH-OS/xhypervisor.git", branch = "aa [dependencies] bitflags = "1.3" byteorder = "1.4" -byte-unit = "4.0" -clap = "2.34" +byte-unit = { version = "4.0", default-features = false, features = ["std"] } +clap = { version = "3", features = ["derive", "env"] } core_affinity = "0.5" either = "1.6" env_logger = "0.9" @@ -57,6 +57,7 @@ goblin = { version = "0.4", default-features = false, features = ["elf64", "elf3 lazy_static = "1.4" libc = "0.2" log = "0.4" +mac_address = "1.1" rustc-serialize = "0.3" thiserror = "1.0" @@ -66,7 +67,6 @@ rftrace-frontend = { version = "0.1", optional = true } [target.'cfg(target_os = "linux")'.dependencies] kvm-bindings = "0.5" kvm-ioctls = "0.10" -mac_address = "1.1" nix = "0.23" tun-tap = { version = "0.1", default-features = false } virtio-bindings = { version = "0.1", features = ["virtio-v4_14_0"] } diff --git a/src/bin/uhyve.rs b/src/bin/uhyve.rs index 71612dd8..f51e5438 100644 --- a/src/bin/uhyve.rs +++ b/src/bin/uhyve.rs @@ -1,24 +1,22 @@ #![warn(rust_2018_idioms)] -#[macro_use] -extern crate log; -#[macro_use] -extern crate clap; - -use std::collections::HashSet; -use std::env; +use std::ffi::OsString; +use std::net::Ipv4Addr; +use std::num::{NonZeroU32, ParseIntError, TryFromIntError}; +use std::ops::RangeInclusive; use std::path::PathBuf; +use std::process; use std::str::FromStr; +use std::{fmt, iter}; -use uhyvelib::utils; -use uhyvelib::vm; -use uhyvelib::Uhyve; - -use byte_unit::Byte; -use clap::{App, Arg}; +use byte_unit::{AdjustedByte, Byte, ByteError}; +use clap::{App, ErrorKind, IntoApp, Parser}; +use core_affinity::CoreId; +use either::Either; +use mac_address::MacAddress; +use thiserror::Error; -const MINIMAL_GUEST_SIZE: usize = 16 * 1024 * 1024; -const DEFAULT_GUEST_SIZE: usize = 64 * 1024 * 1024; +use uhyvelib::{vm, Uhyve}; #[cfg(feature = "instrument")] fn setup_trace() { @@ -44,224 +42,365 @@ fn setup_trace() { } } -// Note that we end main with `std::process::exit` to set the return value and -// as a result destructors are not run and cleanup may not happen. -fn main() { - #[cfg(feature = "instrument")] - setup_trace(); +#[derive(Parser, Debug)] +#[clap(version, author, about)] +struct Args { + /// Print kernel messages + #[clap(short, long)] + verbose: bool, - env_logger::init(); + #[clap(flatten, help_heading = "MEMORY")] + memory_args: MemoryArgs, - let matches = App::new("uhyve") - .version(crate_version!()) - .setting(clap::AppSettings::TrailingVarArg) - .setting(clap::AppSettings::AllowLeadingHyphen) - .author(crate_authors!("\n")) - .about("A minimal hypervisor for RustyHermit") - .arg( - Arg::with_name("VERBOSE") - .short("v") - .long("verbose") - .help("Print also kernel messages"), - ) - .arg( - Arg::with_name("DISABLE_HUGEPAGE") - .long("disable-hugepages") - .help("Disable the usage of huge pages"), - ) - .arg( - Arg::with_name("MERGEABLE") - .long("mergeable") - .help("Enable kernel feature to merge same pages"), - ) - .arg( - Arg::with_name("MEM") - .short("m") - .long("memsize") - .value_name("MEM") - .help("Memory size of the guest") - .takes_value(true) - .env("HERMIT_MEM"), - ) - .arg( - Arg::with_name("CPUS") - .short("c") - .long("cpus") - .value_name("CPUS") - .help("Number of guest processors") - .takes_value(true) - .env("HERMIT_CPUS"), - ) - .arg( - Arg::with_name("CPU_AFFINITY") - .short("a") - .long("affinity") - .value_name("cpulist") - .help("CPU Affinity of guest CPUs on Host") - .long_help( - "A list of CPUs delimited by commas onto which - the virtual CPUs should be bound. This may improve - performance. - ", - ), - ) - .arg( - Arg::with_name("GDB_PORT") - .short("s") - .long("gdb_port") - .value_name("GDB_PORT") - .help("Enables GDB-Stub on given port") - .takes_value(true) - .env("HERMIT_GDB_PORT"), - ) - .arg( - Arg::with_name("NETIF") - .long("nic") - .value_name("NETIF") - .help("Name of the network interface") - .takes_value(true) - .env("HERMIT_NETIF"), - ) - /*.arg( - Arg::with_name("IP") - .long("ip") - .value_name("IP") - .help("IP address of the guest") - .takes_value(true) - .env("HERMIT_IP"), - ) - .arg( - Arg::with_name("GATEWAY") - .long("gateway") - .value_name("GATEWAY") - .help("Gateway address") - .takes_value(true) - .env("HERMIT_GATEWAY"), - ) - .arg( - Arg::with_name("MASK") - .long("mask") - .value_name("MASK") - .help("Network mask") - .takes_value(true) - .env("HERMIT_MASK"), - ) - .arg( - Arg::with_name("MAC") - .long("mac") - .value_name("MAC") - .help("MAC address of the network interface") - .takes_value(true) - .env("HERMIT_MASK"), - )*/ - .arg( - Arg::with_name("KERNEL") - .help("Sets path to the kernel") - .required(true) - .index(1), - ) - .arg( - Arg::with_name("ARGUMENTS") - .help("Arguments of the unikernel") - .required(false) - .multiple(true) - .max_values(255), - ) - .get_matches(); - - let path = PathBuf::from_str( - matches - .value_of("KERNEL") - .expect("Expect path to the kernel!"), - ) - .expect("Invalid kernel path"); - let mem_size: usize = matches - .value_of("MEM") - .map(|s| { - let mem = Byte::from_str(s) - .expect("Invalid MEM specified") - .get_bytes() - .try_into() - .unwrap(); - if mem < MINIMAL_GUEST_SIZE { - warn!( - "Resize guest memory to {}", - Byte::from_bytes(MINIMAL_GUEST_SIZE.try_into().unwrap()) - ); - MINIMAL_GUEST_SIZE - } else { - mem + #[clap(flatten, help_heading = "CPU")] + cpu_args: CpuArgs, + + /// GDB server port + /// + /// Starts a GDB server on the provided port and waits for a connection. + #[clap(short = 's', long, env = "HERMIT_GDB_PORT")] + gdb_port: Option, + + // #[clap(flatten, help_heading = "NETWORK")] + #[clap(skip)] + network_args: NetworkArgs, + + /// The kernel to execute + #[clap(parse(from_os_str))] + kernel: PathBuf, + + /// Arguments to forward to the kernel + #[clap(parse(from_os_str))] + kernel_args: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct GuestMemorySize(Byte); + +impl GuestMemorySize { + const fn minimum() -> Byte { + Byte::from_bytes(16 * 1024 * 1024) + } + + pub fn get(self) -> usize { + self.0.get_bytes().try_into().unwrap() + } +} + +impl Default for GuestMemorySize { + fn default() -> Self { + Self(Byte::from_bytes(64 * 1024 * 1024)) + } +} + +impl fmt::Display for GuestMemorySize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.get_appropriate_unit(true).fmt(f) + } +} + +#[derive(Error, Debug)] +#[error("invalid amount of guest memory (minimum: {}, found {0})", GuestMemorySize::minimum().get_appropriate_unit(true))] +pub struct InvalidGuestMemorySizeError(AdjustedByte); + +impl TryFrom for GuestMemorySize { + type Error = InvalidGuestMemorySizeError; + + fn try_from(value: Byte) -> Result { + if value >= Self::minimum() { + Ok(Self(value)) + } else { + let value = value.get_appropriate_unit(true); + Err(InvalidGuestMemorySizeError(value)) + } + } +} + +#[derive(Error, Debug)] +pub enum ParseByteError { + #[error(transparent)] + Parse(#[from] ByteError), + + #[error(transparent)] + InvalidMemorySize(#[from] InvalidGuestMemorySizeError), +} + +impl FromStr for GuestMemorySize { + type Err = ParseByteError; + + fn from_str(s: &str) -> Result { + let requested = Byte::from_str(s)?; + let memory_size = requested.try_into()?; + Ok(memory_size) + } +} + +#[derive(Parser, Debug)] +struct MemoryArgs { + /// Guest RAM size + #[clap(short = 'm', long, default_value_t, env = "HERMIT_MEMORY_SIZE")] + memory_size: GuestMemorySize, + + /// No Transparent Hugepages + /// + /// Don't advise the kernel to enable Transparent Hugepages [THP] on the virtual RAM. + /// + /// [THP]: https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html + #[clap(long)] + no_thp: bool, + + /// Kernel Samepage Merging + /// + /// Advise the kernel to enable Kernel Samepage Merging [KSM] on the virtual RAM. + /// + /// [KSM]: https://www.kernel.org/doc/html/latest/admin-guide/mm/ksm.html + #[clap(long)] + ksm: bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct CpuCount(NonZeroU32); + +impl CpuCount { + pub fn get(self) -> u32 { + self.0.get() + } +} + +impl Default for CpuCount { + fn default() -> Self { + let default = 1.try_into().unwrap(); + Self(default) + } +} + +impl fmt::Display for CpuCount { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl TryFrom for CpuCount { + type Error = TryFromIntError; + + fn try_from(value: u32) -> Result { + value.try_into().map(Self) + } +} + +impl FromStr for CpuCount { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + let count = s.parse()?; + Ok(Self(count)) + } +} + +#[derive(Debug, Clone)] +struct Affinity(Vec); + +impl Affinity { + fn parse_ranges_iter<'a>( + ranges: impl IntoIterator + 'a, + ) -> impl Iterator> + 'a { + struct ParsedRange(RangeInclusive); + + impl FromStr for ParsedRange { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + let range = match s.split_once('-') { + Some((start, end)) => start.parse()?..=end.parse()?, + None => { + let idx = s.parse()?; + idx..=idx + } + }; + Ok(Self(range)) } - }) - .unwrap_or(DEFAULT_GUEST_SIZE); - let num_cpus = matches - .value_of("CPUS") - .and_then(|cpus| cpus.parse().ok()) - .unwrap_or(1); - - let cpu_affinity = matches.values_of("CPU_AFFINITY").map(|affinity| { - let parsed_affinity = utils::parse_ranges(affinity) - .collect::, _>>() - .expect("Invalid parameters passed for CPU_AFFINITY"); - - // According to https://github.com/Elzair/core_affinity_rs/issues/3 - // on linux this gives a list of CPUs the process is allowed to run on - // (as opposed to all CPUs available on the system as the docs suggest) - let core_ids = core_affinity::get_core_ids() - .expect("Dependency core_affinity failed to find any available CPUs") + } + + ranges .into_iter() - .filter(|core_id| parsed_affinity.contains(&core_id.id)) - .collect::>(); - assert_eq!(core_ids.len(), num_cpus as usize); - core_ids - }); - - let ip = None; //matches.value_of("IP").or(None); - let gateway = None; // matches.value_of("GATEWAY").or(None); - let mask = None; //matches.value_of("MASK").or(None); - let nic = None; //matches.value_of("NETIF").or(None); - - let mut mergeable = envmnt::is_or("HERMIT_MERGEABLE", false); - if matches.is_present("MERGEABLE") { - mergeable = true; + .map(ParsedRange::from_str) + .flat_map(|range| match range { + Ok(range) => Either::Left(range.0.map(Ok)), + Err(err) => Either::Right(iter::once(Err(err))), + }) } - // per default we use huge page to improve the performance, - // if the kernel supports transparent hugepages - let hugepage_default = true; - info!("Default hugepages set to: {}", hugepage_default); - // HERMIT_HUGEPAGES overrides the default we detected. - let mut hugepage = envmnt::is_or("HERMIT_HUGEPAGE", hugepage_default); - if matches.is_present("DISABLE_HUGEPAGE") { - hugepage = false; + + fn parse_ranges(ranges: &str) -> Result, ParseIntError> { + Self::parse_ranges_iter(ranges.split([' ', ','].as_slice())).collect() } - let mut verbose = envmnt::is_or("HERMIT_VERBOSE", false); - if matches.is_present("VERBOSE") { - verbose = true; +} + +#[derive(Error, Debug)] +enum ParseAffinityError { + #[error(transparent)] + Parse(#[from] ParseIntError), + + #[error( + "Available cores: {available_cores:?}, requested affinities: {requested_affinities:?}" + )] + InvalidValue { + available_cores: Vec, + requested_affinities: Vec, + }, +} + +impl FromStr for Affinity { + type Err = ParseAffinityError; + + fn from_str(s: &str) -> Result { + let available_cores = core_affinity::get_core_ids() + .unwrap() + .into_iter() + .map(|core_id| core_id.id) + .collect::>(); + + let requested_affinities = Self::parse_ranges(s)?; + + if !requested_affinities + .iter() + .all(|affinity| available_cores.contains(affinity)) + { + return Err(ParseAffinityError::InvalidValue { + available_cores, + requested_affinities, + }); + } + + let core_ids = requested_affinities + .into_iter() + .map(|affinity| CoreId { id: affinity }) + .collect(); + Ok(Self(core_ids)) } - let gdbport = matches - .value_of("GDB_PORT") - .map(|p| p.parse::().expect("Could not parse gdb port")) - .or_else(|| { - env::var("HERMIT_GDB_PORT") - .ok() - .map(|p| p.parse::().expect("Could not parse gdb port")) - }); +} + +#[derive(Parser, Debug, Clone)] +struct CpuArgs { + /// Number of guest CPUs + #[clap(short, long, default_value_t, env = "HERMIT_CPU_COUNT")] + cpu_count: CpuCount, + + /// Bind guest vCPUs to host cpus + /// + /// A list of host CPU numbers onto which the guest vCPUs should be bound to obtain performance benefits. + /// List items may be single numbers or inclusive ranges. + /// List items may be separated with commas or spaces. + /// + /// # Examples + /// + /// * `--affinity "0 1 2"` + /// + /// * `--affinity 0-1,2` + #[clap(short, long, name = "CPUs")] + affinity: Option, +} + +impl CpuArgs { + fn get_affinity(self, app: &mut App<'_>) -> Option> { + self.affinity.map(|affinity| { + let affinity_num_vals = affinity.0.len(); + let cpus_num_vals = self.cpu_count.get().try_into().unwrap(); + if affinity_num_vals != cpus_num_vals { + let affinity_arg = app + .get_arguments() + .find(|arg| arg.get_name() == "affinity") + .unwrap(); + let cpus_arg = app + .get_arguments() + .find(|arg| arg.get_name() == "cpus") + .unwrap(); + let verb = if affinity_num_vals > 1 { "were" } else { "was" }; + let message = format!( + "The argument '{affinity_arg}' requires {cpus_num_vals} values (matching '{cpus_arg}'), but {affinity_num_vals} {verb} provided", + affinity_arg = affinity_arg, + cpus_num_vals = cpus_num_vals, + cpus_arg = cpus_arg, + affinity_num_vals = affinity_num_vals, + verb = verb, + ); + app.error(ErrorKind::WrongNumberOfValues, message).exit() + } else { + affinity.0 + } + }) + } +} + +#[derive(Parser, Debug, Default)] +struct NetworkArgs { + /// Guest IP address + #[clap(long, env = "HERMIT_IP")] + ip: Option, + + /// Guest gateway address + #[clap(long, env = "HERMIT_GATEWAY")] + gateway: Option, + + /// Guest network mask + #[clap(long, env = "HERMIT_MASK")] + mask: Option, + + /// Name of the network interface + #[clap(long, env = "HERMIT_NETIF")] + nic: Option, + + /// MAC address of the network interface + #[clap(long, env = "HERMIT_MAC")] + _mac: Option, +} + +fn run_uhyve() -> i32 { + #[cfg(feature = "instrument")] + setup_trace(); + + env_logger::init(); + + let mut app = Args::into_app(); + let Args { + verbose, + memory_args, + cpu_args, + gdb_port, + network_args: NetworkArgs { + ip, + gateway, + mask, + nic, + _mac, + }, + kernel, + kernel_args: _kernel_args, + } = Args::parse(); + let cpu_count = cpu_args.cpu_count; + let affinity = cpu_args.get_affinity(&mut app); + + let ip = ip.map(|ip| ip.to_string()); + let gateway = gateway.map(|ip| ip.to_string()); + let mask = mask.map(|ip| ip.to_string()); let params = vm::Parameter { - mem_size, - num_cpus, + mem_size: memory_args.memory_size.get(), + num_cpus: cpu_count.get(), verbose, - hugepage, - mergeable, - ip, - gateway, - mask, - nic, - gdbport, + hugepage: !memory_args.no_thp, + mergeable: memory_args.ksm, + ip: ip.as_deref(), + gateway: gateway.as_deref(), + mask: mask.as_deref(), + nic: nic.as_deref(), + gdbport: gdb_port, }; - let code = Uhyve::new(path, ¶ms) + Uhyve::new(kernel, ¶ms) .expect("Unable to create VM! Is the hypervisor interface (e.g. KVM) activated?") - .run(cpu_affinity); - std::process::exit(code); + .run(affinity) +} + +fn main() { + process::exit(run_uhyve()) } diff --git a/src/lib.rs b/src/lib.rs index d804b0bf..3173c8cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,6 @@ pub mod macos; pub use macos as os; #[cfg(target_os = "linux")] pub mod shared_queue; -pub mod utils; pub mod vm; pub use arch::*; diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 32777722..00000000 --- a/src/utils.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Utilities for the binary frontend. -//! -//! These functions are used to parse command line arguments or determining defaults. - -use std::{iter, num::ParseIntError}; - -use either::Either; - -/// Parses ranges from strings into discrete steps. -pub fn parse_ranges<'a>( - ranges: impl IntoIterator + 'a, -) -> impl Iterator> + 'a { - ranges - .into_iter() - .map(|range| { - let range = match range.split_once('-') { - Some((start, end)) => start.parse()?..=end.parse()?, - None => { - let idx = range.parse()?; - idx..=idx - } - }; - Ok(range) - }) - .flat_map(|range| match range { - Ok(range) => Either::Left(range.map(Ok)), - Err(err) => Either::Right(iter::once(Err(err))), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_cpu_affinity() { - assert_eq!( - parse_ranges(["8-10", "5", "3", "7-9"]) - .collect::, _>>() - .unwrap(), - [8, 9, 10, 5, 3, 7, 8, 9] - ); - - parse_ranges(["-1-2", "-5"]).for_each(|res| assert!(res.is_err())); - } -}