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 125 additions and 448 deletions
-128
View File
@@ -1,128 +0,0 @@
name: Build AppImage
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [ main ]
pull_request:
branches: [ main ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
BUILD_TYPE: RelWithDebInfo
jobs:
build-appimage:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Update Package Database
run: sudo apt-get update
- name: Install Dependencies
run: sudo apt-get install libgtkmm-3.0-dev gettext wget file libfuse2
- name: Download linuxdeploy
# Note: Using 'continuous' release for latest version. For reproducible builds,
# pin to a specific release tag and verify SHA256 checksum.
run: |
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
sudo mv linuxdeploy-x86_64.AppImage /usr/local/bin/linuxdeploy
- name: Download appimagetool
# Note: Using 'continuous' release for latest version. For reproducible builds,
# pin to a specific release tag and verify SHA256 checksum.
run: |
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool
- name: Create Build Directory
run: cmake -E make_directory build-appimage
- name: Configure CMake for AppImage
shell: bash
working-directory: ${{github.workspace}}/build-appimage
env:
CC: gcc-11
CXX: g++-11
run: >
cmake $GITHUB_WORKSPACE
-DCMAKE_BUILD_TYPE=$BUILD_TYPE
-DCMAKE_INSTALL_PREFIX=/usr
-DAPP_BUILD_APPIMAGE=ON
-DCMAKE_TOOLCHAIN_FILE="$GITHUB_WORKSPACE/toolchains/linux-dev.cmake"
- name: Build
shell: bash
working-directory: ${{github.workspace}}/build-appimage
run: cmake --build . --config $BUILD_TYPE -j$(nproc)
- name: Install to AppDir
shell: bash
working-directory: ${{github.workspace}}/build-appimage
run: DESTDIR=${{github.workspace}}/AppDir cmake --install .
- name: Setup AppDir structure
shell: bash
working-directory: ${{github.workspace}}
run: |
# Copy desktop file and icon to AppDir root
cp AppDir/usr/share/applications/dev.shaduri.gsmartcontrol.desktop AppDir/
cp AppDir/usr/share/icons/hicolor/256x256/apps/gsmartcontrol.png AppDir/
# Create AppRun script
cat > AppDir/AppRun << 'EOF'
#!/bin/bash
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export PATH="$HERE/usr/bin:${PATH:+:$PATH}"
export GSETTINGS_SCHEMA_DIR="$HERE/usr/share/glib-2.0/schemas:${GSETTINGS_SCHEMA_DIR:+:$GSETTINGS_SCHEMA_DIR}"
export GDK_PIXBUF_MODULEDIR="$HERE/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders"
export GDK_PIXBUF_MODULE_FILE="$HERE/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"
exec "$HERE/usr/bin/gsmartcontrol" "$@"
EOF
chmod +x AppDir/AppRun
- name: Bundle dependencies with linuxdeploy
shell: bash
working-directory: ${{github.workspace}}
run: |
linuxdeploy --appdir AppDir \
--executable AppDir/usr/bin/gsmartcontrol \
--desktop-file AppDir/dev.shaduri.gsmartcontrol.desktop \
--icon-file AppDir/gsmartcontrol.png
- name: Update GDK pixbuf cache
shell: bash
working-directory: ${{github.workspace}}
continue-on-error: true
run: |
if [ -d "AppDir/usr/lib/gdk-pixbuf-2.0" ]; then
gdk-pixbuf-query-loaders > AppDir/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache || true
fi
- name: Get version
id: version
shell: bash
run: |
VERSION=$(sed -n 's/.*CMAKE_PROJECT_VERSION[[:space:]]*"\([^"]*\)".*/\1/p' version.txt)
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Create AppImage
shell: bash
working-directory: ${{github.workspace}}
run: |
ARCH=x86_64 appimagetool AppDir GSmartControl-${{ steps.version.outputs.VERSION }}-x86_64.AppImage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: GSmartControl AppImage
path: ${{github.workspace}}/GSmartControl-*-x86_64.AppImage
-10
View File
@@ -175,16 +175,6 @@ build_main_workflow:
- x86_64
- i586
# AppImage build (using Ubuntu 22.04 as base)
- name: AppImage
paths:
- target_project: openSUSE:Tools
target_repository: xUbuntu_22.04
- target_project: Ubuntu:22.04
target_repository: universe
architectures:
- x86_64
- set_flags:
flags:
- type: publish
-1
View File
@@ -39,7 +39,6 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
# User-controlled build options
option(APP_BUILD_EXAMPLES "Build examples" OFF)
option(APP_BUILD_TESTS "Build tests" OFF)
option(APP_BUILD_APPIMAGE "Build for AppImage (portable data file paths)" OFF)
# Install documentation
+6 -24
View File
@@ -6,17 +6,8 @@
# Generate files
if (APP_BUILD_APPIMAGE)
set(APPIMAGE_DESKTOP_ID "dev.shaduri.gsmartcontrol")
set(APPIMAGE_EXEC_LINE "Exec=\"${CMAKE_INSTALL_FULL_BINDIR}/gsmartcontrol\"")
configure_file("gsmartcontrol.appdata.in.xml" "gsmartcontrol.appdata.xml" ESCAPE_QUOTES @ONLY)
configure_file("gsmartcontrol.in.desktop" "dev.shaduri.gsmartcontrol.desktop" ESCAPE_QUOTES @ONLY)
else()
set(APPIMAGE_DESKTOP_ID "gsmartcontrol")
set(APPIMAGE_EXEC_LINE "Exec=\"${CMAKE_INSTALL_FULL_BINDIR}/gsmartcontrol-root\"")
configure_file("gsmartcontrol.appdata.in.xml" "gsmartcontrol.appdata.xml" ESCAPE_QUOTES @ONLY)
configure_file("gsmartcontrol.in.desktop" "gsmartcontrol.desktop" ESCAPE_QUOTES @ONLY)
endif()
configure_file("gsmartcontrol.appdata.in.xml" "gsmartcontrol.appdata.xml" ESCAPE_QUOTES @ONLY)
configure_file("gsmartcontrol.in.desktop" "gsmartcontrol.desktop" ESCAPE_QUOTES @ONLY)
configure_file("gsmartcontrol-root.in.sh" "gsmartcontrol-root.sh" ESCAPE_QUOTES @ONLY)
configure_file("org.gsmartcontrol.in.policy" "org.gsmartcontrol.policy" ESCAPE_QUOTES @ONLY)
@@ -51,13 +42,8 @@ endif()
# Desktop file
if (NOT WIN32)
if (APP_BUILD_APPIMAGE)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dev.shaduri.gsmartcontrol.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications/")
else()
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications/")
endif()
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications/")
# Appdata file
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol.appdata.xml"
@@ -69,13 +55,9 @@ if (NOT WIN32)
# Man pages
install(FILES "man1/gsmartcontrol.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1")
if (NOT APP_BUILD_APPIMAGE)
install(FILES "man1/gsmartcontrol.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1" RENAME "gsmartcontrol-root.1")
endif()
install(FILES "man1/gsmartcontrol.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1" RENAME "gsmartcontrol-root.1")
# Scripts (this goes to bin, not sbin, as it doesn't require root privileges before running)
if (NOT APP_BUILD_APPIMAGE)
install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol-root.sh" TYPE BIN RENAME "gsmartcontrol-root")
endif()
install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/gsmartcontrol-root.sh" TYPE BIN RENAME "gsmartcontrol-root")
endif()
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>dev.shaduri.gsmartcontrol</id>
<id>gsmartcontrol</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<name>GSmartControl</name>
@@ -14,7 +14,7 @@
on it.
</p>
</description>
<launchable type="desktop-id">@APPIMAGE_DESKTOP_ID@.desktop</launchable>
<launchable type="desktop-id">gsmartcontrol.desktop</launchable>
<url type="homepage">https://gsmartcontrol.shaduri.dev</url>
<screenshots>
<screenshot type="default">
+1 -2
View File
@@ -63,5 +63,4 @@ Icon=gsmartcontrol
#X-KDE-RootOnly=true
# Run with root permissions.
# For AppImage builds, execute the binary directly (AppImage does not support embedded sudo scripts)
@APPIMAGE_EXEC_LINE@
Exec="@CMAKE_INSTALL_FULL_BINDIR@/gsmartcontrol-root"
-106
View File
@@ -1,106 +0,0 @@
# GSmartControl AppImage Build
This directory contains scripts and configurations for building GSmartControl as an AppImage.
## What is AppImage?
AppImage is a format for distributing portable software on Linux without requiring installation. AppImages are self-contained applications that can run on most Linux distributions.
## Building the AppImage
### Prerequisites
You need to have the following installed:
- cmake (>= 3.14)
- g++ or clang with C++20 support
- GTK3 and gtkmm-3.0 development packages
- smartmontools (for runtime)
- linuxdeploy and appimagetool (the script will download them if not available)
On Ubuntu/Debian:
```bash
sudo apt-get install cmake g++ libgtkmm-3.0-dev gettext smartmontools
```
On Fedora:
```bash
sudo dnf install cmake gcc-c++ gtkmm30-devel gettext smartmontools
```
### Build Steps
From the repository root directory, run:
```bash
./packaging/appimage/build-appimage.sh
```
The script will:
1. Configure CMake with `-DAPP_BUILD_APPIMAGE=ON`
2. Build the application
3. Create an AppDir with the proper structure
4. Bundle dependencies using linuxdeploy
5. Create the final AppImage file
### Output
The script creates an AppImage file named `GSmartControl-<version>-<arch>.AppImage` in the repository root.
## Running the AppImage
**Important:** GSmartControl requires root privileges to access disk drives. You must run the AppImage with sudo:
```bash
sudo ./GSmartControl-<version>-x86_64.AppImage
```
### Why sudo is required?
- AppImage format does not support embedded privilege escalation scripts
- The binary must be executed directly as root to access `/dev/sd*` and `/dev/nvme*` devices
- This is different from traditional package installations which use the `gsmartcontrol-root` wrapper script
## AppImage-Specific Changes
When building with `-DAPP_BUILD_APPIMAGE=ON`, the following changes are applied:
1. **Data file paths**: Uses relative paths from binary location (`bin/../share/...`)
2. **Desktop file**:
- Uses rDNS format: `dev.shaduri.gsmartcontrol.desktop`
- Executes binary directly without `gsmartcontrol-root` wrapper
3. **Binary location**: Installed to `bin/` instead of `sbin/`
4. **No wrapper script**: `gsmartcontrol-root` is not installed
## Compatibility
The AppImage should work on:
- Fedora Atomic (Silverblue, Kinoite, etc.)
- Most modern Linux distributions with GTK3 support
- Systems with read-only root filesystems
Tested architectures:
- x86_64 (Intel/AMD 64-bit)
## Troubleshooting
### "Permission denied" when running AppImage
Make the AppImage executable:
```bash
chmod +x GSmartControl-*.AppImage
```
### "No drives detected"
Make sure to run with sudo:
```bash
sudo ./GSmartControl-*.AppImage
```
### GTK theme issues
You may need to install GTK3 themes on your system. The AppImage bundles the necessary libraries but uses system themes when available.
## Notes for Developers
- The AppImage build uses the same source code as regular builds
- The `BuildEnv::is_appimage_build()` function can be used to detect AppImage builds at runtime
- Icon and UI file paths are resolved at runtime relative to the binary location
- The AppImage includes all necessary GTK3 and gtkmm libraries
-147
View File
@@ -1,147 +0,0 @@
#!/bin/bash
###############################################################################
# License: BSD Zero Clause License file
# Copyright:
# (C) 2025 Alexander Shaduri <ashaduri@gmail.com>
###############################################################################
# Script to build GSmartControl AppImage
# This script should be run from the repository root directory
#
# Requirements:
# - cmake, wget, file
# - libgtkmm-3.0-dev (or equivalent GTK3 development packages)
# - libfuse2 (required to run linuxdeploy and appimagetool AppImages)
# On Ubuntu/Debian: sudo apt-get install libfuse2
# On Ubuntu 24.04+: sudo apt-get install libfuse2t64
set -e # Exit on error
set -u # Exit on undefined variable
# Configuration
APPDIR="${APPDIR:-AppDir}"
BUILD_DIR="${BUILD_DIR:-build-appimage}"
ARCH="${ARCH:-x86_64}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}Building GSmartControl AppImage${NC}"
echo "Architecture: $ARCH"
echo "Build directory: $BUILD_DIR"
echo "AppDir: $APPDIR"
echo
# Check dependencies
if ! command -v cmake &> /dev/null; then
echo -e "${RED}Error: cmake not found. Please install cmake.${NC}"
exit 1
fi
if ! command -v linuxdeploy &> /dev/null; then
echo -e "${YELLOW}Warning: linuxdeploy not found. Will attempt to download it.${NC}"
# Note: Using 'continuous' release for latest version. For reproducible builds,
# pin to a specific release tag and verify SHA256 checksum.
LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage"
wget -N "$LINUXDEPLOY_URL" -O linuxdeploy
chmod +x linuxdeploy
LINUXDEPLOY="./linuxdeploy"
else
LINUXDEPLOY="linuxdeploy"
fi
if ! command -v appimagetool &> /dev/null; then
echo -e "${YELLOW}Warning: appimagetool not found. Will attempt to download it.${NC}"
# Note: Using 'continuous' release for latest version. For reproducible builds,
# pin to a specific release tag and verify SHA256 checksum.
APPIMAGETOOL_URL="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage"
wget -N "$APPIMAGETOOL_URL" -O appimagetool
chmod +x appimagetool
APPIMAGETOOL="./appimagetool"
else
APPIMAGETOOL="appimagetool"
fi
# Clean previous build
echo -e "${GREEN}Cleaning previous build...${NC}"
rm -rf "$BUILD_DIR"
rm -rf "$APPDIR"
# Create build directory
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
# Configure with CMake for AppImage build
echo -e "${GREEN}Configuring CMake for AppImage build...${NC}"
cmake .. \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_INSTALL_PREFIX=/usr \
-DAPP_BUILD_APPIMAGE=ON \
-DCMAKE_TOOLCHAIN_FILE="../toolchains/linux-dev.cmake"
# Build
echo -e "${GREEN}Building...${NC}"
cmake --build . --config RelWithDebInfo -j$(nproc)
# Install to AppDir
echo -e "${GREEN}Installing to AppDir...${NC}"
DESTDIR="../$APPDIR" cmake --install .
cd ..
# Copy desktop file and icon to AppDir root (required by AppImage)
echo -e "${GREEN}Setting up AppDir structure...${NC}"
cp "$APPDIR/usr/share/applications/dev.shaduri.gsmartcontrol.desktop" "$APPDIR/"
cp "$APPDIR/usr/share/icons/hicolor/256x256/apps/gsmartcontrol.png" "$APPDIR/"
# Create AppRun script
cat > "$APPDIR/AppRun" << 'EOF'
#!/bin/bash
# AppRun script for GSmartControl AppImage
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
# Export library paths
export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export PATH="$HERE/usr/bin:${PATH:+:$PATH}"
# Export GTK settings
export GSETTINGS_SCHEMA_DIR="$HERE/usr/share/glib-2.0/schemas:${GSETTINGS_SCHEMA_DIR:+:$GSETTINGS_SCHEMA_DIR}"
export GDK_PIXBUF_MODULEDIR="$HERE/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders"
export GDK_PIXBUF_MODULE_FILE="$HERE/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"
# Run the application
exec "$HERE/usr/bin/gsmartcontrol" "$@"
EOF
chmod +x "$APPDIR/AppRun"
# Use linuxdeploy to bundle dependencies
echo -e "${GREEN}Bundling dependencies with linuxdeploy...${NC}"
$LINUXDEPLOY --appdir "$APPDIR" \
--executable "$APPDIR/usr/bin/gsmartcontrol" \
--desktop-file "$APPDIR/dev.shaduri.gsmartcontrol.desktop" \
--icon-file "$APPDIR/gsmartcontrol.png"
# Update GDK pixbuf cache
if [ -d "$APPDIR/usr/lib/gdk-pixbuf-2.0" ]; then
echo -e "${GREEN}Updating GDK pixbuf cache...${NC}"
gdk-pixbuf-query-loaders > "$APPDIR/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" 2>/dev/null || true
fi
# Get version from version.txt (extract quoted CMAKE_PROJECT_VERSION value)
VERSION=$(sed -n 's/.*CMAKE_PROJECT_VERSION[[:space:]]*"\([^"]*\)".*/\1/p' version.txt | head -n1)
# Create AppImage
echo -e "${GREEN}Creating AppImage...${NC}"
ARCH=$ARCH $APPIMAGETOOL "$APPDIR" "GSmartControl-${VERSION}-${ARCH}.AppImage"
echo
echo -e "${GREEN}AppImage created successfully!${NC}"
echo -e "Output: ${GREEN}GSmartControl-${VERSION}-${ARCH}.AppImage${NC}"
echo
echo -e "${YELLOW}Note: The AppImage must be run with sudo to access disk drives.${NC}"
echo -e "Example: ${GREEN}sudo ./GSmartControl-${VERSION}-${ARCH}.AppImage${NC}"
-6
View File
@@ -82,12 +82,6 @@ elseif ("${CMAKE_SYSTEM_NAME}" MATCHES "QNX")
set(CONFIG_KERNEL_QNX TRUE)
endif()
# AppImage build configuration
if (APP_BUILD_APPIMAGE)
set(CONFIG_APPIMAGE_BUILD TRUE)
message(STATUS "AppImage build enabled - using portable data file paths")
endif()
# Additionally, these macros are defined by OS / compilers:
-11
View File
@@ -26,7 +26,6 @@ Copyright:
#cmakedefine CONFIG_KERNEL_SOLARIS
#cmakedefine CONFIG_KERNEL_DARWIN
#cmakedefine CONFIG_KERNEL_QNX
#cmakedefine CONFIG_APPIMAGE_BUILD
#if defined CONFIG_KERNEL_WINDOWS32 || defined CONFIG_KERNEL_WINDOWS64
#define CONFIG_KERNEL_FAMILY_WINDOWS
@@ -184,16 +183,6 @@ struct BuildEnv {
#endif
}
/// Check if this is an AppImage build (portable data file paths).
static constexpr bool is_appimage_build()
{
#ifdef CONFIG_APPIMAGE_BUILD
return true;
#else
return false;
#endif
}
};
-2
View File
@@ -62,8 +62,6 @@ target_link_libraries(gsmartcontrol
if (WIN32)
install(TARGETS gsmartcontrol DESTINATION .)
elseif (APP_BUILD_APPIMAGE)
install(TARGETS gsmartcontrol DESTINATION "${CMAKE_INSTALL_BINDIR}/")
else()
install(TARGETS gsmartcontrol DESTINATION "${CMAKE_INSTALL_SBINDIR}/")
endif()
-6
View File
@@ -475,13 +475,7 @@ bool app_init_and_loop(int& argc, char**& argv)
hz::data_file_add_search_directory("icons", application_dir / "icons");
hz::data_file_add_search_directory("ui", application_dir / "ui");
hz::data_file_add_search_directory("doc", application_dir / "doc");
} else if constexpr(BuildEnv::is_appimage_build()) {
// AppImage: Use relative paths from binary location (bin/../share/...)
hz::data_file_add_search_directory("icons", application_dir.parent_path() / "share" / BuildEnv::package_name() / "icons");
hz::data_file_add_search_directory("ui", application_dir.parent_path() / "share" / BuildEnv::package_name() / "ui");
hz::data_file_add_search_directory("doc", application_dir.parent_path() / "share" / "doc" / BuildEnv::package_name());
} else {
// Traditional Unix installation: Use absolute paths from BuildEnv
hz::data_file_add_search_directory("icons", hz::fs_path_from_string(BuildEnv::package_pkgdata_dir()) / BuildEnv::package_name() / "icons"); // /usr/share/program_name/icons
hz::data_file_add_search_directory("ui", hz::fs_path_from_string(BuildEnv::package_pkgdata_dir()) / BuildEnv::package_name() / "ui"); // /usr/share/program_name/ui
hz::data_file_add_search_directory("doc", hz::fs_path_from_string(BuildEnv::package_doc_dir())); // /usr/share/doc/[packages/]gsmartcontrol
+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 {};