From e9dcc5717ea615f4d05a98eff0888252e0036f4d Mon Sep 17 00:00:00 2001
From: bicarus <202771338+bicarus-dev@users.noreply.github.com>
Date: Sat, 16 May 2026 16:27:59 -0700
Subject: [PATCH] launcher: option to run as user without elevation (#695)
## Link to GitHub Issue or related Pull Request, if one exists
#694
## Description of change
Before this change, we have it set in the executable manifest to always
ask for UAC elevation.
This change removes that and instead attempts to prompt for elevation
when we launch, depending on the new developer option that is being
added here to run as current user instead of elevating.
The default behavior continues to run as admin. This is still the safest
in terms of regression risk.
A bit of history on this: while spicecfg never technically needed to
elevate, we continue to do so to avoid difference in behavior when the
game is launched. For example, if the user is using an input remapper
running without elevation, it would inject input into spicecfg running
as user but fail to do so if the game runs elevated.
WinXP builds are unaffected (there is no UAC).
Some features will straight up fail without elevation so this will
remain as a debug option. To name a few (not a complete list):
* reboot/shutdown of PC
* running at realtime process priority
* `-nvprofile` option
* ...
## Testing
---
src/spice2x/build/manifest.manifest | 2 +-
src/spice2x/cfg/manifest.manifest | 10 +++---
src/spice2x/launcher/launcher.cpp | 21 +++++++++++
src/spice2x/launcher/options.cpp | 16 +++++++++
src/spice2x/launcher/options.h | 1 +
src/spice2x/util/sysutils.cpp | 55 +++++++++++++++++++++++++++++
src/spice2x/util/sysutils.h | 11 ++++--
7 files changed, 108 insertions(+), 8 deletions(-)
diff --git a/src/spice2x/build/manifest.manifest b/src/spice2x/build/manifest.manifest
index 1810dc4..f4d072b 100644
--- a/src/spice2x/build/manifest.manifest
+++ b/src/spice2x/build/manifest.manifest
@@ -23,7 +23,7 @@
-
+
diff --git a/src/spice2x/cfg/manifest.manifest b/src/spice2x/cfg/manifest.manifest
index 813e32b..528088f 100644
--- a/src/spice2x/cfg/manifest.manifest
+++ b/src/spice2x/cfg/manifest.manifest
@@ -20,10 +20,10 @@
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp
index a61876a..67b67a3 100644
--- a/src/spice2x/launcher/launcher.cpp
+++ b/src/spice2x/launcher/launcher.cpp
@@ -5,6 +5,7 @@
#include
#include
+#include
#include
#include "api/modules/capture.h"
@@ -301,6 +302,26 @@ int main_implementation(int argc, char *argv[]) {
}
auto &options = *options_ptr;
+#if !SPICE_XP
+
+ {
+ // skip elevation only if AutoElevate is explicitly set to "user"
+ const bool skip_elevation = options[launcher::Options::AutoElevate].is_active() &&
+ options[launcher::Options::AutoElevate].value_text() == "user";
+ if (!skip_elevation && !sysutils::is_running_as_admin()) {
+ log_info("launcher", "relaunching with administrator privileges");
+ if (sysutils::relaunch_as_admin()) {
+ exit(0);
+ } else {
+ // elevation failed or was denied by the user
+ log_fatal("launcher", "failed to launch with administrator privileges");
+ exit(1);
+ }
+ }
+ }
+
+#endif // !SPICE_XP
+
// check options
// TODO: get rid of some booleans here and make use of the options directly
if (options[launcher::Options::OpenConfigurator].value_bool()) {
diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp
index e5014ec..9ed847b 100644
--- a/src/spice2x/launcher/options.cpp
+++ b/src/spice2x/launcher/options.cpp
@@ -2812,6 +2812,22 @@ static const std::vector OPTION_DEFINITIONS = {
.type = OptionType::Bool,
.category = "Network (Development)",
},
+ {
+ // AutoElevate
+ .title = "Run as",
+ .name = "runas",
+ .desc = "Controls whether spice will automatically re-launch with administrator privileges at startup.\n\n"
+ "admin (default): automatically re-launch with administrator privileges, "
+ "needed for most games to function properly\n\n"
+ "user: skip elevation, run with current user privileges; game may fail to launch, "
+ "crash, or critical features may silently fail; use at your own risk.",
+ .type = OptionType::Enum,
+ .category = "Development",
+ .elements = {
+ {"admin", ""},
+ {"user", ""},
+ },
+ },
};
const std::vector &launcher::get_categories(Options::OptionsCategory category) {
diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h
index c0fec7d..dfddf57 100644
--- a/src/spice2x/launcher/options.h
+++ b/src/spice2x/launcher/options.h
@@ -283,6 +283,7 @@ namespace launcher {
OtocaCamHook,
DisableHighResTimer,
EnableICMPHook,
+ AutoElevate,
};
enum class OptionsCategory {
diff --git a/src/spice2x/util/sysutils.cpp b/src/spice2x/util/sysutils.cpp
index 0ceb621..e053d45 100644
--- a/src/spice2x/util/sysutils.cpp
+++ b/src/spice2x/util/sysutils.cpp
@@ -10,6 +10,9 @@
#include
#undef WIN32_NO_STATUS
+#include
+#include
+
#include "hooks/graphics/graphics.h"
#include "avs/game.h"
#include "util/detour.h"
@@ -585,4 +588,56 @@ namespace sysutils {
(void*)EnumDisplayDevicesA_hook,
(void**)&EnumDisplayDevicesA_orig);
}
+
+#if !SPICE_XP
+
+ bool is_running_as_admin() {
+ HANDLE token_handle = nullptr;
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token_handle)) {
+ return false;
+ }
+
+ TOKEN_ELEVATION elevation = {};
+ DWORD size = sizeof(elevation);
+ const BOOL ok = GetTokenInformation(
+ token_handle, TokenElevation, &elevation, size, &size);
+ CloseHandle(token_handle);
+
+ if (!ok) {
+ return false;
+ }
+ return elevation.TokenIsElevated != 0;
+ }
+
+ bool relaunch_as_admin() {
+ // get the current executable path
+ wchar_t executable_path[MAX_PATH];
+ if (!GetModuleFileNameW(nullptr, executable_path, MAX_PATH)) {
+ return false;
+ }
+
+ // get the current working directory to pass to the elevated process
+ wchar_t working_dir[MAX_PATH];
+ if (!GetCurrentDirectoryW(MAX_PATH, working_dir)) {
+ return false;
+ }
+
+ // skip past argv[0] in the original command line
+ const wchar_t *args = PathGetArgsW(GetCommandLineW());
+
+ // use ShellExecuteW with "runas" verb to elevate
+ HINSTANCE result = ShellExecuteW(
+ nullptr,
+ L"runas",
+ executable_path,
+ args,
+ working_dir,
+ SW_SHOW
+ );
+
+ // ShellExecute returns > 32 on success
+ return (intptr_t)result > 32;
+ }
+
+#endif // !SPICE_XP
}
diff --git a/src/spice2x/util/sysutils.h b/src/spice2x/util/sysutils.h
index 343504f..44d32fa 100644
--- a/src/spice2x/util/sysutils.h
+++ b/src/spice2x/util/sysutils.h
@@ -21,5 +21,12 @@ namespace sysutils {
const std::vector &enumerate_monitors();
extern std::string SECOND_MONITOR_OVERRIDE;
- void hook_EnumDisplayDevicesA();
-}
\ No newline at end of file
+ void hook_EnumDisplayDevicesA();
+
+#if !SPICE_XP
+
+ bool is_running_as_admin();
+ bool relaunch_as_admin();
+
+#endif // !SPICE_XP
+}