Compare commits

..
Author SHA1 Message Date
anthropic-code-agent[bot]andashaduri 460ed687ee Fix Help menu links not working when running as root
Implement fallback mechanism to launch URLs as the original user when gsmartcontrol is running with root privileges. This fixes the issue where gtk_show_uri_on_window() fails when running as root due to inaccessible D-Bus session.

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
2026-03-06 15:12:15 +00:00
anthropic-code-agent[bot] b8de0d933b Initial plan 2026-03-06 15:08:19 +00:00
13 changed files with 126 additions and 353 deletions
-55
View File
@@ -11,8 +11,6 @@ Copyright:
#include "gui_utils.h"
#include <cmath>
namespace {
@@ -167,59 +165,6 @@ bool gui_show_text_entry_dialog(const std::string& title, const std::string& mes
namespace {
/// Storage for Windows fractional scaling percentage (0 if no fractional scaling)
int g_windows_fractional_scaling_percent = 0;
}
int app_get_windows_fractional_scaling_percent()
{
return g_windows_fractional_scaling_percent;
}
void app_set_windows_fractional_scaling_percent(int percent)
{
g_windows_fractional_scaling_percent = percent;
}
void app_apply_fractional_scaling_to_default_size(Gtk::Window* window, int config_size_w, int config_size_h)
{
if (!window) {
return;
}
int size_w = config_size_w;
int size_h = config_size_h;
// Apply fractional scaling adjustment on Windows if no custom size is configured
// This compensates for GTK3's lack of fractional scaling support
const int full_percent = g_windows_fractional_scaling_percent;
if (full_percent > 0 && size_w == 0 && size_h == 0) {
// Get the default size from glade and scale it
int glade_w = 0, glade_h = 0;
window->get_default_size(glade_w, glade_h);
if (glade_w > 0 && glade_h > 0) {
const double system_scale = static_cast<double>(full_percent) / 100.0;
const int integer_ui_scale = full_percent / 100; // GTK3 uses floor (integer) scaling
const double correction = system_scale / static_cast<double>(integer_ui_scale);
size_w = static_cast<int>(std::lround(glade_w * correction));
size_h = static_cast<int>(std::lround(glade_h * correction));
}
}
if (size_w > 0 && size_h > 0) {
window->set_default_size(size_w, size_h);
}
}
-17
View File
@@ -59,23 +59,6 @@ bool gui_show_text_entry_dialog(const std::string& title, const std::string& mes
/// Get the fractional scaling percentage detected on Windows (0 if not detected or integer scale).
/// For example, at 150% scaling, this returns 150; at 125% scaling, this returns 125; at 250% scaling, this returns 250.
/// Returns 0 for exact integer scales (100%, 200%, etc.).
int app_get_windows_fractional_scaling_percent();
/// Set the fractional scaling percentage detected on Windows.
/// This should only be called once during application initialization.
void app_set_windows_fractional_scaling_percent(int percent);
/// Apply fractional scaling to default window size if fractional scaling is detected.
/// This compensates for GTK3's lack of fractional scaling support on Windows.
/// \param window The window to apply scaling to
/// \param config_size_w Configured width (0 if using glade default)
/// \param config_size_h Configured height (0 if using glade default)
void app_apply_fractional_scaling_to_default_size(Gtk::Window* window, int config_size_w, int config_size_h);
-1
View File
@@ -68,5 +68,4 @@ endif()
add_subdirectory(ui)
add_subdirectory(tests)
-3
View File
@@ -31,9 +31,6 @@ Copyright:
GscExecutorLogWindow::GscExecutorLogWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder> ui)
: AppBuilderWidget<GscExecutorLogWindow, false>(gtkcobj, std::move(ui))
{
// Apply fractional scaling to default window size
app_apply_fractional_scaling_to_default_size(this, 0, 0);
// Connect callbacks
Gtk::Button* window_close_button = nullptr;
+3 -2
View File
@@ -35,7 +35,6 @@ Copyright:
#include "gsc_info_window.h"
#include "gsc_executor_error_dialog.h"
#include "gsc_startup_settings.h"
#include "gsc_init.h" // app_apply_fractional_scaling_to_default_size()
@@ -167,7 +166,9 @@ GscInfoWindow::GscInfoWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder>
{
const int def_size_w = rconfig::get_data<int>("gui/info_window/default_size_w");
const int def_size_h = rconfig::get_data<int>("gui/info_window/default_size_h");
app_apply_fractional_scaling_to_default_size(this, def_size_w, def_size_h);
if (def_size_w > 0 && def_size_h > 0) {
set_default_size(def_size_w, def_size_h);
}
}
// Create missing widgets
+4 -10
View File
@@ -47,7 +47,6 @@
#include "applib/window_instance_manager.h"
#include "applib/gsc_settings.h"
#include "applib/gui_utils.h" // app_set_windows_fractional_scaling_percent()
#include "gsc_main_window.h"
#include "gsc_executor_log_window.h"
#include "gsc_init.h"
@@ -81,7 +80,6 @@ namespace {
return channel;
}
}
@@ -521,14 +519,10 @@ bool app_init_and_loop(int& argc, char**& argv)
if (h_ppi > 0) {
const double scale = h_ppi / 96.0;
debug_out_info("app", "Windows system DPI: " << h_ppi << ", scale: " << scale << "\n");
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100; // 250 -> 200, 150 -> 100
if (full_scale_percent != integer_scale_percent) { // fractional scaling detected
// Store the full scale percent for use in window sizing
app_set_windows_fractional_scaling_percent(full_scale_percent);
// Increase the font size by the fractional amount
const int fraction_percent = full_scale_percent - integer_scale_percent;
debug_out_dump("app", "Fractional scaling detected (" << full_scale_percent << "%), increasing font size by " << fraction_percent << "%.\n");
const int fraction_percent = static_cast<int>(std::round(scale * 100)) % 100;
if (fraction_percent != 0) { // fractional scaling
// Increase the font size by fraction, but round down the size to match the Windows behavior (?)
debug_out_dump("app", "Fractional scaling detected, increasing font size by " << fraction_percent << "%.\n");
Gtk::Settings::get_default()->property_gtk_font_name()
.set_value("Segoe UI " + hz::number_to_string_nolocale(static_cast<int>(9 * (1. + fraction_percent/100.))));
}
-19
View File
@@ -15,11 +15,6 @@ Copyright:
#include <string>
namespace Gtk {
class Window;
}
/// Initialize the application and run the main loop
bool app_init_and_loop(int& argc, char**& argv);
@@ -33,20 +28,6 @@ void app_quit();
std::string app_get_debug_buffer_str();
/// Get the fractional scaling percentage detected on Windows (0 if not detected or integer scale).
/// For example, at 150% scaling, this returns 150; at 125% scaling, this returns 125; at 250% scaling, this returns 250.
/// Returns 0 for exact integer scales (100%, 200%, etc.).
int app_get_windows_fractional_scaling_percent();
/// Apply fractional scaling to default window size if fractional scaling is detected.
/// This compensates for GTK3's lack of fractional scaling support on Windows.
/// \param window The window to apply scaling to
/// \param config_size_w Configured width (0 if using glade default)
/// \param config_size_h Configured height (0 if using glade default)
void app_apply_fractional_scaling_to_default_size(Gtk::Window* window, int config_size_w, int config_size_h);
#endif
+3 -1
View File
@@ -60,7 +60,9 @@ GscMainWindow::GscMainWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder>
{
const int def_size_w = rconfig::get_data<int>("gui/main_window/default_size_w");
const int def_size_h = rconfig::get_data<int>("gui/main_window/default_size_h");
app_apply_fractional_scaling_to_default_size(this, def_size_w, def_size_h);
if (def_size_w > 0 && def_size_h > 0) {
set_default_size(def_size_w, def_size_h);
}
}
// show the window first, scan later
-4
View File
@@ -24,7 +24,6 @@ Copyright:
#include "applib/app_builder_widget.h"
#include "applib/app_gtkmm_tools.h"
#include "applib/gui_utils.h" // app_apply_fractional_scaling_to_default_size()
@@ -50,9 +49,6 @@ class GscTextWindow : public AppBuilderWidget<GscTextWindow<InstanceSwitch>, Ins
GscTextWindow(typename Gtk::Window::BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder> ui)
: AppBuilderWidget<GscTextWindow<InstanceSwitch>, InstanceSwitch::multi_instance>(gtkcobj, std::move(ui))
{
// Apply fractional scaling to default window size
app_apply_fractional_scaling_to_default_size(this, 0, 0);
// Connect callbacks
Gtk::Button* save_as_button = nullptr;
-21
View File
@@ -1,21 +0,0 @@
###############################################################################
# License: BSD Zero Clause License file
# Copyright:
# (C) 2026 Alexander Shaduri <ashaduri@gmail.com>
###############################################################################
if (NOT APP_BUILD_TESTS)
set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL true)
else()
set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL false)
endif()
# Use Object libraries to allow runtime test discovery
add_library(gui_tests OBJECT)
target_sources(gui_tests PRIVATE
test_fractional_scaling.cpp
)
target_link_libraries(gui_tests PRIVATE
Catch2
)
-216
View File
@@ -1,216 +0,0 @@
/******************************************************************************
License: BSD Zero Clause License
Copyright:
(C) 2008 - 2026 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup gui_tests
/// \weakgroup gui_tests
/// @{
#include "catch2/catch.hpp"
#include <cmath>
/// Test the fractional scaling calculation logic
/// This tests the mathematical correctness of the scaling correction ratio
TEST_CASE("FractionalScalingCalculation", "[gui][scaling]")
{
// Test the calculation logic that's used in app_apply_fractional_scaling_to_default_size()
// correction = system_scale / integer_ui_scale
SECTION("125% scaling (fractional)")
{
const int full_percent = 125;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 1.25
const int integer_ui_scale = full_percent / 100; // 1
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 1.25);
REQUIRE(integer_ui_scale == 1);
REQUIRE(correction == 1.25);
// Test window scaling: 800x600 base should become 1000x750
const int base_w = 800;
const int base_h = 600;
const int scaled_w = static_cast<int>(std::lround(base_w * correction));
const int scaled_h = static_cast<int>(std::lround(base_h * correction));
REQUIRE(scaled_w == 1000);
REQUIRE(scaled_h == 750);
}
SECTION("150% scaling (fractional)")
{
const int full_percent = 150;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 1.5
const int integer_ui_scale = full_percent / 100; // 1
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 1.5);
REQUIRE(integer_ui_scale == 1);
REQUIRE(correction == 1.5);
// Test window scaling: 800x600 base should become 1200x900
const int base_w = 800;
const int base_h = 600;
const int scaled_w = static_cast<int>(std::lround(base_w * correction));
const int scaled_h = static_cast<int>(std::lround(base_h * correction));
REQUIRE(scaled_w == 1200);
REQUIRE(scaled_h == 900);
}
SECTION("175% scaling (fractional)")
{
const int full_percent = 175;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 1.75
const int integer_ui_scale = full_percent / 100; // 1
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 1.75);
REQUIRE(integer_ui_scale == 1);
REQUIRE(correction == 1.75);
// Test window scaling: 800x600 base should become 1400x1050
const int base_w = 800;
const int base_h = 600;
const int scaled_w = static_cast<int>(std::lround(base_w * correction));
const int scaled_h = static_cast<int>(std::lround(base_h * correction));
REQUIRE(scaled_w == 1400);
REQUIRE(scaled_h == 1050);
}
SECTION("250% scaling (fractional) - Critical test case")
{
// This is the case that was broken before the fix
const int full_percent = 250;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 2.5
const int integer_ui_scale = full_percent / 100; // 2
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 2.5);
REQUIRE(integer_ui_scale == 2);
REQUIRE(correction == 1.25); // NOT 1.5 (the old broken behavior)
// Test window scaling: GTK already applies 2x, we need to apply 1.25x more
// For a base of 800x600, GTK makes it 1600x1200, we should scale to 2000x1500
const int gtk_scaled_w = 1600; // After GTK's 2x integer scaling
const int gtk_scaled_h = 1200;
const int final_w = static_cast<int>(std::lround(gtk_scaled_w * correction));
const int final_h = static_cast<int>(std::lround(gtk_scaled_h * correction));
REQUIRE(final_w == 2000); // 2.5x total = 800 * 2.5
REQUIRE(final_h == 1500); // 2.5x total = 600 * 2.5
}
SECTION("225% scaling (fractional)")
{
const int full_percent = 225;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 2.25
const int integer_ui_scale = full_percent / 100; // 2
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 2.25);
REQUIRE(integer_ui_scale == 2);
REQUIRE(correction == 1.125);
// Test window scaling: 800x600 at 2x GTK = 1600x1200, with correction = 1800x1350
const int gtk_scaled_w = 1600;
const int gtk_scaled_h = 1200;
const int final_w = static_cast<int>(std::lround(gtk_scaled_w * correction));
const int final_h = static_cast<int>(std::lround(gtk_scaled_h * correction));
REQUIRE(final_w == 1800);
REQUIRE(final_h == 1350);
}
SECTION("100% scaling (integer, no fractional)")
{
// Should not trigger fractional scaling at all (full_percent would be 0)
const int full_percent = 100;
const int integer_scale_percent = (full_percent / 100) * 100;
REQUIRE(full_percent == integer_scale_percent); // No fractional component
}
SECTION("200% scaling (integer, no fractional)")
{
// Should not trigger fractional scaling at all (full_percent would be 0)
const int full_percent = 200;
const int integer_scale_percent = (full_percent / 100) * 100;
REQUIRE(full_percent == integer_scale_percent); // No fractional component
}
}
/// Test the DPI detection logic
TEST_CASE("DPIDetectionLogic", "[gui][scaling][dpi]")
{
SECTION("Detect 125% scaling from DPI")
{
const int h_ppi = 120; // 120 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 125);
REQUIRE(integer_scale_percent == 100);
REQUIRE(full_scale_percent != integer_scale_percent); // Fractional scaling detected
}
SECTION("Detect 150% scaling from DPI")
{
const int h_ppi = 144; // 144 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 150);
REQUIRE(integer_scale_percent == 100);
REQUIRE(full_scale_percent != integer_scale_percent); // Fractional scaling detected
}
SECTION("Detect 250% scaling from DPI")
{
const int h_ppi = 240; // 240 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 250);
REQUIRE(integer_scale_percent == 200);
REQUIRE(full_scale_percent != integer_scale_percent); // Fractional scaling detected
}
SECTION("No fractional scaling at 100% (96 DPI)")
{
const int h_ppi = 96; // 96 DPI (standard)
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 100);
REQUIRE(integer_scale_percent == 100);
REQUIRE(full_scale_percent == integer_scale_percent); // No fractional scaling
}
SECTION("No fractional scaling at 200% (192 DPI)")
{
const int h_ppi = 192; // 192 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 200);
REQUIRE(integer_scale_percent == 200);
REQUIRE(full_scale_percent == integer_scale_percent); // No fractional scaling
}
}
/// @}
+116 -3
View File
@@ -21,6 +21,11 @@ Copyright:
#include "win32_tools.h" // hz::win32_utf8_to_utf16
#else
#include <memory>
#include <unistd.h> // geteuid, fork, execvp, setuid, setgid
#include <sys/types.h> // uid_t, gid_t
#include <sys/wait.h> // waitpid
#include <pwd.h> // getpwuid
#include "env_tools.h" // hz::env_get_value
#endif
@@ -29,6 +34,89 @@ Copyright:
namespace hz {
#ifndef _WIN32
/// Launch URL as the original user when running as root.
/// This is needed because gtk_show_uri_on_window() doesn't work when running as root
/// (D-Bus session is not accessible).
/// \return error message on error, empty string on success.
inline std::string launch_url_as_original_user(const std::string& link)
{
// Get the original user's UID from environment variables
// SUDO_UID is set by sudo, PKEXEC_UID is set by pkexec
std::string uid_str;
uid_t original_uid = 0;
gid_t original_gid = 0;
if (hz::env_get_value("SUDO_UID", uid_str) || hz::env_get_value("PKEXEC_UID", uid_str)) {
try {
original_uid = static_cast<uid_t>(std::stoul(uid_str));
} catch (...) {
return "Cannot parse original user UID";
}
// Get the original user's GID
struct passwd* pw = getpwuid(original_uid);
if (pw) {
original_gid = pw->pw_gid;
} else {
return "Cannot get original user information";
}
} else {
return "Cannot determine original user UID";
}
// Fork and execute xdg-open as the original user
pid_t pid = fork();
if (pid < 0) {
return "Cannot fork process";
}
if (pid == 0) {
// Child process
// Restore HOME environment variable if available
// This helps xdg-open find the correct configuration
std::string sudo_user;
if (hz::env_get_value("SUDO_USER", sudo_user)) {
struct passwd* pw = getpwnam(sudo_user.c_str());
if (pw && pw->pw_dir) {
setenv("HOME", pw->pw_dir, 1);
}
}
// Drop privileges to original user
// Set GID first, then UID (order matters for security)
if (setgid(original_gid) != 0) {
_exit(1);
}
if (setuid(original_uid) != 0) {
_exit(1);
}
// Execute xdg-open with the URL
const char* argv[] = {"xdg-open", link.c_str(), nullptr};
execvp("xdg-open", const_cast<char* const*>(argv));
// If execvp returns, it failed
_exit(1);
}
// Parent process - wait for child
int status = 0;
if (waitpid(pid, &status, 0) == -1) {
return "Cannot wait for child process";
}
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
return {}; // Success
}
return "xdg-open failed to launch URL";
}
#endif // _WIN32
/// Open URL in browser or mailto: link in mail client.
/// Return error message on error, empty string otherwise.
@@ -50,16 +138,41 @@ inline std::string launch_url([[maybe_unused]] GtkWindow* window, const std::str
#else
GError* error = nullptr;
bool status = false;
// Check if running as root
bool is_root = (geteuid() == 0);
// If running as root, try to launch as the original user first
if (is_root) {
std::string result = launch_url_as_original_user(link);
if (result.empty()) {
return {}; // Success
}
// If launching as original user failed, fall through to try GTK method
}
// Try the standard GTK method
#if GTK_CHECK_VERSION(3, 22, 0)
bool status = static_cast<bool>(gtk_show_uri_on_window(window, link.c_str(), GDK_CURRENT_TIME, &error));
status = static_cast<bool>(gtk_show_uri_on_window(window, link.c_str(), GDK_CURRENT_TIME, &error));
#else
GdkScreen* screen = (window ? gtk_window_get_screen(window) : nullptr);
bool status = static_cast<bool>(gtk_show_uri(screen, link.c_str(), GDK_CURRENT_TIME, &error));
status = static_cast<bool>(gtk_show_uri(screen, link.c_str(), GDK_CURRENT_TIME, &error));
#endif
std::unique_ptr<GError, decltype(&g_error_free)> uerror(error, &g_error_free);
if (!status) {
return std::string("Cannot open URL: ")
// GTK method failed. If running as root, we already tried the fallback.
// Otherwise, try the fallback now.
if (!is_root) {
std::string result = launch_url_as_original_user(link);
if (result.empty()) {
return {}; // Success
}
}
// Both methods failed, return error
return std::string("Cannot open URL")
+ ((error && error->message) ? (std::string(": ") + error->message) : ".");
}
return {};
-1
View File
@@ -22,7 +22,6 @@ target_link_libraries(test_all PRIVATE
libdebug
applib_tests
hz_tests
gui_tests
Catch2
)