Add initial eproc system monitor

- Add the eproc process-monitoring application and persistent window/table state
  - Implement Linux process, network, GPU, icon, and GUI-window collection
  - Add optional process columns, styling, i18n-ready strings, and command copying
  - Retain exited processes for one update and color them as ended
  - Extend table sizing and header behavior for persisted column layouts
  - Organize Linux-specific sources under platform/linux
  - Keep unsupported platforms buildable with a startup WIP warning
This commit is contained in:
Martín Lucas Golini
2026-09-20 02:36:54 -03:00
parent b838a42fee
commit 1ae2b7180b
27 changed files with 4895 additions and 9 deletions
+57
View File
@@ -0,0 +1,57 @@
#include "appconfig.hpp"
#include <eepp/system/filesystem.hpp>
using namespace EE;
using namespace EE::System;
namespace eproc {
AppConfig::AppConfig( std::string configPath ) : mConfigPath( std::move( configPath ) ) {
FileSystem::dirAddSlashAtEnd( mConfigPath );
mState.path( mConfigPath + "state.cfg" );
}
void AppConfig::load() {
mState.loadFromFile( mState.path() );
windowState.size.setWidth( mState.getValueI( "window", "width", windowState.size.getWidth() ) );
windowState.size.setHeight(
mState.getValueI( "window", "height", windowState.size.getHeight() ) );
windowState.position.x = mState.getValueI( "window", "x", windowState.position.x );
windowState.position.y = mState.getValueI( "window", "y", windowState.position.y );
windowState.displayIndex =
mState.getValueI( "window", "display_index", windowState.displayIndex );
windowState.maximized = mState.getValueB( "window", "maximized", windowState.maximized );
processTableState = mState.getValue( "process_table", "state", processTableState );
}
bool AppConfig::saveWindowState() {
if ( !FileSystem::isDirectory( mConfigPath ) && !FileSystem::makeDir( mConfigPath, true ) )
return false;
mState.setValueI( "window", "width", windowState.size.getWidth() );
mState.setValueI( "window", "height", windowState.size.getHeight() );
mState.setValueI( "window", "x", windowState.position.x );
mState.setValueI( "window", "y", windowState.position.y );
mState.setValueI( "window", "display_index", windowState.displayIndex );
mState.setValueB( "window", "maximized", windowState.maximized );
mState.setValue( "process_table", "state", processTableState );
return mState.writeFile();
}
void AppConfig::captureWindowState( EE::Window::Window* window ) {
if ( !window )
return;
windowState.size = Sys::getPlatformType() == Sys::PlatformType::macOS
? window->getSizeInScreenCoordinates()
: window->getLastWindowedSizeInScreenCoordinates();
windowState.position = window->getPosition();
windowState.position.x = eemax( 0, windowState.position.x );
windowState.position.y = eemax( 0, windowState.position.y );
windowState.displayIndex = window->getCurrentDisplayIndex();
windowState.maximized = window->isMaximized();
}
} // namespace eproc
+46
View File
@@ -0,0 +1,46 @@
#ifndef EPROC_APPCONFIG_HPP
#define EPROC_APPCONFIG_HPP
#include <eepp/math/size.hpp>
#include <eepp/math/vector2.hpp>
#include <eepp/system/inifile.hpp>
#include <eepp/window/window.hpp>
#include <string>
using namespace EE;
using namespace EE::Math;
using namespace EE::System;
using namespace EE::Window;
namespace eproc {
struct WindowStateConfig {
Sizei size{ 1280, 720 };
Vector2i position{ -1, -1 };
int displayIndex{ 0 };
bool maximized{ false };
};
class AppConfig {
public:
explicit AppConfig( std::string configPath );
void load();
bool saveWindowState();
void captureWindowState( EE::Window::Window* window );
const std::string& getConfigPath() const { return mConfigPath; }
WindowStateConfig windowState;
// Serialized process table columns, widths, and sorting state.
std::string processTableState;
private:
std::string mConfigPath;
IniFile mState;
};
} // namespace eproc
#endif // EPROC_APPCONFIG_HPP
+963
View File
@@ -0,0 +1,963 @@
#include "eproc.hpp"
#include <eepp/system/log.hpp>
#include <eepp/ui/uimenu.hpp>
#include <eepp/ui/uimenuitem.hpp>
#include <nlohmann/json.hpp>
#include <array>
#include <cmath>
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \
EE_PLATFORM == EE_PLATFORM_BSD
#include <unistd.h>
#endif
#if EE_PLATFORM == EE_PLATFORM_LINUX
#include <signal.h>
#endif
namespace eproc {
namespace {
constexpr int kProcessTableStateVersion = 3;
constexpr int kPreviousProcessTableStateVersion = 2;
constexpr size_t kPreviousCommandColumn = 11;
constexpr std::array<size_t, 8> kOptionalProcessColumns = { {
ProcessModel::ColTotalMemory,
ProcessModel::ColVirtualSize,
ProcessModel::ColCpuTime,
ProcessModel::ColNiceness,
ProcessModel::ColRelativeStartTime,
ProcessModel::ColTty,
ProcessModel::ColIoRead,
ProcessModel::ColIoWrite,
} };
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;
}
bool isValidProcessTableWidthState( const nlohmann::json& widths, size_t columnCount,
const UIAbstractTableView& tableView ) {
if ( !widths.is_object() || !widths.contains( "mode" ) || !widths["mode"].is_string() ||
!widths.contains( "widths" ) || !widths["widths"].is_array() ||
widths["widths"].size() != columnCount )
return false;
const std::string mode = widths["mode"].get<std::string>();
if ( mode != "pixels" && mode != "percentage" )
return false;
bool hasVisibleWidth = false;
for ( size_t column = 0; column < columnCount; ++column ) {
const auto& width = widths["widths"][column];
const double value = width.is_number() ? width.get<double>() : 0;
if ( !width.is_number() || !std::isfinite( value ) || value < 0 )
return false;
if ( mode == "pixels" && !tableView.isColumnHidden( column ) && value <= 1.0 )
return false;
if ( !tableView.isColumnHidden( column ) && value > 0 )
hasVisibleWidth = true;
}
return hasVisibleWidth;
}
size_t remapPreviousProcessTableColumn( size_t column ) {
if ( column == kPreviousCommandColumn )
return ProcessModel::ColCommand;
if ( column > kPreviousCommandColumn )
return column - 1;
return column;
}
bool migratePreviousProcessTableState( nlohmann::json& state ) {
if ( !state.contains( "widths" ) || !state["widths"].is_object() ||
!state["widths"].contains( "widths" ) || !state["widths"]["widths"].is_array() ||
state["widths"]["widths"].size() != ProcessModel::ColCount )
return false;
nlohmann::json remappedWidths = nlohmann::json::array();
for ( size_t column = 0; column < ProcessModel::ColCount; ++column )
remappedWidths.push_back( 0 );
for ( size_t previousColumn = 0; previousColumn < ProcessModel::ColCount; ++previousColumn ) {
remappedWidths[remapPreviousProcessTableColumn( previousColumn )] =
state["widths"]["widths"][previousColumn];
}
state["widths"]["widths"] = std::move( remappedWidths );
if ( state.contains( "hidden_columns" ) && state["hidden_columns"].is_array() ) {
nlohmann::json remappedHiddenColumns = nlohmann::json::array();
for ( const auto& column : state["hidden_columns"] ) {
if ( !column.is_number_integer() )
continue;
const Int64 previousColumn = column.get<Int64>();
if ( previousColumn >= 0 &&
static_cast<size_t>( previousColumn ) < ProcessModel::ColCount )
remappedHiddenColumns.push_back(
remapPreviousProcessTableColumn( static_cast<size_t>( previousColumn ) ) );
}
state["hidden_columns"] = std::move( remappedHiddenColumns );
}
if ( state.contains( "sort" ) && state["sort"].is_object() &&
state["sort"].contains( "column" ) && state["sort"]["column"].is_number_integer() ) {
const Int64 previousColumn = state["sort"]["column"].get<Int64>();
if ( previousColumn >= 0 && static_cast<size_t>( previousColumn ) < ProcessModel::ColCount )
state["sort"]["column"] =
remapPreviousProcessTableColumn( static_cast<size_t>( previousColumn ) );
}
state["version"] = kProcessTableStateVersion;
return true;
}
bool isCurrentUser( const ProcessInfo& process ) {
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \
EE_PLATFORM == EE_PLATFORM_BSD
return process.uid == static_cast<long>( getuid() );
#else
return false;
#endif
}
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 ( isCurrentUser( process ) )
return "eproc-process-username-own";
if ( process.uid < 100 || !process.canLogin )
return "eproc-process-username-system";
return "eproc-process-username-other";
}
} // namespace
App::App() {
mConfig = std::make_unique<AppConfig>( Sys::getConfigPath( "eproc" ) );
mConfig->load();
// Scanning /proc takes long enough to race the first frame, which would leave the table briefly
// empty. Kick the worker off before the window exists so the first snapshot is already staged
// by the time the scene is rendered.
startCollection();
const Sizei storedSize = mConfig->windowState.size;
const Uint32 windowWidth =
storedSize.getWidth() > 0 ? static_cast<Uint32>( storedSize.getWidth() ) : 1280;
const Uint32 windowHeight =
storedSize.getHeight() > 0 ? static_cast<Uint32>( storedSize.getHeight() ) : 720;
WindowSettings ws( windowWidth, windowHeight, "", WindowStyle::Default, WindowBackend::Default,
32, Sys::getProcessPath() + "assets/icon/ee.png" );
mApp = std::make_unique<UIApplication>( ws );
if ( mApp->getUI() && mApp->getWindow() )
mApp->getWindow()->setTitle(
mApp->getUI()->i18n( "eproc_window_title", "System Monitor" ) );
}
App::~App() {}
int App::run() {
if ( !init() )
return EXIT_FAILURE;
const int result = mApp->run();
if ( !mWindowStateSaved )
saveWindowState();
return result;
}
bool App::init() {
auto* ui = mApp->getUI();
if ( !ui )
return false;
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();
} );
}
mRoot = ui->loadLayoutFromString( R"xml(
<style>
tableview::cell {
background-color: transparent;
text-align: left;
}
tableview::cell.eproc-process-column-icon,
tableview::cell.eproc-process-column-username,
tableview::cell.eproc-process-column-cpu {
text-align: center;
}
tableview::cell.eproc-process-username-own {
background-color: #00D0D432;
border-top: 1dprd solid rgba(0, 255, 255, 0.026);
border-bottom: 1dprd solid rgba(0, 255, 255, 0.085);
}
tableview::cell.eproc-process-username-system {
background-color: #DADCD732;
}
tableview::cell.eproc-process-username-other {
background-color: #029A3632;
}
tableview::cell.eproc-process-username-traced {
background-color: #FFFF00;
}
tableview::cell.eproc-process-username-ended {
background-color: #D3D3D3;
}
tableview::cell.eproc-process-ended {
color: #808080;
tint: #808080;
}
tableview::cell.eproc-process-column-pid,
tableview::cell.eproc-process-column-memory,
tableview::cell.eproc-process-column-shared-memory,
tableview::cell.eproc-process-column-gpu-usage,
tableview::cell.eproc-process-column-gpu-memory,
tableview::cell.eproc-process-column-download,
tableview::cell.eproc-process-column-upload,
tableview::cell.eproc-process-column-total-memory,
tableview::cell.eproc-process-column-virtual-size,
tableview::cell.eproc-process-column-cpu-time,
tableview::cell.eproc-process-column-niceness,
tableview::cell.eproc-process-column-relative-start-time,
tableview::cell.eproc-process-column-io-read,
tableview::cell.eproc-process-column-io-write {
text-align: right;
}
tableview::row:nth-child(even),
treeview::row:nth-child(even) {
background-color: var(--list-back-alt);
}
tableview::row:nth-child(even):hover,
treeview::row:nth-child(even):hover {
background-color: var(--item-hover);
}
tableview::row:nth-child(even):selected,
treeview::row:nth-child(even):selected {
background-color: var(--primary);
}
</style>
<vbox id="main_layout" lw="mp" lh="mp">
<TabWidget id="tab_widget" lw="mp" lh="mp">
<!-- Process Table tab content -->
<vbox id="process_table_area" lw="mp" lh="mp">
<hbox id="toolbar" lw="mp" lh="wc" padding="4dp">
<PushButton id="end_process_btn" lw="wc" lh="wc" margin-right="4dp" />
<TextInput id="search_input" lw="0" lw8="1" lh="wc" margin-right="4dp" />
<DropDownList id="filter_dropdown" lw="120dp" lh="wc" margin-right="4dp" />
</hbox>
<TableView id="process_table" lw="mp" lh="0" lw8="1"
column-width-mode-menu="true"
table-flags="headers|row-search|focus-on-selection|auto-columns" />
<hbox id="status_bar" lw="mp" lh="wc" padding="4dp">
<TextView id="process_count" lw="0" lw8="0.25" lh="wc" />
<TextView id="cpu_text" lw="0" lw8="0.25" lh="wc" />
<TextView id="mem_text" lw="0" lw8="0.25" lh="wc" halign="right" />
<TextView id="swap_text" lw="0" lw8="0.25" lh="wc" />
</hbox>
</vbox>
<!-- System Load placeholder tab content -->
<!--
<TextView id="system_load_area" lw="mp" lh="mp"
text="System Load - Coming soon" />
-->
<!-- Tab definitions -->
<Tab id="tab_process_table" owns="process_table_area" />
<!-- <Tab id="tab_system_load" text="System Load" owns="system_load_area" /> -->
</TabWidget>
</vbox>
)xml" );
if ( !mRoot ) {
return false;
}
setupUI();
setupProcessTable();
#if EE_PLATFORM != EE_PLATFORM_LINUX && EE_PLATFORM != EE_PLATFORM_BSD
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();
#endif
// The first snapshot was requested before the window existed, so it is normally ready by now.
// Publishing it here (before the loop, so nothing is being drawn yet) means the very first
// frame already shows data instead of flashing an empty table.
waitForFirstSnapshot( 250 );
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();
mConfig->captureWindowState( mApp->getWindow() );
if ( !mConfig->saveWindowState() )
Log::error( "Could not save eproc window state to %s", mConfig->getConfigPath() );
else
mWindowStateSaved = true;
}
std::string App::serializeProcessTableState() const {
if ( !mTableView || !mSortProxy )
return {};
nlohmann::json state;
state["version"] = kProcessTableStateVersion;
state["widths"] = mTableView->serializeColumnWidths();
state["hidden_columns"] = nlohmann::json::array();
for ( size_t column = 0; column < mSortProxy->columnCount(); ++column ) {
if ( mTableView->isColumnHidden( column ) )
state["hidden_columns"].push_back( column );
}
state["sort"]["column"] = mSortProxy->keyColumn();
state["sort"]["order"] = sortOrderName( mSortProxy->sortOrder() );
return state.dump();
}
void App::restoreProcessTableState() {
if ( !mConfig || mConfig->processTableState.empty() || !mTableView || !mSortProxy )
return;
nlohmann::json state =
nlohmann::json::parse( mConfig->processTableState, nullptr, false, true );
if ( state.is_discarded() || !state.is_object() || !state.contains( "version" ) ||
!state["version"].is_number_integer() )
return;
const int stateVersion = state["version"].get<int>();
if ( stateVersion == kPreviousProcessTableStateVersion ) {
if ( !migratePreviousProcessTableState( state ) )
return;
} else if ( stateVersion != kProcessTableStateVersion ) {
return;
}
const size_t columnCount = mSortProxy->columnCount();
if ( !state.contains( "widths" ) ||
!isValidProcessTableWidthState( state["widths"], columnCount, *mTableView ) )
return;
if ( state.contains( "hidden_columns" ) && state["hidden_columns"].is_array() ) {
std::vector<bool> hidden( columnCount, false );
for ( const auto& column : state["hidden_columns"] ) {
if ( !column.is_number_integer() )
continue;
const Int64 index = column.get<Int64>();
if ( index >= 0 && static_cast<size_t>( index ) < columnCount )
hidden[static_cast<size_t>( index )] = true;
}
std::vector<size_t> visible;
visible.reserve( columnCount );
for ( size_t column = 0; column < columnCount; ++column ) {
if ( !hidden[column] )
visible.push_back( column );
}
// Always leave one column visible, even if a malformed or old state hides everything.
if ( !visible.empty() )
mTableView->setColumnsVisible( visible );
}
// Pixel restoration calls setColumnWidth() once per column. Automatic sizing must already be
// disabled, otherwise each call immediately recalculates all columns and overwrites the saved
// width with content-based sizing.
const bool pixelWidths = state["widths"]["mode"] == "pixels";
if ( pixelWidths )
mTableView->setAutoColumnsWidth( false );
if ( !mTableView->unserializeColumnWidths( state["widths"] ) && pixelWidths )
mTableView->setAutoColumnsWidth( true );
if ( state.contains( "sort" ) && state["sort"].is_object() ) {
const auto& sort = state["sort"];
const int column = sort.contains( "column" ) && sort["column"].is_number_integer()
? sort["column"].get<int>()
: -1;
const std::string orderName = sort.contains( "order" ) && sort["order"].is_string()
? sort["order"].get<std::string>()
: "none";
SortOrder order = SortOrder::None;
if ( column >= 0 && static_cast<size_t>( column ) < columnCount &&
parseSortOrder( orderName, order ) && order != SortOrder::None &&
mSortProxy->isColumnSortable( column ) )
mTableView->sortByColumn( static_cast<size_t>( column ), order );
}
}
bool App::closeWindow( EE::Window::Window* ) {
saveWindowState();
return true;
}
void App::setupUI() {
mTabWidget = mRoot->find<UITabWidget>( "tab_widget" );
mEndProcessBtn = mRoot->find<UIPushButton>( "end_process_btn" );
mSearchInput = mRoot->find<UITextInput>( "search_input" );
mFilterDropdown = mRoot->find<UIDropDownList>( "filter_dropdown" );
mTableView = mRoot->find<UITableView>( "process_table" );
mStatusText = mRoot->find<UITextView>( "process_count" );
mCpuText = mRoot->find<UITextView>( "cpu_text" );
mMemText = mRoot->find<UITextView>( "mem_text" );
mSwapText = mRoot->find<UITextView>( "swap_text" );
auto* ui = mApp->getUI();
mRoot->find<UITab>( "tab_process_table" )
->setText( ui->i18n( "eproc_process_table_tab", "Process Table" ) );
mEndProcessBtn->setText( ui->i18n( "eproc_end_process_button", "End Process..." ) );
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.
// Filter entries mirror the original's ProcessFilter::State order (flat variants only; the
// tree variants need a hierarchical model).
static const std::pair<const char*, const char*> filters[] = {
{ "eproc_filter_all_processes", "All Processes" },
{ "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 ( const auto& filter : filters )
filterListBox->addListBoxItem( ui->i18n( filter.first, filter.second ) );
filterListBox->setSelected( 0 );
// Connect events
if ( mEndProcessBtn )
mEndProcessBtn->onClick( [this]( const MouseEvent* ) { onEndProcess(); } );
if ( mSearchInput )
mSearchInput->on( Event::OnTextChanged, [this]( const Event* ) { onSearchChanged(); } );
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() );
}
} );
}
void App::setupProcessTable() {
mProcessModel = ProcessModel::create( mApp->getUI() );
mSortProxy = SortingProxyModel::New( mProcessModel );
if ( mTableView ) {
mTableView->setModel( mSortProxy );
mTableView->setColumnsHidden(
std::vector<size_t>( kOptionalProcessColumns.begin(), kOptionalProcessColumns.end() ),
true );
mTableView->setOnUpdateCellCb( [this]( UITableCell* cell, Model* model ) {
if ( !cell || !model )
return;
const ModelIndex index = cell->getCurIndex();
const ProcessInfo* process = processForProxyIndex( index );
const Variant columnClass = model->data( index, ModelRole::Class );
std::vector<std::string> classes;
if ( columnClass.isValid() )
classes.emplace_back( columnClass.toString() );
if ( process ) {
if ( process->status == ProcessStatus::Ended )
classes.emplace_back( "eproc-process-ended" );
if ( index.column() == ProcessModel::ColUsername )
classes.emplace_back( usernameClass( *process ) );
}
cell->setClasses( classes );
} );
mTableView->setRowHeight( 28 );
// The flexible column is Name; the icon column is fixed so every row lines up.
mTableView->setMainColumn( ProcessModel::ColName );
mTableView->setSortIconSize( 12 );
mTableView->setIconSize( PixelDensity::dpToPxI( 16 ) );
mTableView->setColumnWidth( ProcessModel::ColIcon, PixelDensity::dpToPx( 26 ) );
mTableView->setOnSelectionChange( [this]() { onSelectionChange(); } );
mTableView->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 );
}
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 (destroyed after mThreadPool,
// since members are destroyed in reverse declaration order).
mThreadPool = ThreadPool::createShared( 1, false );
// 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 on the same 2s cadence would leave the table empty until the second tick.
mRoot->setInterval( [this] { onRefreshTick(); }, Milliseconds( mTickIntervalMs ) );
}
bool App::waitForFirstSnapshot( Uint32 timeoutMs ) {
Clock clock;
while ( clock.getElapsedTime().asMilliseconds() < timeoutMs ) {
{
Lock lock( mStagingMutex );
if ( mStagedReady )
return true;
}
Sys::sleep( Milliseconds( 2 ) );
}
return false;
}
void App::onRefreshTick() {
publishStagedSnapshot();
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;
mThreadPool->run( [this] {
std::vector<ProcessInfo> processes;
SystemInfo sysInfo;
if ( mCollector->collect( processes, sysInfo ) ) {
Lock lock( mStagingMutex );
mStagedProcesses = std::move( processes );
mStagedSystemInfo = sysInfo;
mStagedReady = true;
}
mCollectInFlight.store( false );
} );
}
void App::publishStagedSnapshot() {
std::vector<ProcessInfo> processes;
SystemInfo sysInfo;
{
Lock lock( mStagingMutex );
if ( !mStagedReady )
return;
processes = std::move( mStagedProcesses );
sysInfo = mStagedSystemInfo;
mStagedReady = 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 ( mProcessModel ) {
mGuiWindows.refresh();
mProcessModel->setGuiWindowPids( UnorderedSet<long>( mGuiWindows.windowPids() ) );
}
// A full model reset clears the view's selection (SortingProxyModel drops it on every
// invalidation), so the chosen process is remembered by PID and re-selected afterwards. PIDs
// are stable across snapshots while row indexes are not: the table re-sorts on every update.
long selectedPid = getSelectedPid();
if ( mProcessModel )
mProcessModel->applySnapshot( std::move( processes ), sysInfo );
if ( !mProcessTableStateRestored && !mProcessTableStateRestoreScheduled && mApp &&
mApp->getUI() ) {
mProcessTableStateRestoreScheduled = true;
mApp->getUI()->runOnMainThread( [this] {
if ( !mProcessTableStateRestored ) {
restoreProcessTableState();
mProcessTableStateRestored = true;
}
mProcessTableStateRestoreScheduled = false;
} );
}
updateStatusBar();
restoreSelection( selectedPid );
}
long App::getSelectedPid() const {
if ( !mTableView || !mProcessModel )
return -1;
ModelIndex proxyIndex = mTableView->getSelection().first();
if ( !proxyIndex.isValid() )
return -1;
ModelIndex sourceIndex = mSortProxy ? mSortProxy->mapToSource( proxyIndex ) : proxyIndex;
const ProcessInfo* proc = mProcessModel->getProcessByRow( sourceIndex.row() );
return proc ? proc->pid : -1;
}
void App::restoreSelection( long pid ) {
if ( pid < 0 || !mTableView || !mProcessModel || !mSortProxy )
return;
int row = mProcessModel->rowForPid( pid );
if ( row < 0 )
return; // the process exited, or the filter no longer matches it
ModelIndex proxyIndex = mSortProxy->mapToProxy( mProcessModel->index( row, 0 ) );
if ( proxyIndex.isValid() )
mTableView->setSelection( proxyIndex, false );
}
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<Int32>( 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->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() {
#if EE_PLATFORM == EE_PLATFORM_LINUX
requestSignal( selectedPids(), SIGTERM,
mApp->getUI()->i18n( "eproc_end_process", "End Process" ).toUtf8(), true );
#endif
}
void App::onSearchChanged() {
if ( mSearchInput && mProcessModel ) {
mProcessModel->setTextFilter( mSearchInput->getText().toUtf8() );
updateStatusBar();
}
}
void App::onFilterChanged() {
if ( mFilterDropdown && mProcessModel ) {
// The dropdown is built in the same order as the enum, so the index maps directly.
Uint32 selected = mFilterDropdown->getListBox()->getItemSelectedIndex();
if ( selected < ProcessModel::FilterModeCount )
mProcessModel->setFilter( static_cast<ProcessModel::FilterMode>( selected ) );
updateStatusBar();
}
}
void App::onSelectionChange() {
// Update End Process button state
if ( mEndProcessBtn && mTableView ) {
mEndProcessBtn->setEnabled( !selectedPids().empty() );
}
}
const ProcessInfo* App::processForProxyIndex( const ModelIndex& proxyIndex ) const {
if ( !mProcessModel || !mSortProxy || !proxyIndex.isValid() )
return nullptr;
return mProcessModel->getProcessByRow( mSortProxy->mapToSource( proxyIndex ).row() );
}
std::vector<long> App::selectedPids() const {
std::vector<long> pids;
if ( !mProcessModel || !mTableView )
return pids;
for ( const auto& proxyIndex : mTableView->getSelection().indexes() ) {
const ProcessInfo* proc = processForProxyIndex( proxyIndex );
if ( proc && proc->status != ProcessStatus::Ended )
pids.push_back( proc->pid );
}
return pids;
}
void App::selectProcess( long pid ) {
if ( pid <= 0 || !mProcessModel || !mSortProxy || !mTableView )
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() ) {
mProcessModel->setTextFilter( "" );
mSearchInput->setText( "" );
row = mProcessModel->rowForPid( pid );
}
if ( row < 0 )
return;
ModelIndex proxyIndex = mSortProxy->mapToProxy( mProcessModel->index( row, 0 ) );
if ( proxyIndex.isValid() )
mTableView->setSelection( proxyIndex );
}
void App::requestSignal( std::vector<long> pids, int signal, const std::string& actionLabel,
bool confirm ) {
if ( pids.empty() )
return;
auto send = [pids, signal]() {
for ( long pid : pids )
sendProcessSignal( pid, signal );
};
if ( !confirm ) {
send();
return;
}
const std::string target =
pids.size() == 1
? String::format( mApp->getUI()->i18n( "eproc_process_target", "process %ld" ).toUtf8(),
pids.front() )
: String::format(
mApp->getUI()->i18n( "eproc_processes_target", "%zu processes" ).toUtf8(),
pids.size() );
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 ) {
if ( !mTableView )
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 ( proxyIndex.isValid() && !mTableView->getSelection().contains( proxyIndex ) )
mTableView->setSelection( proxyIndex, false );
const std::vector<long> pids = selectedPids();
if ( pids.empty() )
return;
const ProcessInfo* proc = processForProxyIndex( proxyIndex );
const long parentPid = proc ? proc->parentPid : 0;
const long 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
static const std::array<SignalItem, 8> 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 },
} };
#else
static const std::array<SignalItem, 0> signalItems{};
#endif
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 );
UIPopUpMenu* menu = UIPopUpMenu::New();
menu->setId( "process_context_menu" );
menu->addSubMenu( mApp->getUI()->i18n( "eproc_send_signal", "Send Signal" ), nullptr,
signalMenu )
->setId( "send-signal" );
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
menu->addSeparator();
menu->add( mApp->getUI()->i18n( "eproc_end_process", "End Process" ) )->setId( "end-process" );
menu->add( mApp->getUI()->i18n( "eproc_forcibly_kill_process", "Forcibly Kill Process" ) )
->setId( "kill-process" );
#endif
menu->on( Event::OnItemClicked, [this, pids, parentPid, tracerPid,
copyCommandLine =
std::move( copyCommandLine )]( const Event* event ) {
UIMenuItem* item = event->getNode()->asType<UIMenuItem>();
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 );
#if EE_PLATFORM == EE_PLATFORM_LINUX
} else if ( id == "end-process" ) {
requestSignal( pids, SIGTERM,
mApp->getUI()->i18n( "eproc_end_process", "End Process" ).toUtf8(),
true );
} else if ( id == "kill-process" ) {
requestSignal( pids, SIGKILL,
mApp->getUI()
->i18n( "eproc_forcibly_kill_process", "Forcibly Kill Process" )
.toUtf8(),
true );
#endif
} else {
// Signals picked explicitly from the submenu are sent straight away: the original
// only asks for confirmation on End Process and Forcibly Kill.
for ( const auto& sig : signalItems ) {
if ( id == sig.id ) {
requestSignal(
pids, sig.signal,
mApp->getUI()->i18n( "eproc_send_signal", "Send Signal" ).toUtf8(), false );
break;
}
}
}
} );
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, char*[] ) {
eproc::App app;
return app.run();
}
+157
View File
@@ -0,0 +1,157 @@
#ifndef EPROC_HPP
#define EPROC_HPP
#include "appconfig.hpp"
#include "gui_window_tracker.hpp"
#include "process_model.hpp"
#include <atomic>
#include <eepp/ee.hpp>
#include <eepp/system/clock.hpp>
#include <eepp/system/lock.hpp>
#include <eepp/system/mutex.hpp>
#include <eepp/system/threadpool.hpp>
#include <eepp/ui/models/sortingproxymodel.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uidropdownlist.hpp>
#include <eepp/ui/uimenubar.hpp>
#include <eepp/ui/uimessagebox.hpp>
#include <eepp/ui/uipopupmenu.hpp>
#include <eepp/ui/uipushbutton.hpp>
#include <eepp/ui/uitab.hpp>
#include <eepp/ui/uitableview.hpp>
#include <eepp/ui/uitabwidget.hpp>
#include <eepp/ui/uitextinput.hpp>
#include <eepp/ui/uitextview.hpp>
#include <memory>
using namespace EE;
using namespace EE::System;
using namespace EE::UI;
using namespace EE::UI::Models;
namespace eproc {
class App {
public:
App();
~App();
int run();
private:
bool init();
void setupUI();
void setupProcessTable();
/** Restores the saved display, position, size, and maximized state. */
void restoreWindowState();
/** Captures and writes the current window state. */
void saveWindowState();
/** Restores the process table's columns, widths, and sorting state. */
void restoreProcessTableState();
/** Serializes the process table's columns, widths, and sorting state. */
std::string serializeProcessTableState() const;
/** Saves the state before the primary window is closed. */
bool closeWindow( EE::Window::Window* window );
/** Creates the collector and worker, and dispatches the first sample. Called from the
* constructor so collection overlaps window creation. */
void startCollection();
/** Starts the poll/dispatch timer. Requires the UI to exist. */
void setupRefreshTimer();
/** Blocks up to @p timeoutMs until the first snapshot is staged. Called before the render
* loop, where blocking cannot stall a frame. */
bool waitForFirstSnapshot( Uint32 timeoutMs );
/** Collects the current snapshot and publishes it to the UI thread through the staging
* buffer. Runs on a worker thread; never touches the model or any Node. */
void collectAsync();
/** UI-thread tick: publishes any staged snapshot, then schedules the next collection. */
void onRefreshTick();
/** Moves a staged snapshot into the model, preserving the selected process by PID. UI thread
* only. */
void publishStagedSnapshot();
/** PID of the currently selected process, or -1. */
long getSelectedPid() const;
/** Re-selects the process with @p pid after a snapshot, so a refresh is invisible to the
* user. Does nothing when that process is gone. */
void restoreSelection( long pid );
void updateStatusBar();
void onEndProcess();
void onSearchChanged();
void onFilterChanged();
void onSelectionChange();
/** Builds and shows the process context menu for a right-clicked row. */
void showProcessContextMenu( const ModelIndex& proxyIndex );
/** PIDs of the currently selected rows. */
std::vector<long> selectedPids() const;
/** Maps a proxy index to the process behind it, or nullptr. */
const ProcessInfo* processForProxyIndex( const ModelIndex& proxyIndex ) const;
/** Sends @p signal to every pid in @p pids, asking for confirmation first when @p confirm. */
void requestSignal( std::vector<long> pids, int signal, const std::string& actionLabel,
bool confirm );
/** Selects the row holding @p pid and scrolls it into view. */
void selectProcess( long pid );
std::unique_ptr<AppConfig> mConfig;
std::unique_ptr<UIApplication> mApp;
UIWidget* mRoot{ nullptr };
UITabWidget* mTabWidget{ nullptr };
UIWidget* mProcessTableLayout{ nullptr };
UIPushButton* mEndProcessBtn{ nullptr };
UITextInput* mSearchInput{ nullptr };
UIDropDownList* mFilterDropdown{ nullptr };
UITableView* mTableView{ nullptr };
UITextView* mStatusText{ nullptr };
UITextView* mCpuText{ nullptr };
UITextView* mMemText{ nullptr };
UITextView* mSwapText{ nullptr };
std::shared_ptr<ProcessModel> mProcessModel;
std::shared_ptr<SortingProxyModel> mSortProxy;
bool mProcessTableStateRestored{ false };
bool mProcessTableStateRestoreScheduled{ false };
bool mWindowStateSaved{ false };
// Window ownership backs the Programs Only filter. Refreshed on the UI thread, since Xlib is
// not safe to drive from the collection worker.
GuiWindowTracker mGuiWindows;
// Collection runs on a worker thread so the render loop never blocks on /proc. The worker
// owns mCollector exclusively; the UI thread only ever reads staged snapshots.
std::unique_ptr<ProcessCollector> mCollector;
std::shared_ptr<ThreadPool> mThreadPool;
std::atomic<bool> mCollectInFlight{ false };
Mutex mStagingMutex;
std::vector<ProcessInfo> mStagedProcesses;
SystemInfo mStagedSystemInfo;
bool mStagedReady{ false };
Uint32 mUpdateIntervalMs{ 1000 };
// The publish poll runs much faster than the sampling cadence so a finished snapshot reaches
// the table immediately instead of waiting for the next sample tick.
Uint32 mTickIntervalMs{ 100 };
Clock mDispatchClock;
};
} // namespace eproc
#endif // EPROC_HPP
+242
View File
@@ -0,0 +1,242 @@
#include "gui_window_tracker.hpp"
#include <eepp/system/log.hpp>
#include <eepp/system/sys.hpp>
// Which processes own a top-level window is read through a backend selected at runtime, so the
// class keeps the very same interface whether or not X11 is available on the platform.
//
// - X11: the only backend implemented today. It relies on an X11 session, reading the EWMH
// properties the running window manager publishes on the root window. On Wayland it therefore
// only ever sees XWayland clients, since a native Wayland compositor does not expose its
// toplevels over X11. A Wayland backend would have to speak a compositor protocol instead
// (wlr-foreign-toplevel-management, or KDE's plasma-window-management) and is not implemented
// yet.
// - Anywhere else: the stub backend at the bottom of this file, which no X11 header or library is
// needed for.
//
// The X11 backend is compiled where eepp's config found the X11 headers and loads libX11 at
// runtime, which keeps the eproc project independent of an installed X11 runtime.
#if EE_PLATFORM == EE_PLATFORM_LINUX && defined( EE_X11_PLATFORM )
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#endif // EE_PLATFORM == EE_PLATFORM_LINUX && defined( EE_X11_PLATFORM )
using namespace EE::System;
namespace eproc {
#if EE_PLATFORM == EE_PLATFORM_LINUX && defined( EE_X11_PLATFORM )
// X11 backend -----------------------------------------------------------------------------------
struct GuiWindowTracker::X11Api {
using OpenDisplayFn = Display* ( * )( const char* );
using CloseDisplayFn = int ( * )( Display* );
using DefaultRootWindowFn = Window ( * )( Display* );
using InternAtomFn = Atom ( * )( Display*, const char*, Bool );
using GetWindowPropertyFn = int ( * )( Display*, Window, Atom, long, long, Bool, Atom, Atom*,
int*, unsigned long*, unsigned long*, unsigned char** );
using FreeFn = int ( * )( void* );
using SyncFn = int ( * )( Display*, Bool );
using SetErrorHandlerFn = XErrorHandler ( * )( XErrorHandler );
void* library{ nullptr };
OpenDisplayFn openDisplay{ nullptr };
CloseDisplayFn closeDisplay{ nullptr };
DefaultRootWindowFn defaultRootWindow{ nullptr };
InternAtomFn internAtom{ nullptr };
GetWindowPropertyFn getWindowProperty{ nullptr };
FreeFn freeMemory{ nullptr };
SyncFn sync{ nullptr };
SetErrorHandlerFn setErrorHandler{ nullptr };
~X11Api() {
if ( library )
Sys::unloadObject( library );
}
template <typename Function> Function resolve( const char* name ) {
return reinterpret_cast<Function>( Sys::loadFunction( library, name ) );
}
bool load() {
library = Sys::loadObject( "libX11.so.6" );
if ( !library )
library = Sys::loadObject( "libX11.so" );
if ( !library )
return false;
openDisplay = resolve<OpenDisplayFn>( "XOpenDisplay" );
closeDisplay = resolve<CloseDisplayFn>( "XCloseDisplay" );
defaultRootWindow = resolve<DefaultRootWindowFn>( "XDefaultRootWindow" );
internAtom = resolve<InternAtomFn>( "XInternAtom" );
getWindowProperty = resolve<GetWindowPropertyFn>( "XGetWindowProperty" );
freeMemory = resolve<FreeFn>( "XFree" );
sync = resolve<SyncFn>( "XSync" );
setErrorHandler = resolve<SetErrorHandlerFn>( "XSetErrorHandler" );
return openDisplay && closeDisplay && defaultRootWindow && internAtom &&
getWindowProperty && freeMemory && sync && setErrorHandler;
}
};
// A window list holds a handful of entries in practice. The request length is capped so that a
// malformed or hostile property can never make the monitor allocate an unbounded amount of memory.
static constexpr long MAX_PROPERTY_ITEMS = 1 << 16;
// Xlib terminates the process on any X error it is not told about, and a window listed in
// _NET_CLIENT_LIST can be destroyed by its owner between reading the list and reading that
// window's PID, in which case XGetWindowProperty reports BadWindow for a window that no longer
// exists. That race is expected here, so BadWindow is ignored, while any other error keeps the
// behaviour of the handler installed by the application (libX11's own handler when there is none).
static XErrorHandler sPreviousErrorHandler = nullptr;
static int ignoreMissingWindowError( Display* display, XErrorEvent* event ) {
if ( event && event->error_code == BadWindow )
return 0;
if ( sPreviousErrorHandler )
return sPreviousErrorHandler( display, event );
return 0;
}
// Fetches a 32-bit X property from @p window. On success returns the raw property data, to be
// released by the caller with XFree, and stores its item count in @p itemCount. Returns null -
// leaving @p itemCount at zero - when the property is missing or empty, has another type or
// format, or is longer than MAX_PROPERTY_ITEMS, in which case the reply would be truncated into an
// incomplete list.
template <typename X11Api>
static unsigned char* fetchProperty( X11Api& api, Display* display, Window window, Atom property,
Atom expectedType, unsigned long& itemCount ) {
itemCount = 0;
Atom actualType = None;
int actualFormat = 0;
unsigned long bytesAfter = 0;
unsigned char* data = nullptr;
if ( api.getWindowProperty( display, window, property, 0, MAX_PROPERTY_ITEMS, False,
expectedType, &actualType, &actualFormat, &itemCount, &bytesAfter,
&data ) != Success ||
!data ) {
itemCount = 0;
if ( data )
api.freeMemory( data );
return nullptr;
}
// A property stored under another type or format cannot be read as a list of ids.
if ( actualType != expectedType || actualFormat != 32 || itemCount == 0 || bytesAfter != 0 ) {
itemCount = 0;
api.freeMemory( data );
return nullptr;
}
return data;
}
GuiWindowTracker::GuiWindowTracker() {
mX11 = new X11Api();
if ( !mX11->load() ) {
delete mX11;
mX11 = nullptr;
return;
}
Display* display = mX11->openDisplay( nullptr );
if ( !display ) {
Log::warning(
"eproc: could not open an X11 display, no process will be reported as having a GUI "
"window" );
return;
}
mDisplay = display;
mRootWindow = mX11->defaultRootWindow( display );
}
GuiWindowTracker::~GuiWindowTracker() {
if ( mDisplay )
mX11->closeDisplay( static_cast<Display*>( mDisplay ) );
delete mX11;
}
void GuiWindowTracker::refresh() {
mPids.clear();
Display* display = static_cast<Display*>( mDisplay );
if ( !display )
return;
Atom clientListAtom = mX11->internAtom( display, "_NET_CLIENT_LIST", True );
Atom pidAtom = mX11->internAtom( display, "_NET_WM_PID", True );
if ( clientListAtom == None || pidAtom == None )
return;
XErrorHandler previousHandler = mX11->setErrorHandler( ignoreMissingWindowError );
unsigned long windowCount = 0;
unsigned long* windows = reinterpret_cast<unsigned long*>(
fetchProperty( *mX11, display, mRootWindow, clientListAtom, XA_WINDOW, windowCount ) );
if ( windows ) {
for ( unsigned long i = 0; i < windowCount; i++ ) {
if ( windows[i] == None )
continue;
unsigned long pidCount = 0;
unsigned long* pid = reinterpret_cast<unsigned long*>(
fetchProperty( *mX11, display, windows[i], pidAtom, XA_CARDINAL, pidCount ) );
if ( pid ) {
if ( pid[0] > 0 )
mPids.insert( static_cast<long>( pid[0] ) );
mX11->freeMemory( pid );
}
}
mX11->freeMemory( windows );
}
// Flush the requests issued above so that the errors they produced are handled here instead of
// reaching the application's handler once the previous one is restored.
mX11->sync( display, False );
mX11->setErrorHandler( previousHandler );
}
bool GuiWindowTracker::isAvailable() const {
return mDisplay != nullptr;
}
#else // X11 backend
// Stub backend ---------------------------------------------------------------------------------
// No window list can be read on this platform, so a tracker always reports itself unavailable and
// never lists a window. The bodies below exist to keep every method defined for every build.
GuiWindowTracker::GuiWindowTracker() {}
GuiWindowTracker::~GuiWindowTracker() {}
void GuiWindowTracker::refresh() {
mPids.clear();
}
bool GuiWindowTracker::isAvailable() const {
return false;
}
#endif // X11 backend
// Shared by every backend: with no backend built, mPids is always empty.
bool GuiWindowTracker::hasWindowForPid( long pid ) const {
return mPids.find( pid ) != mPids.end();
}
} // namespace eproc
+57
View File
@@ -0,0 +1,57 @@
#ifndef EPROC_GUI_WINDOW_TRACKER_HPP
#define EPROC_GUI_WINDOW_TRACKER_HPP
#include <eepp/config.hpp>
#include <eepp/core/containers.hpp>
using namespace EE;
namespace eproc {
/** @brief Tracks which processes own a top-level GUI window on the current display.
*
* This answers the "does this process have a GUI window?" question used by the process list
* filters (the equivalent of ksysguard's hasGUIWindow()). The X11 backend reads the
* _NET_CLIENT_LIST property of the root window and the owner of each window from its _NET_WM_PID
* property. Both are EWMH extensions published by the running window manager, so on a display
* without a window manager, or without an X display at all, the tracker simply reports that no
* process owns a window. X11 is loaded at runtime when available, keeping it optional for
* Wayland and headless systems. Where no backend is built, the tracker compiles to a stub that
* reports itself unavailable and lists no window PIDs.
*
* The Xlib display is held as an opaque pointer, keeping this header free of X11 headers. */
class GuiWindowTracker {
public:
GuiWindowTracker();
~GuiWindowTracker();
GuiWindowTracker( const GuiWindowTracker& ) = delete;
GuiWindowTracker& operator=( const GuiWindowTracker& ) = delete;
/** Re-reads the window list and the PIDs owning windows. MAIN THREAD ONLY. */
void refresh();
/** True when the last refresh() saw a top-level window owned by @p pid. */
bool hasWindowForPid( long pid ) const;
/** PIDs that owned a top-level window at the last refresh(). */
const UnorderedSet<long>& windowPids() const { return mPids; }
/** True when the backend connected to a display and can list the windows it owns. */
bool isAvailable() const;
private:
UnorderedSet<long> mPids;
// State of the X11 backend. Builds without X11 headers carry no state no backend could fill and
// are still fully usable: they report no window for any process.
#if EE_PLATFORM == EE_PLATFORM_LINUX && defined( EE_X11_PLATFORM )
struct X11Api;
X11Api* mX11{ nullptr };
void* mDisplay{ nullptr }; // Display*
unsigned long mRootWindow{ 0 };
#endif
};
} // namespace eproc
#endif // EPROC_GUI_WINDOW_TRACKER_HPP
@@ -0,0 +1,211 @@
#include "gpu_reader_nvidia.hpp"
#include <eepp/system/sys.hpp>
#include <algorithm>
using namespace EE::System;
namespace eproc {
namespace {
// Return codes and sentinels mirrored from nvml.h.
constexpr int NvmlSuccess = 0;
constexpr int NvmlErrorInsufficientSize = 7;
constexpr unsigned long long NvmlValueNotAvailable = ~0ULL;
// GPU memory is reported in bytes, the readers of this tool work in KiB.
constexpr unsigned long long BytesPerKiB = 1024;
// Bounds the retries of the two call queries, in case a device reports more entries than the
// previous call reserved room for.
constexpr int MaxQueryAttempts = 4;
template <typename Fn> Fn resolveSymbol( void* lib, const char* name ) {
return reinterpret_cast<Fn>( Sys::loadFunction( lib, name ) );
}
} // namespace
NvidiaGpuReader::NvidiaGpuReader() {
mLib = Sys::loadObject( "libnvidia-ml.so.1" );
if ( !mLib )
mLib = Sys::loadObject( "libnvidia-ml.so" );
// No driver installed: stay unavailable, the tool keeps working without GPU information.
if ( !mLib )
return;
mNvmlInit = resolveSymbol<NvmlInitFn>( mLib, "nvmlInit" );
mNvmlShutdown = resolveSymbol<NvmlShutdownFn>( mLib, "nvmlShutdown" );
mNvmlDeviceGetCount = resolveSymbol<NvmlDeviceGetCountFn>( mLib, "nvmlDeviceGetCount" );
mNvmlDeviceGetHandleByIndex =
resolveSymbol<NvmlDeviceGetHandleByIndexFn>( mLib, "nvmlDeviceGetHandleByIndex" );
mNvmlDeviceGetProcessUtilization =
resolveSymbol<NvmlDeviceGetProcessUtilizationFn>( mLib, "nvmlDeviceGetProcessUtilization" );
mNvmlComputeProcesses = resolveSymbol<NvmlDeviceGetRunningProcessesFn>(
mLib, "nvmlDeviceGetComputeRunningProcesses" );
mNvmlGraphicsProcesses = resolveSymbol<NvmlDeviceGetRunningProcessesFn>(
mLib, "nvmlDeviceGetGraphicsRunningProcesses" );
// Optional entry point, drivers that do not support MPS do not export it.
mNvmlMpsComputeProcesses = resolveSymbol<NvmlDeviceGetRunningProcessesFn>(
mLib, "nvmlDeviceGetMPSComputeRunningProcesses" );
if ( !mNvmlInit || !mNvmlShutdown || !mNvmlDeviceGetCount || !mNvmlDeviceGetHandleByIndex ||
!mNvmlDeviceGetProcessUtilization || !mNvmlComputeProcesses || !mNvmlGraphicsProcesses )
return;
if ( mNvmlInit() != NvmlSuccess )
return;
mInitialized = true;
}
NvidiaGpuReader::~NvidiaGpuReader() {
if ( mInitialized && mNvmlShutdown )
mNvmlShutdown();
if ( mLib )
Sys::unloadObject( mLib );
}
bool NvidiaGpuReader::isAvailable() const {
return mInitialized;
}
void NvidiaGpuReader::query( UnorderedMap<long, int>& usagePercent,
UnorderedMap<long, long>& memoryKiB ) {
usagePercent.clear();
memoryKiB.clear();
if ( !mInitialized )
return;
// The device count is read on every query, GPUs can come and go while the tool runs.
unsigned int deviceCount = 0;
if ( mNvmlDeviceGetCount( &deviceCount ) != NvmlSuccess )
return;
unsigned long long newestTimestamp = mLastTimestamp;
for ( unsigned int index = 0; index < deviceCount; index++ ) {
NvmlDevice device = nullptr;
// A device that cannot be queried is skipped, the remaining ones are still reported.
if ( mNvmlDeviceGetHandleByIndex( index, &device ) != NvmlSuccess || !device )
continue;
collectUtilization( device, usagePercent, newestTimestamp );
collectMemory( device, memoryKiB );
}
// Only samples newer than this timestamp are requested on the next query.
mLastTimestamp = newestTimestamp;
}
void NvidiaGpuReader::collectUtilization( NvmlDevice device, UnorderedMap<long, int>& usagePercent,
unsigned long long& newestTimestamp ) {
// The first call only asks for the number of samples the device has since mLastTimestamp.
unsigned int count = 0;
if ( mNvmlDeviceGetProcessUtilization( device, nullptr, &count, mLastTimestamp ) !=
NvmlErrorInsufficientSize ||
count == 0 )
return;
int result = NvmlErrorInsufficientSize;
unsigned int filled = 0;
for ( int attempt = 0; attempt < MaxQueryAttempts; attempt++ ) {
mSampleBuffer.resize( count );
filled = count;
result = mNvmlDeviceGetProcessUtilization( device, mSampleBuffer.data(), &filled,
mLastTimestamp );
if ( result != NvmlErrorInsufficientSize )
break;
// The device produced even more samples in the meantime: retry with the reported size.
if ( filled <= count )
return;
count = filled;
}
if ( result != NvmlSuccess )
return;
const unsigned int samples =
std::min<unsigned int>( filled, static_cast<unsigned int>( mSampleBuffer.size() ) );
for ( unsigned int i = 0; i < samples; i++ ) {
const ProcessUtilizationSample& sample = mSampleBuffer[i];
if ( sample.timeStamp > newestTimestamp )
newestTimestamp = sample.timeStamp;
const long pid = static_cast<long>( sample.pid );
const int utilization = static_cast<int>( sample.smUtil );
auto found = usagePercent.find( pid );
if ( found == usagePercent.end() )
usagePercent[pid] = utilization;
else if ( utilization > found->second )
found->second = utilization;
}
}
void NvidiaGpuReader::collectMemory( NvmlDevice device, UnorderedMap<long, long>& memoryKiB ) {
collectRunningProcesses( device, mNvmlComputeProcesses, memoryKiB );
collectRunningProcesses( device, mNvmlGraphicsProcesses, memoryKiB );
// MPS compute processes are only reported by their own query, and the entry point is missing on
// drivers without MPS support.
if ( mNvmlMpsComputeProcesses )
collectRunningProcesses( device, mNvmlMpsComputeProcesses, memoryKiB );
}
void NvidiaGpuReader::collectRunningProcesses( NvmlDevice device,
NvmlDeviceGetRunningProcessesFn entryPoint,
UnorderedMap<long, long>& memoryKiB ) {
if ( !entryPoint )
return;
// The first call only asks for the number of processes the device reports.
unsigned int count = 0;
if ( entryPoint( device, &count, nullptr ) != NvmlErrorInsufficientSize || count == 0 )
return;
int result = NvmlErrorInsufficientSize;
unsigned int filled = 0;
for ( int attempt = 0; attempt < MaxQueryAttempts; attempt++ ) {
mProcessBuffer.resize( count );
filled = count;
result = entryPoint( device, &filled, mProcessBuffer.data() );
if ( result != NvmlErrorInsufficientSize )
break;
// More processes appeared in the meantime: retry with the reported size.
if ( filled <= count )
return;
count = filled;
}
if ( result != NvmlSuccess )
return;
const unsigned int processes =
std::min<unsigned int>( filled, static_cast<unsigned int>( mProcessBuffer.size() ) );
for ( unsigned int i = 0; i < processes; i++ ) {
const RunningProcessInfo& process = mProcessBuffer[i];
// Windows and some of the older drivers do not report the used memory of a process.
if ( process.usedGpuMemory == NvmlValueNotAvailable )
continue;
// A process can be listed by more than one query (compute, graphics and MPS) and by more
// than one device; the reported usage is accumulated because that sum is the process's
// overall GPU memory, which is what nvtop reports.
//
// Do NOT "de-duplicate" this with a per-query maximum: a process that appears in both the
// compute and graphics lists genuinely uses the memory reported by each, and taking the
// maximum would halve the figure for exactly those processes. ksysguard6 accumulates the
// same way.
memoryKiB[static_cast<long>( process.pid )] +=
static_cast<long>( process.usedGpuMemory / BytesPerKiB );
}
}
} // namespace eproc
@@ -0,0 +1,98 @@
#ifndef EPROC_GPU_READER_NVIDIA_HPP
#define EPROC_GPU_READER_NVIDIA_HPP
#include <eepp/core/containers.hpp>
#include <vector>
using namespace EE;
namespace eproc {
/** Per-process GPU utilization and GPU memory of the NVIDIA devices in the system.
*
* The NVIDIA Management Library is loaded at run time, so the tool builds and runs on machines
* without an NVIDIA driver: an unavailable reader simply reports no GPU information. */
class NvidiaGpuReader {
public:
NvidiaGpuReader();
~NvidiaGpuReader();
NvidiaGpuReader( const NvidiaGpuReader& ) = delete;
NvidiaGpuReader& operator=( const NvidiaGpuReader& ) = delete;
/** True when libnvidia-ml loaded and nvmlInit() succeeded. */
bool isAvailable() const;
/** Fills per-PID GPU utilization in percent (0..100) and per-PID GPU memory in KiB.
* Entries are only present for processes actually using the GPU. */
void query( UnorderedMap<long, int>& usagePercent, UnorderedMap<long, long>& memoryKiB );
private:
// Only the NVML declarations this reader uses are mirrored here, the rest of the API lives in
// the driver's own nvml.h which is deliberately not vendored.
using NvmlDevice = void*;
using NvmlReturn = int;
// nvmlProcessUtilizationSample_t: process id, sample timestamp in microseconds and the
// per-engine utilization values in percent.
struct ProcessUtilizationSample {
unsigned int pid;
unsigned long long timeStamp;
unsigned int smUtil;
unsigned int memUtil;
unsigned int encUtil;
unsigned int decUtil;
};
static_assert( sizeof( ProcessUtilizationSample ) == 32,
"must match nvmlProcessUtilizationSample_t" );
// nvmlProcessInfo_v1_t: the structure filled by the version-less running-process queries, a
// process id followed by the device memory that process uses, in bytes.
struct RunningProcessInfo {
unsigned int pid;
unsigned long long usedGpuMemory;
};
static_assert( sizeof( RunningProcessInfo ) == 16, "must match nvmlProcessInfo_v1_t" );
using NvmlInitFn = NvmlReturn ( * )();
using NvmlShutdownFn = NvmlReturn ( * )();
using NvmlDeviceGetCountFn = NvmlReturn ( * )( unsigned int* );
using NvmlDeviceGetHandleByIndexFn = NvmlReturn ( * )( unsigned int, NvmlDevice* );
using NvmlDeviceGetProcessUtilizationFn = NvmlReturn ( * )( NvmlDevice,
ProcessUtilizationSample*,
unsigned int*, unsigned long long );
using NvmlDeviceGetRunningProcessesFn = NvmlReturn ( * )( NvmlDevice, unsigned int*,
RunningProcessInfo* );
/** Adds the utilization of the processes sampled on one device, keeping the highest value seen
* per process and refreshing newestTimestamp with the newest sample returned. */
void collectUtilization( NvmlDevice device, UnorderedMap<long, int>& usagePercent,
unsigned long long& newestTimestamp );
/** Adds the GPU memory of the processes running on one device, summed per process. */
void collectMemory( NvmlDevice device, UnorderedMap<long, long>& memoryKiB );
void collectRunningProcesses( NvmlDevice device, NvmlDeviceGetRunningProcessesFn entryPoint,
UnorderedMap<long, long>& memoryKiB );
void* mLib{ nullptr };
bool mInitialized{ false };
unsigned long long mLastTimestamp{ 0 };
// resolved function pointers
NvmlInitFn mNvmlInit{ nullptr };
NvmlShutdownFn mNvmlShutdown{ nullptr };
NvmlDeviceGetCountFn mNvmlDeviceGetCount{ nullptr };
NvmlDeviceGetHandleByIndexFn mNvmlDeviceGetHandleByIndex{ nullptr };
NvmlDeviceGetProcessUtilizationFn mNvmlDeviceGetProcessUtilization{ nullptr };
NvmlDeviceGetRunningProcessesFn mNvmlComputeProcesses{ nullptr };
NvmlDeviceGetRunningProcessesFn mNvmlGraphicsProcesses{ nullptr };
// Optional: drivers without MPS support do not export this entry point.
NvmlDeviceGetRunningProcessesFn mNvmlMpsComputeProcesses{ nullptr };
// Reused across queries, so polling does not allocate per device.
std::vector<ProcessUtilizationSample> mSampleBuffer;
std::vector<RunningProcessInfo> mProcessBuffer;
};
} // namespace eproc
#endif // EPROC_GPU_READER_NVIDIA_HPP
@@ -0,0 +1,662 @@
#include "process_collector_linux.hpp"
#include <eepp/core/string.hpp>
#include <eepp/system/fileinfo.hpp>
#include <dirent.h>
#include <fcntl.h>
#include <pwd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <charconv>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string_view>
using namespace EE::System;
namespace eproc {
namespace {
// /proc pseudo-files are read several times per process on every pass. Building the paths with
// string concatenation and reading them through std::ifstream allocated a path string plus the
// stream's own multi-kilobyte buffer per file; formatting into a stack buffer and issuing a single
// read(2) keeps the heap untouched.
constexpr size_t kProcPathCapacity = 64;
constexpr size_t kProcFileCapacity = 8192;
/** How often (in passes) the per-pid caches are swept of processes that exited. A sweep walks the
* whole map, so it is amortised over many passes instead of running on every one. */
constexpr Uint32 kCacheSweepInterval = 60;
/** Formats "/proc/<pid>/<leaf>" into @p out. Returns the length, or 0 on overflow. */
inline size_t formatProcPath( char* out, size_t capacity, long pid, const char* leaf ) {
int written = snprintf( out, capacity, "/proc/%ld/%s", pid, leaf );
return written > 0 && static_cast<size_t>( written ) < capacity ? static_cast<size_t>( written )
: 0;
}
/** Reads a small pseudo-file into @p out, NUL-terminated. Larger files are truncated, which is
* fine for the fields parsed here. */
inline bool readProcFile( const char* path, char* out, size_t capacity, size_t& length ) {
length = 0;
int fd = ::open( path, O_RDONLY | O_CLOEXEC );
if ( fd < 0 )
return false;
ssize_t readBytes = ::read( fd, out, capacity - 1 );
::close( fd );
if ( readBytes <= 0 )
return false;
length = static_cast<size_t>( readBytes );
out[length] = '\0';
return true;
}
/** Skips the whitespace that follows a "Label:" separator. */
inline const char* skipSeparator( std::string_view line ) {
size_t colon = line.find( ':' );
if ( colon == std::string_view::npos )
return nullptr;
const char* p = line.data() + colon + 1;
const char* end = line.data() + line.size();
while ( p < end && ( *p == ' ' || *p == '\t' ) )
++p;
return p;
}
// /proc files pad values with spaces (meminfo) or tabs (status); the parsers below tolerate either
// separator so one implementation reads both formats.
/** Returns the first integer following the ':' of a "Label: value" line, or 0 when absent. */
inline long parseLabeledLong( std::string_view line ) {
const char* p = skipSeparator( line );
return p ? strtol( p, nullptr, 10 ) : 0;
}
/** Reads up to @p count whitespace-separated integers following the ':' of a "Label: v v v" line.
* Used for the status "Uid:" line, which carries the real, effective, saved and fs ids. */
inline void parseLabeledLongs( std::string_view line, long* out, int count ) {
const char* p = skipSeparator( line );
if ( !p )
return;
const char* end = line.data() + line.size();
for ( int i = 0; i < count; ++i ) {
while ( p < end && ( *p == ' ' || *p == '\t' ) )
++p;
if ( p >= end )
return;
long value = 0;
auto result = std::from_chars( p, end, value );
if ( result.ec != std::errc() )
return;
out[i] = value;
p = result.ptr;
}
}
/** Returns the trimmed text following the ':' of a "Label: value" line, as a view into it. */
inline std::string_view parseLabeledString( std::string_view line ) {
const char* p = skipSeparator( line );
if ( !p )
return {};
std::string_view value( p, line.data() + line.size() - p );
return String::trim( value, " \t\r\n" );
}
std::string ttyName( long ttyNumber ) {
if ( ttyNumber == 0 )
return {};
const unsigned long device = static_cast<unsigned long>( ttyNumber );
const unsigned int major = static_cast<unsigned int>( ( device >> 8 ) & 0xff );
const unsigned int minor = static_cast<unsigned int>( device & 0xff );
char buffer[32];
if ( major == 136 )
snprintf( buffer, sizeof( buffer ), "pts/%u", minor );
else if ( major == 4 )
snprintf( buffer, sizeof( buffer ), minor < 64 ? "tty/%u" : "ttyS/%u",
minor < 64 ? minor : minor - 64 );
else
return {};
return buffer;
}
/** Resolves the executable behind a pid into @p out. Returns an empty string for kernel threads
* and for processes the caller may not inspect. The " (deleted)" suffix readlink adds for an
* unlinked executable is left in place: ProcessIconResolver strips it when matching names. */
std::string readExePath( long pid, char* out, size_t capacity ) {
char linkPath[kProcPathCapacity];
if ( 0 == formatProcPath( linkPath, sizeof( linkPath ), pid, "exe" ) )
return {};
ssize_t length = readlink( linkPath, out, capacity - 1 );
if ( length <= 0 )
return {};
return std::string( out, static_cast<size_t>( length ) );
}
// ksysguard treats an account as a system account when its shell cannot be used to log in.
bool isLoginShell( const char* shell ) {
if ( !shell || !*shell )
return false;
const char* base = strrchr( shell, '/' );
base = base ? base + 1 : shell;
return strcmp( base, "nologin" ) != 0 && strcmp( base, "false" ) != 0;
}
} // namespace
ProcessCollectorLinux::ProcessCollectorLinux() {
mPageSizeKb = sysconf( _SC_PAGESIZE ) / 1024;
mProcessorCount = sysconf( _SC_NPROCESSORS_ONLN );
if ( mProcessorCount < 1 )
mProcessorCount = 1;
mJiffiesPerSecond = sysconf( _SC_CLK_TCK );
if ( mJiffiesPerSecond < 1 )
mJiffiesPerSecond = 100;
// Require roughly a fifth of a second of machine-wide time before trusting a CPU delta.
mMinSampleJiffies = static_cast<long long>( mProcessorCount ) * mJiffiesPerSecond / 5;
}
ProcessCollectorLinux::~ProcessCollectorLinux() {}
bool ProcessCollectorLinux::readCpuTimes( long long& idle, long long& total ) {
idle = 0;
total = 0;
char buffer[kProcFileCapacity];
size_t length = 0;
if ( !readProcFile( "/proc/stat", buffer, sizeof( buffer ), length ) )
return false;
long long user = 0, nice = 0, system = 0, idleVal = 0, iowait = 0, irq = 0, softirq = 0,
steal = 0;
// The leading "cpu" aggregate line has at least the first four counters on every kernel.
if ( sscanf( buffer, "cpu %lld %lld %lld %lld %lld %lld %lld %lld", &user, &nice, &system,
&idleVal, &iowait, &irq, &softirq, &steal ) < 4 )
return false;
idle = idleVal + iowait;
total = user + nice + system + idleVal + iowait + irq + softirq + steal;
return true;
}
// Parses /proc/[pid]/stat — extracts: name, state, ppid, utime, stime, nice, num_threads,
// vsize (bytes), rss (pages). Returns false if the line can't be parsed.
bool ProcessCollectorLinux::readProcessStat( ProcessInfo& proc, const char* statLine ) {
const char* p = statLine;
// pid
while ( *p && *p != ' ' )
p++;
if ( !*p )
return false;
p++; // skip space
// comm — may contain spaces/parens, so find the last ')'
if ( *p != '(' )
return false;
p++;
const char* end = strrchr( p, ')' );
if ( !end || *( end + 1 ) != ' ' )
return false;
proc.name.assign( p, end - p );
p = end + 2; // skip ') '
// state
if ( !*p )
return false;
switch ( *p ) {
case 'R':
proc.status = ProcessStatus::Running;
break;
case 'S':
proc.status = ProcessStatus::Sleeping;
break;
case 'D':
proc.status = ProcessStatus::DiskSleep;
break;
case 'Z':
proc.status = ProcessStatus::Zombie;
break;
case 'T':
case 't':
proc.status = ProcessStatus::Stopped;
break;
case 'W':
proc.status = ProcessStatus::Paging;
break;
default:
proc.status = ProcessStatus::Other;
break;
}
p++; // past state char
if ( *p )
p++; // past space
// Now we're at field 4 (1-indexed): ppid
// Fields: ppid(4) pgrp(5) session(6) tty_nr(7) tpgid(8) flags(9)
// minflt(10) cminflt(11) majflt(12) cmajflt(13) utime(14) stime(15)
// cutime(16) cstime(17) priority(18) nice(19) num_threads(20)
// itrealvalue(21) starttime(22) vsize(23) rss(24)
auto skipField = [&]() {
while ( *p && *p != ' ' )
p++;
if ( *p )
p++;
};
// ppid (field 4)
proc.parentPid = strtol( p, nullptr, 10 );
skipField();
// pgrp(5), session(6)
skipField();
skipField();
// tty_nr (field 7) — the controlling terminal; 0 means none. Used by the "Programs Only"
// filter, which mirrors ksysguard's tty test.
proc.ttyNr = strtol( p, nullptr, 10 );
skipField();
// tpgid(8), flags(9)
skipField();
skipField();
// minflt(10), cminflt(11), majflt(12), cmajflt(13) — skip 4 fields
for ( int i = 0; i < 4; i++ )
skipField();
// utime (field 14)
proc.userTime = strtol( p, nullptr, 10 );
skipField();
// stime (field 15)
proc.sysTime = strtol( p, nullptr, 10 );
skipField();
// cutime(16), cstime(17), priority(18) — skip 3 fields
for ( int i = 0; i < 3; i++ )
skipField();
// nice (field 19)
proc.niceLevel = strtol( p, nullptr, 10 );
skipField();
// num_threads (field 20)
proc.numThreads = strtol( p, nullptr, 10 );
skipField();
// itrealvalue (field 21) — skip
skipField();
// starttime (field 22) — the process's start, in clock ticks since boot
proc.startTime = strtoll( p, nullptr, 10 );
skipField();
// vsize (field 23) — bytes
proc.vmSize = strtol( p, nullptr, 10 ) / 1024;
skipField();
// rss (field 24) — pages; mPageSizeKb is already KiB per page
if ( *p )
proc.vmRSS = strtol( p, nullptr, 10 ) * mPageSizeKb;
proc.tty = ttyName( proc.ttyNr );
return true;
}
void ProcessCollectorLinux::readProcessStatus( ProcessInfo& proc, const char* content,
size_t length ) {
String::readBySeparator( std::string_view( content, length ), [&proc]( std::string_view line ) {
if ( String::startsWith( line, "Uid:" ) ) {
long ids[4] = { 0, 0, 0, 0 };
parseLabeledLongs( line, ids, 4 );
proc.uid = ids[0];
proc.euid = ids[1];
proc.suid = ids[2];
proc.fsuid = ids[3];
} else if ( String::startsWith( line, "TracerPid:" ) ) {
proc.tracerPid = parseLabeledLong( line );
} else if ( String::startsWith( line, "VmRSS:" ) ) {
long rss = parseLabeledLong( line );
if ( rss > 0 )
proc.vmRSS = rss;
} else if ( String::startsWith( line, "RssFile:" ) ) {
proc.sharedMem += parseLabeledLong( line );
proc.hasSharedInfo = true;
} else if ( String::startsWith( line, "RssShmem:" ) ) {
proc.sharedMem += parseLabeledLong( line );
proc.hasSharedInfo = true;
} else if ( String::startsWith( line, "Name:" ) ) {
if ( proc.name.empty() )
proc.name.assign( parseLabeledString( line ) );
}
} );
}
void ProcessCollectorLinux::readProcessCmdline( ProcessInfo& proc, long pid ) {
char path[kProcPathCapacity];
if ( 0 == formatProcPath( path, sizeof( path ), pid, "cmdline" ) )
return;
int fd = ::open( path, O_RDONLY | O_CLOEXEC );
if ( fd < 0 )
return;
char buffer[kProcFileCapacity];
proc.commandLine.clear();
for ( ;; ) {
ssize_t bytes = ::read( fd, buffer, sizeof( buffer ) );
if ( bytes <= 0 )
break;
proc.commandLine.append( buffer, static_cast<size_t>( bytes ) );
}
::close( fd );
if ( proc.commandLine.empty() )
return;
// argv entries are NUL separated. Keep the executable separately for the table, then replace
// the separators with spaces for a useful clipboard command line.
proc.command.clear();
const size_t firstArgEnd = proc.commandLine.find( '\0' );
proc.command.assign( proc.commandLine.data(),
firstArgEnd == std::string::npos ? proc.commandLine.size() : firstArgEnd );
for ( char& character : proc.commandLine ) {
if ( character == '\0' )
character = ' ';
}
while ( !proc.commandLine.empty() && proc.commandLine.back() == ' ' )
proc.commandLine.pop_back();
}
void ProcessCollectorLinux::readProcessIO( ProcessInfo& proc, long pid ) {
char path[kProcPathCapacity];
if ( 0 == formatProcPath( path, sizeof( path ), pid, "io" ) )
return;
char buffer[kProcFileCapacity];
size_t length = 0;
if ( !readProcFile( path, buffer, sizeof( buffer ), length ) )
return;
String::readBySeparator( std::string_view( buffer, length ), [&proc]( std::string_view line ) {
const char* value = nullptr;
if ( String::startsWith( line, "read_bytes:" ) ) {
value = skipSeparator( line );
if ( value )
proc.ioReadBytes = strtoll( value, nullptr, 10 );
} else if ( String::startsWith( line, "write_bytes:" ) ) {
value = skipSeparator( line );
if ( value )
proc.ioWriteBytes = strtoll( value, nullptr, 10 );
}
} );
}
void ProcessCollectorLinux::readSystemMemory( SystemInfo& sysInfo ) {
char buffer[kProcFileCapacity];
size_t length = 0;
if ( !readProcFile( "/proc/meminfo", buffer, sizeof( buffer ), length ) )
return;
String::readBySeparator( std::string_view( buffer, length ),
[&sysInfo]( std::string_view line ) {
if ( String::startsWith( line, "MemTotal:" ) ) {
sysInfo.totalMemory = parseLabeledLong( line );
} else if ( String::startsWith( line, "MemFree:" ) ) {
sysInfo.freeMemory = parseLabeledLong( line );
} else if ( String::startsWith( line, "MemAvailable:" ) ) {
sysInfo.availableMemory = parseLabeledLong( line );
} else if ( String::startsWith( line, "SwapTotal:" ) ) {
sysInfo.totalSwap = parseLabeledLong( line );
} else if ( String::startsWith( line, "SwapFree:" ) ) {
sysInfo.freeSwap = parseLabeledLong( line );
}
} );
}
void ProcessCollectorLinux::readSystemUptime( SystemInfo& sysInfo ) {
char buffer[kProcFileCapacity];
size_t length = 0;
if ( !readProcFile( "/proc/uptime", buffer, sizeof( buffer ), length ) )
return;
char* end = nullptr;
const double uptime = strtod( buffer, &end );
if ( end != buffer && uptime >= 0 )
sysInfo.uptimeSeconds = uptime;
}
void ProcessCollectorLinux::resolveUser( ProcessInfo& proc ) {
// Insert first, then read: an insertion can rehash the cache, so no reference may be held
// across a lookup for a second uid.
userInfo( proc.uid );
if ( proc.euid != proc.uid )
userInfo( proc.euid );
const UserInfo& real = mUserCache.at( proc.uid );
proc.username = real.name;
proc.canLogin = real.canLogin;
proc.euidCanLogin = proc.euid == proc.uid ? real.canLogin : mUserCache.at( proc.euid ).canLogin;
}
const ProcessCollectorLinux::UserInfo& ProcessCollectorLinux::userInfo( long uid ) {
auto it = mUserCache.find( uid );
if ( it != mUserCache.end() )
return it->second;
UserInfo info;
struct passwd* pw = getpwuid( static_cast<uid_t>( uid ) );
if ( pw ) {
info.name = pw->pw_name ? pw->pw_name : std::to_string( uid );
info.canLogin = isLoginShell( pw->pw_shell );
} else {
info.name = std::to_string( uid );
info.canLogin = false;
}
return mUserCache.emplace( uid, std::move( info ) ).first->second;
}
bool ProcessCollectorLinux::collect( std::vector<ProcessInfo>& processes, SystemInfo& sysInfo ) {
// 0 is the "never seen" sentinel for the per-pid caches below.
++mPass;
if ( mPass == 0 )
++mPass;
// CPU usage needs two samples separated by a real interval. A sub-jiffy window quantises into
// noise (a few busy jiffies out of a few total reads as ~100%), and the very first sample has
// no reference point, so both keep the last known value instead of inventing one.
long long idle = 0, total = 0;
long long deltaTotal = 0;
long long deltaIdle = 0;
if ( readCpuTimes( idle, total ) ) {
if ( mPrevTotalCpu > 0 && total > mPrevTotalCpu ) {
deltaTotal = total - mPrevTotalCpu;
deltaIdle = idle - mPrevIdleCpu;
}
mPrevTotalCpu = total;
mPrevIdleCpu = idle;
}
const bool haveSample = deltaTotal >= mMinSampleJiffies;
if ( haveSample )
mLastCpuUsage = static_cast<float>( deltaTotal - deltaIdle ) / deltaTotal * 100.f;
sysInfo.cpuUsage = mLastCpuUsage;
sysInfo.cpuCount = mProcessorCount;
// System memory
readSystemMemory( sysInfo );
sysInfo.clockTicksPerSecond = mJiffiesPerSecond;
readSystemUptime( sysInfo );
// GPU figures are per-PID and driver-provided, so query them once per pass and apply below.
// The maps are members so the per-pass queries do not reallocate them.
mGpuUsage.clear();
mGpuMemory.clear();
if ( mGpuReader.isAvailable() )
mGpuReader.query( mGpuUsage, mGpuMemory );
// Network packet capture runs continuously in its own thread; this refresh only joins the
// current procfs socket ownership with the endpoints seen by that capture thread.
mNetworkMonitor.refreshMapping();
DIR* procDir = opendir( "/proc" );
if ( !procDir )
return false;
processes.clear();
struct dirent* entry;
while ( ( entry = readdir( procDir ) ) != nullptr ) {
// Check if directory entry is a PID
char* endptr = nullptr;
long pid = strtol( entry->d_name, &endptr, 10 );
if ( *endptr != '\0' || pid <= 0 )
continue;
// /proc reports DT_DIR, but other filesystems may return DT_UNKNOWN; verify those.
if ( entry->d_type == DT_UNKNOWN ) {
char dirPath[kProcPathCapacity];
int written = snprintf( dirPath, sizeof( dirPath ), "/proc/%ld", pid );
if ( written <= 0 || !FileInfo( dirPath ).isDirectory() )
continue;
} else if ( entry->d_type != DT_DIR ) {
continue;
}
ProcessInfo proc;
proc.pid = pid;
char path[kProcPathCapacity];
char buffer[kProcFileCapacity];
size_t length = 0;
// /proc/[pid]/stat — name, state, ppid, utime, stime, nice, threads, vsize, rss
if ( 0 == formatProcPath( path, sizeof( path ), pid, "stat" ) ||
!readProcFile( path, buffer, sizeof( buffer ), length ) )
continue;
if ( !readProcessStat( proc, buffer ) )
continue;
// /proc/[pid]/status — uid variants, VmRSS, the shared breakdown and TracerPid
if ( 0 != formatProcPath( path, sizeof( path ), pid, "status" ) &&
readProcFile( path, buffer, sizeof( buffer ), length ) )
readProcessStatus( proc, buffer, length );
// ksysguard's Memory column is the process's private memory: resident memory minus the
// pages it shares with other processes. Only derived when the kernel reported the shared
// breakdown; otherwise vmURSS stays -1 and the display falls back to RSS.
if ( proc.hasSharedInfo )
proc.vmURSS = proc.vmRSS - proc.sharedMem;
// CPU usage delta against the previous pass for this PID. Entries are updated in place and
// swept periodically, so the steady state does not allocate. A pid whose start time
// changed is a different process that reused the number, so it starts from scratch.
long long curTicks = proc.userTime + proc.sysTime;
TickEntry& tickEntry = mProcessTicks[pid];
const bool sameProcess = tickEntry.startTime == proc.startTime;
const bool hadPrevious = sameProcess && tickEntry.pass == mPass - 1;
long long deltaTicks = hadPrevious ? curTicks - tickEntry.ticks : 0;
tickEntry.ticks = curTicks;
tickEntry.startTime = proc.startTime;
tickEntry.pass = mPass;
if ( haveSample && deltaTicks > 0 ) {
// deltaTotal spans every core, so dividing by the core count turns it into elapsed
// wall-clock ticks: the result is percent of a single core, matching ksysguard.
double wallTicks = static_cast<double>( deltaTotal ) / mProcessorCount;
proc.userUsage = static_cast<int>( deltaTicks * 100.0 / wallTicks );
}
// Read command line
readProcessCmdline( proc, pid );
// Read actual storage I/O totals. Processes without permission to expose this file keep the
// default -1 values, so the corresponding optional columns remain empty.
readProcessIO( proc, pid );
// Resolve username and login capability from uid
resolveUser( proc );
auto usageIt = mGpuUsage.find( pid );
if ( usageIt != mGpuUsage.end() )
proc.gpuUsage = usageIt->second;
auto memoryIt = mGpuMemory.find( pid );
if ( memoryIt != mGpuMemory.end() )
proc.gpuMemory = memoryIt->second;
// Icons are resolved once per process, not once per pass: the executable behind a pid does
// not change, and the resolver walks the desktop index on a miss. A pid that was reused by
// a new process is a miss, so it does not keep the dead process's icon.
auto iconIt = mProcessIcons.find( pid );
if ( iconIt != mProcessIcons.end() && iconIt->second.startTime != proc.startTime )
iconIt = mProcessIcons.end();
if ( iconIt == mProcessIcons.end() ) {
char exeBuffer[PATH_MAX];
IconEntry iconEntry;
iconEntry.path = mIconResolver.iconFor(
readExePath( pid, exeBuffer, sizeof( exeBuffer ) ), proc.name );
iconEntry.startTime = proc.startTime;
iconEntry.pass = mPass;
iconIt = mProcessIcons.insert_or_assign( pid, std::move( iconEntry ) ).first;
} else {
iconIt->second.pass = mPass;
}
proc.iconPath = iconIt->second.path;
processes.push_back( std::move( proc ) );
}
closedir( procDir );
// Sweeping is amortised: entries of exited processes only cost memory until the next sweep.
if ( ( mPass % kCacheSweepInterval ) == 0 )
pruneCaches();
mNetworkMonitor.applyRates( processes );
return true;
}
void ProcessCollectorLinux::pruneCaches() {
for ( auto it = mProcessTicks.begin(); it != mProcessTicks.end(); ) {
if ( it->second.pass != mPass )
it = mProcessTicks.erase( it );
else
++it;
}
for ( auto it = mProcessIcons.begin(); it != mProcessIcons.end(); ) {
if ( it->second.pass != mPass )
it = mProcessIcons.erase( it );
else
++it;
}
}
} // namespace eproc
@@ -0,0 +1,87 @@
#ifndef EPROC_PROCESS_COLLECTOR_LINUX_HPP
#define EPROC_PROCESS_COLLECTOR_LINUX_HPP
#include "../../process_collector.hpp"
#include "gpu_reader_nvidia.hpp"
#include "process_icon_resolver.hpp"
#include "process_network_monitor.hpp"
#include <unordered_map>
namespace eproc {
class ProcessCollectorLinux : public ProcessCollector {
public:
ProcessCollectorLinux();
~ProcessCollectorLinux() override;
bool collect( std::vector<ProcessInfo>& processes, SystemInfo& sysInfo ) override;
private:
// Cached per-uid identity, since passwd lookups can hit NSS.
struct UserInfo {
std::string name;
bool canLogin{ false };
};
// Per-pid values tagged with the pass they were last seen in, so exited processes can be
// pruned without rebuilding (and reallocating every node of) the map on each pass. The start
// time pins an entry to one process incarnation, because a recycled pid would otherwise
// inherit the figures of the process that died.
struct TickEntry {
long long ticks{ 0 };
long long startTime{ 0 };
Uint32 pass{ 0 };
};
struct IconEntry {
std::string path;
long long startTime{ 0 };
Uint32 pass{ 0 };
};
// KiB per memory page (e.g. 4 for a 4096-byte page size)
long mPageSizeKb{ 4 };
int mProcessorCount{ 1 };
// Kernel clock ticks per second, used to size the minimum meaningful sampling window.
long mJiffiesPerSecond{ 100 };
// Previous CPU totals for delta calculation
long long mPrevTotalCpu{ 0 };
long long mPrevIdleCpu{ 0 };
// Machine-wide jiffies that must elapse before a CPU delta is trusted (~200ms across cores).
long long mMinSampleJiffies{ 0 };
float mLastCpuUsage{ 0.f };
UnorderedMap<long, TickEntry> mProcessTicks;
UnorderedMap<long, IconEntry> mProcessIcons;
Uint32 mPass{ 0 };
// Reused between passes so the GPU queries do not allocate a fresh map every second.
UnorderedMap<long, int> mGpuUsage;
UnorderedMap<long, long> mGpuMemory;
bool readCpuTimes( long long& idle, long long& total );
bool readProcessStat( ProcessInfo& proc, const char* statLine );
void readProcessStatus( ProcessInfo& proc, const char* content, size_t length );
void readProcessCmdline( ProcessInfo& proc, long pid );
void readProcessIO( ProcessInfo& proc, long pid );
void readSystemMemory( SystemInfo& sysInfo );
void readSystemUptime( SystemInfo& sysInfo );
void resolveUser( ProcessInfo& proc );
void pruneCaches();
/** Cached passwd lookup for a uid (name + login-shell capability). */
const UserInfo& userInfo( long uid );
UnorderedMap<long, UserInfo> mUserCache;
// Both run on the collection worker. Icons are resolved through the XDG desktop index (cached
// internally), and GPU figures come from NVML when an NVIDIA driver is present.
ProcessIconResolver mIconResolver;
NvidiaGpuReader mGpuReader;
ProcessNetworkMonitor mNetworkMonitor;
};
} // namespace eproc
#endif // EPROC_PROCESS_COLLECTOR_LINUX_HPP
@@ -0,0 +1,451 @@
#include "process_icon_resolver.hpp"
#include <dirent.h>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <vector>
#include <eepp/core/string.hpp>
#include <eepp/system/fileinfo.hpp>
#include <eepp/system/log.hpp>
using namespace EE;
using namespace EE::System;
namespace eproc {
// The icon themes probed for an icon name, in preference order. Every one of them is optional.
static const char* kIconThemes[] = { "hicolor", "breeze", "Adwaita" };
// The unthemed icon directory, searched after all the themes.
static const char* kIconPixmapDir = "/usr/share/pixmaps/";
// Sizes present in a theme, and the extensions an icon file may use. The two extra large sizes at
// the end are not part of the classic set but are the only ones a few applications ship.
static const char* kIconSizes[] = { "16x16", "22x22", "24x24", "32x32", "48x48",
"64x64", "128x128", "scalable", "256x256", "512x512" };
static const char* kIconExtensions[] = { ".png", ".svg", ".xpm" };
// Sizes preferred for the PNG pass, in order; the scalable SVG comes right after them.
static const char* kPreferredPngSizes[] = { "24x24", "32x32", "48x48" };
// Suffixes of a wrapper or bundle binary that the corresponding desktop id does not carry.
static const char* kExecutableSuffixes[] = { ".bin", ".sh", ".py", ".pl", ".run" };
// Directory names that identify a location rather than an application, and are never an app id.
static const char* kGenericDirNames[] = { "bin", "sbin", "lib", "lib64", "libexec", "usr",
"opt", "local", "share", "program", "applications" };
static bool isGenericDirName( std::string_view name ) {
for ( const char* generic : kGenericDirNames ) {
if ( name == generic )
return true;
}
return false;
}
// Everything after the last separator. The kernel appends " (deleted)" to a /proc/<pid>/exe link
// when the binary has been replaced or removed, which would never match an index key. The result is
// always a new string, so the argument is only ever read.
static std::string baseName( std::string_view path ) {
std::string name( path );
size_t slash = name.find_last_of( '/' );
if ( slash != std::string::npos )
name.erase( 0, slash + 1 );
if ( String::endsWith( name, " (deleted)" ) )
name.resize( name.size() - strlen( " (deleted)" ) );
return name;
}
// First token of an XDG Exec value, with the field codes (%U, %F, %u, %f, %%, ...) removed: that is
// the executable name the desktop entry is keyed by.
static std::string execToken( const std::string& exec ) {
size_t start = exec.find_first_not_of( " \t" );
if ( start == std::string::npos )
return {};
std::string token;
if ( exec[start] == '"' ) {
size_t end = exec.find( '"', start + 1 );
if ( end == std::string::npos )
return {};
token = exec.substr( start + 1, end - start - 1 );
} else {
size_t end = exec.find_first_of( " \t", start );
token = end == std::string::npos ? exec.substr( start ) : exec.substr( start, end - start );
}
std::string cleaned;
cleaned.reserve( token.size() );
for ( size_t i = 0; i < token.size(); i++ ) {
if ( token[i] == '%' && i + 1 < token.size() ) {
if ( token[i + 1] == '%' )
cleaned += '%';
i++;
continue;
}
cleaned += token[i];
}
return baseName( cleaned );
}
// True when an icon name carries its own extension, in which case it is never extended further: an
// "Icon=foo.svg" entry must not be probed as "foo.svg.png".
static bool hasIconExtension( const std::string& name ) {
for ( const char* extension : kIconExtensions ) {
if ( String::endsWith( name, extension ) )
return true;
}
return false;
}
static bool allowsExtension( const std::string& name, const char* extension ) {
return !hasIconExtension( name ) || String::endsWith( name, extension );
}
static std::string themeIconPath( const char* theme, const char* size, const std::string& name ) {
std::string path = "/usr/share/icons/";
path += theme;
path += '/';
path += size;
path += "/apps/";
path += name;
return path;
}
// Maps an icon name to the icon file that backs it. Preference order: 32x32 PNG, then 48x48 PNG,
// then the scalable SVG, then any other icon of any size and known extension, and finally the
// unthemed /usr/share/pixmaps directory.
static std::string findIconFile( const std::string& iconName ) {
if ( iconName.empty() )
return {};
// An absolute path (used by a few hand written entries) is taken as is.
if ( iconName[0] == '/' ) {
FileInfo icon( iconName );
return icon.isRegularFile() ? iconName : std::string();
}
// "pixmaps/<name>" refers to the unthemed pixmap directory.
std::string name = String::startsWith( iconName, "pixmaps/" )
? iconName.substr( strlen( "pixmaps/" ) )
: iconName;
if ( name.empty() )
return {};
if ( allowsExtension( name, ".png" ) ) {
for ( const char* size : kPreferredPngSizes ) {
for ( const char* theme : kIconThemes ) {
std::string path = themeIconPath( theme, size, name + ".png" );
if ( FileInfo( path ).isRegularFile() )
return path;
}
}
}
if ( allowsExtension( name, ".svg" ) ) {
for ( const char* theme : kIconThemes ) {
std::string path = themeIconPath( theme, "scalable", name + ".svg" );
if ( FileInfo( path ).isRegularFile() )
return path;
}
}
for ( const char* theme : kIconThemes ) {
for ( const char* size : kIconSizes ) {
for ( const char* extension : kIconExtensions ) {
if ( !allowsExtension( name, extension ) )
continue;
std::string path = themeIconPath( theme, size, name + extension );
if ( FileInfo( path ).isRegularFile() )
return path;
}
}
}
for ( const char* extension : kIconExtensions ) {
if ( !allowsExtension( name, extension ) )
continue;
std::string path = std::string( kIconPixmapDir ) + name + extension;
if ( FileInfo( path ).isRegularFile() )
return path;
}
// Some entries point straight at an extension-less file in the pixmap directory.
std::string nakedPath = std::string( kIconPixmapDir ) + name;
if ( FileInfo( nakedPath ).isRegularFile() )
return nakedPath;
return {};
}
// Every directory that can hold desktop entries, in XDG precedence order: the user's own entries
// shadow the system-wide ones, which is why they are scanned (and therefore indexed) first.
static std::vector<std::string> desktopDirectories() {
std::vector<std::string> dirs;
auto addDirectory = [&dirs]( const std::string& dir ) {
if ( dir.empty() )
return;
for ( const std::string& existing : dirs ) {
if ( existing == dir )
return;
}
dirs.push_back( dir );
};
auto addDataDirectory = [&addDirectory]( const std::string& dataDir ) {
if ( dataDir.empty() )
return;
addDirectory( dataDir.back() == '/' ? dataDir + "applications"
: dataDir + "/applications" );
};
// XDG_DATA_HOME defaults to $HOME/.local/share.
const char* dataHome = std::getenv( "XDG_DATA_HOME" );
if ( dataHome && *dataHome ) {
addDataDirectory( dataHome );
} else {
const char* home = std::getenv( "HOME" );
if ( home && *home )
addDataDirectory( std::string( home ) + "/.local/share" );
}
// XDG_DATA_DIRS defaults to /usr/local/share:/usr/share. The list is ordered, earlier entries
// taking precedence; addDirectory() keeps that order and drops the duplicates.
const char* dataDirs = std::getenv( "XDG_DATA_DIRS" );
const std::string dataDirList =
( dataDirs && *dataDirs ) ? dataDirs : "/usr/local/share:/usr/share";
for ( std::string dataDir : String::split( dataDirList, ':' ) ) {
// A trailing separator would turn into a doubled one in addDataDirectory().
while ( !dataDir.empty() && dataDir.back() == '/' )
dataDir.pop_back();
addDataDirectory( dataDir );
}
// The standard locations are always tried, even with a trimmed down environment.
addDirectory( "/usr/local/share/applications" );
addDirectory( "/usr/share/applications" );
return dirs;
}
// Reads the [Desktop Entry] group of a .desktop file. Only the unlocalized keys are considered:
// "Key[lang]=..." only ever repeats an already seen "Key=" value.
static void parseDesktopEntry( const std::string& path, std::string& exec, std::string& wmClass,
std::string& icon, bool& hidden ) {
std::ifstream file( path );
if ( !file.is_open() )
return;
std::string line;
bool inGroup = false;
while ( std::getline( file, line ) ) {
if ( !line.empty() && line.back() == '\r' )
line.pop_back();
if ( !line.empty() && line[0] == '[' ) {
// Past the desktop entry group there is nothing to index.
if ( inGroup )
break;
size_t end = line.find( ']' );
inGroup =
end != std::string::npos && line.compare( 0, end + 1, "[Desktop Entry]" ) == 0;
continue;
}
if ( !inGroup || line.empty() || line[0] == '#' )
continue;
size_t separator = line.find( '=' );
if ( separator == std::string::npos )
continue;
// Key and value are trimmed as views of the line, so no string is copied per entry line.
const std::string_view lineView( line );
const std::string_view key = String::trim( lineView.substr( 0, separator ), " \t\r\n" );
if ( key.empty() || key.find( '[' ) != std::string_view::npos )
continue;
const std::string_view value = String::trim( lineView.substr( separator + 1 ), " \t\r\n" );
if ( value.empty() )
continue;
if ( key == "Exec" ) {
if ( exec.empty() )
exec = value;
} else if ( key == "StartupWMClass" ) {
if ( wmClass.empty() )
wmClass = value;
} else if ( key == "Icon" ) {
if ( icon.empty() )
icon = value;
} else if ( key == "Hidden" ) {
// A hidden entry belongs to a removed or shadowed application.
if ( value == "true" )
hidden = true;
}
}
}
// Desktop ids are the desktop file basename without the ".desktop" suffix. An executable path
// rarely spells one out, so derive the plausible ids from it: the executable name without a common
// wrapper suffix, and the application directory it lives in (/usr/lib/firefox/firefox).
static std::vector<std::string> desktopIdCandidates( const std::string& exePath,
const std::string& exeName ) {
std::vector<std::string> ids;
auto addId = [&ids, &exeName]( std::string_view id ) {
if ( id.empty() || id == exeName )
return;
for ( const std::string& existing : ids ) {
if ( existing == id )
return;
}
ids.emplace_back( id );
};
const std::string_view exeNameView( exeName );
for ( const char* suffix : kExecutableSuffixes ) {
if ( String::endsWith( exeName, suffix ) ) {
addId( exeNameView.substr( 0, exeName.size() - strlen( suffix ) ) );
break;
}
}
// The directory holding the executable, which is where a few vendors keep the app id
// (/usr/lib/firefox/firefox, /usr/lib/libreoffice/program/soffice).
size_t lastSlash = exePath.find_last_of( '/' );
if ( lastSlash != std::string::npos && lastSlash > 0 ) {
const std::string parent = baseName( std::string_view( exePath ).substr( 0, lastSlash ) );
if ( !isGenericDirName( parent ) )
addId( parent );
}
return ids;
}
const std::string& ProcessIconResolver::lookupKey( const std::string& key ) const {
if ( key.empty() )
return mEmpty;
auto it = mExecIndex.find( key );
if ( it != mExecIndex.end() )
return it->second;
it = mIdIndex.find( key );
if ( it != mIdIndex.end() )
return it->second;
return mEmpty;
}
const std::string& ProcessIconResolver::resolveIconName( const std::string& iconName ) {
auto it = mIconPathIndex.find( iconName );
if ( it != mIconPathIndex.end() )
return it->second;
std::string path = findIconFile( iconName );
return mIconPathIndex.emplace( iconName, std::move( path ) ).first->second;
}
void ProcessIconResolver::scanDirectory( const std::string& dir ) {
DIR* handle = opendir( dir.c_str() );
if ( !handle )
return;
struct dirent* entry;
while ( ( entry = readdir( handle ) ) != nullptr ) {
const std::string fileName = entry->d_name;
if ( !String::endsWith( fileName, ".desktop" ) )
continue;
// A directory named "something.desktop" is not a desktop entry, and neither is a file
// that cannot be read.
const std::string path = dir + "/" + fileName;
if ( !FileInfo( path ).isRegularFile() )
continue;
indexDesktopFile( path );
}
closedir( handle );
}
void ProcessIconResolver::indexDesktopFile( const std::string& path ) {
std::string exec, wmClass, icon;
bool hidden = false;
parseDesktopEntry( path, exec, wmClass, icon, hidden );
if ( hidden || icon.empty() )
return;
// The desktop file id is the weakest key: it only matches an executable that happens to be
// named after its desktop file, so it is kept apart and never shadows a real key.
std::string id = baseName( path );
if ( String::endsWith( id, ".desktop" ) )
id.resize( id.size() - strlen( ".desktop" ) );
if ( !id.empty() && mIdIndex.find( id ) == mIdIndex.end() )
mIdIndex.emplace( id, icon );
// First entry wins, which is the user's own one because it is scanned first.
if ( !wmClass.empty() && mExecIndex.find( wmClass ) == mExecIndex.end() )
mExecIndex.emplace( wmClass, icon );
std::string executable = execToken( exec );
if ( !executable.empty() && mExecIndex.find( executable ) == mExecIndex.end() )
mExecIndex.emplace( executable, icon );
}
void ProcessIconResolver::buildIndex() {
mIndexBuilt = true;
for ( const std::string& dir : desktopDirectories() )
scanDirectory( dir );
if ( mExecIndex.empty() && mIdIndex.empty() ) {
Log::warning( "eproc: no desktop entries found, process icons will be unavailable" );
}
}
const std::string& ProcessIconResolver::iconFor( const std::string& exePath,
const std::string& name ) {
std::lock_guard<std::mutex> lock( mMutex );
if ( !mIndexBuilt )
buildIndex();
std::string cacheKey = exePath;
cacheKey += '|';
cacheKey += name;
auto cached = mResultCache.find( cacheKey );
if ( cached != mResultCache.end() )
return cached->second;
const std::string exeName = baseName( exePath );
std::string iconName = lookupKey( exeName );
if ( iconName.empty() ) {
for ( const std::string& id : desktopIdCandidates( exePath, exeName ) ) {
iconName = lookupKey( id );
if ( !iconName.empty() )
break;
}
}
if ( iconName.empty() ) {
// A blank name has no key to look up.
const std::string_view trimmedName = String::trim( std::string_view( name ), " \t\r\n" );
if ( !trimmedName.empty() )
iconName = lookupKey( std::string( trimmedName ) );
}
if ( iconName.empty() )
return mResultCache.emplace( std::move( cacheKey ), std::string() ).first->second;
return mResultCache.emplace( std::move( cacheKey ), resolveIconName( iconName ) ).first->second;
}
} // namespace eproc
@@ -0,0 +1,73 @@
#ifndef EPROC_PROCESS_ICON_RESOLVER_HPP
#define EPROC_PROCESS_ICON_RESOLVER_HPP
#include <eepp/core/containers.hpp>
#include <mutex>
#include <string>
using namespace EE;
namespace eproc {
/** Resolves the icon of a process to an absolute icon file path following the XDG desktop entry
* route: an executable name is mapped to a .desktop entry, and that entry's icon name to a file
* inside the installed icon themes. X11 window icons are deliberately not used, so the result
* does not depend on a display connection.
*
* The desktop index is built once, lazily, on the first iconFor() call. Every result - including
* the "no icon" ones - is cached under the queried exe/name pair, so the steady state is a couple
* of hash lookups with no filesystem access at all.
*
* The resolver never throws and never fails hard: unreadable directories, missing environment
* variables and malformed desktop files are skipped, and an unavailable icon simply resolves to
* the empty string. All methods are safe to call from any thread. */
class ProcessIconResolver {
public:
/** Resolves the icon file for a process.
* @param exePath absolute path of /proc/<pid>/exe target (may be empty)
* @param name process name (comm), used as a fallback key
* @return absolute path to a PNG/SVG/XPM icon, or empty string when none found.
* The returned reference stays valid for the resolver's lifetime. */
const std::string& iconFor( const std::string& exePath, const std::string& name );
private:
/** Reads every desktop entry once, keying the executable name, the window class and the desktop
* file id to the entry's icon name. Only ever called once, from iconFor(). */
void buildIndex();
/** Scans @p dir for *.desktop files. Missing or unreadable directories are skipped silently. */
void scanDirectory( const std::string& dir );
/** Indexes a single .desktop file. Silently ignores unreadable, hidden and icon-less entries.
*/
void indexDesktopFile( const std::string& path );
/** Returns the icon name registered for @p key, or an empty string when there is none. The
* executable/window-class index wins over the weaker desktop id index. */
const std::string& lookupKey( const std::string& key ) const;
/** Resolves an icon name to an absolute file path, caching the result (misses included). */
const std::string& resolveIconName( const std::string& iconName );
bool mIndexBuilt{ false };
// Icon name per executable basename and per startup window class.
UnorderedMap<std::string, std::string> mExecIndex;
// Icon name per desktop file id (the file name without ".desktop"), used as a weak fallback.
UnorderedMap<std::string, std::string> mIdIndex;
// Icon name -> icon file path. Misses are stored as empty strings so they are not retried.
UnorderedMap<std::string, std::string> mIconPathIndex;
// "<exePath>|<name>" -> icon file path, negative results included.
UnorderedMap<std::string, std::string> mResultCache;
// Returned when nothing matched; a member so the returned reference always stays valid.
std::string mEmpty;
std::mutex mMutex;
};
} // namespace eproc
#endif // EPROC_PROCESS_ICON_RESOLVER_HPP
@@ -0,0 +1,542 @@
#include "process_network_monitor.hpp"
#include <eepp/system/log.hpp>
#include <arpa/inet.h>
#include <dirent.h>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <pcap/pcap.h>
#include <pcap/sll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <algorithm>
#include <charconv>
#include <cstdio>
#include <cstring>
#include <limits>
#include <string>
using namespace EE::System;
namespace eproc {
namespace {
constexpr Uint8 kIPv4 = 4;
constexpr Uint8 kIPv6 = 6;
constexpr size_t kProcPathCapacity = 64;
constexpr size_t kFdTargetCapacity = 128;
#ifndef DLT_LINUX_SLL2
constexpr int kLinuxSll2 = 276;
#else
constexpr int kLinuxSll2 = DLT_LINUX_SLL2;
#endif
inline Uint16 readBigEndian16( const Uint8* data ) {
return static_cast<Uint16>( data[0] << 8 | data[1] );
}
inline Uint32 readBigEndian32( const Uint8* data ) {
return static_cast<Uint32>( data[0] ) << 24 | static_cast<Uint32>( data[1] ) << 16 |
static_cast<Uint32>( data[2] ) << 8 | static_cast<Uint32>( data[3] );
}
inline bool isNumericName( const char* name, long& value ) {
if ( !name || !*name )
return false;
char* end = nullptr;
value = strtol( name, &end, 10 );
return value > 0 && end != name && *end == '\0';
}
inline int hexDigit( char value ) {
if ( value >= '0' && value <= '9' )
return value - '0';
if ( value >= 'a' && value <= 'f' )
return value - 'a' + 10;
if ( value >= 'A' && value <= 'F' )
return value - 'A' + 10;
return -1;
}
inline bool parseHexByte( std::string_view value, size_t offset, Uint8& result ) {
if ( offset + 2 > value.size() )
return false;
const int high = hexDigit( value[offset] );
const int low = hexDigit( value[offset + 1] );
if ( high < 0 || low < 0 )
return false;
result = static_cast<Uint8>( high * 16 + low );
return true;
}
inline bool nextToken( const char*& cursor, const char* end, std::string_view& token ) {
while ( cursor < end &&
( *cursor == ' ' || *cursor == '\t' || *cursor == '\r' || *cursor == '\n' ) )
++cursor;
if ( cursor >= end )
return false;
const char* begin = cursor;
while ( cursor < end && *cursor != ' ' && *cursor != '\t' && *cursor != '\r' &&
*cursor != '\n' )
++cursor;
token = std::string_view( begin, static_cast<size_t>( cursor - begin ) );
return true;
}
} // namespace
std::size_t
ProcessNetworkMonitor::EndpointHash::operator()( const Endpoint& endpoint ) const noexcept {
std::size_t hash = endpoint.family * 131u + endpoint.port;
for ( Uint8 byte : endpoint.address )
hash = hash * 16777619u ^ byte;
return hash;
}
ProcessNetworkMonitor::ProcessNetworkMonitor() : mLastSnapshot( std::chrono::steady_clock::now() ) {
mCaptureThread = std::thread( &ProcessNetworkMonitor::captureLoop, this );
}
ProcessNetworkMonitor::~ProcessNetworkMonitor() {
mRunning.store( false );
if ( mCaptureThread.joinable() )
mCaptureThread.join();
}
bool ProcessNetworkMonitor::parseUnsigned( std::string_view token, Uint64& value, int base ) {
if ( token.empty() )
return false;
value = 0;
auto result = std::from_chars( token.data(), token.data() + token.size(), value, base );
return result.ec == std::errc() && result.ptr == token.data() + token.size();
}
bool ProcessNetworkMonitor::parseProcEndpoint( std::string_view token, Endpoint& endpoint ) {
const size_t separator = token.rfind( ':' );
if ( separator == std::string_view::npos )
return false;
const std::string_view address = token.substr( 0, separator );
Uint64 port = 0;
if ( !parseUnsigned( token.substr( separator + 1 ), port, 16 ) || port > 0xffff )
return false;
Endpoint parsed;
parsed.port = static_cast<Uint16>( port );
if ( address.size() == 8 ) {
parsed.family = kIPv4;
for ( size_t i = 0; i < 4; ++i ) {
if ( !parseHexByte( address, ( 3 - i ) * 2, parsed.address[i] ) )
return false;
}
} else if ( address.size() == 32 ) {
parsed.family = kIPv6;
for ( size_t word = 0; word < 4; ++word ) {
for ( size_t byte = 0; byte < 4; ++byte ) {
if ( !parseHexByte( address, word * 8 + ( 3 - byte ) * 2,
parsed.address[word * 4 + byte] ) )
return false;
}
}
} else {
return false;
}
bool isMapped = parsed.family == kIPv6;
for ( size_t i = 0; isMapped && i < 10; ++i )
isMapped = parsed.address[i] == 0;
isMapped = isMapped && parsed.address[10] == 0xff && parsed.address[11] == 0xff;
if ( isMapped ) {
Endpoint mapped;
mapped.family = kIPv4;
mapped.port = parsed.port;
std::copy_n( parsed.address.begin() + 12, 4, mapped.address.begin() );
parsed = mapped;
}
endpoint = parsed;
return true;
}
bool ProcessNetworkMonitor::parseSocketLine( std::string_view line, Endpoint& endpoint,
Uint64& inode ) {
const char* cursor = line.data();
const char* end = cursor + line.size();
std::string_view token;
std::string_view localAddress;
std::string_view inodeToken;
for ( int index = 0; index <= 9; ++index ) {
if ( !nextToken( cursor, end, token ) )
return false;
if ( index == 1 )
localAddress = token;
else if ( index == 9 )
inodeToken = token;
}
return parseProcEndpoint( localAddress, endpoint ) && parseUnsigned( inodeToken, inode, 10 );
}
void ProcessNetworkMonitor::collectSocketOwners( UnorderedMap<Uint64, long>& owners ) {
DIR* procDirectory = opendir( "/proc" );
if ( !procDirectory )
return;
char fdPath[kProcPathCapacity];
char target[kFdTargetCapacity];
while ( dirent* processEntry = readdir( procDirectory ) ) {
long pid = 0;
if ( !isNumericName( processEntry->d_name, pid ) )
continue;
const int pathLength = snprintf( fdPath, sizeof( fdPath ), "/proc/%ld/fd", pid );
if ( pathLength <= 0 || static_cast<size_t>( pathLength ) >= sizeof( fdPath ) )
continue;
DIR* fdDirectory = opendir( fdPath );
if ( !fdDirectory )
continue;
while ( dirent* fdEntry = readdir( fdDirectory ) ) {
if ( fdEntry->d_name[0] == '.' &&
( fdEntry->d_name[1] == '\0' ||
( fdEntry->d_name[1] == '.' && fdEntry->d_name[2] == '\0' ) ) )
continue;
ssize_t length =
readlinkat( dirfd( fdDirectory ), fdEntry->d_name, target, sizeof( target ) - 1 );
if ( length <= 9 )
continue;
target[length] = '\0';
std::string_view link( target, static_cast<size_t>( length ) );
if ( link.compare( 0, 8, "socket:[" ) != 0 || link.back() != ']' )
continue;
Uint64 inode = 0;
if ( parseUnsigned( link.substr( 8, link.size() - 9 ), inode, 10 ) )
owners.insert_or_assign( inode, pid );
}
closedir( fdDirectory );
}
closedir( procDirectory );
}
void ProcessNetworkMonitor::collectSocketTable( const char* path, Uint8 family,
const UnorderedMap<Uint64, long>& owners,
EndpointMap& endpoints ) {
FILE* file = fopen( path, "r" );
if ( !file )
return;
char line[512];
while ( fgets( line, sizeof( line ), file ) ) {
Endpoint endpoint;
Uint64 inode = 0;
if ( !parseSocketLine( line, endpoint, inode ) ||
( endpoint.family != family && !( family == kIPv6 && endpoint.family == kIPv4 ) ) )
continue;
auto owner = owners.find( inode );
if ( owner != owners.end() )
endpoints.insert_or_assign( endpoint, owner->second );
}
fclose( file );
}
void ProcessNetworkMonitor::refreshMapping() {
UnorderedMap<Uint64, long> owners;
collectSocketOwners( owners );
EndpointMap endpoints;
collectSocketTable( "/proc/net/tcp", kIPv4, owners, endpoints );
collectSocketTable( "/proc/net/tcp6", kIPv6, owners, endpoints );
collectSocketTable( "/proc/net/udp", kIPv4, owners, endpoints );
collectSocketTable( "/proc/net/udp6", kIPv6, owners, endpoints );
std::lock_guard<std::mutex> lock( mMutex );
mEndpoints = std::move( endpoints );
}
long ProcessNetworkMonitor::findPid( const Endpoint& endpoint ) const {
auto exact = mEndpoints.find( endpoint );
if ( exact != mEndpoints.end() )
return exact->second;
Endpoint wildcard = endpoint;
wildcard.address.fill( 0 );
auto anyAddress = mEndpoints.find( wildcard );
return anyAddress != mEndpoints.end() ? anyAddress->second : 0;
}
bool ProcessNetworkMonitor::parsePacket( int dataLink, const Uint8* data, size_t length,
Endpoint& source, Endpoint& destination ) {
size_t networkOffset = 0;
Uint16 etherType = 0;
switch ( dataLink ) {
case DLT_EN10MB:
if ( length < 14 )
return false;
networkOffset = 14;
etherType = readBigEndian16( data + 12 );
while ( etherType == ETHERTYPE_VLAN || etherType == 0x88a8 || etherType == 0x9100 ) {
if ( length < networkOffset + 4 )
return false;
etherType = readBigEndian16( data + networkOffset + 2 );
networkOffset += 4;
}
break;
case DLT_LINUX_SLL:
if ( length < 16 )
return false;
networkOffset = 16;
etherType = readBigEndian16( data + 14 );
break;
case kLinuxSll2:
if ( length < 20 )
return false;
networkOffset = 20;
etherType = readBigEndian16( data );
break;
case DLT_RAW:
networkOffset = 0;
break;
case DLT_NULL: {
if ( length < 4 )
return false;
Uint32 linkFamily = 0;
std::memcpy( &linkFamily, data, sizeof( linkFamily ) );
if ( linkFamily != AF_INET && linkFamily != AF_INET6 )
linkFamily = ntohl( linkFamily );
if ( linkFamily == AF_INET )
etherType = ETHERTYPE_IP;
else if ( linkFamily == AF_INET6 )
etherType = ETHERTYPE_IPV6;
else
return false;
networkOffset = 4;
break;
}
case DLT_LOOP: {
if ( length < 4 )
return false;
const Uint32 linkFamily = readBigEndian32( data );
if ( linkFamily == AF_INET )
etherType = ETHERTYPE_IP;
else if ( linkFamily == AF_INET6 )
etherType = ETHERTYPE_IPV6;
else
return false;
networkOffset = 4;
break;
}
default:
return false;
}
Uint8 transportProtocol = 0;
size_t transportOffset = 0;
if ( etherType == ETHERTYPE_IP ) {
if ( length < networkOffset + 20 )
return false;
const Uint8 versionAndLength = data[networkOffset];
if ( ( versionAndLength >> 4 ) != 4 )
return false;
const size_t headerLength = static_cast<size_t>( versionAndLength & 0x0f ) * 4;
if ( headerLength < 20 || length < networkOffset + headerLength )
return false;
if ( ( readBigEndian16( data + networkOffset + 6 ) & 0x1fff ) != 0 )
return false;
source = Endpoint{};
destination = Endpoint{};
source.family = destination.family = kIPv4;
std::copy_n( data + networkOffset + 12, 4, source.address.begin() );
std::copy_n( data + networkOffset + 16, 4, destination.address.begin() );
transportProtocol = data[networkOffset + 9];
transportOffset = networkOffset + headerLength;
} else if ( etherType == ETHERTYPE_IPV6 ) {
if ( length < networkOffset + 40 )
return false;
if ( ( data[networkOffset] >> 4 ) != 6 )
return false;
source = Endpoint{};
destination = Endpoint{};
source.family = destination.family = kIPv6;
std::copy_n( data + networkOffset + 8, 16, source.address.begin() );
std::copy_n( data + networkOffset + 24, 16, destination.address.begin() );
transportProtocol = data[networkOffset + 6];
transportOffset = networkOffset + 40;
while ( transportProtocol != IPPROTO_TCP && transportProtocol != IPPROTO_UDP ) {
size_t extensionLength = 0;
if ( transportProtocol == IPPROTO_HOPOPTS || transportProtocol == IPPROTO_ROUTING ||
transportProtocol == IPPROTO_DSTOPTS ) {
if ( length < transportOffset + 2 )
return false;
extensionLength = static_cast<size_t>( data[transportOffset + 1] + 1 ) * 8;
} else if ( transportProtocol == IPPROTO_FRAGMENT ) {
if ( length < transportOffset + 8 ||
( readBigEndian16( data + transportOffset + 2 ) & 0xfff8 ) != 0 )
return false;
extensionLength = 8;
} else if ( transportProtocol == IPPROTO_AH ) {
if ( length < transportOffset + 2 )
return false;
extensionLength = static_cast<size_t>( data[transportOffset + 1] + 2 ) * 4;
} else {
return false;
}
if ( length < transportOffset + extensionLength )
return false;
transportProtocol = data[transportOffset];
transportOffset += extensionLength;
}
} else {
return false;
}
if ( ( transportProtocol != IPPROTO_TCP && transportProtocol != IPPROTO_UDP ) ||
length < transportOffset + 4 )
return false;
source.port = readBigEndian16( data + transportOffset );
destination.port = readBigEndian16( data + transportOffset + 2 );
return source.port != 0 && destination.port != 0;
}
void ProcessNetworkMonitor::processPacket( int dataLink, const Uint8* data, size_t length,
size_t wireLength ) {
Endpoint source;
Endpoint destination;
if ( !parsePacket( dataLink, data, length, source, destination ) )
return;
std::lock_guard<std::mutex> lock( mMutex );
const long uploadPid = findPid( source );
const long downloadPid = findPid( destination );
const long pid = uploadPid > 0 ? uploadPid : downloadPid;
if ( pid <= 0 )
return;
Traffic& traffic = mTraffic[pid];
if ( uploadPid > 0 )
traffic.upload += wireLength;
else
traffic.download += wireLength;
}
long ProcessNetworkMonitor::rateFor( Uint64 bytes, std::chrono::steady_clock::duration elapsed ) {
const long double seconds = std::chrono::duration<long double>( elapsed ).count();
if ( seconds <= 0.0L )
return 0;
const long double rate = static_cast<long double>( bytes ) / seconds;
if ( rate >= static_cast<long double>( std::numeric_limits<long>::max() ) )
return std::numeric_limits<long>::max();
return static_cast<long>( rate );
}
void ProcessNetworkMonitor::applyRates( std::vector<ProcessInfo>& processes ) {
const auto now = std::chrono::steady_clock::now();
std::lock_guard<std::mutex> lock( mMutex );
const bool available = mAvailable.load();
const auto elapsed = now - mLastSnapshot;
for ( ProcessInfo& process : processes ) {
if ( !available ) {
process.netDownload = -1;
process.netUpload = -1;
continue;
}
auto traffic = mTraffic.find( process.pid );
if ( traffic == mTraffic.end() ) {
process.netDownload = 0;
process.netUpload = 0;
} else {
process.netDownload = rateFor( traffic->second.download, elapsed );
process.netUpload = rateFor( traffic->second.upload, elapsed );
}
}
mTraffic.clear();
mLastSnapshot = now;
}
void ProcessNetworkMonitor::captureLoop() {
char errorBuffer[PCAP_ERRBUF_SIZE] = {};
pcap_t* capture = pcap_create( nullptr, errorBuffer );
if ( !capture ) {
Log::warning( "eproc: could not start per-process network capture: %s", errorBuffer );
return;
}
pcap_set_snaplen( capture, 256 );
pcap_set_promisc( capture, 0 );
pcap_set_timeout( capture, 250 );
int result = pcap_activate( capture );
if ( result < 0 ) {
Log::warning( "eproc: per-process network capture is unavailable: %s",
pcap_geterr( capture ) );
pcap_close( capture );
return;
}
bpf_program filter;
const int compileResult =
pcap_compile( capture, &filter, "tcp or udp", 1, PCAP_NETMASK_UNKNOWN );
if ( compileResult < 0 ) {
Log::warning( "eproc: could not install the per-process network capture filter: %s",
pcap_geterr( capture ) );
pcap_close( capture );
return;
}
if ( pcap_setfilter( capture, &filter ) < 0 ) {
Log::warning( "eproc: could not install the per-process network capture filter: %s",
pcap_geterr( capture ) );
pcap_freecode( &filter );
pcap_close( capture );
return;
}
pcap_freecode( &filter );
mDataLink = pcap_datalink( capture );
if ( mDataLink < 0 ) {
pcap_close( capture );
return;
}
mAvailable.store( true );
while ( mRunning.load() ) {
pcap_pkthdr* header = nullptr;
const u_char* data = nullptr;
result = pcap_next_ex( capture, &header, &data );
if ( result == 1 )
processPacket( mDataLink, data, header->caplen, header->len );
else if ( result == PCAP_ERROR_BREAK || result == PCAP_ERROR )
break;
}
mAvailable.store( false );
pcap_close( capture );
}
} // namespace eproc
@@ -0,0 +1,87 @@
#ifndef EPROC_PROCESS_NETWORK_MONITOR_HPP
#define EPROC_PROCESS_NETWORK_MONITOR_HPP
#include "../../process_info.hpp"
#include <array>
#include <atomic>
#include <chrono>
#include <mutex>
#include <string_view>
#include <thread>
#include <vector>
namespace eproc {
/** Collects per-process network traffic on Linux.
*
* Linux does not expose network byte counters per process. The implementation therefore captures
* TCP/UDP packet headers and joins their local endpoints with socket inodes from procfs. Packet
* capture runs continuously in its own thread; the process collector only refreshes the procfs
* ownership map and consumes the counters at snapshot time.
*/
class ProcessNetworkMonitor {
public:
ProcessNetworkMonitor();
~ProcessNetworkMonitor();
/** Refreshes the socket endpoint to PID map. This is intended for the collection worker. */
void refreshMapping();
/** Copies the current byte rates into @p processes and starts a new measurement interval. */
void applyRates( std::vector<ProcessInfo>& processes );
private:
struct Endpoint {
Uint8 family{ 0 };
Uint16 port{ 0 };
std::array<Uint8, 16> address{};
bool operator==( const Endpoint& other ) const {
return family == other.family && port == other.port && address == other.address;
}
};
struct EndpointHash {
std::size_t operator()( const Endpoint& endpoint ) const noexcept;
};
struct Traffic {
Uint64 download{ 0 };
Uint64 upload{ 0 };
};
using EndpointMap = UnorderedMap<Endpoint, long, EndpointHash>;
using TrafficMap = UnorderedMap<long, Traffic>;
void captureLoop();
void processPacket( int dataLink, const Uint8* data, size_t length, size_t wireLength );
static bool parsePacket( int dataLink, const Uint8* data, size_t length, Endpoint& source,
Endpoint& destination );
static bool parseProcEndpoint( std::string_view token, Endpoint& endpoint );
static bool parseSocketLine( std::string_view line, Endpoint& endpoint, Uint64& inode );
static bool parseUnsigned( std::string_view token, Uint64& value, int base );
static void collectSocketOwners( UnorderedMap<Uint64, long>& owners );
static void collectSocketTable( const char* path, Uint8 family,
const UnorderedMap<Uint64, long>& owners,
EndpointMap& endpoints );
long findPid( const Endpoint& endpoint ) const;
static long rateFor( Uint64 bytes, std::chrono::steady_clock::duration elapsed );
std::atomic<bool> mRunning{ true };
std::atomic<bool> mAvailable{ false };
std::thread mCaptureThread;
int mDataLink{ -1 };
mutable std::mutex mMutex;
EndpointMap mEndpoints;
TrafficMap mTraffic;
std::chrono::steady_clock::time_point mLastSnapshot;
};
} // namespace eproc
#endif // EPROC_PROCESS_NETWORK_MONITOR_HPP
+37
View File
@@ -0,0 +1,37 @@
#include "process_collector.hpp"
#include <eepp/system/log.hpp>
#if EE_PLATFORM == EE_PLATFORM_LINUX
#include "platform/linux/process_collector_linux.hpp"
#include <signal.h>
#include <sys/types.h>
#endif
using namespace EE::System;
namespace eproc {
std::unique_ptr<ProcessCollector> ProcessCollector::create() {
#if EE_PLATFORM == EE_PLATFORM_LINUX
return std::make_unique<ProcessCollectorLinux>();
#else
Log::error( "eproc: no process collector available for this platform" );
return nullptr;
#endif
}
bool sendProcessSignal( long pid, int signal ) {
#if EE_PLATFORM == EE_PLATFORM_LINUX
return ::kill( static_cast<pid_t>( pid ), signal ) == 0;
#else
Log::error( "eproc: sendProcessSignal is not implemented for this platform" );
return false;
#endif
}
bool killProcess( long pid ) {
return sendProcessSignal( pid, 9 ); // SIGKILL
}
} // namespace eproc
+51
View File
@@ -0,0 +1,51 @@
#ifndef EPROC_PROCESS_COLLECTOR_HPP
#define EPROC_PROCESS_COLLECTOR_HPP
#include "process_info.hpp"
#include <memory>
#include <vector>
namespace eproc {
struct SystemInfo {
long totalMemory{ 0 };
long freeMemory{ 0 };
long availableMemory{ 0 };
long totalSwap{ 0 };
long freeSwap{ 0 };
int cpuCount{ 1 };
float cpuUsage{ 0.f };
long clockTicksPerSecond{ 100 };
double uptimeSeconds{ 0.0 };
long getTotalMemoryKB() const { return totalMemory; }
long getUsedMemoryKB() const { return totalMemory - availableMemory; }
long getUsedSwapKB() const { return totalSwap - freeSwap; }
};
class ProcessCollector {
public:
virtual ~ProcessCollector() = default;
/** Collects every process and the system-wide counters into the given buffers.
* Not thread-safe: one instance must never be used concurrently, because per-process CPU
* usage is derived from the delta against the previous sample held by the instance.
* @return true on success. */
virtual bool collect( std::vector<ProcessInfo>& processes, SystemInfo& sysInfo ) = 0;
/** Returns the collector for the running OS, or nullptr when the platform is unsupported. */
static std::unique_ptr<ProcessCollector> create();
protected:
ProcessCollector() = default;
};
/** Sends a process signal (e.g. SIGTERM 15, SIGKILL 9) to @p pid. Returns true on success. */
bool sendProcessSignal( long pid, int signal );
/** Sends SIGKILL to @p pid. Returns true on success. */
bool killProcess( long pid );
} // namespace eproc
#endif // EPROC_PROCESS_COLLECTOR_HPP
+148
View File
@@ -0,0 +1,148 @@
#include "process_info.hpp"
#include <cstdio>
namespace eproc {
static std::string formatScaledIEC( double amount, const char* const* units, size_t unitCount ) {
for ( size_t i = 0; i + 1 < unitCount; ++i ) {
if ( amount < 1024.0 ) {
char buf[32];
snprintf( buf, sizeof( buf ), "%.1f %s", amount, units[i] );
return buf;
}
amount /= 1024.0;
}
char buf[32];
snprintf( buf, sizeof( buf ), "%.1f %s", amount, units[unitCount - 1] );
return buf;
}
std::string formatBytesPerSecond( long bytes ) {
if ( bytes <= 0 )
return {};
static const char* const units[] = { "K/s", "M/s", "G/s", "T/s" };
if ( bytes < 1024 )
return std::to_string( bytes ) + " B/s";
return formatScaledIEC( static_cast<double>( bytes ) / 1024.0, units, 4 );
}
std::string formatBytes( long long bytes ) {
if ( bytes < 0 )
return {};
if ( bytes < 1024 )
return std::to_string( bytes ) + " B";
static const char* const units[] = { "KiB", "MiB", "GiB", "TiB", "PiB" };
return formatScaledIEC( static_cast<double>( bytes ) / 1024.0, units, 5 );
}
// Mirrors ksysguard6's ProcessModel::formatByteSize(): the unit is picked with a 0.9 hysteresis
// threshold so a value never reads as "1.0" of a unit it barely exceeds, kilobytes stay integral,
// and larger units keep one decimal. That is what produces "0 K", "853.8 M" and "4.2 G".
std::string formatKiB( long kib ) {
if ( kib < 0 )
return {};
static const double KiB = 1024.0;
static const char* const units[] = { "M", "G", "T", "P" };
if ( static_cast<double>( kib ) < KiB * 0.9 )
return std::to_string( kib ) + " K";
double amount = kib;
for ( size_t i = 0; i + 1 < 4; ++i ) {
amount /= KiB;
if ( amount < KiB * 0.9 ) {
char buf[32];
snprintf( buf, sizeof( buf ), "%.1f %s", amount, units[i] );
return buf;
}
}
char buf[32];
snprintf( buf, sizeof( buf ), "%.1f %s", amount / KiB, units[3] );
return buf;
}
// IEC form used by the status bar: "3.2 MiB", "62.7 GiB". Kilobyte amounts stay integral,
// matching how the original renders small memory totals.
std::string formatKiBIEC( long kib ) {
if ( kib < 0 )
return {};
if ( kib < 1024 )
return std::to_string( kib ) + " KiB";
static const char* const units[] = { "MiB", "GiB", "TiB", "PiB" };
return formatScaledIEC( static_cast<double>( kib ) / 1024.0, units, 4 );
}
std::string ProcessInfo::formatMemory() const {
return formatKiB( getMemoryForSort() );
}
std::string ProcessInfo::formatSharedMem() const {
return formatKiB( sharedMem );
}
std::string ProcessInfo::formatCpu() const {
int total = userUsage + sysUsage;
if ( total <= 0 )
return {};
return std::to_string( total ) + "%";
}
std::string ProcessInfo::formatGpuUsage() const {
if ( gpuUsage < 0 )
return {};
return std::to_string( gpuUsage ) + "%";
}
std::string ProcessInfo::formatGpuMemory() const {
if ( gpuMemory < 0 )
return {};
return formatKiBIEC( gpuMemory );
}
std::string ProcessInfo::formatDownload() const {
return formatBytesPerSecond( netDownload );
}
std::string ProcessInfo::formatUpload() const {
return formatBytesPerSecond( netUpload );
}
std::string ProcessInfo::formatCpuTime( long ticksPerSecond ) const {
const long long totalTicks = static_cast<long long>( userTime ) + sysTime;
if ( totalTicks < 0 || ticksPerSecond <= 0 )
return {};
const long long totalSeconds = totalTicks / ticksPerSecond;
char buffer[32];
snprintf( buffer, sizeof( buffer ), "%lld:%02lld", totalSeconds / 60, totalSeconds % 60 );
return buffer;
}
std::string ProcessInfo::formatRelativeStartTime( double uptimeSeconds,
long ticksPerSecond ) const {
if ( startTime <= 0 || uptimeSeconds < 0 || ticksPerSecond <= 0 )
return {};
const double age = uptimeSeconds - static_cast<double>( startTime ) / ticksPerSecond;
if ( age < 0 )
return {};
const long long totalSeconds = static_cast<long long>( age );
const long long hours = totalSeconds / 3600;
const long long minutes = ( totalSeconds / 60 ) % 60;
const long long seconds = totalSeconds % 60;
char buffer[48];
snprintf( buffer, sizeof( buffer ), "%lld:%02lld:%02lld", hours, minutes, seconds );
return buffer;
}
} // namespace eproc
+125
View File
@@ -0,0 +1,125 @@
#ifndef EPROC_PROCESS_INFO_HPP
#define EPROC_PROCESS_INFO_HPP
#include <eepp/core/core.hpp>
#include <string>
using namespace EE;
namespace eproc {
enum class ProcessStatus : Uint8 {
Running,
Sleeping,
DiskSleep,
Zombie,
Stopped,
Paging,
Ended,
Other
};
struct ProcessInfo {
long pid{ 0 };
long parentPid{ 0 };
std::string name;
std::string username;
// Real, effective, saved and filesystem user ids (all four from the status "Uid:" line).
long uid{ 0 };
long euid{ 0 };
long suid{ 0 };
long fsuid{ 0 };
// True when the account's shell is a real login shell (used to tell system users apart).
bool canLogin{ false };
// Same check for the effective uid, which the original's User Processes filter also consults.
bool euidCanLogin{ false };
ProcessStatus status{ ProcessStatus::Other };
// CPU
int userUsage{ 0 };
int sysUsage{ 0 };
long userTime{ 0 };
long sysTime{ 0 };
// Memory (kilobytes)
long vmSize{ 0 };
long vmRSS{ 0 };
// Private memory: resident pages the process does not share with anyone else. This is what
// ksysguard6 shows in its Memory column (VmRSS - shared, from statm), and -1 means the kernel
// did not report the shared breakdown so the display falls back to vmRSS.
long vmURSS{ -1 };
long vmPSS{ 0 };
long sharedMem{ 0 };
// True when the kernel reported the shared-memory breakdown, so vmURSS could be derived.
bool hasSharedInfo{ false };
// Network (bytes, -1 if not available)
long netDownload{ -1 };
long netUpload{ -1 };
// GPU (percentage, -1 if not available)
int gpuUsage{ -1 };
long gpuMemory{ -1 };
// Disk I/O totals (bytes, -1 if not available)
long long ioReadBytes{ -1 };
long long ioWriteBytes{ -1 };
// Other
int niceLevel{ 0 };
int numThreads{ 0 };
// The complete command line, reconstructed from argv. The command column uses only the
// executable name, while this value is used by actions that need the original arguments.
std::string commandLine;
std::string command;
// Controlling terminal device number, 0 when the process has none.
long ttyNr{ 0 };
// Friendly Linux tty name (for example, "pts/2"). Empty when there is no controlling tty.
std::string tty;
// PID of the process tracing (debugging) this one, 0 when it is not traced.
long tracerPid{ 0 };
// Process start time in clock ticks since boot (stat field 22). Together with the pid it
// identifies one incarnation of a process, which matters because pids are recycled.
long long startTime{ 0 };
// Resolved icon file for this process, empty when none could be found.
std::string iconPath;
// Sorting helpers
/** Private memory when known, otherwise plain RSS. */
long getMemoryForSort() const { return vmURSS >= 0 ? vmURSS : vmRSS; }
int getCpuForSort() const { return userUsage + sysUsage; }
// Formatted display strings
std::string formatMemory() const;
std::string formatSharedMem() const;
std::string formatCpu() const;
std::string formatGpuUsage() const;
std::string formatGpuMemory() const;
std::string formatDownload() const;
std::string formatUpload() const;
std::string formatCpuTime( long ticksPerSecond ) const;
std::string formatRelativeStartTime( double uptimeSeconds, long ticksPerSecond ) const;
};
/** Formats a KiB amount using the largest fitting binary unit (K/M/G/T/P), matching the short
* form used by the original process table (e.g. "0 K", "853.8 M", "4.2 G").
* Returns an empty string for negative input. */
std::string formatKiB( long kib );
/** Formats a KiB amount using IEC units (KiB/MiB/GiB/TiB/PiB), used by the status bar.
* Returns an empty string for negative input. */
std::string formatKiBIEC( long kib );
/** Formats a byte-per-second amount using the largest fitting binary unit (B/K/M/G).
* Returns an empty string for zero or negative input. */
std::string formatBytesPerSecond( long bytes );
/** Formats a byte total using binary units. Returns an empty string for negative input. */
std::string formatBytes( long long bytes );
} // namespace eproc
#endif // EPROC_PROCESS_INFO_HPP
+527
View File
@@ -0,0 +1,527 @@
#include "process_model.hpp"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <eepp/graphics/glyphdrawable.hpp>
#include <eepp/graphics/image.hpp>
#include <eepp/graphics/pixeldensity.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <fstream>
#include <string_view>
#if EE_PLATFORM != EE_PLATFORM_WIN
#include <unistd.h>
#endif
namespace eproc {
namespace {
const char* processColumnClass( size_t column ) {
switch ( column ) {
case ProcessModel::ColIcon:
return "eproc-process-column-icon";
case ProcessModel::ColName:
return "eproc-process-column-name";
case ProcessModel::ColPid:
return "eproc-process-column-pid";
case ProcessModel::ColUsername:
return "eproc-process-column-username";
case ProcessModel::ColCpu:
return "eproc-process-column-cpu";
case ProcessModel::ColMemory:
return "eproc-process-column-memory";
case ProcessModel::ColSharedMem:
return "eproc-process-column-shared-memory";
case ProcessModel::ColGpuUsage:
return "eproc-process-column-gpu-usage";
case ProcessModel::ColGpuMemory:
return "eproc-process-column-gpu-memory";
case ProcessModel::ColDownload:
return "eproc-process-column-download";
case ProcessModel::ColUpload:
return "eproc-process-column-upload";
case ProcessModel::ColCommand:
return "eproc-process-column-command";
case ProcessModel::ColTotalMemory:
return "eproc-process-column-total-memory";
case ProcessModel::ColVirtualSize:
return "eproc-process-column-virtual-size";
case ProcessModel::ColCpuTime:
return "eproc-process-column-cpu-time";
case ProcessModel::ColNiceness:
return "eproc-process-column-niceness";
case ProcessModel::ColRelativeStartTime:
return "eproc-process-column-relative-start-time";
case ProcessModel::ColTty:
return "eproc-process-column-tty";
case ProcessModel::ColIoRead:
return "eproc-process-column-io-read";
case ProcessModel::ColIoWrite:
return "eproc-process-column-io-write";
default:
return "eproc-process-column-base";
}
}
} // namespace
ProcessModel::ProcessModel( UISceneNode* ui ) : mUI( ui ) {}
ProcessModel::~ProcessModel() {}
size_t ProcessModel::rowCount( const ModelIndex& ) const {
return mFilteredProcesses.size();
}
size_t ProcessModel::columnCount( const ModelIndex& ) const {
return ColCount;
}
std::string ProcessModel::columnName( const size_t& column ) const {
switch ( column ) {
case ColIcon:
return "";
case ColName:
return mUI->i18n( "eproc_column_name", "Name" ).toUtf8();
case ColPid:
return mUI->i18n( "eproc_column_pid", "PID" ).toUtf8();
case ColUsername:
return mUI->i18n( "eproc_column_username", "Username" ).toUtf8();
case ColCpu:
return mUI->i18n( "eproc_column_cpu", "CPU %" ).toUtf8();
case ColMemory:
return mUI->i18n( "eproc_column_memory", "Memory" ).toUtf8();
case ColSharedMem:
return mUI->i18n( "eproc_column_shared_memory", "Shared Mem" ).toUtf8();
case ColGpuUsage:
return mUI->i18n( "eproc_column_gpu_usage", "GPU Usage" ).toUtf8();
case ColGpuMemory:
return mUI->i18n( "eproc_column_gpu_memory", "GPU Memory" ).toUtf8();
case ColDownload:
return mUI->i18n( "eproc_column_download", "Download" ).toUtf8();
case ColUpload:
return mUI->i18n( "eproc_column_upload", "Upload" ).toUtf8();
case ColCommand:
return mUI->i18n( "eproc_column_command", "Command" ).toUtf8();
case ColTotalMemory:
return mUI->i18n( "eproc_column_total_memory", "Total Memory" ).toUtf8();
case ColVirtualSize:
return mUI->i18n( "eproc_column_virtual_size", "Virtual Size" ).toUtf8();
case ColCpuTime:
return mUI->i18n( "eproc_column_cpu_time", "CPU Time" ).toUtf8();
case ColNiceness:
return mUI->i18n( "eproc_column_niceness", "Niceness" ).toUtf8();
case ColRelativeStartTime:
return mUI->i18n( "eproc_column_relative_start_time", "Relative Start Time" ).toUtf8();
case ColTty:
return mUI->i18n( "eproc_column_tty", "TTY" ).toUtf8();
case ColIoRead:
return mUI->i18n( "eproc_column_io_read", "IO Read" ).toUtf8();
case ColIoWrite:
return mUI->i18n( "eproc_column_io_write", "IO Write" ).toUtf8();
default:
return {};
}
}
Variant ProcessModel::data( const ModelIndex& index, ModelRole role ) const {
const std::string EMPTY = "";
if ( role == ModelRole::Class ) {
const char* cls = processColumnClass( index.column() );
return cls ? Variant( cls ) : Variant();
}
if ( role == ModelRole::Icon ) {
// The icon lives in its own column so that every icon lines up, instead of padding the
// name text.
const auto* proc = getProcessByRow( index.row() );
if ( !proc || index.column() != ColIcon || proc->iconPath.empty() )
return Variant();
return Variant( iconFor( proc->iconPath ) );
}
if ( role == ModelRole::Sort ) {
const auto* proc = getProcessByRow( index.row() );
if ( !proc )
return Variant();
switch ( index.column() ) {
case ColPid:
return Variant( static_cast<Int64>( proc->pid ) );
case ColCpu:
return Variant( static_cast<Int64>( proc->getCpuForSort() ) );
case ColMemory:
return Variant( static_cast<Int64>( proc->getMemoryForSort() ) );
case ColSharedMem:
return Variant( static_cast<Int64>( proc->sharedMem ) );
case ColGpuUsage:
return Variant( static_cast<Int64>( proc->gpuUsage ) );
case ColGpuMemory:
return Variant( static_cast<Int64>( proc->gpuMemory ) );
case ColDownload:
return Variant( static_cast<Int64>( proc->netDownload ) );
case ColUpload:
return Variant( static_cast<Int64>( proc->netUpload ) );
case ColTotalMemory:
return Variant( static_cast<Int64>( proc->vmRSS ) );
case ColVirtualSize:
return Variant( static_cast<Int64>( proc->vmSize ) );
case ColCpuTime:
return Variant( static_cast<Int64>( proc->userTime ) + proc->sysTime );
case ColNiceness:
return Variant( static_cast<Int64>( proc->niceLevel ) );
case ColRelativeStartTime:
return Variant( static_cast<Int64>( proc->startTime ) );
case ColTty:
return Variant( static_cast<Int64>( proc->ttyNr ) );
case ColIoRead:
return Variant( static_cast<Int64>( proc->ioReadBytes ) );
case ColIoWrite:
return Variant( static_cast<Int64>( proc->ioWriteBytes ) );
default:
break;
}
}
if ( role != ModelRole::Display )
return Variant();
const auto* proc = getProcessByRow( index.row() );
if ( !proc )
return Variant();
switch ( index.column() ) {
case ColName:
return Variant( proc->name );
case ColPid:
return Variant( String::toString( static_cast<Int64>( proc->pid ) ) );
case ColUsername:
return Variant( proc->username );
case ColCpu: {
std::string cpu = proc->formatCpu();
return Variant( cpu );
}
case ColMemory:
return Variant( proc->formatMemory() );
case ColSharedMem:
return Variant( proc->formatSharedMem() );
case ColGpuUsage:
return Variant( proc->formatGpuUsage() );
case ColGpuMemory:
return Variant( proc->formatGpuMemory() );
case ColDownload:
return Variant( proc->formatDownload() );
case ColUpload:
return Variant( proc->formatUpload() );
case ColCommand:
return Variant( proc->command );
case ColTotalMemory:
return Variant( formatKiB( proc->vmRSS ) );
case ColVirtualSize:
return Variant( formatKiB( proc->vmSize ) );
case ColCpuTime:
return Variant( proc->formatCpuTime( mSystemInfo.clockTicksPerSecond ) );
case ColNiceness:
return Variant( String::toString( static_cast<Int64>( proc->niceLevel ) ) );
case ColRelativeStartTime:
return Variant( proc->formatRelativeStartTime( mSystemInfo.uptimeSeconds,
mSystemInfo.clockTicksPerSecond ) );
case ColTty:
return Variant( proc->tty );
case ColIoRead:
return Variant( formatBytes( proc->ioReadBytes ) );
case ColIoWrite:
return Variant( formatBytes( proc->ioWriteBytes ) );
default:
return Variant( EMPTY );
}
}
ModelIndex ProcessModel::index( int row, int column, const ModelIndex& ) const {
return createIndex( row, column );
}
void ProcessModel::applySnapshot( std::vector<ProcessInfo>&& processes,
const SystemInfo& sysInfo ) {
// Keep processes that disappeared from the latest snapshot for one more update. This mirrors
// ksysguard's Ended state and is especially useful when a process exits between two refreshes:
// its last known row remains visible, but is marked as ended by the view.
UnorderedMap<long, long long> currentStartTimes;
currentStartTimes.reserve( processes.size() );
for ( const auto& process : processes )
currentStartTimes[process.pid] = process.startTime;
std::vector<ProcessInfo> endedProcesses;
endedProcesses.reserve( mProcesses.size() );
for ( auto& previous : mProcesses ) {
// An ended process was already shown during the previous update. Do not keep it for a
// second update.
if ( previous.status == ProcessStatus::Ended )
continue;
auto current = currentStartTimes.find( previous.pid );
const bool processStillExists = current != currentStartTimes.end() &&
( previous.startTime == 0 || current->second == 0 ||
previous.startTime == current->second );
if ( processStillExists )
continue;
previous.status = ProcessStatus::Ended;
endedProcesses.emplace_back( std::move( previous ) );
}
processes.reserve( processes.size() + endedProcesses.size() );
for ( auto& process : endedProcesses )
processes.emplace_back( std::move( process ) );
mProcesses = std::move( processes );
mSystemInfo = sysInfo;
applyFilters();
onModelUpdate();
}
void ProcessModel::setFilter( FilterMode mode ) {
mFilterMode = mode;
applyFilters();
onModelUpdate();
}
void ProcessModel::setTextFilter( const std::string& text ) {
mTextRegex.reset();
mTextLiteral.clear();
if ( !text.empty() ) {
// Compiled here rather than per row: the filter runs over every process on every pass.
// useCache is false because the pattern changes on each keystroke: the cache is
// least-recently-used and shared with the syntax definitions, so caching every typed prefix
// would evict the patterns the tokenizer is working from.
// AllowFallback keeps the default engine behaviour: a pattern PCRE2 rejects is retried
// with Oniguruma before it is treated as invalid.
auto regex = std::make_unique<RegEx>(
text, RegEx::Options::Utf | RegEx::Options::AllowFallback | RegEx::Options::Caseless,
false );
if ( regex->isValid() ) {
mTextRegex = std::move( regex );
} else {
// An unfinished pattern matches nothing, which would blank the table while the user is
// still typing it, so it is searched for literally instead.
mTextLiteral = text;
std::transform( mTextLiteral.begin(), mTextLiteral.end(), mTextLiteral.begin(),
[]( unsigned char c ) { return std::tolower( c ); } );
}
}
applyFilters();
onModelUpdate();
}
// Case-insensitive substring test that does not allocate.
static bool containsIgnoreCase( std::string_view haystack, std::string_view lowerNeedle ) {
if ( lowerNeedle.empty() )
return true;
if ( haystack.size() < lowerNeedle.size() )
return false;
for ( size_t i = 0; i + lowerNeedle.size() <= haystack.size(); ++i ) {
size_t j = 0;
for ( ; j < lowerNeedle.size(); ++j ) {
if ( std::tolower( static_cast<unsigned char>( haystack[i + j] ) ) != lowerNeedle[j] )
break;
}
if ( j == lowerNeedle.size() )
return true;
}
return false;
}
bool ProcessModel::matchesText( const ProcessInfo& proc ) const {
if ( !mTextRegex && mTextLiteral.empty() )
return true;
// The original lets the user search by PID too. The digits are formatted into a stack buffer
// because this runs for every process on every pass while a filter is active.
char pidBuffer[24];
int pidLength = snprintf( pidBuffer, sizeof( pidBuffer ), "%ld", proc.pid );
size_t pidSize = pidLength > 0 ? static_cast<size_t>( pidLength ) : 0;
if ( mTextRegex ) {
// Each column is matched on its own, so an anchored pattern applies to every one of them.
return mTextRegex->matches( proc.name ) || mTextRegex->matches( proc.command ) ||
mTextRegex->matches( proc.username ) ||
mTextRegex->matches( pidBuffer, 0, nullptr, pidSize );
}
const std::string_view pid( pidBuffer, pidSize );
return containsIgnoreCase( proc.name, mTextLiteral ) ||
containsIgnoreCase( proc.command, mTextLiteral ) ||
containsIgnoreCase( proc.username, mTextLiteral ) ||
containsIgnoreCase( pid, mTextLiteral );
}
bool ProcessModel::accepts( const ProcessInfo& proc ) const {
switch ( mFilterMode ) {
case AllProcesses:
return true;
case SystemProcesses:
// System accounts are those below uid 100, or accounts that cannot log in.
return proc.uid < 100 || !proc.canLogin;
case UserProcesses:
// Keep a process when either its real or effective id belongs to a login-capable
// account, mirroring the original's paired check.
return ( proc.uid >= 100 && proc.canLogin ) ||
( proc.euid >= 100 && proc.euidCanLogin );
case OwnProcesses: {
#if EE_PLATFORM == EE_PLATFORM_WIN
const long own = -1;
#else
const long own = static_cast<long>( getuid() );
#endif
return proc.uid == own || proc.euid == own || proc.suid == own || proc.fsuid == own;
}
case ProgramsOnly: {
// A "program" owns a terminal or a GUI window. login/getty *are* the tty rather than
// something started from it, so the original hides them as well.
if ( proc.ttyNr == 0 && mGuiPids.count( proc.pid ) == 0 )
return false;
if ( proc.parentPid == 1 &&
( proc.name == "login" ||
proc.name.compare( std::max<int>( 0, (int)proc.name.size() - 5 ), 5, "getty" ) ==
0 ) )
return false;
return true;
}
default:
return true;
}
}
void ProcessModel::applyFilters() {
mFilteredProcesses.clear();
mFilteredProcesses.reserve( mProcesses.size() );
for ( auto& proc : mProcesses ) {
if ( !accepts( proc ) || !matchesText( proc ) )
continue;
mFilteredProcesses.push_back( &proc );
}
}
void ProcessModel::setGuiWindowPids( UnorderedSet<long>&& pids ) {
mGuiPids = std::move( pids );
}
// nanosvg substitutes its own default (white) fill for the SVG constructs it cannot parse, so an
// unsupported icon rasterizes to a blank white square. Such a result is reported as "no icon"
// rather than drawn as a misleading box.
static bool isBlankIcon( const Image& image ) {
const Uint8* pixels = image.getPixelsPtr();
const unsigned int channels = image.getChannels();
const unsigned int width = image.getWidth();
const unsigned int height = image.getHeight();
if ( !pixels || width == 0 || height == 0 || channels < 3 )
return false; // no pixel access: trust the image rather than dropping the icon
for ( unsigned int y = 0; y < height; ++y ) {
for ( unsigned int x = 0; x < width; ++x ) {
const Uint8* pixel = pixels + ( static_cast<size_t>( y ) * width + x ) * channels;
if ( channels == 4 && pixel[3] < 8 )
continue; // fully transparent
if ( pixel[0] < 240 || pixel[1] < 240 || pixel[2] < 240 )
return false; // real content
}
}
return true;
}
// The original renders 16px icons in the name column.
static constexpr int kIconSizeDp = 16;
DrawablePtr ProcessModel::iconFor( const std::string& path ) const {
auto it = mIconCache.find( path );
if ( it != mIconCache.end() )
return it->second;
// Loading is done once per icon file: the table asks for this on every cell refresh, and
// re-loading the texture each time would be pathological.
const Uint32 iconPx = static_cast<Uint32>( PixelDensity::dpToPxI( kIconSizeDp ) );
Image image;
if ( String::endsWith( path, ".svg" ) || String::endsWith( path, ".svgz" ) ) {
// Scalable icons are rasterized before resizing, so the same high-quality image resampler
// is used for both SVG and bitmap icons.
std::ifstream file( path, std::ios::binary );
std::string svg( ( std::istreambuf_iterator<char>( file ) ),
std::istreambuf_iterator<char>() );
if ( !svg.empty() ) {
Image::FormatConfiguration format;
int width = 0, height = 0, channels = 0;
if ( Image::getInfoFromMemory( reinterpret_cast<const unsigned char*>( svg.data() ),
svg.size(), &width, &height, &channels, format ) ) {
format.svgScale( iconPx / static_cast<Float>( eemax( width, height ) ) );
image = Image( reinterpret_cast<const Uint8*>( svg.data() ),
static_cast<unsigned int>( svg.size() ), 4, format );
}
}
} else {
image = Image( path, 4 );
}
// Decode at the final pixel size and use Lanczos resampling for bitmap icons. This avoids
// relying on the GPU's texture filtering to reduce large process icons at draw time.
if ( image.getPixelsPtr() && image.getWidth() > 0 && image.getHeight() > 0 ) {
if ( image.getWidth() != iconPx || image.getHeight() != iconPx )
image.resize( iconPx, iconPx, Image::RESAMPLER_LANCZOS4 );
if ( isBlankIcon( image ) )
image = Image();
}
TexturePtr texture;
if ( image.getPixelsPtr() && image.getWidth() > 0 && image.getHeight() > 0 ) {
texture = TextureFactory::instance()->loadFromPixels(
image.getPixelsPtr(), image.getWidth(), image.getHeight(), image.getChannels(), false,
Texture::ClampMode::ClampToEdge, false, false, path );
}
// The table draws a ModelRole::Icon drawable at its own size, so the icon is built already
// scaled to the row height; at native resolution it would overflow the row.
DrawablePtr drawable;
if ( texture ) {
// GlyphDrawable feeds the source rect to quadsSetTexCoord as u/v, so the rect is
// normalized: (0,0,1,1) selects the whole texture.
Rect srcRect( 0, 0, 1, 1 );
auto* glyph = GlyphDrawable::New( texture, srcRect, Sizef( iconPx, iconPx ), path );
glyph->setDrawMode( GlyphDrawable::DrawMode::Image );
glyph->setGlyphRenderMode( GlyphRenderMode::Color );
glyph->setPixelDensity( PixelDensity::getPixelDensity() );
drawable = DrawablePtr( glyph );
}
mIconCache.emplace( path, drawable );
return drawable;
}
const ProcessInfo* ProcessModel::getProcessByRow( int row ) const {
if ( row < 0 || static_cast<size_t>( row ) >= mFilteredProcesses.size() )
return nullptr;
return mFilteredProcesses[row];
}
int ProcessModel::rowForPid( long pid ) const {
for ( size_t i = 0; i < mFilteredProcesses.size(); ++i ) {
if ( mFilteredProcesses[i]->pid == pid )
return static_cast<int>( i );
}
return -1;
}
} // namespace eproc
+144
View File
@@ -0,0 +1,144 @@
#ifndef EPROC_PROCESS_MODEL_HPP
#define EPROC_PROCESS_MODEL_HPP
#include "process_collector.hpp"
#include <eepp/graphics/texturedrawable.hpp>
#include <eepp/graphics/texturefactory.hpp>
#include <eepp/system/regex.hpp>
#include <eepp/ui/models/model.hpp>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace EE::UI {
class UISceneNode;
}
using namespace EE;
using namespace EE::Graphics;
using namespace EE::System;
using namespace EE::UI;
using namespace EE::UI::Models;
namespace eproc {
/** Presents a snapshot of the process list to the views.
*
* Collection happens off the UI thread (see App::collectAsync), so this model never reads
* /proc itself: it only owns the last snapshot handed to it by applySnapshot(). All methods are
* UI-thread only. */
class ProcessModel : public Model {
public:
enum Columns : size_t {
ColIcon = 0,
ColName,
ColPid,
ColUsername,
ColCpu,
ColMemory,
ColSharedMem,
ColGpuUsage,
ColGpuMemory,
ColDownload,
ColUpload,
ColTotalMemory,
ColVirtualSize,
ColCpuTime,
ColNiceness,
ColRelativeStartTime,
ColTty,
ColIoRead,
ColIoWrite,
ColCommand,
ColCount
};
/** Mirrors ksysguard6's ProcessFilter::State, minus the two tree variants (this model is
* flat). The numeric order matches the original enum so the dropdown maps directly. */
enum FilterMode {
AllProcesses = 0,
SystemProcesses,
UserProcesses,
OwnProcesses,
ProgramsOnly,
FilterModeCount
};
static std::shared_ptr<ProcessModel> create( UISceneNode* ui ) {
return std::shared_ptr<ProcessModel>( new ProcessModel( ui ) );
}
~ProcessModel();
size_t rowCount( const ModelIndex& = ModelIndex() ) const override;
size_t columnCount( const ModelIndex& = ModelIndex() ) const override;
std::string columnName( const size_t& column ) const override;
Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const override;
ModelIndex index( int row, int column = 0,
const ModelIndex& parent = ModelIndex() ) const override;
/** Replaces the snapshot and notifies the views. UI thread only. */
void applySnapshot( std::vector<ProcessInfo>&& processes, const SystemInfo& sysInfo );
void setFilter( FilterMode mode );
/** Quick search: a case-insensitive regular expression matched against the name, command,
* username and PID columns (the original also lets the user search by PID). */
void setTextFilter( const std::string& text );
/** PIDs owning a top-level window, used by the Programs Only filter. Set this before
* applySnapshot so the filter sees current data. */
void setGuiWindowPids( UnorderedSet<long>&& pids );
FilterMode getFilter() const { return mFilterMode; }
const SystemInfo& getSystemInfo() const { return mSystemInfo; }
/** Number of rows currently visible after filtering. */
size_t visibleCount() const { return mFilteredProcesses.size(); }
const ProcessInfo* getProcessByRow( int row ) const;
/** Row of the currently visible list holding @p pid, or -1 when it is filtered out or gone.
* Used to re-select a process by identity across a snapshot, since rows reorder. */
int rowForPid( long pid ) const;
/** The icon column carries no data of its own, so sorting it is meaningless. */
bool isColumnSortable( const size_t& columnIndex ) const override {
return columnIndex != ColIcon;
}
/** Enables per-column CSS classes for process table cells. */
bool classModelRoleEnabled() override { return true; }
private:
explicit ProcessModel( UISceneNode* ui );
void applyFilters();
/** Applies the active filter mode to a single process, mirroring the original's predicates. */
bool accepts( const ProcessInfo& proc ) const;
bool matchesText( const ProcessInfo& proc ) const;
/** Loads (once) and caches the drawable for an icon file. */
DrawablePtr iconFor( const std::string& path ) const;
std::vector<ProcessInfo> mProcesses;
std::vector<ProcessInfo*> mFilteredProcesses;
UISceneNode* mUI{ nullptr };
SystemInfo mSystemInfo;
FilterMode mFilterMode{ AllProcesses };
// Compiled once per filter change, never per row. Null means "no text filter".
std::unique_ptr<RegEx> mTextRegex;
// Set only when the typed text does not compile as a pattern (a group still open, a stray
// quantifier): it is then searched literally, lowercased, so a half-typed pattern does not
// blank the table. Empty when it is not in use.
std::string mTextLiteral;
UnorderedSet<long> mGuiPids;
mutable UnorderedMap<std::string, DrawablePtr> mIconCache;
};
} // namespace eproc
#endif // EPROC_PROCESS_MODEL_HPP