libsurfer/
batch_commands.rs

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