Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,10 @@ jobs:
env:
RUSTFLAGS: -D warnings

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable

- uses: swatinem/rust-cache@v2
with:
workspaces: core

- name: Core tests
working-directory: core
run: cargo test --verbose
run: cargo test

- name: App tests
working-directory: app/src-tauri
run: cargo test --lib
64 changes: 63 additions & 1 deletion app/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ fix-core = { path = "../../core" }
librqbit = "8"
bytes = "1"
tokio = { version = "1", features = ["time"] }
steamlocate = "2.1"
keyvalues-parser = "0.2"

[dev-dependencies]
tempfile = "3"
Expand Down
2 changes: 1 addition & 1 deletion app/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ pub async fn game_assets(title: String) -> Result<Option<fix_core::GameAssets>,

pub async fn cancel_download(app: tauri::AppHandle, engine: tauri::State<'_, DownloadEngine>, title: String) -> Result<bool, String> {

let safe_title = title.replace('/', "_");
let safe_title = crate::safe_title(&title);

let http_found = {

Expand Down
10 changes: 4 additions & 6 deletions app/src-tauri/src/game_process.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::process::Command;

#[cfg(not(windows))]
use std::process::Stdio;

Expand All @@ -13,7 +11,7 @@ pub fn game_launch_started(appid: u32) -> bool {

let pattern = format!("reaper SteamLaunch AppId={}", appid);

Command::new("pgrep")
crate::quiet_command::quiet_command("pgrep")
.args(["-f", &pattern])
.stdout(Stdio::null())
.stderr(Stdio::null())
Expand Down Expand Up @@ -79,7 +77,7 @@ fn parse_ps_game_pid(output: &str, exe_basename: &str) -> Option<u32> {
#[cfg(not(windows))]
pub fn find_game_pid(exe_basename: &str) -> Option<u32> {

let output = Command::new("ps").args(["-eo", "pid=,args="]).output().ok()?;
let output = crate::quiet_command::quiet_command("ps").args(["-eo", "pid=,args="]).output().ok()?;

parse_ps_game_pid(&String::from_utf8_lossy(&output.stdout), exe_basename)

Expand Down Expand Up @@ -113,7 +111,7 @@ pub fn find_game_pid(exe_basename: &str) -> Option<u32> {

let filter = format!("IMAGENAME eq {}", exe_basename);

let output = Command::new("tasklist").args(["/FI", &filter, "/FO", "CSV", "/NH"]).output().ok()?;
let output = crate::quiet_command::quiet_command("tasklist").args(["/FI", &filter, "/FO", "CSV", "/NH"]).output().ok()?;

parse_tasklist_csv_pid(&String::from_utf8_lossy(&output.stdout), exe_basename)

Expand All @@ -131,7 +129,7 @@ pub fn pid_alive(pid: u32) -> bool {

let filter = format!("PID eq {}", pid);

Command::new("tasklist")
crate::quiet_command::quiet_command("tasklist")
.args(["/FI", &filter, "/NH"])
.output()
.map(|output| crate::steam_client::tasklist_reports_running(&String::from_utf8_lossy(&output.stdout), &pid.to_string()))
Expand Down
118 changes: 53 additions & 65 deletions app/src-tauri/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,89 +91,72 @@ pub fn games_root() -> Option<std::path::PathBuf> {

}

pub fn game_folder(title: &str) -> Option<String> {

games_root().map(|root| root.join(title).to_string_lossy().to_string())

}

pub fn find_shortcuts_vdf() -> Option<String> {

let mut roots: Vec<std::path::PathBuf> = Vec::new();

if let Some(home) = home_dir() {

let home_path = std::path::PathBuf::from(&home);

roots.push(home_path.join(".steam/steam/userdata"));

roots.push(home_path.join(".local/share/Steam/userdata"));

}

if let Ok(x86) = std::env::var("ProgramFiles(x86)") {
const WINDOWS_RESERVED_NAMES: [&str; 22] = [
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
];

// One folder name on every OS, valid on the strictest (Windows): catalog titles carry ':' and '?'
// ("Subtitle: Edition"), which create_dir_all rejects there.
pub fn safe_title(title: &str) -> String {

let replaced: String = title
.chars()
.map(|character| if "<>:\"/\\|?*".contains(character) || character.is_control() { '_' } else { character })
.collect();

roots.push(std::path::PathBuf::from(&x86).join("Steam/userdata"));
let trimmed = replaced.trim_end_matches(['.', ' ']).trim_start();

}
let stem = trimmed.split('.').next().unwrap_or("").to_uppercase();

if let Ok(program_files) = std::env::var("ProgramFiles") {
if trimmed.is_empty() {

roots.push(std::path::PathBuf::from(&program_files).join("Steam/userdata"));
return String::from("_");

}

if let Ok(steam) = std::env::var("STEAM_PATH") {
if WINDOWS_RESERVED_NAMES.contains(&stem.as_str()) {

roots.push(std::path::PathBuf::from(steam).join("userdata"));
return format!("{}_", trimmed);

}

for userdata in roots {

let Ok(entries) = std::fs::read_dir(&userdata) else {

continue;
trimmed.to_string()

};
}

for entry in entries.flatten() {
// Linux installs made before safe_title keep their raw folder (':' was legal there).
pub fn game_folder(title: &str) -> Option<String> {

let candidate = entry.path().join("config").join("shortcuts.vdf");
let root = games_root()?;

if candidate.exists() {
let legacy = root.join(title);

return candidate.to_str().map(String::from);
let folder = if legacy.is_dir() { legacy } else { root.join(safe_title(title)) };

}
Some(folder.to_string_lossy().to_string())

}
}

}
pub(crate) fn start_dir_of(exe: &str) -> String {

None
std::path::Path::new(exe)
.parent()
.map(|parent| format!("{}{}", parent.to_string_lossy(), std::path::MAIN_SEPARATOR))
.unwrap_or_default()

}

pub fn write_stable_shortcut(title: &str, exe: &str, appid: u32) -> Result<(), String> {

let Some(vdf) = find_shortcuts_vdf() else {

return Err(String::from("steam shortcuts.vdf not found"));

};
let vdf = crate::steam_user::shortcuts_vdf()?;

let _ = fix_core::remove_steam_shortcut(&vdf, title);

let start_dir = std::path::Path::new(exe)
let start_dir = start_dir_of(exe);

.parent()

.map(|parent| format!("{}/", parent.to_string_lossy()))

.unwrap_or_default();

fix_core::add_shortcut_with_appid(&vdf, title, exe, &start_dir, fix_core::ONLINE_FIX_LAUNCH_OPTIONS, appid)?;
fix_core::add_shortcut_with_appid(&vdf, title, exe, &start_dir, fix_core::launch_options(), appid)?;

crate::flog(&format!("[STEAM] {}: stable shortcut written, appid {}", title, appid));

Expand All @@ -182,11 +165,18 @@ pub fn write_stable_shortcut(title: &str, exe: &str, appid: u32) -> Result<(), S
}

pub fn add_game_to_steam(title: &str, folder: &str) -> bool {
let Some(vdf) = find_shortcuts_vdf() else {

flog(&format!("[DL] {}: steam shortcuts.vdf not found", title));
let vdf = match crate::steam_user::shortcuts_vdf() {

return false;
Ok(vdf) => vdf,

Err(error) => {

flog(&format!("[DL] {}: not added to Steam: {}", title, error));

return false;

}

};

Expand All @@ -206,15 +196,9 @@ pub fn add_game_to_steam(title: &str, folder: &str) -> bool {

};

let start_dir = std::path::Path::new(&exe)
let start_dir = start_dir_of(&exe);

.parent()

.map(|parent| format!("{}{}", parent.to_string_lossy(), std::path::MAIN_SEPARATOR))

.unwrap_or_default();

match fix_core::add_steam_shortcut(&vdf, title, &exe, &start_dir, fix_core::ONLINE_FIX_LAUNCH_OPTIONS) {
match fix_core::add_steam_shortcut(&vdf, title, &exe, &start_dir, fix_core::launch_options()) {

Ok(index) => {

Expand Down Expand Up @@ -276,3 +260,7 @@ pub fn log_fail(title: &str, step: &str, error: String) -> String {

}


#[cfg(test)]
#[path = "helpers_tests.rs"]
mod helpers_tests;
Loading
Loading