mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-11 03:51:01 +00:00
Handle Windows elevation-required errors when launching the game
Play failed with ERROR_ELEVATION_REQUIRED (740) for clients whose executable requires administrator rights, because Command::spawn shows no permission prompt. Launch normally first and, only on 740, start the game through ShellExecuteExW with the runas verb, keeping the client folder as the working directory and closing the returned process handle. Other errors, including a declined prompt (1223), are returned unchanged. The Manager itself stays unelevated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
19791edb94
commit
36d82ecc78
3 files changed
+67
-3
No files matched your search
@@ -0,0 +1,7 @@
|
||||
---
|
||||
area: manager
|
||||
type: fixed
|
||||
audience: players
|
||||
title: Play can launch Windows clients that require administrator approval
|
||||
---
|
||||
Windows now shows its normal permission prompt when the game requires elevation. Other launch errors retain their original details.
|
||||
@@ -32,7 +32,7 @@ rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.60", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Threading", "Win32_System_SystemInformation", "Win32_System_Diagnostics_ToolHelp", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock"] }
|
||||
windows-sys = { version = "0.60", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Threading", "Win32_System_SystemInformation", "Win32_System_Diagnostics_ToolHelp", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_UI_Shell", "Win32_System_Registry", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
||||
[features]
|
||||
testkit = []
|
||||
@@ -313,14 +313,71 @@ pub fn launch(client: &Path) -> Result<u32> {
|
||||
if crate::platform::flavor() == crate::platform::Flavor::Docker {
|
||||
return crate::wine::launch(client, exe);
|
||||
}
|
||||
let child = Command::new(client.join(exe)).current_dir(client).spawn()?;
|
||||
Ok(child.id())
|
||||
let executable = client.join(exe);
|
||||
let spawn = || Command::new(&executable).current_dir(client).spawn().map(|child| child.id());
|
||||
#[cfg(windows)]
|
||||
{ launch_with_elevation_fallback(spawn, || launch_elevated(&executable, client)).map_err(Into::into) }
|
||||
#[cfg(not(windows))]
|
||||
{ spawn().map_err(Into::into) }
|
||||
}
|
||||
|
||||
/// Start normally first; ask Windows for elevation only when the client requires it (ERROR_ELEVATION_REQUIRED, 740).
|
||||
/// Any other error, including the player declining the prompt (1223), is returned unchanged.
|
||||
#[cfg(windows)]
|
||||
fn launch_with_elevation_fallback(
|
||||
spawn: impl FnOnce() -> std::io::Result<u32>,
|
||||
elevate: impl FnOnce() -> std::io::Result<u32>,
|
||||
) -> std::io::Result<u32> {
|
||||
match spawn() {
|
||||
Err(error) if error.raw_os_error() == Some(740) => elevate(),
|
||||
result => result,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start only the game with the normal Windows permission prompt; the Manager itself stays unelevated.
|
||||
#[cfg(windows)]
|
||||
fn launch_elevated(executable: &Path, directory: &Path) -> std::io::Result<u32> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::System::Threading::GetProcessId;
|
||||
use windows_sys::Win32::UI::Shell::{ShellExecuteExW, SHELLEXECUTEINFOW, SEE_MASK_NOCLOSEPROCESS, SEE_MASK_NOASYNC};
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
|
||||
let file: Vec<u16> = executable.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let cwd: Vec<u16> = directory.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let verb: Vec<u16> = "runas".encode_utf16().chain(Some(0)).collect();
|
||||
let mut info = SHELLEXECUTEINFOW {
|
||||
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as u32,
|
||||
fMask: SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC,
|
||||
lpVerb: verb.as_ptr(),
|
||||
lpFile: file.as_ptr(),
|
||||
lpDirectory: cwd.as_ptr(),
|
||||
nShow: SW_SHOWNORMAL,
|
||||
..Default::default()
|
||||
};
|
||||
if unsafe { ShellExecuteExW(&mut info) } == 0 { return Err(std::io::Error::last_os_error()); }
|
||||
if info.hProcess.is_null() { return Err(std::io::Error::other("Windows did not return the game process handle.")); }
|
||||
let pid = unsafe { GetProcessId(info.hProcess) };
|
||||
let error = std::io::Error::last_os_error();
|
||||
unsafe { CloseHandle(info.hProcess); }
|
||||
if pid == 0 { return Err(error); }
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn elevation_is_requested_only_when_windows_requires_it() {
|
||||
assert_eq!(launch_with_elevation_fallback(|| Ok(17), || panic!("unneeded elevation")).unwrap(), 17);
|
||||
assert_eq!(launch_with_elevation_fallback(|| Err(std::io::Error::from_raw_os_error(740)), || Ok(23)).unwrap(), 23);
|
||||
let error = launch_with_elevation_fallback(|| Err(std::io::Error::from_raw_os_error(2)), || panic!("wrong error elevated")).unwrap_err();
|
||||
assert_eq!(error.raw_os_error(), Some(2));
|
||||
let cancelled = launch_with_elevation_fallback(|| Err(std::io::Error::from_raw_os_error(740)), || Err(std::io::Error::from_raw_os_error(1223))).unwrap_err();
|
||||
assert_eq!(cancelled.raw_os_error(), Some(1223));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realm_selection_preserves_other_settings_bytes_and_line_endings() {
|
||||
let old = b"\xef\xbb\xbfSET realmName \"Wildcard\"\r\nSET accountName \"\xff\"\r\nset REALMNAME \"old\"\r\nSET realmList \"127.0.0.1\"";
|
||||
|
||||
Reference in new issue
Block a user