From ddfd3937893b29b04d6892c50cf411cf5e81b25c Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Wed, 10 Jan 2024 22:38:30 -0800 Subject: [PATCH] Tools: Adds new FEXpidof tool This behaves exactly like pidof but only searches for FEX applications. This fixes a long standing annoyance of mine that pidof doesn't work for FEX. This behaves exactly like pidof but knows how to decode the command line options to pull out the program data. If the Linux kernel ever accepts the patches for binfmt_misc to change how the interpreter is handled then this will become redundant, but until that happens here is a utility that I want. --- Source/Tools/CMakeLists.txt | 1 + Source/Tools/pidof/CMakeLists.txt | 25 ++++ Source/Tools/pidof/pidof.cpp | 212 ++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 Source/Tools/pidof/CMakeLists.txt create mode 100644 Source/Tools/pidof/pidof.cpp diff --git a/Source/Tools/CMakeLists.txt b/Source/Tools/CMakeLists.txt index 49455fc43..a6b151a7c 100644 --- a/Source/Tools/CMakeLists.txt +++ b/Source/Tools/CMakeLists.txt @@ -21,6 +21,7 @@ if (NOT MINGW_BUILD) add_subdirectory(LinuxEmulation/) add_subdirectory(FEXLoader/) + add_subdirectory(pidof/) endif() if (BUILD_TESTS) diff --git a/Source/Tools/pidof/CMakeLists.txt b/Source/Tools/pidof/CMakeLists.txt new file mode 100644 index 000000000..8a5f79426 --- /dev/null +++ b/Source/Tools/pidof/CMakeLists.txt @@ -0,0 +1,25 @@ +add_executable(FEXpidof pidof.cpp) +target_include_directories(FEXpidof + PRIVATE + ${PROJECT_SOURCE_DIR}/Source/Common/cpp-optparse/) + +target_link_libraries(FEXpidof + PRIVATE + cpp-optparse + fmt::fmt +) + +if (CMAKE_BUILD_TYPE MATCHES "RELEASE") + target_link_options(FEXpidof + PRIVATE + "LINKER:--gc-sections" + "LINKER:--strip-all" + "LINKER:--as-needed" + ) +endif() + +install(TARGETS FEXpidof + RUNTIME + DESTINATION bin + COMPONENT runtime +) diff --git a/Source/Tools/pidof/pidof.cpp b/Source/Tools/pidof/pidof.cpp new file mode 100644 index 000000000..568c84c91 --- /dev/null +++ b/Source/Tools/pidof/pidof.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: MIT +#include "OptionParser.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Config { + +bool SingleShot{}; +bool SkipZombie{true}; +bool DoNotDisplay{}; +std::string Separator {" "}; +std::unordered_set OmitPids; +std::unordered_set Programs; + +void LoadOptions(int argc, char **argv) { + optparse::OptionParser Parser{}; + + Parser.add_option("-s") + .help("Single shot - Only returns one pid") + .action("store_true") + .set_default(SingleShot); + + Parser.add_option("-q") + .help("Do not display matched PIDs to stdout. Simply exit with status of true or false if a PID was found") + .action("store_true") + .set_default(DoNotDisplay); + + Parser.add_option("-z") + .help("Try to detect zombie processes - Usually zombie processes are skipped") + .action("store_false") + .set_default(SkipZombie); + + Parser.add_option("-d") + .help("Use a different separator if more than one pid is show - Default is space") + .set_default(Separator); + + Parser.add_option("-o") + .help("Ignore processes with matched pids") + .action("append"); + + optparse::Values Options = Parser.parse_args(argc, argv); + + SingleShot = Options.get("s"); + DoNotDisplay = Options.get("q"); + SkipZombie = Options.get("z"); + Separator = Options["d"]; + + for (const auto &Omit: Options.all("o")) { + std::istringstream ss{Omit}; + std::string sub; + while (std::getline(ss, sub, ',')) { + int64_t pid; + auto ConvResult = std::from_chars(sub.data(), sub.data() + sub.size(), pid, 10); + + // Invalid pid, skip. + if (ConvResult.ec == std::errc::invalid_argument) continue; + + OmitPids.emplace(pid); + } + } + + for (const auto &Program : Parser.args()) { + Programs.emplace(Program); + } +} +} + +struct PIDInfo { + int64_t pid; + std::string cmdline; + std::string exe_link; + char State; +}; + +std::vector PIDs; + +int main(int argc, char **argv) { + Config::LoadOptions(argc, argv); + + // Iterate over all pids, storing the data for investigating afterwards. + for (const auto &Entry : std::filesystem::directory_iterator("/proc/")) { + // If not a directory then skip. + if (!Entry.is_directory()) continue; + + auto CMDLinePath = Entry.path() / "cmdline"; + auto StatusPath = Entry.path() / "status"; + auto ExePath = Entry.path() / "exe"; + + // If cmdline doesn't exist then skip. + if (!std::filesystem::exists(CMDLinePath)) continue; + + auto Filename = Entry.path().filename().string(); + int64_t pid; + auto ConvResult = std::from_chars(Filename.data(), Filename.data() + Filename.size(), pid, 10); + + // If the filename couldn't be converted to a PID then skip. + // Happens with folders like `self` and a few other folders in this directory. + if (ConvResult.ec == std::errc::invalid_argument) continue; + + std::ostringstream CMDLineData; + { + std::ifstream fs(CMDLinePath, std::ios_base::in | std::ios_base::binary); + + if (!fs.is_open()) continue; + + CMDLineData << fs.rdbuf(); + + // If cmdline was empty then skip. + if (CMDLineData.str().empty()) continue; + } + + std::error_code ec; + std::string exe_link = std::filesystem::read_symlink(ExePath, ec); + + // Couldn't read exe path? skip. + if (ec) continue; + + // Read state + char State; + + { + std::ifstream fs(StatusPath, std::ios_base::in | std::ios_base::binary); + + if (!fs.is_open()) continue; + + std::string Line; + + while (std::getline(fs, Line)) { + if (fs.eof()) break; + + if (Line.find("State") == Line.npos) { + continue; + } + + if (sscanf(Line.c_str(), "State: %c", &State) == 1) { + break; + } + } + } + + PIDs.emplace_back(PIDInfo { + .pid = pid, + .cmdline = CMDLineData.str(), + .exe_link = exe_link, + .State = State, + }); + } + + std::unordered_set MatchedPIDs; + for (const auto &pid : PIDs) { + if (pid.State == 'Z' && Config::SkipZombie) continue; + if (Config::OmitPids.contains(pid.pid)) continue; + + std::vector Args; + const char *arg = pid.cmdline.data(); + + while (arg[0]) { + Args.emplace_back(arg); + arg += strlen(arg) + 1; + } + + auto IsFEX = [](auto &Path) { + if (Path.ends_with("FEXInterpreter")) return true; + if (Path.ends_with("FEXLoader")) return true; + + return false; + }; + bool IsFEXBin = IsFEX(pid.exe_link) || IsFEX(Args[0]); + if (!IsFEXBin) continue; + + auto Arg1 = Args[1]; + auto Arg1Program = std::filesystem::path(Arg1).filename(); + + bool Matched = false; + for (const auto &CompareProgram : Config::Programs) { + auto CompareProgramFilename = std::filesystem::path(CompareProgram).filename(); + if (CompareProgram == Arg1Program || + CompareProgram == Arg1 || + CompareProgramFilename == Arg1Program) { + MatchedPIDs.emplace(pid.pid); + Matched = true; + break; + } + } + + if (Matched && Config::SingleShot) { + break; + } + } + + if (!MatchedPIDs.empty() && !Config::DoNotDisplay) { + bool first = true; + for (const auto &MatchedPID : MatchedPIDs) { + if (first) { + fmt::print("{}", MatchedPID); + first = false; + } + else { + fmt::print("{}{}", Config::Separator, MatchedPID); + } + } + fmt::print("\n"); + } + + return MatchedPIDs.empty() ? 1 : 0; +}