async_file_watcher/
async_file_watcher.rs

1use std::{
2    fs,
3    path::PathBuf,
4    time::{Duration, SystemTime},
5};
6
7use wolfram_library_link::{self as wll, sys::mint, AsyncTaskObject, DataStore};
8
9/// Start an asynchronous task that will watch for modifications to a file.
10///
11/// See `RustLink/Tests/AsyncExamples.wlt` for example usage of this function.
12#[wll::export]
13fn start_file_watcher(pause_interval_ms: mint, path: String) -> mint {
14    let pause_interval_ms =
15        u64::try_from(pause_interval_ms).expect("mint interval overflows u64");
16
17    let path = PathBuf::from(path);
18
19    // Spawn a new thread, which will run in the background and check for file
20    // modifications.
21    let task = AsyncTaskObject::spawn_with_thread(move |task: AsyncTaskObject| {
22        file_watch_thread_function(task, pause_interval_ms, &path);
23    });
24
25    task.id()
26}
27
28/// This function is called from the spawned background thread.
29fn file_watch_thread_function(
30    task: AsyncTaskObject,
31    pause_interval_ms: u64,
32    path: &PathBuf,
33) {
34    let mut prev_changed: Option<SystemTime> = fs::metadata(path)
35        .and_then(|metadata| metadata.modified())
36        .ok();
37
38    // Stateful closure which checks if the file at `path` has been modified since the
39    // last time this closure was called (and `prev_changed was updated). Using a closure
40    // simplifies the control flow in the main `loop` below, which should sleep on every
41    // iteration regardless of how this function returns.
42    let mut check_for_modification = || -> Option<_> {
43        let changed: Option<fs::Metadata> = fs::metadata(path).ok();
44
45        let notify: Option<SystemTime> = match (&prev_changed, changed) {
46            (Some(prev), Some(latest)) => {
47                let latest: SystemTime = match latest.modified() {
48                    Ok(latest) => latest,
49                    Err(_) => return None,
50                };
51
52                if *prev != latest {
53                    prev_changed = Some(latest.clone());
54                    Some(latest)
55                } else {
56                    None
57                }
58            },
59            // TODO: Notify on file removal?
60            (Some(_prev), None) => None,
61            (None, Some(latest)) => latest.modified().ok(),
62            (None, None) => None,
63        };
64
65        let time = notify?;
66
67        let since_epoch = match time.duration_since(std::time::UNIX_EPOCH) {
68            Ok(duration) => duration,
69            Err(_) => return None,
70        };
71
72        let since_epoch = since_epoch.as_secs();
73
74        Some(since_epoch)
75    };
76
77    loop {
78        if !task.is_alive() {
79            break;
80        }
81
82        // Check to see if the file has been modified. If it has, raise an async event
83        // called "change", and attach the modification timestamp as event data.
84        if let Some(modification) = check_for_modification() {
85            let mut data = DataStore::new();
86            data.add_i64(modification as i64);
87
88            task.raise_async_event("change", data);
89        }
90
91        // Wait for a bit before polling again for any changes to the file.
92        std::thread::sleep(Duration::from_millis(pause_interval_ms));
93    }
94}