Skip to main content

pacenotes_installer\worker/
apply_selection.rs

1/// Plans against the ledger and carries the plan out, once everything selected
2/// is in the cache, then runs the game's own configuration step.
3pub fn apply_selection(
4    job: &super::InstallJob,
5    cache: &pacenotes_installer::cache::Cache,
6    reporter: &super::Reporter,
7) -> Result<super::Applied, String> {
8    let mut ledger = pacenotes_installer::ledger::Ledger::open(&job.game_dir);
9    let available = cache
10        .entries()
11        .into_iter()
12        .map(|entry| (entry.key(), entry))
13        .collect();
14    let choice_categories: Vec<&str> = job.choice_categories.iter().map(String::as_str).collect();
15    let plan = pacenotes_installer::engine::plan(&pacenotes_installer::engine::PlanInput {
16        game: job.game.id(),
17        selection: &job.selection,
18        choice_categories: &choice_categories,
19        installed: ledger.packages(),
20        available: &available,
21    })
22    .map_err(|e| e.to_string())?;
23
24    let mut watcher = PostingWatcher { reporter };
25    let prepared = pacenotes_installer::engine::preflight(
26        &plan,
27        &job.game_dir,
28        cache,
29        job.facts,
30        &mut watcher,
31    )
32    .map_err(|e| e.to_string())?;
33    let overlaps = log_overlaps(&plan, &prepared, reporter);
34
35    let outcome = pacenotes_installer::engine::execute(
36        &plan,
37        &prepared,
38        &job.game_dir,
39        cache,
40        &mut ledger,
41        &mut watcher,
42    )
43    .map_err(|e| e.to_string())?;
44    report_overlaps(job, &overlaps, reporter);
45
46    let configured = if plan.is_empty() {
47        Vec::new()
48    } else {
49        configure(
50            job.game,
51            &job.game_dir,
52            &job.picks,
53            &mut ledger,
54            &mut watcher,
55        )?
56    };
57    Ok(super::Applied {
58        installed: outcome.installed,
59        uninstalled: outcome.uninstalled,
60        left_alone: outcome.left_alone,
61        configured,
62        overlaps: overlaps.len(),
63        restore_point: outcome.restore_point,
64    })
65}
66
67/// The later package wins an overlap, so it is logged rather than refused.
68fn log_overlaps(
69    plan: &pacenotes_installer::engine::Plan,
70    prepared: &pacenotes_installer::engine::Preflight,
71    reporter: &super::Reporter,
72) -> Vec<pacenotes_installer::engine::Overlap> {
73    let install_order: Vec<String> = plan.installs().iter().map(|key| key.to_string()).collect();
74    let overlaps = pacenotes_installer::engine::overlaps(&install_order, &prepared.paths());
75    for overlap in &overlaps {
76        reporter.log(format!(
77            "{} is written by more than one package ({}); the last one wins",
78            overlap.path,
79            overlap.packages.join(", ")
80        ));
81    }
82    overlaps
83}
84
85/// Sent after the install, so what is reported is what happened; a failure is
86/// only logged, since the user's install succeeded either way.
87fn report_overlaps(
88    job: &super::InstallJob,
89    overlaps: &[pacenotes_installer::engine::Overlap],
90    reporter: &super::Reporter,
91) {
92    if overlaps.is_empty() {
93        return;
94    }
95    let api = crate::api::Api::new(&job.base_url, None);
96    match api.report_conflicts(job.game.id(), overlaps, &job.cancel) {
97        Ok(()) => reporter.log("the overlapping files were reported to the server".into()),
98        Err(e) => reporter.log(format!(
99            "could not report the overlapping files — {}",
100            e.message
101        )),
102    }
103}
104
105/// Returns the files the step wrote, with their new hashes in the ledger. The
106/// step failing is logged, not fatal, since the packages are installed and a
107/// missing plugin is fixed elsewhere.
108pub(super) fn configure(
109    game: pacenotes_installer::gamedir::Game,
110    game_dir: &std::path::Path,
111    picks: &std::collections::BTreeMap<String, String>,
112    ledger: &mut pacenotes_installer::ledger::Ledger,
113    watcher: &mut PostingWatcher,
114) -> Result<Vec<String>, String> {
115    let reporter = watcher.reporter;
116    reporter.send(super::Progress {
117        phase: super::Phase::Configure,
118        item: String::new(),
119        item_done: 0,
120        item_size: 0,
121        done: 0,
122        total: 0,
123        log: None,
124    });
125    let configured = match pacenotes_installer::install::configure(game, game_dir, picks) {
126        Ok(configured) => configured,
127        Err(e) => {
128            reporter.log(format!("configuration skipped — {e}"));
129            return Ok(Vec::new());
130        }
131    };
132    for note in configured.notes {
133        reporter.log(note);
134    }
135    pacenotes_installer::engine::record_configured(game_dir, &configured.files, ledger, watcher)
136        .map_err(|e| e.to_string())?;
137    Ok(configured.files)
138}
139
140/// Posts the engine's progress to scene 6.
141pub(super) struct PostingWatcher<'a> {
142    pub(super) reporter: &'a super::Reporter,
143}
144
145impl pacenotes_installer::engine::Watcher for PostingWatcher<'_> {
146    fn phase(
147        &mut self,
148        phase: pacenotes_installer::engine::Phase,
149        item: &str,
150        done: u64,
151        total: u64,
152    ) {
153        self.reporter.send(super::Progress {
154            phase: scene_phase(phase),
155            item: item.to_string(),
156            item_done: 0,
157            item_size: 0,
158            done,
159            total,
160            log: None,
161        });
162    }
163
164    fn log(&mut self, line: &str) {
165        self.reporter.log(line.to_string());
166    }
167}
168
169/// The library's engine knows nothing of the UI, so its phases are mapped here.
170fn scene_phase(phase: pacenotes_installer::engine::Phase) -> super::Phase {
171    match phase {
172        pacenotes_installer::engine::Phase::Checking => super::Phase::Checking,
173        pacenotes_installer::engine::Phase::Uninstalling => super::Phase::Uninstall,
174        pacenotes_installer::engine::Phase::BackingUp => super::Phase::Backup,
175        pacenotes_installer::engine::Phase::Installing => super::Phase::Install,
176    }
177}