Skip to main content

smb_server_test_dashboard/
main.rs

1//! A tiny, dependency-free HTTP server that serves MS-SMB2 test results.
2//!
3//! It reads a CSV (the source of truth for pass/fail state), exposes a REST API
4//! over it, and serves a static single-page dashboard that renders progress.
5//! It listens on the SMB port plus one so it sits next to the server under test.
6
7mod csv;
8mod http;
9mod json;
10
11use std::net::TcpListener;
12use std::path::PathBuf;
13use std::sync::Arc;
14use std::thread;
15
16/// Default SMB port the dashboard sits next to (it listens on this plus one).
17const DEFAULT_SMB_PORT: u16 = 4450;
18/// Exit code for a command-line usage error.
19const EXIT_USAGE: i32 = 2;
20
21/// Runtime configuration resolved from the command line.
22struct Config {
23    port: u16,
24    csv_path: PathBuf,
25}
26
27fn main() {
28    let config = parse_args();
29    serve(config);
30}
31
32/// Parse `--smb-port`, `--port` and `--csv`. The dashboard defaults to the SMB
33/// port (4450) plus one; an explicit `--port` overrides that.
34fn parse_args() -> Config {
35    let mut smb_port: u16 = DEFAULT_SMB_PORT;
36    let mut explicit_port: Option<u16> = None;
37    let mut csv_path = PathBuf::from("test_status.csv");
38
39    let mut args = std::env::args().skip(1);
40    while let Some(arg) = args.next() {
41        match arg.as_str() {
42            "--smb-port" => smb_port = next_u16(&mut args, "--smb-port"),
43            "--port" => explicit_port = Some(next_u16(&mut args, "--port")),
44            "--csv" => csv_path = PathBuf::from(next_value(&mut args, "--csv")),
45            "-h" | "--help" => print_help_and_exit(),
46            other => fail(&format!("unknown argument: {other}")),
47        }
48    }
49
50    Config { port: explicit_port.unwrap_or(smb_port + 1), csv_path }
51}
52
53fn next_value(args: &mut impl Iterator<Item = String>, flag: &str) -> String {
54    args.next().unwrap_or_else(|| fail(&format!("{flag} needs a value")))
55}
56
57fn next_u16(args: &mut impl Iterator<Item = String>, flag: &str) -> u16 {
58    next_value(args, flag)
59        .parse()
60        .unwrap_or_else(|_| fail(&format!("{flag} needs a number")))
61}
62
63fn print_help_and_exit() -> ! {
64    println!(
65        "smb-server-test-dashboard [--smb-port <n>] [--port <n>] [--csv <path>]\n\
66         \n\
67         Serves the MS-SMB2 test dashboard. Defaults to SMB port + 1 (4451) and\n\
68         reads/writes test_status.csv in the working directory."
69    );
70    std::process::exit(0);
71}
72
73fn fail(msg: &str) -> ! {
74    eprintln!("smb-server-test-dashboard: {msg}");
75    std::process::exit(EXIT_USAGE);
76}
77
78/// Bind the listener and hand each connection to a worker thread. The CSV path
79/// is shared read-only; every request re-reads the file so external test runs
80/// are reflected immediately.
81fn serve(config: Config) {
82    let addr = ("0.0.0.0", config.port);
83    let listener = TcpListener::bind(addr)
84        .unwrap_or_else(|e| fail(&format!("cannot bind port {}: {e}", config.port)));
85    let csv_path = Arc::new(config.csv_path);
86    println!(
87        "smb-server-test-dashboard listening on http://0.0.0.0:{} (csv: {})",
88        config.port,
89        csv_path.display()
90    );
91
92    for stream in listener.incoming() {
93        let Ok(stream) = stream else { continue };
94        let csv_path = Arc::clone(&csv_path);
95        thread::spawn(move || http::handle_connection(stream, &csv_path));
96    }
97}