diff options
Diffstat (limited to '')
| -rw-r--r-- | src/minecraft/launcher.rs | 262 |
1 files changed, 199 insertions, 63 deletions
diff --git a/src/minecraft/launcher.rs b/src/minecraft/launcher.rs index 2eeaf21..3cca948 100644 --- a/src/minecraft/launcher.rs +++ b/src/minecraft/launcher.rs | |||
| @@ -1,64 +1,104 @@ | |||
| 1 | use std::process::Command; | 1 | use paths::{client_jar, library_file, natives_dir}; |
| 2 | 2 | use serde::Deserialize; | |
| 3 | use log::{debug, info}; | 3 | use std::path::Path; |
| 4 | use std::process::Output; | ||
| 5 | use std::{ | ||
| 6 | path::PathBuf, | ||
| 7 | process::{Command, ExitStatus}, | ||
| 8 | }; | ||
| 9 | use McError::Process; | ||
| 4 | 10 | ||
| 5 | use crate::{ | 11 | use crate::{ |
| 6 | config::Config, | 12 | config::Config, errors::McError, minecraft::manifests::Version, |
| 7 | errors::McError, | 13 | platform::paths, util::fs::library_allowed, |
| 8 | minecraft::manifests::{Library, Version}, | ||
| 9 | platform::paths, | ||
| 10 | }; | 14 | }; |
| 11 | 15 | ||
| 12 | fn build_classpath( | 16 | #[derive(Debug, Clone, Deserialize)] |
| 13 | config: &Config, | 17 | pub struct JavaRuntime { |
| 14 | version: &Version, | 18 | pub major: u8, |
| 15 | ) -> Result<String, McError> { | 19 | pub path: PathBuf, |
| 16 | let sep = if cfg!(windows) { ";" } else { ":" }; | 20 | } |
| 17 | let mut entries = Vec::new(); | 21 | |
| 22 | impl JavaRuntime { | ||
| 23 | fn validate(&self) -> Result<(), McError> { | ||
| 24 | let output: Output = Command::new(&self.path) | ||
| 25 | .arg("-version") | ||
| 26 | .output() | ||
| 27 | .map_err(|e| { | ||
| 28 | McError::Runtime(format!( | ||
| 29 | "Failed to execute Java at {}: {}", | ||
| 30 | self.path.display(), | ||
| 31 | e | ||
| 32 | )) | ||
| 33 | })?; | ||
| 34 | |||
| 35 | if !output.status.success() { | ||
| 36 | return Err(McError::Runtime(format!( | ||
| 37 | "Invalid Java binary: {}", | ||
| 38 | self.path.display() | ||
| 39 | ))); | ||
| 40 | } | ||
| 41 | |||
| 42 | Ok(()) | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 46 | fn required_java_major(java_major: Option<u8>) -> u8 { | ||
| 47 | java_major.unwrap_or(8) | ||
| 48 | } | ||
| 49 | |||
| 50 | fn resolve_runtime( | ||
| 51 | required: u8, | ||
| 52 | runtimes: &[JavaRuntime], | ||
| 53 | ) -> Result<PathBuf, McError> { | ||
| 54 | let mut candidates: Vec<&JavaRuntime> = runtimes | ||
| 55 | .iter() | ||
| 56 | .filter(|r| r.major >= required) | ||
| 57 | .collect(); | ||
| 58 | |||
| 59 | candidates.sort_by_key(|r| r.major); | ||
| 60 | |||
| 61 | let runtime = candidates.first().ok_or_else(|| { | ||
| 62 | McError::Runtime(format!( | ||
| 63 | "No suitable Java runtime found (required: Java {})", | ||
| 64 | required | ||
| 65 | )) | ||
| 66 | })?; | ||
| 67 | |||
| 68 | runtime.validate()?; | ||
| 69 | Ok(runtime.path.clone()) | ||
| 70 | } | ||
| 71 | |||
| 72 | fn build_classpath(config: &Config, version: &Version) -> String { | ||
| 73 | let system_separator: &str = if cfg!(windows) { ";" } else { ":" }; | ||
| 74 | let mut entries: Vec<String> = Vec::new(); | ||
| 18 | 75 | ||
| 19 | for library in &version.libraries { | 76 | for library in &version.libraries { |
| 20 | if !library_allowed(library) { | 77 | if !library_allowed(library) { |
| 21 | continue; | 78 | continue; |
| 22 | } | 79 | } |
| 80 | |||
| 23 | if let Some(artifact) = &library.downloads.artifact { | 81 | if let Some(artifact) = &library.downloads.artifact { |
| 24 | let path = paths::library_file(config, &artifact.path)?; | 82 | let path = library_file(config, &artifact.path); |
| 25 | entries.push(path.to_string_lossy().to_string()); | 83 | entries.push(path.to_string_lossy().to_string()); |
| 26 | } | 84 | } |
| 27 | } | 85 | } |
| 28 | 86 | ||
| 29 | let client_jar = paths::client_jar(config, &version.id)?; | 87 | let client: PathBuf = client_jar(config, &version.id); |
| 30 | entries.push(client_jar.to_string_lossy().to_string()); | 88 | entries.push(client.to_string_lossy().to_string()); |
| 31 | 89 | ||
| 32 | Ok(entries.join(sep)) | 90 | entries.join(system_separator) |
| 33 | } | 91 | } |
| 34 | 92 | ||
| 35 | pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | 93 | fn build_command( |
| 36 | let java = &config.java_path; | 94 | java_path: PathBuf, |
| 37 | let classpath = build_classpath(config, version)?; | 95 | config: &Config, |
| 38 | let natives_dir = paths::natives_dir(config, &version.id); | 96 | version: &Version, |
| 39 | 97 | natives_dir: &Path, | |
| 40 | if !natives_dir.exists() { | 98 | classpath: String, |
| 41 | return Err(McError::Runtime(format!( | 99 | asset_index_id: String, |
| 42 | "Natives folder does not exist: {}", | 100 | ) -> Command { |
| 43 | natives_dir.display() | 101 | let mut cmd: Command = Command::new(java_path); |
| 44 | ))); | ||
| 45 | } | ||
| 46 | |||
| 47 | let asset_index_id = version | ||
| 48 | .asset_index | ||
| 49 | .as_ref() | ||
| 50 | .ok_or_else(|| { | ||
| 51 | McError::Runtime("Missing assetIndex in version.json".into()) | ||
| 52 | })? | ||
| 53 | .id | ||
| 54 | .clone(); | ||
| 55 | |||
| 56 | info!("Launching Minecraft {}", version.id); | ||
| 57 | debug!("Classpath: {}", classpath); | ||
| 58 | debug!("Natives: {}", natives_dir.display()); | ||
| 59 | debug!("Asset index: {}", asset_index_id); | ||
| 60 | |||
| 61 | let mut cmd = Command::new(java); | ||
| 62 | 102 | ||
| 63 | cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) | 103 | cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) |
| 64 | .arg(format!("-Djava.library.path={}", natives_dir.display())); | 104 | .arg(format!("-Djava.library.path={}", natives_dir.display())); |
| @@ -76,11 +116,11 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | |||
| 76 | .arg("--version") | 116 | .arg("--version") |
| 77 | .arg(&version.id) | 117 | .arg(&version.id) |
| 78 | .arg("--gameDir") | 118 | .arg("--gameDir") |
| 79 | .arg(paths::game_dir(config)) | 119 | .arg(paths::game_directory(config)) |
| 80 | .arg("--assetsDir") | 120 | .arg("--assetsDir") |
| 81 | .arg(paths::assets_dir(config)) | 121 | .arg(paths::assets_directory(config)) |
| 82 | .arg("--assetIndex") | 122 | .arg("--assetIndex") |
| 83 | .arg(&asset_index_id) | 123 | .arg(asset_index_id) |
| 84 | .arg("--uuid") | 124 | .arg("--uuid") |
| 85 | .arg(&config.uuid) | 125 | .arg(&config.uuid) |
| 86 | .arg("--userProperties") | 126 | .arg("--userProperties") |
| @@ -90,32 +130,128 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | |||
| 90 | .arg("--userType") | 130 | .arg("--userType") |
| 91 | .arg("legacy"); | 131 | .arg("legacy"); |
| 92 | 132 | ||
| 93 | let status = cmd.status()?; | 133 | cmd |
| 134 | } | ||
| 135 | |||
| 136 | pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | ||
| 137 | let required: u8 = required_java_major( | ||
| 138 | version | ||
| 139 | .java_version | ||
| 140 | .as_ref() | ||
| 141 | .map(|j| j.major_version), | ||
| 142 | ); | ||
| 143 | |||
| 144 | let java_path: PathBuf = resolve_runtime(required, &config.runtimes)?; | ||
| 145 | |||
| 146 | let classpath: String = build_classpath(config, version); | ||
| 147 | let natives_dir: PathBuf = natives_dir(config, &version.id); | ||
| 148 | |||
| 149 | if !natives_dir.exists() { | ||
| 150 | return Err(McError::Runtime(format!( | ||
| 151 | "Natives folder does not exist: {}", | ||
| 152 | natives_dir.display() | ||
| 153 | ))); | ||
| 154 | } | ||
| 155 | |||
| 156 | let asset_index_id = version | ||
| 157 | .asset_index | ||
| 158 | .as_ref() | ||
| 159 | .ok_or_else(|| { | ||
| 160 | McError::Runtime("Missing assetIndex in version.json".into()) | ||
| 161 | })? | ||
| 162 | .id | ||
| 163 | .clone(); | ||
| 164 | |||
| 165 | let mut cmd: Command = build_command( | ||
| 166 | java_path, | ||
| 167 | config, | ||
| 168 | version, | ||
| 169 | &natives_dir, | ||
| 170 | classpath, | ||
| 171 | asset_index_id, | ||
| 172 | ); | ||
| 173 | |||
| 174 | let status: ExitStatus = cmd.status()?; | ||
| 94 | 175 | ||
| 95 | if !status.success() { | 176 | if !status.success() { |
| 96 | return Err(McError::Process("Minecraft exited with error".into())); | 177 | return Err(Process("Minecraft exited with error".into())); |
| 97 | } | 178 | } |
| 98 | 179 | ||
| 99 | Ok(()) | 180 | Ok(()) |
| 100 | } | 181 | } |
| 101 | 182 | ||
| 102 | fn library_allowed(lib: &Library) -> bool { | 183 | #[cfg(test)] |
| 103 | let rules = match &lib.rules { | 184 | mod tests { |
| 104 | | Some(r) => r, | 185 | use super::*; |
| 105 | | None => return true, | 186 | use std::path::PathBuf; |
| 106 | }; | 187 | |
| 188 | #[test] | ||
| 189 | fn required_java_major_defaults_to_8() { | ||
| 190 | assert_eq!(required_java_major(None), 8); | ||
| 191 | } | ||
| 192 | |||
| 193 | #[test] | ||
| 194 | fn required_java_major_uses_given_value() { | ||
| 195 | assert_eq!(required_java_major(Some(17)), 17); | ||
| 196 | } | ||
| 197 | |||
| 198 | #[test] | ||
| 199 | fn resolve_runtime_errors_when_no_runtimes() { | ||
| 200 | let runtimes: Vec<JavaRuntime> = vec![]; | ||
| 201 | assert!(resolve_runtime(17, &runtimes).is_err()); | ||
| 202 | } | ||
| 203 | |||
| 204 | #[test] | ||
| 205 | fn resolve_runtime_picks_lowest_matching_major() { | ||
| 206 | let runtimes: Vec<JavaRuntime> = vec![ | ||
| 207 | JavaRuntime { | ||
| 208 | major: 21, | ||
| 209 | path: PathBuf::from("/bin/true"), | ||
| 210 | }, | ||
| 211 | JavaRuntime { | ||
| 212 | major: 17, | ||
| 213 | path: PathBuf::from("/bin/true"), | ||
| 214 | }, | ||
| 215 | ]; | ||
| 216 | |||
| 217 | let result: PathBuf = resolve_runtime(8, &runtimes).unwrap(); | ||
| 218 | assert_eq!(result, PathBuf::from("/bin/true")); | ||
| 219 | } | ||
| 107 | 220 | ||
| 108 | let mut allowed = false; | 221 | #[test] |
| 222 | fn resolve_runtime_respects_required_version() { | ||
| 223 | let runtimes: Vec<JavaRuntime> = vec![ | ||
| 224 | JavaRuntime { | ||
| 225 | major: 8, | ||
| 226 | path: PathBuf::from("/bin/true"), | ||
| 227 | }, | ||
| 228 | JavaRuntime { | ||
| 229 | major: 17, | ||
| 230 | path: PathBuf::from("/bin/true"), | ||
| 231 | }, | ||
| 232 | ]; | ||
| 233 | |||
| 234 | let result: PathBuf = resolve_runtime(17, &runtimes).unwrap(); | ||
| 235 | assert_eq!(result, PathBuf::from("/bin/true")); | ||
| 236 | } | ||
| 109 | 237 | ||
| 110 | for rule in rules { | 238 | #[test] |
| 111 | let os_match = match &rule.os { | 239 | fn validate_succeeds_for_true_binary() { |
| 112 | | Some(os) => os.name == "linux", | 240 | let runtime = JavaRuntime { |
| 113 | | None => true, | 241 | major: 17, |
| 242 | path: PathBuf::from("/bin/true"), | ||
| 114 | }; | 243 | }; |
| 115 | if os_match { | 244 | |
| 116 | allowed = rule.action == "allow"; | 245 | assert!(runtime.validate().is_ok()); |
| 117 | } | ||
| 118 | } | 246 | } |
| 119 | 247 | ||
| 120 | allowed | 248 | #[test] |
| 249 | fn validate_fails_for_false_binary() { | ||
| 250 | let runtime = JavaRuntime { | ||
| 251 | major: 17, | ||
| 252 | path: PathBuf::from("/bin/false"), | ||
| 253 | }; | ||
| 254 | |||
| 255 | assert!(runtime.validate().is_err()); | ||
| 256 | } | ||
| 121 | } | 257 | } |
