#include "eproc.hpp" #include "process_table_state.hpp" #include "process_view.hpp" #include "settingspanel.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ EE_PLATFORM == EE_PLATFORM_BSD #include #include #endif namespace eproc { using namespace ProcessTableState; namespace { constexpr size_t kPerformanceHistorySamples = 600; constexpr double kPerformanceVisibleSeconds = 60.0; static_assert( kPerformanceHistorySamples >= kPerformanceVisibleSeconds * 1000 / AppConfig::MinRefreshIntervalMs + 2 ); constexpr int kForceKillSignal = 9; // SIGKILL on Linux; TerminateProcess on Windows. #if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ EE_PLATFORM == EE_PLATFORM_BSD constexpr int kEndProcessSignal = SIGTERM; #else constexpr int kEndProcessSignal = kForceKillSignal; #endif String endProcessLabel( UISceneNode* ui, size_t count ) { return count == 1 ? ui->i18n( "eproc_end_process", "End Process" ) : ui->i18n( "eproc_end_processes", "End Processes" ); } String forceKillLabel( UISceneNode* ui, size_t count ) { return count == 1 ? ui->i18n( "eproc_forcibly_kill_process", "Forcibly Kill Process" ) : ui->i18n( "eproc_forcibly_kill_processes", "Forcibly Kill Processes" ); } String formatUptime( UISceneNode* ui, double uptimeSeconds ) { const Int64 seconds = std::isfinite( uptimeSeconds ) && uptimeSeconds > 0 ? static_cast( uptimeSeconds ) : 0; const Int64 days = seconds / 86400; const Int64 hours = ( seconds / 3600 ) % 24; const Int64 minutes = ( seconds / 60 ) % 60; String result; auto append = [&]( Int64 value, const char* singularKey, const char* singular, const char* pluralKey, const char* plural ) { if ( !result.empty() ) result += ", "; result += String::format( ui->i18n( value == 1 ? singularKey : pluralKey, value == 1 ? singular : plural ) .toUtf8(), static_cast( value ) ); }; if ( days > 0 ) append( days, "eproc_uptime_day", "%lld day", "eproc_uptime_days", "%lld days" ); if ( hours > 0 ) append( hours, "eproc_uptime_hour", "%lld hour", "eproc_uptime_hours", "%lld hours" ); if ( minutes > 0 ) append( minutes, "eproc_uptime_minute", "%lld minute", "eproc_uptime_minutes", "%lld minutes" ); if ( result.empty() ) append( seconds, "eproc_uptime_second", "%lld second", "eproc_uptime_seconds", "%lld seconds" ); return result; } String processColumnTooltip( UISceneNode* ui, size_t column ) { switch ( column ) { case ProcessModel::ColIcon: return ui->i18n( "eproc_tip_icon", "Application icon, when available." ); case ProcessModel::ColName: return ui->i18n( "eproc_tip_name", "Process name reported by the operating system." ); case ProcessModel::ColPid: return ui->i18n( "eproc_tip_pid", "Process identifier (PID)." ); case ProcessModel::ColUsername: return ui->i18n( "eproc_tip_username", "User account that owns the process." ); case ProcessModel::ColCpu: return ui->i18n( "eproc_tip_cpu", "CPU usage during the latest sampling interval." ); case ProcessModel::ColMemory: #if EE_PLATFORM == EE_PLATFORM_LINUX return ui->i18n( "eproc_tip_memory_linux", "Anonymous resident memory (RssAnon); falls back to RSS when unavailable." ); #elif EE_PLATFORM == EE_PLATFORM_WIN return ui->i18n( "eproc_tip_memory_windows", "Private committed memory; falls back to resident memory when unavailable." ); #else return ui->i18n( "eproc_tip_memory", "Resident memory of this process." ); #endif case ProcessModel::ColSharedMem: return ui->i18n( "eproc_tip_shared_memory", "File-backed and shared resident memory (RssFile + RssShmem)." ); case ProcessModel::ColGpuUsage: return ui->i18n( "eproc_tip_gpu_usage", "GPU engine usage reported for this process." ); case ProcessModel::ColGpuMemory: return ui->i18n( "eproc_tip_gpu_memory", "GPU memory reported for this process." ); case ProcessModel::ColDownload: return ui->i18n( "eproc_tip_download", "Estimated network receive rate for this process." ); case ProcessModel::ColUpload: return ui->i18n( "eproc_tip_upload", "Estimated network send rate for this process." ); case ProcessModel::ColTotalMemory: return ui->i18n( "eproc_tip_resident_memory", "Resident memory of this process (RSS or working set). Shared pages " "can appear in multiple processes." ); case ProcessModel::ColVirtualSize: return ui->i18n( "eproc_tip_virtual_size", "Virtual address space reserved or mapped by this process." ); case ProcessModel::ColCpuTime: return ui->i18n( "eproc_tip_cpu_time", "Total CPU time consumed by this process." ); case ProcessModel::ColNiceness: return ui->i18n( "eproc_tip_niceness", "Scheduling nice value of this process." ); case ProcessModel::ColRelativeStartTime: return ui->i18n( "eproc_tip_start_time", "Time elapsed since this process started." ); case ProcessModel::ColTty: return ui->i18n( "eproc_tip_tty", "Controlling terminal of this process." ); case ProcessModel::ColIoRead: return ui->i18n( "eproc_tip_io_read", "Bytes read by this process since it started." ); case ProcessModel::ColIoWrite: return ui->i18n( "eproc_tip_io_write", "Bytes written by this process since it started." ); case ProcessModel::ColCommand: return ui->i18n( "eproc_tip_command", "Executable path and arguments, when available." ); case ProcessModel::ColThreads: return ui->i18n( "eproc_tip_threads", "Number of threads in this process." ); case ProcessModel::ColMemoryPercent: return ui->i18n( "eproc_tip_memory_percent", "Memory column as a percentage of total physical memory." ); case ProcessModel::ColFamilyMemory: #if EE_PLATFORM == EE_PLATFORM_LINUX return ui->i18n( "eproc_tip_family_memory_linux", "Sum of proportional resident memory (PSS) for this process and all live " "descendants. Shared pages are apportioned; blank if any member is unavailable." ); #else return ui->i18n( "eproc_tip_family_memory", "Sum of the Memory column for this process and all live descendants. " "Approximate application memory." ); #endif case ProcessModel::ColFamilyMemoryPercent: return ui->i18n( "eproc_tip_family_memory_percent", "Family Memory as a percentage of total physical memory." ); default: return {}; } } void hideUnsupportedProcessColumns( UIAbstractTableView& view ) { if ( !kUnavailableProcessColumns.empty() ) view.setColumnsHidden( std::vector( kUnavailableProcessColumns.begin(), kUnavailableProcessColumns.end() ), true ); } const char* sortOrderName( SortOrder order ) { switch ( order ) { case SortOrder::Ascending: return "ascending"; case SortOrder::Descending: return "descending"; default: return "none"; } } bool parseSortOrder( const std::string& value, SortOrder& order ) { if ( value == "ascending" ) { order = SortOrder::Ascending; return true; } if ( value == "descending" ) { order = SortOrder::Descending; return true; } return false; } const char* usernameClass( const ProcessInfo& process ) { if ( process.status == ProcessStatus::Ended ) return "eproc-process-username-ended"; if ( process.tracerPid > 0 ) return "eproc-process-username-traced"; #if EE_PLATFORM == EE_PLATFORM_LINUX if ( process.uid == static_cast( getuid() ) ) #else if ( process.ownedByCurrentUser ) #endif return "eproc-process-username-own"; if ( process.uid < 100 || !process.canLogin ) return "eproc-process-username-system"; return "eproc-process-username-other"; } void setCellClassEnabled( UITableCell& cell, std::string_view className, bool enabled ) { const bool hasClass = cell.hasClass( className ); if ( enabled == hasClass ) return; if ( enabled ) cell.addClass( std::string{ className } ); else cell.removeClass( std::string{ className } ); } constexpr std::array kUsernameClasses = { "eproc-process-username-ended", "eproc-process-username-traced", "eproc-process-username-own", "eproc-process-username-system", "eproc-process-username-other", }; constexpr std::array kCpuFillClasses = { "eproc-cpu-fill-0", "eproc-cpu-fill-10", "eproc-cpu-fill-20", "eproc-cpu-fill-30", "eproc-cpu-fill-40", "eproc-cpu-fill-50", "eproc-cpu-fill-60", "eproc-cpu-fill-70", "eproc-cpu-fill-80", "eproc-cpu-fill-90", "eproc-cpu-fill-100", }; std::string formatPerformanceRate( Int64 bytes ) { return bytes > 0 ? formatBytesPerSecond( bytes ) : "0 B/s"; } Color themeColor( UISceneNode* ui, const char* name, Color fallback ) { const auto value = ui->getRoot()->getUIStyle()->getVariable( name ); return value.isEmpty() ? fallback : Color::fromString( value.getValue() ); } } // namespace App::App() { mConfig = std::make_unique( Sys::getConfigPath( "eproc" ) ); mConfig->load(); if ( mConfig->pixelDensity > 0 ) mPixelDensity = mConfig->pixelDensity; mFontHinting = mConfig->fontHinting; mFontAntialiasing = mConfig->fontAntialiasing; mColorScheme = mConfig->colorScheme; mUpdateIntervalMs = mConfig->refreshIntervalMs; } App::~App() { // The worker may still publish a snapshot through mStagingMutex. Join it before member // destruction reaches the staging state and mutex. mThreadPool.reset(); } int App::run( int argc, char* argv[] ) { args::ArgumentParser parser( "eproc" ); args::HelpFlag help( parser, "help", "Display this help menu", { 'h', "help" } ); args::ValueFlag pixelDensity( parser, "pixel-density", "Set default application pixel density", { 'd', "pixel-density" } ); const std::unordered_map fontHintingMap{ { "none", FontHinting::None }, { "slight", FontHinting::Slight }, { "full", FontHinting::Full } }; args::MapFlag fontHinting( parser, "font-hinting", "Font hinting mode (accepted values: none, slight, full)", { "font-hinting" }, fontHintingMap, mFontHinting ); const std::unordered_map fontAntialiasingMap{ { "none", FontAntialiasing::None }, { "grayscale", FontAntialiasing::Grayscale }, { "subpixel", FontAntialiasing::Subpixel } }; args::MapFlag fontAntialiasing( parser, "font-antialiasing", "Font antialiasing mode (accepted values: none, grayscale, subpixel)", { "font-antialiasing" }, fontAntialiasingMap, mFontAntialiasing ); args::ValueFlag prefersColorScheme( parser, "prefers-color-scheme", "Set the preferred color scheme (\"light\", \"dark\" or \"system\")", { 'c', "prefers-color-scheme" } ); try { parser.ParseCLI( argc, argv ); } catch ( const args::Help& ) { std::cout << parser; return EXIT_SUCCESS; } catch ( const args::ParseError& error ) { std::cerr << error.what() << '\n' << parser; return EXIT_FAILURE; } catch ( const args::ValidationError& error ) { std::cerr << error.what() << '\n' << parser; return EXIT_FAILURE; } if ( pixelDensity ) { if ( !std::isfinite( pixelDensity.Get() ) || pixelDensity.Get() <= 0 ) { std::cerr << "Pixel density must be a positive finite number\n"; return EXIT_FAILURE; } mPixelDensity = pixelDensity.Get(); } mFontHinting = fontHinting.Get(); mFontAntialiasing = fontAntialiasing.Get(); if ( prefersColorScheme ) { const std::string& scheme = prefersColorScheme.Get(); if ( scheme != "light" && scheme != "dark" && scheme != "system" ) { std::cerr << "Color scheme must be light, dark, or system\n"; return EXIT_FAILURE; } mColorScheme = ColorSchemePreferences::fromStringExt( scheme ); } if ( !init() ) return EXIT_FAILURE; const int result = mApp->run(); if ( !mWindowStateSaved ) saveWindowState(); return result; } bool App::init() { // Start collection before creating the window so the first frame can use the staged snapshot. startCollection(); const Sizei storedSize = mConfig->windowState.size; const Uint32 windowWidth = storedSize.getWidth() > 0 ? static_cast( storedSize.getWidth() ) : 1280; const Uint32 windowHeight = storedSize.getHeight() > 0 ? static_cast( storedSize.getHeight() ) : 720; #if EE_PLATFORM == EE_PLATFORM_MACOS const char* windowIcon = "assets/icon/eproc-macos.png"; #else const char* windowIcon = "assets/icon/eproc.png"; #endif WindowSettings ws( windowWidth, windowHeight, "", WindowStyle::Default, WindowBackend::Default, 32, Sys::getProcessPath() + windowIcon ); ContextSettings ctx; ctx.Multisamples = mConfig->multisamples; ctx.VSync = mConfig->vsync; ctx.FrameRateLimit = mConfig->frameRateLimit; ctx.Version = mConfig->rendererVersion; UIApplication::Settings settings; settings.pixelDensity = mPixelDensity; settings.fontHinting = mFontHinting; settings.fontAntialiasing = mFontAntialiasing; mApp = std::make_unique( ws, settings, ctx ); if ( mApp->getUI() && mApp->getWindow() ) { mApp->getWindow()->setTitle( mApp->getUI()->i18n( "eproc_window_title", "eproc - System Monitor" ) ); } auto* ui = mApp->getUI(); if ( !ui ) return false; ui->setColorSchemePreference( mColorScheme ); ui->updateWindowTitleBarColor(); restoreWindowState(); if ( mApp->getWindow() ) { mApp->getWindow()->setCloseRequestCallback( [this]( EE::Window::Window* window ) { return closeWindow( window ); } ); mApp->getWindow()->setQuitCallback( [this]( EE::Window::Window* window ) { if ( window->isOpen() && closeWindow( window ) ) window->close(); } ); } UIWidgetCreator::registerWidget( "eproc-table-view", ProcessTableView::New ); UIWidgetCreator::registerWidget( "eproc-tree-view", ProcessTreeView::New ); mRoot = ui->loadLayoutFromString( R"xml( )xml" ); if ( !mRoot ) { return false; } setupUI(); setupProcessTable(); setupPerformance(); if ( !mCollector ) { UIMessageBox* platformMessage = UIMessageBox::New( UIMessageBox::OK, ui->i18n( "eproc_platform_wip_message", "eproc is currently a work in progress. This operating system is not implemented " "yet." ) ); platformMessage->setTitle( ui->i18n( "eproc_platform_wip_title", "Work in Progress" ) ); platformMessage->center(); platformMessage->showWhenReady(); } // Publish whatever finished during UI initialization. Neither process collection nor PSS // accounting may delay the first frame; later results arrive through the refresh tick. publishStagedSnapshot(); setupRefreshTimer(); return true; } void App::restoreWindowState() { if ( !mConfig || !mApp || !mApp->getWindow() ) return; EE::Window::Window* window = mApp->getWindow(); DisplayManager* displayManager = Engine::instance()->getDisplayManager(); const auto& state = mConfig->windowState; if ( state.position != Vector2i( -1, -1 ) && displayManager && state.displayIndex >= 0 && state.displayIndex < displayManager->getDisplayCount() ) { window->setPosition( state.position.x + ( state.maximized ? -1 : 0 ), state.position.y ); } #if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN if ( state.maximized ) { #if EE_PLATFORM == EE_PLATFORM_LINUX mApp->getUI()->runOnMainThread( [window] { window->maximize(); } ); #elif EE_PLATFORM != EE_PLATFORM_MACOS window->maximize(); #endif } #endif } void App::saveWindowState() { if ( !mConfig || !mApp ) return; if ( mTableView && mSortProxy ) mConfig->processTableState = serializeProcessTableState(); if ( mProcessModel ) { mConfig->filterMode = mProcessModel->getFilter(); mConfig->treeView = mConfig->filterMode == ProcessModel::AllProcessesInTreeForm; } mConfig->captureWindowState( mApp->getWindow() ); if ( !mConfig->save() ) Log::error( "Could not save eproc window state to %s", mConfig->getConfigPath() ); else mWindowStateSaved = true; } void App::savePreferences() { if ( !mConfig->save() ) Log::error( "Could not save eproc settings to %s", mConfig->getConfigPath() ); } void App::showSettings() { if ( mSettingsWindow ) { mSettingsWindow->show(); mSettingsWindow->toFront(); auto* panel = mSettingsWindow->find( "settings_panel_content" ); panel->runOnMainThread( [panel] { panel->focusSearch(); } ); return; } mSettingsWindow = SettingsPanel::create( *this ); mSettingsWindow->on( Event::OnWindowClose, [this]( const Event* ) { mSettingsWindow = nullptr; } ); } static nlohmann::json serializeProcessColumns( const UIAbstractTableView& view, size_t columnCount ) { nlohmann::json state; state["widths"] = view.serializeColumnWidths(); state["column_order"] = view.getColumnOrder(); state["hidden_columns"] = nlohmann::json::array(); for ( size_t column = 0; column < columnCount; ++column ) { if ( view.isColumnHidden( column ) ) state["hidden_columns"].push_back( column ); } return state; } static void restoreProcessColumns( UIAbstractTableView& view, const nlohmann::json& state, size_t columnCount ) { if ( state.contains( "hidden_columns" ) && state["hidden_columns"].is_array() ) { std::vector hidden( columnCount, false ); for ( const auto& column : state["hidden_columns"] ) { if ( !column.is_number_integer() ) continue; const Int64 index = column.get(); if ( index >= 0 && static_cast( index ) < columnCount ) hidden[static_cast( index )] = true; } std::vector visible; visible.reserve( columnCount ); for ( size_t column = 0; column < columnCount; ++column ) { if ( !hidden[column] && isProcessColumnSupported( column ) ) visible.push_back( column ); } // Always leave one column visible, even if a malformed state hides everything. if ( !visible.empty() ) view.setColumnsVisible( visible ); } hideUnsupportedProcessColumns( view ); if ( state.contains( "widths" ) && isValidWidths( state["widths"], columnCount, [&view]( size_t column ) { return view.isColumnHidden( column ); } ) ) { // Pixel restoration sets each width separately, so disable automatic sizing first. const bool pixelWidths = state["widths"]["mode"] == "pixels"; if ( pixelWidths ) view.setAutoColumnsWidth( false ); if ( !view.unserializeColumnWidths( state["widths"] ) && pixelWidths ) view.setAutoColumnsWidth( true ); } if ( state.contains( "column_order" ) && state["column_order"].is_array() && state["column_order"].size() == columnCount ) { std::vector order; order.reserve( columnCount ); for ( const auto& column : state["column_order"] ) { if ( !column.is_number_integer() || column.get() < 0 ) break; order.push_back( static_cast( column.get() ) ); } if ( order.size() == columnCount ) view.setColumnOrder( std::move( order ) ); } } std::string App::serializeProcessTableState() const { if ( !mTableView || !mSortProxy ) return {}; nlohmann::json state = serializeProcessColumns( *mTableView, mSortProxy->columnCount() ); state["version"] = kProcessTableStateVersion; state["sort"]["column"] = mSortProxy->keyColumn(); state["sort"]["order"] = sortOrderName( mSortProxy->sortOrder() ); if ( mTreeView ) { state["tree"] = serializeProcessColumns( *mTreeView, mTreeModel->columnCount() ); state["tree"]["sort"]["column"] = mTreeModel->keyColumn(); state["tree"]["sort"]["order"] = sortOrderName( mTreeModel->sortOrder() ); } return state.dump(); } static void restoreProcessSort( UIAbstractTableView& view, const nlohmann::json& state, size_t columnCount ) { if ( !state.contains( "sort" ) || !state["sort"].is_object() ) return; const auto& sort = state["sort"]; const int column = sort.contains( "column" ) && sort["column"].is_number_integer() ? sort["column"].get() : -1; const std::string orderName = sort.contains( "order" ) && sort["order"].is_string() ? sort["order"].get() : "none"; SortOrder order = SortOrder::None; Model* model = view.getModel(); if ( model && column >= 0 && static_cast( column ) < columnCount && isProcessColumnSupported( static_cast( column ) ) && parseSortOrder( orderName, order ) && order != SortOrder::None && model->isColumnSortable( column ) ) view.sortByColumn( static_cast( column ), order ); } void App::restoreProcessTableState() { if ( !mConfig || mConfig->processTableState.empty() || !mTableView || !mSortProxy ) return; nlohmann::json state = parse( mConfig->processTableState ); if ( state.is_null() ) return; const size_t columnCount = mSortProxy->columnCount(); if ( !state.contains( "widths" ) || !isValidWidths( state["widths"], columnCount, [this]( size_t column ) { return mTableView->isColumnHidden( column ); } ) ) return; restoreProcessColumns( *mTableView, state, columnCount ); restoreProcessSort( *mTableView, state, columnCount ); if ( mTreeView && state.contains( "tree" ) && state["tree"].is_object() ) { restoreProcessColumns( *mTreeView, state["tree"], columnCount ); restoreProcessSort( *mTreeView, state["tree"], columnCount ); } } bool App::closeWindow( EE::Window::Window* ) { saveWindowState(); return true; } void App::setupUI() { mTabWidget = mRoot->find( "tab_widget" ); auto* settingsBtn = mRoot->find( "settings" ); mEndProcessBtn = mRoot->find( "end_process_btn" ); mSearchInput = mRoot->find( "search_input" ); mFilterDropdown = mRoot->find( "filter_dropdown" ); mTableView = mRoot->find( "process_table" ); mTreeView = mRoot->find( "process_tree" ); static_cast( mTableView )->setSearchInput( mSearchInput ); static_cast( mTreeView )->setSearchInput( mSearchInput ); mStatusText = mRoot->find( "process_count" ); mCpuText = mRoot->find( "cpu_text" ); mMemText = mRoot->find( "mem_text" ); mSwapText = mRoot->find( "swap_text" ); auto* ui = mApp->getUI(); ui->setKeyBindingCommand( "next-tab", [this] { mTabWidget->focusNextTab(); } ); ui->setKeyBindingCommand( "previous-tab", [this] { mTabWidget->focusPreviousTab(); } ); ui->addKeyBindingString( "mod+tab", "next-tab" ); ui->addKeyBindingString( "mod+shift+tab", "previous-tab" ); mRoot->find( "tab_process_table" ) ->setText( ui->i18n( "eproc_process_table_tab", "Process Table" ) ); mRoot->find( "tab_performance" ) ->setText( ui->i18n( "eproc_performance_tab", "Performance" ) ); mEndProcessBtn->setText( endProcessLabel( ui, 0 ) ); mSearchInput->setHint( ui->i18n( "eproc_quick_search_hint", "Quick search" ) ); mStatusText->setText( ui->i18n( "eproc_process_count", "0 processes" ) ); mCpuText->setText( ui->i18n( "eproc_cpu_status", "CPU: 0%" ) ); mMemText->setText( ui->i18n( "eproc_memory_status", "Memory: 0 / 0" ) ); mSwapText->setText( ui->i18n( "eproc_swap_status", "Swap: 0 / 0" ) ); // Tabs are declared in the XML layout via Tab elements with owns= attributes. for ( size_t index = 0; index < mTabFocus.size(); ++index ) { auto* content = mTabWidget->getTab( index )->getOwnedWidget(); mTabFocus[index].focusConnection = content->connect( Event::OnFocusWithin, [this, index]( const Event* event ) { auto* focused = mApp->getUI()->getEventDispatcher()->getFocusNode(); // Tab selection briefly focuses the container; preserve its last focused child. if ( mTabFocusRestorePending || !focused || focused == event->getNode() || !focused->isWidget() || !event->getNode()->isParentOf( focused ) ) return; auto& state = mTabFocus[index]; if ( state.node == focused && state.closeConnection ) return; state.closeConnection = focused->connect( Event::OnClose, [this, index]( const Event* ) { mTabFocus[index].node = nullptr; } ); state.node = focused; } ); } mTabSelectedConnection = mTabWidget->connect( Event::OnTabSelected, [this]( const Event* ) { // Mouse-down and Shift+Tab processing can move focus again after selection finishes. if ( mTabFocusRestorePending ) return; mTabFocusRestorePending = true; mTabWidget->runOnMainThread( [this] { restoreTabFocus(); } ); } ); // Filter entries mirror the original's ProcessFilter::State order. static const std::pair filters[] = { { "eproc_filter_all_processes", "All Processes" }, { "eproc_filter_all_processes_tree", "All Processes, Tree" }, { "eproc_filter_system_processes", "System Processes" }, { "eproc_filter_user_processes", "User Processes" }, { "eproc_filter_own_processes", "Own Processes" }, { "eproc_filter_programs_only", "Programs Only" }, }; auto* filterListBox = mFilterDropdown->getListBox(); for ( size_t i = 0; i < std::size( filters ); ++i ) { if ( i == ProcessModel::ProgramsOnly && ( !mCollector || !mCollector->supportsProgramsOnly() ) ) continue; filterListBox->addListBoxItem( ui->i18n( filters[i].first, filters[i].second ) ); } filterListBox->setSelected( 0 ); // Connect events settingsBtn->on( Event::MouseClick, [this]( const Event* ) { showSettings(); } ); if ( mEndProcessBtn ) mEndProcessBtn->onClick( [this]( const MouseEvent* ) { onEndProcess(); } ); if ( mSearchInput ) { mSearchInput->on( Event::OnTextChanged, [this]( const Event* ) { onSearchChanged(); } ); mSearchInput->on( Event::KeyDown, [this]( const Event* event ) { const KeyEvent* key = event->asKeyEvent(); if ( key->getKeyCode() != KEY_DOWN || key->getSanitizedMod() != KEYMOD_NONE ) return; focusFirstProcessRow( activeProcessView() ); } ); } if ( mFilterDropdown ) mFilterDropdown->on( Event::OnItemSelected, [this]( const Event* ) { onFilterChanged(); } ); mApp->getUI()->on( Event::KeyUp, [this]( const Event* event ) { if ( event->asKeyEvent()->getKeyCode() == KEY_F11 ) { UIWidgetInspector::create( mApp->getUI() ); } } ); mSearchInput->setFocus(); } void App::restoreTabFocus() { mTabFocusRestorePending = false; const auto index = mTabWidget->getTabSelectedIndex(); if ( index >= mTabFocus.size() ) return; auto* tab = mTabWidget->getTabSelected(); auto* content = tab->getOwnedWidget(); const auto& state = mTabFocus[index]; // Connection expiry also protects against widgets deleted without a close notification. auto* focused = state.closeConnection ? state.node : nullptr; if ( focused && content->isParentOf( focused ) ) { for ( auto* ancestor = focused; ancestor; ancestor = ancestor->getParent() ) { if ( !ancestor->isVisible() || !ancestor->isEnabled() || ancestor->isClosing() ) { focused = nullptr; break; } if ( ancestor == content ) break; } } else { focused = nullptr; } if ( !focused ) focused = tab->getId() == "tab_process_table" ? mSearchInput : content; focused->setFocus(); } void App::setupPerformance() { constexpr std::array ids = { "cpu", "memory", "gpu", "network" }; constexpr std::array names = { "CPU", "Memory", "GPU", "Network" }; const Color primary = themeColor( mApp->getUI(), "--primary", Color( 49, 178, 224 ) ); const Color gridColor = Color( themeColor( mApp->getUI(), "--separator", Color( 100, 113, 124 ) ), 45 ); const std::array colors = { primary, themeColor( mApp->getUI(), "--theme-warning", Color( 190, 151, 226 ) ), themeColor( mApp->getUI(), "--theme-success", Color( 119, 203, 135 ) ), themeColor( mApp->getUI(), "--font-highlight", Color( 91, 196, 223 ) ), }; mPerformanceDetailChart = mRoot->find( "perf_detail_chart" ); mPerformanceTitle = mRoot->find( "perf_title" ); mPerformanceSummary = mRoot->find( "perf_summary" ); mPerformanceDetail = mRoot->find( "perf_detail" ); mCoreChartScroll = mRoot->find( "perf_cores_scroll" ); mCoreChartStack = mRoot->find( "perf_cores_stack" ); mCpuModeControls = mRoot->find( "perf_cpu_mode" ); mOverallButton = mRoot->find( "perf_overall" ); mPerCoreButton = mRoot->find( "perf_per_core" ); mOverallButton->onClick( [this]( const MouseEvent* ) { setPerCoreView( false ); } ); mPerCoreButton->onClick( [this]( const MouseEvent* ) { setPerCoreView( true ); } ); ChartStyle detailStyle = mPerformanceDetailChart->chartStyle(); detailStyle.verticalGrid.mode = ChartGridMode::ScreenInterval; detailStyle.verticalGrid.spacing = 64.0; detailStyle.horizontalGrid.mode = ChartGridMode::AxisTicks; detailStyle.verticalGrid.color = gridColor; detailStyle.horizontalGrid.color = gridColor; detailStyle.leftMargin = 68.f; detailStyle.bottomMargin = 28.f; detailStyle.minimumAxisMargin = 46.f; mPerformanceDetailChart->setChartStyle( detailStyle ); for ( size_t i = 0; i < ids.size(); ++i ) { auto& metric = mPerformanceMetrics[i]; const std::string prefix = std::string( "perf_" ) + ids[i]; metric.card = mRoot->find( prefix + "_card" ); metric.preview = mRoot->find( prefix + "_preview" ); metric.value = mRoot->find( prefix + "_value" ); metric.primary = std::make_shared( kPerformanceHistorySamples ); auto& line = metric.preview->addLineSeries( names[i] ); line.setDataSource( metric.primary ); line.setColor( colors[i] ); line.setFilled( true ); metric.preview->xAxis().setFormatter( [this]( double value, double ) { return formatPerformanceTime( value ); } ); if ( i == 3 ) { metric.preview->yAxis().setFormatter( []( double value, double ) { if ( !std::isfinite( value ) || value <= 0 ) return String( "0 B/s" ); return String( formatPerformanceRate( static_cast( std::min( value, static_cast( std::numeric_limits::max() ) ) ) ) ); } ); } else { metric.preview->yAxis().setFormatter( []( double value, double ) { return String::format( "%.0f%%", value ); } ); } if ( i == 3 ) { metric.secondary = std::make_shared( kPerformanceHistorySamples ); auto& upload = metric.preview->addLineSeries( "Upload" ); upload.setDataSource( metric.secondary ); upload.setColor( themeColor( mApp->getUI(), "--theme-error", Color( 236, 115, 103 ) ) ); upload.setFilled( true ); } ChartStyle smallStyle = metric.preview->chartStyle(); smallStyle.leftMargin = smallStyle.rightMargin = smallStyle.topMargin = smallStyle.bottomMargin = smallStyle.minimumAxisMargin = 2.f; smallStyle.tickLength = 0.f; smallStyle.axisColor = Color( 0, 0, 0, 0 ); smallStyle.tickColor = Color( 0, 0, 0, 0 ); smallStyle.tickLabelColor = Color( 0, 0, 0, 0 ); metric.preview->setChartStyle( smallStyle ); if ( i != 3 ) metric.preview->setAxisRange( metric.preview->yAxis(), { 0.0, 100.0 } ); metric.preview->setAxisRange( metric.preview->xAxis(), { -kPerformanceVisibleSeconds, 0.0 } ); // The chart and text are display-only. Let the card receive clicks anywhere inside it. for ( Node* child = metric.card->getFirstChild(); child; child = child->getNextNode() ) child->writeNodeFlag( NODE_FLAG_OVER_FIND_ALLOWED, 0 ); metric.card->onClick( [this, i]( const MouseEvent* ) { selectPerformanceMetric( i ); } ); metric.value->setText( i < 2 ? "0%" : "Unavailable" ); } selectPerformanceMetric( 0 ); } String App::formatPerformanceTime( double value ) const { const double elapsed = mPerformanceClock.getElapsedTime().asSeconds() - value; if ( elapsed < 2.0 ) return "Now"; const int seconds = static_cast( std::round( std::max( 0.0, elapsed ) ) ); return String::format( "-%d:%02d", seconds / 60, seconds % 60 ); } void App::selectPerformanceMetric( size_t metric ) { if ( metric >= mPerformanceMetrics.size() ) return; mSelectedPerformanceMetric = metric; for ( size_t i = 0; i < mPerformanceMetrics.size(); ++i ) { auto* card = mPerformanceMetrics[i].card; if ( i == metric ) card->addClass( "eproc-performance-card-selected" ); else card->removeClass( "eproc-performance-card-selected" ); } static const char* names[] = { "CPU", "Memory", "GPU", "Network" }; mPerformanceTitle->setText( names[metric] ); mPerformanceSummary->setText( mPerformanceSummaries[metric] ); mPerformanceDetail->setText( mPerformanceDetails[metric] ); mCpuModeControls->setVisible( metric == 0 && !mCoreHistory.empty() ); mPerformanceDetailChart->setModel( mPerformanceMetrics[metric].preview->sharedModel() ); if ( metric != 3 ) mPerformanceDetailChart->setAxisRange( mPerformanceDetailChart->yAxis(), { 0.0, 100.0 } ); const double now = mPerformanceClock.getElapsedTime().asSeconds(); mPerformanceDetailChart->setAxisRange( mPerformanceDetailChart->xAxis(), { now - kPerformanceVisibleSeconds, now } ); mPerformanceDetailChart->setVisible( metric != 0 || !mPerCoreView ); mCoreChartScroll->setVisible( metric == 0 && mPerCoreView ); } void App::setPerCoreView( bool perCore ) { mPerCoreView = perCore && !mCoreHistory.empty(); mConfig->performancePerCore = mPerCoreView; mOverallButton->setSelected( !mPerCoreView ); mPerCoreButton->setSelected( mPerCoreView ); if ( mPerCoreView && mCoreCharts.size() != mCoreHistory.size() ) rebuildCoreCharts( mCoreHistory.size() ); mPerformanceDetailChart->setVisible( !mPerCoreView ); mCoreChartScroll->setVisible( mPerCoreView ); } void App::rebuildCoreCharts( size_t count ) { mCoreChartStack->closeAllChildren(); mCoreCharts.clear(); mCoreLabels.clear(); for ( size_t core = 0; core < count; ++core ) { auto* panel = mApp->getUI()->loadLayoutFromString( "" "", mCoreChartStack ); auto* label = panel->find( "core_title" ); label->setText( String::format( "CPU %zu", core ) ); label->writeNodeFlag( NODE_FLAG_OVER_FIND_ALLOWED, 0 ); mCoreLabels.push_back( label ); auto* chart = panel->find( "core_chart" ); chart->addClass( "eproc-performance-chart" ); auto& line = chart->addLineSeries( String::format( "CPU %zu", core ) ); line.setDataSource( mCoreHistory[core] ); line.setColor( themeColor( mApp->getUI(), "--primary", Color( 49, 178, 224 ) ) ); line.setFilled( true ); chart->xAxis().setFormatter( []( double, double ) { return String(); } ); chart->yAxis().setFormatter( []( double, double ) { return String(); } ); ChartStyle style = chart->chartStyle(); style.leftMargin = 0.f; style.rightMargin = 0.f; style.topMargin = 0.f; style.bottomMargin = 0.f; style.minimumAxisMargin = 0.f; style.tickLength = 0.f; style.axisColor = Color( 0, 0, 0, 0 ); style.tickColor = Color( 0, 0, 0, 0 ); style.tickLabelColor = Color( 0, 0, 0, 0 ); style.verticalGrid.mode = ChartGridMode::ScreenInterval; style.verticalGrid.spacing = 24.0; style.verticalGrid.color = Color( themeColor( mApp->getUI(), "--separator", Color( 100, 113, 124 ) ), 35 ); style.horizontalGrid.mode = ChartGridMode::DataInterval; style.horizontalGrid.spacing = 25.0; style.horizontalGrid.color = Color( themeColor( mApp->getUI(), "--separator", Color( 100, 113, 124 ) ), 35 ); chart->setChartStyle( style ); chart->setAxisRange( chart->yAxis(), { 0.0, 100.0 } ); const double now = mPerformanceClock.getElapsedTime().asSeconds(); chart->setAxisRange( chart->xAxis(), { now - kPerformanceVisibleSeconds, now } ); mCoreCharts.push_back( chart ); } } void App::updatePerformance( const SystemInfo& sysInfo, const std::vector& processes ) { const double now = mPerformanceClock.getElapsedTime().asSeconds(); const double memoryPercent = sysInfo.totalMemory > 0 ? 100.0 * sysInfo.getUsedMemoryKB() / sysInfo.totalMemory : 0.0; Int64 download = 0, upload = 0, gpuMemory = 0; int gpuUsage = 0; bool hasNetwork = false, hasGpu = false; for ( const auto& process : processes ) { if ( process.netDownload >= 0 ) { download += process.netDownload; hasNetwork = true; } if ( process.netUpload >= 0 ) { upload += process.netUpload; hasNetwork = true; } if ( process.gpuUsage >= 0 ) { gpuUsage += process.gpuUsage; hasGpu = true; } if ( process.gpuMemory >= 0 ) { gpuMemory += process.gpuMemory; hasGpu = true; } } mPerformanceMetrics[0].primary->append( { now, sysInfo.cpuUsage } ); mPerformanceMetrics[1].primary->append( { now, memoryPercent } ); if ( hasGpu ) mPerformanceMetrics[2].primary->append( { now, static_cast( std::min( gpuUsage, 100 ) ) } ); if ( hasNetwork ) { mPerformanceMetrics[3].primary->append( { now, static_cast( download ) } ); mPerformanceMetrics[3].secondary->append( { now, static_cast( upload ) } ); } mPerformanceMetrics[0].value->setText( String::format( "%.1f%%", sysInfo.cpuUsage ) ); mPerformanceMetrics[1].value->setText( String::format( "%.1f%%", memoryPercent ) ); mPerformanceMetrics[2].value->setText( hasGpu ? String::format( "%d%% tracked", std::min( gpuUsage, 100 ) ) : "Unavailable" ); mPerformanceMetrics[3].value->setText( hasNetwork ? String::format( "%s down", formatPerformanceRate( download ).c_str() ) : "Unavailable" ); if ( mCoreHistory.size() < sysInfo.coreCpuUsage.size() ) { mCoreHistory.reserve( sysInfo.coreCpuUsage.size() ); while ( mCoreHistory.size() < sysInfo.coreCpuUsage.size() ) mCoreHistory.push_back( std::make_shared( kPerformanceHistorySamples ) ); } for ( size_t i = 0; i < sysInfo.coreCpuUsage.size(); ++i ) mCoreHistory[i]->append( { now, sysInfo.coreCpuUsage[i] } ); if ( mConfig->performancePerCore && !mPerCoreView && !mCoreHistory.empty() ) setPerCoreView( true ); for ( auto& metric : mPerformanceMetrics ) metric.preview->setAxisRange( metric.preview->xAxis(), { now - kPerformanceVisibleSeconds, now } ); mPerformanceDetailChart->setAxisRange( mPerformanceDetailChart->xAxis(), { now - kPerformanceVisibleSeconds, now } ); for ( auto* chart : mCoreCharts ) chart->setAxisRange( chart->xAxis(), { now - kPerformanceVisibleSeconds, now } ); mCpuModeControls->setVisible( mSelectedPerformanceMetric == 0 && !mCoreHistory.empty() ); if ( mPerCoreView && mCoreCharts.size() != mCoreHistory.size() ) rebuildCoreCharts( mCoreHistory.size() ); for ( size_t i = 0; i < mCoreLabels.size() && i < sysInfo.coreCpuUsage.size(); ++i ) mCoreLabels[i]->setText( String::format( "CPU %zu %.1f%%", i, sysInfo.coreCpuUsage[i] ) ); mPerformanceSummaries[0] = String::format( "%.1f%% utilization", sysInfo.cpuUsage ); mPerformanceDetails[0] = String::format( mApp->getUI() ->i18n( "eproc_performance_cpu_details", "%d logical processors %zu processes Uptime: %s" ) .toUtf8(), sysInfo.cpuCount, processes.size(), formatUptime( mApp->getUI(), sysInfo.uptimeSeconds ).toUtf8().c_str() ); mPerformanceSummaries[1] = String::format( "%.1f%% physical memory", memoryPercent ); mPerformanceDetails[1] = String::format( "In use %s Available %s Total %s Swap %s / %s", formatKiBIEC( sysInfo.getUsedMemoryKB() ).c_str(), formatKiBIEC( sysInfo.availableMemory ).c_str(), formatKiBIEC( sysInfo.totalMemory ).c_str(), formatKiBIEC( sysInfo.getUsedSwapKB() ).c_str(), formatKiBIEC( sysInfo.totalSwap ).c_str() ); mPerformanceSummaries[2] = "Tracked process GPU usage"; mPerformanceDetails[2] = hasGpu ? String::format( "Usage %d%% Tracked GPU memory %s", std::min( gpuUsage, 100 ), formatKiBIEC( gpuMemory ).c_str() ) : "GPU data is unavailable on this system"; mPerformanceSummaries[3] = "Tracked process network traffic"; mPerformanceDetails[3] = hasNetwork ? String::format( "Download %s Upload %s", formatPerformanceRate( download ).c_str(), formatPerformanceRate( upload ).c_str() ) : "Network data is unavailable on this system"; mPerformanceSummary->setText( mPerformanceSummaries[mSelectedPerformanceMetric] ); mPerformanceDetail->setText( mPerformanceDetails[mSelectedPerformanceMetric] ); } void App::setupProcessTable() { mProcessModel = ProcessModel::create( mApp->getUI() ); mProcessModel->setDivideCpuUsage( mConfig->divideCpuUsage ); mTreeModel = ProcessTreeModel::create( mProcessModel ); mSortProxy = SortingProxyModel::New( mProcessModel ); for ( UIAbstractTableView* view : { static_cast( mTableView ), static_cast( mTreeView ) } ) { if ( !view ) continue; view->setSelectionKind( UIAbstractView::SelectionKind::Multiple ); view->setColumnReorderingEnabled( true ); if ( view == mTreeView ) view->setModel( mTreeModel ); else view->setModel( mSortProxy ); view->moveColumn( ProcessModel::ColCommand, ProcessModel::ColCount - 1 ); for ( size_t column = 0; column < ProcessModel::ColCount; ++column ) { if ( auto* header = view->getHeaderColumn( column ) ) header->setTooltipText( processColumnTooltip( mApp->getUI(), column ) ); } view->setColumnsHidden( std::vector( kOptionalProcessColumns.begin(), kOptionalProcessColumns.end() ), true ); #if EE_PLATFORM == EE_PLATFORM_MACOS view->setColumnHidden( ProcessModel::ColTotalMemory, false ); #endif hideUnsupportedProcessColumns( *view ); view->setOnUpdateCellCb( [this]( UITableCell* cell, Model* ) { if ( !cell ) return; const ModelIndex index = cell->getCurIndex(); const ProcessInfo* process = processForIndex( index ); setCellClassEnabled( *cell, "eproc-process-ended", process && process->status == ProcessStatus::Ended ); const std::string_view desiredUsernameClass = process && index.column() == ProcessModel::ColUsername ? usernameClass( *process ) : std::string_view{}; for ( const auto className : kUsernameClasses ) setCellClassEnabled( *cell, className, className == desiredUsernameClass ); const int cpuUsage = process && index.column() == ProcessModel::ColCpu ? mProcessModel->displayedCpuUsage( *process ) : 0; const size_t fillIndex = cpuUsage > 0 ? static_cast( std::min( 10, ( cpuUsage + 9 ) / 10 ) ) : 0; // A removed class does not reset its CSS background image, so every reused cell // must receive an explicit fill class, including the 0% state. for ( size_t i = 0; i < kCpuFillClasses.size(); ++i ) { if ( i != fillIndex ) setCellClassEnabled( *cell, kCpuFillClasses[i], false ); } setCellClassEnabled( *cell, kCpuFillClasses[fillIndex], true ); } ); view->setOnHeaderContextMenuCb( [this]( UIPopUpMenu* menu, size_t column ) { for ( Uint32 i = 0; i < menu->getCount(); ) { const UIWidget* item = menu->getItem( i ); if ( item->getId() == "show-column" && !isProcessColumnSupported( static_cast( item->getData() ) ) ) menu->remove( i ); else ++i; } if ( column != ProcessModel::ColCpu ) return; menu->addSeparator(); menu->addCheckBox( mApp->getUI()->i18n( "eproc_divide_cpu_usage", "Divide CPU usage by number of CPUs" ), mConfig->divideCpuUsage ) ->setId( "divide-cpu-usage" ); menu->on( Event::OnItemClicked, [this]( const Event* event ) { if ( event->getNode()->getId() != "divide-cpu-usage" ) return; std::vector selected = selectedPids(); captureTreeExpansion(); if ( mTreeView ) mTreeView->clearViewMetadata(); mConfig->divideCpuUsage = !mConfig->divideCpuUsage; mProcessModel->setDivideCpuUsage( mConfig->divideCpuUsage ); restoreTreeExpansion(); restoreSelection( selected ); } ); } ); view->setRowHeight( PixelDensity::dpToPx( 18 ) ); // The flexible column is Name; the icon column is fixed so every row lines up. view->setMainColumn( ProcessModel::ColName ); view->setSortIconSize( 12 ); view->setIconSize( PixelDensity::dpToPxI( 16 ) ); view->setColumnWidth( ProcessModel::ColIcon, PixelDensity::dpToPx( 26 ) ); view->setOnSelectionChange( [this]() { onSelectionChange(); } ); view->on( Event::KeyDown, [this, view]( const Event* event ) { const KeyEvent* key = event->asKeyEvent(); if ( view == activeProcessView() && key->getKeyCode() == KEY_A && ( key->getSanitizedMod() & KeyMod::getDefaultModifier() ) && !view->isEditing() ) { view->selectAll(); } } ); view->onModelEvent( [this]( const ModelEvent* event ) { if ( event->getModelEventType() == ModelEventType::OpenMenu ) showProcessContextMenu( event->getModelIndex() ); } ); } // The original opens sorted by memory usage, descending, which surfaces the heavy processes // instead of the kernel threads that /proc happens to enumerate first. Sorting through the // view (not the model directly) also renders the sort indicator in the header. mTableView->sortByColumn( ProcessModel::ColMemory, SortOrder::Descending ); if ( mConfig->filterMode == ProcessModel::ProgramsOnly && ( !mCollector || !mCollector->supportsProgramsOnly() ) ) mConfig->filterMode = ProcessModel::AllProcesses; if ( mConfig->filterMode > ProcessModel::AllProcesses && mConfig->filterMode < ProcessModel::FilterModeCount ) { mFilterDropdown->getListBox()->setSelected( mConfig->filterMode ); if ( mProcessModel->getFilter() != mConfig->filterMode ) onFilterChanged(); } } void App::startCollection() { mCollector = ProcessCollector::create(); if ( !mCollector ) { Log::error( "eproc: no process collector for this platform, the table will stay empty" ); return; } // A single worker keeps the collector's previous-sample state exclusive to one thread, and // keeps /proc walking off the UI thread entirely. // // terminateOnClose must stay false: the pool destructor then joins the worker, which // guarantees no in-flight collect() can outlive mCollector. App's destructor explicitly // joins the pool before destroying the staging mutex that the worker uses. mThreadPool = ThreadPool::createShared( 1, false ); // Config is available before any table exists. Include PSS in the first snapshot and // overlap the cold scan with window/UI initialization when the saved active view needs it. mCollectProportionalMemory = showsFamilyMemory( parse( mConfig->processTableState ), mConfig->filterMode == ProcessModel::AllProcessesInTreeForm ); // Timed from this first dispatch so the next sample lands a full period later, giving the CPU // deltas a meaningful window instead of the few milliseconds of a back-to-back pair. mDispatchClock.getElapsedTimeAndReset(); collectAsync(); } void App::setupRefreshTimer() { if ( !mCollector ) return; // The worker finishes a snapshot long before the next sample is due, so the tick only polls // for staged results and dispatches a new sample once the refresh period has elapsed. // Publishing only at the sampling cadence would delay the first visible snapshot. mRoot->setInterval( [this] { onRefreshTick(); }, Milliseconds( mTickIntervalMs ) ); } void App::onRefreshTick() { publishStagedSnapshot(); if ( mCollectInFlight.load() ) return; if ( mProcessTableStateRestored ) reconcileProportionalMemory(); if ( mCollectionRequested ) { mCollectionRequested = false; mDispatchClock.getElapsedTimeAndReset(); collectAsync(); return; } if ( mDispatchClock.getElapsedTime().asMilliseconds() < mUpdateIntervalMs ) return; mDispatchClock.getElapsedTimeAndReset(); collectAsync(); } void App::collectAsync() { if ( !mCollector || !mThreadPool ) return; // Never overlap collections: CPU usage is computed from the delta against the instance's // previous sample, so concurrent runs would corrupt it. if ( mCollectInFlight.exchange( true ) ) return; const bool collectProportionalMemory = mCollectProportionalMemory; const bool initialCollection = mInitialCollection; mInitialCollection = false; mThreadPool->run( [this, collectProportionalMemory, initialCollection] { // Policy travels with the job; the collector and its caches stay worker-owned. mCollector->setCollectProportionalMemory( collectProportionalMemory ); std::vector processes; SystemInfo sysInfo; std::vector memory; bool publishedBase = false; const auto publishBase = [this, &publishedBase]( std::vector&& processes, SystemInfo&& sysInfo ) { Lock lock( mStagingMutex ); mStagedProcesses = std::move( processes ); mStagedSystemInfo = std::move( sysInfo ); mStagedFamilyMemoryReady = false; mStagedReady = true; publishedBase = true; }; const bool collected = initialCollection ? mCollector->collectInitial( processes, sysInfo, publishBase, &memory ) : mCollector->collect( processes, sysInfo ); if ( collected ) { Lock lock( mStagingMutex ); if ( publishedBase ) { mStagedProportionalMemory = std::move( memory ); mStagedProportionalMemoryReady = true; } else { mStagedProcesses = std::move( processes ); mStagedSystemInfo = std::move( sysInfo ); mStagedFamilyMemoryReady = true; mStagedReady = true; // A newer ordinary snapshot supersedes any unconsumed startup completion. mStagedProportionalMemory.clear(); mStagedProportionalMemoryReady = false; } } mCollectInFlight.store( false ); } ); } void App::publishStagedSnapshot() { std::vector processes; SystemInfo sysInfo; std::vector memory; bool hasSnapshot = false; bool hasMemory = false; bool familyMemoryReady = true; { Lock lock( mStagingMutex ); if ( !mStagedReady && !mStagedProportionalMemoryReady ) return; hasSnapshot = mStagedReady; hasMemory = mStagedProportionalMemoryReady; familyMemoryReady = mStagedFamilyMemoryReady || hasMemory; if ( hasSnapshot ) { processes = std::move( mStagedProcesses ); sysInfo = std::move( mStagedSystemInfo ); mStagedReady = false; } if ( hasMemory ) { memory = std::move( mStagedProportionalMemory ); mStagedProportionalMemoryReady = false; } } // Window ownership is needed by the Programs Only filter, so it is refreshed on the UI thread // once per published snapshot rather than per tick. if ( hasSnapshot && mProcessModel && mCollector && mCollector->supportsProgramsOnly() ) { mGuiWindows.refresh(); mProcessModel->setGuiWindowPids( UnorderedSet( mGuiWindows.windowPids().begin(), mGuiWindows.windowPids().end() ) ); for ( auto& process : processes ) { if ( process.iconPath.empty() && mGuiWindows.hasWindowForPid( process.pid ) ) process.windowIcon = mGuiWindows.iconForPid( process.pid ); } } // A full model reset clears the view's selection (SortingProxyModel drops it on every // invalidation), so selected processes are remembered by PID and re-selected afterwards. PIDs // are stable across snapshots while row indexes are not: the table re-sorts on every update. std::vector selected = selectedPids(); captureTreeExpansion(); if ( mTreeView ) mTreeView->clearViewMetadata(); if ( hasSnapshot && mPerformanceDetailChart ) updatePerformance( sysInfo, processes ); if ( mProcessModel ) { if ( hasSnapshot ) { mProcessModel->applySnapshot( std::move( processes ), sysInfo, familyMemoryReady, memory ); } else if ( hasMemory ) { mProcessModel->applyProportionalMemory( memory ); } } if ( !mProcessTableStateRestored && !mProcessTableStateRestoreScheduled && mApp && mApp->getUI() ) { mProcessTableStateRestoreScheduled = true; mApp->getUI()->runOnMainThread( [this] { if ( !mProcessTableStateRestored ) { restoreProcessTableState(); mProcessTableStateRestored = true; reconcileProportionalMemory(); } mProcessTableStateRestoreScheduled = false; } ); } updateStatusBar(); restoreTreeExpansion(); restoreSelection( selected ); } UIAbstractTableView* App::activeProcessView() const { return mTreeMode ? static_cast( mTreeView ) : static_cast( mTableView ); } void App::reconcileProportionalMemory() { UIAbstractTableView* view = activeProcessView(); const bool enabled = view && ( !view->isColumnHidden( ProcessModel::ColFamilyMemory ) || !view->isColumnHidden( ProcessModel::ColFamilyMemoryPercent ) ); if ( enabled == mCollectProportionalMemory ) return; mCollectProportionalMemory = enabled; // Normally startup prediction matches restoration. Correct a mismatch, or a later // visibility/view change, as soon as the current collection finishes. mCollectionRequested = enabled; } void App::captureTreeExpansion() { if ( !mTreeMode || !mTreeExpansionInitialized || !mTreeView || !mTreeModel || !mProcessModel || mTreeSearchActive ) return; mExpandedTreePids.clear(); for ( size_t row = 0; row < mProcessModel->visibleCount(); ++row ) { const ProcessInfo* process = mProcessModel->getProcessByRow( static_cast( row ) ); if ( process && mTreeView->isExpanded( mTreeModel->indexForPid( process->pid ) ) ) mExpandedTreePids.push_back( process->pid ); } } void App::restoreTreeExpansion() { if ( !mTreeMode || !mTreeView || !mTreeModel ) return; std::vector indexes; if ( mTreeSearchActive ) { for ( Int64 pid : mProcessModel->textMatchedPids() ) { ModelIndex parent = mTreeModel->indexForPid( pid ).parent(); while ( parent.isValid() ) { indexes.push_back( parent ); parent = parent.parent(); } } if ( !indexes.empty() ) mTreeView->setExpanded( indexes, true ); return; } if ( !mTreeExpansionInitialized ) { mTreeExpansionInitialized = mTreeModel->rowCount() > 0; if ( mTreeExpansionInitialized ) mTreeView->expandAll(); return; } else { indexes.reserve( mExpandedTreePids.size() ); for ( Int64 pid : mExpandedTreePids ) { ModelIndex index = mTreeModel->indexForPid( pid ); if ( index.isValid() ) indexes.push_back( index ); } } if ( !indexes.empty() ) mTreeView->setExpanded( indexes, true ); } void App::restoreSelection( const std::vector& pids ) { UIAbstractTableView* view = activeProcessView(); if ( pids.empty() || !view || !mProcessModel ) return; std::vector indexes; indexes.reserve( pids.size() ); for ( Int64 pid : pids ) { ModelIndex index; if ( mTreeMode ) { index = mTreeModel->indexForPid( pid ); } else { int row = mProcessModel->rowForPid( pid ); if ( row >= 0 ) index = mSortProxy->mapToProxy( mProcessModel->index( row, 0 ) ); } if ( index.isValid() ) indexes.push_back( index ); } if ( !indexes.empty() ) { view->getSelection().set( indexes ); if ( mTreeMode ) mTreeView->openModelIndexParentTree( indexes.front() ); } } void App::updateStatusBar() { if ( !mProcessModel ) return; const auto& sys = mProcessModel->getSystemInfo(); if ( mStatusText ) mStatusText->setText( String::format( mApp->getUI()->i18n( "eproc_process_count_format", "%zu processes" ).toUtf8(), mProcessModel->visibleCount() ) ); if ( mCpuText ) mCpuText->setText( String::format( mApp->getUI()->i18n( "eproc_cpu_status_format", "CPU: %d%%" ).toUtf8(), static_cast( sys.cpuUsage ) ) ); if ( mMemText ) mMemText->setText( String::format( mApp->getUI()->i18n( "eproc_memory_status_format", "Memory: %s / %s" ).toUtf8(), formatKiBIEC( sys.getUsedMemoryKB() ).c_str(), formatKiBIEC( sys.getTotalMemoryKB() ).c_str() ) ); if ( mSwapText ) { mSwapText->setVisible( sys.totalSwap >= 0 ); if ( sys.totalSwap >= 0 ) { mSwapText->setText( String::format( mApp->getUI()->i18n( "eproc_swap_status_format", "Swap: %s / %s" ).toUtf8(), formatKiBIEC( sys.getUsedSwapKB() ).c_str(), formatKiBIEC( sys.totalSwap ).c_str() ) ); } } } void App::onEndProcess() { std::vector pids = selectedPids(); const std::string label = endProcessLabel( mApp->getUI(), pids.size() ).toUtf8(); requestSignal( std::move( pids ), kEndProcessSignal, label, true ); } void App::onSearchChanged() { if ( mSearchInput && mProcessModel ) { std::vector selected = selectedPids(); captureTreeExpansion(); if ( mTreeView ) mTreeView->clearViewMetadata(); const std::string text = mSearchInput->getText().toUtf8(); mTreeSearchActive = !text.empty(); mProcessModel->setTextFilter( text ); restoreTreeExpansion(); restoreSelection( selected ); updateStatusBar(); } } void App::onFilterChanged() { if ( mFilterDropdown && mProcessModel ) { Uint32 selected = mFilterDropdown->getListBox()->getItemSelectedIndex(); if ( selected >= ProcessModel::FilterModeCount ) return; std::vector selectedPidsBeforeSwitch = selectedPids(); captureTreeExpansion(); if ( mTreeView ) mTreeView->clearViewMetadata(); mProcessModel->setFilter( static_cast( selected ) ); mTreeMode = selected == ProcessModel::AllProcessesInTreeForm; mTableView->setVisible( !mTreeMode ); mTreeView->setVisible( mTreeMode ); restoreTreeExpansion(); restoreSelection( selectedPidsBeforeSwitch ); updateStatusBar(); } } void App::onSelectionChange() { // Update End Process button state UIAbstractTableView* view = activeProcessView(); if ( mEndProcessBtn && view ) { bool hasProcess = false; size_t processCount = 0; for ( const auto& index : view->getSelection().indexes() ) { const ProcessInfo* proc = processForIndex( index ); if ( proc && proc->status != ProcessStatus::Ended ) { hasProcess = true; ++processCount; } } mEndProcessBtn->setEnabled( hasProcess ); mEndProcessBtn->setText( endProcessLabel( mApp->getUI(), processCount ) ); } } const ProcessInfo* App::processForIndex( const ModelIndex& index ) const { if ( !mProcessModel || !index.isValid() ) return nullptr; if ( index.model() == mTreeModel.get() ) return mTreeModel->processForIndex( index ); if ( index.model() == mSortProxy.get() ) return mProcessModel->getProcessByRow( mSortProxy->mapToSource( index ).row() ); return nullptr; } std::vector App::selectedPids() const { std::vector pids; UIAbstractTableView* view = activeProcessView(); if ( !mProcessModel || !view ) return pids; const auto indexes = view->getSelection().indexes(); pids.reserve( indexes.size() ); for ( const auto& proxyIndex : indexes ) { const ProcessInfo* proc = processForIndex( proxyIndex ); if ( proc && proc->status != ProcessStatus::Ended ) pids.push_back( proc->pid ); } return pids; } void App::selectProcess( Int64 pid ) { UIAbstractTableView* view = activeProcessView(); if ( pid <= 0 || !mProcessModel || !view ) return; int row = mProcessModel->rowForPid( pid ); // The active filter may be hiding the target, so drop it instead of silently doing nothing // (the original clears its text filter for the same reason). if ( row < 0 && mSearchInput && !mSearchInput->getText().empty() ) { mSearchInput->setText( "" ); if ( mTreeSearchActive ) onSearchChanged(); row = mProcessModel->rowForPid( pid ); } if ( row < 0 ) return; ModelIndex index = mTreeMode ? mTreeModel->indexForPid( pid ) : mSortProxy->mapToProxy( mProcessModel->index( row, 0 ) ); if ( index.isValid() ) { if ( mTreeMode ) mTreeView->openModelIndexParentTree( index ); view->setSelection( index ); } } void App::requestSignal( std::vector pids, int signal, const std::string& actionLabel, bool confirm ) { if ( pids.empty() ) return; const size_t processCount = pids.size(); const Int64 firstPid = pids.front(); auto send = [pids = std::move( pids ), signal]() { for ( Int64 pid : pids ) sendProcessSignal( pid, signal ); }; if ( !confirm ) { send(); return; } const std::string target = processCount == 1 ? String::format( mApp->getUI()->i18n( "eproc_process_target", "process %lld" ).toUtf8(), static_cast( firstPid ) ) : String::format( mApp->getUI()->i18n( "eproc_processes_target", "%zu processes" ).toUtf8(), processCount ); const std::string message = String::format( mApp->getUI()->i18n( "eproc_confirm_action", "%s %s?" ).toUtf8(), actionLabel.c_str(), target.c_str() ); UIMessageBox* box = UIMessageBox::New( UIMessageBox::OK_CANCEL, message ); box->setTitle( actionLabel ); box->on( Event::OnConfirm, [send]( const Event* ) { send(); } ); box->center(); box->showWhenReady(); } void App::showProcessContextMenu( const ModelIndex& proxyIndex ) { UIAbstractTableView* view = activeProcessView(); if ( !view ) return; // Right-clicking outside the selection moves the selection to the clicked row, as the // original does, so the menu always acts on what the user pointed at. if ( const ProcessInfo* clicked = processForIndex( proxyIndex ) ) { bool alreadySelected = false; for ( const auto& selected : view->getSelection().indexes() ) { const ProcessInfo* process = processForIndex( selected ); if ( process && process->pid == clicked->pid ) { alreadySelected = true; break; } } if ( !alreadySelected ) view->setSelection( proxyIndex, false ); } const std::vector pids = selectedPids(); if ( pids.empty() ) return; const bool singleProcess = view->getSelection().size() == 1; const ProcessInfo* proc = singleProcess ? processForIndex( proxyIndex ) : nullptr; const Int64 parentPid = proc ? proc->parentPid : 0; const Int64 tracerPid = proc ? proc->tracerPid : 0; std::string copyCommandLine = proc ? proc->commandLine : std::string(); struct SignalItem { const char* id; const char* key; const char* label; int signal; }; // The same signal set the original offers, in the same order. #if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ EE_PLATFORM == EE_PLATFORM_BSD static const std::array signalItems = { { { "signal-stop", "eproc_signal_suspend", "Suspend (STOP)", SIGSTOP }, { "signal-cont", "eproc_signal_continue", "Continue (CONT)", SIGCONT }, { "signal-hup", "eproc_signal_hangup", "Hangup (HUP)", SIGHUP }, { "signal-int", "eproc_signal_interrupt", "Interrupt (INT)", SIGINT }, { "signal-term", "eproc_signal_terminate", "Terminate (TERM)", SIGTERM }, { "signal-kill", "eproc_signal_kill", "Kill (KILL)", SIGKILL }, { "signal-usr1", "eproc_signal_user1", "User 1 (USR1)", SIGUSR1 }, { "signal-usr2", "eproc_signal_user2", "User 2 (USR2)", SIGUSR2 }, } }; #endif UIPopUpMenu* menu = UIPopUpMenu::New(); menu->setId( "process_context_menu" ); #if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ EE_PLATFORM == EE_PLATFORM_BSD UIPopUpMenu* signalMenu = UIPopUpMenu::New(); signalMenu->setId( "process_signal_menu" ); for ( const auto& item : signalItems ) signalMenu->add( mApp->getUI()->i18n( item.key, item.label ) )->setId( item.id ); // Menu item events go to their immediate menu, not the parent context menu. signalMenu->on( Event::OnItemClicked, [this, pids]( const Event* event ) { const auto* item = event->getNode()->asType(); if ( !item ) return; for ( const auto& sig : signalItems ) { if ( item->getId() == sig.id ) { requestSignal( pids, sig.signal, mApp->getUI()->i18n( "eproc_send_signal", "Send Signal" ).toUtf8(), false ); break; } } } ); menu->addSubMenu( mApp->getUI()->i18n( "eproc_send_signal", "Send Signal" ), nullptr, signalMenu ) ->setId( "send-signal" ); #endif if ( singleProcess && proc ) { menu->add( mApp->getUI()->i18n( "eproc_jump_to_parent", "Jump to Parent Process" ) ) ->setId( "jump-parent" ); if ( tracerPid > 0 ) { menu->add( mApp->getUI()->i18n( "eproc_jump_to_tracer", "Jump to Process Debugging This One" ) ) ->setId( "jump-tracer" ); } menu->add( mApp->getUI()->i18n( "eproc_copy_command_line", "Copy Command Line" ) ) ->setId( "copy-command-line" ); } #if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_WIN || \ EE_PLATFORM == EE_PLATFORM_MACOS || EE_PLATFORM == EE_PLATFORM_BSD menu->addSeparator(); menu->add( endProcessLabel( mApp->getUI(), pids.size() ) )->setId( "end-process" ); menu->add( forceKillLabel( mApp->getUI(), pids.size() ) )->setId( "kill-process" ); #endif menu->on( Event::OnItemClicked, [this, pids, parentPid, tracerPid, copyCommandLine = std::move( copyCommandLine )]( const Event* event ) { UIMenuItem* item = event->getNode()->asType(); if ( !item ) return; const std::string id( item->getId() ); if ( id == "jump-parent" ) { selectProcess( parentPid ); } else if ( id == "jump-tracer" ) { selectProcess( tracerPid ); } else if ( id == "copy-command-line" ) { if ( !copyCommandLine.empty() && mApp->getWindow()->getClipboard() ) mApp->getWindow()->getClipboard()->setText( copyCommandLine ); } else if ( id == "end-process" ) { requestSignal( pids, kEndProcessSignal, endProcessLabel( mApp->getUI(), pids.size() ).toUtf8(), true ); } else if ( id == "kill-process" ) { requestSignal( pids, kForceKillSignal, forceKillLabel( mApp->getUI(), pids.size() ).toUtf8(), true ); } } ); Vector2f pos( mApp->getWindow()->getInput()->getMousePos().asFloat() ); menu->nodeToWorldTranslation( pos ); UIMenu::findBestMenuPos( pos, menu ); menu->setPixelsPosition( pos ); menu->show(); } } // namespace eproc EE_MAIN_FUNC int main( int argc, char* argv[] ) { eproc::App app; return app.run( argc, argv ); }