Skip to content

Orphaned language-server:diagnostics Process When File Contains namespace Timer #2913

@happystraw

Description

@happystraw

Describe the bug

When a PHP file contains the line namespace Timer;, Phpactor spawns a language-server:diagnostics process that becomes orphaned and does not terminate as expected. This issue does not occur with other namespace names.

Environment

$ php -v
PHP 8.4.10 (cli) (built: Jul  4 2025 12:41:44) (NTS)
Copyright (c) The PHP Group
$ phpactor -V
Phpactor 2025.04.17.0

To Reproduce

  1. Create a directory (e.g., lsp-test).

  2. Add two files:

    • code.php — contains the PHP code that triggers the issue.
    • lsp-test.php — a script that simulates an LSP client to reproduce the problem.

    Directory structure:

    lsp-test/
    ├── code.php
    └── lsp-test.php
    

    code.php

    <?php
    
    namespace Timer;
    lsp-test.php
    <?php
    
    function sendLspMessage($pipes, $message)
    {
        $lspMessage = "Content-Length: " . strlen($message) . "\r\n\r\n" . $message;
    
        echo "------------------\n"; // Debugging output
        echo ">>>\n$lspMessage\n"; // Debugging output
    
        fwrite($pipes[0], $lspMessage);
        fflush($pipes[0]);
    
        // Read response
        $response = '';
        while (!feof($pipes[1])) {
            $line = fgets($pipes[1]);
            if ($line === FALSE) {
                break;
            }
    
            $response .= $line;
            if (strpos($response, "\r\n\r\n") !== FALSE) {
                // Read the JSON content based on Content-Length
                if (preg_match('/Content-Length: (\d+)/', $response, $matches)) {
                    $contentLength = (int)$matches[1];
                    $jsonStart = strpos($response, "\r\n\r\n") + 4;
                    $currentJsonLength = strlen($response) - $jsonStart;
    
                    while ($currentJsonLength < $contentLength) {
                        $chunk = fread($pipes[1], $contentLength - $currentJsonLength);
                        $response .= $chunk;
                        $currentJsonLength = strlen($response) - $jsonStart;
                    }
    
                    break;
                }
            }
        }
    
        echo "<<<\n$response\n"; // Debugging output
        echo "------------------\n"; // Debugging output"
        return $response;
    }
    
    function initializeLsp($pipes, $rootPath)
    {
        $initRequest = [
            'jsonrpc' => '2.0',
            'id' => 1,
            'method' => 'initialize',
            'params' => [
                'processId' => getmypid(),
                'rootUri' => 'file://' . realpath($rootPath),
                'capabilities' => [
                    'textDocument' => [
                        'synchronization' => [
                            'didOpen' => TRUE,
                            'didChange' => TRUE,
                            'didClose' => TRUE,
                        ],
                    ],
                ],
            ],
        ];
    
        $jsonRequest = json_encode($initRequest);
        return sendLspMessage($pipes, $jsonRequest);
    }
    
    function openDocument($pipes, $filePath)
    {
        $lspRequest = [
            'jsonrpc' => '2.0',
            'method' => 'textDocument/didOpen',
            'params' => [
                'textDocument' => [
                    'uri' => 'file://' . realpath($filePath),
                    'languageId' => 'php',
                    'version' => 1,
                    'text' => file_get_contents($filePath),
                ],
            ],
        ];
    
        $jsonRequest = json_encode($lspRequest);
        return sendLspMessage($pipes, $jsonRequest);
    }
    
    function shutdownLsp($pipes)
    {
        $shutdownRequest = [
            'jsonrpc' => '2.0',
            'id' => 2,
            'method' => 'shutdown',
            'params' => NULL,
        ];
    
        $jsonRequest = json_encode($shutdownRequest);
        return sendLspMessage($pipes, $jsonRequest);
    }
    
    function exitLsp($pipes)
    {
        $exitNotification = [
            'jsonrpc' => '2.0',
            'method' => 'exit',
            'params' => NULL,
        ];
    
        $jsonRequest = json_encode($exitNotification);
        return sendLspMessage($pipes, $jsonRequest);
    }
    
    // ----------------------------------------------------
    
    // Launch phpactor language server using proc_open
    $descriptorspec = [
        0 => ['pipe', 'r'], // stdin
        1 => ['pipe', 'w'], // stdout
        2 => ['pipe', 'w'],  // stderr
    ];
    
    
    $lspCmd = 'phpactor language-server';
    echo "!!! Starting phpactor language server by command: {$lspCmd}\n";
    $process = proc_open($lspCmd, $descriptorspec, $pipes);
    if (!is_resource($process)) {
        echo "Failed to start phpactor language server, please change the command or check your installation.\n";
        exit(1);
    }
    
    // Initialize LSP
    initializeLsp($pipes, __DIR__);
    echo "!!! Initialize completed\n";
    // Send initialized notification
    $initializedNotification = json_encode(
        [
            'jsonrpc' => '2.0',
            'method' => 'initialized',
            'params' => [],
        ]
    );
    sendLspMessage($pipes, $initializedNotification);
    echo "!!! Initialized notification sent\n";
    
    // Open document
    openDocument($pipes, __DIR__ . '/code.php');
    echo "!!! Document opened\n";
    
    // Wait for a while to ensure the server is ready and processing the document
    sleep(10);
    
    // Shutdown LSP server gracefully
    shutdownLsp($pipes);
    echo "!!! Shutdown request sent\n";
    
    // Send exit notification
    exitLsp($pipes);
    echo "!!! Exit notification sent\n";
    
    // Close pipes and process
    fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($process);
  3. Run php lsp-test.php to simulate an LSP client and trigger the issue.

    Demo
    phpactor.mp4

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions