Skip to content

Commit e98d2ae

Browse files
committed
fix(Process): detect defunct processes reliably in isRunning #1097
- Replace optional<atomic<int>> with separate atomic status/flag for better portability across compilers - Fix PID-only isRunningImpl to use waitpid(WNOHANG) before falling back to kill(pid, 0), properly detecting zombie children - Fix missing debug suffix in test binary names - Rewrite testIsRunningAllowsForTermination to reproduce the original defunct-with-pipes scenario from issue #1097 - Add tests: PID-only isRunning, wait-after-isRunning, concurrent wait/isRunning thread safety - Remove unused Mutex.h and optional includes
1 parent 483d0a7 commit e98d2ae

File tree

4 files changed

+142
-19
lines changed

4 files changed

+142
-19
lines changed

Foundation/include/Poco/Process_UNIX.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@
2020

2121
#include "Poco/Foundation.h"
2222
#include "Poco/Event.h"
23-
#include "Poco/Mutex.h"
2423
#include "Poco/RefCountedObject.h"
2524
#include <unistd.h>
2625
#include <vector>
2726
#include <map>
2827
#include <atomic>
29-
#include <optional>
3028

3129

3230
namespace Poco {
@@ -51,7 +49,8 @@ class Foundation_API ProcessHandleImpl: public RefCountedObject
5149
static int statusToExitCode(int status);
5250
const std::atomic<pid_t> _pid;
5351
mutable Event _event;
54-
mutable std::optional<std::atomic<int>> _status;
52+
mutable std::atomic<int> _status{0};
53+
mutable std::atomic<bool> _hasStatus{false};
5554
};
5655

5756

Foundation/src/Process_UNIX.cpp

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@ int ProcessHandleImpl::wait() const
7171
if (wait(0) != _pid)
7272
throw SystemException("Cannot wait for process", NumberFormatter::format(_pid));
7373

74-
return statusToExitCode(_status.value());
74+
return statusToExitCode(_status.load());
7575
}
7676

7777

7878
int ProcessHandleImpl::wait(int options) const
7979
{
80-
if (_status.has_value()) return _pid;
80+
if (_hasStatus.load()) return _pid;
8181
int status;
8282
int rc;
8383
do
@@ -87,14 +87,15 @@ int ProcessHandleImpl::wait(int options) const
8787
while (rc < 0 && errno == EINTR);
8888
if (rc == _pid)
8989
{
90-
_status = status;
90+
_status.store(status);
91+
_hasStatus.store(true);
9192
_event.set();
9293
}
9394
else if (rc < 0 && errno == ECHILD)
9495
{
9596
// Another thread reaped the process; wait for it to update the status.
9697
// Use a timeout to prevent hanging indefinitely if something goes wrong.
97-
if (_event.tryWait(5000) && _status.has_value())
98+
if (_event.tryWait(5000) && _hasStatus.load())
9899
{
99100
rc = _pid;
100101
}
@@ -111,13 +112,13 @@ int ProcessHandleImpl::tryWait() const
111112
return -1;
112113
if (rc != _pid)
113114
throw SystemException("Cannot wait for process", NumberFormatter::format(_pid));
114-
return statusToExitCode(_status.value());
115+
return statusToExitCode(_status.load());
115116
}
116117

117118

118119
bool ProcessHandleImpl::isRunning() const
119120
{
120-
if (_status.has_value())
121+
if (_hasStatus.load())
121122
return false;
122123
return wait(WNOHANG) == 0;
123124
}
@@ -352,14 +353,14 @@ bool ProcessImpl::isRunningImpl(const ProcessHandleImpl& handle)
352353

353354
bool ProcessImpl::isRunningImpl(PIDImpl pid)
354355
{
355-
if (::kill(pid, 0) == 0)
356-
{
357-
return true;
358-
}
359-
else
360-
{
356+
int status;
357+
int rc = ::waitpid(pid, &status, WNOHANG);
358+
if (rc == pid)
361359
return false;
362-
}
360+
if (rc == 0)
361+
return true;
362+
// Not our child or error; fall back to kill check
363+
return ::kill(pid, 0) == 0;
363364
}
364365

365366

Foundation/testsuite/src/ProcessTest.cpp

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "Poco/Environment.h"
2020
#include "Poco/ProcessOptions.h"
2121
#include "Poco/Thread.h"
22+
#include <atomic>
2223
#include <csignal>
2324
#include <iostream>
2425

@@ -359,29 +360,145 @@ void ProcessTest::testIsRunningAllowsForTermination()
359360
#if !defined(_WIN32_WCE)
360361
std::string name("TestApp");
361362
std::string cmd;
363+
#if defined(_DEBUG) && (POCO_OS != POCO_OS_ANDROID)
364+
name += "d";
365+
#endif
362366

363367
#if defined(POCO_OS_FAMILY_UNIX)
364-
cmd = "./";
365368
cmd += name;
366369
#else
367370
cmd = name;
368371
#endif
369372

373+
// Launch with pipes to reproduce the original defunct process scenario (issue #1097).
374+
// When a process launched with pipes exits, it becomes defunct until reaped.
370375
std::vector<std::string> args;
371-
ProcessHandle ph = Process::launch(cmd, args, nullptr, nullptr, nullptr);
376+
args.push_back("-count");
377+
Pipe inPipe;
378+
ProcessHandle ph = Process::launch(cmd, args, &inPipe, nullptr, nullptr);
379+
PipeOutputStream ostr(inPipe);
380+
ostr << std::string(10, 'x');
381+
ostr.close();
382+
while (Process::isRunning(ph))
383+
Thread::sleep(100);
384+
// Process should be reaped by now; wait must not hang
385+
int rc = ph.wait();
386+
assertTrue (rc == 10);
387+
#endif // !defined(_WIN32_WCE)
388+
}
389+
390+
391+
void ProcessTest::testIsRunningByPidAllowsForTermination()
392+
{
393+
#if defined(POCO_OS_FAMILY_UNIX)
394+
std::string name("TestApp");
395+
std::string cmd;
396+
#if defined(_DEBUG) && (POCO_OS != POCO_OS_ANDROID)
397+
name += "d";
398+
#endif
399+
400+
cmd += name;
401+
402+
// Test the PID-only isRunning overload with a child process
403+
std::vector<std::string> args;
404+
args.push_back("-count");
405+
Pipe inPipe;
406+
ProcessHandle ph = Process::launch(cmd, args, &inPipe, nullptr, nullptr);
407+
Process::PID pid = ph.id();
408+
PipeOutputStream ostr(inPipe);
409+
ostr << std::string(10, 'x');
410+
ostr.close();
411+
while (Process::isRunning(pid))
412+
Thread::sleep(100);
413+
#endif // defined(POCO_OS_FAMILY_UNIX)
414+
}
415+
416+
417+
void ProcessTest::testWaitAfterIsRunning()
418+
{
419+
#if !defined(_WIN32_WCE)
420+
std::string name("TestApp");
421+
std::string cmd;
422+
#if defined(_DEBUG) && (POCO_OS != POCO_OS_ANDROID)
423+
name += "d";
424+
#endif
425+
426+
#if defined(POCO_OS_FAMILY_UNIX)
427+
cmd += name;
428+
#else
429+
cmd = name;
430+
#endif
431+
432+
// Verify that calling wait() after isRunning() returns false works correctly.
433+
// isRunning() reaps the process internally via waitpid; wait() must still succeed.
434+
std::vector<std::string> args;
435+
args.push_back("-count");
436+
Pipe inPipe;
437+
ProcessHandle ph = Process::launch(cmd, args, &inPipe, nullptr, nullptr);
438+
PipeOutputStream ostr(inPipe);
439+
ostr << std::string(42, 'x');
440+
ostr.close();
372441
while (Process::isRunning(ph))
373442
Thread::sleep(100);
443+
int rc = ph.wait();
444+
assertTrue (rc == 42);
374445
#endif // !defined(_WIN32_WCE)
375446
}
376447

