1use crate::util::pause_on_error;
2use std::path::PathBuf;
3use std::time::Duration;
4use uxn_tal_defined::v1::{ProtocolParseResult, ProtocolQueryVarVar, ProtocolVarVar};
5use uxn_tal_defined::EmulatorLauncher;
6
7pub fn resolve_arg_url(arg_value: &str, cache_dir: Option<&std::path::Path>) -> String {
10 println!("[DEBUG] resolve_arg_url called with: {}", arg_value);
11 if arg_value.starts_with("@http://") || arg_value.starts_with("@https://") {
13 let url_to_check = &arg_value[1..]; println!("[DEBUG] Processing @http URL: {}", url_to_check);
15
16 match download_url_to_cache(url_to_check, cache_dir) {
17 Ok(local_path) => {
18 println!("[DEBUG] Downloaded and saved to: {}", local_path.display());
19 let filename = local_path
21 .file_name()
22 .and_then(|name| name.to_str())
23 .unwrap_or("downloaded_file");
24 println!("[DEBUG] Returning filename: {}", filename);
25 filename.to_string()
26 }
27 Err(e) => {
28 eprintln!("Warning: Failed to download {}: {}", url_to_check, e);
29 eprintln!("Using original URL as argument");
30 arg_value.to_string()
31 }
32 }
33 } else {
34 println!("[DEBUG] Not an @http URL, returning as-is: {}", arg_value);
35 arg_value.to_string()
37 }
38}
39
40fn download_url_to_cache(
42 url: &str,
43 cache_dir: Option<&std::path::Path>,
44) -> Result<PathBuf, Box<dyn std::error::Error>> {
45 let target_dir = if let Some(dir) = cache_dir {
47 dir.to_path_buf()
48 } else {
49 std::env::temp_dir()
50 };
51
52 let filename = url
54 .rsplit('/')
55 .find(|s| !s.is_empty())
56 .unwrap_or("downloaded_file");
57
58 let local_path = target_dir.join(filename);
59
60 if local_path.exists() {
62 println!("Using cached file: {}", local_path.display());
63 return Ok(local_path);
64 }
65
66 println!(
68 "Downloading !arg1 file: {} -> {}",
69 url,
70 local_path.display()
71 );
72
73 let bytes = crate::fetch::downloader::http_get(url)?;
74 crate::fetch::downloader::write_bytes(&local_path, &bytes)?;
75
76 println!("Downloaded !arg1 file successfully");
77 Ok(local_path)
78}
79
80pub fn spawn_emulator_with_logging(
82 mut cmd: std::process::Command,
83 verbose: bool,
84) -> Result<(), String> {
85 if verbose {
86 println!("[DEBUG] Spawning emulator: {:?}", cmd);
87 }
88
89 let program = cmd.get_program().to_string_lossy();
91 let args: Vec<String> = cmd
92 .get_args()
93 .map(|a| a.to_string_lossy().to_string())
94 .collect();
95 if args.is_empty() {
96 println!("Launched emulator: {}", program);
97 } else {
98 println!("Launched emulator: {} {}", program, args.join(" "));
99 }
100
101 match cmd.spawn() {
102 Ok(_) => Ok(()),
103 Err(e) => {
104 let error_msg = format!("Failed to spawn emulator: {}", e);
105 eprintln!("{}", error_msg);
106 pause_on_error();
107 Err(error_msg)
108 }
109 }
110}
111
112pub fn spawn_emulator_with_stdin_and_logging(
114 mut cmd: std::process::Command,
115 result: &ProtocolParseResult,
116 verbose: bool,
117) -> Result<(), String> {
118 if verbose {
119 println!("[DEBUG] Spawning emulator: {:?}", cmd);
120 }
121
122 let stdin_content = if let Some(stdin_var) = result.bang_vars.get("stdin") {
124 match &stdin_var.value {
125 ProtocolQueryVarVar::String(stdin_value) => {
126 println!("Found !stdin variable: {}", stdin_value);
127 match fetch_stdin_content(stdin_value) {
128 Ok(mut content) => {
129 println!("Successfully fetched {} bytes for stdin", content.len());
130 content.push(0x1A);
132 Some(content)
133 }
134 Err(e) => {
135 eprintln!("Failed to fetch stdin content: {}", e);
136 return Err(format!("Failed to fetch stdin content: {}", e));
137 }
138 }
139 }
140 _ => {
141 eprintln!("!stdin variable is not a string");
142 return Err("!stdin variable must be a string".to_string());
143 }
144 }
145 } else {
146 None
147 };
148
149 if stdin_content.is_some() {
151 cmd.stdin(std::process::Stdio::piped());
152 }
153
154 let program = cmd.get_program().to_string_lossy();
156 let args: Vec<String> = cmd
157 .get_args()
158 .map(|a| a.to_string_lossy().to_string())
159 .collect();
160 if args.is_empty() {
161 println!("Launched emulator: {}", program);
162 } else {
163 println!("Launched emulator: {} {}", program, args.join(" "));
164 }
165
166 match cmd.spawn() {
167 Ok(mut child) => {
168 if let Some(content) = stdin_content {
170 if let Some(mut stdin) = child.stdin.take() {
171 println!("Piping {} bytes to emulator stdin", content.len());
172 std::thread::spawn(move || {
173 use std::io::Write;
174 std::thread::sleep(std::time::Duration::from_millis(200));
176 if let Err(e) = stdin.write_all(&content) {
177 eprintln!("Failed to write to stdin: {}", e);
178 }
179 if let Err(e) = stdin.flush() {
180 eprintln!("Failed to flush stdin: {}", e);
181 }
182 });
184 } else {
185 eprintln!("Failed to get stdin handle for emulator");
186 }
187 }
188
189 Ok(())
190 }
191 Err(e) => {
192 let error_msg = format!("Failed to spawn emulator: {}", e);
193 eprintln!("{}", error_msg);
194 pause_on_error();
195 Err(error_msg)
196 }
197 }
198}
199
200pub fn spawn_emulator_with_timeout(
204 launcher: &dyn EmulatorLauncher,
205 result: &ProtocolParseResult,
206 mut cmd: std::process::Command,
207 emulator_path: &std::path::Path,
208 verbose: bool,
209) -> Result<(), String> {
210 if verbose {
211 println!("[DEBUG] Spawning emulator with timeout support: {:?}", cmd);
212 }
213
214 let stdin_content = if let Some(stdin_var) = result.bang_vars.get("stdin") {
216 match &stdin_var.value {
217 ProtocolQueryVarVar::String(stdin_value) => {
218 println!("Found !stdin variable: {}", stdin_value);
219 match fetch_stdin_content(stdin_value) {
220 Ok(mut content) => {
221 println!("Successfully fetched {} bytes for stdin", content.len());
222 content.push(0x1A);
224 Some(content)
225 }
226 Err(e) => {
227 eprintln!("Failed to fetch stdin content: {}", e);
228 return Err(format!("Failed to fetch stdin content: {}", e));
229 }
230 }
231 }
232 _ => {
233 eprintln!("!stdin variable is not a string");
234 return Err("!stdin variable must be a string".to_string());
235 }
236 }
237 } else {
238 None
239 };
240
241 if stdin_content.is_some() {
243 cmd.stdin(std::process::Stdio::piped());
244 }
245
246 let timeout_seconds = get_timeout_from_result(result);
248
249 if launcher.timeout_follow_forceexit(emulator_path) {
250 if let Some(timeout_seconds) = timeout_seconds {
251 let timeout_duration = Duration::from_secs_f32(timeout_seconds);
253
254 match cmd.spawn() {
255 Ok(mut child) => {
256 if let Some(content) = stdin_content {
258 if let Some(mut stdin) = child.stdin.take() {
259 println!("Piping {} bytes to emulator stdin", content.len());
260 std::thread::spawn(move || {
261 use std::io::Write;
262 std::thread::sleep(std::time::Duration::from_millis(200));
264 if let Err(e) = stdin.write_all(&content) {
265 eprintln!("Failed to write to stdin: {}", e);
266 }
267 if let Err(e) = stdin.flush() {
268 eprintln!("Failed to flush stdin: {}", e);
269 }
270 });
272 } else {
273 eprintln!("Failed to get stdin handle for emulator");
274 }
275 }
276
277 let program = cmd.get_program().to_string_lossy();
279 let args: Vec<String> = cmd
280 .get_args()
281 .map(|a| a.to_string_lossy().to_string())
282 .collect();
283 if args.is_empty() {
284 println!(
285 "Launched emulator: {} (manual timeout: {:.1}s)",
286 program, timeout_seconds
287 );
288 } else {
289 println!(
290 "Launched emulator: {} {} (manual timeout: {:.1}s)",
291 program,
292 args.join(" "),
293 timeout_seconds
294 );
295 }
296
297 let timeout_val = timeout_seconds;
299 let handle = std::thread::spawn(move || {
300 let start_time = std::time::Instant::now();
301
302 loop {
303 match child.try_wait() {
305 Ok(Some(_status)) => {
306 return;
308 }
309 Ok(None) => {
310 if start_time.elapsed() >= timeout_duration {
312 if let Err(e) = child.kill() {
314 eprintln!("Failed to kill emulator process: {}", e);
315 } else {
316 println!(
317 "Emulator process killed after {:.1}s timeout",
318 timeout_val
319 );
320 }
321 return;
322 }
323 }
324 Err(e) => {
325 eprintln!("Error checking process status: {}", e);
326 return;
327 }
328 }
329
330 std::thread::sleep(Duration::from_millis(100));
332 }
333 });
334
335 if let Err(e) = handle.join() {
337 eprintln!("Timeout thread panicked: {:?}", e);
338 }
339
340 Ok(())
341 }
342 Err(e) => {
343 let error_msg = format!("Failed to spawn emulator: {}", e);
344 eprintln!("{}", error_msg);
345 pause_on_error();
346 Err(error_msg)
347 }
348 }
349 } else {
350 spawn_emulator_with_stdin_and_logging(cmd, result, verbose)
352 }
353 } else {
354 spawn_emulator_with_stdin_and_logging(cmd, result, verbose)
356 }
357}
358
359fn get_timeout_from_result(result: &ProtocolParseResult) -> Option<f32> {
362 if let Some(timeout_var) = result
364 .proto_vars
365 .get("t")
366 .or(result.proto_vars.get("timeout"))
367 {
368 match timeout_var {
369 ProtocolVarVar::Float(f) => return Some(*f as f32),
370 ProtocolVarVar::Int(i) => return Some(*i as f32),
371 _ => {}
372 }
373 }
374
375 if let Some(timeout_query) = result
377 .query_vars
378 .get("t")
379 .or(result.query_vars.get("timeout"))
380 {
381 match &timeout_query.value {
382 ProtocolQueryVarVar::Float(f) => return Some(*f as f32),
383 ProtocolQueryVarVar::Int(i) => return Some(*i as f32),
384 ProtocolQueryVarVar::String(s) => {
385 if let Ok(f) = s.parse::<f32>() {
386 return Some(f);
387 }
388 }
389 _ => {}
390 }
391 }
392
393 None
394}
395
396pub fn fetch_stdin_content(stdin_value: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
399 if stdin_value.starts_with("@http://") || stdin_value.starts_with("@https://") {
400 let url = &stdin_value[1..]; println!("Downloading stdin content from: {}", url);
402 crate::fetch::downloader::http_get(url)
403 } else {
404 Ok(stdin_value.as_bytes().to_vec())
406 }
407}
408
409#[derive(Debug, Clone, Copy, PartialEq)]
410pub enum FileType {
411 Rom,
412 Orca,
413 Basic,
414 Tal,
415}
416
417pub fn detect_file_type(path: &std::path::Path) -> FileType {
419 let path_str = path.to_string_lossy();
420
421 if path_str.ends_with(".tal.txt") {
423 return FileType::Tal;
424 }
425
426 match path.extension().and_then(|ext| ext.to_str()) {
427 Some("rom") => FileType::Rom,
428 Some("orca") => FileType::Orca,
429 Some("bas") => FileType::Basic,
430 Some("tal") => FileType::Tal,
431 _ => FileType::Tal,
432 }
433}