Compare commits

...

12 Commits

Author SHA1 Message Date
Alliballibaba
5112c10950 Fixes test. 2025-09-09 22:04:55 +02:00
Alliballibaba
7324e99536 Adjusts thread name and array shape. 2025-09-09 21:54:33 +02:00
Alliballibaba
618457e2df Merge branch 'main' into feat/frankenphp-info 2025-09-09 21:39:05 +02:00
Alliballibaba
57136919e4 clang-format. 2025-09-04 21:40:56 +02:00
Alliballibaba
d13dbe786f Adds all thread states. 2025-09-04 21:28:48 +02:00
Alliballibaba
0aa4ac1226 Merge branch 'main' into feat/frankenphp-info 2025-09-04 18:53:29 +02:00
Alliballibaba
1142877367 Fixes tests. 2025-07-25 14:59:40 +02:00
Alliballibaba
bda526382b Adds tests. 2025-07-25 14:56:28 +02:00
Alliballibaba
d17baa1b0d Adds stub. 2025-07-25 00:44:19 +02:00
Alliballibaba
99318cfbb6 Removes empty return. 2025-07-25 00:35:05 +02:00
Alliballibaba
41be5a59d8 clang-format 2025-07-25 00:34:20 +02:00
Alliballibaba
86d67e1241 Adds frankenphp info. 2025-07-25 00:32:34 +02:00
9 changed files with 103 additions and 9 deletions

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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));
}

View File

@@ -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();

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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) {

7
testdata/frankenphp_info.php vendored Normal file
View File

@@ -0,0 +1,7 @@
<?php
require_once __DIR__.'/_executor.php';
return function () {
print_r(frankenphp_info());
};

View File

@@ -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) {