From 62b54f33dfb80a2fe4d8ee3199758d329597330e Mon Sep 17 00:00:00 2001 From: fnrir Date: Fri, 18 Aug 2023 13:57:50 +0200 Subject: [PATCH] Add an option that allows binding port 443 Kinda everts some changes of "Implement MongoDB autostart and GC watching". This commit makes launching MongoDB async --- src-tauri/Cargo.lock | 12 ++ src-tauri/Cargo.toml | 1 + src-tauri/lang/en.json | 6 +- src-tauri/lang/pl.json | 6 +- src-tauri/src/main.rs | 3 + src-tauri/src/system_helpers.rs | 134 ++++++++++++++++++++++ src/ui/Main.tsx | 22 +++- src/ui/components/ServerLaunchSection.tsx | 35 +++++- src/ui/components/menu/Options.tsx | 42 +++++++ src/utils/configuration.ts | 6 + 10 files changed, 256 insertions(+), 11 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ffea245..4d9f6ef 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -940,6 +940,7 @@ dependencies = [ "tokio-tungstenite 0.17.2", "tracing", "unrar", + "which", "windows-service", "zip 0.6.2", "zip-extract", @@ -5341,6 +5342,17 @@ dependencies = [ "cc", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "widestring" version = "1.0.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e79a83f..0fadc61 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,6 +25,7 @@ sudo = "0.6.0" [target.'cfg(target_os = "linux")'.dependencies] anyhow = "1.0.58" term-detect = "0.1.7" +which = "4.4" [dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/src-tauri/lang/en.json b/src-tauri/lang/en.json index b50f94a..9f78410 100644 --- a/src-tauri/lang/en.json +++ b/src-tauri/lang/en.json @@ -34,7 +34,8 @@ "auto_mongodb": "Automatically Start MongoDB", "un_elevated": "Run the game non-elevated (no admin)", "redirect_more": "Also redirect other MHY games", - "check_aagl": "For more options, check the other launcher" + "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", @@ -86,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", diff --git a/src-tauri/lang/pl.json b/src-tauri/lang/pl.json index 7a536fc..f520e05 100644 --- a/src-tauri/lang/pl.json +++ b/src-tauri/lang/pl.json @@ -34,7 +34,8 @@ "auto_mongodb": "Automatycznie uruchamiaj MongoDB", "un_elevated": "Uruchamiaj grę bez uprawnień administratora/roota", "redirect_more": "Przekieruj też inne gry MHY", - "check_aagl": "Więcej opcji znajdziesz w drugim launcherze" + "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)", @@ -86,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", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ab457f0..32128ab 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -214,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, @@ -222,6 +223,8 @@ 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, diff --git a/src-tauri/src/system_helpers.rs b/src-tauri/src/system_helpers.rs index 4969395..b94474b 100644 --- a/src-tauri/src/system_helpers.rs +++ b/src-tauri/src/system_helpers.rs @@ -30,6 +30,56 @@ fn guess_user_terminal() -> String { "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<()>; @@ -159,6 +209,38 @@ pub fn run_jar(path: String, execute_in: String, java_path: String) { }); } +#[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) { @@ -476,3 +558,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() +} diff --git a/src/ui/Main.tsx b/src/ui/Main.tsx index ffdf37b..48db0fc 100644 --- a/src/ui/Main.tsx +++ b/src/ui/Main.tsx @@ -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 { // 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 diff --git a/src/ui/components/ServerLaunchSection.tsx b/src/ui/components/ServerLaunchSection.tsx index d8e0ccd..ed41eff 100644 --- a/src/ui/components/ServerLaunchSection.tsx +++ b/src/ui/components/ServerLaunchSection.tsx @@ -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,11 +211,12 @@ export default class ServerLaunchSection extends React.Component 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 invoke('service_status', { service: 'MongoDB' }) } @@ -227,8 +229,31 @@ export default class ServerLaunchSection extends React.Component 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 || '', diff --git a/src/ui/components/menu/Options.tsx b/src/ui/components/menu/Options.tsx index 0836dfa..c1fddc5 100644 --- a/src/ui/components/menu/Options.tsx +++ b/src/ui/components/menu/Options.tsx @@ -17,6 +17,12 @@ import * as meta from '../../../utils/rsa' import HelpButton from '../common/HelpButton' import SmallButton from '../common/SmallButton' +export enum GrasscutterElevation { + None = 'None', + Capability = 'Capability', + Root = 'Root', +} + interface IProps { closeFn: () => void downloadManager: DownloadHandler @@ -44,6 +50,9 @@ interface IState { un_elevated: boolean redirect_more: boolean + // Linux stuff + grasscutter_elevation: string + // Swag stuff akebi_path: string migoto_path: string @@ -76,6 +85,9 @@ export default class Options extends React.Component { un_elevated: false, redirect_more: false, + // Linux stuff + grasscutter_elevation: GrasscutterElevation.None, + // Swag stuff akebi_path: '', migoto_path: '', @@ -132,6 +144,9 @@ export default class Options extends React.Component { 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 || '', @@ -297,6 +312,14 @@ export default class Options extends React.Component { }) } + async setGCElevation(value: string) { + setConfigOption('grasscutter_elevation', value) + + this.setState({ + grasscutter_elevation: value, + }) + } + async removeRSA() { await meta.unpatchGame() } @@ -437,6 +460,25 @@ export default class Options extends React.Component { {this.state.platform === 'linux' && ( <> +