cloudpub_client/plugins/minecraft/
mod.rs

1use 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
33// Minecraft server 1.21.8
34const 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        // Read the server config file and replace 'server-port=XXXX' with the new port
199        let server_config = re.replace_all(&server_config, |_caps: &regex::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        // Use custom Java options if provided, otherwise use defaults
207        let java_opts = config
208            .read()
209            .minecraft_java_opts
210            .clone()
211            .unwrap_or("-Xmx2048M -Xms2048M".to_string());
212
213        // Split the Java options string into individual arguments
214        let mut args: Vec<String> = java_opts
215            .split_whitespace()
216            .map(|s| s.to_string())
217            .collect();
218
219        // Add the jar file argument
220        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}