conduit_cli/core/engine/workflow/loader/
run.rs1use 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}