From 2206f5ea12ed7de481176db5055fed7661f34309 Mon Sep 17 00:00:00 2001
From: Petr Shumilov
Date: Thu, 3 Oct 2024 12:36:52 +0300
Subject: [PATCH] Add `header_register_callback` builtin support (#1101)
* Add header_register_callback builtin support
Signed-off-by: Petr Shumilov
---
builtin-functions/kphp-full/_functions.txt | 1 +
compiler/pipes/final-check.cpp | 24 ++++--
runtime/interface.cpp | 31 +++++++-
runtime/interface.h | 9 +++
.../throw_error_1.php | 10 +++
.../throw_error_2.php | 7 ++
.../throw_error_3.php | 13 ++++
..._register_callback_unreachable_in_cli.phpt | 8 ++
...register_callback_no_invoked_by_flush.phpt | 9 +++
.../phpt/shutdown_functions/throw_error1.php | 2 +-
.../phpt/shutdown_functions/throw_error2.php | 2 +-
.../phpt/shutdown_functions/throw_error3.php | 2 +-
tests/python/tests/http_server/php/index.php | 74 ++++++++++++++++---
tests/python/tests/http_server/test_flush.py | 43 +++++++++++
.../test_header_register_callback.py | 15 ++++
tests/zend-test-list | 2 +
16 files changed, 228 insertions(+), 24 deletions(-)
create mode 100644 tests/phpt/header_register_callback/throw_error_1.php
create mode 100644 tests/phpt/header_register_callback/throw_error_2.php
create mode 100644 tests/phpt/header_register_callback/throw_error_3.php
create mode 100644 tests/phpt/pk/030_header_register_callback_unreachable_in_cli.phpt
create mode 100644 tests/phpt/pk/031_header_register_callback_no_invoked_by_flush.phpt
create mode 100644 tests/python/tests/http_server/test_header_register_callback.py
diff --git a/builtin-functions/kphp-full/_functions.txt b/builtin-functions/kphp-full/_functions.txt
index 48b2d84f79..dbd22a5a41 100644
--- a/builtin-functions/kphp-full/_functions.txt
+++ b/builtin-functions/kphp-full/_functions.txt
@@ -149,6 +149,7 @@ function setrawcookie ($name ::: string, $value ::: string, $expire ::: int = 0,
function register_shutdown_function (callable():void $function) ::: void;
function ignore_user_abort ($enable ::: ?bool = null) ::: int;
function flush() ::: void;
+function header_register_callback (callable():void $callback) ::: bool;
/* // removed because it's not working now.
function fastcgi_finish_request() ::: void;
*/
diff --git a/compiler/pipes/final-check.cpp b/compiler/pipes/final-check.cpp
index 2b7b7dcaa6..466c02f426 100644
--- a/compiler/pipes/final-check.cpp
+++ b/compiler/pipes/final-check.cpp
@@ -319,8 +319,7 @@ static void check_kphp_tracing_func_enter_branch_call(FunctionPtr current_functi
"kphp_tracing_func_enter_branch() is available only inside functions with @kphp-tracing aggregate");
}
-void check_register_shutdown_functions(VertexAdaptor call) {
- auto callback = call->args()[0].as();
+void raise_error_if_throwable(const std::string &where, const VertexAdaptor &callback) {
if (!callback->func_id->can_throw()) {
return;
}
@@ -328,11 +327,20 @@ void check_register_shutdown_functions(VertexAdaptor call) {
for (const auto &e : callback->func_id->exceptions_thrown) {
throws.emplace_back(e->name);
}
- kphp_error(false,
- fmt_format("register_shutdown_callback should not throw exceptions\n"
- "But it may throw {}\n"
- "Throw chain: {}",
- vk::join(throws, ", "), callback->func_id->get_throws_call_chain()));
+ kphp_error(false, fmt_format("{} should not throw exceptions\n"
+ "But it throws {}\n"
+ "Throw chain: {}",
+ where.c_str(), vk::join(throws, ", "), callback->func_id->get_throws_call_chain()));
+}
+
+void check_register_shutdown_functions(VertexAdaptor call) {
+ auto callback = call->args()[0].as();
+ raise_error_if_throwable(call->func_id->name, callback);
+}
+
+void check_header_register_callback(VertexAdaptor call) {
+ auto callback = call->args()[0].as();
+ raise_error_if_throwable(call->func_id->name, callback);
}
void mark_global_vars_for_memory_stats(const std::vector &vars_list) {
@@ -871,6 +879,8 @@ void FinalCheckPass::check_op_func_call(VertexAdaptor call) {
kphp_error(arg_type->can_store_null(), fmt_format("is_null() will be always false for {}", arg_type->as_human_readable()));
} else if (function_name == "register_shutdown_function") {
check_register_shutdown_functions(call);
+ } else if (function_name == "header_register_callback") {
+ check_header_register_callback(call);
} else if (function_name == "to_mixed") {
check_to_mixed_call(call);
} else if (vk::string_view{function_name}.starts_with("rpc_tl_query")) {
diff --git a/runtime/interface.cpp b/runtime/interface.cpp
index 76f75e837c..cb4439dc8f 100644
--- a/runtime/interface.cpp
+++ b/runtime/interface.cpp
@@ -206,6 +206,8 @@ static string http_status_line;
static char headers_storage[sizeof(array)];
static array *headers = reinterpret_cast *> (headers_storage);
static long long header_last_query_num = -1;
+static bool headers_sent = false;
+static headers_custom_handler_function_type headers_custom_handler_function;
static bool check_status_line_int(const char *str, int str_len, int *pos) {
if (*pos != str_len && str[*pos] == '0') {
@@ -562,7 +564,11 @@ static int ob_merge_buffers() {
void f$flush() {
php_assert(ob_cur_buffer >= 0 && php_worker.has_value());
-
+ // Run custom headers handler before body processing
+ if (headers_custom_handler_function && !headers_sent && query_type == QUERY_TYPE_HTTP) {
+ headers_sent = true;
+ headers_custom_handler_function();
+ }
string_buffer const * http_body = compress_http_query_body(&oub[ob_system_level]);
string_buffer const * http_headers = nullptr;
if (!php_worker->flushed_http_connection) {
@@ -576,7 +582,12 @@ void f$flush() {
}
void f$fastcgi_finish_request(int64_t exit_code) {
- int const ob_total_buffer = ob_merge_buffers();
+ // Run custom headers handler before body processing
+ if (headers_custom_handler_function && !headers_sent && query_type == QUERY_TYPE_HTTP) {
+ headers_sent = true;
+ headers_custom_handler_function();
+ }
+ int ob_total_buffer = ob_merge_buffers();
if (php_worker.has_value() && php_worker->flushed_http_connection) {
string const raw_response = oub[ob_total_buffer].str();
http_set_result(nullptr, 0, raw_response.c_str(), raw_response.size(), static_cast(exit_code));
@@ -690,6 +701,14 @@ void register_shutdown_function_impl(shutdown_function_type &&f) {
new(&shutdown_functions[shutdown_functions_count++]) shutdown_function_type{std::move(f)};
}
+void register_header_handler_impl(headers_custom_handler_function_type &&f) {
+ dl::CriticalSectionGuard critical_section;
+ // Move assignment leads to lhs object invalidation and fires memory releasing mechanism
+ // But memory is already released by destructor after previous run
+ // Therefore we need to use placement new
+ new(&headers_custom_handler_function) headers_custom_handler_function_type{std::move(f)};
+}
+
void finish(int64_t exit_code, bool from_exit) {
check_script_timeout();
if (!finished) {
@@ -2344,9 +2363,17 @@ static void free_shutdown_functions() {
shutdown_functions_count = 0;
}
+static void free_header_handler_function() {
+ headers_custom_handler_function.~headers_custom_handler_function_type();
+ new(&headers_custom_handler_function) headers_custom_handler_function_type{};
+ headers_sent = false;
+}
+
+
static void free_interface_lib() {
dl::enter_critical_section();//OK
free_shutdown_functions();
+ free_header_handler_function();
if (dl::query_num == uploaded_files_last_query_num) {
const array *const_uploaded_files = uploaded_files;
for (auto p = const_uploaded_files->begin(); p != const_uploaded_files->end(); ++p) {
diff --git a/runtime/interface.h b/runtime/interface.h
index b1a05f8e55..cd459ddac0 100644
--- a/runtime/interface.h
+++ b/runtime/interface.h
@@ -17,6 +17,7 @@
extern string_buffer *coub;//TODO static
using shutdown_function_type = std::function;
+using headers_custom_handler_function_type = std::function;
enum class shutdown_functions_status {
not_executed,
@@ -80,6 +81,14 @@ void f$register_shutdown_function(F &&f) {
register_shutdown_function_impl(shutdown_function_type{std::forward(f)});
}
+void register_header_handler_impl(headers_custom_handler_function_type &&f);
+
+template
+bool f$header_register_callback(F &&f) {
+ register_header_handler_impl(headers_custom_handler_function_type{std::forward(f)});
+ return true;
+}
+
void f$fastcgi_finish_request(int64_t exit_code = 0);
__attribute__((noreturn))
diff --git a/tests/phpt/header_register_callback/throw_error_1.php b/tests/phpt/header_register_callback/throw_error_1.php
new file mode 100644
index 0000000000..0e4496d46b
--- /dev/null
+++ b/tests/phpt/header_register_callback/throw_error_1.php
@@ -0,0 +1,10 @@
+@kphp_should_fail k2_skip
+/header_register_callback should not throw exceptions/
+port = $port;
}
- public function work() {
+ public function work(string $output) {
$conn = new_rpc_connection('localhost', $this->port, 0, 5);
$req_id = rpc_tl_query_one($conn, ["_" => "engine.sleep",
"time_ms" => 120]);
$resp = rpc_tl_query_result_one($req_id);
assert($resp['result']);
- fwrite(STDERR, "test_ignore_user_abort/finish_rpc_work_" . $_GET["level"] . "\n");
+ fwrite(STDERR, $output);
}
}
@@ -119,6 +119,37 @@ public function work() {
flush();
sleep(2);
throw new Exception('Exception');
+ case "flush_and_header_register_callback_flush_inside_callback":
+ echo "Zero ";
+ header_register_callback(function () {
+ echo "Two ";
+ flush();
+ sleep(2);
+ echo "Three ";
+ });
+ echo "One ";
+ return;
+ case 'flush_and_header_register_callback_invoked_after_flush':
+ header_register_callback(function () {
+ echo "One ";
+ });
+ echo "Zero ";
+ flush();
+ sleep(2);
+ echo "Two ";
+ return;
+ case 'flush_and_header_register_callback_no_double_invoked_after_flush':
+ header_register_callback(function () {
+ echo "One ";
+ });
+ echo "Zero ";
+ flush();
+ sleep(2);
+ echo "Two ";
+ flush();
+ sleep(2);
+ echo "Three ";
+ return;
}
echo "OK";
@@ -151,37 +182,40 @@ public function work() {
register_shutdown_function('shutdown_function');
/** @var I */
$worker = null;
+ $msg = "";
switch($_GET["type"]) {
case "rpc":
$worker = new RpcWorker(intval($_GET["port"]));
+ $msg = "test_ignore_user_abort/finish_rpc_work_" . $_GET["level"] . "\n";
break;
case "resumable":
$worker = new ResumableWorker;
+ $msg = "test_ignore_user_abort/finish_resumable_work_" . $_GET["level"] . "\n";
break;
default:
echo "ERROR"; return;
}
switch($_GET["level"]) {
case "no_ignore":
- $worker->work();
+ $worker->work($msg);
break;
case "ignore":
ignore_user_abort(true);
- $worker->work();
+ $worker->work($msg);
fwrite(STDERR, "test_ignore_user_abort/finish_ignore_" . $_GET["type"] . "\n");
ignore_user_abort(false);
break;
case "multi_ignore":
ignore_user_abort(true);
- $worker->work();
- $worker->work();
+ $worker->work($msg);
+ $worker->work($msg);
fwrite(STDERR, "test_ignore_user_abort/finish_multi_ignore_" . $_GET["type"] . "\n");
ignore_user_abort(false);
break;
case "nested_ignore":
ignore_user_abort(true);
ignore_user_abort(true);
- $worker->work();
+ $worker->work($msg);
ignore_user_abort(false);
fwrite(STDERR, "test_ignore_user_abort/finish_nested_ignore_" . $_GET["type"] . "\n");
ignore_user_abort(false);
@@ -217,9 +251,25 @@ public function work() {
} else if ($_SERVER["PHP_SELF"] === "/pid") {
echo "pid=" . posix_getpid();
} else if ($_SERVER["PHP_SELF"] === "/test_script_errors") {
- critical_error("Test error");
+ critical_error("Test error");
} else if ($_SERVER["PHP_SELF"] === "/test_oom_handler") {
- require_once "test_oom_handler.php";
+ require_once "test_oom_handler.php";
+} else if ($_SERVER["PHP_SELF"] === "/test_header_register_callback") {
+ header_register_callback(function () {
+ global $_GET;
+ switch($_GET["act_in_callback"]) {
+ case "rpc":
+ $msg = "test_header_register_callback/rpc_in_callback\n";
+ (new RpcWorker(intval($_GET["port"])))->work($msg);
+ break;
+ case "exit":
+ $msg = "test_header_register_callback/exit_in_callback";
+ exit($msg);
+ default:
+ echo "ERROR";
+ return;
+ }
+ });
} else {
if ($_GET["hints"] === "yes") {
send_http_103_early_hints(["Content-Type: text/plain or application/json", "Link: ; rel=preload; as=script"]);
diff --git a/tests/python/tests/http_server/test_flush.py b/tests/python/tests/http_server/test_flush.py
index 73ecf24cfa..5e52018ed1 100644
--- a/tests/python/tests/http_server/test_flush.py
+++ b/tests/python/tests/http_server/test_flush.py
@@ -62,3 +62,46 @@ def test_error_on_flush(self):
second_chunk = s.recv(4096)
self.kphp_server.assert_log(['Exception'], timeout=5)
self.assertEqual(second_chunk, b'')
+ def test_flush_and_header_register_callback_flush_inside_callback(self):
+ request = b"GET /test_script_flush?type=flush_and_header_register_callback_flush_inside_callback HTTP/1.1\r\nHost:localhost\r\n\r\n"
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.connect(('127.0.0.1', self.kphp_server.http_port))
+ s.send(request)
+ first_chunk = RawResponse(s.recv(200))
+ self.assertEqual(first_chunk.status_code, 200)
+ self.assertEqual(first_chunk.content, b'Zero One Two ')
+
+ s.settimeout(None)
+ second_chunk = s.recv(4096)
+ self.assertEqual(second_chunk, b'Three ')
+
+ def test_flush_and_header_register_callback_invoked_after_flush(self):
+ request = b"GET /test_script_flush?type=flush_and_header_register_callback_invoked_after_flush HTTP/1.1\r\nHost:localhost\r\n\r\n"
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.connect(('127.0.0.1', self.kphp_server.http_port))
+ s.send(request)
+ first_chunk = RawResponse(s.recv(200))
+ self.assertEqual(first_chunk.status_code, 200)
+ self.assertEqual(first_chunk.content, b'Zero One ')
+
+ s.settimeout(None)
+ second_chunk = s.recv(4096)
+ self.assertEqual(second_chunk, b'Two ')
+
+ def test_flush_and_header_register_callback_no_double_invoked_after_flush(self):
+ request = b"GET /test_script_flush?type=flush_and_header_register_callback_no_double_invoked_after_flush HTTP/1.1\r\nHost:localhost\r\n\r\n"
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.connect(('127.0.0.1', self.kphp_server.http_port))
+ s.send(request)
+ first_chunk = RawResponse(s.recv(200))
+ self.assertEqual(first_chunk.status_code, 200)
+ self.assertEqual(first_chunk.content, b'Zero One ')
+
+ s.settimeout(None)
+ second_chunk = s.recv(4096)
+ self.assertEqual(second_chunk, b'Two ')
+
+ s.settimeout(None)
+ second_chunk = s.recv(4096)
+ self.assertEqual(second_chunk, b'Three ')
+
diff --git a/tests/python/tests/http_server/test_header_register_callback.py b/tests/python/tests/http_server/test_header_register_callback.py
new file mode 100644
index 0000000000..a870ff12ef
--- /dev/null
+++ b/tests/python/tests/http_server/test_header_register_callback.py
@@ -0,0 +1,15 @@
+import socket
+
+from python.lib.testcase import KphpServerAutoTestCase
+from python.lib.http_client import RawResponse
+
+
+class TestHeaderRegisterCallback(KphpServerAutoTestCase):
+ def test_rpc_in_callback(self):
+ self.kphp_server.http_request(uri='/test_header_register_callback?act_in_callback=rpc&port={}'.format(str(self.kphp_server.master_port)), timeout=5)
+ self.kphp_server.assert_log(["test_header_register_callback/rpc_in_callback"], timeout=5)
+
+ def test_exit_in_callback(self):
+ response = self.kphp_server.http_request(uri='/test_header_register_callback?act_in_callback=exit&port={}'.format(str(self.kphp_server.master_port)), timeout=5)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, b"test_header_register_callback/exit_in_callback")
\ No newline at end of file
diff --git a/tests/zend-test-list b/tests/zend-test-list
index 8b5c776c2a..d456eee81e 100644
--- a/tests/zend-test-list
+++ b/tests/zend-test-list
@@ -406,6 +406,7 @@ ext/standard/tests/general_functions/ob_get_flush_error.phpt
ext/standard/tests/general_functions/ob_get_length_basic.phpt
ext/standard/tests/general_functions/php_uname_basic.phpt
ext/standard/tests/general_functions/php_uname_error.phpt
+ext/standard/tests/general_functions/bug71891.phpt
ext/standard/tests/math/abs_basic.phpt
ext/standard/tests/math/acos_variation.phpt
ext/standard/tests/math/atan_variation.phpt
@@ -747,6 +748,7 @@ tests/basic/rfc1867_missing_boundary.phpt
tests/basic/rfc1867_missing_boundary_2.phpt
tests/basic/rfc1867_post_max_filesize.phpt
tests/basic/rfc1867_post_max_size.phpt
+tests/basic/header_register_callback.phpt
tests/classes/constants_basic_005.phpt
tests/classes/constants_error_002.phpt
tests/classes/constants_scope_001.phpt