Skip to main content

pacenotes_installer\selftest/
download_check.rs

1/// `--selftest-download <game> [--server URL] [--token T] [--out FILE] [--largest]`:
2/// downloads that game's smallest package (or `--largest`) into a throwaway
3/// cache, exercising stream, hash, verify, promote and index without the wizard.
4pub struct DownloadSelfTest {
5    game: String,
6    server: String,
7    token: Option<String>,
8    out: Option<std::path::PathBuf>,
9    largest: bool,
10}
11
12impl DownloadSelfTest {
13    pub fn from_args() -> Option<DownloadSelfTest> {
14        let mut arguments = std::env::args().skip(1);
15        let mut game = None;
16        let mut server = None;
17        let mut token = None;
18        let mut out = None;
19        let mut largest = false;
20        while let Some(argument) = arguments.next() {
21            match argument.as_str() {
22                "--selftest-download" => game = arguments.next(),
23                "--server" => server = arguments.next(),
24                "--token" => token = arguments.next(),
25                "--out" => out = arguments.next().map(std::path::PathBuf::from),
26                "--largest" => largest = true,
27                _ => {}
28            }
29        }
30        game.map(|game| DownloadSelfTest {
31            game,
32            server: server.unwrap_or_else(|| crate::DEFAULT_SERVER.to_string()),
33            token,
34            out,
35            largest,
36        })
37    }
38
39    /// The process exit code: 0 when the package arrived verified and indexed.
40    pub fn run(&self) -> i32 {
41        let (exit_code, report) = match self.pick_package() {
42            Ok(package) => self.download(&package),
43            Err(message) => (
44                1,
45                format!("RESULT: FAIL\nSTAGE: manifest\nMESSAGE: {message}\n"),
46            ),
47        };
48        super::report::deliver(self.out.as_deref(), &report, exit_code)
49    }
50
51    fn pick_package(&self) -> Result<pacenotes_shared::PackageInfo, String> {
52        let cancel = std::sync::atomic::AtomicBool::new(false);
53        let api = crate::api::Api::new(&self.server, self.token.clone());
54        let manifest = api
55            .fetch_manifest(&self.game, &cancel)
56            .map_err(|e| e.message)?;
57        let mut packages: Vec<_> = manifest
58            .packages
59            .into_iter()
60            .filter(|package| package.game == self.game)
61            .collect();
62        packages.sort_by_key(|package| package.size);
63        let picked = if self.largest {
64            packages.pop()
65        } else {
66            packages.into_iter().next()
67        };
68        picked.ok_or_else(|| format!("no packages for game {}", self.game))
69    }
70
71    /// Downloads into a throwaway cache, so a check never disturbs the real one.
72    fn download(&self, package: &pacenotes_shared::PackageInfo) -> (i32, String) {
73        let cancel = std::sync::atomic::AtomicBool::new(false);
74        let api = crate::api::Api::new(&self.server, self.token.clone());
75        let root = std::env::temp_dir().join(format!("pacenotes-selftest-{}", std::process::id()));
76        let mut cache = pacenotes_installer::cache::Cache::open(&root);
77
78        let started = std::time::Instant::now();
79        let mut bytes_read = 0u64;
80        let mut progress = |read: u64, _: Option<u64>| bytes_read = read;
81        let mut retries = 0;
82        let outcome = crate::package_download::download_retrying(
83            &api,
84            package,
85            &mut cache,
86            &mut progress,
87            &cancel,
88            &mut |_, _| retries += 1,
89        );
90        // Whole milliseconds, since formatting a float links ~16 kB.
91        let millis = started.elapsed().as_millis();
92
93        let result = match outcome {
94            Ok(()) => (
95                0,
96                format!(
97                    "RESULT: OK\nPACKAGE: {}\nSIZE: {}\nREAD: {bytes_read}\nSHA256: {}\nVERIFIED: yes\n\
98                     PAYLOAD_ON_DISK: {}\nINDEXED: {}\nRETRIES: {retries}\nMILLIS: {millis}\n",
99                    package.download_path(),
100                    package.size,
101                    package.sha256,
102                    cache.has_payload(&package.sha256),
103                    cache.get(&package.download_path()).is_some(),
104                ),
105            ),
106            Err(e) => (
107                1,
108                format!(
109                    "RESULT: FAIL\nSTAGE: download\nPACKAGE: {}\nSIZE: {}\nRETRIES: {retries}\nMESSAGE: {}\n",
110                    package.download_path(),
111                    package.size,
112                    e.message
113                ),
114            ),
115        };
116        let _ = std::fs::remove_dir_all(&root);
117        result
118    }
119}