Skip to main content

vector/
cli.rs

1#![allow(missing_docs)]
2
3use std::{
4    num::{NonZeroU64, NonZeroUsize},
5    path::PathBuf,
6};
7
8use clap::{ArgAction, CommandFactory, FromArgMatches, Parser};
9
10#[cfg(windows)]
11use crate::service;
12#[cfg(feature = "api-client")]
13use crate::tap;
14#[cfg(feature = "top")]
15use crate::top;
16
17use crate::{
18    completion, config, convert_config, generate, generate_schema, get_version, graph, list,
19    signal, unit_test, validate,
20};
21
22#[derive(Parser, Debug)]
23#[command(rename_all = "kebab-case")]
24pub struct Opts {
25    #[command(flatten)]
26    pub root: RootOpts,
27
28    #[command(subcommand)]
29    pub sub_command: Option<SubCommand>,
30}
31
32impl Opts {
33    pub fn get_matches() -> Result<Self, clap::Error> {
34        let version = get_version();
35        let app = Opts::command().version(version);
36        Opts::from_arg_matches(&app.get_matches())
37    }
38
39    pub const fn log_level(&self) -> &'static str {
40        let (quiet_level, verbose_level) = match self.sub_command {
41            Some(SubCommand::Validate(_))
42            | Some(SubCommand::Graph(_))
43            | Some(SubCommand::Generate(_))
44            | Some(SubCommand::ConvertConfig(_))
45            | Some(SubCommand::List(_))
46            | Some(SubCommand::Test(_)) => {
47                if self.root.verbose == 0 {
48                    (self.root.quiet + 1, self.root.verbose)
49                } else {
50                    (self.root.quiet, self.root.verbose - 1)
51                }
52            }
53            _ => (self.root.quiet, self.root.verbose),
54        };
55        match quiet_level {
56            0 => match verbose_level {
57                0 => "info",
58                1 => "debug",
59                2..=255 => "trace",
60            },
61            1 => "warn",
62            2 => "error",
63            3..=255 => "off",
64        }
65    }
66}
67
68#[derive(Parser, Debug)]
69#[command(rename_all = "kebab-case")]
70pub struct RootOpts {
71    /// Read configuration from one or more files. Wildcard paths are supported.
72    /// File format is detected from the file name.
73    /// If zero files are specified, the deprecated default config path
74    /// `/etc/vector/vector.yaml` is targeted.
75    #[arg(
76        id = "config",
77        short,
78        long,
79        env = "VECTOR_CONFIG",
80        value_delimiter(',')
81    )]
82    pub config_paths: Vec<PathBuf>,
83
84    /// Read configuration from files in one or more directories.
85    /// File format is detected from the file name.
86    ///
87    /// Files not ending in .toml, .json, .yaml, or .yml will be ignored.
88    #[arg(
89        id = "config-dir",
90        short = 'C',
91        long,
92        env = "VECTOR_CONFIG_DIR",
93        value_delimiter(',')
94    )]
95    pub config_dirs: Vec<PathBuf>,
96
97    /// Read configuration from one or more files. Wildcard paths are supported.
98    /// TOML file format is expected.
99    #[arg(
100        id = "config-toml",
101        long,
102        env = "VECTOR_CONFIG_TOML",
103        value_delimiter(',')
104    )]
105    pub config_paths_toml: Vec<PathBuf>,
106
107    /// Read configuration from one or more files. Wildcard paths are supported.
108    /// JSON file format is expected.
109    #[arg(
110        id = "config-json",
111        long,
112        env = "VECTOR_CONFIG_JSON",
113        value_delimiter(',')
114    )]
115    pub config_paths_json: Vec<PathBuf>,
116
117    /// Read configuration from one or more files. Wildcard paths are supported.
118    /// YAML file format is expected.
119    #[arg(
120        id = "config-yaml",
121        long,
122        env = "VECTOR_CONFIG_YAML",
123        value_delimiter(',')
124    )]
125    pub config_paths_yaml: Vec<PathBuf>,
126
127    /// Exit on startup if any sinks fail healthchecks
128    #[arg(short, long, env = "VECTOR_REQUIRE_HEALTHY")]
129    pub require_healthy: Option<bool>,
130
131    /// Number of threads to use for processing (default is number of available cores)
132    #[arg(short, long, env = "VECTOR_THREADS")]
133    pub threads: Option<usize>,
134
135    /// Number of events batched per source send and used as the base for source output buffer sizing
136    /// (source output buffer capacity is this value multiplied by the number of worker threads)
137    #[arg(long, env = "VECTOR_CHUNK_SIZE_EVENTS")]
138    pub chunk_size_events: Option<NonZeroUsize>,
139
140    /// Enable more detailed internal logging. Repeat to increase level. Overridden by `--quiet`.
141    #[arg(short, long, action = ArgAction::Count)]
142    pub verbose: u8,
143
144    /// Reduce detail of internal logging. Repeat to reduce further. Overrides `--verbose`.
145    #[arg(short, long, action = ArgAction::Count)]
146    pub quiet: u8,
147
148    /// Allow interpolation of environment variables in configuration files. Enabling this may
149    /// expose environment secrets into your Vector configuration.
150    #[arg(
151        long,
152        env = "VECTOR_DANGEROUSLY_ALLOW_ENV_VAR_INTERPOLATION",
153        default_value = "false"
154    )]
155    pub dangerously_allow_env_var_interpolation: bool,
156
157    /// Set the logging format
158    #[arg(long, default_value = "text", env = "VECTOR_LOG_FORMAT")]
159    pub log_format: LogFormat,
160
161    /// Control when ANSI terminal formatting is used.
162    ///
163    /// By default `vector` will try and detect if `stdout` is a terminal, if it is
164    /// ANSI will be enabled. Otherwise it will be disabled. By providing this flag with
165    /// the `--color always` option will always enable ANSI terminal formatting. `--color never`
166    /// will disable all ANSI terminal formatting. `--color auto` will attempt
167    /// to detect it automatically.
168    #[arg(long, default_value = "auto", env = "VECTOR_COLOR")]
169    pub color: Color,
170
171    /// Watch for changes in configuration file, and reload accordingly.
172    #[arg(short, long, env = "VECTOR_WATCH_CONFIG")]
173    pub watch_config: bool,
174
175    /// Method for configuration watching.
176    ///
177    /// By default, `vector` uses recommended watcher for host OS
178    /// - `inotify` for Linux-based systems.
179    /// - `kqueue` for unix/macos
180    /// - `ReadDirectoryChangesWatcher` for windows
181    ///
182    /// The `poll` watcher can be used in cases where `inotify` doesn't work, e.g., when attaching the configuration via NFS.
183    #[arg(
184        long,
185        default_value = "recommended",
186        env = "VECTOR_WATCH_CONFIG_METHOD"
187    )]
188    pub watch_config_method: WatchConfigMethod,
189
190    /// Poll for changes in the configuration file at the given interval.
191    ///
192    /// This setting is only applicable if `Poll` is set in `--watch-config-method`.
193    #[arg(
194        long,
195        env = "VECTOR_WATCH_CONFIG_POLL_INTERVAL_SECONDS",
196        default_value = "30"
197    )]
198    pub watch_config_poll_interval_seconds: NonZeroU64,
199
200    /// Set the internal log rate limit in seconds.
201    ///
202    /// This controls the time window for rate limiting Vector's own internal logs.
203    /// Within each time window, the first occurrence of a log is emitted, the second
204    /// shows a suppression warning, and subsequent occurrences are silent until the
205    /// window expires. When the window expires and the log fires again, a summary of
206    /// the suppressed count is emitted followed by the log itself.
207    ///
208    /// Logs are grouped by their location in the code and the `component_id` field, so logs
209    /// from different components are rate limited independently.
210    ///
211    /// Examples:
212    /// - 1: Very verbose, logs can repeat every second
213    /// - 10 (default): Logs can repeat every 10 seconds
214    /// - 60: Less verbose, logs can repeat every minute
215    #[arg(
216        short,
217        long,
218        env = "VECTOR_INTERNAL_LOG_RATE_LIMIT",
219        default_value = "10"
220    )]
221    pub internal_log_rate_limit: u64,
222
223    /// Apply a rate limit (in seconds) to the broadcast channel that feeds all `internal_logs`
224    /// sources. When set, the first occurrence of a repeated log is emitted, the second shows a
225    /// suppression warning, and subsequent occurrences are silent until the window expires. When
226    /// the window expires and the log fires again, a summary of the suppressed count is emitted
227    /// followed by the log itself. Unset by default so that `internal_logs` consumers receive
228    /// every log event. This limit is independent of `--internal-log-rate-limit`, which only
229    /// applies to stdout/stderr output.
230    #[arg(long, env = "VECTOR_INTERNAL_LOGS_SOURCE_RATE_LIMIT")]
231    pub internal_logs_source_rate_limit: Option<NonZeroU64>,
232
233    /// Set the duration in seconds to wait for graceful shutdown after SIGINT or SIGTERM are
234    /// received. After the duration has passed, Vector will force shutdown. To never force
235    /// shutdown, use `--no-graceful-shutdown-limit`.
236    #[arg(
237        long,
238        default_value = "60",
239        env = "VECTOR_GRACEFUL_SHUTDOWN_LIMIT_SECS",
240        group = "graceful-shutdown-limit"
241    )]
242    pub graceful_shutdown_limit_secs: NonZeroU64,
243
244    /// Never time out while waiting for graceful shutdown after SIGINT or SIGTERM received.
245    /// This is useful when you would like for Vector to attempt to send data until terminated
246    /// by a SIGKILL. Overrides/cannot be set with `--graceful-shutdown-limit-secs`.
247    #[arg(
248        long,
249        default_value = "false",
250        env = "VECTOR_NO_GRACEFUL_SHUTDOWN_LIMIT",
251        group = "graceful-shutdown-limit"
252    )]
253    pub no_graceful_shutdown_limit: bool,
254
255    /// Set runtime allocation tracing
256    #[cfg(all(unix, feature = "tikv-jemallocator"))]
257    #[arg(long, env = "ALLOCATION_TRACING", default_value = "false")]
258    pub allocation_tracing: bool,
259
260    /// Set allocation tracing reporting rate in milliseconds.
261    #[cfg(all(unix, feature = "tikv-jemallocator"))]
262    #[arg(
263        long,
264        env = "ALLOCATION_TRACING_REPORTING_INTERVAL_MS",
265        default_value = "5000"
266    )]
267    pub allocation_tracing_reporting_interval_ms: u64,
268
269    /// Disable probing and configuration of root certificate locations on the system for OpenSSL.
270    ///
271    /// The probe functionality manipulates the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables
272    /// in the Vector process. This behavior can be problematic for users of the `exec` source, which by
273    /// default inherits the environment of the Vector process.
274    #[arg(long, env = "VECTOR_OPENSSL_NO_PROBE", default_value = "false")]
275    pub openssl_no_probe: bool,
276
277    /// Allow the configuration to run without any components. This is useful for loading in an
278    /// empty stub config that will later be replaced with actual components. Note that this is
279    /// likely not useful without also watching for config file changes as described in
280    /// `--watch-config`.
281    #[arg(long, env = "VECTOR_ALLOW_EMPTY_CONFIG", default_value = "false")]
282    pub allow_empty_config: bool,
283
284    /// Maximum number of bytes allowed after decompressing a payload.
285    ///
286    /// Sources that decompress incoming payloads (gzip, deflate, zstd, snappy) enforce this cap to
287    /// prevent a compressed "bomb" from exhausting memory. Payloads whose decompressed size exceeds
288    /// the limit are rejected.
289    ///
290    /// Defaults to 104857600 (100 MiB). Raise this only when sources routinely receive
291    /// legitimately large compressed payloads.
292    #[arg(
293        long,
294        env = "VECTOR_MAX_DECOMPRESSED_SIZE_BYTES",
295        default_value = "104857600"
296    )]
297    pub max_decompressed_size_bytes: usize,
298
299    /// Raise the file descriptor soft limit (RLIMIT_NOFILE) to the hard limit at startup.
300    ///
301    /// Many systems default the soft limit to 1024 (Linux) or 256 (macOS), which is too low
302    /// when Vector monitors large numbers of log files. This flag raises the soft limit to
303    /// prevent "Too many open files" errors without requiring manual sysadmin intervention.
304    #[cfg(unix)]
305    #[arg(long, env = "VECTOR_RAISE_FD_LIMIT", default_value = "false")]
306    pub raise_fd_limit: bool,
307}
308
309impl RootOpts {
310    /// Return a list of config paths with the associated formats.
311    pub fn config_paths_with_formats(&self) -> Vec<config::ConfigPath> {
312        config::merge_path_lists(vec![
313            (&self.config_paths, None),
314            (&self.config_paths_toml, Some(config::Format::Toml)),
315            (&self.config_paths_json, Some(config::Format::Json)),
316            (&self.config_paths_yaml, Some(config::Format::Yaml)),
317        ])
318        .map(|(path, hint)| config::ConfigPath::File(path, hint))
319        .chain(
320            self.config_dirs
321                .iter()
322                .map(|dir| config::ConfigPath::Dir(dir.to_path_buf())),
323        )
324        .collect()
325    }
326
327    pub fn init_global(&self) {
328        if !self.openssl_no_probe {
329            unsafe {
330                openssl_probe::init_openssl_env_vars();
331            }
332        }
333
334        crate::metrics::init_global().expect("metrics initialization failed");
335    }
336}
337
338/// Raise the soft file descriptor limit (RLIMIT_NOFILE) as high as the OS allows.
339///
340/// Many systems default the soft limit to 1024 (Linux) or 256 (macOS), which is too low
341/// for Vector when it monitors large numbers of log files. Raising it prevents
342/// "Too many open files (os error 24)" errors without requiring manual sysadmin intervention.
343///
344/// On Linux, the soft limit is raised to the hard limit (typically 65536+).
345/// On macOS, the hard limit can be RLIM_INFINITY, so we first try the hard limit,
346/// then fall back to the kernel-enforced `kern.maxfilesperproc` (typically 10240).
347#[cfg(unix)]
348pub(crate) fn raise_file_descriptor_limit() {
349    use nix::sys::resource::{Resource, getrlimit, setrlimit};
350    use tracing::{info, warn};
351
352    let (soft, hard) = match getrlimit(Resource::RLIMIT_NOFILE) {
353        Ok(limits) => limits,
354        Err(err) => {
355            warn!(message = "Failed to get file descriptor limit.", %err);
356            return;
357        }
358    };
359
360    if soft >= hard {
361        return; // Already at maximum
362    }
363
364    // Try setting soft limit to hard limit (works on Linux, may fail on macOS)
365    if setrlimit(Resource::RLIMIT_NOFILE, hard, hard).is_ok() {
366        info!(
367            message = "Raised file descriptor limit.",
368            from = soft,
369            to = hard,
370        );
371        return;
372    }
373
374    // On macOS, the hard limit can be RLIM_INFINITY which setrlimit rejects.
375    // Fall back to the kernel-enforced kern.maxfilesperproc.
376    #[cfg(target_os = "macos")]
377    {
378        if let Some(maxfiles) = macos_maxfilesperproc()
379            && maxfiles > soft
380            && setrlimit(Resource::RLIMIT_NOFILE, maxfiles, hard).is_ok()
381        {
382            info!(
383                message = "Raised file descriptor limit.",
384                from = soft,
385                to = maxfiles,
386            );
387            return;
388        }
389    }
390
391    warn!(
392        message = "Failed to raise file descriptor limit.",
393        current = soft,
394        attempted = hard,
395    );
396}
397
398/// Query the macOS kernel limit on per-process open files.
399#[cfg(target_os = "macos")]
400fn macos_maxfilesperproc() -> Option<libc::rlim_t> {
401    let mut maxfiles: libc::c_int = 0;
402    let mut len = std::mem::size_of::<libc::c_int>() as libc::size_t;
403    // Safety: sysctlbyname with a valid null-terminated name and correctly sized output buffer.
404    // No safe wrapper exists for this macOS-specific call.
405    let ret = unsafe {
406        libc::sysctlbyname(
407            c"kern.maxfilesperproc".as_ptr(),
408            &mut maxfiles as *mut libc::c_int as *mut libc::c_void,
409            &mut len,
410            std::ptr::null_mut(),
411            0,
412        )
413    };
414    if ret == 0 && maxfiles > 0 {
415        Some(maxfiles as libc::rlim_t)
416    } else {
417        None
418    }
419}
420
421#[derive(Parser, Debug)]
422#[command(rename_all = "kebab-case")]
423pub enum SubCommand {
424    /// Validate the target config, then exit.
425    Validate(validate::Opts),
426
427    /// Convert a config file from one format to another.
428    /// This command can also walk directories recursively and convert all config files that are discovered.
429    /// Note that this is a best effort conversion due to the following reasons:
430    /// * The comments from the original config file are not preserved.
431    /// * Explicitly set default values in the original implementation might be omitted.
432    /// * Depending on how each source/sink config struct configures serde, there might be entries with null values.
433    ConvertConfig(convert_config::Opts),
434
435    /// Generate a Vector configuration containing a list of components.
436    Generate(generate::Opts),
437
438    /// Generate the configuration schema for this version of Vector. (experimental)
439    ///
440    /// A JSON Schema document will be generated that represents the valid schema for a
441    /// Vector configuration. This schema is based on the "full" configuration, such that for usages
442    /// where a configuration is split into multiple files, the schema would apply to those files
443    /// only when concatenated together.
444    ///
445    /// By default all output is written to stdout. The `output_path` option can be used to redirect to a file.
446    GenerateSchema(generate_schema::Opts),
447
448    /// Generate shell completion, then exit.
449    #[command(hide = true)]
450    Completion(completion::Opts),
451
452    /// List available components, then exit.
453    List(list::Opts),
454
455    /// Run Vector config unit tests, then exit. This command is experimental and therefore subject to change.
456    /// For guidance on how to write unit tests check out <https://vector.dev/guides/level-up/unit-testing/>.
457    Test(unit_test::Opts),
458
459    /// Output the topology as visual representation using the DOT language which can be rendered by GraphViz
460    Graph(graph::Opts),
461
462    /// Display topology and metrics in the console, for a local or remote Vector instance
463    #[cfg(feature = "top")]
464    Top(top::Opts),
465
466    /// Observe output log events from source or transform components. Logs are sampled at a specified interval.
467    #[cfg(feature = "api-client")]
468    Tap(tap::Opts),
469
470    /// Manage the vector service.
471    #[cfg(windows)]
472    Service(service::Opts),
473
474    /// Vector Remap Language CLI
475    Vrl(vrl::cli::Opts),
476}
477
478impl SubCommand {
479    #[expect(
480        clippy::missing_const_for_fn,
481        reason = "the #[cfg(windows)] arm calls a non-const method"
482    )]
483    pub fn dangerously_allow_env_var_interpolation(&self) -> bool {
484        match self {
485            Self::Graph(g) => g.dangerously_allow_env_var_interpolation,
486            Self::Test(t) => t.dangerously_allow_env_var_interpolation,
487            Self::Validate(v) => v.dangerously_allow_env_var_interpolation,
488            #[cfg(windows)]
489            Self::Service(s) => s.dangerously_allow_env_var_interpolation(),
490            _ => false,
491        }
492    }
493
494    pub async fn execute(
495        &self,
496        mut signals: signal::SignalPair,
497        color: bool,
498    ) -> exitcode::ExitCode {
499        match self {
500            Self::Completion(s) => completion::cmd(s),
501            Self::ConvertConfig(opts) => convert_config::cmd(opts),
502            Self::Generate(g) => generate::cmd(g),
503            Self::GenerateSchema(opts) => generate_schema::cmd(opts),
504            Self::Graph(g) => graph::cmd(g),
505            Self::List(l) => list::cmd(l),
506            #[cfg(windows)]
507            Self::Service(s) => service::cmd(s),
508            #[cfg(feature = "api-client")]
509            Self::Tap(t) => tap::cmd(t, signals.receiver).await,
510            Self::Test(t) => unit_test::cmd(t, &mut signals.handler).await,
511            #[cfg(feature = "top")]
512            Self::Top(t) => top::cmd(t).await,
513            Self::Validate(v) => validate::validate(v, color).await,
514            Self::Vrl(s) => vrl::cli::cmd::cmd(s, vector_vrl_functions::all()),
515        }
516    }
517}
518
519#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
520pub enum Color {
521    Auto,
522    Always,
523    Never,
524}
525
526impl Color {
527    pub fn use_color(&self) -> bool {
528        match self {
529            #[cfg(unix)]
530            Color::Auto => {
531                use std::io::IsTerminal;
532                std::io::stdout().is_terminal()
533            }
534            #[cfg(windows)]
535            Color::Auto => false, // ANSI colors are not supported by cmd.exe
536            Color::Always => true,
537            Color::Never => false,
538        }
539    }
540}
541
542#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
543pub enum LogFormat {
544    Text,
545    Json,
546}
547
548#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
549pub enum WatchConfigMethod {
550    /// Recommended watcher for the current OS, usually `inotify` for Linux-based systems.
551    Recommended,
552    /// Poll-based watcher, typically used for watching files on EFS/NFS-like network storage systems.
553    /// The interval is determined by  [`RootOpts::watch_config_poll_interval_seconds`].
554    Poll,
555}
556
557pub fn handle_config_errors(errors: Vec<String>) -> exitcode::ExitCode {
558    for error in errors {
559        error!(message = "Configuration error.", %error, internal_log_rate_limit = false);
560    }
561
562    exitcode::CONFIG
563}
564
565#[cfg(test)]
566mod tests {
567    #[cfg(unix)]
568    fn run_in_subprocess(test_name: &str) {
569        let exe = std::env::current_exe().unwrap();
570        let output = std::process::Command::new(exe)
571            .env("__VECTOR_SUBPROCESS_TEST", "1")
572            .args(["--exact", test_name, "--nocapture"])
573            .output()
574            .unwrap();
575        assert!(
576            output.status.success(),
577            "subprocess test failed:\nstdout: {}\nstderr: {}",
578            String::from_utf8_lossy(&output.stdout),
579            String::from_utf8_lossy(&output.stderr),
580        );
581    }
582
583    #[test]
584    #[cfg(unix)]
585    fn test_raise_file_descriptor_limit() {
586        if std::env::var("__VECTOR_SUBPROCESS_TEST").is_err() {
587            run_in_subprocess("cli::tests::test_raise_file_descriptor_limit");
588            return;
589        }
590
591        use nix::sys::resource::{Resource, getrlimit, setrlimit};
592
593        let (original_soft, hard) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
594        let lowered = std::cmp::min(original_soft, 256);
595        if lowered < hard {
596            setrlimit(Resource::RLIMIT_NOFILE, lowered, hard).unwrap();
597
598            let (soft_before, _) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
599            assert_eq!(soft_before, lowered);
600
601            super::raise_file_descriptor_limit();
602
603            let (soft_after, _) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
604            assert!(
605                soft_after > lowered,
606                "Expected soft limit to be raised above {lowered}, got {soft_after}"
607            );
608        }
609    }
610
611    #[test]
612    #[cfg(unix)]
613    fn test_raise_file_descriptor_limit_already_at_max() {
614        if std::env::var("__VECTOR_SUBPROCESS_TEST").is_err() {
615            run_in_subprocess("cli::tests::test_raise_file_descriptor_limit_already_at_max");
616            return;
617        }
618
619        use nix::sys::resource::{Resource, getrlimit, setrlimit};
620
621        let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
622
623        if setrlimit(Resource::RLIMIT_NOFILE, hard, hard).is_err() {
624            #[cfg(target_os = "macos")]
625            if let Some(maxfiles) = super::macos_maxfilesperproc() {
626                setrlimit(Resource::RLIMIT_NOFILE, maxfiles, hard).ok();
627            }
628        }
629
630        let (soft_before, _) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
631
632        super::raise_file_descriptor_limit();
633
634        let (soft_after, _) = getrlimit(Resource::RLIMIT_NOFILE).unwrap();
635        assert_eq!(soft_before, soft_after);
636    }
637
638    #[test]
639    #[cfg(target_os = "macos")]
640    fn test_macos_maxfilesperproc_returns_positive() {
641        let result = super::macos_maxfilesperproc();
642        assert!(
643            result.is_some(),
644            "macos_maxfilesperproc() should return Some on macOS"
645        );
646        assert!(
647            result.unwrap() > 0,
648            "kern.maxfilesperproc should be positive"
649        );
650    }
651}