feat: add fastcgi_finish_request() support (#69)

* Add function to ext

* buggy af, but kinda working

* Get worker and regular mode working

* fix formatting

* add eol

* add eol

* 🤦

* properly generate file

* use sync.Once
This commit is contained in:
Rob Landers
2022-11-03 08:36:47 +01:00
committed by GitHub
parent bfb0b17811
commit 9ef3bd7c47
7 changed files with 88 additions and 6 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

7
testdata/finish-request.php vendored Normal file
View File

@@ -0,0 +1,7 @@
<?php
echo "This is output\n";
frankenphp_finish_request();
echo "This is not";

View File

@@ -148,14 +148,19 @@ func go_frankenphp_worker_handle_request_start(rh C.uintptr_t) C.uintptr_t {
}
//export go_frankenphp_worker_handle_request_end
func go_frankenphp_worker_handle_request_end(rh C.uintptr_t) {
func go_frankenphp_worker_handle_request_end(rh C.uintptr_t, deleteHandle bool) {
if rh == 0 {
return
}
rHandle := cgo.Handle(rh)
r := rHandle.Value().(*http.Request)
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
cgo.Handle(rh).Delete()
if deleteHandle {
cgo.Handle(rh).Delete()
}
close(fc.done)
maybeCloseContext(fc)
fc.Logger.Debug("request handling finished", zap.String("worker", fc.Env["SCRIPT_FILENAME"]), zap.String("url", r.RequestURI))
}