Skip to main content

conduit_cli/core/engine/workflow/loader/
run.rs

1use crate::core::domain::loader::Loader;
2use crate::core::engine::workflow::Workflow;
3use crate::core::schemas::lock::Lockfile;
4use crate::errors::{ConduitError, ConduitResult};
5use std::process::Stdio;
6use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
7use tokio::process::{ChildStdin, Command};
8
9impl Workflow {
10    pub async fn run_server(&self, lock: &Lockfile) -> ConduitResult<()> {
11        let mut cmd = Command::new("java");
12        cmd.current_dir(&self.project_root);
13        cmd.kill_on_drop(true);
14
15        self.setup_loader_args(&mut cmd, lock).await?;
16        cmd.arg("nogui");
17
18        cmd.stdin(Stdio::piped());
19        cmd.stdout(Stdio::piped());
20        cmd.stderr(Stdio::inherit());
21
22        let mut child = cmd.spawn().map_err(ConduitError::Io)?;
23
24        let stdin = child.stdin.take().ok_or(ConduitError::NoEntryPoint)?;
25        let stdin_task = tokio::spawn(Self::bridge_stdin(stdin));
26
27        let stdout = child.stdout.take().ok_or(ConduitError::NoEntryPoint)?;
28        let mut reader = BufReader::new(stdout).lines();
29
30        while let Ok(Some(line)) = reader.next_line().await {
31            println!("{line}");
32        }
33
34        let _ = child.wait().await;
35        stdin_task.abort();
36
37        Ok(())
38    }
39
40    async fn setup_loader_args(&self, cmd: &mut Command, lock: &Lockfile) -> ConduitResult<()> {
41        match &lock.instance.loader {
42            Loader::Neoforge { version } => {
43                let args_file = self.find_args_file(version).await?;
44                cmd.arg(format!("@{args_file}"));
45            }
46            Loader::Forge { version } => {
47                if Self::is_modern_forge(version) {
48                    let args_file = self.find_args_file(version).await?;
49                    cmd.arg(format!("@{args_file}"));
50                } else {
51                    cmd.arg("-jar").arg("server.jar");
52                }
53            }
54            Loader::Fabric => {
55                let jar = if self.project_root.join("fabric-server-launch.jar").exists() {
56                    "fabric-server-launch.jar"
57                } else {
58                    "server.jar"
59                };
60                cmd.arg("-jar").arg(jar);
61            }
62            _ => {
63                cmd.arg("-jar").arg("server.jar");
64            }
65        }
66        Ok(())
67    }
68
69    async fn bridge_stdin(mut child_stdin: ChildStdin) {
70        let mut reader = BufReader::new(io::stdin()).lines();
71        while let Ok(Some(line)) = reader.next_line().await {
72            let payload = format!("{line}\n");
73            if child_stdin.write_all(payload.as_bytes()).await.is_err() {
74                break;
75            }
76            let _ = child_stdin.flush().await;
77        }
78    }
79
80    async fn find_args_file(&self, loader_version: &str) -> ConduitResult<String> {
81        let libs = self.project_root.join("libraries");
82        if !libs.exists() {
83            return Err(ConduitError::NoEntryPoint);
84        }
85
86        let target_name = if cfg!(windows) {
87            "win_args.txt"
88        } else {
89            "unix_args.txt"
90        };
91        let mut stack = vec![libs];
92        let mut fallback = None;
93
94        while let Some(current_dir) = stack.pop() {
95            let mut entries = tokio::fs::read_dir(current_dir).await?;
96            while let Some(entry) = entries.next_entry().await? {
97                let path = entry.path();
98                if path.is_dir() {
99                    stack.push(path);
100                } else if path.file_name().is_some_and(|n| n == target_name) {
101                    let relative = path
102                        .strip_prefix(&self.project_root)
103                        .map(|p| p.to_string_lossy().to_string())
104                        .unwrap_or_default();
105
106                    if path.to_string_lossy().contains(loader_version) {
107                        return Ok(relative);
108                    }
109                    fallback = Some(relative);
110                }
111            }
112        }
113        fallback.ok_or(ConduitError::NoEntryPoint)
114    }
115    
116    pub fn is_modern_forge(version: &str) -> bool {
117        let parts: Vec<&str> = version.split('-').next().unwrap_or("").split('.').collect();
118        let major = parts
119            .first()
120            .and_then(|v| v.parse::<u32>().ok())
121            .unwrap_or(0);
122        let minor = parts
123            .get(1)
124            .and_then(|v| v.parse::<u32>().ok())
125            .unwrap_or(0);
126        major >= 25 || (major == 1 && minor >= 17)
127    }
128}