Skip to main content

libsurfer/
batch_commands.rs

1use camino::Utf8PathBuf;
2use eyre::Context as _;
3use futures::FutureExt as _;
4use tracing::{error, info, trace};
5
6use crate::{
7    SystemState,
8    async_util::perform_async_work,
9    command_parser::get_parser,
10    fzcmd::parse_command,
11    message::Message,
12    wave_source::{LoadProgress, LoadProgressStatus},
13};
14
15impl SystemState {
16    /// After user messages are addressed, we try to execute batch commands as they are ready to run
17    pub(crate) fn handle_batch_commands(&mut self) {
18        let mut should_exit = false;
19        // we only execute commands while we aren't waiting for background operations to complete
20        while self.can_start_batch_command() {
21            if let Some(cmd) = self.batch_messages.pop_front() {
22                if matches!(cmd, Message::Exit) {
23                    should_exit = true;
24                }
25                info!("Applying batch command: {cmd:?}");
26                self.update(cmd);
27            } else {
28                break; // no more messages
29            }
30        }
31
32        // if there are no messages and all operations have completed, we are done
33        if !self.batch_messages_completed
34            && self.batch_messages.is_empty()
35            && self.can_start_batch_command()
36        {
37            self.batch_messages_completed = true;
38
39            if should_exit {
40                info!("Exiting due to batch command");
41                let sender = self.channels.msg_sender.clone();
42                if let Err(e) = sender.send(Message::Exit) {
43                    error!("Failed to send exit message: {e}");
44                }
45            }
46        }
47    }
48
49    /// Returns whether it is OK to start a new batch command.
50    pub(crate) fn can_start_batch_command(&self) -> bool {
51        // if the progress tracker is none -> all operations have completed
52        self.progress_tracker.is_none()
53    }
54
55    /// Returns true once all batch commands have been completed and their effects are all executed.
56    pub fn batch_commands_completed(&self) -> bool {
57        debug_assert!(
58            self.batch_messages_completed || !self.batch_messages.is_empty(),
59            "completed implies no commands"
60        );
61        self.batch_messages_completed
62    }
63
64    pub fn add_batch_commands<I: IntoIterator<Item = String>>(&mut self, commands: I) {
65        let parsed = self.parse_batch_commands(commands);
66        for msg in parsed {
67            self.batch_messages.push_back(msg);
68            self.batch_messages_completed = false;
69        }
70    }
71
72    pub fn add_batch_messages<I: IntoIterator<Item = Message>>(&mut self, messages: I) {
73        for msg in messages {
74            self.batch_messages.push_back(msg);
75            self.batch_messages_completed = false;
76        }
77    }
78
79    pub fn add_batch_message(&mut self, msg: Message) {
80        self.add_batch_messages([msg]);
81    }
82
83    pub fn parse_batch_commands<I: IntoIterator<Item = String>>(
84        &mut self,
85        cmds: I,
86    ) -> Vec<Message> {
87        trace!("Parsing batch commands");
88
89        cmds
90            .into_iter()
91            // Add line numbers
92            .enumerate()
93            // trace
94            .map(|(no, line)| {
95                trace!("{no: >2} {line}");
96                (no, line)
97            })
98            // Make the line numbers start at 1 as is tradition
99            .map(|(no, line)| (no + 1, line))
100            .map(|(no, line)| (no, line.trim().to_string()))
101            // NOTE: Safe unwrap. Split will always return one element
102            .map(|(no, line)| (no, line.split('#').next().unwrap().to_string()))
103            .filter(|(_no, line)| !line.is_empty())
104            .flat_map(|(no, line)| {
105                line.split(';')
106                    .map(|cmd| (no, cmd.trim().to_string()))
107                    .collect::<Vec<_>>()
108            })
109            .filter_map(|(no, command)| {
110                if command.starts_with("run_command_file ") {
111                    // Load commands from other file in place, otherwise they will be
112                    // loaded when the corresponding message is processed, leading to
113                    // a different position in the processing than expected.
114                    #[cfg(not(target_arch = "wasm32"))]
115                    {
116                        if let Some(path_str) = command.split_ascii_whitespace().nth(1) {
117                            match Utf8PathBuf::from_path_buf(path_str.into()) {
118                                Ok(utf8_path) => {
119                                    self.add_batch_commands(read_command_file(&utf8_path));
120                                }
121                                Err(_) => {
122                                    error!("Invalid UTF-8 path in run_command_file on line {no}: {path_str}");
123                                }
124                            }
125                        } else {
126                            error!("Missing file path in run_command_file command on line {no}");
127                        }
128                    }
129                    #[cfg(target_arch = "wasm32")]
130                    error!("Cannot use run_command_file in command files running on WASM");
131                    None
132                } else {
133                    parse_command(&command, get_parser(self))
134                        .map_err(|e| {
135                            error!("Error on batch commands line {no}: {e:#?}");
136                            e
137                        })
138                        .ok()
139                }
140            })
141            .collect::<Vec<_>>()
142    }
143
144    pub fn load_commands_from_url(&mut self, url: String) {
145        let sender = self.channels.msg_sender.clone();
146        let url_ = url.clone();
147        perform_async_work(async move {
148            let maybe_response = reqwest::get(&url)
149                .map(|e| e.with_context(|| format!("Failed fetch download {url}")))
150                .await;
151            let response: reqwest::Response = match maybe_response {
152                Ok(r) => r,
153                Err(e) => {
154                    if let Err(e) = sender.send(Message::Error(e)) {
155                        error!("Failed to send error message: {e}");
156                    }
157                    return;
158                }
159            };
160
161            // load the body to get at the file
162            let bytes = response
163                .bytes()
164                .map(|e| e.with_context(|| format!("Failed to download {url}")))
165                .await;
166
167            let msg = match bytes {
168                Ok(b) => Message::CommandFileDownloaded(url, b),
169                Err(e) => Message::Error(e),
170            };
171            if let Err(e) = sender.send(msg) {
172                error!("Failed to send message: {e}");
173            }
174        });
175
176        self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::Downloading(url_)));
177    }
178}
179
180#[must_use]
181pub fn read_command_file(cmd_file: &Utf8PathBuf) -> Vec<String> {
182    std::fs::read_to_string(cmd_file)
183        .map_err(|e| error!("Failed to read commands from {cmd_file}. {e:#?}"))
184        .ok()
185        .map(|file_content| file_content.lines().map(str::to_string).collect())
186        .unwrap_or_default()
187}
188
189#[must_use]
190pub fn read_command_bytes(bytes: Vec<u8>) -> Vec<String> {
191    String::from_utf8(bytes)
192        .map_err(|e| error!("Failed to read commands from file. {e:#?}"))
193        .ok()
194        .map(|file_content| file_content.lines().map(str::to_string).collect())
195        .unwrap_or_default()
196}