Skip to main content

run/engine/
java.rs

1use std::io::{BufRead, BufReader, Read, Write};
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4use std::sync::{Arc, Mutex};
5use std::thread;
6use std::time::{Duration, Instant};
7
8use anyhow::{Context, Result};
9use tempfile::Builder;
10
11use super::{ExecutionOutcome, ExecutionPayload, LanguageEngine, LanguageSession, hash_source};
12
13pub struct JavaEngine {
14    compiler: Option<PathBuf>,
15    runtime: Option<PathBuf>,
16    jshell: Option<PathBuf>,
17}
18
19impl JavaEngine {
20    pub fn new() -> Self {
21        Self {
22            compiler: resolve_javac_binary(),
23            runtime: resolve_java_binary(),
24            jshell: resolve_jshell_binary(),
25        }
26    }
27
28    fn ensure_compiler(&self) -> Result<&Path> {
29        self.compiler.as_deref().ok_or_else(|| {
30            anyhow::anyhow!(
31                "Java support requires the `javac` compiler. Install the JDK from https://adoptium.net/ or your vendor of choice and ensure it is on your PATH."
32            )
33        })
34    }
35
36    fn ensure_runtime(&self) -> Result<&Path> {
37        self.runtime.as_deref().ok_or_else(|| {
38            anyhow::anyhow!(
39                "Java support requires the `java` runtime. Install the JDK from https://adoptium.net/ or your vendor of choice and ensure it is on your PATH."
40            )
41        })
42    }
43
44    fn ensure_jshell(&self) -> Result<&Path> {
45        self.jshell.as_deref().ok_or_else(|| {
46            anyhow::anyhow!(
47                "Interactive Java REPL requires `jshell`. Install a full JDK and ensure `jshell` is on your PATH."
48            )
49        })
50    }
51
52    fn write_inline_source(&self, code: &str, dir: &Path) -> Result<(PathBuf, String)> {
53        let source_path = dir.join("Main.java");
54        let wrapped = wrap_inline_java(code);
55        std::fs::write(&source_path, wrapped).with_context(|| {
56            format!(
57                "failed to write temporary Java source to {}",
58                source_path.display()
59            )
60        })?;
61        Ok((source_path, "Main".to_string()))
62    }
63
64    fn write_from_stdin(&self, code: &str, dir: &Path) -> Result<(PathBuf, String)> {
65        self.write_inline_source(code, dir)
66    }
67
68    fn copy_source(&self, original: &Path, dir: &Path) -> Result<(PathBuf, String)> {
69        let file_name = original
70            .file_name()
71            .map(|f| f.to_owned())
72            .ok_or_else(|| anyhow::anyhow!("invalid Java source path"))?;
73        let target = dir.join(&file_name);
74        std::fs::copy(original, &target).with_context(|| {
75            format!(
76                "failed to copy Java source from {} to {}",
77                original.display(),
78                target.display()
79            )
80        })?;
81        let class_name = original
82            .file_stem()
83            .and_then(|stem| stem.to_str())
84            .ok_or_else(|| anyhow::anyhow!("unable to determine Java class name"))?
85            .to_string();
86        Ok((target, class_name))
87    }
88
89    fn compile(&self, source: &Path, output_dir: &Path) -> Result<std::process::Output> {
90        let compiler = self.ensure_compiler()?;
91        let mut cmd = Command::new(compiler);
92        cmd.arg("-d")
93            .arg(output_dir)
94            .arg(source)
95            .stdout(Stdio::piped())
96            .stderr(Stdio::piped());
97        cmd.output().with_context(|| {
98            format!(
99                "failed to invoke {} to compile {}",
100                compiler.display(),
101                source.display()
102            )
103        })
104    }
105
106    fn run(&self, class_dir: &Path, class_name: &str) -> Result<std::process::Output> {
107        let runtime = self.ensure_runtime()?;
108        let mut cmd = Command::new(runtime);
109        cmd.arg("-cp")
110            .arg(class_dir)
111            .arg(class_name)
112            .stdout(Stdio::piped())
113            .stderr(Stdio::piped());
114        cmd.stdin(Stdio::inherit());
115        cmd.output().with_context(|| {
116            format!(
117                "failed to execute {} for class {} with classpath {}",
118                runtime.display(),
119                class_name,
120                class_dir.display()
121            )
122        })
123    }
124}
125
126impl LanguageEngine for JavaEngine {
127    fn id(&self) -> &'static str {
128        "java"
129    }
130
131    fn display_name(&self) -> &'static str {
132        "Java"
133    }
134
135    fn aliases(&self) -> &[&'static str] {
136        &[]
137    }
138
139    fn supports_sessions(&self) -> bool {
140        self.jshell.is_some()
141    }
142
143    fn validate(&self) -> Result<()> {
144        let compiler = self.ensure_compiler()?;
145        let mut compile_check = Command::new(compiler);
146        compile_check
147            .arg("-version")
148            .stdout(Stdio::null())
149            .stderr(Stdio::null());
150        compile_check
151            .status()
152            .with_context(|| format!("failed to invoke {}", compiler.display()))?
153            .success()
154            .then_some(())
155            .ok_or_else(|| anyhow::anyhow!("{} is not executable", compiler.display()))?;
156
157        let runtime = self.ensure_runtime()?;
158        let mut runtime_check = Command::new(runtime);
159        runtime_check
160            .arg("-version")
161            .stdout(Stdio::null())
162            .stderr(Stdio::null());
163        runtime_check
164            .status()
165            .with_context(|| format!("failed to invoke {}", runtime.display()))?
166            .success()
167            .then_some(())
168            .ok_or_else(|| anyhow::anyhow!("{} is not executable", runtime.display()))?;
169
170        if let Some(jshell) = self.jshell.as_ref() {
171            let mut jshell_check = Command::new(jshell);
172            jshell_check
173                .arg("--version")
174                .stdout(Stdio::null())
175                .stderr(Stdio::null());
176            jshell_check
177                .status()
178                .with_context(|| format!("failed to invoke {}", jshell.display()))?
179                .success()
180                .then_some(())
181                .ok_or_else(|| anyhow::anyhow!("{} is not executable", jshell.display()))?;
182        }
183
184        Ok(())
185    }
186
187    fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
188        // Check class file cache for inline/stdin payloads
189        if let Some(code) = match payload {
190            ExecutionPayload::Inline { code } | ExecutionPayload::Stdin { code } => Some(code.as_str()),
191            _ => None,
192        } {
193            let wrapped = wrap_inline_java(code);
194            let src_hash = hash_source(&wrapped);
195            let cache_dir = std::env::temp_dir().join("run-compile-cache").join(format!("java-{:016x}", src_hash));
196            let class_file = cache_dir.join("Main.class");
197            if class_file.exists() {
198                let start = Instant::now();
199                if let Ok(output) = self.run(&cache_dir, "Main") {
200                    return Ok(ExecutionOutcome {
201                        language: self.id().to_string(),
202                        exit_code: output.status.code(),
203                        stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
204                        stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
205                        duration: start.elapsed(),
206                    });
207                }
208            }
209        }
210
211        let temp_dir = Builder::new()
212            .prefix("run-java")
213            .tempdir()
214            .context("failed to create temporary directory for java build")?;
215        let dir_path = temp_dir.path();
216
217        let (source_path, class_name) = match payload {
218            ExecutionPayload::Inline { code } => self.write_inline_source(code, dir_path)?,
219            ExecutionPayload::Stdin { code } => self.write_from_stdin(code, dir_path)?,
220            ExecutionPayload::File { path } => self.copy_source(path, dir_path)?,
221        };
222
223        let start = Instant::now();
224
225        let compile_output = self.compile(&source_path, dir_path)?;
226        if !compile_output.status.success() {
227            return Ok(ExecutionOutcome {
228                language: self.id().to_string(),
229                exit_code: compile_output.status.code(),
230                stdout: String::from_utf8_lossy(&compile_output.stdout).into_owned(),
231                stderr: String::from_utf8_lossy(&compile_output.stderr).into_owned(),
232                duration: start.elapsed(),
233            });
234        }
235
236        // Cache compiled class files for inline/stdin
237        if let Some(code) = match payload {
238            ExecutionPayload::Inline { code } | ExecutionPayload::Stdin { code } => Some(code.as_str()),
239            _ => None,
240        } {
241            let wrapped = wrap_inline_java(code);
242            let src_hash = hash_source(&wrapped);
243            let cache_dir = std::env::temp_dir().join("run-compile-cache").join(format!("java-{:016x}", src_hash));
244            let _ = std::fs::create_dir_all(&cache_dir);
245            // Copy all .class files
246            if let Ok(entries) = std::fs::read_dir(dir_path) {
247                for entry in entries.flatten() {
248                    if entry.path().extension().and_then(|e| e.to_str()) == Some("class") {
249                        let _ = std::fs::copy(entry.path(), cache_dir.join(entry.file_name()));
250                    }
251                }
252            }
253        }
254
255        let run_output = self.run(dir_path, &class_name)?;
256        Ok(ExecutionOutcome {
257            language: self.id().to_string(),
258            exit_code: run_output.status.code(),
259            stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
260            stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
261            duration: start.elapsed(),
262        })
263    }
264
265    fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
266        let jshell = self.ensure_jshell()?;
267        let mut cmd = Command::new(jshell);
268        cmd.arg("--execution=local")
269            .arg("--feedback=concise")
270            .arg("--no-startup")
271            .stdin(Stdio::piped())
272            .stdout(Stdio::piped())
273            .stderr(Stdio::piped());
274
275        let mut child = cmd
276            .spawn()
277            .with_context(|| format!("failed to start {} REPL", jshell.display()))?;
278
279        let stdout = child.stdout.take().context("missing stdout handle")?;
280        let stderr = child.stderr.take().context("missing stderr handle")?;
281
282        let stderr_buffer = Arc::new(Mutex::new(String::new()));
283        let stderr_collector = stderr_buffer.clone();
284        thread::spawn(move || {
285            let mut reader = BufReader::new(stderr);
286            let mut buf = String::new();
287            loop {
288                buf.clear();
289                match reader.read_line(&mut buf) {
290                    Ok(0) => break,
291                    Ok(_) => {
292                        let Ok(mut lock) = stderr_collector.lock() else { break };
293                        lock.push_str(&buf);
294                    }
295                    Err(_) => break,
296                }
297            }
298        });
299
300        let mut session = JavaSession {
301            child,
302            stdout: BufReader::new(stdout),
303            stderr: stderr_buffer,
304            closed: false,
305        };
306
307        session.discard_prompt()?;
308
309        Ok(Box::new(session))
310    }
311}
312
313fn resolve_javac_binary() -> Option<PathBuf> {
314    which::which("javac").ok()
315}
316
317fn resolve_java_binary() -> Option<PathBuf> {
318    which::which("java").ok()
319}
320
321fn resolve_jshell_binary() -> Option<PathBuf> {
322    which::which("jshell").ok()
323}
324
325fn wrap_inline_java(body: &str) -> String {
326    if body.contains("class ") {
327        return body.to_string();
328    }
329
330    let mut header_lines = Vec::new();
331    let mut rest_lines = Vec::new();
332    let mut in_header = true;
333
334    for line in body.lines() {
335        let trimmed = line.trim_start();
336        if in_header && (trimmed.starts_with("import ") || trimmed.starts_with("package ")) {
337            header_lines.push(line);
338            continue;
339        }
340        in_header = false;
341        rest_lines.push(line);
342    }
343
344    let mut result = String::new();
345    if !header_lines.is_empty() {
346        for hl in header_lines {
347            result.push_str(hl);
348            if !hl.ends_with('\n') {
349                result.push('\n');
350            }
351        }
352        result.push('\n');
353    }
354
355    result.push_str(
356        "public class Main {\n    public static void main(String[] args) throws Exception {\n",
357    );
358    for line in rest_lines {
359        if line.trim().is_empty() {
360            result.push_str("        \n");
361        } else {
362            result.push_str("        ");
363            result.push_str(line);
364            result.push('\n');
365        }
366    }
367    result.push_str("    }\n}\n");
368    result
369}
370
371struct JavaSession {
372    child: std::process::Child,
373    stdout: BufReader<std::process::ChildStdout>,
374    stderr: Arc<Mutex<String>>,
375    closed: bool,
376}
377
378impl JavaSession {
379    fn write_code(&mut self, code: &str) -> Result<()> {
380        if self.closed {
381            anyhow::bail!("jshell session has already exited; start a new session with :reset");
382        }
383        let stdin = self
384            .child
385            .stdin
386            .as_mut()
387            .context("jshell session stdin closed")?;
388        stdin.write_all(code.as_bytes())?;
389        if !code.ends_with('\n') {
390            stdin.write_all(b"\n")?;
391        }
392        stdin.flush()?;
393        Ok(())
394    }
395
396    fn read_until_prompt(&mut self) -> Result<String> {
397        const PROMPT: &[u8] = b"jshell> ";
398        let mut buffer = Vec::new();
399        loop {
400            let mut byte = [0u8; 1];
401            let read = self.stdout.read(&mut byte)?;
402            if read == 0 {
403                break;
404            }
405            buffer.extend_from_slice(&byte[..read]);
406            if buffer.ends_with(PROMPT) {
407                break;
408            }
409        }
410
411        if buffer.ends_with(PROMPT) {
412            buffer.truncate(buffer.len() - PROMPT.len());
413        }
414
415        let mut text = String::from_utf8_lossy(&buffer).into_owned();
416        text = text.replace("\r\n", "\n");
417        text = text.replace('\r', "");
418        Ok(strip_feedback(text))
419    }
420
421    fn take_stderr(&self) -> String {
422        let Ok(mut lock) = self.stderr.lock() else {
423            return String::new();
424        };
425        if lock.is_empty() {
426            String::new()
427        } else {
428            let mut output = String::new();
429            std::mem::swap(&mut output, &mut *lock);
430            output
431        }
432    }
433
434    fn discard_prompt(&mut self) -> Result<()> {
435        let _ = self.read_until_prompt()?;
436        let _ = self.take_stderr();
437        Ok(())
438    }
439}
440
441impl LanguageSession for JavaSession {
442    fn language_id(&self) -> &str {
443        "java"
444    }
445
446    fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
447        if self.closed {
448            return Ok(ExecutionOutcome {
449                language: self.language_id().to_string(),
450                exit_code: None,
451                stdout: String::new(),
452                stderr: "jshell session already exited. Use :reset to start a new session.\n"
453                    .to_string(),
454                duration: Duration::default(),
455            });
456        }
457
458        let trimmed = code.trim();
459        let exit_requested = matches!(trimmed, "/exit" | "/exit;" | ":exit");
460        let start = Instant::now();
461        self.write_code(code)?;
462        let stdout = match self.read_until_prompt() {
463            Ok(output) => output,
464            Err(_) if exit_requested => String::new(),
465            Err(err) => return Err(err),
466        };
467        let stderr = self.take_stderr();
468
469        if exit_requested {
470            self.closed = true;
471            let _ = self.child.stdin.take();
472            let _ = self.child.wait();
473        }
474
475        Ok(ExecutionOutcome {
476            language: self.language_id().to_string(),
477            exit_code: None,
478            stdout,
479            stderr,
480            duration: start.elapsed(),
481        })
482    }
483
484    fn shutdown(&mut self) -> Result<()> {
485        if !self.closed {
486            if let Some(mut stdin) = self.child.stdin.take() {
487                let _ = stdin.write_all(b"/exit\n");
488                let _ = stdin.flush();
489            }
490        }
491        let _ = self.child.wait();
492        self.closed = true;
493        Ok(())
494    }
495}
496
497fn strip_feedback(text: String) -> String {
498    let mut lines = Vec::new();
499    for line in text.lines() {
500        if let Some(stripped) = line.strip_prefix("|  ") {
501            lines.push(stripped.to_string());
502        } else if let Some(stripped) = line.strip_prefix("| ") {
503            lines.push(stripped.to_string());
504        } else if line.starts_with("|=") {
505            lines.push(line.trim_start_matches('|').trim().to_string());
506        } else if !line.trim().is_empty() {
507            lines.push(line.to_string());
508        }
509    }
510    lines.join("\n")
511}