Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
922de2d
feat: register phpinfo info entries under frankenphp extension
henderkes Jul 29, 2026
ce102e5
report e-dant/watcher, dunglas/caddy-cbrotli, libbrotli and dunglas/m…
henderkes Jul 29, 2026
032a742
keep go array, only convert to c array in init function
henderkes Jul 29, 2026
98aadc4
clang-format
henderkes Jul 29, 2026
87a0874
why is this missing in CI? @dunglas
henderkes Jul 29, 2026
f06d146
rename method
henderkes Aug 4, 2026
fdbf473
test for phpinfo as plaintext
henderkes Aug 11, 2026
ef483ee
suggestion by @dunglas - also include all go modules and go version
henderkes Aug 11, 2026
b95c966
don't capitalise Go version in PHPInfo entry, nothing else is capital…
henderkes Aug 11, 2026
bec8ae3
Merge remote-tracking branch 'origin/main' into feat/register_phpinfo…
henderkes Aug 21, 2026
df91cab
amend cli test
henderkes Aug 21, 2026
1bffc57
hook frankenphp_module into cli execution too
henderkes Aug 21, 2026
c3950da
make sure tests set display_errors=1 when they rely on it
henderkes Aug 21, 2026
72c3a25
Merge branch 'main' into feat/register_phpinfo_entries
henderkes Sep 3, 2026
0296a89
fix cli metadata without server runtime hooks
henderkes Sep 5, 2026
8019e98
restore extension registration hooks after cli execution
henderkes Sep 5, 2026
083ba46
respect module replacements in component version entries
henderkes Sep 5, 2026
f8eabf6
include main module in phpinfo module inventory
henderkes Sep 5, 2026
5ea3d90
test cli behavior across startup and shutdown
henderkes Sep 5, 2026
c604428
test phpinfo module metadata and escaped rendering
henderkes Sep 5, 2026
acaad7e
reword comment to shut copilot up
henderkes Sep 5, 2026
0df6262
Merge remote-tracking branch 'origin/main' into feat/register_phpinfo…
henderkes Sep 8, 2026
78136e7
fix: zero phpinfo arrays before Go pointer writes
henderkes Sep 8, 2026
2a4d1d3
fix windows stream closing issue
henderkes Sep 8, 2026
44cfc28
fix linux amd64 race tests
henderkes Sep 8, 2026
1b7a973
satisfy go fmt
henderkes Sep 8, 2026
6d5a4fd
suggestions
henderkes Sep 8, 2026
c64dd5b
safety fix for userland closing non-duplicate streams
henderkes Sep 8, 2026
873ca4a
pin instead of manual c memory management
henderkes Sep 9, 2026
d055b38
switch to caddy's simpleVersion
henderkes Sep 9, 2026
90a1fb0
move phpinfo helpers into phpinfo.go
henderkes Sep 9, 2026
f991a3a
centralize phpinfo module version lookup
henderkes Sep 9, 2026
2b6244b
lazily collect phpinfo metadata through minfo callback
henderkes Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ jobs:
include:
- race: ""
- platform: linux/amd64
race: "-race" # The Go race detector is only supported on amd64
# The Go race detector is only enabled on amd64. PHP's inherited PIE
# flags require external linking for runtime/cgo in pure-Go test packages.
race: "-race -ldflags=-linkmode=external"
exclude:
# arm/v6 is only available for Alpine: https://github.com/docker-library/golang/issues/502
- variant: php-${{ needs.prepare.outputs.php82_version }}-trixie
Expand Down
6 changes: 6 additions & 0 deletions caddy/br.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@

package caddy

import "github.com/dunglas/frankenphp"

var brotli = true

func init() {
frankenphp.AddPHPInfoModule("dunglas/caddy-cbrotli", "github.com/dunglas/caddy-cbrotli")
}
5 changes: 5 additions & 0 deletions caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/dunglas/frankenphp"
)

