From ccef1dff6bc4045c8fa961a4d54a17b21664d40b Mon Sep 17 00:00:00 2001 From: MarcoM <34962376+marcozzxx810@users.noreply.github.com> Date: Wed, 12 Apr 2023 00:09:01 +0800 Subject: [PATCH] feat: VRServer gracefully shutdown (#632) Co-authored-by: ImUrX --- gui/src-tauri/src/main.rs | 266 ++++-------------- gui/src-tauri/src/util.rs | 221 +++++++++++++++ server/build.gradle.kts | 2 + server/src/main/java/dev/slimevr/Main.kt | 35 +-- .../src/main/java/dev/slimevr/VRServer.java | 5 +- .../io/eiren/util/logging/LogManager.java | 7 + 6 files changed, 300 insertions(+), 236 deletions(-) create mode 100644 gui/src-tauri/src/util.rs diff --git a/gui/src-tauri/src/main.rs b/gui/src-tauri/src/main.rs index 0a62dea737..4326edabf0 100644 --- a/gui/src-tauri/src/main.rs +++ b/gui/src-tauri/src/main.rs @@ -1,125 +1,26 @@ #![cfg_attr(all(not(debug_assertions), windows), windows_subsystem = "windows")] use std::env; -use std::ffi::{OsStr, OsString}; -use std::io::Write; -#[cfg(windows)] -use std::os::windows::process::CommandExt; use std::panic; -use std::path::{Path, PathBuf}; -use std::process::{Child, Stdio}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use std::time::Instant; use clap::Parser; -use const_format::concatcp; -use rand::{seq::SliceRandom, thread_rng}; -use shadow_rs::shadow; -use tauri::api::process::Command; +use tauri::api::process::{Command, CommandChild}; use tauri::Manager; +use tauri::RunEvent; + #[cfg(windows)] use tauri::WindowEvent; -use tempfile::Builder; -#[cfg(windows)] -/// For Commands on Windows so they dont create terminals -const CREATE_NO_WINDOW: u32 = 0x0800_0000; -/// It's an i32 because we check it through exit codes of the process -const MINIMUM_JAVA_VERSION: i32 = 17; -const JAVA_BIN: &str = if cfg!(windows) { "java.exe" } else { "java" }; -static POSSIBLE_TITLES: &[&str] = &[ - "Panicking situation", - "looking for spatula", - "never gonna give you up", - "never gonna let you down", - "uwu sowwy", -]; -shadow!(build); -// Tauri has a way to return the package.json version, but it's not a constant... -const VERSION: &str = if build::TAG.is_empty() { - build::SHORT_COMMIT -} else { - build::TAG +use crate::util::{ + get_launch_path, show_error, valid_java_paths, Cli, JAVA_BIN, MINIMUM_JAVA_VERSION, }; -const MODIFIED: &str = if build::GIT_CLEAN { "" } else { "-dirty" }; - -#[derive(Debug, Parser)] -#[clap( - version = concatcp!(VERSION, MODIFIED), - about -)] -struct Cli { - #[clap(short, long)] - display_console: bool, - #[clap(long)] - launch_from_path: Option, - #[clap(flatten)] - verbose: clap_verbosity_flag::Verbosity, -} - -fn is_valid_path(path: &Path) -> bool { - path.join("slimevr.jar").exists() -} - -fn get_launch_path(cli: Cli) -> Option { - let paths = [ - cli.launch_from_path, - // AppImage passes the fakeroot in `APPDIR` env var. - env::var_os("APPDIR").map(|x| PathBuf::from(x)), - env::current_dir().ok(), - // getcwd in Mac can't be trusted, so let's get the executable's path - env::current_exe() - .map(|mut f| { - f.pop(); - f - }) - .ok(), - Some(PathBuf::from(env!("CARGO_MANIFEST_DIR"))), - // For flatpak container - Some(PathBuf::from("/app/share/slimevr/")), - Some(PathBuf::from("/usr/share/slimevr/")), - ]; - - paths - .into_iter() - .filter_map(|x| x) - .find(|x| is_valid_path(x)) -} - -fn spawn_java(java: &OsStr, java_version: &OsStr) -> std::io::Result { - let mut cmd = std::process::Command::new(java); - - #[cfg(windows)] - cmd.creation_flags(CREATE_NO_WINDOW); - - cmd.arg("-jar") - .arg(java_version) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .stdout(Stdio::null()) - .spawn() -} - -#[cfg(desktop)] -fn show_error(text: &str) -> bool { - use tauri::api::dialog::{ - blocking::MessageDialogBuilder, MessageDialogButtons, MessageDialogKind, - }; - - MessageDialogBuilder::new( - format!( - "SlimeVR GUI crashed - {}", - POSSIBLE_TITLES.choose(&mut thread_rng()).unwrap() - ), - text, - ) - .buttons(MessageDialogButtons::Ok) - .kind(MessageDialogKind::Error) - .show() -} -#[cfg(mobile)] -fn show_error(text: &str) -> bool { - // needs to do native stuff on mobile - false -} +mod util; fn main() { // Make an error dialog box when panicking @@ -142,6 +43,7 @@ fn main() { // and then check for WebView2's existence #[cfg(windows)] { + use crate::util::webview2_exists; use win32job::{ExtendedLimitInfo, Job}; let mut info = ExtendedLimitInfo::new(); @@ -173,6 +75,8 @@ fn main() { } // Spawn server process + let exit_flag = Arc::new(AtomicBool::new(false)); + let mut backend: Option = None; let run_path = get_launch_path(cli); let stdout_recv = if let Some(p) = run_path { @@ -190,20 +94,22 @@ fn main() { }; log::info!("Using Java binary: {:?}", java_bin); - let (recv, _child) = Command::new(java_bin.to_str().unwrap()) + let (recv, child) = Command::new(java_bin.to_str().unwrap()) .current_dir(p) .args(["-Xmx512M", "-jar", "slimevr.jar", "--no-gui"]) .spawn() .expect("Unable to start the server jar"); + backend = Some(child); Some(recv) } else { log::warn!("No server found. We will not start the server."); None }; - let run_result = tauri::Builder::default() + let exit_flag_terminated = exit_flag.clone(); + let build_result = tauri::Builder::default() .plugin(tauri_plugin_window_state::Builder::default().build()) - .setup(|app| { + .setup(move |app| { if let Some(mut recv) = stdout_recv { let app_handle = app.app_handle(); tauri::async_runtime::spawn(async move { @@ -215,6 +121,7 @@ fn main() { CommandEvent::Stdout(s) => ("stdout", s), CommandEvent::Error(s) => ("error", s), CommandEvent::Terminated(s) => { + exit_flag_terminated.store(true, Ordering::Relaxed); ("terminated", format!("{s:?}")) } _ => ("other", "".to_string()), @@ -237,8 +144,29 @@ fn main() { WindowEvent::Resized(_) => std::thread::sleep(std::time::Duration::from_nanos(1)), _ => (), }) - .run(tauri::generate_context!()); - match run_result { + .build(tauri::generate_context!()); + match build_result { + Ok(app) => { + app.run(move |_app_handle, event| match event { + RunEvent::ExitRequested { .. } => { + let Some(ref mut child) = backend else { return }; + let write_result = child.write(b"exit\n"); + match write_result { + Ok(()) => log::info!("send exit to backend"), + Err(_) => log::info!("fail to send exit to backend"), + } + let ten_seconds = Duration::from_secs(10); + let start_time = Instant::now(); + while start_time.elapsed() < ten_seconds { + if exit_flag.load(Ordering::Relaxed) { + break; + } + thread::sleep(Duration::from_secs(1)); + } + } + _ => {} + }); + } #[cfg(windows)] // Often triggered when the user doesn't have webview2 installed Err(tauri::Error::Runtime(tauri_runtime::Error::CreateWebview(error))) => { @@ -256,110 +184,10 @@ fn main() { if confirm { open::that("https://docs.slimevr.dev/common-issues.html#webview2-is-missing--slimevr-gui-crashes-immediately--panicked-at--webview2error").unwrap(); } - return; } - _ => run_result.expect("error while running tauri application"), - } -} - -#[cfg(windows)] -/// Check if WebView2 exists -fn webview2_exists() -> bool { - use winreg::enums::*; - use winreg::RegKey; - - // First on the machine itself - let machine: Option = RegKey::predef(HKEY_LOCAL_MACHINE) - .open_subkey(r"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}") - .map(|r| r.get_value("pv").ok()).ok().flatten(); - let mut exists = false; - if let Some(version) = machine { - exists = version.split('.').any(|x| x != "0"); - } - // Then in the current user - if !exists { - let user: Option = RegKey::predef(HKEY_CURRENT_USER) - .open_subkey( - r"Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", - ) - .map(|r| r.get_value("pv").ok()) - .ok() - .flatten(); - if let Some(version) = user { - exists = version.split('.').any(|x| x != "0"); + Err(error) => { + log::error!("tauri build error {}", error); + show_error(&error.to_string()); } } - exists -} - -fn valid_java_paths() -> Vec<(OsString, i32)> { - let mut file = Builder::new() - .suffix(".jar") - .tempfile() - .expect("Couldn't generate .jar file"); - file.write_all(include_bytes!("JavaVersion.jar")) - .expect("Couldn't write to .jar file"); - let java_version = file.into_temp_path(); - - // Check if main Java is a supported version - let main_java = if let Ok(java_home) = std::env::var("JAVA_HOME") { - PathBuf::from(java_home) - .join("bin") - .join(JAVA_BIN) - .into_os_string() - } else { - JAVA_BIN.into() - }; - if let Some(main_child) = spawn_java(&main_java, java_version.as_os_str()) - .expect("Couldn't spawn the main Java binary") - .wait() - .expect("Couldn't execute the main Java binary") - .code() - { - if main_child >= MINIMUM_JAVA_VERSION { - return vec![(main_java, main_child)]; - } - } - - // Otherwise check if anything else is a supported version - let mut childs = vec![]; - cfg_if::cfg_if! { - if #[cfg(target_os = "macos")] { - // macOS JVMs are saved on multiple possible places, - // /Library/Java/JavaVirtualMachines are the ones installed by an admin - // /Users/$USER/Library/Java/JavaVirtualMachines are the ones installed locally by the user - let libs = glob::glob(concatcp!("/Library/Java/JavaVirtualMachines/*/Contents/Home/bin/", JAVA_BIN)) - .unwrap() - .filter_map(|res| res.ok()); - } else if #[cfg(unix)] { - // Linux JVMs are saved on /usr/lib/jvm from what I found out, - // there is usually a default dir and a default-runtime dir also which are linked - // to the current default runtime and the current default JDK (I think it's JDK) - let libs = glob::glob(concatcp!("/usr/lib/jvm/*/bin/", JAVA_BIN)) - .unwrap() - .filter_map(|res| res.ok()); - } else { - let libs = which::which_all(JAVA_BIN).unwrap(); - } - } - - for java in libs { - let res = spawn_java(java.as_os_str(), java_version.as_os_str()); - - match res { - Ok(child) => childs.push((java.into_os_string(), child)), - Err(e) => println!("Error on trying to spawn a Java executable: {}", e), - } - } - - childs - .into_iter() - .filter_map(|(p, mut c)| { - c.wait() - .expect("Failed on executing a Java executable") - .code() - .map(|code| (p, code)) - .filter(|(_p, code)| *code >= MINIMUM_JAVA_VERSION) - }) - .collect() } diff --git a/gui/src-tauri/src/util.rs b/gui/src-tauri/src/util.rs new file mode 100644 index 0000000000..fd66125c26 --- /dev/null +++ b/gui/src-tauri/src/util.rs @@ -0,0 +1,221 @@ +#[cfg(windows)] +use std::os::windows::process::CommandExt; +use std::{ + env, + ffi::{OsStr, OsString}, + io::Write, + path::{Path, PathBuf}, + process::{Child, Stdio}, +}; + +use clap::Parser; +use const_format::concatcp; +use shadow_rs::shadow; +use tempfile::Builder; + +#[cfg(windows)] +/// For Commands on Windows so they dont create terminals +const CREATE_NO_WINDOW: u32 = 0x0800_0000; +/// It's an i32 because we check it through exit codes of the process +pub const MINIMUM_JAVA_VERSION: i32 = 17; +pub const JAVA_BIN: &str = if cfg!(windows) { "java.exe" } else { "java" }; +pub static POSSIBLE_TITLES: &[&str] = &[ + "Panicking situation", + "looking for spatula", + "never gonna give you up", + "never gonna let you down", + "uwu sowwy", +]; + +shadow!(build); +// Tauri has a way to return the package.json version, but it's not a constant... +const VERSION: &str = if build::TAG.is_empty() { + build::SHORT_COMMIT +} else { + build::TAG +}; +const MODIFIED: &str = if build::GIT_CLEAN { "" } else { "-dirty" }; + +#[derive(Debug, Parser)] +#[clap( + version = concatcp!(VERSION, MODIFIED), + about +)] +pub struct Cli { + #[clap(short, long)] + display_console: bool, + #[clap(long)] + launch_from_path: Option, + #[clap(flatten)] + verbose: clap_verbosity_flag::Verbosity, +} + +pub fn is_valid_path(path: &Path) -> bool { + path.join("slimevr.jar").exists() +} + +pub fn get_launch_path(cli: Cli) -> Option { + let paths = [ + cli.launch_from_path, + // AppImage passes the fakeroot in `APPDIR` env var. + env::var_os("APPDIR").map(|x| PathBuf::from(x)), + env::current_dir().ok(), + // getcwd in Mac can't be trusted, so let's get the executable's path + env::current_exe() + .map(|mut f| { + f.pop(); + f + }) + .ok(), + Some(PathBuf::from(env!("CARGO_MANIFEST_DIR"))), + // For flatpak container + Some(PathBuf::from("/app/share/slimevr/")), + Some(PathBuf::from("/usr/share/slimevr/")), + ]; + + paths + .into_iter() + .filter_map(|x| x) + .find(|x| is_valid_path(x)) +} + +pub fn spawn_java(java: &OsStr, java_version: &OsStr) -> std::io::Result { + let mut cmd = std::process::Command::new(java); + + #[cfg(windows)] + cmd.creation_flags(CREATE_NO_WINDOW); + + cmd.arg("-jar") + .arg(java_version) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .spawn() +} + +#[cfg(desktop)] +pub fn show_error(text: &str) -> bool { + use rand::{seq::SliceRandom, thread_rng}; + use tauri::api::dialog::{ + blocking::MessageDialogBuilder, MessageDialogButtons, MessageDialogKind, + }; + + MessageDialogBuilder::new( + format!( + "SlimeVR GUI crashed - {}", + POSSIBLE_TITLES.choose(&mut thread_rng()).unwrap() + ), + text, + ) + .buttons(MessageDialogButtons::Ok) + .kind(MessageDialogKind::Error) + .show() +} + +#[cfg(mobile)] +pub fn show_error(text: &str) -> bool { + // needs to do native stuff on mobile + false +} + +#[cfg(windows)] +/// Check if WebView2 exists +pub fn webview2_exists() -> bool { + use winreg::enums::*; + use winreg::RegKey; + + // First on the machine itself + let machine: Option = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey(r"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}") + .map(|r| r.get_value("pv").ok()).ok().flatten(); + let mut exists = false; + if let Some(version) = machine { + exists = version.split('.').any(|x| x != "0"); + } + // Then in the current user + if !exists { + let user: Option = RegKey::predef(HKEY_CURRENT_USER) + .open_subkey( + r"Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + ) + .map(|r| r.get_value("pv").ok()) + .ok() + .flatten(); + if let Some(version) = user { + exists = version.split('.').any(|x| x != "0"); + } + } + exists +} + +pub fn valid_java_paths() -> Vec<(OsString, i32)> { + let mut file = Builder::new() + .suffix(".jar") + .tempfile() + .expect("Couldn't generate .jar file"); + file.write_all(include_bytes!("JavaVersion.jar")) + .expect("Couldn't write to .jar file"); + let java_version = file.into_temp_path(); + + // Check if main Java is a supported version + let main_java = if let Ok(java_home) = std::env::var("JAVA_HOME") { + PathBuf::from(java_home) + .join("bin") + .join(JAVA_BIN) + .into_os_string() + } else { + JAVA_BIN.into() + }; + if let Some(main_child) = spawn_java(&main_java, java_version.as_os_str()) + .expect("Couldn't spawn the main Java binary") + .wait() + .expect("Couldn't execute the main Java binary") + .code() + { + if main_child >= MINIMUM_JAVA_VERSION { + return vec![(main_java, main_child)]; + } + } + + // Otherwise check if anything else is a supported version + let mut childs = vec![]; + cfg_if::cfg_if! { + if #[cfg(target_os = "macos")] { + // macOS JVMs are saved on multiple possible places, + // /Library/Java/JavaVirtualMachines are the ones installed by an admin + // /Users/$USER/Library/Java/JavaVirtualMachines are the ones installed locally by the user + let libs = glob::glob(concatcp!("/Library/Java/JavaVirtualMachines/*/Contents/Home/bin/", JAVA_BIN)) + .unwrap() + .filter_map(|res| res.ok()); + } else if #[cfg(unix)] { + // Linux JVMs are saved on /usr/lib/jvm from what I found out, + // there is usually a default dir and a default-runtime dir also which are linked + // to the current default runtime and the current default JDK (I think it's JDK) + let libs = glob::glob(concatcp!("/usr/lib/jvm/*/bin/", JAVA_BIN)) + .unwrap() + .filter_map(|res| res.ok()); + } else { + let libs = which::which_all(JAVA_BIN).unwrap(); + } + } + + for java in libs { + let res = spawn_java(java.as_os_str(), java_version.as_os_str()); + + match res { + Ok(child) => childs.push((java.into_os_string(), child)), + Err(e) => println!("Error on trying to spawn a Java executable: {}", e), + } + } + + childs + .into_iter() + .filter_map(|(p, mut c)| { + c.wait() + .expect("Failed on executing a Java executable") + .code() + .map(|code| (p, code)) + .filter(|(_p, code)| *code >= MINIMUM_JAVA_VERSION) + }) + .collect() +} diff --git a/server/build.gradle.kts b/server/build.gradle.kts index ca05cb9677..9927931e57 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -168,3 +168,5 @@ configure { eclipse().configFile("spotless.xml") } } + +tasks.getByName("run", JavaExec::class) { standardInput = System.`in` } diff --git a/server/src/main/java/dev/slimevr/Main.kt b/server/src/main/java/dev/slimevr/Main.kt index 5ca119671a..a308373200 100644 --- a/server/src/main/java/dev/slimevr/Main.kt +++ b/server/src/main/java/dev/slimevr/Main.kt @@ -15,12 +15,14 @@ import java.io.IOException import java.lang.System import java.net.ServerSocket import javax.swing.JOptionPane +import kotlin.concurrent.thread import kotlin.system.exitProcess val VERSION = (GIT_VERSION_TAG.ifEmpty { GIT_COMMIT_HASH }) + if (GIT_CLEAN) "" else "-dirty" -var vrServer: VRServer? = null +lateinit var vrServer: VRServer + private set fun main(args: Array) { System.setProperty("awt.useSystemAAFontSettings", "on") @@ -63,6 +65,7 @@ fun main(args: Array) { "SlimeVR: Java Runtime Mismatch", JOptionPane.ERROR_MESSAGE ) + LogManager.closeLogger() return } try { @@ -84,27 +87,27 @@ fun main(args: Array) { "SlimeVR: Ports are busy", JOptionPane.ERROR_MESSAGE ) + LogManager.closeLogger() return } try { vrServer = VRServer() - vrServer!!.start() + vrServer.start() Keybinding(vrServer) + val scanner = thread { + while (true) { + if (readln() == "exit") { + vrServer.interrupt() + break + } + } + } + vrServer.join() + scanner.join() + LogManager.closeLogger() + exitProcess(0) } catch (e: Throwable) { e.printStackTrace() - try { - Thread.sleep(2000L) - } catch (e2: InterruptedException) { - e.printStackTrace() - } - exitProcess(1) // Exit in case error happened on init and window - // not appeared, but some thread - // started - } finally { - try { - Thread.sleep(2000L) - } catch (e: InterruptedException) { - e.printStackTrace() - } + exitProcess(1) } } diff --git a/server/src/main/java/dev/slimevr/VRServer.java b/server/src/main/java/dev/slimevr/VRServer.java index 934a03514b..59cb813e16 100644 --- a/server/src/main/java/dev/slimevr/VRServer.java +++ b/server/src/main/java/dev/slimevr/VRServer.java @@ -299,7 +299,10 @@ public void run() { // final long time = System.currentTimeMillis() - start; try { Thread.sleep(1); // 1000Hz - } catch (InterruptedException ignored) {} + } catch (InterruptedException error) { + LogManager.info("VRServer thread interrupted"); + break; + } } } diff --git a/server/src/main/java/io/eiren/util/logging/LogManager.java b/server/src/main/java/io/eiren/util/logging/LogManager.java index a153e506a6..e608480636 100644 --- a/server/src/main/java/io/eiren/util/logging/LogManager.java +++ b/server/src/main/java/io/eiren/util/logging/LogManager.java @@ -105,6 +105,13 @@ public static void log(Level level, String message, Throwable t) { log.log(level, message, t); } + public static void closeLogger() { + for (Handler handler : global.getHandlers()) { + handler.close(); + removeHandler(handler); + } + } + static { boolean hasConsoleHandler = false; for (Handler h : global.getHandlers()) {