frankenphp icon indicating copy to clipboard operation
frankenphp copied to clipboard

ExecuteScriptCLI never returns, just exits

Open leosunmo opened this issue 1 year ago • 6 comments

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

leosunmo avatar Feb 22 '24 15:02 leosunmo

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

dunglas avatar Feb 26 '24 22:02 dunglas

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

withinboredom avatar Feb 29 '24 00:02 withinboredom

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).

dunglas avatar Feb 29 '24 00:02 dunglas

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

leosunmo avatar Mar 04 '24 14:03 leosunmo

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")
}```

leosunmo avatar Mar 04 '24 15:03 leosunmo

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.

withinboredom avatar Mar 04 '24 15:03 withinboredom