Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 8 additions & 5 deletions caddy/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/dunglas/frankenphp/internal/fastabs"
"io"
"net/http"
"strings"
"sync"
"testing"

Expand Down Expand Up @@ -251,6 +251,7 @@ func TestAddModuleWorkerViaAdminApi(t *testing.T) {
}

// Create a Caddyfile configuration with a module worker
workerName := "dynamiclly added worker"
workerConfig := `
{
skip_install_trust
Expand All @@ -262,7 +263,10 @@ func TestAddModuleWorkerViaAdminApi(t *testing.T) {
route {
root ../testdata
php {
worker ../testdata/worker-with-counter.php 1
worker ../testdata/worker-with-counter.php {
num 1
name "` + workerName + `"
}
}
}
}
Expand All @@ -280,19 +284,18 @@ func TestAddModuleWorkerViaAdminApi(t *testing.T) {
updatedDebugState := getDebugState(t, tester)
updatedWorkerCount := 0
workerFound := false
filename, _ := fastabs.FastAbs("../testdata/worker-with-counter.php")
for _, thread := range updatedDebugState.ThreadDebugStates {
if thread.Name != "" && thread.Name != "ready" {
updatedWorkerCount++
if thread.Name == "Worker PHP Thread - "+filename {
if strings.Contains(thread.Name, workerName) {
workerFound = true
}
}
}

// Assert that the worker was added
assert.Greater(t, updatedWorkerCount, initialWorkerCount, "Worker count should have increased")
assert.True(t, workerFound, fmt.Sprintf("Worker with name %q should be found", "Worker PHP Thread - "+filename))
assert.True(t, workerFound, fmt.Sprintf("Worker with name '%q' should be found", workerName))

// Make a request to the worker to verify it's working
tester.AssertGetResponse("http://localhost:"+testPort+"/worker-with-counter.php", http.StatusOK, "requests:1")
Expand Down
37 changes: 37 additions & 0 deletions debugstate.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package frankenphp

// #include "frankenphp.h"
import "C"

// EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only
type ThreadDebugState struct {
Index int
Expand Down Expand Up @@ -44,3 +47,37 @@ func threadDebugState(thread *phpThread) ThreadDebugState {
WaitingSinceMilliseconds: thread.state.waitTime(),
}
}

// EXPERIMENTAL: Expose the current thread's information to PHP
//
//export go_frankenphp_info
func go_frankenphp_info(threadIndex C.uintptr_t) *C.zval {
currentThread := phpThreads[threadIndex]
_, isWorker := currentThread.handler.(*workerThread)

threadInfos := make([]any, 0, len(phpThreads))
for _, thread := range phpThreads {
if thread.state.is(stateReserved) {
continue
}
threadInfos = append(threadInfos, map[string]any{
"index": thread.threadIndex,
"name": thread.name(),
"state": thread.state.name(),
"is_waiting": thread.state.isInWaitingState(),
"waiting_since_milliseconds": thread.state.waitTime(),
})
}

zval := (*C.zval)(PHPMap(map[string]any{
"frankenphp_version": C.GoString(C.frankenphp_get_version().frankenphp_version),
"current_thread_index": int64(threadIndex),
"is_worker_thread": isWorker,
"threads": threadInfos,
}))

// TODO: how to circumvent pinning?
currentThread.Pin(zval)

return zval
}
17 changes: 14 additions & 3 deletions frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ static const char *MODULES_TO_RELOAD[] = {"filter", "session", NULL};

frankenphp_version frankenphp_get_version() {
return (frankenphp_version){
PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION,
PHP_EXTRA_VERSION, PHP_VERSION, PHP_VERSION_ID,
};
PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION,
PHP_EXTRA_VERSION, PHP_VERSION, PHP_VERSION_ID,
TOSTRING(FRANKENPHP_VERSION)};
}

frankenphp_config frankenphp_get_config() {
Expand Down Expand Up @@ -1209,3 +1209,14 @@ void register_extensions(zend_module_entry *m, int len) {
php_register_internal_extensions_func;
php_register_internal_extensions_func = register_internal_extensions;
}

/* EXPERIMENTAL */
PHP_FUNCTION(frankenphp_info) {
if (zend_parse_parameters_none() == FAILURE) {
RETURN_THROWS();
}

zval *result = go_frankenphp_info(thread_index);

RETURN_ARR(Z_ARR_P(result));
}
1 change: 1 addition & 0 deletions frankenphp.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ typedef struct frankenphp_version {
const char *extra_version;
const char *version;
unsigned long version_id;
const char *frankenphp_version;
} frankenphp_version;
frankenphp_version frankenphp_get_version();

Expand Down
15 changes: 15 additions & 0 deletions frankenphp.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,18 @@ function frankenphp_response_headers(): array|bool {}
*/
function apache_response_headers(): array|bool {}

/**
* @return array{
* "frankenphp_version": string,
* "current_thread_index": int,
* "is_worker_thread": bool,
* "threads": array {
* "index": int,
* "name": string,
* "state": string,
* "is_waiting": bool,
* "waiting_since_milliseconds": int,
* },
* }
*/
function frankenphp_info(): array {}

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.

Do we have the array shape already so it can be part of the stub?

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.

Not yet, but good point! It might also be nice to add the FrankenPHP version, but I'm currently not sure if it's even available from inside of the process.

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.

Looking at the PHPArray function, it might also make sense to instead allow direct convertion of either a slice or map to an array (unless that's something you already considered).

func SliceToPHPArray(slice []interface{})
func MapToPHPArray(map map[string]interface{})

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.

This is a good idea on the paper. I think that we discussed this with @dunglas and the final word is that we'd like to avoid ending with a clunky API with to many functions. I'm not exactly sure if this was the conclusion of this discussion or another, but this is something that Kevin may confirm I guess

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.

This would save a few functions though, since on the go side you just manipulate the slice or map.

Instead of:

type PHPKeyType int
type PHPKey struct
type Array struct
func (arr *Array) SetInt
func (arr *Array) SetString
func (arr *Array) Append
func (arr *Array) getNextIntKey
func (arr *Array) Len
func (arr *Array) At

you'd just need something like:

type Array = map[string]interface{}
func PHPArray(a Array) unsafe.Pointer
func GoArray(arr unsafe.Pointer) Array

type PackedArray = []interface{}
func PHPPackedArray(a PackedArray) unsafe.Pointer
func GoPackedArray(arr unsafe.Pointer) PackedArray

The unfortunate thing here is that the PHP Array is a union of the 'packed' and 'unpacked' hash table. Like this you leave it up to the user to determine what they need.

Would also solve the problem that currently duplicate keys are possible in the Array struct.

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.

There's also the problem that PHP hashmaps are ordered. But maybe this is no big deal as we cannot pass arguments by reference in our case. 馃

@AlliBalliBaba AlliBalliBaba Jul 25, 2025

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.

Yeah it potentially looses its order in the unpacked case, but would be cleaner and more performant otherwise. Also I could just do something like this 馃槃

return PHPArray(&Array{
	"frankenphp_version" : C.GoString(C.frankenphp_get_version().server_version),
	"thread_name":        thread.name(),
	"thread_index":       int(threadIndex),
	"is_worker_thread":   thread.handler.(*workerThread) != nil,
	"num_threads":        mainThread.numThreads,
	"max_threads":        mainThread.maxThreads,
})

@AlliBalliBaba AlliBalliBaba Jul 25, 2025

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.

Otherwise, if keeping the order is completely necessary, the correct data type on the go side would be an ordered map for the 'unpacked' array (sadly no built in ordered map by go)

The issues with the current implementation are mainly that duplicate keys are allowed on the go side and keys can be a mix of ints and strings. But exactly mirroring PHP Arrays probably cannot be done anyways without it getting too messy. So either mapping an array to a slice (if you need order) or a map (if you need association) seems like it makes most sense IMO.

Not sure though what others think

6 changes: 6 additions & 0 deletions frankenphp_arginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ ZEND_END_ARG_INFO()

#define arginfo_apache_response_headers arginfo_frankenphp_response_headers

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_info, 0, 0, IS_ARRAY,
0)
ZEND_END_ARG_INFO()

ZEND_FUNCTION(frankenphp_handle_request);
ZEND_FUNCTION(headers_send);
ZEND_FUNCTION(frankenphp_finish_request);
ZEND_FUNCTION(frankenphp_request_headers);
ZEND_FUNCTION(frankenphp_response_headers);
ZEND_FUNCTION(frankenphp_info);

// clang-format off
static const zend_function_entry ext_functions[] = {
Expand All @@ -47,6 +52,7 @@ static const zend_function_entry ext_functions[] = {
ZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders)
ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)
ZEND_FE(frankenphp_info, arginfo_frankenphp_info)
ZEND_FE_END
};
// clang-format on
14 changes: 14 additions & 0 deletions frankenphp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,20 @@ func TestFileStreamInWorkerMode(t *testing.T) {
}, &testOptions{workerScript: "file-stream.php", nbParallelRequests: 1, nbWorkers: 1})
}

func TestFrankenPHPInfo_module(t *testing.T) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
body, _ := testGet("http://example.com/frankenphp_info.php", handler, t)
assert.Contains(t, body, "[is_worker_thread] => 1")
}, &testOptions{workerScript: "frankenphp_info.php"})
}

func TestFrankenPHPInfo_worker(t *testing.T) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
body, _ := testGet("http://example.com/frankenphp_info.php", handler, t)
assert.Contains(t, body, "[is_worker_thread] => \n")
}, &testOptions{})
}

// To run this fuzzing test use: go test -fuzz FuzzRequest
// TODO: Cover more potential cases
func FuzzRequest(f *testing.F) {
Expand Down
7 changes: 7 additions & 0 deletions testdata/frankenphp_info.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

require_once __DIR__.'/_executor.php';

return function () {
print_r(frankenphp_info());
};
2 changes: 1 addition & 1 deletion threadworker.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (handler *workerThread) getRequestContext() *frankenPHPContext {
}

func (handler *workerThread) name() string {
return "Worker PHP Thread - " + handler.worker.fileName
return "Worker PHP Thread - " + handler.worker.name
}

func setupWorkerScript(handler *workerThread, worker *worker) {
Expand Down
Loading