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 message::Message,
11 wave_source::{LoadProgress, LoadProgressStatus},
12};
13
14impl SystemState {
15 pub(crate) fn handle_batch_commands(&mut self) {
17 let mut should_exit = false;
18 while self.can_start_batch_command() {
20 if let Some(cmd) = self.batch_messages.pop_front() {
21 if matches!(cmd, Message::Exit) {
22 should_exit = true;
23 }
24 info!("Applying batch command: {cmd:?}");
25 self.update(cmd);
26 } else {
27 break; }
29 }
30
31 if !self.batch_messages_completed
33 && self.batch_messages.is_empty()
34 && self.can_start_batch_command()
35 {
36 self.batch_messages_completed = true;
37
38 if should_exit {
39 info!("Exiting due to batch command");
40 let sender = self.channels.msg_sender.clone();
41 checked_send(&sender, Message::Exit);
42 }
43 }
44 }
45
46 pub(crate) fn can_start_batch_command(&self) -> bool {
48 self.progress_tracker.is_none()
50 }
51
52 #[cfg(test)]
54 pub(crate) fn batch_commands_completed(&self) -> bool {
55 debug_assert!(
56 self.batch_messages_completed || !self.batch_messages.is_empty(),
57 "completed implies no commands"
58 );
59 self.batch_messages_completed
60 }
61
62 pub fn add_batch_commands<I: IntoIterator<Item = String>>(&mut self, commands: I) {
63 let messages = self.parse_batch_commands(commands);
64 self.add_batch_messages(messages);
65 }
66
67 #[inline(always)]
68 pub fn add_batch_messages<I: IntoIterator<Item = Message>>(&mut self, messages: I) {
69 messages
70 .into_iter()
71 .for_each(|msg| self.add_batch_message(msg));
72 }
73
74 #[inline(always)]
75 pub fn add_batch_message(&mut self, msg: Message) {
76 self.batch_messages.push_back(msg);
77 self.batch_messages_completed = false;
78 }
79
80 #[inline(always)]
81 fn parse_batch_commands<I: IntoIterator<Item = String>>(&mut self, cmds: I) -> Vec<Message> {
82 trace!("Parsing batch commands");
83
84 cmds
85 .into_iter()
86 .enumerate()
88 .map(|(no, line)| {
90 trace!("{no: >2} {line}");
91 (no, line)
92 })
93 .map(|(no, line)| (no + 1, line))
95 .map(|(no, line)| (no, line.trim().to_string()))
96 .map(|(no, line)| (no, line.split('#').next().unwrap().to_string()))
98 .filter(|(_no, line)| !line.is_empty())
99 .flat_map(|(no, line)| {
100 line.split(';')
101 .map(|cmd| (no, cmd.trim().to_string()))
102 .collect::<Vec<_>>()
103 })
104 .filter_map(|(no, command)| {
105 if let Some(path_str) = command.strip_prefix("run_command_file ") {
106 #[cfg(not(target_arch = "wasm32"))]
110 {
111 let path_str = path_str.trim().trim_matches('"'); if path_str.is_empty() {
113 error!("Empty path in run_command_file on line {no}");
114 return None;
115 }
116 match Utf8PathBuf::from_path_buf(path_str.into()) {
117 Ok(utf8_path) => {
118 self.add_batch_commands(read_command_file(&utf8_path));
119 }
120 Err(_) => {
121 error!("Invalid UTF-8 path in run_command_file on line {no}: {path_str}");
122 }
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 Some(Message::ExecuteBatchCommand { line: no, command })
130 }
131 })
132 .collect::<Vec<_>>()
133 }
134
135 pub(crate) fn load_commands_from_url(&mut self, url: String) {
136 #[cfg(all(not(target_arch = "wasm32"), feature = "https"))]
137 crate::async_util::ensure_rustls_crypto_provider();
138 let sender = self.channels.msg_sender.clone();
139 let url_ = url.clone();
140 perform_async_work(async move {
141 let maybe_response = reqwest::get(&url)
142 .map(|e| e.with_context(|| format!("Failed to fetch {url}")))
143 .await
144 .and_then(|response| {
145 response.error_for_status().with_context(|| {
146 format!("Failed to fetch {url}: server returned error status")
147 })
148 });
149 let response: reqwest::Response = match maybe_response {
150 Ok(r) => r,
151 Err(e) => {
152 error!("{e:?}");
153 return;
154 }
155 };
156
157 let bytes = response
159 .bytes()
160 .map(|e| e.with_context(|| format!("Failed to download {url}")))
161 .await;
162
163 let bytes = match bytes {
164 Ok(bytes) => bytes,
165 Err(e) => {
166 error!("{e:?}");
167 return;
168 }
169 };
170 checked_send(&sender, Message::CommandFileDownloaded(url, bytes));
171 });
172
173 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::Downloading(url_)));
174 }
175}
176
177#[must_use]
178pub fn read_command_file(cmd_file: &Utf8PathBuf) -> Vec<String> {
179 std::fs::read_to_string(cmd_file)
180 .map_err(|e| error!("Failed to read commands from {cmd_file}. {e:#?}"))
181 .ok()
182 .map(|file_content| file_content.lines().map(str::to_string).collect())
183 .unwrap_or_default()
184}
185
186#[must_use]
187pub fn read_command_bytes(bytes: Vec<u8>) -> Vec<String> {
188 String::from_utf8(bytes)
189 .map_err(|e| error!("Failed to read commands from file. {e:#?}"))
190 .ok()
191 .map(|file_content| file_content.lines().map(str::to_string).collect())
192 .unwrap_or_default()
193}