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
5 changed files with 119 additions and 221 deletions
+2 -42
View File
@@ -89,46 +89,15 @@ std::chrono::seconds SelfTest::get_remaining_seconds() const
{
using namespace std::literals;
// Use adaptive estimation if we have observed at least one completed segment.
// This works for all drive types including NVMe (which may not report total duration).
if (!segment_durations_.empty()) {
// Calculate average duration of observed segments
double sum = 0.0;
for (const auto& duration : segment_durations_) {
sum += duration;
}
const double avg_segment_duration = sum / segment_durations_.size();
// Estimate remaining time based on observed average and remaining segments
// remaining_percent_ goes from 100 (start) to 0 (end), in 10% decrements
const int remaining_segments = (remaining_percent_ + 9) / 10; // round up
const double estimated_remaining = avg_segment_duration * remaining_segments - timer_.elapsed();
const auto rem_rounded = static_cast<int64_t>(std::round(estimated_remaining));
if (rem_rounded < 0) {
return -1s; // estimate exhausted; return unknown
}
return std::chrono::seconds(rem_rounded);
}
// Fall back to drive's initial estimate when we don't have observed data yet
const std::chrono::seconds total = get_min_duration_seconds();
if (total <= 0s)
return -1s; // unknown
// seconds per 10% (drive estimate)
const double gran = (double(total.count()) / 9.);
const double gran = (double(total.count()) / 9.); // seconds per 10%
// since remaining_percent_ may be manually set to 100, we limit from the above.
const double rem_seconds_at_last_change = std::min(double(total.count()), gran * remaining_percent_ / 10.);
const double rem = rem_seconds_at_last_change - timer_.elapsed();
const auto rem_rounded = static_cast<int64_t>(std::round(rem));
// If the estimated time for the current percentage has elapsed but the drive hasn't
// progressed, the drive's estimate was inaccurate. Return -1 (unknown) instead of 0
// to avoid misleading "ETA: 0 sec" which could persist for hours.
if (rem_rounded < 0) {
return -1s;
}
return std::chrono::seconds(rem_rounded);
return std::chrono::seconds(std::max(int64_t(0), (int64_t)std::round(rem))); // don't return negative values.
}
@@ -523,15 +492,6 @@ hz::ExpectedVoid<SelfTestExecutionError> SelfTest::update(const std::shared_ptr<
// and reaches 00% on completion. That's 9 pieces.
if (status_ == SelfTestStatus::InProgress) {
if (remaining_percent_ != last_seen_percent_) {
// Record the duration of the completed segment for adaptive ETA calculation.
// Skip the first segment (typically 90→80) as it may be instant or partially
// completed when monitoring begins, which would skew the average.
if (first_segment_seen_) {
const double elapsed = timer_.elapsed();
segment_durations_.push_back(elapsed);
} else {
first_segment_seen_ = true; // Mark that we've seen the first transition
}
last_seen_percent_ = remaining_percent_;
timer_.start(); // restart the timer
}
+1 -12
View File
@@ -18,7 +18,6 @@ Copyright:
#include <cstdint>
#include <chrono>
#include <unordered_map>
#include <vector>
#include "storage_device.h"
#include "command_executor.h"
@@ -127,15 +126,7 @@ class SelfTest {
/// Get estimated time of completion for the test.
/// The estimation uses an adaptive algorithm:
/// - Initially uses the drive's reported test duration estimate
/// - After completing one or more 10% segments, switches to using the observed
/// average segment duration to predict remaining time
/// - This provides more accurate ETAs when the drive's estimate is inaccurate
/// (e.g., under load or with drives that consistently under/overestimate)
/// \return -1 if N/A or unknown (including when the drive's estimated duration has been
/// exceeded without a percentage change, which means the estimate was inaccurate).
/// Note that 0 is a valid value meaning the test is finishing right now.
/// \return -1 if N/A or unknown. Note that 0 is a valid value.
[[nodiscard]] std::chrono::seconds get_remaining_seconds() const;
@@ -189,8 +180,6 @@ class SelfTest {
std::chrono::seconds poll_in_seconds_ = std::chrono::seconds(-1); ///< The user is asked to poll after this much seconds have passed.
Glib::Timer timer_; ///< Counts time since the last percent change
std::vector<double> segment_durations_; ///< Actual durations of completed 10% segments (in seconds), for adaptive ETA calculation
bool first_segment_seen_ = false; ///< Whether we've observed the first percentage change (to skip the potentially instant/partial first segment)
};
-1
View File
@@ -15,7 +15,6 @@ endif()
add_library(applib_tests OBJECT)
target_sources(applib_tests PRIVATE
test_app_regex.cpp
test_selftest.cpp
test_smartctl_parser.cpp
test_smartctl_version_parser.cpp
)
-163
View File
@@ -1,163 +0,0 @@
/******************************************************************************
License: BSD Zero Clause License
Copyright:
(C) 2026 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup applib_tests
/// \weakgroup applib_tests
/// @{
#include "catch2/catch.hpp"
#include "applib/selftest.h"
#include "applib/storage_device.h"
#include <chrono>
TEST_CASE("SelfTest basic functionality", "[selftest]")
{
using namespace std::literals;
SECTION("Test type names are correct")
{
REQUIRE(SelfTest::get_test_displayable_name(SelfTest::TestType::ShortTest) != "[internal_error]");
REQUIRE(SelfTest::get_test_displayable_name(SelfTest::TestType::LongTest) != "[internal_error]");
REQUIRE(SelfTest::get_test_displayable_name(SelfTest::TestType::Conveyance) != "[internal_error]");
}
SECTION("Test status severity mapping")
{
REQUIRE(get_self_test_status_severity(SelfTestStatus::Unknown) == SelfTestStatusSeverity::None);
REQUIRE(get_self_test_status_severity(SelfTestStatus::CompletedNoError) == SelfTestStatusSeverity::None);
REQUIRE(get_self_test_status_severity(SelfTestStatus::ManuallyAborted) == SelfTestStatusSeverity::Warning);
REQUIRE(get_self_test_status_severity(SelfTestStatus::Interrupted) == SelfTestStatusSeverity::Warning);
REQUIRE(get_self_test_status_severity(SelfTestStatus::CompletedWithError) == SelfTestStatusSeverity::Error);
REQUIRE(get_self_test_status_severity(SelfTestStatus::InProgress) == SelfTestStatusSeverity::None);
REQUIRE(get_self_test_status_severity(SelfTestStatus::Reserved) == SelfTestStatusSeverity::None);
}
SECTION("Test not active by default")
{
auto device = std::make_shared<StorageDevice>("/dev/mock");
SelfTest test(device, SelfTest::TestType::ShortTest);
// Test should not be active immediately after construction
REQUIRE(test.is_active() == false);
REQUIRE(test.get_status() == SelfTestStatus::Unknown);
REQUIRE(test.get_remaining_percent() == -1);
}
SECTION("Remaining seconds returns unknown when not running")
{
auto device = std::make_shared<StorageDevice>("/dev/mock");
SelfTest test(device, SelfTest::TestType::ShortTest);
// When no test is running, remaining seconds should be -1 (unknown)
REQUIRE(test.get_remaining_seconds() == -1s);
}
SECTION("NVMe device without duration estimate")
{
auto device = std::make_shared<StorageDevice>("/dev/nvme0");
device->set_detected_type(StorageDeviceDetectedType::Nvme);
SelfTest test(device, SelfTest::TestType::ShortTest);
// NVMe devices don't report duration, should return -1
REQUIRE(test.get_min_duration_seconds() == -1s);
// Without a running test, remaining should also be -1
REQUIRE(test.get_remaining_seconds() == -1s);
}
SECTION("Test type is correctly stored")
{
auto device = std::make_shared<StorageDevice>("/dev/mock");
SelfTest short_test(device, SelfTest::TestType::ShortTest);
REQUIRE(short_test.get_test_type() == SelfTest::TestType::ShortTest);
SelfTest long_test(device, SelfTest::TestType::LongTest);
REQUIRE(long_test.get_test_type() == SelfTest::TestType::LongTest);
SelfTest conveyance_test(device, SelfTest::TestType::Conveyance);
REQUIRE(conveyance_test.get_test_type() == SelfTest::TestType::Conveyance);
}
SECTION("Poll time is initially unknown")
{
auto device = std::make_shared<StorageDevice>("/dev/mock");
SelfTest test(device, SelfTest::TestType::ShortTest);
// Before starting, poll time should be -1 (unknown)
REQUIRE(test.get_poll_in_seconds() == -1s);
}
}
TEST_CASE("SelfTest EXT enum helpers", "[selftest][enum_helpers]")
{
SECTION("Status enum to string conversion")
{
// Verify that enum helper works for common statuses
auto status_str = SelfTestStatusExt::get_displayable_name(SelfTestStatus::InProgress);
REQUIRE(!status_str.empty());
status_str = SelfTestStatusExt::get_displayable_name(SelfTestStatus::CompletedNoError);
REQUIRE(!status_str.empty());
status_str = SelfTestStatusExt::get_displayable_name(SelfTestStatus::Unknown);
REQUIRE(!status_str.empty());
}
SECTION("Status enum storable name")
{
// Verify storable names (for serialization/deserialization)
auto storable = SelfTestStatusExt::get_storable_name(SelfTestStatus::InProgress);
REQUIRE(storable == "in_progress");
storable = SelfTestStatusExt::get_storable_name(SelfTestStatus::ManuallyAborted);
REQUIRE(storable == "manually_aborted");
storable = SelfTestStatusExt::get_storable_name(SelfTestStatus::CompletedNoError);
REQUIRE(storable == "completed_no_error");
}
SECTION("Default value is Unknown")
{
REQUIRE(SelfTestStatusExt::default_value == SelfTestStatus::Unknown);
}
}
TEST_CASE("SelfTest support detection", "[selftest][support]")
{
SECTION("ATA device capabilities check")
{
auto device = std::make_shared<StorageDevice>("/dev/sda");
device->set_detected_type(StorageDeviceDetectedType::AtaSsd);
// Without capability properties, tests should not be supported
SelfTest short_test(device, SelfTest::TestType::ShortTest);
REQUIRE(short_test.is_supported() == false);
SelfTest long_test(device, SelfTest::TestType::LongTest);
REQUIRE(long_test.is_supported() == false);
}
SECTION("NVMe conveyance test unsupported")
{
auto device = std::make_shared<StorageDevice>("/dev/nvme0");
device->set_detected_type(StorageDeviceDetectedType::Nvme);
// Conveyance test is not supported on NVMe
SelfTest conveyance_test(device, SelfTest::TestType::Conveyance);
REQUIRE(conveyance_test.is_supported() == false);
}
}
/// @}
+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 {};