Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
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
17 changes: 17 additions & 0 deletions caddy/br.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,21 @@

package caddy

import (
"runtime/debug"

"github.com/dunglas/frankenphp"
)

var brotli = true

func init() {
if buildInfo, ok := debug.ReadBuildInfo(); ok {
for _, dep := range buildInfo.Deps {
if dep.Path == "github.com/dunglas/caddy-cbrotli" {
frankenphp.AddPHPInfoModule("dunglas/caddy-cbrotli", dep)
break
}
}
}
}
9 changes: 9 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,14 @@ func init() {
caddy.RegisterModule(&FrankenPHPModule{})
caddy.RegisterModule(&FrankenPHPAdmin{})

// Report Caddy version in phpinfo()
simpleVersion, fullVersion := caddy.Version()
if fullVersion != "" {
frankenphp.AddPHPInfoEntry("caddy", fullVersion)
} else if simpleVersion != "" {
frankenphp.AddPHPInfoEntry("caddy", simpleVersion)
}

httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)

httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
Expand Down
1 change: 1 addition & 0 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "unsafe"
func ExecuteScriptCLI(script string, args []string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
initPHPInfoEntries()

cScript := C.CString(script)
defer C.free(unsafe.Pointer(cScript))
Expand Down
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