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