diff --git a/Source/Tools/CMakeLists.txt b/Source/Tools/CMakeLists.txt index 49455fc431..a6b151a7ca 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 0000000000..8a5f794268 --- /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 0000000000..568c84c91c --- /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; +}