Windows TCP server bug - Warning: Ev::run(): Libev error(10038): Unknown error in test_bug.php on line 26

Issue #36 on hold
Former user created an issue

The following TCP server code (test_bug.php) does not work using the PECL libev extension for Windows (v1.0.8):

<?php
    $host = "127.0.0.1";
    $port = 1234;

    $fp = stream_socket_server("tcp://" . $host . ":" . $port, $errornum, $errorstr);
    if ($fp === false)
    {
        var_dump($errorstr);

        exit();
    }

    // Enable non-blocking mode.
    stream_set_blocking($fp, 0);

    function HandleStuff($watcher, $revents)
    {
var_dump($watcher);
var_dump($revents);
    }

var_dump($fp);
    $w = new EvIo($fp, Ev::READ, "HandleStuff", "blah");
var_dump($w);

    Ev::run(Ev::RUN_ONCE);
var_dump($w);

    Ev::run(Ev::RUN_ONCE);
var_dump($w);
?>

Outputs:

>php test_bug.php
resource(5) of type (stream)
object(EvIo)#1 (6) {
  ["fd"]=>
  resource(6) of type (stream)
  ["events"]=>
  int(1)
  ["is_active"]=>
  bool(true)
  ["data"]=>
  string(4) "blah"
  ["is_pending"]=>
  bool(false)
  ["priority"]=>
  int(0)
}
PHP Warning:  Ev::run(): Libev error(10038): Unknown error in test_bug.php on line 26

Warning: Ev::run(): Libev error(10038): Unknown error in test_bug.php on line 26
object(EvIo)#1 (6) {
  ["fd"]=>
  resource(7) of type (stream)
  ["events"]=>
  int(1)
  ["is_active"]=>
  bool(false)
  ["data"]=>
  string(4) "blah"
  ["is_pending"]=>
  bool(false)
  ["priority"]=>
  int(0)
}
object(EvIo)#1 (6) {
  ["fd"]=>
  resource(8) of type (stream)
  ["events"]=>
  int(1)
  ["is_active"]=>
  bool(false)
  ["data"]=>
  string(4) "blah"
  ["is_pending"]=>
  bool(false)
  ["priority"]=>
  int(0)
}

The example output above shows a changing stream resource per run() call. Venturing a guess, Windows does not generally allow socket handles to be duplicated. Even if duplication appears to succeed, the underlying Winsock driver provider (i.e. the network interface driver) might not function properly or just return an error on the invalid socket.

On a side note, this extension on Windows has a dependency on the PHP sockets extension. The sockets extension won't load on Windows without copying 'php_sockets.dll' to the directory where php.exe is located OR modifying the system PATH to add the 'ext' directory (IMO, a very bad idea) due to how the Windows loader functions. It would be great if you could eliminate the dependency on the slightly outdated PHP sockets extension because loading the sockets extension twice from two different directories (once for this extension + loading the sockets extension separately) is theoretically possible and will probably break something badly if that happens.

Comments (2)

  1. Ruslan Osmanov repo owner

    The example output above shows a changing stream resource per run() call. Venturing a guess, Windows does not generally allow socket handles to be duplicated. Even if duplication appears to succeed, the underlying Winsock driver provider (i.e. the network interface driver) might not function properly or just return an error on the invalid socket.

    In fact, every time the `fd` property is read a new resource is allocated. This is done for the sole purpose of providing convenient interface to the user. Underlying numeric file descriptor is wrapped in a read-only PHP stream resource.

    So when `var_dump()` is called with an `EvIo` argument, the underlying file descriptor is converted to a PHP stream resource by the `fd` property read handler (see `ev_io_prop_fd_read` function in `php7/pe.c`.) No network operations are performed, only some extra memory allocation for the wrapper structure. Well, there could be more to it, but, definitely, no "socket handle"/file descriptor duplication is performed.

    But I don't know what was the cause of the error number 10038 (I guess, `WSAENOTSOCK`, "Socket operation on nonsocket".) There is only one place where the warning could come from, -- the internal implementation of the watcher callback (`php_ev_watcher_callback` function in `php7/watcher.c`). When `revents` contains `EV_ERROR`, the warning is written to stderr, and the watcher is stopped. Maybe Libev called `select()` on the file descriptor and the `errno` was the reaction. I'm not particularly good at Windows questions, and even not a Libev expert. Honestly, I don't feel like debugging on Windows right now, but I'd recommend rewriting the code as follows:

    <?php
    $host = '127.0.0.1';
    $port = 1234;
    
    $fp = stream_socket_server("tcp://{$host}:$port", $errornum, $errorstr);
    if ($fp === false) {
        fprintf(STDERR, "stream_socket_server failed: \n", $errorstr);
        exit(1);
    }
    stream_set_timeout($fp, 5);
    stream_set_blocking($fp, false);
    
    $w = new EvIo(
        $fp,
        Ev::READ,
        function (EvWatcher $watcher, int $revents) use ($fp)
        {
            if ($revents & Ev::ERROR) {
                fprintf(STDERR, "Error flag received in revents. Stopping watcher.\n");
                $watcher->stop();
                return;
            }
            $con = stream_socket_accept($fp);
            if (!$con) {
                $watcher->stop();
                fprintf(STDERR, "stream_socket_accept() failed\n");
                return;
            }
    
            $dateTime = new DateTimeImmutable();
    
            fwrite($con, "HTTP/1.1 200 OK\r\n");
            fwrite($con, "Date: " . $dateTime->format('D, d M Y H:i:s \G\M\T') . "\r\n");
            fwrite($con, "Content-Length: 0\r\n");
            fwrite($con, "Connection: close\r\n\r\n");
            fclose($con);
        }
    );
    
    printf("Starting TCP server at http://%s:%d\n", $host, $port);
    Ev::run();
    echo "Server stopped\n";
    

    It is far from being a good server, but it accepts and serves connections, at least.

  2. Log in to comment