Skip to main content

uxn_tal/
emulator_utils.rs

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
7/// Download an HTTP URL to a temporary file and return the local path
8/// Returns the original string if it doesn't start with @http
9pub 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    // Only process arguments that start with @http
12    if arg_value.starts_with("@http://") || arg_value.starts_with("@https://") {
13        let url_to_check = &arg_value[1..]; // Remove the @ prefix
14        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                // Return just the filename, not the full path
20                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        // Not a @http URL, return as-is
36        arg_value.to_string()
37    }
38}
39
40/// Download a URL to the cache directory
41fn download_url_to_cache(
42    url: &str,
43    cache_dir: Option<&std::path::Path>,
44) -> Result<PathBuf, Box<dyn std::error::Error>> {
45    // Use the provided cache_dir or create a temp directory
46    let target_dir = if let Some(dir) = cache_dir {
47        dir.to_path_buf()
48    } else {
49        std::env::temp_dir()
50    };
51
52    // Extract filename from URL
53    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    // Skip download if file already exists
61    if local_path.exists() {
62        println!("Using cached file: {}", local_path.display());
63        return Ok(local_path);
64    }
65
66    // Download the file
67    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
80/// Spawn emulator with common error handling and debug output
81pub 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    // Show the full command with arguments
90    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
112/// Spawn emulator with stdin support and common error handling and debug output
113pub 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    // Check for !stdin bang variable and prepare stdin content
123    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                        // Always append Ctrl+Z (0x1A) to explicitly send EOF
131                        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    // Configure stdin pipe if we have content to send
150    if stdin_content.is_some() {
151        cmd.stdin(std::process::Stdio::piped());
152    }
153
154    // Show the full command with arguments
155    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            // Handle stdin piping if we have content
169            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                        // Longer sleep to ensure emulator is ready for input
175                        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                        // stdin is automatically closed when it goes out of scope
183                    });
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
200/// Spawn emulator with timeout support
201/// For emulators that support native --timeout (like cuxn), just spawn normally
202/// For emulators that don't support --timeout (like buxn, uxn), spawn and kill after timeout
203pub 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    // Check for !stdin bang variable and prepare stdin content
215    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                        // Always append Ctrl+Z (0x1A) to explicitly send EOF
223                        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    // Configure stdin pipe if we have content to send
242    if stdin_content.is_some() {
243        cmd.stdin(std::process::Stdio::piped());
244    }
245
246    // Get timeout value from protocol result (either t or timeout parameter)
247    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            // Emulator doesn't support native timeout, so we need to manually kill it
252            let timeout_duration = Duration::from_secs_f32(timeout_seconds);
253
254            match cmd.spawn() {
255                Ok(mut child) => {
256                    // Handle stdin piping if we have content
257                    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                                // Longer sleep to ensure emulator is ready for input
263                                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                                // stdin is automatically closed when it goes out of scope
271                            });
272                        } else {
273                            eprintln!("Failed to get stdin handle for emulator");
274                        }
275                    }
276
277                    // Show the full command with arguments
278                    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                    // Wait for the child process or timeout, whichever comes first
298                    let timeout_val = timeout_seconds;
299                    let handle = std::thread::spawn(move || {
300                        let start_time = std::time::Instant::now();
301
302                        loop {
303                            // Check if the process has exited naturally
304                            match child.try_wait() {
305                                Ok(Some(_status)) => {
306                                    // Process exited naturally
307                                    return;
308                                }
309                                Ok(None) => {
310                                    // Process is still running, check if we've hit timeout
311                                    if start_time.elapsed() >= timeout_duration {
312                                        // Timeout reached, kill the process
313                                        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                            // Sleep briefly before checking again
331                            std::thread::sleep(Duration::from_millis(100));
332                        }
333                    });
334
335                    // Join the timeout thread to ensure we wait for the process to complete or timeout
336                    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            // Emulator supports native timeout or no timeout specified, use normal spawn
351            spawn_emulator_with_stdin_and_logging(cmd, result, verbose)
352        }
353    } else {
354        // Emulator supports native timeout or no timeout specified, use normal spawn
355        spawn_emulator_with_stdin_and_logging(cmd, result, verbose)
356    }
357}
358
359/// Extract timeout value from ProtocolParseResult
360/// Returns Some(seconds) if timeout is specified, None otherwise
361fn get_timeout_from_result(result: &ProtocolParseResult) -> Option<f32> {
362    // Check protocol variables first (t or timeout)
363    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    // Check query variables
376    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
396/// Fetch content for stdin from a bang variable
397/// Supports @http/https URLs and direct content
398pub 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..]; // Remove the @ prefix
401        println!("Downloading stdin content from: {}", url);
402        crate::fetch::downloader::http_get(url)
403    } else {
404        // Direct content, just return as bytes
405        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
417/// Detect file type based on extension
418pub fn detect_file_type(path: &std::path::Path) -> FileType {
419    let path_str = path.to_string_lossy();
420
421    // Check for .tal.txt files first
422    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}