aboutsummaryrefslogtreecommitdiff
path: root/src-tauri/src
diff options
context:
space:
mode:
Diffstat (limited to 'src-tauri/src')
-rw-r--r--src-tauri/src/constants.rs3
-rw-r--r--src-tauri/src/github/pull_requests.rs17
-rw-r--r--src-tauri/src/github/release_notes.rs45
-rw-r--r--src-tauri/src/main.rs13
-rw-r--r--src-tauri/src/mod_management/mod.rs44
-rw-r--r--src-tauri/src/northstar/install.rs28
-rw-r--r--src-tauri/src/northstar/mod.rs22
-rw-r--r--src-tauri/src/platform_specific/linux.rs167
-rw-r--r--src-tauri/src/platform_specific/mod.rs19
-rw-r--r--src-tauri/src/platform_specific/windows.rs70
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))
+ }
+}