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