cloudpub_client/plugins/minecraft/
mod.rs1use crate::config::{ClientConfig, ClientOpts};
2use crate::plugins::Plugin;
3use crate::shell::{download, get_cache_dir, SubProcess};
4use anyhow::{bail, Context, Result};
5use async_trait::async_trait;
6use cloudpub_common::protocol::message::Message;
7use cloudpub_common::protocol::ServerEndpoint;
8use cloudpub_common::utils::find_free_tcp_port;
9use parking_lot::RwLock;
10use regex::Regex;
11use std::path::PathBuf;
12use std::sync::Arc;
13use tokio::sync::mpsc;
14use tracing::info;
15
16#[cfg(unix)]
17use crate::shell::execute;
18
19#[cfg(target_os = "windows")]
20use crate::shell::unzip;
21
22#[cfg(target_os = "windows")]
23const JDK_URL: &str =
24 "https://download.java.net/openjdk/jdk23/ri/openjdk-23+37_windows-x64_bin.zip";
25
26#[cfg(target_os = "linux")]
27const JDK_URL: &str =
28 "https://download.java.net/openjdk/jdk23/ri/openjdk-23+37_linux-x64_bin.tar.gz";
29
30#[cfg(target_os = "macos")]
31const JDK_URL: &str = "https://download.java.net/java/GA/jdk23/3c5b90190c68498b986a97f276efd28a/37/GPL/openjdk-23_macos-x64_bin.tar.gz";
32
33const MINECRAFT_SERVER_URL: &str =
35 "https://piston-data.mojang.com/v1/objects/6bce4ef400e4efaa63a13d5e6f6b500be969ef81/server.jar";
36
37const MINECRAFT_SERVER_CFG: &str = include_str!("server.properties");
38
39pub const JDK_SUBDIR: &str = "jdk";
40pub const DOWNLOAD_SUBDIR: &str = "download";
41
42#[cfg(not(target_os = "macos"))]
43fn get_java() -> Result<PathBuf> {
44 Ok(get_cache_dir(JDK_SUBDIR)?.join("bin").join("java"))
45}
46
47#[cfg(target_os = "macos")]
48fn get_java() -> Result<PathBuf> {
49 Ok(get_cache_dir(JDK_SUBDIR)?
50 .join("Home")
51 .join("bin")
52 .join("java"))
53}
54
55pub struct MinecraftPlugin;
56
57#[async_trait]
58impl Plugin for MinecraftPlugin {
59 fn name(&self) -> &'static str {
60 "minecraft"
61 }
62
63 async fn setup(
64 &self,
65 config: &Arc<RwLock<ClientConfig>>,
66 _opts: &ClientOpts,
67 command_rx: &mut mpsc::Receiver<Message>,
68 result_tx: &mpsc::Sender<Message>,
69 ) -> Result<()> {
70 info!("Setup minecraft server");
71
72 let download_dir = get_cache_dir(DOWNLOAD_SUBDIR)?;
73 let jdk_dir = get_cache_dir(JDK_SUBDIR)?;
74 let jdk_filename = JDK_URL.split('/').next_back().unwrap();
75 let jdk_file = download_dir.join(jdk_filename);
76
77 let minecraft_file = download_dir.join("server.jar");
78
79 let mut touch = jdk_dir.clone();
80 touch.push("installed.txt");
81
82 if touch.exists() {
83 return Ok(());
84 }
85
86 download(
87 &crate::t!("downloading-jdk"),
88 config.clone(),
89 JDK_URL,
90 &jdk_file,
91 command_rx,
92 result_tx,
93 )
94 .await
95 .context(crate::t!("error-downloading-jdk"))?;
96
97 #[cfg(unix)]
98 execute(
99 "tar".into(),
100 vec![
101 "xvf".to_string(),
102 jdk_file.to_str().unwrap().to_string(),
103 "-C".to_string(),
104 jdk_dir.to_str().unwrap().to_string(),
105 #[cfg(target_os = "macos")]
106 "--strip-components=3".to_string(),
107 #[cfg(not(target_os = "macos"))]
108 "--strip-components=1".to_string(),
109 ],
110 None,
111 Default::default(),
112 Some((crate::t!("installing-jdk"), result_tx.clone(), 450)),
113 command_rx,
114 )
115 .await?;
116
117 #[cfg(target_os = "windows")]
118 unzip(
119 &crate::t!("installing-jdk"),
120 &jdk_file,
121 &jdk_dir,
122 1,
123 result_tx,
124 )
125 .await
126 .context(crate::t!("error-unpacking-jdk"))?;
127
128 let minecraft_jar = config
129 .read()
130 .minecraft_server
131 .clone()
132 .unwrap_or(MINECRAFT_SERVER_URL.to_string());
133
134 let maybe_path = PathBuf::from(&minecraft_jar);
135
136 if maybe_path.is_file() {
137 std::fs::copy(&minecraft_jar, &minecraft_file).with_context(
138 || crate::t!("error-copying-minecraft-server", "path" => minecraft_jar),
139 )?;
140 } else if maybe_path.is_dir() {
141 bail!(crate::t!("error-invalid-minecraft-jar-directory", "path" => minecraft_jar));
142 } else if minecraft_jar.starts_with("http") {
143 download(
144 &crate::t!("downloading-minecraft-server"),
145 config.clone(),
146 &minecraft_jar,
147 &minecraft_file,
148 command_rx,
149 result_tx,
150 )
151 .await
152 .with_context(
153 || crate::t!("error-downloading-minecraft-server", "url" => minecraft_jar),
154 )?;
155 } else {
156 bail!(crate::t!("error-invalid-minecraft-path", "path" => minecraft_jar));
157 }
158 std::fs::write(touch, "Delete to reinstall").context(crate::t!("error-creating-marker"))?;
159
160 Ok(())
161 }
162
163 async fn publish(
164 &self,
165 endpoint: &ServerEndpoint,
166 config: &Arc<RwLock<ClientConfig>>,
167 opts: &ClientOpts,
168 result_tx: &mpsc::Sender<Message>,
169 ) -> Result<SubProcess> {
170 let minecraft_dir: PathBuf = endpoint.client.as_ref().unwrap().local_addr.clone().into();
171 std::fs::create_dir_all(&minecraft_dir)
172 .context(crate::t!("error-creating-server-directory"))?;
173
174 let download_dir = get_cache_dir(DOWNLOAD_SUBDIR)?;
175 let minecraft_file = download_dir.join("server.jar");
176
177 let mut server_cfg = minecraft_dir.clone();
178 server_cfg.push("server.properties");
179
180 let mut eula = minecraft_dir.clone();
181 eula.push("eula.txt");
182
183 if !server_cfg.exists() {
184 std::fs::write(&server_cfg, MINECRAFT_SERVER_CFG)
185 .context(crate::t!("error-creating-server-properties"))?;
186 std::fs::write(eula, "eula=true").context(crate::t!("error-creating-eula-file"))?;
187 }
188
189 let port = find_free_tcp_port()
190 .await
191 .context(crate::t!("error-finding-free-port"))?;
192
193 let re = Regex::new(r"server\-port\s*=\s*\d+").unwrap();
194
195 let server_config = std::fs::read_to_string(&server_cfg)
196 .context(crate::t!("error-reading-server-properties"))?;
197
198 let server_config = re.replace_all(&server_config, |_caps: ®ex::Captures| {
200 format!("server-port={}", port)
201 });
202
203 std::fs::write(&server_cfg, server_config.to_string())
204 .context(crate::t!("error-writing-server-properties"))?;
205
206 let java_opts = config
208 .read()
209 .minecraft_java_opts
210 .clone()
211 .unwrap_or("-Xmx2048M -Xms2048M".to_string());
212
213 let mut args: Vec<String> = java_opts
215 .split_whitespace()
216 .map(|s| s.to_string())
217 .collect();
218
219 args.push("-jar".to_string());
221 args.push(minecraft_file.to_str().unwrap().to_string());
222 if !opts.gui {
223 args.push("nogui".to_string());
224 }
225
226 let server = SubProcess::new(
227 get_java().context(crate::t!("error-getting-java-path"))?,
228 args,
229 Some(minecraft_dir),
230 Default::default(),
231 result_tx.clone(),
232 port,
233 );
234 Ok(server)
235 }
236}