diff --git a/frankenphp.c b/frankenphp.c index 1a43998e3..0ef81237f 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -55,6 +55,7 @@ typedef struct frankenphp_server_context { uintptr_t current_request; uintptr_t main_request; /* Only available during worker initialization */ char *cookie_data; + bool finished; } frankenphp_server_context; static void frankenphp_request_reset() { @@ -104,7 +105,7 @@ static void frankenphp_worker_request_shutdown(uintptr_t current_request) { sapi_deactivate(); } zend_end_try(); - if (current_request != 0) go_frankenphp_worker_handle_request_end(current_request); + if (current_request != 0) go_frankenphp_worker_handle_request_end(current_request, true); zend_set_memory_limit(PG(memory_limit)); @@ -154,6 +155,10 @@ static int frankenphp_worker_request_startup() { zend_is_auto_global(ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_SERVER)); + // unfinish the request + frankenphp_server_context *ctx = SG(server_context); + ctx->finished = false; + // TODO: store the list of modules to reload in a global module variable const char **module_name; zend_module_entry *module; @@ -171,6 +176,27 @@ static int frankenphp_worker_request_startup() { return retval; } +PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */ + if (zend_parse_parameters_none() == FAILURE) { + RETURN_THROWS(); + } + + frankenphp_server_context *ctx = SG(server_context); + + if(!ctx->finished) { + php_output_end_all(); + php_header(); + + go_frankenphp_worker_handle_request_end(ctx->current_request, false); + ctx->finished = true; + + RETURN_TRUE; + } + + RETURN_FALSE; + +} /* }}} */ + PHP_FUNCTION(frankenphp_handle_request) { zend_fcall_info fci; zend_fcall_info_cache fcc; @@ -320,6 +346,7 @@ int frankenphp_create_server_context() ctx->current_request = 0; ctx->main_request = 0; ctx->cookie_data = NULL; + ctx->finished = false; SG(server_context) = ctx; @@ -373,6 +400,11 @@ static size_t frankenphp_ub_write(const char *str, size_t str_length) { frankenphp_server_context* ctx = SG(server_context); + if(ctx->finished) { + // todo: maybe log a warning that we tried to write to a finished request? + return 0; + } + return go_ub_write(ctx->current_request ? ctx->current_request : ctx->main_request, (char *) str, str_length); } diff --git a/frankenphp.go b/frankenphp.go index 8f3f171cb..bc6563c2e 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -121,6 +121,9 @@ type FrankenPHPContext struct { populated bool authPassword string + // Whether the request is already closed + closed sync.Once + responseWriter http.ResponseWriter done chan interface{} } @@ -359,6 +362,12 @@ func go_fetch_request() C.uintptr_t { return C.uintptr_t(cgo.NewHandle(r)) } +func maybeCloseContext(fc *FrankenPHPContext) { + fc.closed.Do(func() { + close(fc.done) + }) +} + //export go_execute_script func go_execute_script(rh unsafe.Pointer) { handle := cgo.Handle(rh) @@ -369,7 +378,7 @@ func go_execute_script(rh unsafe.Pointer) { if !ok { panic(InvalidRequestError) } - defer close(fc.done) + defer maybeCloseContext(fc) if C.frankenphp_create_server_context() < 0 { panic(RequestContextCreationError) diff --git a/frankenphp.stub.php b/frankenphp.stub.php index 7c51c9ad1..eb62d5da1 100644 --- a/frankenphp.stub.php +++ b/frankenphp.stub.php @@ -5,3 +5,10 @@ function frankenphp_handle_request(callable $callback): bool {} function headers_send(int $status = 200): int {} + +function frankenphp_finish_request(): bool {} + +/** + * @alias frankenphp_finish_request + */ +function fastcgi_finish_request(): bool {} diff --git a/frankenphp_arginfo.h b/frankenphp_arginfo.h index 21177b5bb..a9fcc7ab5 100644 --- a/frankenphp_arginfo.h +++ b/frankenphp_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: f9ead962eae043fa397a4e573e8905876b7b390b */ + * Stub hash: de4dc4063fafd8c933e3068c8349889a7ece5f03 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) @@ -9,13 +9,21 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +#define arginfo_fastcgi_finish_request arginfo_frankenphp_finish_request + ZEND_FUNCTION(frankenphp_handle_request); ZEND_FUNCTION(headers_send); +ZEND_FUNCTION(frankenphp_finish_request); static const zend_function_entry ext_functions[] = { ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request) ZEND_FE(headers_send, arginfo_headers_send) + ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request) + ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request) ZEND_FE_END }; diff --git a/frankenphp_test.go b/frankenphp_test.go index 3067d5f36..cbaee1460 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -100,6 +100,20 @@ func testHelloWorld(t *testing.T, opts *testOptions) { }, opts) } +func TestFinishRequest_module(t *testing.T) { testFinishRequest(t, nil) } +func TestFinishRequest_worker(t *testing.T) { testFinishRequest(t, &testOptions{workerScript: "index.php"}) } +func testFinishRequest(t *testing.T, opts *testOptions) { + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { + req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/finish-request.php?i=%d", i), nil) + w := httptest.NewRecorder() + handler(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + assert.Equal(t, fmt.Sprintf("This is output\n"), string(body)) + }, opts) +} + func TestServerVariable_module(t *testing.T) { testServerVariable(t, nil) } func TestServerVariable_worker(t *testing.T) { testServerVariable(t, &testOptions{workerScript: "server-variable.php"}) diff --git a/testdata/finish-request.php b/testdata/finish-request.php new file mode 100644 index 000000000..aa6028550 --- /dev/null +++ b/testdata/finish-request.php @@ -0,0 +1,7 @@ +