Skip to content

Getting Phpactor running on Windows – it almost works! #2551

@MatmaRex

Description

@MatmaRex

After reading #1858, I decided to see what it would take to get Phpactor running on Windows, without Docker, WSL etc. as discussed in that issue. I'm happy to report that I got it working, with a few small but really ugly hacks to Phpactor and its dependencies. I'm filing this to document the underlying issues, and ask for some suggestions as for how to resolve them properly.

This is the full list of changes I have locally. I haven't tested everything, but at the very least I get completions and hover tips when using Sublime Text on Windows 10 on the https://github.com/wikimedia/mediawiki repository (tested with PHP 8.1 and 8.3).

diff -u phpactor_orig/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php phpactor/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php
--- phpactor_orig/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php	2024-02-21 03:28:24.667006800 +0100
+++ phpactor/lib/Extension/LanguageServerCompletion/Handler/CompletionHandler.php	2024-02-21 05:17:32.214568200 +0100
@@ -119,7 +119,7 @@
                     $isIncomplete = true;
                     break;
                 }
-                yield new Delayed(0);
+                // yield new Delayed(0);
             }
 
 
diff -u phpactor_orig/lib/Extension/LanguageServerIndexer/Handler/IndexerHandler.php phpactor/lib/Extension/LanguageServerIndexer/Handler/IndexerHandler.php
--- phpactor_orig/lib/Extension/LanguageServerIndexer/Handler/IndexerHandler.php  2024-02-21 03:28:24.704035500 +0100
+++ phpactor/lib/Extension/LanguageServerIndexer/Handler/IndexerHandler.php 2024-02-21 05:18:00.712987900 +0100
@@ -99,7 +99,7 @@
                     break;
                 }
 
-                yield new Delayed(1);
+                // yield new Delayed(1);
             }
 
             $process = yield $this->watcher->watch();
@@ -162,7 +162,7 @@
                     $process->stop();
                     return;
                 }
-                yield new Delayed(100);
+                // yield new Delayed(100);
             }
         });
         try {
@@ -185,7 +185,7 @@
                     continue;
                 }
                 $this->logger->debug(sprintf('Indexed file: %s', $file->path()));
-                yield new Delayed(0);
+                // yield new Delayed(0);
             }
         } catch (WatcherDied $watcherDied) {
             $this->clientApi->window()->showMessage()->error(sprintf('File watcher died: %s', $watcherDied->getMessage()));
diff -u phpactor_orig/lib/Extension/LanguageServerReferenceFinder/Handler/ReferencesHandler.php phpactor/lib/Extension/LanguageServerReferenceFinder/Handler/ReferencesHandler.php
--- phpactor_orig/lib/Extension/LanguageServerReferenceFinder/Handler/ReferencesHandler.php 2024-02-21 03:28:24.732353300 +0100
+++ phpactor/lib/Extension/LanguageServerReferenceFinder/Handler/ReferencesHandler.php  2024-02-21 05:18:03.791091100 +0100
@@ -106,7 +106,7 @@
 
                 if ($count % 10) {
                     // give other co-routines a chance
-                    yield new Delayed(0);
+                    // yield new Delayed(0);
                 }
             }
 
