Merge pull request #195 from fnr1r/linux/main

Linux support
This commit is contained in:
SpikeHD
2023-09-07 17:20:26 -07:00
committed by GitHub
15 changed files with 1350 additions and 223 deletions

552
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,12 @@ registry = "1.2.1"
[target.'cfg(unix)'.dependencies]
sudo = "0.6.0"
[target.'cfg(target_os = "linux")'.dependencies]
anyhow = "1.0.58"
os_type = "2.6"
term-detect = "0.1.7"
which = "4.4"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.9", features = ["api-all"] }
@@ -53,7 +59,7 @@ serde_json = "1"
# Dependencies for the HTTP(S) proxy.
http = "0.2"
hudsucker = "0.19.1"
hudsucker = "0.19.2"
tracing = "0.1.21"
tokio-rustls = "0.23.0"
tokio-tungstenite = "0.17.0"
@@ -71,6 +77,12 @@ file_diff = "1.0.0"
rust-ini = "0.18.0"
ctrlc = "3.2.3"
[target.'cfg(target_os = "linux")'.dependencies.anime-launcher-sdk]
git = "https://github.com/an-anime-team/anime-launcher-sdk.git"
tag = "1.11.1"
default-features = false
features = ["all", "genshin"]
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL

View File

@@ -33,7 +33,9 @@
"horny_mode": "Horny Mode",
"auto_mongodb": "Automatically Start MongoDB",
"un_elevated": "Run the game non-elevated (no admin)",
"redirect_more": "Also redirect other MHY games"
"redirect_more": "Also redirect other MHY games",
"check_aagl": "For more options, check the other launcher",
"grasscutter_elevation": "Method of running GC on restricted ports"
},
"downloads": {
"grasscutter_fullbuild": "Download Grasscutter All-in-One",
@@ -85,7 +87,8 @@
"use_proxy": "Use the Cultivation internal proxy. You should have this enabled unless you use something like Fiddler",
"patch_rsa": "Patch and unpatch your game RSA automatically. Unless playing with old/non-official versions (3.0 and older), this should be enabled.",
"add_delay": "Set delay in 3dmigoto loader! \nThis should fix loading issues, but will add a small delay to when 3dmigoto is loaded upon launching the game. \nYou can now launch with 3dmigoto again.",
"migoto": "For importing models from GameBanana"
"migoto": "For importing models from GameBanana",
"grasscutter_elevation_help_text": "The method used to allow Grasscutter to bind port 443 (which is not allowed for regular users on Linux)\nAvailable methods:\n Capability - grant the Java Virtual Machine the capability to bind ports below 1024. This also allows all other programs running on that JVM to bind these ports.\n Root - run GC as root. This also allows the GC server, its plugins and the JVM to do pretty much anything, including sending your nudes to the NSA, CIA, and the alphabet boys.\n None - for no method. This requires you to change the GC Dispatch port."
},
"swag": {
"akebi_name": "Akebi",

View File

@@ -33,7 +33,9 @@
"horny_mode": "Tryb 34",
"auto_mongodb": "Automatycznie uruchamiaj MongoDB",
"un_elevated": "Uruchamiaj grę bez uprawnień administratora/roota",
"redirect_more": "Przekieruj też inne gry MHY"
"redirect_more": "Przekieruj też inne gry MHY",
"check_aagl": "Więcej opcji znajdziesz w drugim launcherze",
"grasscutter_elevation": "Sposób uruchomienia GC na ograniczonym porcie"
},
"downloads": {
"grasscutter_fullbuild": "Pobierz Grasscutter (wszystko w jednym)",
@@ -85,7 +87,8 @@
"use_proxy": "Używaj wewnętrznego proxy Cultivation. To powinno być włączone, chyba że używasz czegoś jak np. Fiddler",
"patch_rsa": "Patchuj i odpatchuj RSA gry automatycznie. Jeżeli nie grasz w starą lub nieoficjalną wersję (3.0 lub starszą), to powinno być włączone.",
"add_delay": "Ustaw opóźnienie 3dmigoto loadera! \nTo powinno naprawić problemy z ładowaniem, ale doda małe opóźnienie do czasu ładowania 3dmigoto do gry. \nTeraz możecie uruchamiać grę z 3dmigoto.",
"migoto": "Do importowania modeli z GameBanana"
"migoto": "Do importowania modeli z GameBanana",
"grasscutter_elevation_help_text": "Metoda używana przez Grasscuttera do zbindowania portu 443 (co nie jest dozwolone dla zywkłych użytkowników w Linuxie)\nDostępne metody:\n Capability - daje wirtualnej maszynie Javy możliwość zbindowania portów poniżej 1024. To też pozwala wszystkim innym programom odpalonym na tej maszynie JVM zbindować te porty.\n Root - uruchamia GC jako root. To pozwala serwerowi GC, jego pluginom i maszynie JVM zrobić praktycznie wszystko, wliczając w to wysyłanie twoich nudesów do ABW, CBŚ, i innych trzyliterowych służb.\n None - czyli żadna metoda. Ta opcja wymaga zmiana portu Dispatch serwera GC."
},
"swag": {
"akebi_name": "Akebi",

View File

@@ -18,8 +18,5 @@ pub fn reopen_as_admin() {
exit(0);
}
#[cfg(target_os = "linux")]
pub fn reopen_as_admin() {}
#[cfg(target_os = "macos")]
pub fn reopen_as_admin() {}

View File

@@ -11,14 +11,22 @@ use proxy::set_proxy_addr;
use std::fs;
use std::io::Write;
use std::{collections::HashMap, sync::Mutex};
use system_helpers::is_elevated;
use tauri::api::path::data_dir;
use tauri::async_runtime::block_on;
use std::thread;
use sysinfo::{Pid, ProcessExt, System, SystemExt};
#[cfg(target_os = "windows")]
use crate::admin::reopen_as_admin;
#[cfg(target_os = "windows")]
use system_helpers::is_elevated;
#[cfg(target_os = "linux")]
use std::{
thread::{sleep, JoinHandle},
time::{Duration, Instant},
};
mod admin;
mod config;
@@ -37,6 +45,9 @@ static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String:
static WATCH_GRASSCUTTER_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
static GC_PID: std::sync::Mutex<usize> = Mutex::new(696969);
#[cfg(target_os = "linux")]
pub static AAGL_THREAD: Lazy<Mutex<Option<JoinHandle<()>>>> = Lazy::new(|| Mutex::new(None));
fn try_flush() {
std::io::stdout().flush().unwrap_or(())
}
@@ -157,6 +168,7 @@ fn main() -> Result<(), ArgsError> {
let args: Vec<String> = std::env::args().collect();
let parsed_args = block_on(parse_args(&args)).unwrap();
#[cfg(target_os = "windows")]
if !is_elevated() && !parsed_args.value_of("no-admin")? {
println!("===============================================================================");
println!("You running as a non-elevated user. Some stuff will almost definitely not work.");
@@ -202,6 +214,7 @@ fn main() -> Result<(), ArgsError> {
system_helpers::service_status,
system_helpers::stop_service,
system_helpers::run_jar,
system_helpers::run_jar_root,
system_helpers::open_in_browser,
system_helpers::install_location,
system_helpers::is_elevated,
@@ -210,6 +223,10 @@ fn main() -> Result<(), ArgsError> {
system_helpers::wipe_registry,
system_helpers::get_platform,
system_helpers::run_un_elevated,
system_helpers::jvm_add_cap,
system_helpers::jvm_remove_cap,
patch::patch_game,
patch::unpatch_game,
proxy::set_proxy_addr,
proxy::generate_ca_files,
proxy::set_redirect_more,
@@ -267,6 +284,7 @@ fn is_game_running() -> bool {
!proc.is_empty()
}
#[cfg(target_os = "windows")]
#[tauri::command]
fn enable_process_watcher(window: tauri::Window, process: String) {
*WATCH_GAME_PROCESS.lock().unwrap() = process;
@@ -312,6 +330,41 @@ fn enable_process_watcher(window: tauri::Window, process: String) {
});
}
// The library takes care of it
#[cfg(target_os = "linux")]
#[tauri::command]
fn enable_process_watcher(window: tauri::Window, process: String) {
drop(process);
thread::spawn(move || {
let end_time = Instant::now() + Duration::from_secs(60);
let game_thread = loop {
let mut lock = AAGL_THREAD.lock().unwrap();
if lock.is_some() {
break lock.take().unwrap();
}
drop(lock);
if end_time < Instant::now() {
// If more than 60 seconds pass something has gone wrong
println!("Waiting for game thread timed out");
return;
}
// Otherwhise wait in order to not use too many CPU cycles
sleep(Duration::from_millis(128));
};
game_thread.join().unwrap();
println!("Game closed");
*WATCH_GAME_PROCESS.lock().unwrap() = "".to_string();
disconnect();
window.emit("game_closed", &()).unwrap();
});
}
#[cfg(target_os = "macos")]
#[tauri::command]
fn enable_process_watcher(window: tauri::Window, process: String) {}
#[tauri::command]
fn is_grasscutter_running() -> bool {
// Grab the grasscutter process name
@@ -361,7 +414,6 @@ fn restart_grasscutter(_window: tauri::Window) {
}
}
#[cfg(windows)]
#[tauri::command]
fn enable_grasscutter_watcher(window: tauri::Window, process: String) {
let grasscutter_name = process.clone();
@@ -422,13 +474,6 @@ fn enable_grasscutter_watcher(window: tauri::Window, process: String) {
});
}
#[cfg(unix)]
#[tauri::command]
fn enable_grasscutter_watcher(_window: tauri::Window, _process: String) {
let gc_pid = Pid::from(696969);
*GC_PID.lock().unwrap() = gc_pid.into();
}
#[tauri::command]
async fn connect(port: u16, certificate_path: String) {
// Log message to console.

View File

@@ -3,6 +3,53 @@ use crate::file_helpers;
use crate::system_helpers;
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use once_cell::sync::Lazy;
#[cfg(target_os = "linux")]
use std::sync::Arc;
#[cfg(target_os = "linux")]
use tokio::sync::Mutex;
#[cfg(target_os = "linux")]
static PATCH_STATE: Lazy<Arc<Mutex<Option<PatchState>>>> = Lazy::new(|| Arc::new(Mutex::new(None)));
#[cfg(target_os = "linux")]
#[derive(Debug, Clone, Copy)]
enum PatchState {
NotExist,
Same,
BakNotExist,
BakExist,
}
#[cfg(target_os = "linux")]
use PatchState::*;
#[cfg(target_os = "linux")]
impl PatchState {
fn to_wta(self) -> WhatToUnpach {
let (mhyp_renamed, game_was_patched) = match self {
NotExist => (false, true),
Same => (false, true),
BakNotExist => (true, true),
BakExist => (false, false),
};
WhatToUnpach {
mhyp_renamed,
game_was_patched,
}
}
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone)]
struct WhatToUnpach {
mhyp_renamed: bool,
game_was_patched: bool,
}
#[cfg(windows)]
#[tauri::command]
pub async fn patch_game() -> bool {
let patch_path = PathBuf::from(system_helpers::install_location()).join("patch/version.dll");
@@ -37,6 +84,82 @@ pub async fn patch_game() -> bool {
true
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub async fn patch_game() -> bool {
let mut patch_state_mutex = PATCH_STATE.lock().await;
if patch_state_mutex.is_some() {
println!("Game already patched!");
}
let patch_path = PathBuf::from(system_helpers::install_location()).join("patch/version.dll");
let game_mhyp = PathBuf::from(get_game_rsa_path().await.unwrap()).join("mhypbase.dll");
let game_mhyp_bak = PathBuf::from(get_game_rsa_path().await.unwrap()).join("mhypbase.dll.bak");
let patch_state = if !game_mhyp.exists() {
NotExist
} else if file_helpers::are_files_identical(
patch_path.to_str().unwrap(),
game_mhyp.to_str().unwrap(),
) {
Same
} else if !game_mhyp_bak.exists() {
BakNotExist
} else {
BakExist
};
match patch_state {
NotExist => {
// No renaming needed.
// Copy version.dll as mhypbase.dll
file_helpers::copy_file_with_new_name(
patch_path.clone().to_str().unwrap().to_string(),
get_game_rsa_path().await.unwrap(),
String::from("mhypbase.dll"),
);
}
Same => {
// No renaming needed.
// No copying needed.
println!("The game is already patched.");
}
BakNotExist => {
// The current mhypbase.dll is most likely the original
// Rename mhypbase.dll to mhypbase.dll.bak
file_helpers::rename(
game_mhyp.to_str().unwrap().to_string(),
game_mhyp_bak
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string(),
);
// Copy version.dll as mhypbase.dll
file_helpers::copy_file_with_new_name(
patch_path.clone().to_str().unwrap().to_string(),
get_game_rsa_path().await.unwrap(),
String::from("mhypbase.dll"),
);
}
BakExist => {
// Can't rename. mhypbase.dll.bak already exists.
// Can't patch. mhypbase.dll exists.
// This SHOULD NOT HAPPEN
println!("The game directory contains a mhypbase.dll, but it's different from the patch.");
println!("Make sure you have the original mhypbase.dll.");
println!("Delete any other copy, and place the original copy in the game directory with the original name.");
}
}
patch_state_mutex.replace(patch_state);
patch_state.to_wta().game_was_patched
}
#[cfg(windows)]
#[tauri::command]
pub async fn unpatch_game() -> bool {
// Just delete patch since it's not replacing any existing file
let deleted = file_helpers::delete_file(
@@ -50,6 +173,51 @@ pub async fn unpatch_game() -> bool {
deleted
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub async fn unpatch_game() -> bool {
// TODO: Prevent the launcher from unpatching the game two times
// This might be related to redirecting calls from the ts version of
// unpatchGame to the rust version
let mut patch_state_mutex = PATCH_STATE.lock().await;
let patch_state = patch_state_mutex.take();
if patch_state.is_none() {
println!("Game not patched!");
// NOTE: true is returned since otherwhise the launcher thinks unpatching failed
// NOTE: actually it should be false since delete_file always returns false
return false;
}
let patch_state = patch_state.unwrap();
let game_mhyp = PathBuf::from(get_game_rsa_path().await.unwrap()).join("mhypbase.dll");
let game_mhyp_bak = PathBuf::from(get_game_rsa_path().await.unwrap()).join("mhypbase.dll.bak");
let WhatToUnpach {
mhyp_renamed,
game_was_patched,
} = patch_state.to_wta();
// If the current mhypbase.dll is the patch, then delete it.
let deleted = if game_was_patched {
file_helpers::delete_file(game_mhyp.to_str().unwrap().to_string());
true
} else {
false
};
// If we renamed the original mhypbase.dll to mhypbase.dll.bak
// rename mhypbase.dll.bak back to mhypbase.dll
if mhyp_renamed {
file_helpers::rename(
game_mhyp_bak.to_str().unwrap().to_string(),
game_mhyp.to_str().unwrap().to_string(),
)
}
// NOTE: As mentioned in a note above, false should be returned if the function succeded
// and true if it failed
!deleted
}
pub async fn get_game_rsa_path() -> Option<String> {
let config = config::get_config();

View File

@@ -4,8 +4,6 @@
*/
use crate::config::get_config;
#[cfg(target_os = "linux")]
use crate::system_helpers::run_command;
use once_cell::sync::Lazy;
use std::{path::PathBuf, str::FromStr, sync::Mutex};
@@ -28,6 +26,13 @@ use tauri::{api::path::data_dir, http::Uri};
#[cfg(windows)]
use registry::{Data, Hive, Security};
#[cfg(target_os = "linux")]
use crate::system_helpers::{AsRoot, SpawnItsFineReally};
#[cfg(target_os = "linux")]
use anime_launcher_sdk::{config::ConfigExt, genshin::config::Config};
#[cfg(target_os = "linux")]
use std::{fs::File, io::Write, process::Command};
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
@@ -283,24 +288,23 @@ pub fn connect_to_proxy(proxy_port: u16) {
println!("Connected to the proxy.");
}
#[cfg(unix)]
#[cfg(target_os = "linux")]
pub fn connect_to_proxy(proxy_port: u16) {
// Edit /etc/environment to set $http_proxy and $https_proxy
let mut env_file = match fs::read_to_string("/etc/environment") {
Ok(f) => f,
Err(e) => {
println!("Error opening /etc/environment: {}", e);
return;
}
};
// Append the proxy configuration.
// We will not remove the current proxy config if it exists, so we can just remove these last lines when we disconnect
env_file += format!("\nhttps_proxy=127.0.0.1:{}", proxy_port).as_str();
env_file += format!("\nhttp_proxy=127.0.0.1:{}", proxy_port).as_str();
// Save
fs::write("/etc/environment", env_file).unwrap();
let mut config = Config::get().unwrap();
let proxy_addr = format!("127.0.0.1:{}", proxy_port);
if !config.game.environment.contains_key("http_proxy") {
config
.game
.environment
.insert("http_proxy".to_string(), proxy_addr.clone());
}
if !config.game.environment.contains_key("https_proxy") {
config
.game
.environment
.insert("https_proxy".to_string(), proxy_addr);
}
Config::update(config);
}
#[cfg(target_od = "macos")]
@@ -329,21 +333,14 @@ pub fn disconnect_from_proxy() {
#[cfg(target_os = "linux")]
pub fn disconnect_from_proxy() {
println!("Re-writing environment variables");
let regexp = regex::Regex::new(
// This has to be specific as possible or we risk fuckin up their environment LOL
r"(https|http)_proxy=.*127.0.0.1:.*",
)
.unwrap();
let environment = &fs::read_to_string("/etc/environment").expect("Failed to open environment");
let new_environment = regexp.replace_all(environment, "").to_string();
// Write new environment
fs::write("/etc/environment", new_environment.trim_end()).expect(
"Could not write environment, remove proxy declarations manually if they are still set",
);
let mut config = Config::get().unwrap();
if config.game.environment.contains_key("http_proxy") {
config.game.environment.remove("http_proxy");
}
if config.game.environment.contains_key("https_proxy") {
config.game.environment.remove("https_proxy");
}
Config::update(config);
}
#[cfg(target_os = "macos")]
@@ -449,18 +446,72 @@ pub fn install_ca_files(cert_path: &Path) {
println!("Installed certificate.");
}
// If this is borked on non-debian platforms, so be it
#[cfg(target_os = "linux")]
pub fn install_ca_files(cert_path: &Path) {
let usr_certs = PathBuf::from("/usr/local/share/ca-certificates");
let usr_cert_path = usr_certs.join("cultivation.crt");
// Create dir if it doesn't exist
fs::create_dir_all(&usr_certs).expect("Unable to create local certificate directory");
fs::copy(cert_path, usr_cert_path).expect("Unable to copy cert to local certificate directory");
run_command("update-ca-certificates", vec![], None);
let platform = os_type::current_platform();
use os_type::OSType::*;
// TODO: Add more distros
match &platform.os_type {
// Debian-based
Debian | Ubuntu | Kali => {
let usr_certs = PathBuf::from("/usr/local/share/ca-certificates");
let usr_cert_path = usr_certs.join("cultivation.crt");
// We want to execute multiple commands, but we don't want multiple pkexec prompts
// so we have to use a script
let script = Path::new("/tmp/cultivation-inject-ca-cert.sh");
let mut scriptf = File::create(script).unwrap();
#[cfg(debug_assertions)]
let setflags = "xe";
#[cfg(not(debug_assertions))]
let setflags = "e";
write!(
scriptf,
r#"#!/usr/bin/env bash
set -{}
CERT="{}"
CERT_DIR="{}"
CERT_TARGET="{}"
# Create dir if it doesn't exist
if ! [[ -d "$CERT_DIR" ]]; then
mkdir -v "$CERT_DIR"
fi
cp -v "$CERT" "$CERT_TARGET"
update-ca-certificates
"#,
setflags,
cert_path.to_str().unwrap(),
usr_certs.to_str().unwrap(),
usr_cert_path.to_str().unwrap()
)
.unwrap();
scriptf.flush().unwrap();
drop(scriptf);
let _ = Command::new("bash")
.arg(script)
.as_root_gui()
.spawn_its_fine_really("Unable to install certificate");
if let Err(e) = fs::remove_file(script) {
println!("Unable to remove certificate install script: {}", e);
};
}
// RedHat-based
//Redhat | CentOS |
// Arch-based
Arch | Manjaro => {
let _ = Command::new("trust")
.arg("anchor")
.arg("--store")
.arg(cert_path)
.as_root_gui()
.spawn_its_fine_really("Unable to install certificate");
}
OSX => unreachable!(),
_ => {
println!("Unsupported Linux distribution.");
return;
}
}
println!("Installed certificate.");
}

View File

@@ -1,8 +1,9 @@
use ini::Ini;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::process::Command;
#[cfg(windows)]
use std::ffi::OsStr;
#[cfg(windows)]
use {
registry::{Data, Hive, Security},
@@ -10,6 +11,101 @@ use {
windows_service::service_manager::{ServiceManager, ServiceManagerAccess},
};
#[cfg(target_os = "linux")]
use crate::AAGL_THREAD;
#[cfg(target_os = "linux")]
use anime_launcher_sdk::{
config::ConfigExt, genshin::config::Config, genshin::game, genshin::states::LauncherState,
wincompatlib::prelude::*,
};
#[cfg(target_os = "linux")]
use std::{path::Path, process::Stdio, thread};
#[cfg(target_os = "linux")]
use term_detect::get_terminal;
#[cfg(target_os = "linux")]
fn guess_user_terminal() -> String {
if let Ok(term) = get_terminal() {
return term.0;
}
eprintln!("Could not guess default terminal. Try setting the $TERMINAL environment variable.");
// If everything fails, default to xterm
"xterm".to_string()
}
#[cfg(target_os = "linux")]
fn rawstrcmd(cmd: &Command) -> String {
format!("{:?}", cmd)
}
#[cfg(target_os = "linux")]
fn strcmd(cmd: &Command) -> String {
format!("bash -c {:?}", rawstrcmd(cmd))
}
#[cfg(target_os = "linux")]
pub trait AsRoot {
fn as_root(&self) -> Self;
fn as_root_gui(&self) -> Self;
}
#[cfg(target_os = "linux")]
impl AsRoot for Command {
fn as_root(&self) -> Self {
let mut cmd = Command::new("sudo");
cmd.arg("--").arg("bash").arg("-c").arg(rawstrcmd(self));
cmd
}
fn as_root_gui(&self) -> Self {
let mut cmd = Command::new("pkexec");
cmd.arg("bash").arg("-c").arg(rawstrcmd(self));
cmd
}
}
#[cfg(target_os = "linux")]
trait InTerminalEmulator {
fn in_terminal(&self) -> Self;
fn in_terminal_noclose(&self) -> Self;
}
#[cfg(target_os = "linux")]
impl InTerminalEmulator for Command {
fn in_terminal(&self) -> Self {
let mut cmd = Command::new(guess_user_terminal());
cmd.arg("-e").arg(strcmd(self));
cmd
}
fn in_terminal_noclose(&self) -> Self {
let mut cmd = Command::new(guess_user_terminal());
cmd.arg("--noclose");
cmd.arg("-e").arg(strcmd(self));
cmd
}
}
#[cfg(target_os = "linux")]
pub trait SpawnItsFineReally {
fn spawn_its_fine_really(&mut self, msg: &str) -> anyhow::Result<()>;
}
#[cfg(target_os = "linux")]
impl SpawnItsFineReally for Command {
fn spawn_its_fine_really(&mut self, msg: &str) -> anyhow::Result<()> {
let res = self.status();
let Ok(status) = res else {
let error = res.unwrap_err();
println!("{}: {}", msg, &error);
return Err(error.into());
};
if !status.success() {
println!("{}: {}", msg, status);
Err(anyhow::anyhow!("{}: {}", msg, status))
} else {
Ok(())
}
}
}
#[tauri::command]
pub fn run_program(path: String, args: Option<String>) {
// Without unwrap_or, this can crash when UAC prompt is denied
@@ -22,6 +118,7 @@ pub fn run_program(path: String, args: Option<String>) {
};
}
#[cfg(target_os = "windows")]
#[tauri::command]
pub fn run_program_relative(path: String, args: Option<String>) {
// Save the current working directory
@@ -41,6 +138,13 @@ pub fn run_program_relative(path: String, args: Option<String>) {
std::env::set_current_dir(cwd).unwrap();
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn run_program_relative(path: String, args: Option<String>) {
// This program should not run as root
run_un_elevated(path, args)
}
#[tauri::command]
pub fn run_command(program: &str, args: Vec<&str>, relative: Option<bool>) {
let prog = program.to_string();
@@ -81,6 +185,7 @@ pub fn run_jar(path: String, execute_in: String, java_path: String) {
println!("Launching .jar with command: {}", &command);
// Open the program from the specified path.
#[cfg(not(target_os = "linux"))]
match open::with(
format!("/k cd /D \"{}\" & {}", &execute_in, &command),
"C:\\Windows\\System32\\cmd.exe",
@@ -88,8 +193,58 @@ pub fn run_jar(path: String, execute_in: String, java_path: String) {
Ok(_) => (),
Err(e) => println!("Failed to open jar ({} from {}): {}", &path, &execute_in, e),
};
#[cfg(target_os = "linux")]
thread::spawn(move || {
match Command::new(guess_user_terminal())
.arg("-e")
.arg(command)
.current_dir(execute_in.clone())
.spawn()
{
Ok(mut handler) => {
// Prevent creation of zombie processes
handler
.wait()
.expect("Grasscutter exited with non-zero exit code");
}
Err(e) => println!("Failed to open jar ({} from {}): {}", &path, &execute_in, e),
}
});
}
#[cfg(not(target_os = "linux"))]
#[tauri::command]
pub fn run_jar_root(path: String, execute_in: String, java_path: String) {
panic!("Not implemented");
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn run_jar_root(path: String, execute_in: String, java_path: String) {
let mut command = if java_path.is_empty() {
Command::new("java")
} else {
Command::new(java_path)
};
command.arg("-jar").arg(&path).current_dir(&execute_in);
println!("Launching .jar with command: {}", strcmd(&command));
// Open the program from the specified path.
thread::spawn(move || {
match command.as_root_gui().in_terminal().spawn() {
Ok(mut handler) => {
// Prevent creation of zombie processes
handler
.wait()
.expect("Grasscutter exited with non-zero exit code");
}
Err(e) => println!("Failed to open jar ({} from {}): {}", &path, &execute_in, e),
}
});
}
#[cfg(target_os = "windows")]
#[tauri::command]
pub fn run_un_elevated(path: String, args: Option<String>) {
// Open the program non-elevated.
@@ -106,6 +261,98 @@ pub fn run_un_elevated(path: String, args: Option<String>) {
};
}
#[cfg(target_os = "linux")]
fn aagl_wine_run<P: AsRef<Path>>(path: P, args: Option<String>) -> Command {
let config = Config::get().unwrap();
let wine = config.get_selected_wine().unwrap().unwrap();
let wine_run = wine
.to_wine(
config.components.path,
Some(config.game.wine.builds.join(&wine.name)),
)
.with_prefix(config.game.wine.prefix)
.with_loader(WineLoader::Current)
.with_arch(WineArch::Win64);
let env: Vec<(String, String)> = config
.game
.wine
.sync
.get_env_vars()
.clone()
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
use anime_launcher_sdk::components::wine::UnifiedWine::*;
let wined = match wine_run {
Default(wine) => wine,
Proton(proton) => proton.wine().clone(),
};
let mut cmd = Command::new(&wined.binary);
cmd.arg(path.as_ref()).envs(wined.get_envs()).envs(env);
if let Some(args) = args {
let mut args: Vec<String> = args.split(' ').map(|x| x.to_string()).collect();
cmd.args(&mut args);
};
cmd
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn run_un_elevated(path: String, args: Option<String>) {
let path = Path::new(&path);
let exec_name = path.file_name().unwrap().to_str().unwrap();
if exec_name == ["Yuan", "Shen", ".exe"].join("").as_str()
|| exec_name == ["Gen", "shin", "Impact", ".exe"].join("").as_str()
{
let game_thread = thread::spawn(|| {
'statechk: {
let state = LauncherState::get_from_config(|_| {});
let Ok(state) = state else {
println!("Failed to get state: {}", state.unwrap_err());
break 'statechk;
};
use anime_launcher_sdk::genshin::states::LauncherState::*;
match state {
FolderMigrationRequired { from, .. } => Err(format!(
"A folder migration is required ({:?} needs to be moved)",
from
)),
WineNotInstalled => Err("Wine is not installed".to_string()),
PrefixNotExists => Err("The Wine prefix does not exist".to_string()),
GameNotInstalled(_) => Err("The game is not installed".to_string()),
_ => Ok(()),
}
.expect("Can't launch game. Check the other launcher.");
}
if let Err(e) = game::run() {
println!("An error occurred while running the game: {}", e);
}
});
{
let mut game_thead_lock = AAGL_THREAD.lock().unwrap();
game_thead_lock.replace(game_thread);
}
return;
}
// Run exe with wine
if path.extension().unwrap() == "exe" {
let path = path.to_owned().clone();
thread::spawn(move || {
let _ = aagl_wine_run(&path, args)
.current_dir(path.parent().unwrap())
.in_terminal()
.spawn_its_fine_really(&format!(
"Failed to open program ({})",
path.to_str().unwrap()
));
});
}
println!(
"Can't run {:?}. Running this type of file is not supported yet.",
path
);
}
#[tauri::command]
pub fn open_in_browser(url: String) {
// Open the URL in the default browser.
@@ -119,10 +366,28 @@ pub fn open_in_browser(url: String) {
pub fn install_location() -> String {
let mut exe_path = std::env::current_exe().unwrap();
// Get the path to the executable.
exe_path.pop();
#[cfg(windows)]
{
// Get the path to the executable.
exe_path.pop();
return exe_path.to_str().unwrap().to_string();
return exe_path.to_str().unwrap().to_string();
}
#[cfg(target_os = "linux")]
{
let bin_name = exe_path.file_name().unwrap().to_str().unwrap().to_string();
exe_path.pop();
if exe_path.starts_with("/usr/bin") {
let mut path = PathBuf::from("/usr/lib");
path.push(bin_name);
path
} else {
exe_path
}
.to_str()
.unwrap()
.to_string()
}
}
#[tauri::command]
@@ -248,9 +513,41 @@ pub fn service_status(service: String) -> bool {
}
}
#[cfg(unix)]
#[cfg(target_os = "linux")]
fn to_linux_service_name(service: &str) -> Option<String> {
Some(format!(
"{}.service",
match service {
"MongoDB" => "mongod",
_ => return None,
}
))
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn service_status(_service: String) {}
pub fn service_status(service: String) -> bool {
// Change Windows service name into Linux service name
let service_lnx = to_linux_service_name(&service);
if service_lnx.is_none() {
return false;
}
let service_lnx = service_lnx.unwrap();
let status = Command::new("systemctl")
.arg("is-active")
.arg(service_lnx)
.stdout(Stdio::null())
.status();
if status.is_err() {
return false;
}
let status = status.unwrap().success();
if status {
status
} else {
start_service(service)
}
}
#[cfg(windows)]
#[tauri::command]
@@ -271,10 +568,20 @@ pub fn start_service(service: String) -> bool {
true
}
#[cfg(unix)]
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn start_service(_service: String) {
let _started = OsStr::new("Started service!");
pub fn start_service(service: String) -> bool {
println!("Starting service: {}", service);
let service_lnx = to_linux_service_name(&service);
if service_lnx.is_none() {
return false;
}
let service_lnx = service_lnx.unwrap();
Command::new("systemctl")
.arg("start")
.arg(service_lnx)
.spawn_its_fine_really(&format!("Failed to stop service {}", service))
.is_ok()
}
#[cfg(windows)]
@@ -296,11 +603,39 @@ pub fn stop_service(service: String) -> bool {
true
}
#[cfg(unix)]
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn stop_service(_service: String) {}
pub fn stop_service(service: String) -> bool {
println!("Stopping service: {}", service);
let service_lnx = to_linux_service_name(&service);
if service_lnx.is_none() {
return false;
}
let service_lnx = service_lnx.unwrap();
Command::new("systemctl")
.arg("stop")
.arg(service_lnx)
.spawn_its_fine_really(&format!("Failed to start service {}", service))
.is_ok()
}
#[cfg(unix)]
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn wipe_registry(exec_name: String) {
println!("Wiping registry");
let regpath = format!("HKCU\\Software\\miHoYo\\{}", exec_name);
let mut cmd = aagl_wine_run("reg", None);
cmd.args([
"DELETE",
&regpath,
"/f",
"/v",
"MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810",
]);
let _ = cmd.spawn_its_fine_really("Error wiping registry");
}
#[cfg(target_os = "macos")]
#[tauri::command]
pub fn wipe_registry(_exec_name: String) {}
@@ -320,3 +655,55 @@ pub fn is_elevated() -> bool {
pub fn get_platform() -> &'static str {
std::env::consts::OS
}
#[cfg(not(target_os = "linux"))]
#[tauri::command]
pub async fn jvm_add_cap(_java_path: String) -> bool {
panic!("Not implemented");
}
#[cfg(not(target_os = "linux"))]
#[tauri::command]
pub async fn jvm_remove_cap(_java_path: String) -> bool {
panic!("Not implemented");
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub async fn jvm_add_cap(java_path: String) -> bool {
let mut java_bin = if java_path.is_empty() {
which::which("java").expect("Java is not installed")
} else {
PathBuf::from(&java_path)
};
while java_bin.is_symlink() {
java_bin = java_bin.read_link().unwrap()
}
println!("Removing cap on {:?}", &java_bin);
Command::new("setcap")
.arg("CAP_NET_BIND_SERVICE=+eip")
.arg(java_bin)
.as_root_gui()
.spawn_its_fine_really(&format!("Failed to add cap to {}", java_path))
.is_ok()
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub async fn jvm_remove_cap(java_path: String) -> bool {
let mut java_bin = if java_path.is_empty() {
which::which("java").expect("Java is not installed")
} else {
PathBuf::from(&java_path)
};
while java_bin.is_symlink() {
java_bin = java_bin.read_link().unwrap()
}
println!("Setting cap on {:?}", &java_bin);
Command::new("setcap")
.arg("-r")
.arg(java_bin)
.as_root_gui()
.spawn_its_fine_really(&format!("Failed to remove cap from {}", java_path))
.is_ok()
}

View File

@@ -53,7 +53,7 @@
}
},
"security": {
"csp": "default-src 'self'; img-src 'self'; img-src https://* asset: https://asset.localhost; media-src https://* asset: https://asset.localhost; style-src-elem https://* asset https://asset.localhost; script-src-elem https://* asset https://asset.localhost;"
"csp": "default-src 'self'; img-src 'self' https://* asset: https://asset.localhost tauri://localhost; media-src https://* asset: https://asset.localhost tauri://localhost; style-src-elem https://* asset: https://asset.localhost tauri://localhost; script-src-elem https://* asset: https://asset.localhost tauri://localhost;"
},
"updater": {
"active": false,

View File

@@ -4,7 +4,7 @@ import React from 'react'
import TopBar from './components/TopBar'
import ServerLaunchSection from './components/ServerLaunchSection'
import MainProgressBar from './components/common/MainProgressBar'
import Options from './components/menu/Options'
import Options, { GrasscutterElevation } from './components/menu/Options'
import MiniDialog from './components/MiniDialog'
import DownloadList from './components/common/DownloadList'
import Downloads from './components/menu/Downloads'
@@ -15,7 +15,7 @@ import { ExtrasMenu } from './components/menu/ExtrasMenu'
import Notification from './components/common/Notification'
import GamePathNotify from './components/menu/GamePathNotify'
import { getConfigOption, setConfigOption } from '../utils/configuration'
import { getConfig, getConfigOption, setConfigOption } from '../utils/configuration'
import { invoke } from '@tauri-apps/api'
import { getVersion } from '@tauri-apps/api/app'
import { listen } from '@tauri-apps/api/event'
@@ -102,10 +102,28 @@ export class Main extends React.Component<IProps, IState> {
// Emitted for automatic processes
listen('grasscutter_closed', async () => {
const autoService = await getConfigOption('auto_mongodb')
const config = await getConfig()
if (autoService) {
await invoke('stop_service', { service: 'MongoDB' })
}
if ((await invoke('get_platform')) === 'linux') {
switch (config.grasscutter_elevation) {
case GrasscutterElevation.None:
break
case GrasscutterElevation.Capability:
await invoke('jvm_remove_cap', {
javaPath: config.java_path,
})
break
default:
console.error('Invalid grasscutter_elevation')
break
}
}
})
let min = false

View File

@@ -12,6 +12,7 @@ import Plus from '../../resources/icons/plus.svg'
import './ServerLaunchSection.css'
import { dataDir } from '@tauri-apps/api/path'
import { GrasscutterElevation } from './menu/Options'
import { getGameExecutable, getGameVersion, getGrasscutterJar } from '../../utils/game'
import { patchGame, unpatchGame } from '../../utils/rsa'
import { listen } from '@tauri-apps/api/event'
@@ -210,13 +211,14 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
if (!config.grasscutter_path) return alert('Grasscutter not installed or set!')
const grasscutter_jar = await getGrasscutterJar()
await invoke('enable_grasscutter_watcher', {
process: proc_name || grasscutter_jar,
})
if (config.auto_mongodb) {
const grasscutter_jar = await getGrasscutterJar()
await invoke('enable_grasscutter_watcher', {
process: proc_name || grasscutter_jar,
})
// Check if MongoDB is running and start it if not
await invoke('service_status', { service: 'MongoDB' })
invoke('service_status', { service: 'MongoDB' })
}
let jarFolder = config.grasscutter_path
@@ -227,8 +229,31 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
jarFolder = jarFolder.substring(0, config.grasscutter_path.lastIndexOf('\\'))
}
let cmd = 'run_jar'
if ((await invoke('get_platform')) === 'linux') {
switch (config.grasscutter_elevation) {
case GrasscutterElevation.None:
break
case GrasscutterElevation.Capability:
await invoke('jvm_add_cap', {
javaPath: config.java_path,
})
break
case GrasscutterElevation.Root:
cmd = 'run_jar_root'
break
default:
console.error('Invalid grasscutter_elevation')
break
}
}
// Launch the jar
await invoke('run_jar', {
await invoke(cmd, {
path: config.grasscutter_path,
executeIn: jarFolder,
javaPath: config.java_path || '',

View File

@@ -15,9 +15,14 @@ import BigButton from '../common/BigButton'
import DownloadHandler from '../../../utils/download'
import * as meta from '../../../utils/rsa'
import HelpButton from '../common/HelpButton'
import TextInput from '../common/TextInput'
import SmallButton from '../common/SmallButton'
export enum GrasscutterElevation {
None = 'None',
Capability = 'Capability',
Root = 'Root',
}
interface IProps {
closeFn: () => void
downloadManager: DownloadHandler
@@ -45,6 +50,9 @@ interface IState {
un_elevated: boolean
redirect_more: boolean
// Linux stuff
grasscutter_elevation: string
// Swag stuff
akebi_path: string
migoto_path: string
@@ -77,6 +85,9 @@ export default class Options extends React.Component<IProps, IState> {
un_elevated: false,
redirect_more: false,
// Linux stuff
grasscutter_elevation: GrasscutterElevation.None,
// Swag stuff
akebi_path: '',
migoto_path: '',
@@ -133,6 +144,9 @@ export default class Options extends React.Component<IProps, IState> {
un_elevated: config.un_elevated || false,
redirect_more: config.redirect_more || false,
// Linux stuff
grasscutter_elevation: config.grasscutter_elevation || GrasscutterElevation.None,
// Swag stuff
akebi_path: config.akebi_path || '',
migoto_path: config.migoto_path || '',
@@ -298,6 +312,14 @@ export default class Options extends React.Component<IProps, IState> {
})
}
async setGCElevation(value: string) {
setConfigOption('grasscutter_elevation', value)
this.setState({
grasscutter_elevation: value,
})
}
async removeRSA() {
await meta.unpatchGame()
}
@@ -328,25 +350,14 @@ export default class Options extends React.Component<IProps, IState> {
render() {
return (
<Menu closeFn={this.props.closeFn} className="Options" heading="Options">
{!this.state.platform || this.state.platform === 'windows' ? (
<div className="OptionSection" id="menuOptionsContainerGamePath">
<div className="OptionLabel" id="menuOptionsLabelGamePath">
<Tr text="options.game_path" />
</div>
<div className="OptionValue" id="menuOptionsDirGamePath">
<DirInput onChange={this.setGameExecutable} value={this.state?.game_install_path} extensions={['exe']} />
</div>
<div className="OptionSection" id="menuOptionsContainerGamePath">
<div className="OptionLabel" id="menuOptionsLabelGamePath">
<Tr text="options.game_path" />
</div>
) : (
<div className="OptionSection" id="menuOptionsContainerGameCommand">
<div className="OptionLabel" id="menuOptionsLabelGameCommand">
<Tr text="options.game_command" />
</div>
<div className="OptionValue" id="menuOptionsGameCommand">
<TextInput />
</div>
<div className="OptionValue" id="menuOptionsDirGamePath">
<DirInput onChange={this.setGameExecutable} value={this.state?.game_install_path} extensions={['exe']} />
</div>
)}
</div>
<div className="OptionSection" id="menuOptionsContainermetaDownload">
<div className="OptionLabel" id="menuOptionsLabelmetaDownload">
<Tr text="options.recover_rsa" />
@@ -446,6 +457,35 @@ export default class Options extends React.Component<IProps, IState> {
</BigButton>
</div>
</div>
{this.state.platform === 'linux' && (
<>
<Divider />
<div className="OptionSection" id="menuOptionsContainerGCElevation">
<div className="OptionLabel" id="menuOptionsLabelGCElevation">
<Tr text="options.grasscutter_elevation" />
<HelpButton contents="help.grasscutter_elevation_help_text" />
</div>
<select
value={this.state.grasscutter_elevation}
id="menuOptionsSelectGCElevation"
onChange={(event) => {
this.setGCElevation(event.target.value)
}}
>
{Object.keys(GrasscutterElevation).map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
</div>
<div className="OptionSection" id="menuOptionsContainerCheckAAGL">
<div className="OptionLabel" id="menuOptionsLabelCheckAAGL">
<Tr text="options.check_aagl" />
</div>
</div>
</>
)}
{this.state.swag && (
<>
<Divider />
@@ -491,18 +531,20 @@ export default class Options extends React.Component<IProps, IState> {
/>
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerUEGame">
<div className="OptionLabel" id="menuOptionsLabelUEGame">
<Tr text="options.un_elevated" />
{this.state.platform !== 'linux' && (
<div className="OptionSection" id="menuOptionsContainerUEGame">
<div className="OptionLabel" id="menuOptionsLabelUEGame">
<Tr text="options.un_elevated" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxUEGame">
<Checkbox
onChange={() => this.toggleOption('un_elevated')}
checked={this.state?.un_elevated}
id="unElevatedGame"
/>
</div>
</div>
<div className="OptionValue" id="menuOptionsCheckboxUEGame">
<Checkbox
onChange={() => this.toggleOption('un_elevated')}
checked={this.state?.un_elevated}
id="unElevatedGame"
/>
</div>
</div>
)}
{this.state.swag ? (
<div className="OptionSection" id="menuOptionsContainerHorny">
<div className="OptionLabel" id="menuOptionsLabelHorny">

View File

@@ -28,6 +28,9 @@ let defaultConfig: Configuration
auto_mongodb: false,
un_elevated: false,
redirect_more: false,
// Linux stuff
grasscutter_elevation: 'None',
}
})()
@@ -60,6 +63,9 @@ export interface Configuration {
un_elevated: boolean
redirect_more: boolean
// Linux stuff
grasscutter_elevation: string
// Swag stuff
akebi_path?: string
migoto_path?: string

View File

@@ -1,50 +1,10 @@
import { invoke } from '@tauri-apps/api'
import { getGameFolder } from './game'
// Patch file from: https://github.com/34736384/RSAPatch/
export async function patchGame() {
const patchPath = (await invoke('install_location')) + '/patch/version.dll'
// Are we already patched with mhypbase? If so, that's fine, just continue as normal
const gameIsPatched = await invoke('are_files_identical', {
path1: patchPath,
path2: (await getGameRSAPath()) + '/mhypbase.dll',
})
// Tell user they won't be unpatched with manual mhypbase patch
if (gameIsPatched) {
console.log('You are already patched using mhypbase, so you will not be auto patched and unpatched!')
return true
}
// Copy the patch to game files
const replaced = await invoke('copy_file_with_new_name', {
path: patchPath,
newPath: await getGameRSAPath(),
newName: 'version.dll',
})
if (!replaced) {
return false
}
return true
return invoke('patch_game')
}
export async function unpatchGame() {
// Just delete patch since it's not replacing any existing file
const deleted = await invoke('delete_file', {
path: (await getGameRSAPath()) + '/version.dll',
})
return deleted
}
export async function getGameRSAPath() {
const gameData = await getGameFolder()
if (!gameData) {
return null
}
return (gameData + '\\').replace(/\\/g, '/')
return invoke('unpatch_game')
}