Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(PidBasedController) check PID maps to running process #767

Merged
merged 1 commit into from
Nov 29, 2024
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [unreleased] Unreleased

## Fixed

- 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.

## [4.3.9] 2024-11-29;

## Changed
Expand Down
2 changes: 1 addition & 1 deletion src/Extension/BuiltInServerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function start(OutputInterface $output): void
{
$pidFile = $this->getPidFile();

if (is_file($pidFile)) {
if ($this->isProcessRunning($pidFile)) {
$output->writeln('PHP built-in server already running.');
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Extension/ChromeDriverController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function start(OutputInterface $output): void
{
$pidFile = $this->getPidFile();

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

return;
Expand Down
2 changes: 1 addition & 1 deletion src/Extension/MysqlServerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function start(OutputInterface $output): void
{
$pidFile = $this->getPidFile();

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

return;
Expand Down
50 changes: 50 additions & 0 deletions src/Extension/PidBasedController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace lucatume\WPBrowser\Extension;

use Codeception\Exception\ExtensionException;
use lucatume\WPBrowser\Exceptions\RuntimeException;

trait PidBasedController
{
Expand Down Expand Up @@ -49,4 +50,53 @@ protected function removePidFile(string $pidFile): void
);
}
}

/**
* @throws RuntimeException
*/
protected function isProcessRunning(string $pidFile):bool
{
if (!is_file($pidFile)) {
return false;
}

try {
$pidFileContents = file_get_contents($pidFile);
if ($pidFileContents === false) {
throw new \Exception();
}
} catch (\Exception $e) {
if (!unlink($pidFile)) {
throw new RuntimeException("Failed to delete PID file: $pidFile");
}

return false;
}

$pid = trim($pidFileContents);

if (!is_numeric($pid) || (int)$pid === 0) {
return false;
}

if (PHP_OS_FAMILY === 'Windows') {
$output = [];
exec("tasklist /FI \"PID eq $pid\" 2>NUL", $output);

return str_contains(implode("\n", $output), $pid);
} else {
// Check if the process is running on POSIX (Mac or Linux)
exec("ps -p $pid", $output, $resultCode);
if ($resultCode === 0 && count($output) > 1) {
// Process is running
return true;
}
}

if (!unlink($pidFile)) {
throw new RuntimeException("Failed to delete PID file: $pidFile");
}

return false;
}
}
86 changes: 86 additions & 0 deletions tests/unit/lucatume/WPBrowser/Extension/PidBasedControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php


namespace Unit\lucatume\WPBrowser\Extension;

use lucatume\WPBrowser\Extension\PidBasedController;
use lucatume\WPBrowser\Exceptions\RuntimeException;
use Symfony\Component\Process\Process;

class PidBasedControllerTest extends \Codeception\Test\Unit
{
public function test_isProcessRunning_on_posix():void{
$testClass = new class {
use PidBasedController;

public function openIsProcessRunning(string $pidFile):bool{
return $this->isProcessRunning($pidFile);
}
};
$hash = md5(microtime());
$pidFile = sys_get_temp_dir()."/test-{$hash}.pid";
$pid = posix_getpid();
if(!file_put_contents($pidFile,$pid)){
$this->fail('Could not write pid to file '.$pidFile);
}

$this->assertTrue($testClass->openIsProcessRunning($pidFile));
$this->assertFileExists($pidFile);
}

public function test_isProcessRunning_returns_false_if_pid_file_not_exists():void{
$testClass = new class {
use PidBasedController;

public function openIsProcessRunning(string $pidFile):bool{
return $this->isProcessRunning($pidFile);
}
};
$pid = posix_getpid();

$this->assertFalse($testClass->openIsProcessRunning(__DIR__ .'/test.pid'));
}

public function test_isProcessRunning_throws_if_pid_file_cannot_be_read():void{
$testClass = new class {
use PidBasedController;

public function openIsProcessRunning(string $pidFile):bool{
return $this->isProcessRunning($pidFile);
}
};
$pid = posix_getpid();
$hash = md5(microtime());
$pidFile = sys_get_temp_dir()."/test-{$hash}.pid";
$pid = posix_getpid();
if(!file_put_contents($pidFile,$pid)){
$this->fail('Could not write pid to file '.$pidFile);
}
// Change the file mode to not be readable by the current user.
chmod($pidFile,0000);

$this->assertFalse($testClass->openIsProcessRunning($pidFile));
}

public function test_isProcessRunning_returns_false_if_process_is_not_running():void{
$testClass = new class {
use PidBasedController;

public function openIsProcessRunning(string $pidFile):bool{
return $this->isProcessRunning($pidFile);
}
};
$hash = md5(microtime());
$pidFile = sys_get_temp_dir()."/test-{$hash}.pid";
$process = new Process(['echo', '23']);
$process->start();
$pid = $process->getPid();
$process->wait();
if(!file_put_contents($pidFile,$pid)){
$this->fail('Could not write pid to file '.$pidFile);
}

$this->assertFalse($testClass->openIsProcessRunning($pidFile));
$this->assertFileNotExists($pidFile);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Codeception\Lib\Di;
use Codeception\Lib\ModuleContainer;
use Codeception\Test\Unit;
use lucatume\WPBrowser\Module\WPLoader;
use lucatume\WPBrowser\Tests\Traits\DatabaseAssertions;
use lucatume\WPBrowser\Tests\Traits\LoopIsolation;
use lucatume\WPBrowser\Tests\Traits\MainInstallationAccess;
Expand Down Expand Up @@ -56,6 +55,72 @@ private function module(array $moduleContainerConfig = [], ?array $moduleConfig
return new WPLoader($this->mockModuleContainer, ($moduleConfig ?? $this->config));
}

public function test_loads_plugins_from_default_location_correctly(): void
{
$projectDir = FS::tmpDir('wploader_');
$installation = Installation::scaffold($projectDir);
$dbName = Random::dbName();
$dbHost = Env::get('WORDPRESS_DB_HOST');
$dbUser = Env::get('WORDPRESS_DB_USER');
$dbPassword = Env::get('WORDPRESS_DB_PASSWORD');
$installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_');
if (!mkdir($projectDir . '/wp-content/plugins/test-one', 0777, true)) {
throw new \RuntimeException('Unable to create test directory for plugin test-one');
}
if (!file_put_contents(
$projectDir . '/wp-content/plugins/test-one/plugin.php',
<<< PHP
<?php
/**
* Plugin Name: Test One
*/

function test_one_loaded(){}
PHP
)) {
throw new \RuntimeException('Unable to create test plugin file for plugin test-one');
}
if (!mkdir($projectDir . '/wp-content/plugins/test-two', 0777, true)) {
throw new \RuntimeException('Unable to create test directory for plugin test-two');
}
if (!file_put_contents(
$projectDir . '/wp-content/plugins/test-two/plugin.php',
<<< PHP
<?php
/**
* Plugin Name: Test Two
*/

function test_two_loaded(){}
PHP
)) {
throw new \RuntimeException('Unable to create test plugin file for plugin test-two');
}

$this->config = [
'wpRootFolder' => $projectDir,
'dbUrl' => $installationDb->getDbUrl(),
'tablePrefix' => 'test_',
'plugins' => [
'test-one/plugin.php',
'test-two/plugin.php',
]
];
$wpLoader = $this->module();
$projectDirname = basename($projectDir);

$this->assertInIsolation(
static function () use ($wpLoader, $projectDir) {
chdir($projectDir);

$wpLoader->_initialize();

Assert::assertTrue(function_exists('test_one_loaded'));
Assert::assertTrue(function_exists('test_two_loaded'));
}
);
}

/**
* It should allow loading a plugin from an arbitrary path
*
Expand Down