Skip to content

Commit fe3e069

Browse files
authored
Merge pull request #767 from lucatume/v4-better-pid-check
fix(PidBasedController) check PID maps to running process
2 parents 7163a8e + 8993ad5 commit fe3e069

7 files changed

+209
-4
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [unreleased] Unreleased
66

7+
## Fixed
8+
9+
- Check the PID file for the PHP built-in server, MySQL and Chromedriver controllers to make sure the PID maps to an actually running process.
10+
711
## [4.3.9] 2024-11-29;
812

913
## Changed

src/Extension/BuiltInServerController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public function start(OutputInterface $output): void
1919
{
2020
$pidFile = $this->getPidFile();
2121

22-
if (is_file($pidFile)) {
22+
if ($this->isProcessRunning($pidFile)) {
2323
$output->writeln('PHP built-in server already running.');
2424
return;
2525
}

src/Extension/ChromeDriverController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function start(OutputInterface $output): void
2020
{
2121
$pidFile = $this->getPidFile();
2222

23-
if (is_file($pidFile)) {
23+
if ($this->isProcessRunning($pidFile)) {
2424
$output->writeln('ChromeDriver already running.');
2525

2626
return;

src/Extension/MysqlServerController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function start(OutputInterface $output): void
2020
{
2121
$pidFile = $this->getPidFile();
2222

23-
if (is_file($pidFile)) {
23+
if ($this->isProcessRunning($pidFile)) {
2424
$output->writeln('MySQL server already running.');
2525

2626
return;

src/Extension/PidBasedController.php

+50
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace lucatume\WPBrowser\Extension;
44

55
use Codeception\Exception\ExtensionException;
6+
use lucatume\WPBrowser\Exceptions\RuntimeException;
67

78
trait PidBasedController
89
{
@@ -49,4 +50,53 @@ protected function removePidFile(string $pidFile): void
4950
);
5051
}
5152
}
53+
54+
/**
55+
* @throws RuntimeException
56+
*/
57+
protected function isProcessRunning(string $pidFile):bool
58+
{
59+
if (!is_file($pidFile)) {
60+
return false;
61+
}
62+
63+
try {
64+
$pidFileContents = file_get_contents($pidFile);
65+
if ($pidFileContents === false) {
66+
throw new \Exception();
67+
}
68+
} catch (\Exception $e) {
69+
if (!unlink($pidFile)) {
70+
throw new RuntimeException("Failed to delete PID file: $pidFile");
71+
}
72+
73+
return false;
74+
}
75+
76+
$pid = trim($pidFileContents);
77+
78+
if (!is_numeric($pid) || (int)$pid === 0) {
79+
return false;
80+
}
81+
82+
if (PHP_OS_FAMILY === 'Windows') {
83+
$output = [];
84+
exec("tasklist /FI \"PID eq $pid\" 2>NUL", $output);
85+
86+
return str_contains(implode("\n", $output), $pid);
87+
} else {
88+
// Check if the process is running on POSIX (Mac or Linux)
89+
exec("ps -p $pid", $output, $resultCode);
90+
if ($resultCode === 0 && count($output) > 1) {
91+
// Process is running
92+
return true;
93+
}
94+
}
95+
96+
if (!unlink($pidFile)) {
97+
throw new RuntimeException("Failed to delete PID file: $pidFile");
98+
}
99+
100+
return false;
101+
}
52102
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
4+
namespace Unit\lucatume\WPBrowser\Extension;
5+
6+
use lucatume\WPBrowser\Extension\PidBasedController;
7+
use lucatume\WPBrowser\Exceptions\RuntimeException;
8+
use Symfony\Component\Process\Process;
9+
10+
class PidBasedControllerTest extends \Codeception\Test\Unit
11+
{
12+
public function test_isProcessRunning_on_posix():void{
13+
$testClass = new class {
14+
use PidBasedController;
15+
16+
public function openIsProcessRunning(string $pidFile):bool{
17+
return $this->isProcessRunning($pidFile);
18+
}
19+
};
20+
$hash = md5(microtime());
21+
$pidFile = sys_get_temp_dir()."/test-{$hash}.pid";
22+
$pid = posix_getpid();
23+
if(!file_put_contents($pidFile,$pid)){
24+
$this->fail('Could not write pid to file '.$pidFile);
25+
}
26+
27+
$this->assertTrue($testClass->openIsProcessRunning($pidFile));
28+
$this->assertFileExists($pidFile);
29+
}
30+
31+
public function test_isProcessRunning_returns_false_if_pid_file_not_exists():void{
32+
$testClass = new class {
33+
use PidBasedController;
34+
35+
public function openIsProcessRunning(string $pidFile):bool{
36+
return $this->isProcessRunning($pidFile);
37+
}
38+
};
39+
$pid = posix_getpid();
40+
41+
$this->assertFalse($testClass->openIsProcessRunning(__DIR__ .'/test.pid'));
42+
}
43+
44+
public function test_isProcessRunning_throws_if_pid_file_cannot_be_read():void{
45+
$testClass = new class {
46+
use PidBasedController;
47+
48+
public function openIsProcessRunning(string $pidFile):bool{
49+
return $this->isProcessRunning($pidFile);
50+
}
51+
};
52+
$pid = posix_getpid();
53+
$hash = md5(microtime());
54+
$pidFile = sys_get_temp_dir()."/test-{$hash}.pid";
55+
$pid = posix_getpid();
56+
if(!file_put_contents($pidFile,$pid)){
57+
$this->fail('Could not write pid to file '.$pidFile);
58+
}
59+
// Change the file mode to not be readable by the current user.
60+
chmod($pidFile,0000);
61+
62+
$this->assertFalse($testClass->openIsProcessRunning($pidFile));
63+
}
64+
65+
public function test_isProcessRunning_returns_false_if_process_is_not_running():void{
66+
$testClass = new class {
67+
use PidBasedController;
68+
69+
public function openIsProcessRunning(string $pidFile):bool{
70+
return $this->isProcessRunning($pidFile);
71+
}
72+
};
73+
$hash = md5(microtime());
74+
$pidFile = sys_get_temp_dir()."/test-{$hash}.pid";
75+
$process = new Process(['echo', '23']);
76+
$process->start();
77+
$pid = $process->getPid();
78+
$process->wait();
79+
if(!file_put_contents($pidFile,$pid)){
80+
$this->fail('Could not write pid to file '.$pidFile);
81+
}
82+
83+
$this->assertFalse($testClass->openIsProcessRunning($pidFile));
84+
$this->assertFileNotExists($pidFile);
85+
}
86+
}

tests/unit/lucatume/WPBrowser/Module/WPLoaderArbitraryPluginLocationTest.php

+66-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Codeception\Lib\Di;
66
use Codeception\Lib\ModuleContainer;
77
use Codeception\Test\Unit;
8-
use lucatume\WPBrowser\Module\WPLoader;
98
use lucatume\WPBrowser\Tests\Traits\DatabaseAssertions;
109
use lucatume\WPBrowser\Tests\Traits\LoopIsolation;
1110
use lucatume\WPBrowser\Tests\Traits\MainInstallationAccess;
@@ -56,6 +55,72 @@ private function module(array $moduleContainerConfig = [], ?array $moduleConfig
5655
return new WPLoader($this->mockModuleContainer, ($moduleConfig ?? $this->config));
5756
}
5857

58+
public function test_loads_plugins_from_default_location_correctly(): void
59+
{
60+
$projectDir = FS::tmpDir('wploader_');
61+
$installation = Installation::scaffold($projectDir);
62+
$dbName = Random::dbName();
63+
$dbHost = Env::get('WORDPRESS_DB_HOST');
64+
$dbUser = Env::get('WORDPRESS_DB_USER');
65+
$dbPassword = Env::get('WORDPRESS_DB_PASSWORD');
66+
$installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_');
67+
if (!mkdir($projectDir . '/wp-content/plugins/test-one', 0777, true)) {
68+
throw new \RuntimeException('Unable to create test directory for plugin test-one');
69+
}
70+
if (!file_put_contents(
71+
$projectDir . '/wp-content/plugins/test-one/plugin.php',
72+
<<< PHP
73+
<?php
74+
/**
75+
* Plugin Name: Test One
76+
*/
77+
78+
function test_one_loaded(){}
79+
PHP
80+
)) {
81+
throw new \RuntimeException('Unable to create test plugin file for plugin test-one');
82+
}
83+
if (!mkdir($projectDir . '/wp-content/plugins/test-two', 0777, true)) {
84+
throw new \RuntimeException('Unable to create test directory for plugin test-two');
85+
}
86+
if (!file_put_contents(
87+
$projectDir . '/wp-content/plugins/test-two/plugin.php',
88+
<<< PHP
89+
<?php
90+
/**
91+
* Plugin Name: Test Two
92+
*/
93+
94+
function test_two_loaded(){}
95+
PHP
96+
)) {
97+
throw new \RuntimeException('Unable to create test plugin file for plugin test-two');
98+
}
99+
100+
$this->config = [
101+
'wpRootFolder' => $projectDir,
102+
'dbUrl' => $installationDb->getDbUrl(),
103+
'tablePrefix' => 'test_',
104+
'plugins' => [
105+
'test-one/plugin.php',
106+
'test-two/plugin.php',
107+
]
108+
];
109+
$wpLoader = $this->module();
110+
$projectDirname = basename($projectDir);
111+
112+
$this->assertInIsolation(
113+
static function () use ($wpLoader, $projectDir) {
114+
chdir($projectDir);
115+
116+
$wpLoader->_initialize();
117+
118+
Assert::assertTrue(function_exists('test_one_loaded'));
119+
Assert::assertTrue(function_exists('test_two_loaded'));
120+
}
121+
);
122+
}
123+
59124
/**
60125
* It should allow loading a plugin from an arbitrary path
61126
*

0 commit comments

Comments
 (0)