diff options
Diffstat (limited to 'src-tauri/src')
-rw-r--r-- | src-tauri/src/constants.rs | 3 | ||||
-rw-r--r-- | src-tauri/src/github/pull_requests.rs | 17 | ||||
-rw-r--r-- | src-tauri/src/github/release_notes.rs | 45 | ||||
-rw-r--r-- | src-tauri/src/main.rs | 13 | ||||
-rw-r--r-- | src-tauri/src/mod_management/mod.rs | 44 | ||||
-rw-r--r-- | src-tauri/src/northstar/install.rs | 28 | ||||
-rw-r--r-- | src-tauri/src/northstar/mod.rs | 22 | ||||
-rw-r--r-- | src-tauri/src/platform_specific/linux.rs | 167 | ||||
-rw-r--r-- | src-tauri/src/platform_specific/mod.rs | 19 | ||||
-rw-r--r-- | src-tauri/src/platform_specific/windows.rs | 70 |
10 files changed, 263 insertions, 165 deletions
diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 47eeef19..3ad2d6e8 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -26,6 +26,9 @@ pub const BLACKLISTED_MODS: [&str; 3] = [ "ebkr-r2modman", ]; +/// List of Thunderstoremods that have some specific install requirements that makes them different from standard mods +pub const MODS_WITH_SPECIAL_REQUIREMENTS: [&str; 1] = ["NanohmProtogen-VanillaPlus"]; + /// Order in which the sections for release notes should be displayed pub const SECTION_ORDER: [&str; 11] = [ "feat", "fix", "docs", "style", "refactor", "build", "test", "i18n", "ci", "chore", "other", diff --git a/src-tauri/src/github/pull_requests.rs b/src-tauri/src/github/pull_requests.rs index bf7a8fdb..de733feb 100644 --- a/src-tauri/src/github/pull_requests.rs +++ b/src-tauri/src/github/pull_requests.rs @@ -32,6 +32,7 @@ pub struct PullsApiResponseElement { url: String, head: CommitHead, html_url: String, + labels: Vec<String>, } // GitHub API response JSON elements as structs @@ -48,6 +49,7 @@ struct ActionsRunsResponse { #[derive(Debug, Deserialize, Clone)] struct Artifact { id: u64, + name: String, workflow_run: WorkflowRun, } @@ -101,6 +103,14 @@ pub async fn get_pull_requests( repo, }; + // Get labels and their names and put the into vector + let label_names: Vec<String> = item + .labels + .unwrap_or_else(Vec::new) + .into_iter() + .map(|label| label.name) + .collect(); + // TODO there's probably a way to automatically serialize into the struct but I don't know yet how to let elem = PullsApiResponseElement { number: item.number, @@ -111,6 +121,7 @@ pub async fn get_pull_requests( .html_url .ok_or(anyhow!("html_url not found"))? .to_string(), + labels: label_names, }; all_pull_requests.push(elem); @@ -206,8 +217,14 @@ pub async fn get_launcher_download_link(commit_sha: String) -> Result<String, St ) .unwrap(); + let multiple_artifacts = artifacts_response.artifacts.len() > 1; + // Iterate over artifacts for artifact in artifacts_response.artifacts { + if multiple_artifacts && !artifact.name.starts_with("NorthstarLauncher-MSVC") { + continue; + } + // Make sure artifact and CI run commit head sha match if artifact.workflow_run.head_sha == workflow_run.head_sha { // Download artifact diff --git a/src-tauri/src/github/release_notes.rs b/src-tauri/src/github/release_notes.rs index e3a14537..4adfb24b 100644 --- a/src-tauri/src/github/release_notes.rs +++ b/src-tauri/src/github/release_notes.rs @@ -1,3 +1,4 @@ +use rand::prelude::SliceRandom; use serde::{Deserialize, Serialize}; use std::vec::Vec; use ts_rs::TS; @@ -168,9 +169,51 @@ pub async fn generate_release_note_announcement() -> Result<String, String> { let modders_info = "Mod compatibility should not be impacted"; let server_hosters_info = "REPLACE ME"; + let mut rng = rand::thread_rng(); + let attributes = vec![ + "adorable", + "amazing", + "beautiful", + "blithsome", + "brilliant", + "compassionate", + "dazzling", + "delightful", + "distinguished", + "elegant", + "enigmatic", + "enthusiastic", + "fashionable", + "fortuitous", + "friendly", + "generous", + "gleeful", + "gorgeous", + "handsome", + "lively", + "lovely", + "lucky", + "lustrous", + "marvelous", + "merry", + "mirthful", + "phantasmagorical", + "pretty", + "propitious", + "ravishing", + "sincere", + "sophisticated fellow", + "stupendous", + "vivacious", + "wonderful", + "zestful", + ]; + + let selected_attribute = attributes.choose(&mut rng).unwrap(); + // Build announcement string let return_string = format!( - r"Hello beautiful people <3 + r"Hello {selected_attribute} people <3 **Northstar `{current_ns_version}` is out!** {general_info} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0654d626..a9f484f5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,11 +3,7 @@ windows_subsystem = "windows" )] -use std::{ - env, - sync::{Arc, Mutex}, - time::Duration, -}; +use std::{env, time::Duration}; mod constants; mod development; @@ -42,9 +38,6 @@ pub struct NorthstarThunderstoreReleaseWrapper { value: NorthstarThunderstoreRelease, } -#[derive(Default)] -struct Counter(Arc<Mutex<i32>>); - fn main() { // Setup logger let mut log_builder = pretty_env_logger::formatted_builder(); @@ -114,7 +107,7 @@ fn main() { Ok(()) }) - .manage(Counter(Default::default())) + .manage(()) .invoke_handler(tauri::generate_handler![ development::install_git_main, github::compare_tags, @@ -143,10 +136,10 @@ fn main() { northstar::profile::delete_profile, northstar::profile::fetch_profiles, northstar::profile::validate_profile, + platform_specific::check_cgnat, platform_specific::get_host_os, platform_specific::get_local_northstar_proton_wrapper_version, platform_specific::install_northstar_proton_wrapper, - platform_specific::linux_checks, platform_specific::uninstall_northstar_proton_wrapper, repair_and_verify::clean_up_download_folder_wrapper, repair_and_verify::disable_all_but_core, diff --git a/src-tauri/src/mod_management/mod.rs b/src-tauri/src/mod_management/mod.rs index 049eaa6e..2a018920 100644 --- a/src-tauri/src/mod_management/mod.rs +++ b/src-tauri/src/mod_management/mod.rs @@ -1,12 +1,13 @@ // This file contains various mod management functions -use crate::constants::{BLACKLISTED_MODS, CORE_MODS}; +use crate::constants::{BLACKLISTED_MODS, CORE_MODS, MODS_WITH_SPECIAL_REQUIREMENTS}; use async_recursion::async_recursion; use thermite::prelude::ThermiteError; use crate::NorthstarMod; use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; +use std::error::Error; use std::str::FromStr; use std::string::ToString; use std::{fs, path::PathBuf}; @@ -46,9 +47,9 @@ impl std::str::FromStr for ParsedThunderstoreModString { } } -impl ToString for ParsedThunderstoreModString { - fn to_string(&self) -> String { - format!("{}-{}-{}", self.author_name, self.mod_name, self.version) +impl std::fmt::Display for ParsedThunderstoreModString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{}-{}", self.author_name, self.mod_name, self.version) } } @@ -505,10 +506,14 @@ fn delete_older_versions( /// Checks whether some mod is correctly formatted /// Currently checks whether /// - Some `mod.json` exists under `mods/*/mod.json` -fn fc_sanity_check(input: &&fs::File) -> bool { +fn fc_sanity_check(input: &&fs::File) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { let mut archive = match zip::read::ZipArchive::new(*input) { Ok(archive) => archive, - Err(_) => return false, + Err(_) => { + return Err(Box::new(ThermiteError::UnknownError( + "Failed reading zip file".into(), + ))) + } }; let mut has_mods = false; @@ -538,14 +543,22 @@ fn fc_sanity_check(input: &&fs::File) -> bool { if name.to_str().unwrap().contains(".dll") { log::warn!("Plugin detected, prompting user"); if !plugins::plugin_prompt() { - return false; // Plugin detected and user denied install + return Err(Box::new(ThermiteError::UnknownError( + "Plugin detected and install denied".into(), + ))); } } } } } - has_mods && mod_json_exists + if has_mods && mod_json_exists { + Ok(()) + } else { + Err(Box::new(ThermiteError::UnknownError( + "Mod not correctly formatted".into(), + ))) + } } // Copied from `libtermite` source code and modified @@ -596,6 +609,16 @@ pub async fn fc_download_mod_and_install( } } + // Prevent installing mods that have specific install requirements + for special_mod in MODS_WITH_SPECIAL_REQUIREMENTS { + if thunderstore_mod_string.contains(special_mod) { + return Err(format!( + "{} has special install requirements and cannot be installed with FlightCore", + thunderstore_mod_string + )); + } + } + // Get download URL for the specified mod let download_url = get_ns_mod_download_url(thunderstore_mod_string).await?; @@ -643,9 +666,8 @@ pub async fn fc_download_mod_and_install( Err(err) => { log::warn!("libthermite couldn't install mod {thunderstore_mod_string} due to {err:?}",); return match err { - ThermiteError::SanityError => Err( - "Mod failed sanity check during install. It's probably not correctly formatted" - .to_string(), + ThermiteError::SanityError(e) => Err( + format!("Mod failed sanity check during install. It's probably not correctly formatted. {}", e) ), _ => Err(err.to_string()), }; diff --git a/src-tauri/src/northstar/install.rs b/src-tauri/src/northstar/install.rs index 9d9b43d1..89631fdb 100644 --- a/src-tauri/src/northstar/install.rs +++ b/src-tauri/src/northstar/install.rs @@ -306,7 +306,7 @@ pub async fn install_northstar( pub fn find_game_install_location() -> Result<GameInstall, String> { // Attempt parsing Steam library directly match steamlocate::SteamDir::locate() { - Some(mut steamdir) => { + Ok(steamdir) => { #[cfg(target_os = "linux")] { let snap_dir = match std::env::var("SNAP_USER_DATA") { @@ -318,25 +318,37 @@ pub fn find_game_install_location() -> Result<GameInstall, String> { .join("snap"), }; - if steamdir.path.starts_with(snap_dir) { + if steamdir.path().starts_with(snap_dir) { log::warn!("Found Steam installed via Snap, you may encounter issues"); } } - match steamdir.app(&thermite::TITANFALL2_STEAM_ID) { - Some(app) => { - // println!("{:#?}", app); + match steamdir.find_app(thermite::TITANFALL2_STEAM_ID) { + Ok(Some((app, library))) => { + let app_path = library + .path() + .join("steamapps") + .join("common") + .join(app.install_dir) + .into_os_string() + .into_string() + .unwrap(); + let game_install = GameInstall { - game_path: app.path.to_str().unwrap().to_string(), + game_path: app_path, profile: "R2Northstar".to_string(), install_type: InstallType::STEAM, }; return Ok(game_install); } - None => log::info!("Couldn't locate Titanfall2 Steam install"), + Ok(None) => log::info!("Couldn't locate your Titanfall 2 Steam install."), + Err(err) => log::info!( + "Something went wrong while trying to find Titanfall 2 {}", + err + ), } } - None => log::info!("Couldn't locate Steam on this computer!"), + Err(err) => log::info!("Couldn't locate Steam on this computer! {}", err), } // (On Windows only) try parsing Windows registry for Origin install path diff --git a/src-tauri/src/northstar/mod.rs b/src-tauri/src/northstar/mod.rs index 0b37c3f6..4b16f701 100644 --- a/src-tauri/src/northstar/mod.rs +++ b/src-tauri/src/northstar/mod.rs @@ -235,19 +235,25 @@ pub fn launch_northstar_steam(game_install: GameInstall) -> Result<String, Strin } match steamlocate::SteamDir::locate() { - Some(mut steamdir) => { + Ok(steamdir) => { if get_host_os() != "windows" { - match steamdir.compat_tool(&thermite::TITANFALL2_STEAM_ID) { - Some(_) => {} - None => { - return Err( - "Titanfall2 was not configured to use a compatibility tool".to_string() - ); + match steamdir.compat_tool_mapping() { + Ok(map) => match map.get(&thermite::TITANFALL2_STEAM_ID) { + Some(_) => {} + None => { + return Err( + "Titanfall2 was not configured to use a compatibility tool" + .to_string(), + ); + } + }, + Err(_) => { + return Err("Could not get compatibility tool mapping".to_string()); } } } } - None => { + Err(_) => { return Err("Couldn't access Titanfall2 directory".to_string()); } } diff --git a/src-tauri/src/platform_specific/linux.rs b/src-tauri/src/platform_specific/linux.rs index 706a4d22..fcac5b67 100644 --- a/src-tauri/src/platform_specific/linux.rs +++ b/src-tauri/src/platform_specific/linux.rs @@ -1,73 +1,79 @@ // Linux specific code -use regex::Regex; -use std::process::Command; - -// I intend to add more linux related stuff to check here, so making a func -// for now tho it only checks `ldd --version` -// - salmon -pub fn linux_checks_librs() -> Result<(), String> { - // Perform various checks in terms of Linux compatibility - // Return early with error message if a check fails - - // check `ldd --version` to see if glibc is up to date for northstar proton - let min_required_ldd_version = 2.33; - let lddv = check_glibc_v(); - if lddv < min_required_ldd_version { - return Err(format!( - "GLIBC is not version {} or greater", - min_required_ldd_version - )); +fn get_proton_dir() -> Result<String, String> { + let steam_dir = match steamlocate::SteamDir::locate() { + Ok(result) => result, + Err(_) => return Err("Unable to find Steam directory".to_string()), }; + let compat_dir = format!("{}/compatibilitytools.d", steam_dir.path().display()); - // All checks passed - Ok(()) -} - -fn get_proton_dir() -> Option<String> { - let steam_dir = steamlocate::SteamDir::locate()?; - let compat_dir = format!("{}/compatibilitytools.d/", steam_dir.path.display()); - - Some(compat_dir) + Ok(compat_dir) } /// Downloads and installs NS proton /// Assumes Steam install -pub fn install_ns_proton() -> Result<(), thermite::prelude::ThermiteError> { +pub fn install_ns_proton() -> Result<(), String> { // Get latest NorthstarProton release - let latest = thermite::core::latest_release()?; + let latest = match thermite::core::latest_release() { + Ok(result) => result, + Err(_) => return Err("Failed to fetch latest NorthstarProton release".to_string()), + }; let temp_dir = std::env::temp_dir(); let path = format!("{}/nsproton-{}.tar.gz", temp_dir.display(), latest); - let archive = std::fs::File::create(path.clone())?; + let archive = match std::fs::File::create(path.clone()) { + Ok(result) => result, + Err(_) => return Err("Failed to allocate NorthstarProton archive on disk".to_string()), + }; // Download the latest Proton release log::info!("Downloading NorthstarProton to {}", path); - thermite::core::download_ns_proton(latest, archive)?; + match thermite::core::download_ns_proton(latest, archive) { + Ok(_) => {} + Err(_) => return Err("Failed to download NorthstarProton".to_string()), + } + log::info!("Finished Download"); - let compat_dir = get_proton_dir().unwrap(); - std::fs::create_dir_all(compat_dir.clone())?; + let compat_dir = get_proton_dir()?; - let finished = std::fs::File::open(path.clone())?; + match std::fs::create_dir_all(compat_dir.clone()) { + Ok(_) => {} + Err(_) => return Err("Failed to create compatibilitytools directory".to_string()), + } + + let finished = match std::fs::File::open(path.clone()) { + Ok(result) => result, + Err(_) => return Err("Failed to open NorthstarProton archive".to_string()), + }; // Extract to Proton dir log::info!("Installing NorthstarProton to {}", compat_dir); - thermite::core::install_ns_proton(&finished, compat_dir)?; + match thermite::core::install_ns_proton(&finished, compat_dir) { + Ok(_) => {} + Err(_) => return Err("Failed to create install NorthstarProton".to_string()), + } log::info!("Finished Installation"); drop(finished); - std::fs::remove_file(path)?; + // We installed NSProton, lets ignore this if it fails + let _ = std::fs::remove_file(path); Ok(()) } /// Remove NS Proton pub fn uninstall_ns_proton() -> Result<(), String> { - let compat_dir = get_proton_dir().unwrap(); - let pattern = format!("{}/NorthstarProton-*", compat_dir); + let compat_dir = get_proton_dir()?; + let pattern = format!("{}/NorthstarProton*", compat_dir); for e in glob::glob(&pattern).expect("Failed to read glob pattern") { - std::fs::remove_dir_all(e.unwrap()).unwrap(); + match e { + Ok(path) => match std::fs::remove_dir_all(path.clone()) { + Ok(_) => {} + Err(_) => return Err(format!("Failed to remove {}", path.display())), + }, + Err(e) => return Err(format!("Found unprocessable entry {}", e)), + } } Ok(()) @@ -76,84 +82,17 @@ pub fn uninstall_ns_proton() -> Result<(), String> { /// Get the latest installed NS Proton version pub fn get_local_ns_proton_version() -> Result<String, String> { let compat_dir = get_proton_dir().unwrap(); - let ns_prefix = "NorthstarProton-"; - let pattern = format!("{}/{}*/version", compat_dir, ns_prefix); - - let mut version: String = "".to_string(); + let pattern = format!("{}/NorthstarProton*/version", compat_dir); - for e in glob::glob(&pattern).expect("Failed to read glob pattern") { + if let Some(e) = glob::glob(&pattern) + .expect("Failed to read glob pattern") + .next() + { let version_content = std::fs::read_to_string(e.unwrap()).unwrap(); - let version_string = version_content.split(' ').nth(1).unwrap(); - - if version_string.starts_with(ns_prefix) { - version = version_string[ns_prefix.len()..version_string.len() - 1] - .to_string() - .clone(); - } - } + let version = version_content.split(' ').nth(1).unwrap().to_string(); - if version.is_empty() { - return Err("Northstar Proton is not installed".to_string()); + return Ok(version); } - Ok(version) -} - -pub fn check_glibc_v() -> f32 { - let out = Command::new("/bin/ldd") - .arg("--version") - .output() - .expect("failed to run 'ldd --version'"); - - // parse the output down to just the first line - let lddva = String::from_utf8_lossy(&out.stdout); - let lddvl: Vec<&str> = lddva.split('\n').collect(); - let lddvlo = &lddvl[0]; - let reg = Regex::new(r"(2.\d{2}$)").unwrap(); - if let Some(caps) = reg.captures_iter(lddvlo).next() { - return caps.get(1).unwrap().as_str().parse::<f32>().unwrap(); // theres prolly a better way ijdk how tho - } - 0.0 // this shouldnt ever be reached but it has to be here + Err("Northstar Proton is not installed".to_string()) } - -/* -Outputs of ldd --verssion from distros, all we care about is the first line so trimmed, also removed all duplicates -Thanks tony -Distros not included: AmazonLinux, Gentoo, Kali, Debian before 11, Oracle Linux, Scientific Linux, Slackware, Mageia, Neurodebian, RHEL 8 and 9 (Same as AlmaLinux), RockyLinux (Same as AlmaLinux), Ubuntu before 20.04 - -AlmaLinux 8 -ldd (GNU libc) 2.35 - -Centos Stream 8 -ldd (GNU libc) 2.28 - -Centos Stream 9 -ldd (GNU libc) 2.34 - -Centos 7 -ldd (GNU libc) 2.17 - -Debian 11 -ldd (Debian GLIBC 2.31-13+deb11u4) 2.31 - -Debian Testing -ldd (Debian GLIBC 2.35-1) 2.35 - -Debian Unstable -ldd (Debian GLIBC 2.35-3) 2.35 - -Fedora 37 -ldd (GNU libc) 2.36 - -Opensuse Leap -ldd (GNU libc) 2.31 - -Ubuntu 20.04 -ldd (Ubuntu GLIBC 2.31-0ubuntu9.9) 2.31 - -Ubuntu 22.04 -ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35 - -Ubuntu 22.10 -ldd (Ubuntu GLIBC 2.36-0ubuntu2) 2.36 -*/ diff --git a/src-tauri/src/platform_specific/mod.rs b/src-tauri/src/platform_specific/mod.rs index 8dca9424..4e0514d4 100644 --- a/src-tauri/src/platform_specific/mod.rs +++ b/src-tauri/src/platform_specific/mod.rs @@ -39,19 +39,12 @@ pub async fn get_local_northstar_proton_wrapper_version() -> Result<String, Stri Err("Not supported on Windows".to_string()) } -/// Returns true if linux compatible +/// Check whether the current device might be behind a CGNAT #[tauri::command] -pub async fn linux_checks() -> Result<(), String> { - // Different behaviour depending on OS - // MacOS is missing as it is not a target - // in turn this means this application will not build on MacOS. - #[cfg(target_os = "windows")] - { - Err("Not available on Windows".to_string()) - } - +pub async fn check_cgnat() -> Result<String, String> { #[cfg(target_os = "linux")] - { - linux::linux_checks_librs() - } + return Err("Not supported on Linux".to_string()); + + #[cfg(target_os = "windows")] + windows::check_cgnat().await } diff --git a/src-tauri/src/platform_specific/windows.rs b/src-tauri/src/platform_specific/windows.rs index 678e5be5..fc6aab5d 100644 --- a/src-tauri/src/platform_specific/windows.rs +++ b/src-tauri/src/platform_specific/windows.rs @@ -1,5 +1,6 @@ /// Windows specific code use anyhow::{anyhow, Result}; +use std::net::Ipv4Addr; #[cfg(target_os = "windows")] use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey}; @@ -32,3 +33,72 @@ pub fn origin_install_location_detection() -> Result<String, anyhow::Error> { Err(anyhow!("No Origin / EA App install path found")) } + +/// Check whether the current device might be behind a CGNAT +pub async fn check_cgnat() -> Result<String, String> { + // Use external service to grap IP + let url = "https://api.ipify.org"; + let response = reqwest::get(url).await.unwrap().text().await.unwrap(); + + // Check if valid IPv4 address and return early if not + if response.parse::<Ipv4Addr>().is_err() { + return Err(format!("Not valid IPv4 address: {}", response)); + } + + let hops_count = run_tracert(&response)?; + Ok(format!("Counted {} hops to {}", hops_count, response)) +} + +/// Count number of hops in tracert output +fn count_hops(output: &str) -> usize { + // Split the output into lines + let lines: Vec<&str> = output.lines().collect(); + + // Filter lines that appear to represent hops + let hop_lines: Vec<&str> = lines + .iter() + .filter(|&line| line.contains("ms") || line.contains("*")) // TODO check if it contains just the `ms` surrounded by whitespace, otherwise it might falsely pick up some domain names as well + .cloned() + .collect(); + + // Return the number of hops + hop_lines.len() +} + +/// Run `tracert` +fn run_tracert(target_ip: &str) -> Result<usize, String> { + // Ensure valid IPv4 address to avoid prevent command injection + assert!(target_ip.parse::<Ipv4Addr>().is_ok()); + + // Execute the `tracert` command + let output = match std::process::Command::new("tracert") + .arg("-4") // Force IPv4 + .arg("-d") // Prevent resolving intermediate IP addresses + .arg("-w") // Set timeout to 1 second + .arg("1000") + .arg("-h") // Set max hop count + .arg("5") + .arg(target_ip) + .output() + { + Ok(res) => res, + Err(err) => return Err(format!("Failed running tracert: {}", err)), + }; + + // Check if the command was successful + if output.status.success() { + // Convert the output to a string + let stdout = + std::str::from_utf8(&output.stdout).expect("Invalid UTF-8 sequence in command output"); + println!("{}", stdout); + + // Count the number of hops + let hop_count = count_hops(stdout); + Ok(hop_count) + } else { + let stderr = std::str::from_utf8(&output.stderr) + .expect("Invalid UTF-8 sequence in command error output"); + println!("{}", stderr); + Err(format!("Failed collecting tracert output: {}", stderr)) + } +} |