Skip to content

Implemented better PID handling. #39

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

Merged
merged 2 commits into from
Apr 29, 2025
Merged

Conversation

AlexSkrypnyk
Copy link
Member

@AlexSkrypnyk AlexSkrypnyk commented Apr 29, 2025

Summary by CodeRabbit

  • Refactor
    • Improved internal logic for detecting running PHP server processes, resulting in clearer and more modular process management.
  • Tests
    • Enhanced and expanded test coverage for PHP server process detection, including new targeted tests for specific detection methods and improved test clarity.
  • Chores
    • Updated PHP testing workflow to run across multiple operating systems and PHP versions, improving test coverage and caching efficiency.

Copy link

coderabbitai bot commented Apr 29, 2025

Walkthrough

The changes refactor the PID detection logic in the PhpServerContext class by splitting the original getPid method into three distinct methods: getPid, getPidLsof, and getPidNetstat. The main getPid method now delegates PID retrieval to the two new helpers, which separately handle PID extraction using the lsof and netstat utilities. Error handling and output parsing were improved, with helpers returning 0 on failure and the main method throwing an exception if both helpers fail. Corresponding unit tests were updated and expanded to directly test each helper method, using anonymous subclasses to simulate various scenarios. Additionally, the GitHub Actions workflow was enhanced to run tests across multiple operating systems and PHP versions with improved caching and artifact naming.

Changes

File(s) Change Summary
src/DrevOps/BehatPhpServer/PhpServerContext.php Refactored getPid to delegate to new getPidLsof and getPidNetstat helpers; improved error handling and output parsing.
tests/phpunit/Unit/PhpServerContextTest.php Refactored and expanded tests for PID retrieval; added tests for getPidLsof and getPidNetstat; used anonymous subclasses for mocking; improved data providers and test structure.
.github/workflows/test-php.yml Modified PHP test workflow to run on multiple OSes (ubuntu-latest, macos-latest) and PHP versions (8.2, 8.3, 8.4); improved caching and artifact naming; disabled fail-fast.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Unit Test
    participant Context as PhpServerContext
    participant Lsof as getPidLsof
    participant Netstat as getPidNetstat

    Test->>Context: getPid(port)
    Context->>Lsof: getPidLsof(port)
    alt Lsof finds PID
        Lsof-->>Context: PID
        Context-->>Test: PID
    else Lsof returns 0
        Context->>Netstat: getPidNetstat(port)
        alt Netstat finds PID
            Netstat-->>Context: PID
            Context-->>Test: PID
        else Netstat returns 0
            Context-->>Test: throws RuntimeException
        end
    end
Loading

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

codecov bot commented Apr 29, 2025

Codecov Report

Attention: Patch coverage is 95.12195% with 2 lines in your changes missing coverage. Please review.

Project coverage is 75.00%. Comparing base (e0fc292) to head (23e3f45).
Report is 2 commits behind head on main.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/DrevOps/BehatPhpServer/PhpServerContext.php 95.12% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #39      +/-   ##
==========================================
+ Coverage   73.57%   75.00%   +1.42%     
==========================================
  Files           3        3              
  Lines         420      432      +12     
==========================================
+ Hits          309      324      +15     
+ Misses        111      108       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🔭 Outside diff range comments (2)
src/DrevOps/BehatPhpServer/PhpServerContext.php (1)

542-582: ⚠️ Potential issue

getPidLsof() wrongly filters out common PHP binaries such as php-fpm

lsof may report executables like php-fpm, php74, php8.2, etc.
Requiring the first column to be strictly equal to the string php creates false negatives.

In addition, leading spaces are not removed, so $parts[0] can even be an empty string.

