libsurfer/
batch_commands.rs1use camino::Utf8PathBuf;
2use eyre::WrapErr 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 pub(crate) fn handle_batch_commands(&mut self) {
19 let mut should_exit = false;
20 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; }
31 }
32
33 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 pub(crate) fn can_start_batch_command(&self) -> bool {
50 self.progress_tracker.is_none()
52 }
53
54 #[cfg(test)]
56 pub(crate) 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 messages = self.parse_batch_commands(commands);
66 self.add_batch_messages(messages);
67 }
68
69 #[inline(always)]
70 pub fn add_batch_messages<I: IntoIterator<Item = Message>>(&mut self, messages: I) {
71 messages
72 .into_iter()
73 .for_each(|msg| self.add_batch_message(msg));
74 }
75
76 #[inline(always)]
77 pub fn add_batch_message(&mut self, msg: Message) {
78 self.batch_messages.push_back(msg);
79 self.batch_messages_completed = false;
80 }
81
82 #[inline(always)]
83 fn parse_batch_commands<I: IntoIterator<Item = String>>(&mut self, cmds: I) -> Vec<Message> {
84 trace!("Parsing batch commands");
85
86 cmds
87 .into_iter()
88 .enumerate()
90 .map(|(no, line)| {
92 trace!("{no: >2} {line}");
93 (no, line)
94 })
95 .map(|(no, line)| (no + 1, line))
97 .map(|(no, line)| (no, line.trim().to_string()))
98 .map(|(no, line)| (no, line.split('#').next().unwrap().to_string()))
100 .filter(|(_no, line)| !line.is_empty())
101 .flat_map(|(no, line)| {
102 line.split(';')
103 .map(|cmd| (no, cmd.trim().to_string()))
104 .collect::<Vec<_>>()
105 })
106 .filter_map(|(no, command)| {
107 if let Some(path_str) = command.strip_prefix("run_command_file ") {
108 #[cfg(not(target_arch = "wasm32"))]
112 {
113 let path_str = path_str.trim().trim_matches('"'); if path_str.is_empty() {
115 error!("Empty path in run_command_file on line {no}");
116 return None;
117 }
118 match Utf8PathBuf::from_path_buf(path_str.into()) {
119 Ok(utf8_path) => {
120 self.add_batch_commands(read_command_file(&utf8_path));
121 }
122 Err(_) => {
123 error!("Invalid UTF-8 path in run_command_file on line {no}: {path_str}");
124 }
125 }
126 }
127 #[cfg(target_arch = "wasm32")]
128 error!("Cannot use run_command_file in command files running on WASM");
129 None
130 } else {
131 parse_command(&command, get_parser(self))
132 .map_err(|e| {
133 error!("Error on batch commands line {no}: {e:#?}");
134 e
135 })
136 .ok()
137 }
138 })
139 .collect::<Vec<_>>()
140 }
141
142 pub(crate) fn load_commands_from_url(&mut self, url: String) {
143 let sender = self.channels.msg_sender.clone();
144 let url_ = url.clone();
145 perform_async_work(async move {
146 let maybe_response = reqwest::get(&url)
147 .map(|e| e.with_context(|| format!("Failed to fetch {url}")))
148 .await
149 .and_then(|response| {
150 response.error_for_status().with_context(|| {
151 format!("Failed to fetch {url}: server returned error status")
152 })
153 });
154 let response: reqwest::Response = match maybe_response {
155 Ok(r) => r,
156 Err(e) => {
157 checked_send(&sender, Message::Error(e));
158 return;
159 }
160 };
161
162 let bytes = response
164 .bytes()
165 .map(|e| e.with_context(|| format!("Failed to download {url}")))
166 .await;
167
168 let msg = match bytes {
169 Ok(b) => Message::CommandFileDownloaded(url, b),
170 Err(e) => Message::Error(e),
171 };
172 checked_send(&sender, msg);
173 });
174
175 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::Downloading(url_)));
176 }
177}
178
179#[must_use]
180pub fn read_command_file(cmd_file: &Utf8PathBuf) -> Vec<String> {
181 std::fs::read_to_string(cmd_file)
182 .map_err(|e| error!("Failed to read commands from {cmd_file}. {e:#?}"))
183 .ok()
184 .map(|file_content| file_content.lines().map(str::to_string).collect())
185 .unwrap_or_default()
186}
187
188#[must_use]
189pub fn read_command_bytes(bytes: Vec<u8>) -> Vec<String> {
190 String::from_utf8(bytes)
191 .map_err(|e| error!("Failed to read commands from file. {e:#?}"))
192 .ok()
193 .map(|file_content| file_content.lines().map(str::to_string).collect())
194 .unwrap_or_default()
195}