diff --git a/premake4.lua b/premake4.lua index 19b207af5..ac2776502 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1999,7 +1999,7 @@ solution "eepp" "src/tools/eproc/platform/linux/gpu_reader_nvidia.cpp", "src/tools/eproc/platform/linux/gpu_reader_drm.cpp", "src/tools/eproc/platform/linux/process_collector_linux.cpp", - "src/tools/eproc/platform/linux/process_icon_resolver.cpp", + "src/tools/eproc/platform/posix/process_icon_resolver.cpp", "src/tools/eproc/platform/linux/process_network_monitor.cpp", } end @@ -2007,6 +2007,15 @@ solution "eepp" files { "src/tools/eproc/platform/windows/process_collector_windows.cpp" } links { "psapi", "advapi32" } end + if os.is("macosx") then + files { "src/tools/eproc/platform/macos/process_collector_macos.cpp", + "src/tools/eproc/platform/macos/process_icon_resolver_macos.cpp" } + links { "CoreFoundation.framework", "CoreGraphics.framework", "ImageIO.framework" } + end + if os.is("bsd") then + files { "src/tools/eproc/platform/freebsd/process_collector_freebsd.cpp", + "src/tools/eproc/platform/posix/process_icon_resolver.cpp" } + end build_link_configuration( "eproc", false ) -- Tests @@ -2046,6 +2055,12 @@ solution "eepp" if os.is("haiku") then links { "bsd", "network" } end + if os.is("macosx") then + links { "CoreFoundation.framework", "CoreGraphics.framework", "ImageIO.framework" } + files { "src/tools/eproc/process_collector.cpp", + "src/tools/eproc/platform/macos/process_collector_macos.cpp", + "src/tools/eproc/platform/macos/process_icon_resolver_macos.cpp" } + end files { "src/tests/unit_tests/*.cpp", "src/tools/eproc/process_info.cpp", "src/tools/eproc/process_model.cpp", diff --git a/premake5.lua b/premake5.lua index a78c6299a..5706420d6 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1992,6 +1992,7 @@ workspace "eepp" linkoptions { _MAIN_SCRIPT_DIR .. "/bin/assets/icon/eeiv.res" } filter { "system:windows", "action:not vs*", "architecture:x86_64" } linkoptions { _MAIN_SCRIPT_DIR .. "/bin/assets/icon/eeiv.x64.res" } + filter {} files { "src/tools/eeiv/*.cpp" } build_link_configuration( "eeiv", false ) @@ -2006,6 +2007,7 @@ workspace "eepp" linkoptions { _MAIN_SCRIPT_DIR .. "/bin/assets/icon/eproc.res" } filter { "system:windows", "action:not vs*", "architecture:x86_64" } linkoptions { _MAIN_SCRIPT_DIR .. "/bin/assets/icon/eproc.x64.res" } + filter {} files { "src/tools/eproc/appconfig.cpp", "src/tools/eproc/eproc.cpp", @@ -2020,14 +2022,21 @@ workspace "eepp" "src/tools/eproc/platform/linux/gpu_reader_nvidia.cpp", "src/tools/eproc/platform/linux/gpu_reader_drm.cpp", "src/tools/eproc/platform/linux/process_collector_linux.cpp", - "src/tools/eproc/platform/linux/process_icon_resolver.cpp", + "src/tools/eproc/platform/posix/process_icon_resolver.cpp", "src/tools/eproc/platform/linux/process_network_monitor.cpp", } filter "system:windows" files { "src/tools/eproc/platform/windows/process_collector_windows.cpp" } links { "psapi", "advapi32" } + filter "system:macosx" + files { "src/tools/eproc/platform/macos/process_collector_macos.cpp", + "src/tools/eproc/platform/macos/process_icon_resolver_macos.cpp" } + links { "CoreFoundation.framework", "CoreGraphics.framework", "ImageIO.framework" } + filter "system:bsd" + files { "src/tools/eproc/platform/freebsd/process_collector_freebsd.cpp", + "src/tools/eproc/platform/posix/process_icon_resolver.cpp" } filter {} - build_link_configuration( "eproc", true ) + build_link_configuration( "eproc", false ) -- Tests project "eepp-test" @@ -2073,6 +2082,11 @@ workspace "eepp" "src/tools/ecode/plugins/autocomplete/usersnippetstore.cpp" } filter { "system:not windows", "system:not haiku" } links { "pthread" } + filter "system:macosx" + links { "CoreFoundation.framework", "CoreGraphics.framework", "ImageIO.framework" } + files { "src/tools/eproc/process_collector.cpp", + "src/tools/eproc/platform/macos/process_collector_macos.cpp", + "src/tools/eproc/platform/macos/process_icon_resolver_macos.cpp" } filter "system:haiku" links { "bsd", "network" } filter {} diff --git a/src/tests/unit_tests/eproc_macos_collector_tests.cpp b/src/tests/unit_tests/eproc_macos_collector_tests.cpp new file mode 100644 index 000000000..88684c6e1 --- /dev/null +++ b/src/tests/unit_tests/eproc_macos_collector_tests.cpp @@ -0,0 +1,83 @@ +#include + +#if EE_PLATFORM == EE_PLATFORM_MACOS +#include "../../tools/eproc/platform/macos/process_icon_resolver_macos.hpp" +#include "../../tools/eproc/process_collector.hpp" +#include "utest.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace eproc; + +UTEST( EProcMacOSCollector, CollectsCurrentProcessAndUsesMicrosecondCpuTime ) { + auto collector = ProcessCollector::create(); + ASSERT_TRUE( collector != nullptr ); + std::vector processes; + SystemInfo system; + ASSERT_TRUE( collector->collect( processes, system ) ); + const auto current = + std::find_if( processes.begin(), processes.end(), + []( const ProcessInfo& proc ) { return proc.pid == getpid(); } ); + ASSERT_TRUE( current != processes.end() ); + EXPECT_EQ( system.clockTicksPerSecond, 1'000'000 ); + EXPECT_TRUE( system.cpuCount > 0 ); + EXPECT_TRUE( system.totalMemory > 0 ); + EXPECT_TRUE( current->ownedByCurrentUser ); + EXPECT_TRUE( current->vmRSS > 0 ); + EXPECT_TRUE( current->startTime > 0 ); + + rusage usage{}; + ASSERT_EQ( getrusage( RUSAGE_SELF, &usage ), 0 ); + const Int64 userUs = + static_cast( usage.ru_utime.tv_sec ) * 1'000'000 + usage.ru_utime.tv_usec; + // A broad bound tolerates the work between the two samples but catches treating Mach ticks as + // nanoseconds, which reports about 1/40 of actual CPU time on Apple Silicon. + if ( userUs > 10'000 ) { + EXPECT_TRUE( current->userTime > userUs / 2 ); + EXPECT_TRUE( current->userTime < userUs * 2 + 1'000'000 ); + } + const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds( 100 ); + while ( std::chrono::steady_clock::now() < end ) { + // Keep the process busy across a second CPU sample. + } + ASSERT_TRUE( collector->collect( processes, system ) ); + const auto second = + std::find_if( processes.begin(), processes.end(), + []( const ProcessInfo& proc ) { return proc.pid == getpid(); } ); + ASSERT_TRUE( second != processes.end() ); + EXPECT_TRUE( second->getCpuForSort() > 0 ); + EXPECT_TRUE( system.cpuUsage >= 0.f && system.cpuUsage <= 100.f ); +} + +UTEST( EProcMacOSCollector, ResolvesDeclaredBundleIcon ) { + char directory[] = "/private/tmp/eproc-icon-test-XXXXXX"; + ASSERT_TRUE( mkdtemp( directory ) != nullptr ); + const std::filesystem::path root( directory ); + const auto contents = root / "Example.app/Contents"; + std::filesystem::create_directories( contents / "Resources" ); + std::filesystem::create_directories( contents / "MacOS" ); + { + std::ofstream plist( contents / "Info.plist" ); + plist << "" + "" + "" + "CFBundleExecutableExample" + "CFBundleIdentifierorg.eepp.eproc.icon-test" + "CFBundleIconFileExampleIcon" + ""; + } + std::ofstream( contents / "Resources/ExampleIcon.icns" ).put( '\0' ); + std::ofstream( contents / "MacOS/Example" ).put( '\0' ); + ProcessIconResolverMacOS resolver; + const std::string resolved = resolver.resolve( ( contents / "MacOS/Example" ).string() ); + EXPECT_TRUE( resolved == ( contents / "Resources/ExampleIcon.icns" ).string() ); + std::filesystem::remove_all( root ); +} +#endif diff --git a/src/tools/eproc/eproc.cpp b/src/tools/eproc/eproc.cpp index 6697dc95b..9b45d08be 100644 --- a/src/tools/eproc/eproc.cpp +++ b/src/tools/eproc/eproc.cpp @@ -11,7 +11,8 @@ #include #include #include -#if EE_PLATFORM == EE_PLATFORM_LINUX +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ + EE_PLATFORM == EE_PLATFORM_BSD #include #include #endif @@ -22,7 +23,8 @@ namespace { constexpr int kProcessTableStateVersion = 4; constexpr int kForceKillSignal = 9; // SIGKILL on Linux; TerminateProcess on Windows. -#if EE_PLATFORM == EE_PLATFORM_LINUX +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ + EE_PLATFORM == EE_PLATFORM_BSD constexpr int kEndProcessSignal = SIGTERM; #else constexpr int kEndProcessSignal = kForceKillSignal; @@ -66,6 +68,26 @@ constexpr std::array kUnavailableProcessColumns = { { ProcessModel::ColNiceness, ProcessModel::ColTty, } }; +#elif EE_PLATFORM == EE_PLATFORM_MACOS +constexpr std::array kUnavailableProcessColumns = { { + ProcessModel::ColSharedMem, + ProcessModel::ColGpuUsage, + ProcessModel::ColGpuMemory, + ProcessModel::ColDownload, + ProcessModel::ColUpload, + ProcessModel::ColTty, +} }; +#elif EE_PLATFORM == EE_PLATFORM_BSD +constexpr std::array kUnavailableProcessColumns = { { + ProcessModel::ColSharedMem, + ProcessModel::ColGpuUsage, + ProcessModel::ColGpuMemory, + ProcessModel::ColDownload, + ProcessModel::ColUpload, + ProcessModel::ColTty, + ProcessModel::ColIoRead, + ProcessModel::ColIoWrite, +} }; #else constexpr std::array kUnavailableProcessColumns{}; #endif @@ -245,7 +267,11 @@ App::App() { mConfig->load(); } -App::~App() {} +App::~App() { + // The worker may still publish a snapshot through mStagingMutex. Join it before member + // destruction reaches the staging state and mutex. + mThreadPool.reset(); +} int App::run( int argc, char* argv[] ) { args::ArgumentParser parser( "eproc" ); @@ -760,6 +786,9 @@ void App::setupProcessTable() { view->setColumnsHidden( std::vector( kOptionalProcessColumns.begin(), kOptionalProcessColumns.end() ), true ); +#if EE_PLATFORM == EE_PLATFORM_MACOS + view->setColumnHidden( ProcessModel::ColTotalMemory, false ); +#endif hideUnsupportedProcessColumns( *view ); view->setOnUpdateCellCb( [this]( UITableCell* cell, Model* ) { if ( !cell ) @@ -817,7 +846,7 @@ void App::setupProcessTable() { restoreSelection( selected ); } ); } ); - view->setRowHeight( 28 ); + view->setRowHeight( PixelDensity::dpToPx( 18 ) ); // The flexible column is Name; the icon column is fixed so every row lines up. view->setMainColumn( ProcessModel::ColName ); view->setSortIconSize( 12 ); @@ -857,8 +886,8 @@ void App::startCollection() { // 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). + // guarantees no in-flight collect() can outlive mCollector. App's destructor explicitly + // joins the pool before destroying the staging mutex that the worker uses. mThreadPool = ThreadPool::createShared( 1, false ); // Timed from this first dispatch so the next sample lands a full period later, giving the CPU @@ -1075,10 +1104,15 @@ void App::updateStatusBar() { 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() ) ); + if ( mSwapText ) { + mSwapText->setVisible( sys.totalSwap >= 0 ); + if ( sys.totalSwap >= 0 ) { + mSwapText->setText( String::format( + mApp->getUI()->i18n( "eproc_swap_status_format", "Swap: %s / %s" ).toUtf8(), + formatKiBIEC( sys.getUsedSwapKB() ).c_str(), + formatKiBIEC( sys.totalSwap ).c_str() ) ); + } + } } void App::onEndProcess() { @@ -1269,7 +1303,8 @@ void App::showProcessContextMenu( const ModelIndex& proxyIndex ) { }; // The same signal set the original offers, in the same order. -#if EE_PLATFORM == EE_PLATFORM_LINUX +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ + EE_PLATFORM == EE_PLATFORM_BSD static const std::array signalItems = { { { "signal-stop", "eproc_signal_suspend", "Suspend (STOP)", SIGSTOP }, { "signal-cont", "eproc_signal_continue", "Continue (CONT)", SIGCONT }, @@ -1286,7 +1321,8 @@ void App::showProcessContextMenu( const ModelIndex& proxyIndex ) { UIPopUpMenu* menu = UIPopUpMenu::New(); menu->setId( "process_context_menu" ); -#if EE_PLATFORM == EE_PLATFORM_LINUX +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ + EE_PLATFORM == EE_PLATFORM_BSD UIPopUpMenu* signalMenu = UIPopUpMenu::New(); signalMenu->setId( "process_signal_menu" ); for ( const auto& item : signalItems ) @@ -1307,7 +1343,8 @@ void App::showProcessContextMenu( const ModelIndex& proxyIndex ) { ->setId( "copy-command-line" ); } -#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_WIN +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_WIN || \ + EE_PLATFORM == EE_PLATFORM_MACOS || EE_PLATFORM == EE_PLATFORM_BSD menu->addSeparator(); menu->add( endProcessLabel( mApp->getUI(), pids.size() ) )->setId( "end-process" ); menu->add( forceKillLabel( mApp->getUI(), pids.size() ) )->setId( "kill-process" ); diff --git a/src/tools/eproc/platform/freebsd/process_collector_freebsd.cpp b/src/tools/eproc/platform/freebsd/process_collector_freebsd.cpp new file mode 100644 index 000000000..cf72d625a --- /dev/null +++ b/src/tools/eproc/platform/freebsd/process_collector_freebsd.cpp @@ -0,0 +1,195 @@ +#ifdef __FreeBSD__ +#include "process_collector_freebsd.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eproc { + +namespace { + +constexpr Int64 kMicrosecondsPerSecond = 1'000'000; + +template bool readSysctl( const char* name, T& value ) { + size_t size = sizeof( value ); + return sysctlbyname( name, &value, &size, nullptr, 0 ) == 0 && size == sizeof( value ); +} + +Uint64 microseconds( const timeval& time ) { + return static_cast( time.tv_sec ) * kMicrosecondsPerSecond + time.tv_usec; +} + +int cpuPercent( Uint64 elapsed, Int64 wallMicroseconds ) { + if ( wallMicroseconds <= 0 ) + return 0; + const double percent = static_cast( elapsed ) * 100.0 / wallMicroseconds; + return static_cast( + std::min( percent, static_cast( std::numeric_limits::max() ) ) ); +} + +std::string executablePath( pid_t pid ) { + int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid }; + char path[MAXPATHLEN]; + size_t length = sizeof( path ); + if ( sysctl( mib, 4, path, &length, nullptr, 0 ) != 0 || length <= 1 || + length > sizeof( path ) ) + return {}; + path[std::min( length - 1, sizeof( path ) - 1 )] = '\0'; + return path; +} + +} // namespace + +bool ProcessCollectorFreeBSD::collect( std::vector& processes, SystemInfo& sysInfo ) { + const auto sampledAt = std::chrono::steady_clock::now(); + const Int64 elapsedUs = + mPrevSampleTime.time_since_epoch().count() == 0 + ? 0 + : std::chrono::duration_cast( sampledAt - mPrevSampleTime ) + .count(); + mPrevSampleTime = sampledAt; + ++mPass; + processes.clear(); + sysInfo = {}; + sysInfo.clockTicksPerSecond = kMicrosecondsPerSecond; + sysInfo.totalSwap = -1; + sysInfo.freeSwap = -1; + + int cpuCount = 0; + if ( readSysctl( "hw.ncpu", cpuCount ) && cpuCount > 0 ) + sysInfo.cpuCount = cpuCount; + uint64_t totalBytes = 0; + if ( readSysctl( "hw.physmem", totalBytes ) ) + sysInfo.totalMemory = static_cast( totalBytes / 1024 ); + unsigned int freePages = 0; + unsigned int cachePages = 0; + if ( readSysctl( "vm.stats.vm.v_free_count", freePages ) ) { + const Uint64 pageKiB = static_cast( getpagesize() ) / 1024; + sysInfo.freeMemory = freePages * pageKiB; + // The inactive page count gives a cheap approximation of reclaimable memory. + readSysctl( "vm.stats.vm.v_inactive_count", cachePages ); + const Int64 available = + static_cast( ( freePages + static_cast( cachePages ) ) * pageKiB ); + sysInfo.availableMemory = + sysInfo.totalMemory > 0 ? std::min( sysInfo.totalMemory, available ) : available; + } + // FreeBSD swap accounting needs swap device enumeration; leave both fields unavailable here. + + std::array cpuTicks{}; + if ( readSysctl( "kern.cp_time", cpuTicks ) ) { + Uint64 total = 0; + for ( long ticks : cpuTicks ) + total += static_cast( ticks ); + const Uint64 idle = static_cast( cpuTicks[CP_IDLE] ); + if ( mPrevTotalCpu && total > mPrevTotalCpu && idle >= mPrevIdleCpu ) { + const Uint64 delta = total - mPrevTotalCpu; + mLastCpuUsage = static_cast( std::min( + 100.0, 100.0 * + static_cast( delta - std::min( delta, idle - mPrevIdleCpu ) ) / + delta ) ); + } + mPrevTotalCpu = total; + mPrevIdleCpu = idle; + } + sysInfo.cpuUsage = mLastCpuUsage; + + timeval bootTime{}; + readSysctl( "kern.boottime", bootTime ); + timeval now{}; + gettimeofday( &now, nullptr ); + sysInfo.uptimeSeconds = + bootTime.tv_sec > 0 ? std::max( 0.0, static_cast( now.tv_sec - bootTime.tv_sec ) + + ( now.tv_usec - bootTime.tv_usec ) / 1e6 ) + : 0.0; + + int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; + size_t bytes = 0; + if ( sysctl( mib, 3, nullptr, &bytes, nullptr, 0 ) != 0 ) + return false; + std::vector entries; + bool fetched = false; + for ( int attempt = 0; attempt < 3; ++attempt ) { + entries.resize( bytes / sizeof( kinfo_proc ) + 64 ); + size_t capacity = entries.size() * sizeof( kinfo_proc ); + if ( sysctl( mib, 3, entries.data(), &capacity, nullptr, 0 ) == 0 ) { + entries.resize( capacity / sizeof( kinfo_proc ) ); + fetched = true; + break; + } + if ( errno != ENOMEM ) + break; + bytes = capacity + 64 * sizeof( kinfo_proc ); + } + if ( !fetched ) + return false; + + processes.reserve( entries.size() ); + const uid_t currentUid = getuid(); + const Uint64 bootUs = microseconds( bootTime ); + const Uint64 pageKiB = static_cast( getpagesize() ) / 1024; + for ( const kinfo_proc& info : entries ) { + if ( info.ki_pid <= 0 ) + continue; + ProcessInfo proc; + proc.pid = info.ki_pid; + proc.parentPid = info.ki_ppid; + proc.name = info.ki_comm; + proc.uid = info.ki_ruid; + proc.euid = info.ki_uid; + proc.suid = info.ki_svuid; + const auto& account = mAccounts.get( static_cast( proc.uid ) ); + proc.username = account.name; + proc.canLogin = account.canLogin; + proc.euidCanLogin = mAccounts.get( static_cast( proc.euid ) ).canLogin; + proc.ownedByCurrentUser = proc.uid == currentUid || proc.euid == currentUid; + proc.systemProcess = proc.uid == 0 || ( !account.name.empty() && !account.canLogin ); + proc.userProcess = !proc.systemProcess && !account.name.empty(); + proc.numThreads = info.ki_numthreads; + proc.niceLevel = info.ki_nice; + proc.vmSize = static_cast( info.ki_size / 1024 ); + proc.vmRSS = static_cast( info.ki_rssize * pageKiB ); + proc.userTime = static_cast( microseconds( info.ki_rusage.ru_utime ) ); + proc.sysTime = static_cast( microseconds( info.ki_rusage.ru_stime ) ); + const Uint64 startUs = microseconds( info.ki_start ); + proc.startTime = + bootTime.tv_sec > 0 && startUs >= bootUs ? static_cast( startUs - bootUs ) : 0; + proc.commandLine = proc.name; + + const Uint64 user = static_cast( proc.userTime ); + const Uint64 kernel = static_cast( proc.sysTime ); + TickEntry& previous = mProcessTicks[proc.pid]; + if ( previous.pass == mPass - 1 && previous.startTime == startUs && + user >= previous.userTime && kernel >= previous.sysTime ) { + proc.userUsage = cpuPercent( user - previous.userTime, elapsedUs ); + proc.sysUsage = cpuPercent( kernel - previous.sysTime, elapsedUs ); + } + if ( previous.pass == 0 || previous.startTime != startUs ) { + previous.iconPath = mIconResolver.iconFor( executablePath( info.ki_pid ), proc.name ); + } + proc.iconPath = previous.iconPath; + previous.startTime = startUs; + previous.userTime = user; + previous.sysTime = kernel; + previous.pass = mPass; + processes.emplace_back( std::move( proc ) ); + } + for ( auto it = mProcessTicks.begin(); it != mProcessTicks.end(); ) { + if ( it->second.pass != mPass ) + it = mProcessTicks.erase( it ); + else + ++it; + } + return true; +} + +} // namespace eproc +#endif // __FreeBSD__ diff --git a/src/tools/eproc/platform/freebsd/process_collector_freebsd.hpp b/src/tools/eproc/platform/freebsd/process_collector_freebsd.hpp new file mode 100644 index 000000000..63a7f3a06 --- /dev/null +++ b/src/tools/eproc/platform/freebsd/process_collector_freebsd.hpp @@ -0,0 +1,37 @@ +#ifndef EPROC_PROCESS_COLLECTOR_FREEBSD_HPP +#define EPROC_PROCESS_COLLECTOR_FREEBSD_HPP + +#include "../../process_collector.hpp" +#include "../posix/process_account.hpp" +#include "../posix/process_icon_resolver.hpp" +#include +#include + +namespace eproc { + +class ProcessCollectorFreeBSD final : public ProcessCollector { + public: + bool collect( std::vector& processes, SystemInfo& sysInfo ) override; + + private: + struct TickEntry { + std::string iconPath; + Uint64 startTime{ 0 }; + Uint64 userTime{ 0 }; + Uint64 sysTime{ 0 }; + Uint32 pass{ 0 }; + }; + + std::unordered_map mProcessTicks; + ProcessAccounts mAccounts; + ProcessIconResolver mIconResolver; + std::chrono::steady_clock::time_point mPrevSampleTime; + Uint64 mPrevTotalCpu{ 0 }; + Uint64 mPrevIdleCpu{ 0 }; + float mLastCpuUsage{ 0.f }; + Uint32 mPass{ 0 }; +}; + +} // namespace eproc + +#endif // EPROC_PROCESS_COLLECTOR_FREEBSD_HPP diff --git a/src/tools/eproc/platform/linux/process_collector_linux.hpp b/src/tools/eproc/platform/linux/process_collector_linux.hpp index c920ccfdb..0b6f4235a 100644 --- a/src/tools/eproc/platform/linux/process_collector_linux.hpp +++ b/src/tools/eproc/platform/linux/process_collector_linux.hpp @@ -2,9 +2,9 @@ #define EPROC_PROCESS_COLLECTOR_LINUX_HPP #include "../../process_collector.hpp" +#include "../posix/process_icon_resolver.hpp" #include "gpu_reader_drm.hpp" #include "gpu_reader_nvidia.hpp" -#include "process_icon_resolver.hpp" #include "process_network_monitor.hpp" namespace eproc { diff --git a/src/tools/eproc/platform/macos/process_collector_macos.cpp b/src/tools/eproc/platform/macos/process_collector_macos.cpp new file mode 100644 index 000000000..9a5c13b8c --- /dev/null +++ b/src/tools/eproc/platform/macos/process_collector_macos.cpp @@ -0,0 +1,241 @@ +#include "process_collector_macos.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eproc { + +namespace { + +constexpr Int64 kMicrosecondsPerSecond = 1'000'000; +static_assert( CPU_STATE_MAX == 4 ); + +template bool readSysctl( const char* name, T& value ) { + size_t size = sizeof( value ); + return sysctlbyname( name, &value, &size, nullptr, 0 ) == 0 && size == sizeof( value ); +} + +int cpuPercent( Uint64 elapsed, Int64 wallMicroseconds ) { + if ( wallMicroseconds <= 0 ) + return 0; + const double percent = static_cast( elapsed ) * 100.0 / wallMicroseconds; + return static_cast( + std::min( percent, static_cast( std::numeric_limits::max() ) ) ); +} + +Uint64 machTicksToMicroseconds( Uint64 ticks ) { + static const mach_timebase_info_data_t timebase = [] { + mach_timebase_info_data_t value{}; + mach_timebase_info( &value ); + return value; + }(); + if ( timebase.denom == 0 ) + return 0; + return static_cast( static_cast( ticks ) * timebase.numer / + ( static_cast( timebase.denom ) * 1000 ) ); +} + +} // namespace + +bool ProcessCollectorMacOS::collect( std::vector& processes, SystemInfo& sysInfo ) { + const auto sampledAt = std::chrono::steady_clock::now(); + const Int64 elapsedUs = + mPrevSampleTime.time_since_epoch().count() == 0 + ? 0 + : std::chrono::duration_cast( sampledAt - mPrevSampleTime ) + .count(); + mPrevSampleTime = sampledAt; + ++mPass; + processes.clear(); + sysInfo = {}; + sysInfo.clockTicksPerSecond = kMicrosecondsPerSecond; + + int cpuCount = 0; + if ( readSysctl( "hw.logicalcpu", cpuCount ) && cpuCount > 0 ) + sysInfo.cpuCount = cpuCount; + + uint64_t totalBytes = 0; + if ( readSysctl( "hw.memsize", totalBytes ) ) + sysInfo.totalMemory = static_cast( totalBytes / 1024 ); + + const mach_port_t host = mach_host_self(); + vm_size_t pageSize = 0; + if ( host_page_size( host, &pageSize ) == KERN_SUCCESS ) { + vm_statistics64_data_t vm{}; + mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; + if ( host_statistics64( host, HOST_VM_INFO64, reinterpret_cast( &vm ), + &count ) == KERN_SUCCESS ) { + const Uint64 pageKiB = pageSize / 1024; + sysInfo.freeMemory = static_cast( vm.free_count * pageKiB ); + // Approximate reclaimable memory; this is not Linux MemAvailable. + const Uint64 available = ( static_cast( vm.free_count ) + vm.inactive_count + + vm.speculative_count ) * + pageKiB; + sysInfo.availableMemory = static_cast( + sysInfo.totalMemory > 0 ? std::min( sysInfo.totalMemory, available ) + : available ); + } + } + + host_cpu_load_info_data_t cpu{}; + mach_msg_type_number_t cpuInfoCount = HOST_CPU_LOAD_INFO_COUNT; + if ( host_statistics( host, HOST_CPU_LOAD_INFO, reinterpret_cast( &cpu ), + &cpuInfoCount ) == KERN_SUCCESS ) { + Uint64 totalDelta = 0; + Uint64 idleDelta = 0; + for ( int state = 0; state < CPU_STATE_MAX; ++state ) { + const Uint32 ticks = cpu.cpu_ticks[state]; + if ( mHasCpuSample ) { + // host_cpu_load_info uses 32-bit counters, so subtract modulo 2^32. + const Uint32 delta = ticks - mPrevCpuTicks[state]; + totalDelta += delta; + if ( state == CPU_STATE_IDLE ) + idleDelta = delta; + } + mPrevCpuTicks[state] = ticks; + } + if ( totalDelta > 0 ) + mLastCpuUsage = static_cast( + 100.0 * static_cast( totalDelta - idleDelta ) / totalDelta ); + mHasCpuSample = true; + } + sysInfo.cpuUsage = mLastCpuUsage; + mach_port_deallocate( mach_task_self(), host ); + + xsw_usage swap{}; + if ( readSysctl( "vm.swapusage", swap ) ) { + sysInfo.totalSwap = static_cast( swap.xsu_total / 1024 ); + sysInfo.freeSwap = static_cast( swap.xsu_avail / 1024 ); + } + struct timeval bootTime {}; + size_t bootSize = sizeof( bootTime ); + int bootName[] = { CTL_KERN, KERN_BOOTTIME }; + if ( sysctl( bootName, 2, &bootTime, &bootSize, nullptr, 0 ) == 0 ) { + struct timeval now {}; + gettimeofday( &now, nullptr ); + sysInfo.uptimeSeconds = std::max( 0.0, static_cast( now.tv_sec - bootTime.tv_sec ) + + ( now.tv_usec - bootTime.tv_usec ) / 1e6 ); + } + + const int pidCount = proc_listallpids( nullptr, 0 ); + if ( pidCount <= 0 ) + return false; + std::vector pids( static_cast( pidCount ) + 128 ); + const int listed = + proc_listallpids( pids.data(), static_cast( pids.size() * sizeof( pid_t ) ) ); + if ( listed <= 0 ) + return false; + processes.reserve( static_cast( listed ) ); + const uid_t currentUid = getuid(); + for ( int i = 0; i < listed; ++i ) { + const pid_t pid = pids[i]; + if ( pid <= 0 ) + continue; + proc_taskallinfo info{}; + if ( proc_pidinfo( pid, PROC_PIDTASKALLINFO, 0, &info, sizeof( info ) ) != sizeof( info ) ) + continue; + + const auto& bsd = info.pbsd; + const auto& task = info.ptinfo; + ProcessInfo proc; + proc.pid = pid; + proc.parentPid = bsd.pbi_ppid; + proc.name = bsd.pbi_name[0] ? bsd.pbi_name : bsd.pbi_comm; + proc.uid = bsd.pbi_ruid; + proc.euid = bsd.pbi_uid; + proc.suid = bsd.pbi_svuid; + const auto& account = mAccounts.get( static_cast( proc.uid ) ); + proc.username = account.name; + proc.canLogin = account.canLogin; + proc.euidCanLogin = mAccounts.get( static_cast( proc.euid ) ).canLogin; + proc.ownedByCurrentUser = proc.uid == currentUid || proc.euid == currentUid; + proc.systemProcess = proc.uid == 0 || ( bsd.pbi_flags & PROC_FLAG_SYSTEM ) != 0 || + ( !account.name.empty() && !account.canLogin ); + proc.userProcess = !proc.systemProcess && !account.name.empty(); + switch ( bsd.pbi_status ) { + case SRUN: + proc.status = ProcessStatus::Running; + break; + case SSLEEP: + proc.status = ProcessStatus::Sleeping; + break; + case SZOMB: + proc.status = ProcessStatus::Zombie; + break; + case SSTOP: + proc.status = ProcessStatus::Stopped; + break; + default: + break; + } + proc.numThreads = task.pti_threadnum; + proc.niceLevel = bsd.pbi_nice; + proc.vmSize = static_cast( task.pti_virtual_size / 1024 ); + proc.vmRSS = static_cast( task.pti_resident_size / 1024 ); + // proc_taskinfo CPU times use Mach absolute ticks, not nanoseconds. + proc.userTime = static_cast( machTicksToMicroseconds( task.pti_total_user ) ); + proc.sysTime = static_cast( machTicksToMicroseconds( task.pti_total_system ) ); + const Uint64 startUs = bsd.pbi_start_tvsec * kMicrosecondsPerSecond + bsd.pbi_start_tvusec; + const Uint64 bootUs = + static_cast( bootTime.tv_sec ) * kMicrosecondsPerSecond + bootTime.tv_usec; + proc.startTime = + bootTime.tv_sec > 0 && startUs >= bootUs ? static_cast( startUs - bootUs ) : 0; + TickEntry& previous = mProcessTicks[pid]; + if ( previous.startTime != startUs || previous.processName != proc.name ) { + previous.commandPath.clear(); + previous.iconPath.clear(); + previous.pathQueried = false; + previous.processName = proc.name; + } + if ( !previous.pathQueried ) { + char path[PROC_PIDPATHINFO_MAXSIZE]{}; + if ( proc_pidpath( pid, path, sizeof( path ) ) > 0 ) { + previous.commandPath = path; + previous.iconPath = mIconResolver.resolve( previous.commandPath ); + } + previous.pathQueried = true; + } + proc.commandLine = previous.commandPath.empty() ? proc.name : previous.commandPath; + proc.iconPath = previous.iconPath; + + rusage_info_v2 usage{}; + if ( proc_pid_rusage( pid, RUSAGE_INFO_V2, reinterpret_cast( &usage ) ) == + 0 ) { + proc.vmURSS = static_cast( usage.ri_phys_footprint / 1024 ); + proc.ioReadBytes = static_cast( usage.ri_diskio_bytesread ); + proc.ioWriteBytes = static_cast( usage.ri_diskio_byteswritten ); + } + + const Uint64 user = static_cast( proc.userTime ); + const Uint64 kernel = static_cast( proc.sysTime ); + if ( previous.pass == mPass - 1 && previous.startTime == startUs && + user >= previous.userTime && kernel >= previous.sysTime ) { + proc.userUsage = cpuPercent( user - previous.userTime, elapsedUs ); + proc.sysUsage = cpuPercent( kernel - previous.sysTime, elapsedUs ); + } + previous.startTime = startUs; + previous.userTime = user; + previous.sysTime = kernel; + previous.pass = mPass; + processes.emplace_back( std::move( proc ) ); + } + + for ( auto it = mProcessTicks.begin(); it != mProcessTicks.end(); ) { + if ( it->second.pass != mPass ) + it = mProcessTicks.erase( it ); + else + ++it; + } + return true; +} + +} // namespace eproc diff --git a/src/tools/eproc/platform/macos/process_collector_macos.hpp b/src/tools/eproc/platform/macos/process_collector_macos.hpp new file mode 100644 index 000000000..90038a167 --- /dev/null +++ b/src/tools/eproc/platform/macos/process_collector_macos.hpp @@ -0,0 +1,41 @@ +#ifndef EPROC_PROCESS_COLLECTOR_MACOS_HPP +#define EPROC_PROCESS_COLLECTOR_MACOS_HPP + +#include "../../process_collector.hpp" +#include "../posix/process_account.hpp" +#include "process_icon_resolver_macos.hpp" +#include +#include +#include + +namespace eproc { + +class ProcessCollectorMacOS final : public ProcessCollector { + public: + bool collect( std::vector& processes, SystemInfo& sysInfo ) override; + + private: + struct TickEntry { + Uint64 startTime{ 0 }; + Uint64 userTime{ 0 }; + Uint64 sysTime{ 0 }; + Uint32 pass{ 0 }; + bool pathQueried{ false }; + std::string processName; + std::string commandPath; + std::string iconPath; + }; + + std::unordered_map mProcessTicks; + ProcessAccounts mAccounts; + ProcessIconResolverMacOS mIconResolver; + std::chrono::steady_clock::time_point mPrevSampleTime; + std::array mPrevCpuTicks{}; + float mLastCpuUsage{ 0.f }; + Uint32 mPass{ 0 }; + bool mHasCpuSample{ false }; +}; + +} // namespace eproc + +#endif // EPROC_PROCESS_COLLECTOR_MACOS_HPP diff --git a/src/tools/eproc/platform/macos/process_icon_resolver_macos.cpp b/src/tools/eproc/platform/macos/process_icon_resolver_macos.cpp new file mode 100644 index 000000000..db1a4b9bb --- /dev/null +++ b/src/tools/eproc/platform/macos/process_icon_resolver_macos.cpp @@ -0,0 +1,48 @@ +#include "process_icon_resolver_macos.hpp" + +#include + +namespace eproc { + +const std::string& ProcessIconResolverMacOS::resolve( const std::string& executablePath ) { + const size_t bundleEnd = executablePath.find( ".app/Contents/MacOS/" ); + if ( bundleEnd == std::string::npos ) { + static const std::string empty; + return empty; + } + const std::string bundlePath = executablePath.substr( 0, bundleEnd + 4 ); + auto found = mIcons.find( bundlePath ); + if ( found != mIcons.end() ) + return found->second; + + std::string iconPath; + CFURLRef bundleURL = CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, reinterpret_cast( bundlePath.data() ), + static_cast( bundlePath.size() ), true ); + if ( bundleURL ) { + CFBundleRef bundle = CFBundleCreate( kCFAllocatorDefault, bundleURL ); + if ( bundle ) { + CFTypeRef iconName = + CFBundleGetValueForInfoDictionaryKey( bundle, CFSTR( "CFBundleIconFile" ) ); + if ( iconName && CFGetTypeID( iconName ) == CFStringGetTypeID() ) { + CFURLRef iconURL = CFBundleCopyResourceURL( + bundle, static_cast( iconName ), nullptr, nullptr ); + if ( !iconURL ) + iconURL = CFBundleCopyResourceURL( bundle, static_cast( iconName ), + CFSTR( "icns" ), nullptr ); + if ( iconURL ) { + char path[4096]{}; + if ( CFURLGetFileSystemRepresentation( + iconURL, true, reinterpret_cast( path ), sizeof( path ) ) ) + iconPath = path; + CFRelease( iconURL ); + } + } + CFRelease( bundle ); + } + CFRelease( bundleURL ); + } + return mIcons.emplace( bundlePath, std::move( iconPath ) ).first->second; +} + +} // namespace eproc diff --git a/src/tools/eproc/platform/macos/process_icon_resolver_macos.hpp b/src/tools/eproc/platform/macos/process_icon_resolver_macos.hpp new file mode 100644 index 000000000..2046128dc --- /dev/null +++ b/src/tools/eproc/platform/macos/process_icon_resolver_macos.hpp @@ -0,0 +1,20 @@ +#ifndef EPROC_PROCESS_ICON_RESOLVER_MACOS_HPP +#define EPROC_PROCESS_ICON_RESOLVER_MACOS_HPP + +#include +#include + +namespace eproc { + +/** Finds an application bundle's declared icon from its executable path. */ +class ProcessIconResolverMacOS { + public: + const std::string& resolve( const std::string& executablePath ); + + private: + std::unordered_map mIcons; +}; + +} // namespace eproc + +#endif // EPROC_PROCESS_ICON_RESOLVER_MACOS_HPP diff --git a/src/tools/eproc/platform/posix/process_account.hpp b/src/tools/eproc/platform/posix/process_account.hpp new file mode 100644 index 000000000..853739a1a --- /dev/null +++ b/src/tools/eproc/platform/posix/process_account.hpp @@ -0,0 +1,44 @@ +#ifndef EPROC_POSIX_PROCESS_ACCOUNT_HPP +#define EPROC_POSIX_PROCESS_ACCOUNT_HPP + +#include +#include +#include +#include +#include +#include + +namespace eproc { + +/** Cached POSIX account metadata shared by the macOS and FreeBSD collectors. */ +class ProcessAccounts { + public: + struct Account { + std::string name; + bool canLogin{ false }; + }; + + const Account& get( uid_t uid ) { + auto found = mAccounts.find( uid ); + if ( found != mAccounts.end() ) + return found->second; + Account account; + std::array buffer; + passwd pwd{}; + passwd* result = nullptr; + if ( getpwuid_r( uid, &pwd, buffer.data(), buffer.size(), &result ) == 0 && result ) { + account.name = pwd.pw_name; + const char* shell = pwd.pw_shell; + account.canLogin = + shell && *shell && !strstr( shell, "nologin" ) && !strstr( shell, "/false" ); + } + return mAccounts.emplace( uid, std::move( account ) ).first->second; + } + + private: + std::unordered_map mAccounts; +}; + +} // namespace eproc + +#endif // EPROC_POSIX_PROCESS_ACCOUNT_HPP diff --git a/src/tools/eproc/platform/linux/process_icon_resolver.cpp b/src/tools/eproc/platform/posix/process_icon_resolver.cpp similarity index 86% rename from src/tools/eproc/platform/linux/process_icon_resolver.cpp rename to src/tools/eproc/platform/posix/process_icon_resolver.cpp index ac0aeb257..040c31739 100644 --- a/src/tools/eproc/platform/linux/process_icon_resolver.cpp +++ b/src/tools/eproc/platform/posix/process_icon_resolver.cpp @@ -19,8 +19,13 @@ 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/"; +// FreeBSD packages install icons below /usr/local/share; Linux distributions normally use +// /usr/share. The other prefix remains a fallback on both systems. +#ifdef __FreeBSD__ +static const char* kIconDataDirectories[] = { "/usr/local/share", "/usr/share" }; +#else +static const char* kIconDataDirectories[] = { "/usr/share", "/usr/local/share" }; +#endif // 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. @@ -46,8 +51,8 @@ static bool isGenericDirName( std::string_view name ) { return false; } -// Everything after the last separator. The kernel appends " (deleted)" to a /proc//exe link -// when the binary has been replaced or removed, which would never match an index key. The result is +// Everything after the last separator. Linux appends " (deleted)" to a /proc//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 ); @@ -106,8 +111,14 @@ 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/"; +static std::string withIconExtension( const std::string& name, const char* extension ) { + return hasIconExtension( name ) ? name : name + extension; +} + +static std::string themeIconPath( const char* dataDir, const char* theme, const char* size, + const std::string& name ) { + std::string path = dataDir; + path += "/icons/"; path += theme; path += '/'; path += size; @@ -116,9 +127,9 @@ static std::string themeIconPath( const char* theme, const char* size, const std 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. +// Maps an icon name to the icon file that backs it. Preference order: 24x24, 32x32 and 48x48 PNG, +// then scalable SVG, then any other icon of a known size and extension, and finally the unthemed +// pixmap directories. static std::string findIconFile( const std::string& iconName ) { if ( iconName.empty() ) return {}; @@ -139,18 +150,24 @@ static std::string findIconFile( const std::string& iconName ) { 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; + for ( const char* dataDir : kIconDataDirectories ) { + std::string path = + themeIconPath( dataDir, theme, size, withIconExtension( 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* dataDir : kIconDataDirectories ) { + std::string path = + themeIconPath( dataDir, theme, "scalable", withIconExtension( name, ".svg" ) ); + if ( FileInfo( path ).isRegularFile() ) + return path; + } } } @@ -159,9 +176,12 @@ static std::string findIconFile( const std::string& iconName ) { 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* dataDir : kIconDataDirectories ) { + std::string path = + themeIconPath( dataDir, theme, size, withIconExtension( name, extension ) ); + if ( FileInfo( path ).isRegularFile() ) + return path; + } } } } @@ -169,15 +189,20 @@ static std::string findIconFile( const std::string& iconName ) { for ( const char* extension : kIconExtensions ) { if ( !allowsExtension( name, extension ) ) continue; - std::string path = std::string( kIconPixmapDir ) + name + extension; - if ( FileInfo( path ).isRegularFile() ) - return path; + for ( const char* dataDir : kIconDataDirectories ) { + std::string path = + std::string( dataDir ) + "/pixmaps/" + withIconExtension( 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; + for ( const char* dataDir : kIconDataDirectories ) { + std::string nakedPath = std::string( dataDir ) + "/pixmaps/" + name; + if ( FileInfo( nakedPath ).isRegularFile() ) + return nakedPath; + } return {}; } diff --git a/src/tools/eproc/platform/linux/process_icon_resolver.hpp b/src/tools/eproc/platform/posix/process_icon_resolver.hpp similarity index 97% rename from src/tools/eproc/platform/linux/process_icon_resolver.hpp rename to src/tools/eproc/platform/posix/process_icon_resolver.hpp index bd232347f..b96cddbdb 100644 --- a/src/tools/eproc/platform/linux/process_icon_resolver.hpp +++ b/src/tools/eproc/platform/posix/process_icon_resolver.hpp @@ -12,7 +12,7 @@ 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. This resolver does not need a display connection; eproc's - * UI thread may use an X11 window icon when no desktop icon was found. + * UI thread may use an X11 window icon when no desktop icon was found on X11. * * 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 @@ -24,7 +24,7 @@ namespace eproc { class ProcessIconResolver { public: /** Resolves the icon file for a process. - * @param exePath absolute path of /proc//exe target (may be empty) + * @param exePath absolute executable path, when available (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. */ diff --git a/src/tools/eproc/process_collector.cpp b/src/tools/eproc/process_collector.cpp index b8f056e6d..742137183 100644 --- a/src/tools/eproc/process_collector.cpp +++ b/src/tools/eproc/process_collector.cpp @@ -4,6 +4,16 @@ #if EE_PLATFORM == EE_PLATFORM_LINUX #include "platform/linux/process_collector_linux.hpp" +#include +#include +#elif EE_PLATFORM == EE_PLATFORM_MACOS +#include "platform/macos/process_collector_macos.hpp" + +#include +#include +#elif EE_PLATFORM == EE_PLATFORM_BSD && defined( __FreeBSD__ ) +#include "platform/freebsd/process_collector_freebsd.hpp" + #include #include #elif EE_PLATFORM == EE_PLATFORM_WIN @@ -17,6 +27,11 @@ #include #endif +#if EE_PLATFORM == EE_PLATFORM_BSD && !defined( __FreeBSD__ ) +#include +#include +#endif + using namespace EE::System; namespace eproc { @@ -24,6 +39,10 @@ namespace eproc { std::unique_ptr ProcessCollector::create() { #if EE_PLATFORM == EE_PLATFORM_LINUX return std::make_unique(); +#elif EE_PLATFORM == EE_PLATFORM_MACOS + return std::make_unique(); +#elif EE_PLATFORM == EE_PLATFORM_BSD && defined( __FreeBSD__ ) + return std::make_unique(); #elif EE_PLATFORM == EE_PLATFORM_WIN return std::make_unique(); #else @@ -33,8 +52,9 @@ std::unique_ptr ProcessCollector::create() { } bool sendProcessSignal( Int64 pid, int signal ) { -#if EE_PLATFORM == EE_PLATFORM_LINUX - return ::kill( static_cast( pid ), signal ) == 0; +#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \ + EE_PLATFORM == EE_PLATFORM_BSD + return pid > 0 && ::kill( static_cast( pid ), signal ) == 0; #elif EE_PLATFORM == EE_PLATFORM_WIN return signal == 9 && killProcess( pid ); #else diff --git a/src/tools/eproc/process_model.cpp b/src/tools/eproc/process_model.cpp index 29b7c9aa4..b84e9e2f1 100644 --- a/src/tools/eproc/process_model.cpp +++ b/src/tools/eproc/process_model.cpp @@ -1,3 +1,9 @@ +#include +#if EE_PLATFORM == EE_PLATFORM_MACOS +#define Rect AppleRect +#include +#undef Rect +#endif #include "process_model.hpp" #include @@ -110,7 +116,11 @@ std::string ProcessModel::columnName( const size_t& column ) const { case ColCommand: return mUI->i18n( "eproc_column_command", "Command" ).toUtf8(); case ColTotalMemory: +#if EE_PLATFORM == EE_PLATFORM_MACOS + return mUI->i18n( "eproc_column_real_memory", "Real Mem" ).toUtf8(); +#else return mUI->i18n( "eproc_column_total_memory", "Total Memory" ).toUtf8(); +#endif case ColVirtualSize: return mUI->i18n( "eproc_column_virtual_size", "Virtual Size" ).toUtf8(); case ColCpuTime: @@ -471,6 +481,51 @@ static bool isBlankIcon( const Image& image ) { // The original renders 16px icons in the name column. static constexpr int kIconSizeDp = 16; +#if EE_PLATFORM == EE_PLATFORM_MACOS +static Image loadMacIcon( const std::string& path, Uint32 iconPx ) { + Image image; + CFURLRef url = CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, reinterpret_cast( path.data() ), + static_cast( path.size() ), false ); + if ( !url ) + return image; + CGImageSourceRef source = CGImageSourceCreateWithURL( url, nullptr ); + CFRelease( url ); + if ( !source ) + return image; + const int maxSize = static_cast( iconPx ); + CFNumberRef size = CFNumberCreate( kCFAllocatorDefault, kCFNumberIntType, &maxSize ); + const void* keys[] = { kCGImageSourceCreateThumbnailFromImageAlways, + kCGImageSourceThumbnailMaxPixelSize }; + const void* values[] = { kCFBooleanTrue, size }; + CFDictionaryRef options = + CFDictionaryCreate( kCFAllocatorDefault, keys, values, 2, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks ); + CGImageRef thumbnail = CGImageSourceCreateThumbnailAtIndex( source, 0, options ); + CFRelease( options ); + CFRelease( size ); + CFRelease( source ); + if ( !thumbnail ) + return image; + CFMutableDataRef data = CFDataCreateMutable( kCFAllocatorDefault, 0 ); + if ( data ) { + CGImageDestinationRef destination = + CGImageDestinationCreateWithData( data, CFSTR( "public.png" ), 1, nullptr ); + if ( destination ) { + CGImageDestinationAddImage( destination, thumbnail, nullptr ); + if ( CGImageDestinationFinalize( destination ) ) { + image = Image( CFDataGetBytePtr( data ), + static_cast( CFDataGetLength( data ) ), 4 ); + } + CFRelease( destination ); + } + CFRelease( data ); + } + CGImageRelease( thumbnail ); + return image; +} +#endif + DrawablePtr ProcessModel::iconFor( const std::string& path ) const { auto it = mIconCache.find( path ); if ( it != mIconCache.end() ) @@ -481,7 +536,12 @@ DrawablePtr ProcessModel::iconFor( const std::string& path ) const { const Uint32 iconPx = static_cast( PixelDensity::dpToPxI( kIconSizeDp ) ); Image image; - if ( String::endsWith( path, ".svg" ) || String::endsWith( path, ".svgz" ) ) { +#if EE_PLATFORM == EE_PLATFORM_MACOS + if ( String::endsWith( path, ".icns" ) ) { + image = loadMacIcon( path, iconPx ); + } else +#endif + 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 );