diff -u phpactor_orig/lib/FilePathResolver/LoggingPathResolver.php phpactor/lib/FilePathResolver/LoggingPathResolver.php
--- phpactor_orig/lib/FilePathResolver/LoggingPathResolver.php  2024-02-21 03:28:24.845838500 +0100
+++ phpactor/lib/FilePathResolver/LoggingPathResolver.php 2024-02-21 05:32:06.874791500 +0100
@@ -17,6 +17,7 @@
     public function resolve(string $path): string
     {
         $resolvedPath = $this->pathResolver->resolve($path);
+        $resolvedPath = ltrim($resolvedPath, '/\\');
         $this->logger->log(
             $this->level,
             sprintf(
diff -u phpactor_orig/lib/Indexer/Extension/Command/IndexBuildCommand.php phpactor/lib/Indexer/Extension/Command/IndexBuildCommand.php
--- phpactor_orig/lib/Indexer/Extension/Command/IndexBuildCommand.php 2024-02-21 03:28:24.880577000 +0100
+++ phpactor/lib/Indexer/Extension/Command/IndexBuildCommand.php  2024-02-21 05:35:54.829720700 +0100
@@ -106,13 +106,6 @@
         Loop::run(function () use ($output) {
             $process = yield $this->watcher->watch();
 
-            Loop::onSignal(SIGINT, function () use ($output, $process): void {
-                $output->write('Shutting down watchers...');
-                $process->stop();
-                $output->writeln('done');
-                Loop::stop();
-            });
-
             $output->writeln(sprintf('<info>Watching for file changes with </>%s<info>...</>', $this->watcher->describe()));
 
             while (null !== $file = yield $process->wait()) {
diff -u phpactor_orig/lib/Indexer/Extension/Rpc/IndexHandler.php phpactor/lib/Indexer/Extension/Rpc/IndexHandler.php
--- phpactor_orig/lib/Indexer/Extension/Rpc/IndexHandler.php  2024-02-21 03:28:24.883507800 +0100
+++ phpactor/lib/Indexer/Extension/Rpc/IndexHandler.php 2024-02-21 05:18:05.593248200 +0100
@@ -49,7 +49,7 @@
                     assert($file instanceof ModifiedFile);
                     $job = $this->indexer->getJob($file->path());
                     $job->run();
-                    yield new Delayed($arguments[self::PARAM_INTERVAL]);
+                    // yield new Delayed($arguments[self::PARAM_INTERVAL]);
                 }
             });
         }
diff -u phpactor_orig/vendor/amphp/process/lib/Internal/Windows/Runner.php phpactor/vendor/amphp/process/lib/Internal/Windows/Runner.php
--- phpactor_orig/vendor/amphp/process/lib/Internal/Windows/Runner.php  2022-07-07 01:50:12.000000000 +0200
+++ phpactor/vendor/amphp/process/lib/Internal/Windows/Runner.php 2024-02-21 05:39:34.813929900 +0100
@@ -82,6 +82,10 @@
             throw new ProcessException("Can't execute commands that contain null bytes.");
         }
 
+        if ($cwd !== null) {
+          $cwd = ltrim($cwd, '/\\');
+        }
+
         $options['bypass_shell'] = true;
 
         $handle = new Handle;
diff -u phpactor_orig/vendor/amphp/process/lib/Internal/Windows/SocketConnector.php phpactor/vendor/amphp/process/lib/Internal/Windows/SocketConnector.php
--- phpactor_orig/vendor/amphp/process/lib/Internal/Windows/SocketConnector.php 2022-07-07 01:50:12.000000000 +0200
+++ phpactor/vendor/amphp/process/lib/Internal/Windows/SocketConnector.php  2024-02-21 01:44:33.051906400 +0100
@@ -16,7 +16,7 @@
 {
     const SERVER_SOCKET_URI = 'tcp://127.0.0.1:0';
     const SECURITY_TOKEN_SIZE = 16;
-    const CONNECT_TIMEOUT = 1000;
+    const CONNECT_TIMEOUT = 30000;
 
     /** @var resource */
     private $server;
diff -u phpactor_orig/vendor/phpactor/language-server/lib/Core/Server/LanguageServer.php phpactor/vendor/phpactor/language-server/lib/Core/Server/LanguageServer.php
--- phpactor_orig/vendor/phpactor/language-server/lib/Core/Server/LanguageServer.php  2023-09-22 10:53:04.000000000 +0200
+++ phpactor/vendor/phpactor/language-server/lib/Core/Server/LanguageServer.php 2024-02-21 05:28:24.687710200 +0100
@@ -93,11 +93,6 @@
      */
     public function run(): void
     {
-        Loop::onSignal(SIGINT, function (string $watcherId) {
-            Loop::cancel($watcherId);
-            yield $this->shutdown();
-        });
-
         Loop::setErrorHandler(function (Throwable $error): void {
             if ($error instanceof ShutdownServer) {
                 Loop::stop();
diff -u phpactor_orig/vendor/symfony/filesystem/Path.php phpactor/vendor/symfony/filesystem/Path.php
--- phpactor_orig/vendor/symfony/filesystem/Path.php  2024-01-23 14:51:25.000000000 +0100
+++ phpactor/vendor/symfony/filesystem/Path.php 2024-02-21 05:40:56.691359900 +0100
@@ -771,6 +771,10 @@
      */
     private static function split(string $path): array
     {
+        if (str_starts_with($path, '/')) {
+            $path = ltrim($path, '/');
+        }
+
         if ('' === $path) {
             return ['', ''];
         }

There are four kinds of hacks here:

  1. Commented out a few instances of yield new Delayed(…). Otherwise, the event loop would sometimes get stuck, and the Phpactor server would stop processing any requests. This occurred with both Amp\Loop\NativeDriver and with Amp\Loop\EventDriver. I can't figure out why it happens, but it's true. I'm hoping you might have some idea. (Maybe this gets magically resolved in the future by the new AMPHP version that no longer ships its own event loop, as promised at https://amphp.org/upgrade#event-loop – I didn't try to install or even find it.)
    (Commenting them out presumably prevents other coroutines from running in these places, but it didn't seem to break anything for me.)
  2. Added trimming of leading / from file paths in several places. The paths looked like /C:, which is invalid. Some Unix path handling must be leaking somewhere, but I didn't really try to figure out where the bad data was coming from. Apart from this, Windows paths seem to be handled well.
  3. Removed a few instances of Loop::onSignal(SIGINT, …). Signals are not supported on Windows. This is only used to "gracefully shut down" the server, and I'm not sure if that's even necessary, but it might be possible to implement something equivalent using sapi_windows_set_ctrl_handler().
    (And then you could remove the dependencies on ext-pcntl and ext-posix from composer.json – as far as I can tell, the only thing you're directly using from them is the SIGINT constant, everything else is handled by libraries that already support Windows.)
  4. Increased a timeout in amphp/process. I ran into issues that looked like Moderate to high concurrency load causes "Failed to launch process" errors on Windows amphp/process#21. This might be caused by my commenting-out of those yield new Delayed(…) calls?

Lastly, I had the server crash like this a couple of times. I couldn't reproduce it when I tried, and restarting it made it go away.

Fatal error: Uncaught TypeError: stream_select(): supplied resource is not a valid stream resource in C:\dev\phpactor\vendor\amphp\amp\lib\Loop\NativeDriver.php:300
Stack trace:
#0 C:\dev\phpactor\vendor\amphp\amp\lib\Loop\NativeDriver.php(300): stream_select(Array, Array, Array, 0, 0)
#1 C:\dev\phpactor\vendor\amphp\amp\lib\Loop\NativeDriver.php(127): Amp\Loop\NativeDriver->selectStreams(Array, Array, 0)
#2 C:\dev\phpactor\vendor\amphp\amp\lib\Loop\Driver.php(138): Amp\Loop\NativeDriver->dispatch(false)
#3 C:\dev\phpactor\vendor\amphp\amp\lib\Loop\Driver.php(72): Amp\Loop\Driver->tick()
#4 C:\dev\phpactor\vendor\amphp\amp\lib\Loop.php(95): Amp\Loop\Driver->run()
#5 C:\dev\phpactor\vendor\phpactor\language-server\lib\Core\Server\LanguageServer.php(108): Amp\Loop::run(Object(Closure))
#6 C:\dev\phpactor\lib\Extension\LanguageServer\Command\StartCommand.php(50): Phpactor\LanguageServer\Core\Server\LanguageServer->run()
#7 C:\dev\phpactor\vendor\symfony\console\Command\Command.php(298): Phpactor\Extension\LanguageServer\Command\StartCommand->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#8 C:\dev\phpactor\vendor\symfony\console\Application.php(1040): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#9 C:\dev\phpactor\vendor\symfony\console\Application.php(301): Symfony\Component\Console\Application->doRunCommand(Object(Phpactor\Extension\LanguageServer\Command\StartCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#10 C:\dev\phpactor\lib\Application.php(48): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#11 C:\dev\phpactor\vendor\symfony\console\Application.php(171): Phpactor\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#12 C:\dev\phpactor\bin\phpactor(46): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#13 {main}

If you have the time to offer some advice – or at least educated guesses – about what causes these problems, and outline how you'd want them to be fixed, I'd be happy to work on some proper patches, and get rid of my local hacks.

Even if you don't have the time for this, perhaps it helps someone else. :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions