dunglas / frankenphp

🧟 The modern PHP app server
https://frankenphp.dev
MIT License
6.75k stars 227 forks source link

ExecuteScriptCLI never returns, just exits #590

Open leosunmo opened 7 months ago

leosunmo commented 7 months ago

What happened?

I'm trying to use ExecuteScriptCLI in a very simple Go application and it seems to just exit with the PHP application. Here's a minimal example.

My go code:

package main

import (
    "fmt"

    "github.com/dunglas/frankenphp"
)

const (
    phpPath = "script.php"
)

func main() {
    fmt.Println("starting...")
    sc1 := frankenphp.ExecuteScriptCLI(phpPath, []string{"1"})
    fmt.Printf("script finished with status code: %d\n", sc1)
    sc2 := frankenphp.ExecuteScriptCLI(phpPath, []string{"2"})
    fmt.Printf("script finished with status code: %d\n", sc2)
    fmt.Println("finished!")

With my php script looking like this:

<?php
echo "hello world\n";

I get this output to stdout:

go run main.go
starting...
hello world

I suspected that perhaps php was exiting and the Go execution is never continued, but even if I add exit(1); to the end of my php script I get exit 0 from go run.

echo 'exit(1);' >> script.php
go run main.go
starting...
hello world
echo $?
0

So no entirely sure what's going on here, but nothing from the documentation seems to suggest that Go "hands off" to PHP or similar here.

Environment

I am running this inside a docker container (using vscode devcontainers) with a copy-paste of the official FrankenPHP debian container.

FROM golang:1.22

ENV CFLAGS="-ggdb3"
ENV PHPIZE_DEPS \
    autoconf \
    dpkg-dev \
    file \
    g++ \
    gcc \
    libc-dev \
    make \
    pkg-config \
    re2c

# hadolint ignore=DL3009
RUN apt-get update && \
    apt-get -y --no-install-recommends install \
    $PHPIZE_DEPS \
    libargon2-dev \
    libbrotli-dev \
    libcurl4-openssl-dev \
    libonig-dev \
    libreadline-dev \
    libsodium-dev \
    libsqlite3-dev \
    libssl-dev \
    libxml2-dev \
    zlib1g-dev \
    bison \
    libnss3-tools \
    # Dev tools \
    git \
    clang \
    llvm \
    gdb \
    valgrind \
    neovim \
    zsh \
    libtool-bin && \
    echo 'set auto-load safe-path /' > /root/.gdbinit && \
    echo '* soft core unlimited' >> /etc/security/limits.conf \
    && \
    apt-get clean 

WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \
    # --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
    # --with-openssl is NOT required by FrankePHP but is needed for composer to be installed.
    ./buildconf --force && \
    ./configure \
        --with-openssl \
        --enable-embed \
        --enable-zts \
        --disable-zend-signals \
        --enable-zend-max-execution-timers \
        --enable-debug && \
    make -j"$(nproc)" && \
    make install && \
    ldconfig && \
    cp php.ini-development /usr/local/lib/php.ini && \
    echo "zend_extension=opcache.so" >> /usr/local/lib/php.ini && \
    echo "opcache.enable=1" >> /usr/local/lib/php.ini && \
    php --version

# Install composer
RUN curl -sSL https://getcomposer.org/installer | php \
    && chmod +x composer.phar \
    && mv composer.phar /usr/local/bin/composer

The only thing I added was composer.

Build Type

Docker (Debian Bookworm)

Worker Mode

No

Operating System

GNU/Linux

CPU Architecture

x86_64

Relevant log output

No response

dunglas commented 7 months ago

That's weird because we have a test for that: https://github.com/dunglas/frankenphp/blob/a6fc22505cc5a42a0ab5f8beaec962760b1fea7e/frankenphp_test.go#L598

withinboredom commented 7 months ago

I can confirm this bug. I'll have to dig into what is terminating the process, but it does, in fact, never return.

dunglas commented 7 months ago

Regarding the return code being 0, IIRC it's normal when using go run. Compile the program go build and execute it, you should get the proper status code.

I don't remember if it's intended or not that the code never returns. This may be a limitation. Anyway we should document this behavior (or fix it if possible).

leosunmo commented 7 months ago

This behaves no different when compiled or run using go run. I already tested that.

leosunmo commented 7 months ago

The test in question passes when I do this.


func TestExecuteScriptCLI(t *testing.T) {
    if _, err := os.Stat("internal/testcli/testcli"); err != nil {
        t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
    }

    cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
    stdoutStderr, err := cmd.CombinedOutput()
    assert.Error(t, err)

    if exitError, ok := err.(*exec.ExitError); ok {
        assert.Equal(t, 5, exitError.ExitCode()) // Changed to 5. testdata/command.php always exits 3.
    }

    stdoutStderrStr := string(stdoutStderr)

    assert.Contains(t, stdoutStderrStr, `"foo"`)
    assert.Contains(t, stdoutStderrStr, `"bar"`)
    assert.Contains(t, stdoutStderrStr, "From the CLI")
}```
withinboredom commented 7 months ago

I ended up implementing what I wanted by calling the script in question as a fake request and calling ServeHTTP(). :1st_place_medal:

It's not ideal, but it worked for me. I'm still not sure what is going on.