Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1806,3 +1806,29 @@ if(EXISTS "${_AUTO_TUNE_TEST_SRC}")
include(CTest)
add_test(NAME AutoTuneTest COMMAND test_auto_tune)
endif()

# ProcessManager non-mutating is_running() contract test.
set(_PM_TEST_SRC
"${CMAKE_CURRENT_SOURCE_DIR}/test/cpp/test_process_manager.cpp"
)
set(_PM_TEST_LIBS
"${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/server/utils/process_manager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/server/utils/platform/process_unix.cpp"
)
list(GET _PM_TEST_LIBS 0 _pm_lib0)
list(GET _PM_TEST_LIBS 1 _pm_lib1)
if(EXISTS "${_PM_TEST_SRC}" AND EXISTS "${_pm_lib0}" AND EXISTS "${_pm_lib1}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard the Unix-only process test by platform

This block is enabled solely by file existence, so the new target is created on Windows too. The default Windows build will then try to compile test_process_manager.cpp/process_unix.cpp, which include Unix-only headers and link pthread/-pthread, breaking cmake --build --preset windows; gate this test to the Unix/Linux platforms it supports.

Useful? React with 👍 / 👎.

add_executable(test_process_manager
test/cpp/test_process_manager.cpp
src/cpp/server/utils/process_manager.cpp
src/cpp/server/utils/platform/process_unix.cpp
)
target_include_directories(test_process_manager PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/include
${CMAKE_CURRENT_BINARY_DIR}/include
)
target_link_libraries(test_process_manager PRIVATE pthread)
target_link_options(test_process_manager PRIVATE -pthread)
include(CTest)
add_test(NAME ProcessManagerTest COMMAND test_process_manager)
endif()
37 changes: 37 additions & 0 deletions src/cpp/server/utils/platform/process_unix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
#include <chrono>
#include <algorithm>
#include <cctype>
#include <fstream>
#include <sstream>
#include <cstdio>

#ifdef __linux__
#include <sys/prctl.h>
Expand Down Expand Up @@ -82,6 +85,36 @@ static void preserve_capabilities_for_exec() {
}
#endif

// Check whether a process is a zombie by reading /proc/<pid>/stat.
// This is non-mutating — it does not reap the child.
static bool is_zombie_by_proc(pid_t pid) {
char path[64];
std::snprintf(path, sizeof(path), "/proc/%d/stat", pid);
std::ifstream f(path);
if (!f.is_open()) {
return false;
}
std::string line;
if (!std::getline(f, line)) {
return false;
}
auto open_paren = line.find('(');
if (open_paren == std::string::npos) {
return false;
}
auto close_paren = line.rfind(')');
if (close_paren == std::string::npos || close_paren <= open_paren) {
return false;
}
std::string rest = line.substr(close_paren + 1);
std::istringstream iss(rest);
char state;
if (!(iss >> state)) {
return false;
}
return state == 'Z';
}

class UnixProcessPlatform : public ProcessPlatform {
public:
ProcessHandle spawn(
Expand Down Expand Up @@ -370,6 +403,10 @@ bool UnixProcessPlatform::is_running(ProcessHandle handle) {
}
#endif

if (is_zombie_by_proc(handle.pid)) {
return false;
}

errno = 0;
return ::kill(handle.pid, 0) == 0 || errno == EPERM;
}
Expand Down
172 changes: 172 additions & 0 deletions test/cpp/test_process_manager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Standalone test for ProcessManager::is_running() non-mutating contract.
// Verifies:
// 1. is_running() returns true for a running process
// 2. is_running() returns false for an exited process WITHOUT reaping it
// 3. reap_process() retrieves the real exit code after is_running() returned false

#include <lemon/utils/process_manager.h>
#include <lemon/utils/process_platform.h>

#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <thread>
#include <chrono>
#include <fstream>
#include <sstream>

using lemon::utils::ProcessHandle;
using lemon::utils::ProcessManager;

static int g_failures = 0;

static void check(const char* name, bool ok) {
std::printf("[%s] %s\n", ok ? "PASS" : "FAIL", name);
if (!ok) ++g_failures;
}

static pid_t spawn_child(int exit_code) {
pid_t pid = fork();
if (pid < 0) {
return -1;
}
if (pid == 0) {
_exit(exit_code);
}
return pid;
}

static ProcessHandle make_handle(pid_t pid) {
ProcessHandle h;
h.handle = nullptr;
h.pid = static_cast<int>(pid);
return h;
}

// Wait for a child to exit without reaping it.
// Uses /proc/<pid>/stat to detect zombie state.
static pid_t wait_for_exit_no_reap(pid_t child, int timeout_ms = 5000) {
auto start = std::chrono::steady_clock::now();
while (true) {
{
char path[64];
std::snprintf(path, sizeof(path), "/proc/%d/stat", child);
std::ifstream f(path);
if (f.is_open()) {
std::string line;
if (std::getline(f, line)) {
auto open_paren = line.find('(');
if (open_paren != std::string::npos) {
auto close_paren = line.rfind(')');
if (close_paren != std::string::npos && close_paren > open_paren) {
std::string rest = line.substr(close_paren + 1);
std::istringstream iss(rest);
char state;
if (iss >> state && state == 'Z') {
return child;
}
}
}
}
}
}
int status = 0;
pid_t r = waitpid(child, &status, WNOHANG);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop reaping in the no-reap polling helper

If the fast _exit(42) child exits after the /proc stat is read but before this fallback runs, waitpid(..., WNOHANG) reaps it and the helper returns -1, so the first test fails before exercising ProcessManager::is_running(). Since this helper's contract is explicitly to wait without reaping, use only non-mutating polling such as /proc or waitid(..., WNOWAIT) here.

Useful? React with 👍 / 👎.

if (r == child) {
return -1;
}
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > timeout_ms) {
return -1;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}

int main() {
// Test: is_running() returns false for an exited child
{
pid_t child = spawn_child(42);
check("spawn_child returns valid PID", child > 0);

pid_t exited = wait_for_exit_no_reap(child);
check("child exited (detected without reap)", exited > 0);

if (exited > 0) {
ProcessHandle h = make_handle(exited);

bool running = ProcessManager::is_running(h);
check("is_running() returns false for exited child", !running);

int exit_code = ProcessManager::reap_process(h);
check("reap_process() returns real exit code 42", exit_code == 42);
} else {
int status = 0;
waitpid(child, &status, 0);
}
}

// Test: edge cases for invalid PIDs
{
ProcessHandle h = make_handle(0);
check("is_running() returns false for PID 0", !ProcessManager::is_running(h));
}
{
ProcessHandle h = make_handle(-1);
check("is_running() returns false for negative PID", !ProcessManager::is_running(h));
}
{
ProcessHandle h = make_handle(999999);
check("is_running() returns false for non-existent PID", !ProcessManager::is_running(h));
}

// Test: is_running() returns true for a running process
{
pid_t child = fork();
check("fork succeeds", child >= 0);

if (child > 0) {
ProcessHandle h = make_handle(child);
check("is_running() returns true for running child", ProcessManager::is_running(h));

kill(child, SIGKILL);
int status = 0;
waitpid(child, &status, 0);
} else if (child == 0) {
while (true) {
sleep(1);
}
}
}

// Test: reap_process() returns -1 for a still-running process
{
pid_t child = fork();
check("fork for reap test", child >= 0);

if (child > 0) {
ProcessHandle h = make_handle(child);
std::this_thread::sleep_for(std::chrono::milliseconds(50));

int rc = ProcessManager::reap_process(h);
check("reap_process() returns -1 for running process", rc == -1);

kill(child, SIGKILL);
int status = 0;
waitpid(child, &status, 0);
} else if (child == 0) {
while (true) {
sleep(1);
}
}
}

if (g_failures == 0) {
std::printf("\nAll process_manager tests passed\n");
return 0;
}
std::printf("\n%d process_manager test(s) FAILED\n", g_failures);
return 1;
}
Loading