From 86d67e1241f5574e136acaa1a1f1798be154f801 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Fri, 25 Jul 2025 00:32:34 +0200 Subject: [PATCH 01/10] Adds frankenphp info. --- debugstate.go | 25 +++++++++++++++++++++++++ frankenphp.c | 15 +++++++++++++++ frankenphp_arginfo.h | 6 ++++++ 3 files changed, 46 insertions(+) diff --git a/debugstate.go b/debugstate.go index a7941ac79c..4e6abab94f 100644 --- a/debugstate.go +++ b/debugstate.go @@ -1,5 +1,11 @@ package frankenphp +// #include "frankenphp.h" +import "C" +import ( + "unsafe" +) + // EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only type ThreadDebugState struct { Index int @@ -44,3 +50,22 @@ 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) unsafe.Pointer { + thread := phpThreads[threadIndex] + return PHPArray(&Array{ + keys: []PHPKey{ + PHPKey{Type: PHPStringKey, Str: "thread_name"}, + PHPKey{Type: PHPStringKey, Str: "thread_index"}, + PHPKey{Type: PHPStringKey, Str: "is_worker"}, + }, + values: []interface{}{ + thread.name(), + int(threadIndex), + thread.handler.(*workerThread) != nil, + }, + }) +} diff --git a/frankenphp.c b/frankenphp.c index 102af2db7f..acf5a49ef5 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1213,3 +1213,18 @@ 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(); + } + + zend_array *result = go_frankenphp_info(thread_index); + if (result) { + RETURN_ARR(result); + } + + RETURN_EMPTY_ARRAY(); +} \ No newline at end of file diff --git a/frankenphp_arginfo.h b/frankenphp_arginfo.h index c1bd7b550a..c60d20379a 100644 --- a/frankenphp_arginfo.h +++ b/frankenphp_arginfo.h @@ -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[] = { @@ -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 From 41be5a59d8b306be0a1418565fc3e3c564c98604 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Fri, 25 Jul 2025 00:34:20 +0200 Subject: [PATCH 02/10] clang-format --- frankenphp.c | 19 +++++++++---------- frankenphp_arginfo.h | 4 ++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/frankenphp.c b/frankenphp.c index acf5a49ef5..e60f61614f 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1215,16 +1215,15 @@ void register_extensions(zend_module_entry *m, int len) { } /* EXPERIMENTAL */ -PHP_FUNCTION(frankenphp_info) -{ - if (zend_parse_parameters_none() == FAILURE) { - RETURN_THROWS(); - } +PHP_FUNCTION(frankenphp_info) { + if (zend_parse_parameters_none() == FAILURE) { + RETURN_THROWS(); + } - zend_array *result = go_frankenphp_info(thread_index); - if (result) { - RETURN_ARR(result); - } + zend_array *result = go_frankenphp_info(thread_index); + if (result) { + RETURN_ARR(result); + } - RETURN_EMPTY_ARRAY(); + RETURN_EMPTY_ARRAY(); } \ No newline at end of file diff --git a/frankenphp_arginfo.h b/frankenphp_arginfo.h index c60d20379a..373c90bf56 100644 --- a/frankenphp_arginfo.h +++ b/frankenphp_arginfo.h @@ -30,8 +30,8 @@ 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_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_info, 0, 0, IS_ARRAY, + 0) ZEND_END_ARG_INFO() ZEND_FUNCTION(frankenphp_handle_request); From 99318cfbb673243d1fd0f3f1fd7e7c86ae1e448a Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Fri, 25 Jul 2025 00:35:05 +0200 Subject: [PATCH 03/10] Removes empty return. --- frankenphp.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frankenphp.c b/frankenphp.c index e60f61614f..d48d4cf163 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1221,9 +1221,5 @@ PHP_FUNCTION(frankenphp_info) { } zend_array *result = go_frankenphp_info(thread_index); - if (result) { - RETURN_ARR(result); - } - - RETURN_EMPTY_ARRAY(); + RETURN_ARR(result); } \ No newline at end of file From d17baa1b0de4ea339891b972b1044ade9bcd7115 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Fri, 25 Jul 2025 00:44:19 +0200 Subject: [PATCH 04/10] Adds stub. --- frankenphp.stub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/frankenphp.stub.php b/frankenphp.stub.php index 6c5a71cb5c..8a31387e74 100644 --- a/frankenphp.stub.php +++ b/frankenphp.stub.php @@ -32,3 +32,4 @@ function frankenphp_response_headers(): array|bool {} */ function apache_response_headers(): array|bool {} +function frankenphp_info(): array {} From bda526382b45a573e5b359d7dfd3a89d01fd15a1 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Fri, 25 Jul 2025 14:56:28 +0200 Subject: [PATCH 05/10] Adds tests. --- frankenphp.stub.php | 5 +++++ frankenphp_test.go | 14 ++++++++++++++ testdata/frankenphp_info.php | 7 +++++++ 3 files changed, 26 insertions(+) create mode 100644 testdata/frankenphp_info.php diff --git a/frankenphp.stub.php b/frankenphp.stub.php index 8a31387e74..925d39aca8 100644 --- a/frankenphp.stub.php +++ b/frankenphp.stub.php @@ -32,4 +32,9 @@ function frankenphp_response_headers(): array|bool {} */ function apache_response_headers(): array|bool {} +#[\JetBrains\PhpStorm\ArrayShape([ + 'name' => 'string', + 'thread_index' => 'int', + 'is_worker_thread' => 'bool', +])] function frankenphp_info(): array {} diff --git a/frankenphp_test.go b/frankenphp_test.go index 24241e058b..f97bd0bf4c 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -1059,6 +1059,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 := fetchBody("GET", "http://example.com/frankenphp_info.php", handler) + 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 := fetchBody("GET", "http://example.com/frankenphp_info.php", handler) + 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) { diff --git a/testdata/frankenphp_info.php b/testdata/frankenphp_info.php new file mode 100644 index 0000000000..39b5205a64 --- /dev/null +++ b/testdata/frankenphp_info.php @@ -0,0 +1,7 @@ + Date: Fri, 25 Jul 2025 14:59:40 +0200 Subject: [PATCH 06/10] Fixes tests. --- debugstate.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/debugstate.go b/debugstate.go index 4e6abab94f..882b9c56e4 100644 --- a/debugstate.go +++ b/debugstate.go @@ -56,16 +56,19 @@ func threadDebugState(thread *phpThread) ThreadDebugState { //export go_frankenphp_info func go_frankenphp_info(threadIndex C.uintptr_t) unsafe.Pointer { thread := phpThreads[threadIndex] + + _, isWorker := thread.handler.(*workerThread) + return PHPArray(&Array{ keys: []PHPKey{ PHPKey{Type: PHPStringKey, Str: "thread_name"}, PHPKey{Type: PHPStringKey, Str: "thread_index"}, - PHPKey{Type: PHPStringKey, Str: "is_worker"}, + PHPKey{Type: PHPStringKey, Str: "is_worker_thread"}, }, values: []interface{}{ thread.name(), int(threadIndex), - thread.handler.(*workerThread) != nil, + isWorker, }, }) } From d13dbe786fcc2b4e6e8ddd9169be5e223fc6e86a Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Thu, 4 Sep 2025 21:28:48 +0200 Subject: [PATCH 07/10] Adds all thread states. --- debugstate.go | 45 +++++++++++++++++++++++++++------------------ frankenphp.c | 6 ++++-- frankenphp.h | 1 + frankenphp_test.go | 4 ++-- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/debugstate.go b/debugstate.go index 882b9c56e4..8ded8e01c7 100644 --- a/debugstate.go +++ b/debugstate.go @@ -2,9 +2,6 @@ package frankenphp // #include "frankenphp.h" import "C" -import ( - "unsafe" -) // EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only type ThreadDebugState struct { @@ -54,21 +51,33 @@ func threadDebugState(thread *phpThread) ThreadDebugState { // EXPERIMENTAL: Expose the current thread's information to PHP // //export go_frankenphp_info -func go_frankenphp_info(threadIndex C.uintptr_t) unsafe.Pointer { - thread := phpThreads[threadIndex] +func go_frankenphp_info(threadIndex C.uintptr_t) *C.zval { + currentThread := phpThreads[threadIndex] + _, isWorker := currentThread.handler.(*workerThread) - _, isWorker := thread.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": int64(threadIndex), + "is_worker_thread": isWorker, + "threads": threadInfos, + })) + + // TODO: how to circumvent pinning? + currentThread.Pin(zval) - return PHPArray(&Array{ - keys: []PHPKey{ - PHPKey{Type: PHPStringKey, Str: "thread_name"}, - PHPKey{Type: PHPStringKey, Str: "thread_index"}, - PHPKey{Type: PHPStringKey, Str: "is_worker_thread"}, - }, - values: []interface{}{ - thread.name(), - int(threadIndex), - isWorker, - }, - }) + return zval } diff --git a/frankenphp.c b/frankenphp.c index 9f3a27e758..ea9ab75676 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -46,6 +46,7 @@ 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, + TOSTRING(FRANKENPHP_VERSION) }; } @@ -1204,6 +1205,7 @@ PHP_FUNCTION(frankenphp_info) { RETURN_THROWS(); } - zend_array *result = go_frankenphp_info(thread_index); - RETURN_ARR(result); + zval *result = go_frankenphp_info(thread_index); + + RETURN_ARR(Z_ARR_P(result)); } \ No newline at end of file diff --git a/frankenphp.h b/frankenphp.h index c17df6061a..341688107e 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -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(); diff --git a/frankenphp_test.go b/frankenphp_test.go index 235a4103e9..edd8024a05 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -970,14 +970,14 @@ func TestFileStreamInWorkerMode(t *testing.T) { func TestFrankenPHPInfo_module(t *testing.T) { runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { - body := fetchBody("GET", "http://example.com/frankenphp_info.php", handler) + 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 := fetchBody("GET", "http://example.com/frankenphp_info.php", handler) + body, _ := testGet("http://example.com/frankenphp_info.php", handler, t) assert.Contains(t, body, "[is_worker_thread] => \n") }, &testOptions{}) } From 57136919e4feb85c84f52b3be909e99e8213c600 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Thu, 4 Sep 2025 21:40:56 +0200 Subject: [PATCH 08/10] clang-format. --- frankenphp.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frankenphp.c b/frankenphp.c index ea9ab75676..aa8e290134 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -44,10 +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, - TOSTRING(FRANKENPHP_VERSION) - }; + 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() { From 7324e99536b1d7f45603314aeb9e5c6ae1ff1e8e Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Tue, 9 Sep 2025 21:54:33 +0200 Subject: [PATCH 09/10] Adjusts thread name and array shape. --- debugstate.go | 8 ++++---- frankenphp.stub.php | 19 ++++++++++++++----- threadworker.go | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/debugstate.go b/debugstate.go index 8ded8e01c7..d2316210de 100644 --- a/debugstate.go +++ b/debugstate.go @@ -70,10 +70,10 @@ func go_frankenphp_info(threadIndex C.uintptr_t) *C.zval { } zval := (*C.zval)(PHPMap(map[string]any{ - "frankenphp_version": C.GoString(C.frankenphp_get_version().frankenphp_version), - "current_thread": int64(threadIndex), - "is_worker_thread": isWorker, - "threads": threadInfos, + "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? diff --git a/frankenphp.stub.php b/frankenphp.stub.php index 925d39aca8..842dc30ee6 100644 --- a/frankenphp.stub.php +++ b/frankenphp.stub.php @@ -32,9 +32,18 @@ function frankenphp_response_headers(): array|bool {} */ function apache_response_headers(): array|bool {} -#[\JetBrains\PhpStorm\ArrayShape([ - 'name' => 'string', - 'thread_index' => 'int', - 'is_worker_thread' => '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 {} diff --git a/threadworker.go b/threadworker.go index b7dc820367..5f5215aa6a 100644 --- a/threadworker.go +++ b/threadworker.go @@ -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) { From 5112c10950e3cadcf291e0e3a3355783178c67c8 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Tue, 9 Sep 2025 22:04:55 +0200 Subject: [PATCH 10/10] Fixes test. --- caddy/admin_test.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/caddy/admin_test.go b/caddy/admin_test.go index 345ceff806..6e27e6489a 100644 --- a/caddy/admin_test.go +++ b/caddy/admin_test.go @@ -4,9 +4,9 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/dunglas/frankenphp/internal/fastabs" "io" "net/http" + "strings" "sync" "testing" @@ -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 @@ -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 + `" + } } } } @@ -280,11 +284,10 @@ 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 } } @@ -292,7 +295,7 @@ func TestAddModuleWorkerViaAdminApi(t *testing.T) { // 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")