OneLauncher-Cracked/packages/oneclient_core/src/error.rs

278 lines
8.3 KiB
Rust

use thiserror::Error;
#[derive(Debug, Error)]
pub enum LauncherError {
#[error("Unable to parse JSON: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Database execution failed: {0}")]
SqlError(#[from] sqlx::Error),
#[error(transparent)]
IoError(#[from] polyio::IOError),
#[error(transparent)]
StdIoError(#[from] std::io::Error),
#[error(transparent)]
RequestError(#[from] oneclient_net::RequestError),
#[error(transparent)]
PathsError(#[from] oneclient_common::PathsError),
#[error(transparent)]
DbError(#[from] oneclient_db::DbError),
#[error("launcher core is not initialized")]
NotInitialized,
#[error("launcher core is already initialized")]
AlreadyInitialized,
#[error("invalid settings profile: {reason}")]
InvalidSettingsProfile { reason: String },
#[error(transparent)]
JavaError(#[from] oneclient_java::JavaError),
#[error(transparent)]
UrlError(#[from] url::ParseError),
#[error(transparent)]
EventError(#[from] oneclient_events::EventError),
#[error(transparent)]
PackageError(#[from] oneclient_content::packages::PackageError),
#[error(transparent)]
AuthError(#[from] oneclient_auth::AuthError),
#[error(transparent)]
ClusterError(#[from] crate::clusters::ClusterError),
#[error(transparent)]
McError(#[from] oneclient_mc::McError),
#[error(transparent)]
ContentError(#[from] oneclient_content::ContentError),
#[error(transparent)]
BundleError(#[from] oneclient_content::bundles::BundleError),
#[error(transparent)]
GameError(#[from] crate::game::GameError),
#[error(transparent)]
LogsError(#[from] oneclient_cluster::logs::LogsError),
#[error(transparent)]
ScreenshotsError(#[from] oneclient_cluster::screenshots::ScreenshotsError),
#[error("minecraft: {0}")]
Minecraft(String),
}
impl LauncherError {
#[must_use]
pub fn auth_guidance(&self) -> Option<oneclient_auth::AuthErrorGuidance> {
match self {
LauncherError::AuthError(oneclient_auth::AuthError::Minecraft(err)) => {
oneclient_auth::diagnose_auth_error(err)
}
_ => None,
}
}
/// Not something to report the dialog is already gone by the time it lands
#[must_use]
pub fn is_login_cancelled(&self) -> bool {
matches!(
self,
LauncherError::AuthError(oneclient_auth::AuthError::LoginCancelled)
)
}
/// A dialog the user backed out of rather than anything that went wrong
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.is_login_cancelled()
|| matches!(
self,
LauncherError::JavaError(oneclient_java::JavaError::Cancelled)
)
}
/// Whether the install is missing pieces and so is worth repairing
#[must_use]
pub fn indicates_missing_files(&self) -> bool {
let mut source: Option<&(dyn std::error::Error + 'static)> = Some(self);
// Walk the chain the `io::Error` carrying the kind is usually wrapped
// two or three layers deep by the time this surfaces
while let Some(err) = source {
if let Some(io) = err.downcast_ref::<std::io::Error>()
&& io.kind() == std::io::ErrorKind::NotFound
{
return true;
}
source = err.source();
}
false
}
}
pub trait SentryExclusion {
fn is_sentry_excluded(&self) -> bool {
false
}
}
impl SentryExclusion for LauncherError {
fn is_sentry_excluded(&self) -> bool {
match self {
LauncherError::StdIoError(e) => e.is_sentry_excluded(),
LauncherError::IoError(e) => e.is_sentry_excluded(),
LauncherError::RequestError(e) => e.is_transient(),
LauncherError::JavaError(e) => e.is_transient(),
_ => false,
}
}
}
impl SentryExclusion for std::io::Error {
fn is_sentry_excluded(&self) -> bool {
use std::io::ErrorKind;
// `ErrorKind::StorageFull` is still unstable so match raw OS codes
// Platform-gated 112 is the unrelated EHOSTDOWN on Linux
if let Some(code) = self.raw_os_error() {
#[cfg(unix)]
if code == 28 {
// ENOSPC
return true;
}
#[cfg(windows)]
if code == 112 || code == 39 {
// ERROR_DISK_FULL / ERROR_HANDLE_DISK_FULL
return true;
}
let _ = code;
}
matches!(
self.kind(),
ErrorKind::ConnectionRefused
| ErrorKind::ConnectionReset
| ErrorKind::ConnectionAborted
| ErrorKind::NotConnected
| ErrorKind::TimedOut
)
}
}
impl SentryExclusion for reqwest::Error {
fn is_sentry_excluded(&self) -> bool {
// Connectivity problems are the user's network not a launcher bug
self.is_timeout() || self.is_connect()
}
}
impl SentryExclusion for polyio::IOError {
fn is_sentry_excluded(&self) -> bool {
match self {
polyio::IOError::IOError(source)
| polyio::IOError::PathIOError { source, .. } => source.is_sentry_excluded(),
_ => false,
}
}
}
pub type LauncherResult<T> = Result<T, LauncherError>;
#[cfg(test)]
mod tests {
use super::*;
/// Raw OS error rather than a synthesized `ErrorKind` same kind but only
/// this one renders the `(os error 2)` text users report
fn not_found(path: &str) -> LauncherError {
LauncherError::IoError(polyio::IOError::PathIOError {
source: std::io::Error::from_raw_os_error(2),
path: path.to_string(),
})
}
#[test]
fn a_missing_asset_directory_asks_for_a_repair() {
let path =
"/Users/someone/Library/Application Support/org.Polyfrost.OneClient-dev/metadata/assets";
let err = not_found(path);
assert!(err.indicates_missing_files(), "{err}");
assert!(err.to_string().contains(path), "{err}");
}
#[test]
fn a_not_found_nested_several_layers_deep_is_still_found() {
let err = LauncherError::McError(oneclient_mc::McError::Io(polyio::IOError::PathIOError {
source: std::io::Error::from(std::io::ErrorKind::NotFound),
path: "metadata/libraries/foo.jar".to_string(),
}));
assert!(err.indicates_missing_files(), "{err}");
}
#[test]
fn dismissing_a_dialog_is_a_cancellation_not_a_failure() {
assert!(
LauncherError::JavaError(oneclient_java::JavaError::Cancelled).is_cancelled()
);
assert!(
LauncherError::AuthError(oneclient_auth::AuthError::LoginCancelled).is_cancelled()
);
}
/// The whole point of the split anything that is not a dismissal keeps
/// reporting at `error`
#[test]
fn real_java_failures_are_not_cancellations() {
for err in [
oneclient_java::JavaError::MissingJava,
oneclient_java::JavaError::PackageNotFound { major: 21 },
oneclient_java::JavaError::InvalidJavaPath {
path: "/opt/not-java".to_string(),
},
oneclient_java::JavaError::VersionMismatch {
expected: 21,
found: 8,
},
] {
let err = LauncherError::JavaError(err);
assert!(!err.is_cancelled(), "{err}");
}
assert!(!LauncherError::NotInitialized.is_cancelled());
assert!(!not_found("metadata/assets").is_cancelled());
}
#[test]
fn failures_a_repair_could_not_fix_are_left_alone() {
for kind in [
std::io::ErrorKind::PermissionDenied,
std::io::ErrorKind::ConnectionRefused,
std::io::ErrorKind::TimedOut,
] {
let err = LauncherError::IoError(polyio::IOError::PathIOError {
source: std::io::Error::from(kind),
path: "metadata/assets".to_string(),
});
assert!(!err.indicates_missing_files(), "{kind:?} should not repair");
}
assert!(!LauncherError::NotInitialized.indicates_missing_files());
assert!(
!LauncherError::Minecraft("bad manifest".into()).indicates_missing_files()
);
}
}