diff --git a/src/lib/os/linux/CMakeLists.txt b/src/lib/os/linux/CMakeLists.txt index e42ed90..0723a19 100644 --- a/src/lib/os/linux/CMakeLists.txt +++ b/src/lib/os/linux/CMakeLists.txt @@ -27,6 +27,7 @@ set(HEADERS nativeresolutionhandler.h registryfileparser.h registryfilewatcher.h + steamprocesslistobserver.h steamregistryobserver.h waylandresolutionhandler.h x11resolutionhandler.h @@ -44,6 +45,7 @@ set(SOURCES nativeresolutionhandler.cpp registryfileparser.cpp registryfilewatcher.cpp + steamprocesslistobserver.cpp steamregistryobserver.cpp waylandresolutionhandler.cpp x11resolutionhandler.cpp diff --git a/src/lib/os/linux/nativeprocesshandler.cpp b/src/lib/os/linux/nativeprocesshandler.cpp index 69cae37..4d18107 100644 --- a/src/lib/os/linux/nativeprocesshandler.cpp +++ b/src/lib/os/linux/nativeprocesshandler.cpp @@ -19,11 +19,13 @@ namespace { uint getParentPid(uint pid) { + const std::array pids{pid, 0}; + proc_t proc_info; memset(&proc_info, 0, sizeof(proc_info)); // NOLINTNEXTLINE(*-vararg) - PROCTAB* proc = openproc(PROC_FILLSTATUS | PROC_PID, &pid); + PROCTAB* proc = openproc(PROC_FILLSTATUS | PROC_PID, pids.data()); const auto cleanup{qScopeGuard( [&]() { @@ -42,6 +44,47 @@ uint getParentPid(uint pid) //--------------------------------------------------------------------------------------------------------------------- +QString getCmdline(uint pid) +{ + const std::array pids{pid, 0}; + + proc_t proc_info; + memset(&proc_info, 0, sizeof(proc_info)); + + // NOLINTNEXTLINE(*-vararg) + PROCTAB* proc = openproc(PROC_FILLCOM | PROC_PID, pids.data()); + const auto cleanup{qScopeGuard( + [&]() + { + if (proc != nullptr) + { + closeproc(proc); + } + })}; + if (readproc(proc, &proc_info) == nullptr) + { + return {}; + } + + auto* ptr_list{proc_info.cmdline}; + if (ptr_list == nullptr) + { + return {}; + } + + QStringList cmdline; + while (*ptr_list != nullptr) + { + cmdline.append(*ptr_list); + // NOLINTNEXTLINE(*-pointer-arithmetic) + ptr_list++; + } + + return cmdline.join(' '); +} + +//--------------------------------------------------------------------------------------------------------------------- + std::vector getPids() { const QDir proc_dir{"/proc"}; @@ -79,8 +122,8 @@ std::vector getParentPids(const std::vector& pids) std::vector getRelatedPids(uint pid) { - std::vector all_pids{getPids()}; - std::vector parent_pids{getParentPids(all_pids)}; + const std::vector all_pids{getPids()}; + const std::vector parent_pids{getParentPids(all_pids)}; Q_ASSERT(all_pids.size() == parent_pids.size()); std::vector related_pids; @@ -166,4 +209,48 @@ void NativeProcessHandler::terminate(uint pid) const } } } + +//--------------------------------------------------------------------------------------------------------------------- + +std::vector NativeProcessHandler::getChildrenPids(uint pid) const +{ + const std::vector all_pids{getPids()}; + if (std::find(std::begin(all_pids), std::end(all_pids), pid) == std::end(all_pids)) + { + // Process does not exist, early exit. + return {}; + } + + const std::vector parent_pids{getParentPids(all_pids)}; + Q_ASSERT(all_pids.size() == parent_pids.size()); + + std::vector children_pids; + for (std::size_t i = 0; i < all_pids.size(); ++i) + { + const uint parent_pid{parent_pids[i]}; + if (parent_pid != pid) + { + continue; + } + + const uint process_pid{all_pids[i]}; + if (process_pid == pid) + { + // Is this even possible? To be your own parent? + continue; + } + + children_pids.push_back(process_pid); + } + + return children_pids; +} + +//--------------------------------------------------------------------------------------------------------------------- + +// NOLINTNEXTLINE(*-to-static) +QString NativeProcessHandler::getCmdline(uint pid) const +{ + return ::getCmdline(pid); +} } // namespace os diff --git a/src/lib/os/linux/nativeprocesshandler.h b/src/lib/os/linux/nativeprocesshandler.h index dc06f4c..c33d9a2 100644 --- a/src/lib/os/linux/nativeprocesshandler.h +++ b/src/lib/os/linux/nativeprocesshandler.h @@ -19,5 +19,8 @@ class NativeProcessHandler : public NativeProcessHandlerInterface QString getExecPath(uint pid) const override; void close(uint pid) const override; void terminate(uint pid) const override; + + std::vector getChildrenPids(uint pid) const; + QString getCmdline(uint pid) const; }; } // namespace os diff --git a/src/lib/os/linux/steamprocesslistobserver.cpp b/src/lib/os/linux/steamprocesslistobserver.cpp new file mode 100644 index 0000000..3fbf534 --- /dev/null +++ b/src/lib/os/linux/steamprocesslistobserver.cpp @@ -0,0 +1,97 @@ +// header file include +#include "steamprocesslistobserver.h" + +// system/Qt includes +#include + +// local includes +#include "nativeprocesshandler.h" + +//--------------------------------------------------------------------------------------------------------------------- + +namespace os +{ +SteamProcessListObserver::SteamProcessListObserver() +{ + connect(&m_check_timer, &QTimer::timeout, this, &SteamProcessListObserver::slotCheckProcessList); + + const int interval_ms{2000}; + m_check_timer.setInterval(interval_ms); + m_check_timer.setSingleShot(true); +} + +//--------------------------------------------------------------------------------------------------------------------- + +void SteamProcessListObserver::observePid(uint pid) +{ + stopObserving(); + + if (pid != 0) + { + m_steam_pid = pid; + slotCheckProcessList(); + } +} + +//--------------------------------------------------------------------------------------------------------------------- + +void SteamProcessListObserver::stopObserving() +{ + m_check_timer.stop(); + m_steam_pid = 0; +} + +//--------------------------------------------------------------------------------------------------------------------- + +const std::set& SteamProcessListObserver::getAppIds() const +{ + return m_app_ids; +} + +//--------------------------------------------------------------------------------------------------------------------- + +void SteamProcessListObserver::slotCheckProcessList() +{ + std::set running_apps; + + if (m_steam_pid != 0) + { + NativeProcessHandler proc_handler; + + const auto pids{proc_handler.getChildrenPids(m_steam_pid)}; + for (auto pid : pids) + { + static const QRegularExpression app_id_matcher{R"(AppId=([0-9]+))", + QRegularExpression::CaseInsensitiveOption}; + const auto match{app_id_matcher.match(proc_handler.getCmdline(pid))}; + if (!match.hasCaptured(1)) + { + continue; + } + + const QString captured{match.captured(1)}; + + bool success{false}; + const auto result{captured.toUInt(&success)}; + + if (!success) + { + continue; + } + + running_apps.insert(result); + } + } + + if (running_apps != m_app_ids) + { + std::swap(running_apps, m_app_ids); + emit signalListChanged(); + } + + if (m_steam_pid != 0) + { + m_check_timer.start(); + } +} +} // namespace os diff --git a/src/lib/os/linux/steamprocesslistobserver.h b/src/lib/os/linux/steamprocesslistobserver.h new file mode 100644 index 0000000..231d01e --- /dev/null +++ b/src/lib/os/linux/steamprocesslistobserver.h @@ -0,0 +1,36 @@ +#pragma once + +// system/Qt includes +#include +#include + +//--------------------------------------------------------------------------------------------------------------------- + +namespace os +{ +class SteamProcessListObserver : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(SteamProcessListObserver) + +public: + explicit SteamProcessListObserver(); + ~SteamProcessListObserver() override = default; + + void observePid(uint pid); + void stopObserving(); + + const std::set& getAppIds() const; + +signals: + void signalListChanged(); + +private slots: + void slotCheckProcessList(); + +private: + std::set m_app_ids; + QTimer m_check_timer; + uint m_steam_pid{0}; +}; +} // namespace os diff --git a/src/lib/os/linux/steamregistryobserver.cpp b/src/lib/os/linux/steamregistryobserver.cpp index 32a370a..203a33a 100644 --- a/src/lib/os/linux/steamregistryobserver.cpp +++ b/src/lib/os/linux/steamregistryobserver.cpp @@ -10,10 +10,6 @@ namespace { const QStringList PID_PATH{{"Registry", "HKLM", "Software", "Valve", "Steam", "SteamPID"}}; -const QStringList GLOBAL_APP_ID_PATH{{"Registry", "HKCU", "Software", "Valve", "Steam", "RunningAppID"}}; -const QStringList APPS_PATH{{"Registry", "HKCU", "Software", "Valve", "Steam", "apps"}}; -const QStringList APP_RUNNING_PATH{{"Running"}}; -const QStringList APP_UPDATING_PATH{{"Updating"}}; //--------------------------------------------------------------------------------------------------------------------- @@ -68,6 +64,8 @@ SteamRegistryObserver::SteamRegistryObserver() : m_watcher{QDir::homePath() + "/.steam/registry.vdf"} { connect(&m_watcher, &RegistryFileWatcher::signalRegistryChanged, this, &SteamRegistryObserver::slotRegistryChanged); + connect(&m_process_list_observer, &SteamProcessListObserver::signalListChanged, this, + &SteamRegistryObserver::slotRegistryChanged); connect(&m_observation_delay, &QTimer::timeout, this, [this]() { @@ -85,6 +83,7 @@ SteamRegistryObserver::SteamRegistryObserver() void SteamRegistryObserver::startAppObservation() { m_observation_delay.start(); + m_process_list_observer.observePid(m_pid); } //--------------------------------------------------------------------------------------------------------------------- @@ -92,6 +91,8 @@ void SteamRegistryObserver::startAppObservation() void SteamRegistryObserver::stopAppObservation() { m_observation_delay.stop(); + m_process_list_observer.stopObserving(); + m_is_observing_apps = false; m_global_app_id = 0; if (m_tracked_app_data) @@ -122,12 +123,13 @@ void SteamRegistryObserver::slotRegistryChanged() { const auto& data{m_watcher.getData()}; const auto get_uint = [](const qint64* ptr) { return ptr == nullptr ? 0 : static_cast(*ptr); }; - const auto get_bool = [](const qint64* ptr) { return ptr == nullptr ? false : *ptr != 0; }; const auto pid{get_uint(getEntry(PID_PATH, data))}; if (pid != m_pid) { m_pid = pid; + + m_process_list_observer.observePid(m_pid); emit signalSteamPID(m_pid); } @@ -146,31 +148,37 @@ void SteamRegistryObserver::slotRegistryChanged() return; } - const auto global_app_id{get_uint(getEntry(GLOBAL_APP_ID_PATH, data))}; - if (global_app_id != m_global_app_id) - { - m_global_app_id = global_app_id; - emit signalGlobalAppId(m_global_app_id); - } + // Note: everything after here is a workaround, because Steam is no longer saving stuff to the registry + // https://github.com/ValveSoftware/steam-for-linux/issues/9672 + const auto running_apps{m_process_list_observer.getAppIds()}; if (m_tracked_app_data) { - const auto* app_data{getEntry( - APPS_PATH + QStringList{QString::number(m_tracked_app_data->m_app_id)}, data)}; + const bool tracked_app_is_running{running_apps.contains(m_tracked_app_data->m_app_id)}; + const uint global_app_id{tracked_app_is_running ? m_tracked_app_data->m_app_id + : running_apps.empty() ? 0 + : *running_apps.begin()}; - const bool is_updating{app_data == nullptr ? false : get_bool(getEntry(APP_UPDATING_PATH, *app_data))}; - if (is_updating != m_tracked_app_data->m_is_updating) + if (global_app_id != m_global_app_id) { - m_tracked_app_data->m_is_updating = is_updating; - emit signalTrackedAppIsRunning(m_tracked_app_data->m_is_updating); + m_global_app_id = global_app_id; + emit signalGlobalAppId(m_global_app_id); } - const bool is_running{app_data == nullptr ? false : get_bool(getEntry(APP_RUNNING_PATH, *app_data))}; - if (is_running != m_tracked_app_data->m_is_running) + if (tracked_app_is_running != m_tracked_app_data->m_is_running) { - m_tracked_app_data->m_is_running = is_running; + m_tracked_app_data->m_is_running = tracked_app_is_running; emit signalTrackedAppIsRunning(m_tracked_app_data->m_is_running); } } + else + { + const uint global_app_id{running_apps.empty() ? 0 : *running_apps.begin()}; + if (global_app_id != m_global_app_id) + { + m_global_app_id = global_app_id; + emit signalGlobalAppId(m_global_app_id); + } + } } } // namespace os diff --git a/src/lib/os/linux/steamregistryobserver.h b/src/lib/os/linux/steamregistryobserver.h index c4de3bb..997bc35 100644 --- a/src/lib/os/linux/steamregistryobserver.h +++ b/src/lib/os/linux/steamregistryobserver.h @@ -4,6 +4,7 @@ #include "../shared/trackedappdata.h" #include "../steamregistryobserverinterface.h" #include "registryfilewatcher.h" +#include "steamprocesslistobserver.h" //--------------------------------------------------------------------------------------------------------------------- @@ -35,5 +36,6 @@ private slots: uint m_pid{0}; uint m_global_app_id{0}; std::optional m_tracked_app_data; + SteamProcessListObserver m_process_list_observer; }; } // namespace os diff --git a/src/lib/os/steamhandler.cpp b/src/lib/os/steamhandler.cpp index c154519..2c423d4 100644 --- a/src/lib/os/steamhandler.cpp +++ b/src/lib/os/steamhandler.cpp @@ -192,6 +192,17 @@ std::optional SteamHandler::getTrackedUpdatingApp() const //--------------------------------------------------------------------------------------------------------------------- +void SteamHandler::clearTrackedApp() +{ + m_registry_observer->stopTrackingApp(); + if (m_tracked_app) + { + m_tracked_app = std::nullopt; + } +} + +//--------------------------------------------------------------------------------------------------------------------- + void SteamHandler::slotSteamProcessDied() { qCDebug(lc::os) << "Steam is no longer running!"; @@ -205,7 +216,7 @@ void SteamHandler::slotSteamProcessDied() #if defined(Q_OS_LINUX) // On linux there is a race condition where the crashed Steam process may leave the reaper process (game) running... // Let's kill it for fun! - const uint forced_termination_ms{10000}; + const uint forced_termination_ms{5000}; m_process_handler->closeDetached(QRegularExpression(".*?Steam.+?reaper", QRegularExpression::CaseInsensitiveOption), forced_termination_ms); #endif @@ -303,16 +314,4 @@ void SteamHandler::slotTerminateSteam() const uint time_to_kill{10000}; m_process_handler->close(time_to_kill); } - -//--------------------------------------------------------------------------------------------------------------------- - -void SteamHandler::clearTrackedApp() -{ - m_registry_observer->stopTrackingApp(); - if (m_tracked_app) - { - m_tracked_app = std::nullopt; - emit signalAppTrackingHasEnded(); - } -} } // namespace os diff --git a/src/lib/os/steamhandler.h b/src/lib/os/steamhandler.h index de409a6..25cf33b 100644 --- a/src/lib/os/steamhandler.h +++ b/src/lib/os/steamhandler.h @@ -30,10 +30,10 @@ class SteamHandler : public QObject uint getRunningApp() const; std::optional getTrackedActiveApp() const; std::optional getTrackedUpdatingApp() const; + void clearTrackedApp(); signals: void signalProcessStateChanged(); - void signalAppTrackingHasEnded(); private slots: void slotSteamProcessDied(); @@ -45,8 +45,6 @@ private slots: void slotTerminateSteam(); private: - void clearTrackedApp(); - std::unique_ptr m_process_handler; std::unique_ptr m_registry_observer;