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") diff --git a/debugstate.go b/debugstate.go index a7941ac79c..d2316210de 100644 --- a/debugstate.go +++ b/debugstate.go @@ -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 @@ -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 +} diff --git a/frankenphp.c b/frankenphp.c index 3e516ffd48..e01a8e7274 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -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() { @@ -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)); +} \ 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.stub.php b/frankenphp.stub.php index 6c5a71cb5c..842dc30ee6 100644 --- a/frankenphp.stub.php +++ b/frankenphp.stub.php @@ -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 {} diff --git a/frankenphp_arginfo.h b/frankenphp_arginfo.h index c1bd7b550a..373c90bf56 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 diff --git a/frankenphp_test.go b/frankenphp_test.go index 7b4b44dbce..edd8024a05 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -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) { 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 @@ +