use std::collections::{BTreeMap, HashSet};
use std::ffi::{CStr, CString, OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::str::FromStr;
use clap::Parser;
use logger as c_log;
use merge::Merge;
use once_cell::sync::Lazy;
use schemars::{schema_for, JsonSchema};
use serde::{Deserialize, Serialize};
use shadow_shim_helper_rs::simulation_time::SimulationTime;
use crate::cshadow as c;
use crate::host::syscall::formatter::FmtOptions;
use crate::utility::units::{self, Unit};
const START_HELP_TEXT: &str = "\
Run real applications over simulated networks.\n\n\
For documentation, visit https://shadow.github.io/docs/guide";
const END_HELP_TEXT: &str = "\
If units are not specified, all values are assumed to be given in their base \
unit (seconds, bytes, bits, etc). Units can optionally be specified (for \
example: '1024 B', '1024 bytes', '1 KiB', '1 kibibyte', etc) and are \
case-sensitive.";
static VERSION: Lazy<String> = Lazy::new(crate::shadow::version);
#[derive(Debug, Clone, Parser)]
#[clap(name = "Shadow", about = START_HELP_TEXT, after_help = END_HELP_TEXT)]
#[clap(version = VERSION.as_str())]
#[clap(next_display_order = None)]
#[clap(hide_possible_values = true)]
pub struct CliOptions {
#[clap(required_unless_present_any(&["show_build_info", "shm_cleanup"]))]
pub config: Option<String>,
#[clap(long, short = 'g')]
pub gdb: bool,
#[clap(value_parser = parse_set_str)]
#[clap(long, value_name = "hostnames")]
pub debug_hosts: Option<HashSet<String>>,
#[clap(long, exclusive(true))]
pub shm_cleanup: bool,
#[clap(long, exclusive(true))]
pub show_build_info: bool,
#[clap(long)]
pub show_config: bool,
#[clap(flatten)]
pub general: GeneralOptions,
#[clap(flatten)]
pub network: NetworkOptions,
#[clap(flatten)]
pub host_option_defaults: HostDefaultOptions,
#[clap(flatten)]
pub experimental: ExperimentalOptions,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfigFileOptions {
pub general: GeneralOptions,
pub network: NetworkOptions,
#[serde(default)]
pub host_option_defaults: HostDefaultOptions,
#[serde(default)]
pub experimental: ExperimentalOptions,
pub hosts: BTreeMap<HostName, HostOptions>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfigOptions {
pub general: GeneralOptions,
pub network: NetworkOptions,
pub experimental: ExperimentalOptions,
pub hosts: BTreeMap<HostName, HostOptions>,
}
impl ConfigOptions {
pub fn new(mut config_file: ConfigFileOptions, options: CliOptions) -> Self {
config_file.host_option_defaults = config_file
.host_option_defaults
.with_defaults(HostDefaultOptions::new_with_defaults());
config_file.general = options.general.with_defaults(config_file.general);
config_file.network = options.network.with_defaults(config_file.network);
config_file.host_option_defaults = options
.host_option_defaults
.with_defaults(config_file.host_option_defaults);
config_file.experimental = options.experimental.with_defaults(config_file.experimental);
for host in config_file.hosts.values_mut() {
host.host_options = host
.host_options
.clone()
.with_defaults(config_file.host_option_defaults.clone());
}
Self {
general: config_file.general,
network: config_file.network,
experimental: config_file.experimental,
hosts: config_file.hosts,
}
}
pub fn model_unblocked_syscall_latency(&self) -> bool {
self.general.model_unblocked_syscall_latency.unwrap()
}
pub fn max_unapplied_cpu_latency(&self) -> SimulationTime {
let nanos = self.experimental.max_unapplied_cpu_latency.unwrap();
let nanos = nanos.convert(units::TimePrefix::Nano).unwrap().value();
SimulationTime::from_nanos(nanos)
}
pub fn unblocked_syscall_latency(&self) -> SimulationTime {
let nanos = self.experimental.unblocked_syscall_latency.unwrap();
let nanos = nanos.convert(units::TimePrefix::Nano).unwrap().value();
SimulationTime::from_nanos(nanos)
}
pub fn unblocked_vdso_latency(&self) -> SimulationTime {
let nanos = self.experimental.unblocked_vdso_latency.unwrap();
let nanos = nanos.convert(units::TimePrefix::Nano).unwrap().value();
SimulationTime::from_nanos(nanos)
}
pub fn strace_logging_mode(&self) -> Option<FmtOptions> {
match self.experimental.strace_logging_mode.as_ref().unwrap() {
StraceLoggingMode::Standard => Some(FmtOptions::Standard),
StraceLoggingMode::Deterministic => Some(FmtOptions::Deterministic),
StraceLoggingMode::Off => None,
}
}
}
static GENERAL_HELP: Lazy<std::collections::HashMap<String, String>> =
Lazy::new(|| generate_help_strs(schema_for!(GeneralOptions)));
#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
#[clap(next_help_heading = "General (Override configuration file options)")]
#[clap(next_display_order = None)]
#[serde(deny_unknown_fields)]
pub struct GeneralOptions {
#[clap(long, value_name = "seconds")]
#[clap(help = GENERAL_HELP.get("stop_time").unwrap().as_str())]
pub stop_time: Option<units::Time<units::TimePrefix>>,
#[clap(long, value_name = "N")]
#[clap(help = GENERAL_HELP.get("seed").unwrap().as_str())]
#[serde(default = "default_some_1")]
pub seed: Option<u32>,
#[clap(long, short = 'p', value_name = "cores")]
#[clap(help = GENERAL_HELP.get("parallelism").unwrap().as_str())]
#[serde(default = "default_some_0")]
pub parallelism: Option<u32>,
#[clap(long, value_name = "seconds")]
#[clap(help = GENERAL_HELP.get("bootstrap_end_time").unwrap().as_str())]
#[serde(default = "default_some_time_0")]
pub bootstrap_end_time: Option<units::Time<units::TimePrefix>>,
#[clap(long, short = 'l', value_name = "level")]
#[clap(help = GENERAL_HELP.get("log_level").unwrap().as_str())]
#[serde(default = "default_some_info")]
pub log_level: Option<LogLevel>,
#[clap(long, value_name = "seconds")]
#[clap(help = GENERAL_HELP.get("heartbeat_interval").unwrap().as_str())]
#[serde(default = "default_some_nullable_time_1")]
pub heartbeat_interval: Option<NullableOption<units::Time<units::TimePrefix>>>,
#[clap(long, short = 'd', value_name = "path")]
#[clap(help = GENERAL_HELP.get("data_directory").unwrap().as_str())]
#[serde(default = "default_data_directory")]
pub data_directory: Option<String>,
#[clap(long, short = 'e', value_name = "path")]
#[clap(help = GENERAL_HELP.get("template_directory").unwrap().as_str())]
#[serde(default)]
pub template_directory: Option<NullableOption<String>>,
#[clap(long, value_name = "bool")]
#[clap(help = GENERAL_HELP.get("progress").unwrap().as_str())]
#[serde(default = "default_some_false")]
pub progress: Option<bool>,
#[clap(long, value_name = "bool")]
#[clap(help = GENERAL_HELP.get("model_unblocked_syscall_latency").unwrap().as_str())]
#[serde(default = "default_some_false")]
pub model_unblocked_syscall_latency: Option<bool>,
}
impl GeneralOptions {
pub fn with_defaults(mut self, default: Self) -> Self {
self.merge(default);
self
}
}
static NETWORK_HELP: Lazy<std::collections::HashMap<String, String>> =
Lazy::new(|| generate_help_strs(schema_for!(NetworkOptions)));
#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
#[clap(next_help_heading = "Network (Override network options)")]
#[clap(next_display_order = None)]
#[serde(deny_unknown_fields)]
pub struct NetworkOptions {
#[clap(skip)]
pub graph: Option<GraphOptions>,
#[serde(default = "default_some_true")]
#[clap(long, value_name = "bool")]
#[clap(help = NETWORK_HELP.get("use_shortest_path").unwrap().as_str())]
pub use_shortest_path: Option<bool>,
}
impl NetworkOptions {
pub fn with_defaults(mut self, default: Self) -> Self {
self.merge(default);
self
}
}
static EXP_HELP: Lazy<std::collections::HashMap<String, String>> =
Lazy::new(|| generate_help_strs(schema_for!(ExperimentalOptions)));
#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
#[clap(
next_help_heading = "Experimental (Unstable and may change or be removed at any time, regardless of Shadow version)"
)]
#[clap(next_display_order = None)]
#[serde(default, deny_unknown_fields)]
pub struct ExperimentalOptions {
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_sched_fifo").unwrap().as_str())]
pub use_sched_fifo: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_syscall_counters").unwrap().as_str())]
pub use_syscall_counters: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_object_counters").unwrap().as_str())]
pub use_object_counters: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_preload_libc").unwrap().as_str())]
pub use_preload_libc: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_preload_openssl_rng").unwrap().as_str())]
pub use_preload_openssl_rng: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_preload_openssl_crypto").unwrap().as_str())]
pub use_preload_openssl_crypto: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_memory_manager").unwrap().as_str())]
pub use_memory_manager: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_cpu_pinning").unwrap().as_str())]
pub use_cpu_pinning: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_worker_spinning").unwrap().as_str())]
pub use_worker_spinning: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "seconds")]
#[clap(help = EXP_HELP.get("runahead").unwrap().as_str())]
pub runahead: Option<NullableOption<units::Time<units::TimePrefix>>>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_dynamic_runahead").unwrap().as_str())]
pub use_dynamic_runahead: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bytes")]
#[clap(help = EXP_HELP.get("socket_send_buffer").unwrap().as_str())]
pub socket_send_buffer: Option<units::Bytes<units::SiPrefixUpper>>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("socket_send_autotune").unwrap().as_str())]
pub socket_send_autotune: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bytes")]
#[clap(help = EXP_HELP.get("socket_recv_buffer").unwrap().as_str())]
pub socket_recv_buffer: Option<units::Bytes<units::SiPrefixUpper>>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("socket_recv_autotune").unwrap().as_str())]
pub socket_recv_autotune: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "mode")]
#[clap(help = EXP_HELP.get("interface_qdisc").unwrap().as_str())]
pub interface_qdisc: Option<QDiscMode>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "mode")]
#[clap(help = EXP_HELP.get("strace_logging_mode").unwrap().as_str())]
pub strace_logging_mode: Option<StraceLoggingMode>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "seconds")]
#[clap(help = EXP_HELP.get("max_unapplied_cpu_latency").unwrap().as_str())]
pub max_unapplied_cpu_latency: Option<units::Time<units::TimePrefix>>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "seconds")]
#[clap(help = EXP_HELP.get("unblocked_syscall_latency").unwrap().as_str())]
pub unblocked_syscall_latency: Option<units::Time<units::TimePrefix>>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "seconds")]
#[clap(help = EXP_HELP.get("unblocked_vdso_latency").unwrap().as_str())]
pub unblocked_vdso_latency: Option<units::Time<units::TimePrefix>>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "name")]
#[clap(help = EXP_HELP.get("scheduler").unwrap().as_str())]
pub scheduler: Option<Scheduler>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("report_errors_to_stderr").unwrap().as_str())]
pub report_errors_to_stderr: Option<bool>,
#[clap(hide_short_help = true)]
#[clap(long, value_name = "bool")]
#[clap(help = EXP_HELP.get("use_new_tcp").unwrap().as_str())]
pub use_new_tcp: Option<bool>,
}
impl ExperimentalOptions {
pub fn with_defaults(mut self, default: Self) -> Self {
self.merge(default);
self
}
}
impl Default for ExperimentalOptions {
fn default() -> Self {
Self {
use_sched_fifo: Some(false),
use_syscall_counters: Some(true),
use_object_counters: Some(true),
use_preload_libc: Some(true),
use_preload_openssl_rng: Some(true),
use_preload_openssl_crypto: Some(false),
max_unapplied_cpu_latency: Some(units::Time::new(1, units::TimePrefix::Micro)),
unblocked_syscall_latency: Some(units::Time::new(1, units::TimePrefix::Micro)),
unblocked_vdso_latency: Some(units::Time::new(10, units::TimePrefix::Nano)),
use_memory_manager: Some(false),
use_cpu_pinning: Some(true),
use_worker_spinning: Some(true),
runahead: Some(NullableOption::Value(units::Time::new(
1,
units::TimePrefix::Milli,
))),
use_dynamic_runahead: Some(false),
socket_send_buffer: Some(units::Bytes::new(131_072, units::SiPrefixUpper::Base)),
socket_send_autotune: Some(true),
socket_recv_buffer: Some(units::Bytes::new(174_760, units::SiPrefixUpper::Base)),
socket_recv_autotune: Some(true),
interface_qdisc: Some(QDiscMode::Fifo),
strace_logging_mode: Some(StraceLoggingMode::Off),
scheduler: Some(Scheduler::ThreadPerCore),
report_errors_to_stderr: Some(true),
use_new_tcp: Some(false),
}
}
}
static HOST_HELP: Lazy<std::collections::HashMap<String, String>> =
Lazy::new(|| generate_help_strs(schema_for!(HostDefaultOptions)));
#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
#[clap(next_help_heading = "Host Defaults (Default options for hosts)")]
#[clap(next_display_order = None)]
#[serde(default, deny_unknown_fields)]
#[schemars(default = "HostDefaultOptions::new_with_defaults")]
pub struct HostDefaultOptions {
#[clap(long = "host-log-level", name = "host-log-level")]
#[clap(value_name = "level")]
#[clap(help = HOST_HELP.get("log_level").unwrap().as_str())]
pub log_level: Option<NullableOption<LogLevel>>,
#[clap(long, value_name = "bool")]
#[clap(help = HOST_HELP.get("pcap_enabled").unwrap().as_str())]
pub pcap_enabled: Option<bool>,
#[clap(long, value_name = "bytes")]
#[clap(help = HOST_HELP.get("pcap_capture_size").unwrap().as_str())]
pub pcap_capture_size: Option<units::Bytes<units::SiPrefixUpper>>,
}
impl HostDefaultOptions {
pub fn new_with_defaults() -> Self {
Self {
log_level: None,
pcap_enabled: Some(false),
pcap_capture_size: Some(units::Bytes::new(65535, units::SiPrefixUpper::Base)),
}
}
pub fn with_defaults(mut self, default: Self) -> Self {
self.merge(default);
self
}
}
#[allow(clippy::derivable_impls)]
impl Default for HostDefaultOptions {
fn default() -> Self {
Self {
log_level: None,
pcap_enabled: None,
pcap_capture_size: None,
}
}
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Copy, Clone, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum RunningVal {
Running,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum ProcessFinalState {
Exited { exited: i32 },
Signaled { signaled: Signal },
Running(RunningVal),
}
impl Default for ProcessFinalState {
fn default() -> Self {
Self::Exited { exited: 0 }
}
}
impl std::fmt::Display for ProcessFinalState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = serde_yaml::to_string(self).or(Err(std::fmt::Error))?;
write!(f, "{}", s.trim())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ProcessOptions {
pub path: std::path::PathBuf,
#[serde(default = "default_args_empty")]
pub args: ProcessArgs,
#[serde(default)]
pub environment: BTreeMap<EnvName, String>,
#[serde(default)]
pub start_time: units::Time<units::TimePrefix>,
#[serde(default)]
pub shutdown_time: Option<units::Time<units::TimePrefix>>,
#[serde(default = "default_sigterm")]
pub shutdown_signal: Signal,
#[serde(default)]
pub expected_final_state: ProcessFinalState,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HostOptions {
pub network_node_id: u32,
pub processes: Vec<ProcessOptions>,
#[serde(default)]
pub ip_addr: Option<std::net::Ipv4Addr>,
#[serde(default)]
pub bandwidth_down: Option<units::BitsPerSec<units::SiPrefixUpper>>,
#[serde(default)]
pub bandwidth_up: Option<units::BitsPerSec<units::SiPrefixUpper>>,
#[serde(default)]
pub host_options: HostDefaultOptions,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum LogLevel {
Error,
Warning,
Info,
Debug,
Trace,
}
impl FromStr for LogLevel {
type Err = serde_yaml::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_yaml::from_str(s)
}
}
impl LogLevel {
pub fn to_c_loglevel(&self) -> c_log::LogLevel {
match self {
Self::Error => c_log::_LogLevel_LOGLEVEL_ERROR,
Self::Warning => c_log::_LogLevel_LOGLEVEL_WARNING,
Self::Info => c_log::_LogLevel_LOGLEVEL_INFO,
Self::Debug => c_log::_LogLevel_LOGLEVEL_DEBUG,
Self::Trace => c_log::_LogLevel_LOGLEVEL_TRACE,
}
}
}
impl From<LogLevel> for log::Level {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Error => log::Level::Error,
LogLevel::Warning => log::Level::Warn,
LogLevel::Info => log::Level::Info,
LogLevel::Debug => log::Level::Debug,
LogLevel::Trace => log::Level::Trace,
}
}
}
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, JsonSchema)]
pub struct HostName(String);
impl<'de> serde::Deserialize<'de> for HostName {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct HostNameVisitor;
impl<'de> serde::de::Visitor<'de> for HostNameVisitor {
type Value = HostName;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string")
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
fn is_allowed(c: char) -> bool {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.'
}
if let Some(invalid_char) = v.chars().find(|x| !is_allowed(*x)) {
return Err(E::custom(format!(
"invalid hostname character: '{invalid_char}'"
)));
}
if v.is_empty() {
return Err(E::custom("empty hostname"));
}
if v.starts_with('-') {
return Err(E::custom("hostname begins with a '-' character"));
}
if v.len() > 253 {
return Err(E::custom("hostname exceeds 253 characters"));
}
Ok(HostName(v))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_string(v.to_string())
}
}
deserializer.deserialize_string(HostNameVisitor)
}
}
impl std::ops::Deref for HostName {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<HostName> for String {
fn from(name: HostName) -> Self {
name.0
}
}
impl std::fmt::Display for HostName {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, JsonSchema)]
pub struct EnvName(String);
impl EnvName {
pub fn new(name: impl Into<String>) -> Option<Self> {
let name = name.into();
if name.contains('=') {
return None;
}
Some(Self(name))
}
}
impl<'de> serde::Deserialize<'de> for EnvName {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct EnvNameVisitor;
impl<'de> serde::de::Visitor<'de> for EnvNameVisitor {
type Value = EnvName;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string")
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let Some(name) = EnvName::new(v) else {
let e = "environment variable name contains a '=' character";
return Err(E::custom(e));
};
Ok(name)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_string(v.to_string())
}
}
deserializer.deserialize_string(EnvNameVisitor)
}
}
impl std::ops::Deref for EnvName {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<EnvName> for String {
fn from(name: EnvName) -> Self {
name.0
}
}
impl std::fmt::Display for EnvName {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Scheduler {
ThreadPerHost,
ThreadPerCore,
}
impl FromStr for Scheduler {
type Err = serde_yaml::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_yaml::from_str(s)
}
}
fn default_data_directory() -> Option<String> {
Some("shadow.data".into())
}
fn parse_set<T>(s: &str) -> Result<HashSet<T>, <T as FromStr>::Err>
where
T: std::cmp::Eq + std::hash::Hash + FromStr,
{
s.split(',').map(|x| x.trim().parse()).collect()
}
fn parse_set_str(s: &str) -> Result<HashSet<String>, <String as FromStr>::Err> {
parse_set(s)
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
#[repr(C)]
pub enum QDiscMode {
Fifo,
RoundRobin,
}
impl FromStr for QDiscMode {
type Err = serde_yaml::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_yaml::from_str(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum Compression {
Xz,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FileSource {
pub path: String,
pub compression: Option<Compression>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum GraphSource {
File(FileSource),
Inline(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GraphOptions {
Gml(GraphSource),
#[serde(rename = "1_gbit_switch")]
OneGbitSwitch,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum ProcessArgs {
List(Vec<String>),
Str(String),
}
impl<'de> serde::Deserialize<'de> for ProcessArgs {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct ProcessArgsVisitor;
impl<'de> serde::de::Visitor<'de> for ProcessArgsVisitor {
type Value = ProcessArgs;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or a sequence of strings")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Self::Value::Str(v.to_owned()))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut v = vec![];
while let Some(val) = seq.next_element()? {
v.push(val);
}
Ok(Self::Value::List(v))
}
}
deserializer.deserialize_any(ProcessArgsVisitor)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Signal(nix::sys::signal::Signal);
impl From<nix::sys::signal::Signal> for Signal {
fn from(value: nix::sys::signal::Signal) -> Self {
Self(value)
}
}
impl TryFrom<linux_api::signal::Signal> for Signal {
type Error = <nix::sys::signal::Signal as TryFrom<i32>>::Error;
fn try_from(value: linux_api::signal::Signal) -> Result<Self, Self::Error> {
let signal = nix::sys::signal::Signal::try_from(value.as_i32())?;
Ok(Self(signal))
}
}
impl serde::Serialize for Signal {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.as_str())
}
}
impl<'de> serde::Deserialize<'de> for Signal {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct SignalVisitor;
impl<'de> serde::de::Visitor<'de> for SignalVisitor {
type Value = Signal;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a signal string (e.g. \"SIGINT\") or integer")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
nix::sys::signal::Signal::from_str(v)
.map(Signal)
.map_err(|_e| E::custom(format!("Invalid signal string: {v}")))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let v = i32::try_from(v)
.map_err(|_e| E::custom(format!("Invalid signal number: {v}")))?;
nix::sys::signal::Signal::try_from(v)
.map(Signal)
.map_err(|_e| E::custom(format!("Invalid signal number: {v}")))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let v = i64::try_from(v)
.map_err(|_e| E::custom(format!("Invalid signal number: {v}")))?;
self.visit_i64(v)
}
}
deserializer.deserialize_any(SignalVisitor)
}
}
impl std::fmt::Display for Signal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl JsonSchema for Signal {
fn schema_name() -> String {
String::from("Signal")
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::Schema::Bool(true)
}
}
impl std::ops::Deref for Signal {
type Target = nix::sys::signal::Signal;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum StraceLoggingMode {
Off,
Standard,
Deterministic,
}
impl FromStr for StraceLoggingMode {
type Err = serde_yaml::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_yaml::from_str(s)
}
}
#[derive(Debug, Copy, Clone, JsonSchema, Eq, PartialEq)]
pub enum NullableOption<T> {
Value(T),
Null,
}
impl<T> NullableOption<T> {
pub fn as_ref(&self) -> NullableOption<&T> {
match self {
NullableOption::Value(ref x) => NullableOption::Value(x),
NullableOption::Null => NullableOption::Null,
}
}
pub fn as_mut(&mut self) -> NullableOption<&mut T> {
match self {
NullableOption::Value(ref mut x) => NullableOption::Value(x),
NullableOption::Null => NullableOption::Null,
}
}
pub fn to_option(self) -> Option<T> {
match self {
NullableOption::Value(x) => Some(x),
NullableOption::Null => None,
}
}
}
impl<T: serde::Serialize> serde::Serialize for NullableOption<T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Value(x) => Ok(T::serialize(x, serializer)?),
Self::Null => serializer.serialize_none(),
}
}
}
impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for NullableOption<T> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::Value(T::deserialize(deserializer)?))
}
}
impl<T> FromStr for NullableOption<T>
where
T: FromStr<Err: std::fmt::Debug + std::fmt::Display>,
{
type Err = T::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"null" => Ok(Self::Null),
x => Ok(Self::Value(FromStr::from_str(x)?)),
}
}
}
pub trait Flatten<T> {
fn flatten(self) -> Option<T>;
fn flatten_ref(&self) -> Option<&T>;
}
impl<T> Flatten<T> for Option<NullableOption<T>> {
fn flatten(self) -> Option<T> {
self.and_then(|x| x.to_option())
}
fn flatten_ref(&self) -> Option<&T> {
self.as_ref().and_then(|x| x.as_ref().to_option())
}
}
fn default_args_empty() -> ProcessArgs {
ProcessArgs::Str("".to_string())
}
fn default_sigterm() -> Signal {
Signal(nix::sys::signal::Signal::SIGTERM)
}
fn default_some_time_0() -> Option<units::Time<units::TimePrefix>> {
Some(units::Time::new(0, units::TimePrefix::Sec))
}
fn default_some_true() -> Option<bool> {
Some(true)
}
fn default_some_false() -> Option<bool> {
Some(false)
}
fn default_some_0() -> Option<u32> {
Some(0)
}
fn default_some_1() -> Option<u32> {
Some(1)
}
fn default_some_nullable_time_1() -> Option<NullableOption<units::Time<units::TimePrefix>>> {
let time = units::Time::new(1, units::TimePrefix::Sec);
Some(NullableOption::Value(time))
}
fn default_some_info() -> Option<LogLevel> {
Some(LogLevel::Info)
}
pub const ONE_GBIT_SWITCH_GRAPH: &str = r#"graph [
directed 0
node [
id 0
host_bandwidth_up "1 Gbit"
host_bandwidth_down "1 Gbit"
]
edge [
source 0
target 0
latency "1 ms"
packet_loss 0.0
]
]"#;
fn generate_help_strs(
schema: schemars::schema::RootSchema,
) -> std::collections::HashMap<String, String> {
let mut defaults = std::collections::HashMap::<String, String>::new();
for (name, obj) in &schema.schema.object.as_ref().unwrap().properties {
if let Some(meta) = obj.clone().into_object().metadata {
let description = meta.description.unwrap_or_default();
let space = if !description.is_empty() { " " } else { "" };
match meta.default {
Some(default) => defaults.insert(
name.clone(),
format!("{}{}[default: {}]", description, space, default),
),
None => defaults.insert(name.clone(), description.to_string()),
};
}
}
defaults
}
pub fn parse_string_as_args(args_str: &OsStr) -> Result<Vec<OsString>, String> {
if args_str.is_empty() {
return Ok(Vec::new());
}
let args_str = CString::new(args_str.as_bytes()).unwrap();
let mut argc: libc::c_int = 0;
let mut argv: *mut *mut libc::c_char = std::ptr::null_mut();
let mut error: *mut libc::c_char = std::ptr::null_mut();
let rv = unsafe { c::process_parseArgStr(args_str.as_ptr(), &mut argc, &mut argv, &mut error) };
if !rv {
let error_message = match error.is_null() {
false => unsafe { CStr::from_ptr(error) }.to_str().unwrap(),
true => "Unknown parsing error",
}
.to_string();
unsafe { c::process_parseArgStrFree(argv, error) };
return Err(error_message);
}
assert!(!argv.is_null());
let args: Vec<_> = (0..argc)
.map(|x| unsafe {
let arg_ptr = *argv.add(x as usize);
assert!(!arg_ptr.is_null());
OsStr::from_bytes(CStr::from_ptr(arg_ptr).to_bytes()).to_os_string()
})
.collect();
unsafe { c::process_parseArgStrFree(argv, error) };
Ok(args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg_attr(miri, ignore)]
fn test_parse_args() {
let arg_str = r#"the quick brown fox "jumped over" the "\"lazy\" dog""#;
let expected_args = &[
"the",
"quick",
"brown",
"fox",
"jumped over",
"the",
"\"lazy\" dog",
];
let arg_str: OsString = arg_str.into();
let args = parse_string_as_args(&arg_str).unwrap();
assert_eq!(args, expected_args);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_parse_args_empty() {
let arg_str = "";
let expected_args: &[&str] = &[];
let arg_str: OsString = arg_str.into();
let args = parse_string_as_args(&arg_str).unwrap();
assert_eq!(args, expected_args);
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_parse_args_error() {
let arg_str = r#"hello "world"#;
let arg_str: OsString = arg_str.into();
let err_str = parse_string_as_args(&arg_str).unwrap_err();
assert!(!err_str.is_empty());
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_nullable_option() {
let yaml_fmt_fn = |option| {
format!(
r#"
general:
stop_time: 1 min
{}
network:
graph:
type: 1_gbit_switch
hosts:
myhost:
network_node_id: 0
processes:
- path: /bin/true
"#,
option
)
};
let time_1_sec = units::Time::new(1, units::TimePrefix::Sec);
let time_5_sec = units::Time::new(5, units::TimePrefix::Sec);
let yaml = yaml_fmt_fn("heartbeat_interval: null");
let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
let merged = ConfigOptions::new(config_file, cli);
assert_eq!(merged.general.heartbeat_interval, None);
let yaml = yaml_fmt_fn("heartbeat_interval: null");
let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
let cli: CliOptions =
CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
let merged = ConfigOptions::new(config_file, cli);
assert_eq!(
merged.general.heartbeat_interval,
Some(NullableOption::Value(time_5_sec))
);
let yaml = yaml_fmt_fn("heartbeat_interval: null");
let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
let cli: CliOptions =
CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
let merged = ConfigOptions::new(config_file, cli);
assert_eq!(
merged.general.heartbeat_interval,
Some(NullableOption::Null)
);
let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
let merged = ConfigOptions::new(config_file, cli);
assert_eq!(
merged.general.heartbeat_interval,
Some(NullableOption::Value(time_5_sec))
);
let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
let cli: CliOptions =
CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
let merged = ConfigOptions::new(config_file, cli);
assert_eq!(
merged.general.heartbeat_interval,
Some(NullableOption::Value(time_5_sec))
);
let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
let cli: CliOptions =
CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
let merged = ConfigOptions::new(config_file, cli);
assert_eq!(
merged.general.heartbeat_interval,
Some(NullableOption::Null)
);
let yaml = yaml_fmt_fn("");
let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
let merged = ConfigOptions::new(config_file, cli);
assert_eq!(
merged.general.heartbeat_interval,
Some(NullableOption::Value(time_1_sec))
);
let yaml = yaml_fmt_fn("");
let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
let cli: CliOptions =
CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
let merged = ConfigOptions::new(config_file, cli);
assert_eq!(
merged.general.heartbeat_interval,
Some(NullableOption::Value(time_5_sec))
);
let yaml = yaml_fmt_fn("");
let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
let cli: CliOptions =
CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
let merged = ConfigOptions::new(config_file, cli);
assert_eq!(
merged.general.heartbeat_interval,
Some(NullableOption::Null)
);
}
}