diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/launcher.rs | 150 | ||||
| -rw-r--r-- | src/launcher/assets.rs | 5 | ||||
| -rw-r--r-- | src/launcher/constants.rs | 8 |
3 files changed, 150 insertions, 13 deletions
diff --git a/src/launcher.rs b/src/launcher.rs index 25cdcc4..f712e69 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -7,16 +7,21 @@ mod rules; mod assets; use std::borrow::Cow; +use std::cmp::min; use std::collections::HashMap; use std::env::consts::{ARCH, OS}; use std::error::Error; use std::ffi::OsStr; use std::fmt::{Display, Formatter}; +use std::fs::FileType; use std::io::ErrorKind; use std::io::ErrorKind::AlreadyExists; use std::path::{Component, Path, PathBuf}; +use std::process; +use std::time::{SystemTime, UNIX_EPOCH}; +use chrono::NaiveDateTime; use const_format::formatcp; -use futures::TryStreamExt; +use futures::{stream, FutureExt, StreamExt, TryStreamExt}; use log::{debug, info, warn}; use serde::{Deserialize, Serialize}; use sha1_smol::Sha1; @@ -24,10 +29,12 @@ use sysinfo::System; use tokio::{fs, io}; use tokio::fs::File; use tokio::io::AsyncWriteExt; +use tokio_stream::wrappers::ReadDirStream; +use zip::ZipArchive; use download::{MultiDownloader, VerifiedDownload}; use rules::{CompatCheck, IncompatibleError}; use version::{VersionList, VersionResolveError, VersionResult}; -use crate::version::{Logging, Library, OSRestriction, OperatingSystem, DownloadType, DownloadInfo}; +use crate::version::{Logging, Library, OSRestriction, OperatingSystem, DownloadType, DownloadInfo, LibraryExtractRule}; pub use profile::{Instance, Profile}; use crate::launcher::assets::{AssetError, AssetRepository}; @@ -125,7 +132,8 @@ struct SystemInfo { } struct LibraryRepository { - home: PathBuf + home: PathBuf, + natives: PathBuf } pub struct Launcher { @@ -154,6 +162,7 @@ pub enum LaunchError { LibraryDirError(PathBuf, io::Error), LibraryVerifyError(FileVerifyError), LibraryDownloadError, + LibraryExtractZipError(PathBuf, zip::result::ZipError), // ensure file errors MissingURL, @@ -180,6 +189,7 @@ impl Display for LaunchError { LaunchError::LibraryDirError(path, e) => write!(f, "failed to create library directory {}: {}", path.display(), e), LaunchError::LibraryVerifyError(e) => write!(f, "failed to verify library: {}", e), LaunchError::LibraryDownloadError => f.write_str("library download failed (see above logs for details)"), // TODO: booo this sucks + LaunchError::LibraryExtractZipError(path, e) => write!(f, "library extract zip error ({}): {e}", path.display()), LaunchError::MissingURL => f.write_str("cannot download required file, URL is missing"), LaunchError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error), LaunchError::Offline => f.write_str("cannot download file in offline mode"), @@ -201,6 +211,7 @@ impl Error for LaunchError { LaunchError::IncompatibleVersion(e) => Some(e), LaunchError::LibraryDirError(_, e) => Some(e), LaunchError::LibraryVerifyError(e) => Some(e), + LaunchError::LibraryExtractZipError(_, e) => Some(e), LaunchError::IO { error: e, .. } => Some(e), LaunchError::Download { error: e, .. } => Some(e), LaunchError::Integrity(e) => Some(e), @@ -245,12 +256,24 @@ impl Launcher { system_info: SystemInfo::new(), libraries: LibraryRepository { home: home.join("libraries"), + natives: home.join("natives") }, assets: AssetRepository::new(online, &assets_path).await?, home }) } + fn choose_lib_classifier<'lib>(&self, lib: &'lib Library) -> Option<&'lib str> { + lib.natives.as_ref().map_or(None, |n| n.get(&self.system_info.os)).map(|s| s.as_str()) + } + + fn create_extract_job<'lib>(&self, lib: &'lib Library) -> Option<LibraryExtractJob<'lib>> { + Some(LibraryExtractJob { + source: LibraryRepository::get_artifact_path(lib.name.as_str(), self.choose_lib_classifier(lib))?, + rule: lib.extract.as_ref() + }) + } + async fn ensure_file(&self, path: &Path, dlinfo: &DownloadInfo) -> Result<(), LaunchError> { // verify the file match util::verify_file(path, dlinfo.size, dlinfo.sha1).await { @@ -387,7 +410,7 @@ impl Launcher { * - (done) download them * - (done) (if offline mode, explode) * - if virtual or resource-mapped, copy (or perhaps hardlink? that would be cool) - * - the actual client jar + * - (done) the actual client jar * - (done) check integriddy and download if needed * - (done) (explode if offline mode) * - launch the game @@ -418,7 +441,7 @@ impl Launcher { } libs.push(lib); - if let Some(dl) = self.libraries.create_download(lib, self.system_info.os) { + if let Some(dl) = self.libraries.create_download(lib, self.choose_lib_classifier(lib)) { dl.make_dirs().await.map_err(|e| LaunchError::LibraryDirError(dl.get_path().to_path_buf(), e))?; downloads.push(dl); } @@ -477,27 +500,45 @@ impl Launcher { client_jar_path = None; } + // clean up old natives + let nnatives = self.libraries.clean_old_natives().await?; + info!("Cleaned up {} old natives directories.", nnatives); + + // extract natives + //todo!() Ok(()) } } -#[derive(Debug, Clone)] +#[derive(Debug)] enum LibraryError { InvalidName(String), - IOError(ErrorKind) + IO { what: &'static str, error: io::Error } } impl Display for LibraryError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { LibraryError::InvalidName(name) => write!(f, "invalid name: {name}"), - LibraryError::IOError(e) => write!(f, "io error reading library: {e}"), + LibraryError::IO { what, error } => write!(f, "library i/o error ({what}): {error}"), + } + } +} + +impl Error for LibraryError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + LibraryError::IO { error, .. } => Some(error), + _ => None } } } -impl Error for LibraryError {} +struct LibraryExtractJob<'lib> { + source: PathBuf, + rule: Option<&'lib LibraryExtractRule> +} const ARCH_BITS: &'static str = formatcp!("{}", usize::BITS); @@ -542,9 +583,7 @@ impl LibraryRepository { Some(p) } - fn create_download(&self, lib: &Library, os: OperatingSystem) -> Option<VerifiedDownload> { - let classifier = lib.natives.as_ref().map_or(None, |n| n.get(&os)).map(|s| s.as_str()); - + fn create_download(&self, lib: &Library, classifier: Option<&str>) -> Option<VerifiedDownload> { if lib.url.is_some() || lib.downloads.is_none() { // TODO: derive download URL in this situation? warn!("BUG: Deprecated case for library {}: url present or downloads missing. The launcher does not support out-of-line checksums at this time. Not downloading this library.", lib.name); @@ -557,6 +596,93 @@ impl LibraryRepository { Some(VerifiedDownload::new(dlinfo.url.as_ref()?, path.as_path(), dlinfo.size, dlinfo.sha1)) } + + async fn clean_old_natives(&self) -> Result<usize, LaunchError> { + info!("Cleaning up old natives folders..."); + + let boot_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - min(System::uptime(), 7u64*24*60*60); + + let readdir = match fs::read_dir(&self.natives).await { + Ok(readdir) => readdir, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(0), + Err(e) => return Err(LaunchError::IO { what: "reading natives directory", error: e }) + }; + + ReadDirStream::new(readdir) + .map(|entry| Ok(async move { + let entry = entry.map_err(|e| LaunchError::IO { what: "reading natives entry", error: e })?; + let ftype = entry.file_type().await.map_err(|e| LaunchError::IO { what: "'stat'ing natives entry", error: e })?; + + if !ftype.is_dir() { return Ok(false); } + + let Some(ftime) = entry.file_name().to_str() + .map_or(None, |s| constants::NATIVES_DIR_PATTERN.captures(s)) + .map_or(None, |c| c.get(1)) + .map_or(None, |cap| cap.as_str().parse::<u64>().ok()) else { + return Ok(false); + }; + + if ftime < boot_time { + let path = entry.path(); + info!("Deleting old natives directory {}", path.display()); + + /*fs::remove_dir_all(&path).await.map_err(|e| LaunchError::IO { + what: "reading natives entry", + error: e + })?;*/ + + return Ok(true); + } + + Ok(false) + })) + .try_buffer_unordered(8) + .try_fold(0usize, |accum, res| async move { + match res { + true => Ok(accum + 1), + _ => Ok(accum) + } + }).await + } + + fn extract_natives_for<'lib>(natives: &Path, job: LibraryExtractJob<'lib>) -> Result<usize, LaunchError> { + /*let file = File::open(&job.source).await.map_err(|e| LaunchError::IO { + what: "extracting library natives (open)", + error: e + })?; + + let z = ZipArchive::new(file);*/ + + todo!() + } + + async fn extract_natives<'lib>(&self, libs: impl IntoIterator<Item = LibraryExtractJob<'lib>>) -> Result<PathBuf, LaunchError> { + fs::create_dir_all(&self.natives).await.map_err(|e| LaunchError::IO { + what: "creating natives directory", + error: e + })?; + + let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let natives_dir = self.natives.join(format!("{}{}-{}", constants::NATIVES_PREFIX, time, process::id())); + + // create_dir_all suppresses "AlreadyExists", but this is a fatal error here. + fs::create_dir(&natives_dir).await.map_err(|e| LaunchError::IO { + what: "creating natives directory", + error: e + })?; + + let natives_dir_ref = natives_dir.as_path(); + + stream::iter(libs) + .map(|lib| Ok(async move { + // TODO: Self::extract_natives_for(natives_dir_ref, lib) + todo!() as Result<usize, _> + })) + .try_buffer_unordered(8) + .try_fold(0usize, |accum, num| async move {Ok(accum + num)}) + .await.inspect(|num| debug!("Extracted {} native entries to {}", num, natives_dir_ref.display())) + .map(|_| natives_dir) + } } impl SystemInfo { diff --git a/src/launcher/assets.rs b/src/launcher/assets.rs index 7ad368e..e732877 100644 --- a/src/launcher/assets.rs +++ b/src/launcher/assets.rs @@ -168,7 +168,10 @@ impl AssetRepository { if let Some(expect) = index.sha1 {
let actual = Sha1::from(&idx_text).digest();
- return Err(AssetError::Integrity(IntegrityError::Sha1Mismatch { expect, actual }));
+
+ if actual != expect {
+ return Err(AssetError::Integrity(IntegrityError::Sha1Mismatch { expect, actual }));
+ }
}
debug!("Saving downloaded asset index to {}", path.display());
diff --git a/src/launcher/constants.rs b/src/launcher/constants.rs index 83611c9..a0ef036 100644 --- a/src/launcher/constants.rs +++ b/src/launcher/constants.rs @@ -1,4 +1,6 @@ use const_format::formatcp; +use lazy_static::lazy_static; +use regex::Regex; const PKG_NAME: &str = env!("CARGO_PKG_NAME"); const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -8,3 +10,9 @@ pub const USER_AGENT: &str = formatcp!("{PKG_NAME}/{PKG_VERSION} (in {CRATE_NAME pub const URL_VERSION_MANIFEST: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; pub const URL_RESOURCE_BASE: &str = "https://resources.download.minecraft.net/"; pub const URL_JRE_MANIFEST: &str = "https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"; + +pub const NATIVES_PREFIX: &str = "natives-"; + +lazy_static! { + pub static ref NATIVES_DIR_PATTERN: Regex = Regex::new("^natives-(\\d+)").unwrap(); +}
\ No newline at end of file |
