async_file_watcher_raw/
async_file_watcher_raw.rs

1use std::{
2    ffi::CStr,
3    fs,
4    os::raw::{c_uint, c_void},
5    path::PathBuf,
6    time::{Duration, SystemTime},
7};
8
9use wolfram_library_link::{
10    self as wll, rtl,
11    sys::{self, mint, MArgument, LIBRARY_FUNCTION_ERROR, LIBRARY_NO_ERROR},
12};
13
14struct FileWatcherArgs {
15    pause_interval_ms: u64,
16    path: PathBuf,
17}
18
19/// Start an asynchronous task that will watch for modifications to a file.
20///
21/// See `RustLink/Tests/AsyncExamples.wlt` for example usage of this function.
22#[no_mangle]
23pub extern "C" fn start_file_watcher(
24    lib_data: sys::WolframLibraryData,
25    arg_count: mint,
26    args: *mut MArgument,
27    res: MArgument,
28) -> c_uint {
29    let args = unsafe { std::slice::from_raw_parts(args, arg_count as usize) };
30
31    if args.len() != 2 {
32        return LIBRARY_FUNCTION_ERROR;
33    }
34
35    if unsafe { wll::initialize(lib_data) }.is_err() {
36        return LIBRARY_FUNCTION_ERROR;
37    }
38
39    let task_arg = unsafe {
40        FileWatcherArgs {
41            pause_interval_ms: u64::try_from(*args[0].integer)
42                .expect("i64 interval overflows u64"),
43            path: {
44                let cstr = CStr::from_ptr(*args[1].utf8string);
45                match cstr.to_str() {
46                    Ok(s) => PathBuf::from(s),
47                    Err(_) => return LIBRARY_FUNCTION_ERROR,
48                }
49            },
50        }
51    };
52
53    // FIXME: This box is being leaked. Where is an appropriate place to drop it?
54    let task_arg = Box::into_raw(Box::new(task_arg)) as *mut c_void;
55
56    // Spawn a new thread, which will run in the background and check for file
57    // modifications.
58    unsafe {
59        let task_id: mint = wll::rtl::createAsynchronousTaskWithThread(
60            Some(file_watch_thread_function),
61            task_arg,
62        );
63        *res.integer = task_id;
64    }
65
66    LIBRARY_NO_ERROR
67}
68
69/// This function is called from the spawned background thread.
70extern "C" fn file_watch_thread_function(async_object_id: mint, task_arg: *mut c_void) {
71    let task_arg = task_arg as *mut FileWatcherArgs;
72    let task_arg: &FileWatcherArgs = unsafe { &*task_arg };
73
74    let FileWatcherArgs {
75        pause_interval_ms,
76        ref path,
77    } = *task_arg;
78
79    let mut prev_changed: Option<SystemTime> = fs::metadata(path)
80        .and_then(|metadata| metadata.modified())
81        .ok();
82
83    // Stateful closure which checks if the file at `path` has been modified since the
84    // last time this closure was called (and `prev_changed was updated). Using a closure
85    // simplifies the control flow in the main `loop` below, which should sleep on every
86    // iteration regardless of how this function returns.
87    let mut check_for_modification = || -> Option<_> {
88        let changed: Option<fs::Metadata> = fs::metadata(path).ok();
89
90        let notify: Option<SystemTime> = match (&prev_changed, changed) {
91            (Some(prev), Some(latest)) => {
92                let latest: SystemTime = match latest.modified() {
93                    Ok(latest) => latest,
94                    Err(_) => return None,
95                };
96
97                if *prev != latest {
98                    prev_changed = Some(latest.clone());
99                    Some(latest)
100                } else {
101                    None
102                }
103            },
104            // TODO: Notify on file removal?
105            (Some(_prev), None) => None,
106            (None, Some(latest)) => latest.modified().ok(),
107            (None, None) => None,
108        };
109
110        let time = notify?;
111
112        let since_epoch = match time.duration_since(std::time::UNIX_EPOCH) {
113            Ok(duration) => duration,
114            Err(_) => return None,
115        };
116
117        let since_epoch = since_epoch.as_secs();
118
119        Some(since_epoch)
120    };
121
122    loop {
123        if unsafe { rtl::asynchronousTaskAliveQ(async_object_id) } == 0 {
124            break;
125        }
126
127        // Check to see if the file has been modified. If it has, raise an async event
128        // called "change", and attach the modification timestamp as event data.
129        if let Some(modification) = check_for_modification() {
130            unsafe {
131                let data_store: sys::DataStore = rtl::createDataStore();
132                rtl::DataStore_addInteger(data_store, modification as i64);
133
134                rtl::raiseAsyncEvent(
135                    async_object_id,
136                    "change\0".as_ptr() as *mut _,
137                    data_store,
138                )
139            }
140        }
141
142        // Wait for a bit before polling again for any changes to the file.
143        std::thread::sleep(Duration::from_millis(pause_interval_ms));
144    }
145}