Skip to content
Open
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.AddPHPInfoEntry("dunglas/caddy-cbrotli", dep.Version)
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
2 changes: 2 additions & 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 All @@ -22,6 +23,7 @@ func ExecuteScriptCLI(script string, args []string) int {
func ExecutePHPCode(phpCode string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()
initPHPInfoEntries()

cCode := C.CString(phpCode)
defer C.free(unsafe.Pointer(cCode))
Expand Down
17 changes: 17 additions & 0 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ func TestExecuteCLICode(t *testing.T) {
assert.Equal(t, stdoutStderrStr, `Hello World`)
}

// The CLI must print phpinfo() as plain text, like the CLI SAPI does.
func TestExecuteCLICodePHPInfoAsText(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", "-r", "phpinfo();")
stdoutStderr, err := cmd.CombinedOutput()
assert.NoError(t, err)

stdoutStderrStr := string(stdoutStderr)

assert.Contains(t, stdoutStderrStr, "PHP Version => ")
assert.NotContains(t, stdoutStderrStr, "<!DOCTYPE")
assert.NotContains(t, stdoutStderrStr, "<table>")
}

// Regression test for https://github.com/php/frankenphp/issues/1902. A
// long-running CLI script that installs pcntl_signal handlers must
// receive its own signals reliably
Expand Down
46 changes: 45 additions & 1 deletion frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <errno.h>
#include <ext/spl/spl_exceptions.h>
#include <ext/standard/head.h>
#include <ext/standard/info.h>
#ifdef HAVE_PHP_SESSION
#include <ext/session/php_session.h>
#endif
Expand Down Expand Up @@ -107,6 +108,9 @@ frankenphp_config frankenphp_get_config() {
};
}

const char **frankenphp_phpinfo_entries = NULL;
const char **frankenphp_go_modules = NULL;

bool should_filter_var = 0;
bool original_user_abort_setting = 0;
frankenphp_interned_strings_t frankenphp_strings = {0};
Expand Down Expand Up @@ -1101,6 +1105,45 @@ PHP_MINIT_FUNCTION(frankenphp) {
return SUCCESS;
}

static void frankenphp_print_info_rows(const char **entries) {
for (int i = 0; entries[i] != NULL; i += 2) {
php_info_print_table_row(2, entries[i], entries[i + 1]);
}
}

PHP_MINFO_FUNCTION(frankenphp) {
php_info_print_table_start();
php_info_print_table_row(2, "frankenphp", TOSTRING(FRANKENPHP_VERSION));
if (frankenphp_phpinfo_entries) {
frankenphp_print_info_rows(frankenphp_phpinfo_entries);
}
php_info_print_table_end();

if (frankenphp_go_modules == NULL) {
return;
}

/* The list of Go modules is long, collapse it by default when rendering
* HTML */
if (sapi_module.phpinfo_as_text) {
php_info_print_table_start();
php_info_print_table_header(1, "Go modules");
php_info_print_table_end();
} else {
php_printf("<details><summary style=\"cursor: pointer\">Go "
"modules</summary>\n");
}

php_info_print_table_start();
php_info_print_table_header(2, "Module", "Version");
frankenphp_print_info_rows(frankenphp_go_modules);
php_info_print_table_end();

if (!sapi_module.phpinfo_as_text) {
php_printf("</details>\n");
}
}

static zend_module_entry frankenphp_module = {
STANDARD_MODULE_HEADER,
"frankenphp",
Expand All @@ -1109,7 +1152,7 @@ static zend_module_entry frankenphp_module = {
NULL, /* shutdown */
NULL, /* request initialization */
NULL, /* request shutdown */
NULL, /* information */
PHP_MINFO(frankenphp), /* information */
TOSTRING(FRANKENPHP_VERSION),
STANDARD_MODULE_PROPERTIES};

Expand Down Expand Up @@ -1857,6 +1900,7 @@ static void *execute_script_cli(void *arg) {
php_embed_module.name = "cli";
php_embed_module.pretty_name = "PHP CLI embedded in FrankenPHP";
php_embed_module.register_server_variables = sapi_cli_register_variables;
php_embed_module.phpinfo_as_text = 1;

php_embed_init(cli_argc, cli_argv);

Expand Down
107 changes: 106 additions & 1 deletion frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ import (
"os"
"os/signal"
"runtime"
"runtime/debug"
"sort"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"unsafe"
// debug on Linux
//_ "github.com/ianlancetaylor/cgosymbolizer"
// _ "github.com/ianlancetaylor/cgosymbolizer"
)

type contextKeyStruct struct{}
Expand Down Expand Up @@ -156,6 +158,108 @@ func Config() PHPConfig {
}
}

type phpinfoEntry struct {
key, value string
}

var (
phpinfoEntries []phpinfoEntry
goModuleEntries []phpinfoEntry
cPhpinfoArr []*C.char
cGoModulesArr []*C.char
)

// Report the Go toolchain and every Go module linked into the binary. Caddy
// modules, FrankenPHP extensions written in Go and even the standard library
// itself. The list is verbose, so it's displayed in a collapsed section.
func init() {
buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return
}

AddPHPInfoEntry("go", buildInfo.GoVersion)

goModuleEntries = make([]phpinfoEntry, 0, len(buildInfo.Deps))
for _, dep := range buildInfo.Deps {
goModuleEntries = append(goModuleEntries, phpinfoEntry{dep.Path, goModuleVersion(dep)})
}
}

// goModuleVersion returns the version of the given module, taking "replace"
// directives into account.
func goModuleVersion(module *debug.Module) string {
if module.Replace == nil {
return module.Version
}

if module.Replace.Version == "" {
// Replaced by a local directory
return module.Replace.Path
}

return module.Replace.Path + " " + module.Replace.Version
}

// AddPHPInfoEntry adds an entry to the frankenphp section of phpinfo().
func AddPHPInfoEntry(key, value string) {
phpinfoEntries = append(phpinfoEntries, phpinfoEntry{key, value})
}

func initPHPInfoEntries() {
freeCEntries(cPhpinfoArr)
freeCEntries(cGoModulesArr)

cPhpinfoArr = newCEntries(phpinfoEntries)
cGoModulesArr = newCEntries(goModuleEntries)

C.frankenphp_phpinfo_entries = firstCEntry(cPhpinfoArr)
C.frankenphp_go_modules = firstCEntry(cGoModulesArr)
}

// newCEntries converts entries to a null-terminated C array of key, value, key,
// value, ... sorted by key. The returned slice is backed by memory allocated by
// C, free it with freeCEntries().
func newCEntries(entries []phpinfoEntry) []*C.char {
if len(entries) == 0 {
return nil
}

sort.Slice(entries, func(i, j int) bool {
return entries[i].key < entries[j].key
})

n := 2*len(entries) + 1
arr := (*[1 << 28]*C.char)(C.malloc(C.size_t(n) * C.size_t(unsafe.Sizeof(uintptr(0)))))[:n:n]
for i, e := range entries {
arr[2*i] = C.CString(e.key)
arr[2*i+1] = C.CString(e.value)
}
arr[n-1] = nil

return arr
}

func freeCEntries(arr []*C.char) {
for _, cstr := range arr {
if cstr != nil {
C.free(unsafe.Pointer(cstr))
}
}

if arr != nil {
C.free(unsafe.Pointer(&arr[0]))
}
}

func firstCEntry(arr []*C.char) **C.char {
if arr == nil {
return nil
}

return &arr[0]
}

func calculateMaxThreads(opt *opt) (numWorkers int, _ error) {
maxProcs := runtime.GOMAXPROCS(0) * 2
maxThreadsFromWorkers := 0
Expand Down Expand Up @@ -250,6 +354,7 @@ func Init(options ...Option) error {
signal.Ignore(syscall.SIGPIPE)

registerExtensions()
initPHPInfoEntries()

opt := &opt{}
for _, o := range options {
Expand Down
8 changes: 8 additions & 0 deletions frankenphp.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ typedef struct {
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)

/* phpinfo entries from Go - null-terminated array of key, value, key, value,
* ... */
extern const char **frankenphp_phpinfo_entries;

/* Go modules linked into the binary, same layout, displayed in a section
* collapsed by default */
extern const char **frankenphp_go_modules;

typedef struct go_string {
size_t len;
char *data;
Expand Down
1 change: 1 addition & 0 deletions frankenphp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ func testPhpInfo(t *testing.T, opts *testOptions) {

assert.Contains(t, body, "frankenphp")
assert.Contains(t, body, fmt.Sprintf("i=%d", i))
assert.Contains(t, body, runtime.Version())
}, opts)
}

Expand Down
12 changes: 12 additions & 0 deletions mercure.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,23 @@ package frankenphp
import "C"
import (
"log/slog"
"runtime/debug"
"unsafe"

"github.com/dunglas/mercure"
)

func init() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many other modules can influence what FrankenPHP does. I wonder if we should just list all installed modules, with their versions. It's verbose, but it covers all use cases and is practical to debug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've thought about it, but I can't really see a use-case for it. It would just clutter away the information we can really influence.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FrankenPHP extensions, Prometheus-related modules. Actually, even the exact go version matters (HTTP stdlib behavior sometimes change). Maybe could we hide this by default in a <details> HTML element?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

if buildInfo, ok := debug.ReadBuildInfo(); ok {
for _, dep := range buildInfo.Deps {
if dep.Path == "github.com/dunglas/mercure" {
AddPHPInfoEntry("dunglas/mercure", dep.Version)
break
}
}
}
}

type mercureContext struct {
mercureHub *mercure.Hub
}
Expand Down
13 changes: 13 additions & 0 deletions watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,25 @@
package frankenphp

import (
"runtime/debug"
"sync/atomic"

"github.com/dunglas/frankenphp/internal/watcher"
watcherGo "github.com/e-dant/watcher/watcher-go"
)

func init() {
// watcher doesn't expose the version, so get it from go.mod
if buildInfo, ok := debug.ReadBuildInfo(); ok {
for _, dep := range buildInfo.Deps {
if dep.Path == "github.com/e-dant/watcher" {
AddPHPInfoEntry("e-dant/watcher", dep.Version)
break
}
}
}
}

type hotReloadOpt struct {
hotReload []*watcher.PatternGroup
}
Expand Down
Loading