377448

449+
void ProcessTest::testConcurrentWaitAndIsRunning()
450+
{
451+
#if defined(POCO_OS_FAMILY_UNIX)
452+
std::string name("TestApp");
453+
std::string cmd;
454+
#if defined(_DEBUG) && (POCO_OS != POCO_OS_ANDROID)
455+
name += "d";
456+
#endif
457+
458+
cmd += name;
459+
460+
// Test thread safety: one thread polls isRunning while another calls wait.
461+
std::vector<std::string> args;
462+
args.push_back("-count");
463+
Pipe inPipe;
464+
ProcessHandle ph = Process::launch(cmd, args, &inPipe, nullptr, nullptr);
465+
PipeOutputStream ostr(inPipe);
466+
467+
std::atomic<int> waitResult{-1};
468+
std::atomic<bool> waitDone{false};
469+
470+
Thread waiter;
471+
waiter.startFunc([&ph, &waitResult, &waitDone]()
472+
{
473+
waitResult = ph.wait();
474+
waitDone = true;
475+
});
476+
477+
// Let the waiter thread start, then close the pipe to let the process exit
478+
Thread::sleep(100);
479+
ostr << std::string(7, 'x');
480+
ostr.close();
481+
482+
// Poll isRunning concurrently with the waiter thread
483+
while (Process::isRunning(ph))
484+
Thread::sleep(50);
485+
486+
waiter.join();
487+
assertTrue (waitDone);
488+
assertTrue (waitResult == 7);
489+
#endif // defined(POCO_OS_FAMILY_UNIX)
490+
}
491+
492+
378493
void ProcessTest::testSignalExitCode()
379494
{
380495
#if defined(POCO_OS_FAMILY_UNIX)
381496
std::string name("TestApp");
382497
std::string cmd;
498+
#if defined(_DEBUG) && (POCO_OS != POCO_OS_ANDROID)
499+
name += "d";
500+
#endif
383501

384-
cmd = "./";
385502
cmd += name;
386503

387504
std::vector<std::string> args;
@@ -417,6 +534,9 @@ CppUnit::Test* ProcessTest::suite()
417534
CppUnit_addTest(pSuite, ProcessTest, testIsRunning);
418535
CppUnit_addTest(pSuite, ProcessTest, testLaunchCloseHandles);
419536
CppUnit_addTest(pSuite, ProcessTest, testIsRunningAllowsForTermination);
537+
CppUnit_addTest(pSuite, ProcessTest, testIsRunningByPidAllowsForTermination);
538+
CppUnit_addTest(pSuite, ProcessTest, testWaitAfterIsRunning);
539+
CppUnit_addTest(pSuite, ProcessTest, testConcurrentWaitAndIsRunning);
420540
CppUnit_addTest(pSuite, ProcessTest, testSignalExitCode);
421541

422542
return pSuite;

Foundation/testsuite/src/ProcessTest.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class ProcessTest: public CppUnit::TestCase
3434
void testIsRunning();
3535
void testLaunchCloseHandles();
3636
void testIsRunningAllowsForTermination();
37+
void testIsRunningByPidAllowsForTermination();
38+
void testWaitAfterIsRunning();
39+
void testConcurrentWaitAndIsRunning();
3740
void testSignalExitCode();
3841

3942
void setUp();

0 commit comments

Comments
 (0)