Skip to main content

shadow_rs/core/
configuration.rs

1//! Shadow's configuration and cli parsing code using [serde] and [clap]. This contains all of
2//! Shadow's configuration options, some of which are also exposed as CLI options.
3//!
4//! Shadow uses [schemars] to get the option description (its doc comment) and default value so that
5//! it can be shown in the CLI help text.
6//!
7//! This code should be careful about validating or interpreting values. It should be focused on
8//! parsing and checking that the format is correct, and not validating the values. For example for
9//! options that take paths, this code should not verify that the path actually exists or perform
10//! any path canonicalization. That should be left to other code outside of this module. This is so
11//! that the configuration parsing does not become environment-dependent. If a configuration file
12//! parses on one system, it should parse successfully on other systems as well.
13
14use std::borrow::Cow;
15use std::collections::{BTreeMap, HashSet};
16use std::ffi::{CStr, CString, OsStr, OsString};
17use std::os::unix::ffi::OsStrExt;
18use std::str::FromStr;
19
20use clap::Parser;
21use logger as c_log;
22use merge::Merge;
23use once_cell::sync::Lazy;
24use schemars::{JsonSchema, schema_for};
25use serde::{Deserialize, Serialize};
26use shadow_shim_helper_rs::simulation_time::SimulationTime;
27
28use crate::cshadow as c;
29use crate::host::syscall::formatter::FmtOptions;
30use crate::utility::units::{self, Unit};
31
32const START_HELP_TEXT: &str = "\
33    Run real applications over simulated networks.\n\n\
34    For documentation, visit https://shadow.github.io/docs/guide";
35
36const END_HELP_TEXT: &str = "\
37    If units are not specified, all values are assumed to be given in their base \
38    unit (seconds, bytes, bits, etc). Units can optionally be specified (for \
39    example: '1024 B', '1024 bytes', '1 KiB', '1 kibibyte', etc) and are \
40    case-sensitive.";
41
42// clap requires a 'static str for the version
43static VERSION: Lazy<String> = Lazy::new(crate::shadow::version);
44
45#[derive(Debug, Clone, Parser)]
46#[clap(name = "Shadow", about = START_HELP_TEXT, after_help = END_HELP_TEXT)]
47#[clap(version = VERSION.as_str())]
48#[clap(next_display_order = None)]
49// clap only shows the possible values for bool options (unless we add support for the other
50// non-bool options in the future), which isn't very helpful
51#[clap(hide_possible_values = true)]
52pub struct CliOptions {
53    /// Path to the Shadow configuration file. Use '-' to read from stdin
54    #[clap(required_unless_present_any(&["show_build_info", "shm_cleanup"]))]
55    pub config: Option<String>,
56
57    /// Pause to allow gdb to attach
58    #[clap(long, short = 'g')]
59    pub gdb: bool,
60
61    /// Pause after starting any processes on the comma-delimited list of hostnames
62    #[clap(value_parser = parse_set_str)]
63    #[clap(long, value_name = "hostnames")]
64    pub debug_hosts: Option<HashSet<String>>,
65
66    /// Exit after running shared memory cleanup routine
67    #[clap(long, exclusive(true))]
68    pub shm_cleanup: bool,
69
70    /// Exit after printing build information
71    #[clap(long, exclusive(true))]
72    pub show_build_info: bool,
73
74    /// Exit after printing the final configuration
75    #[clap(long)]
76    pub show_config: bool,
77
78    #[clap(flatten)]
79    pub general: GeneralOptions,
80
81    #[clap(flatten)]
82    pub network: NetworkOptions,
83
84    #[clap(flatten)]
85    pub host_option_defaults: HostDefaultOptions,
86
87    #[clap(flatten)]
88    pub experimental: ExperimentalOptions,
89}
90
91/// Options contained in a configuration file.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct ConfigFileOptions {
95    pub general: GeneralOptions,
96
97    pub network: NetworkOptions,
98
99    #[serde(default)]
100    pub host_option_defaults: HostDefaultOptions,
101
102    #[serde(default)]
103    pub experimental: ExperimentalOptions,
104
105    // we use a BTreeMap so that the hosts are sorted by their hostname (useful for determinism)
106    // since shadow parses to a serde_yaml::Value initially, we don't need to worry about duplicate
107    // hostnames here
108    pub hosts: BTreeMap<HostName, HostOptions>,
109}
110
111/// Shadow configuration options after processing command-line and configuration file options.
112#[derive(Debug, Clone, Serialize)]
113pub struct ConfigOptions {
114    pub general: GeneralOptions,
115
116    pub network: NetworkOptions,
117
118    pub experimental: ExperimentalOptions,
119
120    // we use a BTreeMap so that the hosts are sorted by their hostname (useful for determinism)
121    pub hosts: BTreeMap<HostName, HostOptions>,
122}
123
124impl ConfigOptions {
125    pub fn new(mut config_file: ConfigFileOptions, options: CliOptions) -> Self {
126        // the `HostDefaultOptions::default` contains only `None` values, so we must first merge the
127        // config file with the real defaults from `HostDefaultOptions::new_with_defaults`
128        config_file.host_option_defaults = config_file
129            .host_option_defaults
130            .with_defaults(HostDefaultOptions::new_with_defaults());
131
132        // override config options with command line options
133        config_file.general = options.general.with_defaults(config_file.general);
134        config_file.network = options.network.with_defaults(config_file.network);
135        config_file.host_option_defaults = options
136            .host_option_defaults
137            .with_defaults(config_file.host_option_defaults);
138        config_file.experimental = options.experimental.with_defaults(config_file.experimental);
139
140        // copy the host defaults to all of the hosts
141        for host in config_file.hosts.values_mut() {
142            host.host_options = host
143                .host_options
144                .clone()
145                .with_defaults(config_file.host_option_defaults.clone());
146        }
147
148        Self {
149            general: config_file.general,
150            network: config_file.network,
151            experimental: config_file.experimental,
152            hosts: config_file.hosts,
153        }
154    }
155
156    pub fn model_unblocked_syscall_latency(&self) -> bool {
157        self.general.model_unblocked_syscall_latency.unwrap()
158    }
159
160    pub fn max_unapplied_cpu_latency(&self) -> SimulationTime {
161        let nanos = self.experimental.max_unapplied_cpu_latency.unwrap();
162        let nanos = nanos.convert(units::TimePrefix::Nano).unwrap().value();
163        SimulationTime::from_nanos(nanos)
164    }
165
166    pub fn unblocked_syscall_latency(&self) -> SimulationTime {
167        let nanos = self.experimental.unblocked_syscall_latency.unwrap();
168        let nanos = nanos.convert(units::TimePrefix::Nano).unwrap().value();
169        SimulationTime::from_nanos(nanos)
170    }
171
172    pub fn unblocked_vdso_latency(&self) -> SimulationTime {
173        let nanos = self.experimental.unblocked_vdso_latency.unwrap();
174        let nanos = nanos.convert(units::TimePrefix::Nano).unwrap().value();
175        SimulationTime::from_nanos(nanos)
176    }
177
178    pub fn native_preemption_enabled(&self) -> bool {
179        self.experimental.native_preemption_enabled.unwrap()
180    }
181
182    pub fn native_preemption_native_interval(
183        &self,
184    ) -> anyhow::Result<linux_api::time::kernel_old_timeval> {
185        let t = self.experimental.native_preemption_native_interval.unwrap();
186        let t = core::time::Duration::from(t);
187        // TODO: Would be a little nicer to surface this error when we parse the
188        // config. I think ideally we'd update the type such that some bounds
189        // can be enforced at parse time.
190        if t < core::time::Duration::from_micros(1) {
191            return Err(anyhow::anyhow!(
192                "native_preemption_native_interval must be >= 1 microsecond. Got {t:?}."
193            ));
194        }
195        let rv = linux_api::time::kernel_old_timeval {
196            tv_sec: t.as_secs().try_into().unwrap(),
197            tv_usec: t.subsec_micros().into(),
198        };
199        assert!(!(rv.tv_sec == 0 && rv.tv_usec == 0));
200        Ok(rv)
201    }
202
203    pub fn native_preemption_sim_interval(&self) -> SimulationTime {
204        let t = self.experimental.native_preemption_sim_interval.unwrap();
205        let nanos = t.convert(units::TimePrefix::Nano).unwrap().value();
206        SimulationTime::from_nanos(nanos)
207    }
208
209    pub fn strace_logging_mode(&self) -> Option<FmtOptions> {
210        match self.experimental.strace_logging_mode.as_ref().unwrap() {
211            StraceLoggingMode::Standard => Some(FmtOptions::Standard),
212            StraceLoggingMode::Deterministic => Some(FmtOptions::Deterministic),
213            StraceLoggingMode::Long => Some(FmtOptions::Long),
214            StraceLoggingMode::Off => None,
215        }
216    }
217}
218
219/// Help messages used by Clap for command line arguments, combining the doc string with
220/// the Serde default.
221static GENERAL_HELP: Lazy<std::collections::HashMap<String, String>> =
222    Lazy::new(|| generate_help_strs(schema_for!(GeneralOptions)));
223
224// these must all be Option types since they aren't required by the CLI, even if they're
225// required in the configuration file
226#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
227#[clap(next_help_heading = "General (Override configuration file options)")]
228#[clap(next_display_order = None)]
229#[serde(deny_unknown_fields)]
230#[merge(strategy = merge::option::overwrite_none)]
231pub struct GeneralOptions {
232    /// The simulated time at which simulated processes are sent a SIGKILL signal
233    #[clap(long, value_name = "seconds")]
234    #[clap(help = GENERAL_HELP.get("stop_time").unwrap().as_str())]
235    pub stop_time: Option<units::Time<units::TimePrefix>>,
236
237    /// Initialize randomness using seed N
238    #[clap(long, value_name = "N")]
239    #[clap(help = GENERAL_HELP.get("seed").unwrap().as_str())]
240    #[serde(default = "default_some_1")]
241    pub seed: Option<u32>,
242
243    /// How many parallel threads to use to run the simulation. A value of 0 will allow Shadow to
244    /// choose the number of threads.
245    #[clap(long, short = 'p', value_name = "cores")]
246    #[clap(help = GENERAL_HELP.get("parallelism").unwrap().as_str())]
247    #[serde(default = "default_some_0")]
248    pub parallelism: Option<u32>,
249
250    /// The simulated time that ends Shadow's high network bandwidth/reliability bootstrap period
251    #[clap(long, value_name = "seconds")]
252    #[clap(help = GENERAL_HELP.get("bootstrap_end_time").unwrap().as_str())]
253    #[serde(default = "default_some_time_0")]
254    pub bootstrap_end_time: Option<units::Time<units::TimePrefix>>,
255
256    /// Log level of output written on stdout. If Shadow was built in release mode, then log
257    /// messages at level 'trace' will always be dropped
258    #[clap(long, short = 'l', value_name = "level")]
259    #[clap(help = GENERAL_HELP.get("log_level").unwrap().as_str())]
260    #[serde(default = "default_some_info")]
261    pub log_level: Option<LogLevel>,
262
263    /// Interval at which to print heartbeat messages
264    #[clap(long, value_name = "seconds")]
265    #[clap(help = GENERAL_HELP.get("heartbeat_interval").unwrap().as_str())]
266    #[serde(default = "default_some_nullable_time_1")]
267    pub heartbeat_interval: Option<NullableOption<units::Time<units::TimePrefix>>>,
268
269    /// Path to store simulation output
270    #[clap(long, short = 'd', value_name = "path")]
271    #[clap(help = GENERAL_HELP.get("data_directory").unwrap().as_str())]
272    #[serde(default = "default_data_directory")]
273    pub data_directory: Option<String>,
274
275    /// Path to recursively copy during startup and use as the data-directory
276    #[clap(long, short = 'e', value_name = "path")]
277    #[clap(help = GENERAL_HELP.get("template_directory").unwrap().as_str())]
278    #[serde(default)]
279    pub template_directory: Option<NullableOption<String>>,
280
281    /// Show the simulation progress on stderr
282    #[clap(long, value_name = "bool")]
283    #[clap(help = GENERAL_HELP.get("progress").unwrap().as_str())]
284    #[serde(default = "default_some_false")]
285    pub progress: Option<bool>,
286
287    /// Model syscalls and VDSO functions that don't block as having some
288    /// latency. This should have minimal effect on typical simulations, but
289    /// can be helpful for programs with "busy loops" that otherwise deadlock
290    /// under Shadow.
291    #[clap(long, value_name = "bool")]
292    #[clap(help = GENERAL_HELP.get("model_unblocked_syscall_latency").unwrap().as_str())]
293    #[serde(default = "default_some_false")]
294    pub model_unblocked_syscall_latency: Option<bool>,
295}
296
297impl GeneralOptions {
298    /// Replace unset (`None`) values of `base` with values from `default`.
299    pub fn with_defaults(mut self, default: Self) -> Self {
300        self.merge(default);
301        self
302    }
303}
304
305/// Help messages used by Clap for command line arguments, combining the doc string with
306/// the Serde default.
307static NETWORK_HELP: Lazy<std::collections::HashMap<String, String>> =
308    Lazy::new(|| generate_help_strs(schema_for!(NetworkOptions)));
309
310// these must all be Option types since they aren't required by the CLI, even if they're
311// required in the configuration file
312#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
313#[clap(next_help_heading = "Network (Override network options)")]
314#[clap(next_display_order = None)]
315#[serde(deny_unknown_fields)]
316#[merge(strategy = merge::option::overwrite_none)]
317pub struct NetworkOptions {
318    /// The network topology graph
319    #[clap(skip)]
320    pub graph: Option<GraphOptions>,
321
322    /// When routing packets, follow the shortest path rather than following a direct
323    /// edge between nodes. If false, the network graph is required to be complete.
324    #[serde(default = "default_some_true")]
325    #[clap(long, value_name = "bool")]
326    #[clap(help = NETWORK_HELP.get("use_shortest_path").unwrap().as_str())]
327    pub use_shortest_path: Option<bool>,
328}
329
330impl NetworkOptions {
331    /// Replace unset (`None`) values of `base` with values from `default`.
332    pub fn with_defaults(mut self, default: Self) -> Self {
333        self.merge(default);
334        self
335    }
336}
337
338/// Help messages used by Clap for command line arguments, combining the doc string with
339/// the Serde default.
340static EXP_HELP: Lazy<std::collections::HashMap<String, String>> =
341    Lazy::new(|| generate_help_strs(schema_for!(ExperimentalOptions)));
342
343#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
344#[clap(
345    next_help_heading = "Experimental (Unstable and may change or be removed at any time, regardless of Shadow version)"
346)]
347#[clap(next_display_order = None)]
348#[serde(default, deny_unknown_fields)]
349#[merge(strategy = merge::option::overwrite_none)]
350pub struct ExperimentalOptions {
351    /// Use the SCHED_FIFO scheduler. Requires CAP_SYS_NICE. See sched(7), capabilities(7)
352    #[clap(hide_short_help = true)]
353    #[clap(long, value_name = "bool")]
354    #[clap(help = EXP_HELP.get("use_sched_fifo").unwrap().as_str())]
355    pub use_sched_fifo: Option<bool>,
356
357    /// Count the number of occurrences for individual syscalls
358    #[clap(hide_short_help = true)]
359    #[clap(long, value_name = "bool")]
360    #[clap(help = EXP_HELP.get("use_syscall_counters").unwrap().as_str())]
361    pub use_syscall_counters: Option<bool>,
362
363    /// Count object allocations and deallocations. If disabled, we will not be able to detect object memory leaks
364    #[clap(hide_short_help = true)]
365    #[clap(long, value_name = "bool")]
366    #[clap(help = EXP_HELP.get("use_object_counters").unwrap().as_str())]
367    pub use_object_counters: Option<bool>,
368
369    /// Preload our libc library for all managed processes for fast syscall interposition when possible.
370    #[clap(hide_short_help = true)]
371    #[clap(long, value_name = "bool")]
372    #[clap(help = EXP_HELP.get("use_preload_libc").unwrap().as_str())]
373    pub use_preload_libc: Option<bool>,
374
375    /// Preload our OpenSSL RNG library for all managed processes to mitigate non-deterministic use of OpenSSL.
376    #[clap(hide_short_help = true)]
377    #[clap(long, value_name = "bool")]
378    #[clap(help = EXP_HELP.get("use_preload_openssl_rng").unwrap().as_str())]
379    pub use_preload_openssl_rng: Option<bool>,
380
381    /// Preload our OpenSSL crypto library for all managed processes to skip some crypto operations
382    /// (may speed up simulation if your CPU lacks AES-NI support, but can cause bugs so do not use
383    /// unless you know what you're doing).
384    #[clap(hide_short_help = true)]
385    #[clap(long, value_name = "bool")]
386    #[clap(help = EXP_HELP.get("use_preload_openssl_crypto").unwrap().as_str())]
387    pub use_preload_openssl_crypto: Option<bool>,
388
389    /// Use the MemoryManager in memory-mapping mode. This can improve
390    /// performance, but disables support for dynamically spawning processes
391    /// inside the simulation (e.g. the `fork` syscall).
392    #[clap(hide_short_help = true)]
393    #[clap(long, value_name = "bool")]
394    #[clap(help = EXP_HELP.get("use_memory_manager").unwrap().as_str())]
395    pub use_memory_manager: Option<bool>,
396
397    /// Pin each thread and any processes it executes to the same logical CPU Core to improve cache affinity
398    #[clap(hide_short_help = true)]
399    #[clap(long, value_name = "bool")]
400    #[clap(help = EXP_HELP.get("use_cpu_pinning").unwrap().as_str())]
401    pub use_cpu_pinning: Option<bool>,
402
403    /// Each worker thread will spin in a `sched_yield` loop while waiting for a new task. This is
404    /// ignored if not using the thread-per-core scheduler.
405    #[clap(hide_short_help = true)]
406    #[clap(long, value_name = "bool")]
407    #[clap(help = EXP_HELP.get("use_worker_spinning").unwrap().as_str())]
408    pub use_worker_spinning: Option<bool>,
409
410    /// If set, overrides the automatically calculated minimum time workers may run ahead when sending events between nodes
411    #[clap(hide_short_help = true)]
412    #[clap(long, value_name = "seconds")]
413    #[clap(help = EXP_HELP.get("runahead").unwrap().as_str())]
414    pub runahead: Option<NullableOption<units::Time<units::TimePrefix>>>,
415
416    /// Update the minimum runahead dynamically throughout the simulation.
417    #[clap(hide_short_help = true)]
418    #[clap(long, value_name = "bool")]
419    #[clap(help = EXP_HELP.get("use_dynamic_runahead").unwrap().as_str())]
420    pub use_dynamic_runahead: Option<bool>,
421
422    /// Initial size of the socket's send buffer
423    #[clap(hide_short_help = true)]
424    #[clap(long, value_name = "bytes")]
425    #[clap(help = EXP_HELP.get("socket_send_buffer").unwrap().as_str())]
426    pub socket_send_buffer: Option<units::Bytes<units::SiPrefixUpper>>,
427
428    /// Enable send window autotuning
429    #[clap(hide_short_help = true)]
430    #[clap(long, value_name = "bool")]
431    #[clap(help = EXP_HELP.get("socket_send_autotune").unwrap().as_str())]
432    pub socket_send_autotune: Option<bool>,
433
434    /// Initial size of the socket's receive buffer
435    #[clap(hide_short_help = true)]
436    #[clap(long, value_name = "bytes")]
437    #[clap(help = EXP_HELP.get("socket_recv_buffer").unwrap().as_str())]
438    pub socket_recv_buffer: Option<units::Bytes<units::SiPrefixUpper>>,
439
440    /// Enable receive window autotuning
441    #[clap(hide_short_help = true)]
442    #[clap(long, value_name = "bool")]
443    #[clap(help = EXP_HELP.get("socket_recv_autotune").unwrap().as_str())]
444    pub socket_recv_autotune: Option<bool>,
445
446    /// The queueing discipline to use at the network interface
447    #[clap(hide_short_help = true)]
448    #[clap(long, value_name = "mode")]
449    #[clap(help = EXP_HELP.get("interface_qdisc").unwrap().as_str())]
450    pub interface_qdisc: Option<QDiscMode>,
451
452    /// Log the syscalls for each process to individual "strace" files
453    #[clap(hide_short_help = true)]
454    #[clap(long, value_name = "mode")]
455    #[clap(help = EXP_HELP.get("strace_logging_mode").unwrap().as_str())]
456    pub strace_logging_mode: Option<StraceLoggingMode>,
457
458    /// Max amount of execution-time latency allowed to accumulate before the
459    /// clock is moved forward. Moving the clock forward is a potentially
460    /// expensive operation, so larger values reduce simulation overhead, at the
461    /// cost of coarser time jumps. Note also that accumulated-but-unapplied
462    /// latency is discarded when a thread is blocked on a syscall.
463    #[clap(hide_short_help = true)]
464    #[clap(long, value_name = "seconds")]
465    #[clap(help = EXP_HELP.get("max_unapplied_cpu_latency").unwrap().as_str())]
466    pub max_unapplied_cpu_latency: Option<units::Time<units::TimePrefix>>,
467
468    /// Simulated latency of an unblocked syscall. For efficiency Shadow only
469    /// actually adds this latency if and when `max_unapplied_cpu_latency` is
470    /// reached.
471    #[clap(hide_short_help = true)]
472    #[clap(long, value_name = "seconds")]
473    #[clap(help = EXP_HELP.get("unblocked_syscall_latency").unwrap().as_str())]
474    pub unblocked_syscall_latency: Option<units::Time<units::TimePrefix>>,
475
476    /// Simulated latency of a vdso "syscall". For efficiency Shadow only
477    /// actually adds this latency if and when `max_unapplied_cpu_latency` is
478    /// reached.
479    #[clap(hide_short_help = true)]
480    #[clap(long, value_name = "seconds")]
481    #[clap(help = EXP_HELP.get("unblocked_vdso_latency").unwrap().as_str())]
482    pub unblocked_vdso_latency: Option<units::Time<units::TimePrefix>>,
483
484    /// The host scheduler implementation, which decides how to assign hosts to threads and threads
485    /// to CPU cores
486    #[clap(hide_short_help = true)]
487    #[clap(long, value_name = "name")]
488    #[clap(help = EXP_HELP.get("scheduler").unwrap().as_str())]
489    pub scheduler: Option<Scheduler>,
490
491    /// When true, report error-level messages to stderr in addition to logging to stdout.
492    #[clap(hide_short_help = true)]
493    #[clap(long, value_name = "bool")]
494    #[clap(help = EXP_HELP.get("report_errors_to_stderr").unwrap().as_str())]
495    pub report_errors_to_stderr: Option<bool>,
496
497    /// Use the rust TCP implementation
498    #[clap(hide_short_help = true)]
499    #[clap(long, value_name = "bool")]
500    #[clap(help = EXP_HELP.get("use_new_tcp").unwrap().as_str())]
501    pub use_new_tcp: Option<bool>,
502
503    /// When true, and when managed code runs for an extended time without
504    /// returning control to shadow (e.g. by making a syscall), shadow preempts
505    /// the managed code and moves simulated time forward. This can be used to
506    /// escape "pure-CPU busy-loops", but isn't usually needed, breaks
507    /// simulation determinism, and significantly affects simulation
508    /// performance.
509    #[clap(hide_short_help = true)]
510    #[clap(long, value_name = "bool")]
511    #[clap(help = EXP_HELP.get("native_preemption_enabled").unwrap().as_str())]
512    pub native_preemption_enabled: Option<bool>,
513
514    /// When `native_preemption_enabled` is true, amount of native CPU-time to
515    /// wait before preempting managed code that hasn't returned control to
516    /// shadow. Only supports microsecond granularity, and values below 1 microsecond
517    /// are rejected.
518    #[clap(hide_short_help = true)]
519    #[clap(long, value_name = "seconds")]
520    #[clap(help = EXP_HELP.get("native_preemption_native_interval").unwrap().as_str())]
521    pub native_preemption_native_interval: Option<units::Time<units::TimePrefix>>,
522
523    /// When `native_preemption_enabled` is true, amount of simulated time to
524    /// consume after `native_preemption_native_interval` has elapsed without
525    /// returning control to shadow.
526    #[clap(hide_short_help = true)]
527    #[clap(long, value_name = "seconds")]
528    #[clap(help = EXP_HELP.get("native_preemption_sim_interval").unwrap().as_str())]
529    pub native_preemption_sim_interval: Option<units::Time<units::TimePrefix>>,
530}
531
532impl ExperimentalOptions {
533    /// Replace unset (`None`) values of `base` with values from `default`.
534    pub fn with_defaults(mut self, default: Self) -> Self {
535        self.merge(default);
536        self
537    }
538}
539
540impl Default for ExperimentalOptions {
541    fn default() -> Self {
542        Self {
543            use_sched_fifo: Some(false),
544            use_syscall_counters: Some(true),
545            use_object_counters: Some(true),
546            use_preload_libc: Some(true),
547            use_preload_openssl_rng: Some(true),
548            use_preload_openssl_crypto: Some(false),
549            max_unapplied_cpu_latency: Some(units::Time::new(1, units::TimePrefix::Micro)),
550            // 1-2 microseconds is a ballpark estimate of the minimal latency for
551            // context switching to the kernel and back on modern machines.
552            // Default to the lower end to minimize effect in simualations without busy loops.
553            unblocked_syscall_latency: Some(units::Time::new(1, units::TimePrefix::Micro)),
554            // Actual latencies vary from ~40 to ~400 CPU cycles. https://stackoverflow.com/a/13096917
555            // Default to the lower end to minimize effect in simualations without busy loops.
556            unblocked_vdso_latency: Some(units::Time::new(10, units::TimePrefix::Nano)),
557            use_memory_manager: Some(false),
558            use_cpu_pinning: Some(true),
559            use_worker_spinning: Some(true),
560            runahead: Some(NullableOption::Value(units::Time::new(
561                1,
562                units::TimePrefix::Milli,
563            ))),
564            use_dynamic_runahead: Some(false),
565            socket_send_buffer: Some(units::Bytes::new(131_072, units::SiPrefixUpper::Base)),
566            socket_send_autotune: Some(true),
567            socket_recv_buffer: Some(units::Bytes::new(174_760, units::SiPrefixUpper::Base)),
568            socket_recv_autotune: Some(true),
569            interface_qdisc: Some(QDiscMode::Fifo),
570            strace_logging_mode: Some(StraceLoggingMode::Off),
571            scheduler: Some(Scheduler::ThreadPerCore),
572            report_errors_to_stderr: Some(true),
573            use_new_tcp: Some(false),
574            native_preemption_enabled: Some(false),
575            native_preemption_native_interval: Some(units::Time::new(
576                100,
577                units::TimePrefix::Milli,
578            )),
579            native_preemption_sim_interval: Some(units::Time::new(10, units::TimePrefix::Milli)),
580        }
581    }
582}
583
584/// Help messages used by Clap for command line arguments, combining the doc string with
585/// the Serde default.
586static HOST_HELP: Lazy<std::collections::HashMap<String, String>> =
587    Lazy::new(|| generate_help_strs(schema_for!(HostDefaultOptions)));
588
589#[derive(Debug, Clone, Parser, Serialize, Deserialize, Merge, JsonSchema)]
590#[clap(next_help_heading = "Host Defaults (Default options for hosts)")]
591#[clap(next_display_order = None)]
592#[serde(default, deny_unknown_fields)]
593// serde will default all fields to `None`, but in the cli help we want the actual defaults
594#[schemars(default = "HostDefaultOptions::new_with_defaults")]
595#[merge(strategy = merge::option::overwrite_none)]
596pub struct HostDefaultOptions {
597    /// Log level at which to print node messages
598    #[clap(long = "host-log-level", name = "host-log-level")]
599    #[clap(value_name = "level")]
600    #[clap(help = HOST_HELP.get("log_level").unwrap().as_str())]
601    pub log_level: Option<NullableOption<LogLevel>>,
602
603    /// Should shadow generate pcap files?
604    #[clap(long, value_name = "bool")]
605    #[clap(help = HOST_HELP.get("pcap_enabled").unwrap().as_str())]
606    pub pcap_enabled: Option<bool>,
607
608    /// How much data to capture per packet (header and payload) if pcap logging is enabled
609    #[clap(long, value_name = "bytes")]
610    #[clap(help = HOST_HELP.get("pcap_capture_size").unwrap().as_str())]
611    pub pcap_capture_size: Option<units::Bytes<units::SiPrefixUpper>>,
612}
613
614impl HostDefaultOptions {
615    pub fn new_with_defaults() -> Self {
616        Self {
617            log_level: None,
618            pcap_enabled: Some(false),
619            // From pcap(3): "A value of 65535 should be sufficient, on most if not all networks, to
620            // capture all the data available from the packet". The maximum length of an IP packet
621            // (including the header) is 65535 bytes.
622            pcap_capture_size: Some(units::Bytes::new(65535, units::SiPrefixUpper::Base)),
623        }
624    }
625
626    /// Replace unset (`None`) values of `base` with values from `default`.
627    pub fn with_defaults(mut self, default: Self) -> Self {
628        self.merge(default);
629        self
630    }
631}
632
633#[allow(clippy::derivable_impls)]
634impl Default for HostDefaultOptions {
635    fn default() -> Self {
636        // Our config fields would typically be initialized with their real defaults here in the
637        // `Default::default` implementation, but we need to handle the host options differently
638        // because the global `host_option_defaults` can be overridden by host-specific
639        // `host_options`. So instead we use defaults of `None` here and set the real defaults with
640        // `Self::new_with_defaults` in `ConfigOptions::new`.
641        Self {
642            log_level: None,
643            pcap_enabled: None,
644            pcap_capture_size: None,
645        }
646    }
647}
648
649#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Copy, Clone, JsonSchema)]
650#[serde(rename_all = "kebab-case")]
651pub enum RunningVal {
652    Running,
653}
654
655/// The enum variants here have an extra level of indirection to get the
656/// serde serialization that we want.
657#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
658#[serde(untagged)]
659pub enum ProcessFinalState {
660    Exited { exited: i32 },
661    Signaled { signaled: Signal },
662    Running(RunningVal),
663}
664
665impl Default for ProcessFinalState {
666    fn default() -> Self {
667        Self::Exited { exited: 0 }
668    }
669}
670
671impl std::fmt::Display for ProcessFinalState {
672    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673        // We use the yaml serialization here so that when reporting that an
674        // expected state didn't match the actual state, it's clear how to set
675        // the expected state in the config file to match the actual state if
676        // desired.
677        //
678        // The current enum works OK for this since there are no internal
679        // newlines in the serialization; if there are some later we might wand
680        // to serialize to json instead, which can always be put on a single
681        // line and should also be valid yaml.
682        let s = serde_yaml::to_string(self).or(Err(std::fmt::Error))?;
683        write!(f, "{}", s.trim())
684    }
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
688#[serde(deny_unknown_fields)]
689pub struct ProcessOptions {
690    pub path: std::path::PathBuf,
691
692    /// Process arguments
693    #[serde(default = "default_args_empty")]
694    pub args: ProcessArgs,
695
696    /// Environment variables passed when executing this process
697    #[serde(default)]
698    pub environment: BTreeMap<EnvName, String>,
699
700    /// The simulated time at which to execute the process
701    #[serde(default)]
702    pub start_time: units::Time<units::TimePrefix>,
703
704    /// The simulated time at which to send a `shutdown_signal` signal to the process
705    #[serde(default)]
706    pub shutdown_time: Option<units::Time<units::TimePrefix>>,
707
708    /// The signal that will be sent to the process at `shutdown_time`
709    #[serde(default = "default_sigterm")]
710    pub shutdown_signal: Signal,
711
712    /// The expected final state of the process. Shadow will report an error
713    /// if the actual state doesn't match.
714    #[serde(default)]
715    pub expected_final_state: ProcessFinalState,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
719#[serde(deny_unknown_fields)]
720pub struct HostOptions {
721    /// Network graph node ID to assign the host to
722    pub network_node_id: u32,
723
724    pub processes: Vec<ProcessOptions>,
725
726    /// IP address to assign to the host
727    #[serde(default)]
728    pub ip_addr: Option<std::net::Ipv4Addr>,
729
730    /// Downstream bandwidth capacity of the host
731    #[serde(default)]
732    pub bandwidth_down: Option<units::BitsPerSec<units::SiPrefixUpper>>,
733
734    /// Upstream bandwidth capacity of the host
735    #[serde(default)]
736    pub bandwidth_up: Option<units::BitsPerSec<units::SiPrefixUpper>>,
737
738    #[serde(default)]
739    pub host_options: HostDefaultOptions,
740}
741
742#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
743#[serde(rename_all = "kebab-case")]
744pub enum LogLevel {
745    Error,
746    Warning,
747    Info,
748    Debug,
749    Trace,
750}
751
752impl FromStr for LogLevel {
753    type Err = serde_yaml::Error;
754
755    fn from_str(s: &str) -> Result<Self, Self::Err> {
756        serde_yaml::from_str(s)
757    }
758}
759
760impl LogLevel {
761    pub fn to_c_loglevel(&self) -> c_log::LogLevel {
762        match self {
763            Self::Error => c_log::_LogLevel_LOGLEVEL_ERROR,
764            Self::Warning => c_log::_LogLevel_LOGLEVEL_WARNING,
765            Self::Info => c_log::_LogLevel_LOGLEVEL_INFO,
766            Self::Debug => c_log::_LogLevel_LOGLEVEL_DEBUG,
767            Self::Trace => c_log::_LogLevel_LOGLEVEL_TRACE,
768        }
769    }
770}
771
772impl From<LogLevel> for log::Level {
773    fn from(level: LogLevel) -> Self {
774        match level {
775            LogLevel::Error => log::Level::Error,
776            LogLevel::Warning => log::Level::Warn,
777            LogLevel::Info => log::Level::Info,
778            LogLevel::Debug => log::Level::Debug,
779            LogLevel::Trace => log::Level::Trace,
780        }
781    }
782}
783
784#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, JsonSchema)]
785pub struct HostName(String);
786
787impl<'de> serde::Deserialize<'de> for HostName {
788    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
789        struct HostNameVisitor;
790
791        impl serde::de::Visitor<'_> for HostNameVisitor {
792            type Value = HostName;
793
794            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
795                formatter.write_str("a string")
796            }
797
798            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
799            where
800                E: serde::de::Error,
801            {
802                // hostname(7): "Valid characters for hostnames are ASCII(7) letters from a to z,
803                // the digits from 0 to 9, and the hyphen (-)."
804                fn is_allowed(c: char) -> bool {
805                    c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.'
806                }
807                if let Some(invalid_char) = v.chars().find(|x| !is_allowed(*x)) {
808                    return Err(E::custom(format!(
809                        "invalid hostname character: '{invalid_char}'"
810                    )));
811                }
812
813                if v.is_empty() {
814                    return Err(E::custom("empty hostname"));
815                }
816
817                // hostname(7): "A hostname may not start with a hyphen."
818                if v.starts_with('-') {
819                    return Err(E::custom("hostname begins with a '-' character"));
820                }
821
822                // hostname(7): "Each element of the hostname must be from 1 to 63 characters long
823                // and the entire hostname, including the dots, can be at most 253 characters long."
824                if v.len() > 253 {
825                    return Err(E::custom("hostname exceeds 253 characters"));
826                }
827
828                Ok(HostName(v))
829            }
830
831            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
832            where
833                E: serde::de::Error,
834            {
835                // serde::de::Visitor: "It is never correct to implement `visit_string` without
836                // implementing `visit_str`. Implement neither, both, or just `visit_str`.'
837                self.visit_string(v.to_string())
838            }
839        }
840
841        deserializer.deserialize_string(HostNameVisitor)
842    }
843}
844
845impl std::ops::Deref for HostName {
846    type Target = String;
847
848    fn deref(&self) -> &Self::Target {
849        &self.0
850    }
851}
852
853impl From<HostName> for String {
854    fn from(name: HostName) -> Self {
855        name.0
856    }
857}
858
859impl std::fmt::Display for HostName {
860    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
861        self.0.fmt(f)
862    }
863}
864
865#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, JsonSchema)]
866pub struct EnvName(String);
867
868impl EnvName {
869    pub fn new(name: impl Into<String>) -> Option<Self> {
870        let name = name.into();
871
872        // an environment variable name cannot contain a '=' character
873        if name.contains('=') {
874            return None;
875        }
876
877        Some(Self(name))
878    }
879}
880
881impl<'de> serde::Deserialize<'de> for EnvName {
882    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
883        struct EnvNameVisitor;
884
885        impl serde::de::Visitor<'_> for EnvNameVisitor {
886            type Value = EnvName;
887
888            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
889                formatter.write_str("a string")
890            }
891
892            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
893            where
894                E: serde::de::Error,
895            {
896                let Some(name) = EnvName::new(v) else {
897                    let e = "environment variable name contains a '=' character";
898                    return Err(E::custom(e));
899                };
900
901                Ok(name)
902            }
903
904            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
905            where
906                E: serde::de::Error,
907            {
908                // serde::de::Visitor: "It is never correct to implement `visit_string` without
909                // implementing `visit_str`. Implement neither, both, or just `visit_str`.'
910                self.visit_string(v.to_string())
911            }
912        }
913
914        deserializer.deserialize_string(EnvNameVisitor)
915    }
916}
917
918impl std::ops::Deref for EnvName {
919    type Target = String;
920
921    fn deref(&self) -> &Self::Target {
922        &self.0
923    }
924}
925
926impl From<EnvName> for String {
927    fn from(name: EnvName) -> Self {
928        name.0
929    }
930}
931
932impl std::fmt::Display for EnvName {
933    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
934        self.0.fmt(f)
935    }
936}
937
938#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
939#[serde(rename_all = "kebab-case")]
940pub enum Scheduler {
941    ThreadPerHost,
942    ThreadPerCore,
943}
944
945impl FromStr for Scheduler {
946    type Err = serde_yaml::Error;
947
948    fn from_str(s: &str) -> Result<Self, Self::Err> {
949        serde_yaml::from_str(s)
950    }
951}
952
953fn default_data_directory() -> Option<String> {
954    Some("shadow.data".into())
955}
956
957/// Parse a string as a comma-delimited set of `T` values.
958fn parse_set<T>(s: &str) -> Result<HashSet<T>, <T as FromStr>::Err>
959where
960    T: std::cmp::Eq + std::hash::Hash + FromStr,
961{
962    s.split(',').map(|x| x.trim().parse()).collect()
963}
964
965/// Parse a string as a comma-delimited set of `String` values.
966fn parse_set_str(s: &str) -> Result<HashSet<String>, <String as FromStr>::Err> {
967    parse_set(s)
968}
969
970#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
971#[serde(rename_all = "kebab-case")]
972#[repr(C)]
973pub enum QDiscMode {
974    Fifo,
975    RoundRobin,
976}
977
978impl FromStr for QDiscMode {
979    type Err = serde_yaml::Error;
980
981    fn from_str(s: &str) -> Result<Self, Self::Err> {
982        serde_yaml::from_str(s)
983    }
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
987#[serde(rename_all = "kebab-case")]
988pub enum Compression {
989    Xz,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
993#[serde(deny_unknown_fields)]
994pub struct FileSource {
995    /// The path to the file
996    pub path: String,
997    /// The file's compression format
998    pub compression: Option<Compression>,
999}
1000
1001#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1002#[serde(rename_all = "kebab-case")]
1003pub enum GraphSource {
1004    File(FileSource),
1005    Inline(String),
1006}
1007
1008#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1009// we use "kebab-case" for other shadow options, but are leaving this as "snake_case" for backwards
1010// compatibility
1011#[serde(tag = "type", rename_all = "snake_case")]
1012pub enum GraphOptions {
1013    Gml(GraphSource),
1014    #[serde(rename = "1_gbit_switch")]
1015    OneGbitSwitch,
1016}
1017
1018#[derive(Debug, Clone, Serialize, JsonSchema)]
1019#[serde(untagged)]
1020pub enum ProcessArgs {
1021    List(Vec<String>),
1022    Str(String),
1023}
1024
1025/// Serde doesn't provide good deserialization error messages for untagged enums, so we implement
1026/// our own. For example, if serde finds a yaml value such as 4 for the process arguments, it won't
1027/// deserialize it to the string "4" and the yaml parsing will fail. The serde-generated error
1028/// message will say something like "data did not match any variant of untagged enum ProcessArgs at
1029/// line X column Y" which isn't very helpful to the user, so here we try to give a better error
1030/// message.
1031impl<'de> serde::Deserialize<'de> for ProcessArgs {
1032    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1033        struct ProcessArgsVisitor;
1034
1035        impl<'de> serde::de::Visitor<'de> for ProcessArgsVisitor {
1036            type Value = ProcessArgs;
1037
1038            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1039                formatter.write_str("a string or a sequence of strings")
1040            }
1041
1042            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1043            where
1044                E: serde::de::Error,
1045            {
1046                Ok(Self::Value::Str(v.to_owned()))
1047            }
1048
1049            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1050            where
1051                A: serde::de::SeqAccess<'de>,
1052            {
1053                let mut v = vec![];
1054
1055                while let Some(val) = seq.next_element()? {
1056                    v.push(val);
1057                }
1058
1059                Ok(Self::Value::List(v))
1060            }
1061        }
1062
1063        deserializer.deserialize_any(ProcessArgsVisitor)
1064    }
1065}
1066
1067// TODO: use linux_api's Signal internally, which we control and which supports
1068// realtime signals. We need to implement conversion to and from strings to do
1069// so, while being careful that the conversion is compatible with nix's so as
1070// not to be a breaking change to our configuration format.
1071#[derive(Debug, Copy, Clone, Eq, PartialEq)]
1072pub struct Signal(nix::sys::signal::Signal);
1073
1074impl From<nix::sys::signal::Signal> for Signal {
1075    fn from(value: nix::sys::signal::Signal) -> Self {
1076        Self(value)
1077    }
1078}
1079
1080impl TryFrom<linux_api::signal::Signal> for Signal {
1081    type Error = <nix::sys::signal::Signal as TryFrom<i32>>::Error;
1082    fn try_from(value: linux_api::signal::Signal) -> Result<Self, Self::Error> {
1083        let signal = nix::sys::signal::Signal::try_from(value.as_i32())?;
1084        Ok(Self(signal))
1085    }
1086}
1087
1088impl serde::Serialize for Signal {
1089    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1090    where
1091        S: serde::Serializer,
1092    {
1093        serializer.serialize_str(self.0.as_str())
1094    }
1095}
1096
1097impl<'de> serde::Deserialize<'de> for Signal {
1098    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1099        struct SignalVisitor;
1100
1101        impl serde::de::Visitor<'_> for SignalVisitor {
1102            type Value = Signal;
1103
1104            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1105                formatter.write_str("a signal string (e.g. \"SIGINT\") or integer")
1106            }
1107
1108            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1109            where
1110                E: serde::de::Error,
1111            {
1112                nix::sys::signal::Signal::from_str(v)
1113                    .map(Signal)
1114                    .map_err(|_e| E::custom(format!("Invalid signal string: {v}")))
1115            }
1116
1117            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
1118            where
1119                E: serde::de::Error,
1120            {
1121                let v = i32::try_from(v)
1122                    .map_err(|_e| E::custom(format!("Invalid signal number: {v}")))?;
1123                nix::sys::signal::Signal::try_from(v)
1124                    .map(Signal)
1125                    .map_err(|_e| E::custom(format!("Invalid signal number: {v}")))
1126            }
1127
1128            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
1129            where
1130                E: serde::de::Error,
1131            {
1132                let v = i64::try_from(v)
1133                    .map_err(|_e| E::custom(format!("Invalid signal number: {v}")))?;
1134                self.visit_i64(v)
1135            }
1136        }
1137
1138        deserializer.deserialize_any(SignalVisitor)
1139    }
1140}
1141
1142impl std::fmt::Display for Signal {
1143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1144        write!(f, "{}", self.0)
1145    }
1146}
1147
1148impl JsonSchema for Signal {
1149    fn schema_name() -> Cow<'static, str> {
1150        "Signal".into()
1151    }
1152
1153    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1154        // Use the "anything" schema. The Deserialize implementation does the
1155        // actual parsing and error handling.
1156        // TODO: Ideally we'd only accept strings or integers here. The
1157        // documentation isn't very clear about how to construct such a schema
1158        // though, and we currently only use the schemas for command-line-option
1159        // help strings. Since we don't currently take Signals in
1160        // command-line-options, it doesn't matter.
1161        schemars::json_schema!(true)
1162    }
1163}
1164
1165impl std::ops::Deref for Signal {
1166    type Target = nix::sys::signal::Signal;
1167
1168    fn deref(&self) -> &Self::Target {
1169        &self.0
1170    }
1171}
1172
1173#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1174#[serde(rename_all = "kebab-case")]
1175pub enum StraceLoggingMode {
1176    Off,
1177    Standard,
1178    Deterministic,
1179    Long,
1180}
1181
1182impl FromStr for StraceLoggingMode {
1183    type Err = serde_yaml::Error;
1184
1185    fn from_str(s: &str) -> Result<Self, Self::Err> {
1186        serde_yaml::from_str(s)
1187    }
1188}
1189
1190/// This wrapper type allows cli options to specify "null" to overwrite a config file option with
1191/// `None`, and is intended to be used for options where "null" is a valid option value.
1192///
1193/// **Warning**: This may result in unexpected behaviour when wrapping string types. For example, if
1194/// this is used for a file path option, the value "null" will conflict with the valid filename
1195/// "null". So if the user specifies "null" for this option, Shadow will assume it means "no value"
1196/// rather than the filename "null".
1197///
1198/// ### Motivation
1199///
1200/// For configuration options, there are generally three states:
1201/// - set
1202/// - not set
1203/// - null
1204///
1205/// For serde, all three states are configurable:
1206/// - set: `runahead: 5ms`
1207/// - not set: (no `runahead` option used in yaml)
1208/// - null: `runahead: null`
1209///
1210/// For clap, there are only two states:
1211/// - set: `--runahead 5ms`
1212/// - not set: (no `--runahead` option used in command)
1213///
1214/// There is no way to set a "null" state for cli options with clap.
1215///
1216/// ### Configuration in Shadow
1217///
1218/// Shadow first parses the config file and cli options separately before merging them.
1219///
1220/// Parsing for serde:
1221/// - set: `runahead: 5ms` => runahead is set to `Some(5ms)`
1222/// - not set: (no `runahead` option used in yaml) => runahead is set to its default (either
1223///   `Some(..)` or `None`)
1224/// - null: `runahead: null` => runahead is set to `None`
1225///
1226/// Parsing for clap:
1227/// - set: `--runahead 5ms` => runahead is set to `Some(5ms)`
1228/// - not set: (no `--runahead` option used in command) => runahead is set to `None`
1229///
1230/// Then the options are merged such that any `Some(..)` options from the cli options will overwrite
1231/// any `Some` or `None` options from the config file.
1232///
1233/// The issue is that no clap option can overwrite a config file option of `Some` with a value of
1234/// `None`. For example if the config file specifies `runahead: 5ms`, then with clap you can only
1235/// use `--runahead 2ms` to change the runahead to a `Some(2ms)` value, or you can not set
1236/// `--runahead` at all to leave it as a `Some(5ms)` value. But there is no cli option to change the
1237/// runahead to a `None` value.
1238///
1239/// This `NullableOption` type is a wrapper to allow you to specify "null" on the command line to
1240/// overwrite the config file value with `None`. From the example above, you could now specify
1241/// "--runahead null" to overwrite the config file value (for example `Some(5ms)`) with a `None`
1242/// value.
1243#[derive(Debug, Copy, Clone, JsonSchema, Eq, PartialEq)]
1244pub enum NullableOption<T> {
1245    Value(T),
1246    Null,
1247}
1248
1249impl<T> NullableOption<T> {
1250    pub fn as_ref(&self) -> NullableOption<&T> {
1251        match self {
1252            NullableOption::Value(x) => NullableOption::Value(x),
1253            NullableOption::Null => NullableOption::Null,
1254        }
1255    }
1256
1257    pub fn as_mut(&mut self) -> NullableOption<&mut T> {
1258        match self {
1259            NullableOption::Value(x) => NullableOption::Value(x),
1260            NullableOption::Null => NullableOption::Null,
1261        }
1262    }
1263
1264    /// Easier to use than `Into<Option<T>>` since `Option` has a lot of blanket `From`
1265    /// implementations, requiring a lot of type annotations.
1266    pub fn to_option(self) -> Option<T> {
1267        match self {
1268            NullableOption::Value(x) => Some(x),
1269            NullableOption::Null => None,
1270        }
1271    }
1272}
1273
1274impl<T: serde::Serialize> serde::Serialize for NullableOption<T> {
1275    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1276        match self {
1277            // use the inner type's serialize function
1278            Self::Value(x) => Ok(T::serialize(x, serializer)?),
1279            Self::Null => serializer.serialize_none(),
1280        }
1281    }
1282}
1283
1284impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for NullableOption<T> {
1285    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1286        // always use the inner type's deserialize function
1287        Ok(Self::Value(T::deserialize(deserializer)?))
1288    }
1289}
1290
1291impl<T> FromStr for NullableOption<T>
1292where
1293    T: FromStr<Err: std::fmt::Debug + std::fmt::Display>,
1294{
1295    type Err = T::Err;
1296
1297    fn from_str(s: &str) -> Result<Self, Self::Err> {
1298        match s {
1299            // since we use serde-yaml, use "null" to match yaml's "null"
1300            "null" => Ok(Self::Null),
1301            x => Ok(Self::Value(FromStr::from_str(x)?)),
1302        }
1303    }
1304}
1305
1306/// A trait for `Option`-like types that can be flattened into a single `Option`.
1307pub trait Flatten<T> {
1308    fn flatten(self) -> Option<T>;
1309    fn flatten_ref(&self) -> Option<&T>;
1310}
1311
1312impl<T> Flatten<T> for Option<NullableOption<T>> {
1313    fn flatten(self) -> Option<T> {
1314        self.and_then(|x| x.to_option())
1315    }
1316
1317    fn flatten_ref(&self) -> Option<&T> {
1318        self.as_ref().and_then(|x| x.as_ref().to_option())
1319    }
1320}
1321
1322/// Helper function for serde default `ProcessArgs::Str("")` values.
1323fn default_args_empty() -> ProcessArgs {
1324    ProcessArgs::Str("".to_string())
1325}
1326
1327/// Helper function for serde default `Signal(Signal::SIGTERM)` values.
1328fn default_sigterm() -> Signal {
1329    Signal(nix::sys::signal::Signal::SIGTERM)
1330}
1331
1332/// Helper function for serde default `Some(0)` values.
1333fn default_some_time_0() -> Option<units::Time<units::TimePrefix>> {
1334    Some(units::Time::new(0, units::TimePrefix::Sec))
1335}
1336
1337/// Helper function for serde default `Some(true)` values.
1338fn default_some_true() -> Option<bool> {
1339    Some(true)
1340}
1341
1342/// Helper function for serde default `Some(false)` values.
1343fn default_some_false() -> Option<bool> {
1344    Some(false)
1345}
1346
1347/// Helper function for serde default `Some(0)` values.
1348fn default_some_0() -> Option<u32> {
1349    Some(0)
1350}
1351
1352/// Helper function for serde default `Some(1)` values.
1353fn default_some_1() -> Option<u32> {
1354    Some(1)
1355}
1356
1357/// Helper function for serde default `Some(NullableOption::Value(1 sec))` values.
1358fn default_some_nullable_time_1() -> Option<NullableOption<units::Time<units::TimePrefix>>> {
1359    let time = units::Time::new(1, units::TimePrefix::Sec);
1360    Some(NullableOption::Value(time))
1361}
1362
1363/// Helper function for serde default `Some(LogLevel::Info)` values.
1364fn default_some_info() -> Option<LogLevel> {
1365    Some(LogLevel::Info)
1366}
1367
1368// when updating this graph, make sure to also update the copy in docs/shadow_config_spec.md
1369pub const ONE_GBIT_SWITCH_GRAPH: &str = r#"graph [
1370  directed 0
1371  node [
1372    id 0
1373    host_bandwidth_up "1 Gbit"
1374    host_bandwidth_down "1 Gbit"
1375  ]
1376  edge [
1377    source 0
1378    target 0
1379    latency "1 ms"
1380    packet_loss 0.0
1381  ]
1382]"#;
1383
1384/// Generate help strings for objects in a JSON schema, including the Serde defaults if available.
1385fn generate_help_strs(schema: schemars::Schema) -> std::collections::HashMap<String, String> {
1386    // the default for each field
1387    let mut defaults = std::collections::HashMap::<String, String>::new();
1388
1389    // for each field with an entry in "properties"
1390    for (name, obj) in schema.get("properties").unwrap().as_object().unwrap() {
1391        let description = obj
1392            .get("description")
1393            .map(|x| x.as_str().unwrap())
1394            .unwrap_or("");
1395
1396        // schemars gives us the raw doc string, so there is no markdown formatting/rendering
1397        // applied. This means that line breaks aren't collapsed and our help text will have
1398        // unintended line breaks.
1399        //
1400        // We don't want to bring in an entire markdown parser here, so we crudely remove the line
1401        // breaks manually. This will cause issues where there are intended line breaks (lines
1402        // ending with two spaces), new paragraphs ('\n\n'), etc. But hopefully this is good enough
1403        // for now.
1404        //
1405        // See https://github.com/GREsau/schemars/issues/120
1406        let description = description.replace("\n", " ");
1407
1408        let name = name.clone();
1409
1410        match obj.get("default") {
1411            Some(default) => {
1412                let space = if !description.is_empty() { " " } else { "" };
1413                defaults.insert(name, format!("{description}{space}[default: {default}]"))
1414            }
1415            None => defaults.insert(name, description.to_string()),
1416        };
1417    }
1418
1419    defaults
1420}
1421
1422/// Parses a string as a list of arguments following the shell's parsing rules. This
1423/// uses `g_shell_parse_argv()` for parsing.
1424pub fn parse_string_as_args(args_str: &OsStr) -> Result<Vec<OsString>, String> {
1425    if args_str.is_empty() {
1426        return Ok(Vec::new());
1427    }
1428
1429    let args_str = CString::new(args_str.as_bytes()).unwrap();
1430
1431    // parse the argument string
1432    let mut argc: libc::c_int = 0;
1433    let mut argv: *mut *mut libc::c_char = std::ptr::null_mut();
1434    let mut error: *mut libc::c_char = std::ptr::null_mut();
1435    let rv = unsafe { c::process_parseArgStr(args_str.as_ptr(), &mut argc, &mut argv, &mut error) };
1436
1437    // if there was an error, return a copy of the error string
1438    if !rv {
1439        let error_message = match error.is_null() {
1440            false => unsafe { CStr::from_ptr(error) }.to_str().unwrap(),
1441            true => "Unknown parsing error",
1442        }
1443        .to_string();
1444
1445        unsafe { c::process_parseArgStrFree(argv, error) };
1446        return Err(error_message);
1447    }
1448
1449    assert!(!argv.is_null());
1450
1451    // copy the arg strings
1452    let args: Vec<_> = (0..argc)
1453        .map(|x| unsafe {
1454            let arg_ptr = *argv.add(x as usize);
1455            assert!(!arg_ptr.is_null());
1456            OsStr::from_bytes(CStr::from_ptr(arg_ptr).to_bytes()).to_os_string()
1457        })
1458        .collect();
1459
1460    unsafe { c::process_parseArgStrFree(argv, error) };
1461    Ok(args)
1462}
1463
1464#[cfg(test)]
1465mod tests {
1466    use super::*;
1467
1468    #[test]
1469    // can't call foreign function: process_parseArgStr
1470    #[cfg_attr(miri, ignore)]
1471    fn test_parse_args() {
1472        let arg_str = r#"the quick brown fox "jumped over" the "\"lazy\" dog""#;
1473        let expected_args = &[
1474            "the",
1475            "quick",
1476            "brown",
1477            "fox",
1478            "jumped over",
1479            "the",
1480            "\"lazy\" dog",
1481        ];
1482
1483        let arg_str: OsString = arg_str.into();
1484        let args = parse_string_as_args(&arg_str).unwrap();
1485
1486        assert_eq!(args, expected_args);
1487    }
1488
1489    #[test]
1490    // can't call foreign function: process_parseArgStr
1491    #[cfg_attr(miri, ignore)]
1492    fn test_parse_args_empty() {
1493        let arg_str = "";
1494        let expected_args: &[&str] = &[];
1495
1496        let arg_str: OsString = arg_str.into();
1497        let args = parse_string_as_args(&arg_str).unwrap();
1498
1499        assert_eq!(args, expected_args);
1500    }
1501
1502    #[test]
1503    // can't call foreign function: process_parseArgStr
1504    #[cfg_attr(miri, ignore)]
1505    fn test_parse_args_error() {
1506        let arg_str = r#"hello "world"#;
1507
1508        let arg_str: OsString = arg_str.into();
1509        let err_str = parse_string_as_args(&arg_str).unwrap_err();
1510
1511        assert!(!err_str.is_empty());
1512    }
1513
1514    #[test]
1515    // can't call foreign function: process_parseArgStr
1516    #[cfg_attr(miri, ignore)]
1517    fn test_nullable_option() {
1518        // format the yaml with an optional general option
1519        let yaml_fmt_fn = |option| {
1520            format!(
1521                r#"
1522                general:
1523                  stop_time: 1 min
1524                  {option}
1525                network:
1526                  graph:
1527                    type: 1_gbit_switch
1528                hosts:
1529                  myhost:
1530                    network_node_id: 0
1531                    processes:
1532                    - path: /bin/true
1533                "#,
1534            )
1535        };
1536
1537        let time_1_sec = units::Time::new(1, units::TimePrefix::Sec);
1538        let time_5_sec = units::Time::new(5, units::TimePrefix::Sec);
1539
1540        // "heartbeat_interval: null" with no cli option => None
1541        let yaml = yaml_fmt_fn("heartbeat_interval: null");
1542        let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1543        let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
1544
1545        let merged = ConfigOptions::new(config_file, cli);
1546        assert_eq!(merged.general.heartbeat_interval, None);
1547
1548        // "heartbeat_interval: null" with "--heartbeat-interval 5s" => 5s
1549        let yaml = yaml_fmt_fn("heartbeat_interval: null");
1550        let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1551        let cli: CliOptions =
1552            CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
1553
1554        let merged = ConfigOptions::new(config_file, cli);
1555        assert_eq!(
1556            merged.general.heartbeat_interval,
1557            Some(NullableOption::Value(time_5_sec))
1558        );
1559
1560        // "heartbeat_interval: null" with "--heartbeat-interval null" => NullableOption::Null
1561        let yaml = yaml_fmt_fn("heartbeat_interval: null");
1562        let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1563        let cli: CliOptions =
1564            CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
1565
1566        let merged = ConfigOptions::new(config_file, cli);
1567        assert_eq!(
1568            merged.general.heartbeat_interval,
1569            Some(NullableOption::Null)
1570        );
1571
1572        // "heartbeat_interval: 5s" with no cli option => 5s
1573        let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
1574        let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1575        let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
1576
1577        let merged = ConfigOptions::new(config_file, cli);
1578        assert_eq!(
1579            merged.general.heartbeat_interval,
1580            Some(NullableOption::Value(time_5_sec))
1581        );
1582
1583        // "heartbeat_interval: 5s" with "--heartbeat-interval 5s" => 5s
1584        let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
1585        let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1586        let cli: CliOptions =
1587            CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
1588
1589        let merged = ConfigOptions::new(config_file, cli);
1590        assert_eq!(
1591            merged.general.heartbeat_interval,
1592            Some(NullableOption::Value(time_5_sec))
1593        );
1594
1595        // "heartbeat_interval: 5s" with "--heartbeat-interval null" => NullableOption::Null
1596        let yaml = yaml_fmt_fn("heartbeat_interval: 5s");
1597        let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1598        let cli: CliOptions =
1599            CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
1600
1601        let merged = ConfigOptions::new(config_file, cli);
1602        assert_eq!(
1603            merged.general.heartbeat_interval,
1604            Some(NullableOption::Null)
1605        );
1606
1607        // no config option with no cli option => 1s (default)
1608        let yaml = yaml_fmt_fn("");
1609        let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1610        let cli: CliOptions = CliOptions::try_parse_from(["shadow", "-"]).unwrap();
1611
1612        let merged = ConfigOptions::new(config_file, cli);
1613        assert_eq!(
1614            merged.general.heartbeat_interval,
1615            Some(NullableOption::Value(time_1_sec))
1616        );
1617
1618        // no config option with "--heartbeat-interval 5s" => 5s
1619        let yaml = yaml_fmt_fn("");
1620        let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1621        let cli: CliOptions =
1622            CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "5s", "-"]).unwrap();
1623
1624        let merged = ConfigOptions::new(config_file, cli);
1625        assert_eq!(
1626            merged.general.heartbeat_interval,
1627            Some(NullableOption::Value(time_5_sec))
1628        );
1629
1630        // no config option with "--heartbeat-interval null" => NullableOption::Null
1631        let yaml = yaml_fmt_fn("");
1632        let config_file: ConfigFileOptions = serde_yaml::from_str(&yaml).unwrap();
1633        let cli: CliOptions =
1634            CliOptions::try_parse_from(["shadow", "--heartbeat-interval", "null", "-"]).unwrap();
1635
1636        let merged = ConfigOptions::new(config_file, cli);
1637        assert_eq!(
1638            merged.general.heartbeat_interval,
1639            Some(NullableOption::Null)
1640        );
1641    }
1642}