Skip to content

Commit

Permalink
feat: VRServer gracefully shutdown (#632)
Browse files Browse the repository at this point in the history
Co-authored-by: ImUrX <[email protected]>
  • Loading branch information
marcozzxx810 and ImUrX committed Apr 11, 2023
1 parent 0ad3b9f commit ccef1df
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 236 deletions.
266 changes: 47 additions & 219 deletions gui/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf>,
#[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<PathBuf> {
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<Child> {
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
Expand All @@ -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();
Expand Down Expand Up @@ -173,6 +75,8 @@ fn main() {
}

// Spawn server process
let exit_flag = Arc::new(AtomicBool::new(false));
let mut backend: Option<CommandChild> = None;
let run_path = get_launch_path(cli);

let stdout_recv = if let Some(p) = run_path {
Expand All @@ -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 {
Expand All @@ -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()),
Expand All @@ -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))) => {
Expand All @@ -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<String> = 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<String> = 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()
}
Loading

0 comments on commit ccef1df

Please sign in to comment.