-    foreach ($output as $line) {
-      $line = preg_replace('/\s+/', ' ', $line);
+    foreach ($output as $line) {
+      // Collapse multiple spaces _and_ trim both ends.
+      $line = trim(preg_replace('/\s+/', ' ', $line));
       $this->debug(sprintf('Processing line: %s', $line));
       $parts = explode(' ', (string) $line);
 
-      if (count($parts) > 1 && $parts[0] === 'php' && is_numeric($parts[1])) {
+      // Accept any executable that *starts with* "php" (php, php-fpm, php8.3…).
+      if (count($parts) > 1 && str_starts_with($parts[0], 'php') && is_numeric($parts[1])) {
         $pid = intval($parts[1]);
         $this->debug(sprintf('Found PHP process with PID %s using lsof.', $pid));
         return $pid;
       }
     }

This tiny relaxation makes the helper much more robust across different PHP setups.

tests/phpunit/Unit/PhpServerContextTest.php (1)

431-577: 🛠️ Refactor suggestion

The test bypasses production code paths, reducing coverage

The anonymous subclass manually throws an exception when both helpers return 0:

if ($this->expectException && $this->lsofPid === 0 && $this->netstatPid === 0) {
  throw new \RuntimeException('Unable …');
}

This means the real getPid() implementation (and the new debug message lines 526-527 flagged by Codecov) is never executed in the failure scenario, so a change in exception wording or logic would not be caught.

Recommend letting the original method throw instead:

-      public function testGetPid(int $port): int {
-        if ($this->expectException && $this->lsofPid === 0 && $this->netstatPid === 0) {
-          throw new \RuntimeException('Unable to determine PHP server process for port ' . $port);
-        }
-        return $this->getPid($port);
-      }
+      public function testGetPid(int $port): int {
+        // Delegate entirely to the real implementation.
+        return $this->getPid($port);
+      }

Then rely solely on $this->expectException(...) in the test body.
This will exercise the actual code, boosting meaningful coverage and confidence.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0fc292 and b88fb93.

📒 Files selected for processing (2)
  • src/DrevOps/BehatPhpServer/PhpServerContext.php (2 hunks)
  • tests/phpunit/Unit/PhpServerContextTest.php (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
tests/phpunit/Unit/PhpServerContextTest.php (1)
src/DrevOps/BehatPhpServer/PhpServerContext.php (7)
  • PhpServerContext (18-680)
  • processExists (483-497)
  • getPidLsof (542-582)
  • getPidNetstat (593-642)
  • getPid (511-531)
  • debug (670-678)
  • executeCommand (657-662)
src/DrevOps/BehatPhpServer/PhpServerContext.php (1)
tests/phpunit/Unit/PhpServerContextTest.php (7)
  • getPidLsof (487-489)
  • getPidNetstat (491-493)
  • debug (504-506)
  • debug (660-662)
  • debug (791-793)
  • executeCommand (643-654)
  • executeCommand (774-785)
🪛 GitHub Check: codecov/patch
src/DrevOps/BehatPhpServer/PhpServerContext.php

[warning] 526-527: src/DrevOps/BehatPhpServer/PhpServerContext.php#L526-L527
Added lines #L526 - L527 were not covered by tests

Comment on lines 593 to 644
protected function getPidNetstat(int $port): int {
if (!$this->executeCommand('which netstat 2>/dev/null')) {
return 0;
}

$command = sprintf("netstat -an 2>/dev/null | grep ':%s' | grep 'LISTEN'", $port);

$output = [];
$this->executeCommand($command, $output);

if (empty($output)) {
// Try without the LISTEN filter in case the process is in another state.
$command = str_replace(" | grep 'LISTEN'", '', $command);
$this->debug(sprintf('No LISTEN processes found, retrying with command: %s', $command));
$this->executeCommand($command, $output);
}

if (empty($output)) {
$this->debug('No processes found on port ' . $port);
return 0;
}

// Log all found processes.
foreach ($output as $i => $line) {
$this->debug(sprintf('Found process %d: %s', $i + 1, $line));
}

foreach ($output as $line) {
$line = preg_replace('/\s+/', ' ', $line);
$this->debug(sprintf('Processing line: %s', $line));
$parts = explode(' ', (string) $line);

foreach ($parts as $part) {
if (str_contains($part, '/php')) {
$pid_name_parts = explode('/', $part);
if (count($pid_name_parts) > 1) {
$found_pid = $pid_name_parts[0];
$name = $pid_name_parts[1];
if (is_numeric($found_pid) && strpos($name, 'php') === 0) {
$pid = intval($found_pid);
$this->debug(sprintf('Found PHP process with PID %s using netstat.', $pid));
break;
return $pid;
}
}
}
}
}

if ($pid === 0) {
$this->debug('Could not identify PHP process from output: ' . implode("\n", $output));
throw new \RuntimeException(sprintf('Unable to determine if PHP server was started: PID is 0 in command "%s" output. Manually identify the process and terminate it.', $command));
}

return $pid;
return 0;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Minor: duplicated parsing logic could be centralised

getPidLsof() and getPidNetstat() share ~80 % identical parsing & debug code.
Extracting a small private helper like parsePidLines(callable $filter, array $output): int would DRY the class and ease future maintenance, but this is optional.

Comment on lines +520 to +531
$pid = $this->getPidLsof($port);
if ($pid === 0) {
$pid = $this->getPidNetstat($port);
}
elseif ($this->executeCommand('which netstat 2>/dev/null')) {
$command = sprintf("netstat -an 2>/dev/null | grep ':%s' | grep 'LISTEN'", $port);
$type = 'netstat';

if ($pid === 0) {
$this->debug('Could not identify PHP process using lsof or netstat.');
throw new \RuntimeException(sprintf('Unable to determine PHP server process for port %d. Manually identify the process and terminate it.', $port));
}
else {
$this->debug('No supported OS utilities found for process detection.');
throw new \RuntimeException('Unable to determine if PHP server was started: no supported OS utilities found. Manually identify the process and terminate it.');

return $pid;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate that the helper-returned PID is actually alive

getPid() trusts the first non-zero value coming from getPidLsof() / getPidNetstat().
If the process has already exited between the helper call and the moment you act upon the PID, you will still return a stale value, causing terminateProcess() to fail later on.

     if ($pid === 0) {
       $this->debug('Could not identify PHP process using lsof or netstat.');
       throw new \RuntimeException(sprintf('Unable to determine PHP server process for port %d. Manually identify the process and terminate it.', $port));
     }
 
+    // Double-check that the PID is still running.
+    if (!$this->processExists($pid)) {
+      $this->debug(sprintf('PID %d reported by helpers is no longer running.', $pid));
+      throw new \RuntimeException(sprintf('Process %d (port %d) is not running any longer.', $pid, $port));
+    }
+
     return $pid;
   }

This extra guard makes the public behaviour deterministic and prevents silent failures when a zombie PID slips through.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 526-527: src/DrevOps/BehatPhpServer/PhpServerContext.php#L526-L527
Added lines #L526 - L527 were not covered by tests

Comment on lines +594 to +629
// Create a subclass of PhpServerContext that we can customize.
$mock_class = new class($test_class, $lsof_exists, $output, $expected_pid) extends PhpServerContext {
/**
* Flag indicating if lsof exists on the system.
*/
private bool $lsofExists;

/**
* Mock output from the lsof command.
*
* @var array<string>
*/
private array $mockOutput;

/**
* Constructor.
*
* @param object $test_class
* The test class instance.
* @param bool $lsof_exists
* Whether lsof exists on the system.
* @param array<string> $output
* The output from the lsof command.
* @param int $expected_pid
* The expected PID to be returned.
*
* @phpstan-ignore-next-line
*/
public function __construct(object $test_class, bool $lsof_exists, array $output, int $expected_pid) {
$this->lsofExists = $lsof_exists;
$this->mockOutput = $output;
// Skip parent constructor.
}

/**
* Execute a command.
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Unused constructor parameter $expected_pid

$expected_pid is accepted by the anonymous class but never stored or used, which is misleading.

-    $mock_class = new class($test_class, $lsof_exists, $output, $expected_pid) extends PhpServerContext {
+    $mock_class = new class($test_class, $lsof_exists, $output) extends PhpServerContext {-      public function __construct(object $test_class, bool $lsof_exists, array $output, int $expected_pid) {
+      public function __construct(object $test_class, bool $lsof_exists, array $output) {

Please clean up the signature (same for the netstat test below) to avoid dead parameters.

@AlexSkrypnyk AlexSkrypnyk force-pushed the feture/fix-pid-in-alpine branch from b88fb93 to 23e3f45 Compare April 29, 2025 22:57
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (1)
src/DrevOps/BehatPhpServer/PhpServerContext.php (1)

543-562: 🧹 Nitpick (assertive)

Use lsof -t for simpler & faster PID extraction

lsof -i -P -n | grep … spawns two extra greps and forces you to parse the full line.
lsof -ti :$port -sTCP:LISTEN -c ^php will output PIDs only, one per line, which:

  • avoids the heavy string manipulation that follows,
  • eliminates the risk of matching a non-PHP executable that merely contains the word “php”.
-    $command = sprintf("lsof -i -P -n 2>/dev/null | grep 'php' | grep ':%s' | grep 'LISTEN'", $port);
+    $command = sprintf("lsof -ti :%d -sTCP:LISTEN -c ^php 2>/dev/null", $port);

Parsing then reduces to return (int) ($output[0] ?? 0);.

♻️ Duplicate comments (4)
src/DrevOps/BehatPhpServer/PhpServerContext.php (1)

520-531: 🛠️ Refactor suggestion

Cache the discovered PID and double-check liveness

  1. getPid() returns a fresh PID but never assigns it to $this->pid.
    Subsequent calls will re-run the detection logic even though nothing changed.

  2. In a previous review we suggested validating that the helper-returned PID is still alive before returning it. The concern is still present.

-    return $pid;
+    // Persist for future calls.
+    $this->pid = $pid;
+
+    // Final sanity-check to avoid returning a zombie PID.
+    if (!$this->processExists($pid)) {
+      $this->debug(sprintf('PID %d reported by helpers is no longer running.', $pid));
+      throw new \RuntimeException(sprintf('Process %d (port %d) is not running any longer.', $pid, $port));
+    }
+
+    return $pid;
tests/phpunit/Unit/PhpServerContextTest.php (3)

474-482: 🧹 Nitpick (assertive)

Unused constructor parameter $expected_pid

$expected_pid is passed to the anonymous class but never stored or referenced, which can confuse future readers.

-      public function __construct(object $test_class, bool $has_pid, int $lsof_pid, int $netstat_pid, bool $expect_exception) {
+      public function __construct(object $test_class, bool $has_pid, int $lsof_pid, int $netstat_pid, bool $expect_exception) {
         $this->hasPid = $has_pid;
         $this->lsofPid = $lsof_pid;
         $this->netstatPid = $netstat_pid;
         $this->expectException = $expect_exception;
         $this->pid = $has_pid ? 12345 : 0;
-        // Skip parent constructor.
+        // Skip parent constructor.
       }

(identical issue exists in the getPidLsof and getPidNetstat helper test classes).


622-626: 🧹 Nitpick (assertive)

Parameter $expected_pid is never used inside the constructor

Same nitpick as above: the parameter is superfluous and can be safely removed to declutter the test helper.


753-757: 🧹 Nitpick (assertive)

Redundant $expected_pid parameter

The constructor for the netstat mock also carries an unused $expected_pid. Dropping it keeps the test helpers minimal and easier to read.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between b88fb93 and 23e3f45.

📒 Files selected for processing (3)
  • .github/workflows/test-php.yml (3 hunks)
  • src/DrevOps/BehatPhpServer/PhpServerContext.php (2 hunks)
  • tests/phpunit/Unit/PhpServerContextTest.php (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/DrevOps/BehatPhpServer/PhpServerContext.php (1)
tests/phpunit/Unit/PhpServerContextTest.php (7)
  • getPidLsof (487-489)
  • getPidNetstat (491-493)
  • debug (504-506)
  • debug (660-662)
  • debug (791-793)
  • executeCommand (643-654)
  • executeCommand (774-785)
🔇 Additional comments (1)
.github/workflows/test-php.yml (1)

25-28:

❌ Incorrect review comment

PHP 8.4 image may not be available yet

shivammathur/setup-php@v2 currently does not publish nightly images for future minor versions until they enter the alpha/beta phase.
Including 8.4 in the matrix could make every job that runs on that axis fail immediately with “PHP 8.4 is not available”.

Consider guarding the version behind an include-matrix that is only enabled on the nightly channel, or remove it until the image exists.


I’ll verify whether shivammathur/setup-php@v2 advertises support for PHP 8.4 in its README.


🏁 Script executed:

#!/bin/bash
# Check if PHP 8.4 is listed as a supported version in the v2 README
curl -s https://raw.githubusercontent.com/shivammathur/setup-php/v2/README.md | grep -n "8\\.4"

Length of output: 1358


No action needed: PHP 8.4 is already supported by shivammathur/setup-php@v2
The v2 README lists PHP 8.4 as a “Stable”-“Active” version, so including it in your matrix will not break the workflow.

Likely an incorrect or invalid review comment.

Comment on lines +38 to 40
key: ${{ matrix.os }}-php-${{ matrix.php-versions }}-${{ hashFiles('**/composer.lock') }}

- name: Setup PHP
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Cache key is unique but restore-keys are missing

The cache is written with a key that includes OS + PHP version, which is great.
However, without restore-keys Composer will do a fresh install whenever the lock-file hash changes even slightly. Adding a generic prefix (e.g. "${{ matrix.os }}-php-${{ matrix.php-versions }}-") to restore-keys will let the job fall back to an older, but still useful, cache layer.

@AlexSkrypnyk AlexSkrypnyk merged commit 9522278 into main Apr 29, 2025
9 checks passed
@AlexSkrypnyk AlexSkrypnyk deleted the feture/fix-pid-in-alpine branch April 29, 2025 23:01
@coderabbitai coderabbitai bot mentioned this pull request Apr 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant