ExecuteScriptCLI never returns, just exits
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
That's weird because we have a test for that: https://github.com/dunglas/frankenphp/blob/a6fc22505cc5a42a0ab5f8beaec962760b1fea7e/frankenphp_test.go#L598
I can confirm this bug. I'll have to dig into what is terminating the process, but it does, in fact, never return.
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).
This behaves no different when compiled or run using go run. I already tested that.
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")
}```
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.