Skip to main content

harmos_packaging/
dev.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10// Coalesce Relevant Source Changes Into Serialized, Recoverable Plugin Builds.
11
12/* === Modules === */
13
14/* === Re-Exports === */
15
16/* === Imports === */
17
18// Standard Library Imports
19use std::collections::BTreeSet;
20use std::path::PathBuf;
21use std::sync::{Arc, Mutex, mpsc};
22use std::time::{Duration, Instant};
23
24// Third Party Imports
25use notify::{Event, EventKind, RecursiveMode, Watcher};
26
27// Local Imports
28use super::metadata::{Inputs, Metadata};
29use super::{
30    Build, BuildCancellation, Error,
31    build::{build_resolved, validate_backend},
32};
33use crate::config::{BuildSettings, DevSettings};
34
35/* === Constants === */
36
37/* === Macros === */
38
39/* === Type Aliases === */
40
41/* === Structs === */
42
43/* === Enums === */
44
45pub enum DevEvent {
46    Started { attempt: u64 },
47    Finished { attempt: u64, result: Result<Build, Error> },
48}
49
50/* === Traits === */
51
52/* === Implementations === */
53
54/* === Public Functions === */
55
56pub fn dev(
57    settings: &BuildSettings, policy: DevSettings, cancellation: &BuildCancellation, mut report: impl FnMut(DevEvent) -> Result<(), Error>,
58) -> Result<(), Error> {
59    validate_backend(settings)?;
60    if policy.debounce.is_zero() || Instant::now().checked_add(policy.debounce).is_none() {
61        return Err(Error::Settings("development debounce must be positive and representable".into()));
62    }
63    let inputs = Arc::new(Mutex::new(Inputs::fallback(settings)?));
64    let failure = Arc::new(Mutex::new(None));
65    let monitored = inputs.clone();
66    let fault = failure.clone();
67    let (changes, incoming) = mpsc::sync_channel(1);
68    let mut watcher = notify::recommended_watcher(move |result: notify::Result<Event>| match result {
69        Ok(event)
70            if matches!(
71                event.kind,
72                EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) | EventKind::Any | EventKind::Other
73            ) =>
74        {
75            if event.need_rescan() || event.paths.iter().any(|path| monitored.lock().expect("watch inputs are healthy").relevant(path)) {
76                let _ = changes.try_send(());
77            }
78        }
79        Err(error) => {
80            fault.lock().expect("watch failure is healthy").get_or_insert(error);
81            let _ = changes.try_send(());
82        }
83        _ => {}
84    })?;
85    let mut watched = BTreeSet::new();
86    configure(&mut watcher, &mut watched, &inputs.lock().expect("watch inputs are healthy"))?;
87    let mut attempt = 0u64;
88    loop {
89        if cancellation.is_cancelled() {
90            return Ok(());
91        }
92        if let Some(error) = failure.lock().expect("watch failure is healthy").take() {
93            return Err(Error::Watch(error));
94        }
95        attempt = attempt.checked_add(1).ok_or_else(|| Error::Settings("development attempt counter exhausted".into()))?;
96        report(DevEvent::Started { attempt })?;
97        let resolved =
98            Metadata::read(settings, cancellation).and_then(|metadata| metadata.inputs(settings).map(|inputs| (metadata, inputs)));
99        let result = match resolved {
100            Ok((metadata, resolved)) => {
101                configure(&mut watcher, &mut watched, &resolved)?;
102                *inputs.lock().expect("watch inputs are healthy") = resolved;
103                build_resolved(settings, cancellation, metadata)
104            }
105            Err(error) => Err(error),
106        };
107        if cancellation.is_cancelled() {
108            return Ok(());
109        }
110        report(DevEvent::Finished { attempt, result })?;
111        let mut changed = false;
112        let mut settled = Instant::now();
113        loop {
114            if cancellation.is_cancelled() {
115                return Ok(());
116            }
117            if let Some(error) = failure.lock().expect("watch failure is healthy").take() {
118                return Err(Error::Watch(error));
119            }
120            if changed && Instant::now() >= settled {
121                break;
122            }
123            let wait = if changed {
124                settled.saturating_duration_since(Instant::now()).min(Duration::from_millis(20))
125            } else {
126                Duration::from_millis(20)
127            };
128            match incoming.recv_timeout(wait) {
129                Ok(()) => {
130                    changed = true;
131                    settled = Instant::now() + policy.debounce;
132                }
133                Err(mpsc::RecvTimeoutError::Timeout) => {}
134                Err(mpsc::RecvTimeoutError::Disconnected) => {
135                    return Err(Error::Settings("source watcher stopped unexpectedly".into()));
136                }
137            }
138        }
139    }
140}
141
142/* === Private Functions === */
143
144fn configure(watcher: &mut impl Watcher, watched: &mut BTreeSet<PathBuf>, inputs: &Inputs) -> Result<(), Error> {
145    // Watch Source Directories Without Recursively Entering Generated Trees.
146    let mut selected = BTreeSet::new();
147    let mut pending: Vec<_> = inputs.roots.iter().cloned().collect();
148    while let Some(directory) = pending.pop() {
149        if !inputs.relevant(&directory) || !selected.insert(directory.clone()) {
150            continue;
151        }
152        for entry in std::fs::read_dir(directory)? {
153            let entry = entry?;
154            if entry.file_type()?.is_dir() {
155                pending.push(entry.path());
156            }
157        }
158    }
159    for root in &inputs.roots {
160        if let Some(parent) = root.parent() {
161            selected.insert(parent.to_owned());
162        }
163    }
164    for file in &inputs.files {
165        let mut parent = file.parent();
166        while let Some(directory) = parent {
167            if directory.is_dir() {
168                selected.insert(directory.to_owned());
169                break;
170            }
171            parent = directory.parent();
172        }
173    }
174    // Reattach Watches Removed By The Backend When A Directory Was Recreated.
175    for directory in &selected {
176        watcher.watch(directory, RecursiveMode::NonRecursive)?;
177    }
178    for directory in watched.difference(&selected) {
179        if let Err(error) = watcher.unwatch(directory)
180            && !matches!(error.kind, notify::ErrorKind::WatchNotFound)
181        {
182            return Err(Error::Watch(error));
183        }
184    }
185    *watched = selected;
186    Ok(())
187}
188
189/* === Tests === */