const (
Expand All @@ -26,6 +27,10 @@ func init() {
caddy.RegisterModule(&FrankenPHPModule{})
caddy.RegisterModule(&FrankenPHPAdmin{})

// Report Caddy version in phpinfo()
simpleVersion, _ := caddy.Version()
frankenphp.AddPHPInfoEntry("caddy", simpleVersion)

httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)

httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
Expand Down
16 changes: 12 additions & 4 deletions caddy/caddy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -1219,12 +1220,19 @@ func testSingleIniConfiguration(tester *caddytest.Tester, key string, value stri
}

func TestOsEnv(t *testing.T) {
// This is not a reload test: avoid the previous config's listener, which
// Caddy may still be shutting down after FrankenPHP unregisters its server.
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port)
require.NoError(t, listener.Close())

tester := caddytest.NewTester(t)
initServer(t, tester, `
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port `+testPort+`
http_port `+port+`

frankenphp {
num_threads 2
Expand All @@ -1233,7 +1241,7 @@ func TestOsEnv(t *testing.T) {
}
}

localhost:`+testPort+` {
localhost:`+port+` {
route {
root ../testdata
php
Expand All @@ -1242,7 +1250,7 @@ func TestOsEnv(t *testing.T) {
`, "caddyfile")

tester.AssertGetResponse(
"http://localhost:"+testPort+"/env/env.php?keys[]=ENV1&keys[]=ENV2",
"http://localhost:"+port+"/env/env.php?keys[]=ENV1&keys[]=ENV2",
http.StatusOK,
"ENV1=value1,ENV2=value2",
)
Expand Down
41 changes: 41 additions & 0 deletions caddy/phpinfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package caddy_test

import (
"html"
"io"
"net/http"
"regexp"
"testing"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/stretchr/testify/require"
)

func TestPHPInfoCaddyVersion(t *testing.T) {
tester := caddytest.NewTester(t)
initServer(t, tester, `
{
skip_install_trust
admin localhost:2999
}

http://localhost:`+testPort+` {
php_server {
root ../testdata
}
}
`, "caddyfile")

resp, err := tester.Client.Get("http://localhost:" + testPort + "/phpinfo.php")
require.NoError(t, err)
defer func() { require.NoError(t, resp.Body.Close()) }()
require.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

row := regexp.MustCompile(`<tr><td class="e">caddy </td><td class="v">(.*?) </td></tr>`).FindSubmatch(body)
simpleVersion, _ := caddy.Version()
require.Len(t, row, 2, "phpinfo must include the Caddy version row")
require.Equal(t, html.EscapeString(simpleVersion), string(row[1]))
}
118 changes: 118 additions & 0 deletions cli_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//go:build linux

package frankenphp_test

import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)

func TestExecuteScriptCLIDetachedChild(t *testing.T) {
const helperEnv = "FRANKENPHP_TEST_DETACHED_CHILD"
dir := os.Getenv(helperEnv)
if dir == "" {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}
self, err := os.Executable()
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, self, "-test.run=^TestExecuteScriptCLIDetachedChild$", "-test.v")
cmd.Env = append(os.Environ(), helperEnv+"="+t.TempDir())
cmd.WaitDelay = time.Second
output, err := cmd.CombinedOutput()
var exitError *exec.ExitError
if errors.As(err, &exitError) && exitError.ExitCode() == 77 {
t.Skipf("pcntl/posix unavailable: %s", output)
}
require.NoError(t, err, "%s", output)
return
}

// PDEATHSIG and subreapers are Linux-specific. Isolate adoption from other tests.
require.NoError(t, unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0))
input, release, err := os.Pipe()
require.NoError(t, err)
defer func() { _ = input.Close() }()
pid := 0
t.Cleanup(func() {
// EOF also releases a child whose PID was not reported before a parent failure.
_ = release.Close()
if pid > 0 {
_ = unix.Kill(pid, unix.SIGKILL)
}
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
var status unix.WaitStatus
_, err := unix.Wait4(-1, &status, unix.WNOHANG, nil)
if errors.Is(err, unix.ECHILD) {
return
}
if err != nil && !errors.Is(err, unix.EINTR) {
t.Errorf("reaping detached child: %v", err)
return
}
time.Sleep(10 * time.Millisecond)
}
t.Error("detached child cleanup timed out")
})

ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
ready := filepath.Join(dir, "ready")
_, err = os.Lstat(ready)
require.ErrorIs(t, err, os.ErrNotExist, "readiness path must not already exist")
cmd := exec.CommandContext(ctx, "internal/testcli/testcli", "testdata/command-detached.php")
// PHP's emulated and native CLIs expose different script argv layouts.
cmd.Env = append(os.Environ(), "FRANKENPHP_TEST_DETACHED_READY="+ready)
cmd.Stdin = input
cmd.WaitDelay = time.Second
output, err := cmd.CombinedOutput()
var exitError *exec.ExitError
if errors.As(err, &exitError) && exitError.ExitCode() == 2 {
// The fixture checks extensions before forking, so nothing needs reaping.
t.Logf("%s", output)
os.Exit(77)
}
for _, line := range strings.Split(string(output), "\n") {
if strings.HasPrefix(line, "CHILD=") {
pid, _ = strconv.Atoi(strings.TrimPrefix(line, "CHILD="))
}
}
require.NoError(t, err, "CLI parent: %s", output)
require.Greater(t, pid, 0, "no child PID: %s", output)

// CombinedOutput has waited for the actual CLI parent exit, not just readiness.
// The CLI joins its PHP thread before exiting, so this also covers Linux's
// PDEATHSIG on the forking thread's exit rather than the whole process's exit.
_, writeErr := release.WriteString("survived\n")
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
var status unix.WaitStatus
got, err := unix.Wait4(pid, &status, unix.WNOHANG, nil)
if errors.Is(err, unix.EINTR) {
continue
}
require.NoError(t, err)
if got == pid {
pid = 0 // Reaped: cleanup must not signal a potentially reused PID.
require.True(t, status.Exited(), "detached child terminated by signal %d (%s)", status.Signal(), status.Signal())
require.Equal(t, 0, status.ExitStatus(), "detached child failed")
require.NoError(t, writeErr)
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("detached child did not finish after CLI parent exited")
}
Loading
Loading