Skip to content

Commit

Permalink
0.3.5 (#116)
Browse files Browse the repository at this point in the history
- Try to show error dialogs instead of failing silently if app crashes
on startup.
- Handle issues with missing WebView2 dependency, often caused by
"debloating" scripts. If Rai Pal detects this problem, it will now try
to show helpful messages and explain what can be done to fix it.
- Add button to open the logs folder in settings tab.
- Logs have been moved to `%AppData%\raicuparta\rai-pal\data\logs`.
  • Loading branch information
Raicuparta authored Jan 2, 2024
2 parents 99573fc + 2095f02 commit cebabb4
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 71 deletions.
14 changes: 13 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
{
"cSpell.words": [
"Anticheat",
"appguid",
"appname",
"compatdata",
"ctypes",
"flashbang",
"hasher",
"HKEY",
"ICONERROR",
"IDYES",
"installsource",
"isfreeapp",
"KTJZNR",
"needsadmin",
"otherinstallcmd",
"predef",
"repairtype",
"runas",
"shellapi",
"subchild",
"subkey",
"SYSTEMMODAL",
"uevr",
"ureq",
"winapi",
"windowsonlinerepair",
"winreg",
"winuser"
"winuser",
"YESNO"
]
}
1 change: 1 addition & 0 deletions backend/Cargo.lock

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

1 change: 1 addition & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ rand = "0.8.5"
winreg = "0.52.0"
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
log = "0.4.20"
tauri-runtime = "0.14.1"

[target.'cfg(target_os = "linux")'.dependencies]
webkit2gtk = "0.18.2"
Expand Down
37 changes: 24 additions & 13 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,10 @@ use result::{
};
use steamlocate::SteamDir;
use tauri::{
api::dialog::message,
AppHandle,
Manager,
};
use tauri_plugin_log::{
LogLevel,
LogTarget,
};
use tauri_plugin_log::LogTarget;

mod analytics;
mod app_state;
Expand Down Expand Up @@ -483,6 +479,14 @@ async fn frontend_ready() -> Result {
Ok(())
}

#[tauri::command]
#[specta::specta]
async fn open_logs_folder() -> Result {
paths::open_logs_folder()?;

Ok(())
}

#[tauri::command]
#[specta::specta]
async fn dummy_command() -> Result<(InstalledGame, AppEvent)> {
Expand All @@ -495,20 +499,18 @@ fn main() {
// Since I'm making all exposed functions async, panics won't crash anything important, I think.
// So I can just catch panics here and show a system message with the error.
std::panic::set_hook(Box::new(|info| {
error!("Panic: {info}");
message(
None::<&tauri::Window>,
"Failed to execute command",
info.to_string(),
);
windows::error_dialog(&info.to_string());
}));

let tauri_builder = tauri::Builder::default()
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.targets([LogTarget::LogDir, LogTarget::Stdout])
.targets([
paths::logs_path().map_or(LogTarget::LogDir, LogTarget::Folder),
LogTarget::Stdout,
])
.build(),
)
.manage(AppState {
Expand Down Expand Up @@ -566,6 +568,7 @@ fn main() {
get_remote_mods,
open_mod_loader_folder,
refresh_game,
open_logs_folder,
]
);

Expand All @@ -585,7 +588,15 @@ fn main() {
error!("Failed to generate api bindings: {err}");
}
}

tauri_builder
.run(tauri::generate_context!())
.unwrap_or_else(|err| error!("Failed to run Tauri application: {err}"));
.unwrap_or_else(|error| {
if let tauri::Error::Runtime(tauri_runtime::Error::CreateWebview(webview_error)) = error
{
windows::webview_error_dialog(&webview_error.to_string());
} else {
windows::error_dialog(&error.to_string());
}
});
}
8 changes: 8 additions & 0 deletions backend/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ pub fn app_data_path() -> Result<PathBuf> {
Ok(project_dirs.data_dir().to_path_buf())
}

pub fn logs_path() -> Result<PathBuf> {
Ok(app_data_path()?.join("logs"))
}

pub fn open_logs_folder() -> Result {
Ok(open::that_detached(logs_path()?)?)
}

pub fn installed_mods_path() -> Result<PathBuf> {
Ok(app_data_path()?.join("mod-loaders"))
}
Expand Down
203 changes: 163 additions & 40 deletions backend/src/windows.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,30 @@
use std::path::Path;
use std::{
ffi::OsStr,
io,
os::windows::ffi::OsStrExt,
path::Path,
};

use crate::Result;
use winapi::um::{
shellapi::ShellExecuteW,
winuser::SW_SHOW,
};

#[cfg(windows)]
pub fn run_as_admin(exe_path: &Path, parameters: &str) -> Result {
use std::{
ffi::OsStr,
io,
os::windows::ffi::OsStrExt,
ptr,
};

use winapi::{
ctypes::c_int,
um::{
shellapi::ShellExecuteW,
winuser::SW_SHOW,
},
};

use crate::paths;

let exe_path_str: Vec<u16> = OsStr::new(&exe_path).encode_wide().chain(Some(0)).collect();
use crate::{
paths,
Result,
};

let verb = OsStr::new("runas");
let verb_str: Vec<u16> = verb.encode_wide().chain(Some(0)).collect();

let parameters_str: Vec<u16> = OsStr::new(&parameters)
.encode_wide()
.chain(Some(0))
.collect();

let directory = paths::path_parent(exe_path)?;
let directory_str: Vec<u16> = OsStr::new(directory).encode_wide().chain(Some(0)).collect();
pub fn run_as_admin(exe_path: &Path, parameters: &str) -> Result {
let directory = path_to_wide(paths::path_parent(exe_path)?);

let result = unsafe {
ShellExecuteW(
ptr::null_mut(),
verb_str.as_ptr(),
exe_path_str.as_ptr(),
parameters_str.as_ptr(),
directory_str.as_ptr(),
str_to_wide("runas").as_ptr(),
path_to_wide(exe_path).as_ptr(),
str_to_wide(parameters).as_ptr(),
directory.as_ptr(),
SW_SHOW,
)
};
Expand All @@ -53,9 +37,148 @@ pub fn run_as_admin(exe_path: &Path, parameters: &str) -> Result {
}
}

#[cfg(not(windows))]
pub const fn run_as_admin(_exe_path: &Path, _parameters: &str) -> Result {
use crate::result::Error;
use std::{
path::PathBuf,
ptr,
};

use lazy_regex::regex_captures;
use log::error;
use winapi::{
ctypes::{
c_int,
c_uint,
},
um::winuser::{
MessageBoxW,
IDYES,
MB_ICONERROR,
MB_OK,
MB_SYSTEMMODAL,
MB_YESNO,
},
};
use winreg::{
enums::HKEY_LOCAL_MACHINE,
RegKey,
};

fn os_str_to_wide(os_str: &OsStr) -> Vec<u16> {
os_str.encode_wide().chain(std::iter::once(0)).collect()
}

fn str_to_wide(text: &str) -> Vec<u16> {
os_str_to_wide(OsStr::new(text))
}

fn path_to_wide(path: &Path) -> Vec<u16> {
os_str_to_wide(path.as_os_str())
}

fn base_error_dialog(error_text: &str, flags: c_uint) -> c_int {
error!("{error_text}");

unsafe {
MessageBoxW(
ptr::null_mut(),
str_to_wide(error_text).as_ptr(),
str_to_wide("Rai Pal").as_ptr(),
flags | MB_ICONERROR | MB_SYSTEMMODAL,
)
}
}

pub fn error_dialog(error_text: &str) {
base_error_dialog(error_text, MB_OK);
}

pub fn error_question_dialog(error_text: &str) -> bool {
base_error_dialog(error_text, MB_YESNO) == IDYES
}

const WEBVIEW_ERROR_MESSAGE: &str = r#"Webview error. This usually means something is wrong with your Webview2 installation.
Would you like to attempt to repair it?"#;

const WEBVIEW_REPAIR_FAILED_MESSAGE: &str = r#"Ok, that didn't work either.
You can try to repair it yourself by going to Windows "Installed Apps", then selecting WebView2 and picking "Repair".
If that does't work, you can try downloading it from Microsoft. Would you like to open the website to download WebView2?"#;

const WEBVIEW_WEBSITE_URL: &str =
"https://developer.microsoft.com/microsoft-edge/webview2#download";

const DEFAULT_WEBVIEW2_REPAIR_PATH: &str =
r"C:\Program Files (x86)\Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe";
const DEFAULT_WEBVIEW2_REPAIR_ARGS: &str =
"/install appguid={F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}&appname=Microsoft%20Edge%20WebView&needsadmin=true&repairtype=windowsonlinerepair /installsource otherinstallcmd";

const WEBVIEW_REGISTRY_KEY: &str =
r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft EdgeWebView";

fn try_open_logs_folder() {
if let Err(error) = paths::open_logs_folder() {
error!("Failed to even open the logs folder: {error}");
}
}

// Even though Tauri is supposed to check if Webview2 is installed and install it if needed,
// it's easy to corrupt it in such a way that it gets detected as installed, even though it's actually unusable.
// This causes the app to silently crash on launch. So we need to give the user some options on what to do in this case.
pub fn webview_error_dialog(error_text: &str) {
error!("{error_text}");
if error_question_dialog(WEBVIEW_ERROR_MESSAGE) {
let (path, command) = get_webview2_repair();

run_as_admin(&path, &command).unwrap_or_else(|repair_error| {
error!("{repair_error}");
if error_question_dialog(WEBVIEW_REPAIR_FAILED_MESSAGE) {
open::that_detached(WEBVIEW_WEBSITE_URL).unwrap_or_else(|open_website_error| {
error!("{open_website_error}");
// If we reached here then everything failed, just give up please.
error_dialog(&format!(
"Somehow even that failed.\n\nPlease report this error, and include the logs file."
));
try_open_logs_folder();
});
} else {
try_open_logs_folder();
}
});
} else {
try_open_logs_folder();
}
}

// Seems to be common for Webview2 to be broken, but with Edge still present,
// so the Edge installer can be used to repair Webview2.
fn get_webview2_repair() -> (PathBuf, String) {
// We crawl through the windows registry sewage to find the command for repairing Webview2.
RegKey::predef(HKEY_LOCAL_MACHINE)
.open_subkey(WEBVIEW_REGISTRY_KEY)
.and_then(|key| key.get_value::<String, _>("ModifyPath"))
.map_or_else(
|error| get_fallback_webview2_repair(&error.to_string()),
|path_with_args| {
// The command is store in the registry in a format like "C:/Some/Path/To/the.exe \and some=args".
if let Some((_, path, args)) = regex_captures!(r#""(.+)" ?(.*)"#, &path_with_args) {
return (PathBuf::from(path), args.to_string());
}
get_fallback_webview2_repair(&format!(
"Failed to parse registry item: {path_with_args}"
))
},
)
}

Err(Error::NotImplemented)
// If we aren't able to figure out the Webview2 repair command from the registry,
// we just fall back to a hardcoded one, which should be the case for the majority of users.
// If we reach here it's unlikely that the repair exe will be present anyway, but worth a shot I guess.
fn get_fallback_webview2_repair(message: &str) -> (PathBuf, String) {
error!("Failed to get WebView2 repair exe path from registry. Attempting with default path. {message}");
(
PathBuf::from(DEFAULT_WEBVIEW2_REPAIR_PATH),
DEFAULT_WEBVIEW2_REPAIR_ARGS.to_string(),
)
}
Loading

0 comments on commit cebabb4

Please sign in to comment.