diff --git a/builtin-functions/kphp-light/stdlib/server-functions.txt b/builtin-functions/kphp-light/stdlib/server-functions.txt index 0e46d2a141..91996c6b1e 100644 --- a/builtin-functions/kphp-light/stdlib/server-functions.txt +++ b/builtin-functions/kphp-light/stdlib/server-functions.txt @@ -10,6 +10,9 @@ function kphp_get_runtime_config() ::: mixed; function register_shutdown_function (callable():void $callback) ::: void; +/** @kphp-extern-func-info interruptible */ +function ignore_user_abort ($enable ::: ?bool = null) ::: int; + // === URL ======================================================================================== define('PHP_URL_SCHEME', 0); @@ -125,8 +128,6 @@ define('LC_MESSAGES', 5); function debug_backtrace() ::: string[][]; -/** @kphp-extern-func-info stub generation-required */ -function ignore_user_abort ($enable ::: ?bool = null) ::: int; /** @kphp-extern-func-info stub generation-required */ function flush() ::: void; diff --git a/runtime-common/core/allocator/script-allocator-managed.h b/runtime-common/core/allocator/script-allocator-managed.h index 707e7c051b..679a152894 100644 --- a/runtime-common/core/allocator/script-allocator-managed.h +++ b/runtime-common/core/allocator/script-allocator-managed.h @@ -8,13 +8,15 @@ #include "runtime-common/core/allocator/runtime-allocator.h" -class ScriptAllocatorManaged { +namespace kphp::memory { + +class script_allocator_managed { public: static void* operator new(size_t size) noexcept { return RuntimeAllocator::get().alloc_script_memory(size); } - static void* operator new(size_t, void* ptr) noexcept { + static void* operator new(size_t /*unused*/, void* ptr) noexcept { return ptr; } @@ -27,5 +29,7 @@ class ScriptAllocatorManaged { static void operator delete[](void* ptr) = delete; protected: - ~ScriptAllocatorManaged() = default; + ~script_allocator_managed() = default; }; + +} // namespace kphp::memory diff --git a/runtime-common/core/class-instance/refcountable-php-classes.h b/runtime-common/core/class-instance/refcountable-php-classes.h index e046200889..c3cea08249 100644 --- a/runtime-common/core/class-instance/refcountable-php-classes.h +++ b/runtime-common/core/class-instance/refcountable-php-classes.h @@ -9,7 +9,7 @@ #include "runtime-common/core/allocator/script-allocator-managed.h" -class abstract_refcountable_php_interface : public ScriptAllocatorManaged { +class abstract_refcountable_php_interface : public kphp::memory::script_allocator_managed { public: abstract_refcountable_php_interface() __attribute__((always_inline)) = default; virtual ~abstract_refcountable_php_interface() noexcept __attribute__((always_inline)) = default; @@ -98,7 +98,7 @@ class refcountable_polymorphic_php_classes_virt<> : public virtual abstract_refc }; template -class refcountable_php_classes : public ScriptAllocatorManaged { +class refcountable_php_classes : public kphp::memory::script_allocator_managed { public: void add_ref() noexcept { if (refcnt < ExtraRefCnt::for_global_const) { diff --git a/runtime-light/allocator/allocator.h b/runtime-light/allocator/allocator.h index cd418849a8..bd625d9ebc 100644 --- a/runtime-light/allocator/allocator.h +++ b/runtime-light/allocator/allocator.h @@ -10,7 +10,7 @@ #include "runtime-common/core/allocator/script-allocator-managed.h" #include "runtime-light/allocator/allocator-state.h" -template T, typename... Args> +template T, typename... Args> requires std::constructible_from auto make_unique_on_script_memory(Args&&... args) noexcept { return std::make_unique(std::forward(args)...); diff --git a/runtime-light/coroutine/detail/when-any.h b/runtime-light/coroutine/detail/when-any.h index f124f5928e..2d7df28e37 100644 --- a/runtime-light/coroutine/detail/when-any.h +++ b/runtime-light/coroutine/detail/when-any.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include @@ -249,12 +250,17 @@ class when_any_task { }; struct when_any_task_promise_void : public when_any_task_promise_common { - auto yield_value() const noexcept { + private: + std::optional m_result; + + public: + auto yield_value(kphp::coro::void_value&& return_value) noexcept { + m_result.emplace(return_value); return when_any_task_promise_common::final_suspend(); } - constexpr auto result() const noexcept -> std::optional { - return std::optional{std::in_place}; + auto result() noexcept -> std::optional { + return m_result; } constexpr auto return_void() const noexcept -> void {} @@ -296,6 +302,7 @@ template auto make_when_any_task(awaitable_type awaitable) noexcept -> when_any_task::awaiter_return_type> { if constexpr (std::is_void_v::awaiter_return_type>) { co_await std::move(awaitable); + co_yield kphp::coro::void_value{}; } else { co_yield co_await std::move(awaitable); } diff --git a/runtime-light/coroutine/event.h b/runtime-light/coroutine/event.h index 8e428b72ce..39fd6d6974 100644 --- a/runtime-light/coroutine/event.h +++ b/runtime-light/coroutine/event.h @@ -6,8 +6,11 @@ #include #include +#include #include +#include "common/mixin/not_copyable.h" +#include "runtime-common/core/allocator/script-allocator-managed.h" #include "runtime-light/coroutine/async-stack.h" #include "runtime-light/coroutine/coroutine-state.h" #include "runtime-light/stdlib/diagnostics/logs.h" @@ -15,14 +18,22 @@ namespace kphp::coro { class event { - // 1) nullptr => not set - // 2) awaiter* => linked list of awaiters waiting for the event to trigger - // 3) this => The event is triggered and all awaiters are resumed - void* m_state{}; + struct event_controller : kphp::memory::script_allocator_managed, vk::not_copyable { + // 1) nullptr => not set + // 2) awaiter* => linked list of awaiters waiting for the event to trigger + // 3) this => The event is triggered and all awaiters are resumed + void* m_state{}; + + auto set() noexcept -> void; + auto unset() noexcept -> void; + auto is_set() const noexcept -> bool; + }; + + std::unique_ptr m_controller; struct awaiter { - event& m_event; bool m_suspended{}; + event_controller& m_controller; std::coroutine_handle<> m_awaiting_coroutine; kphp::coro::async_stack_root& m_async_stack_root; kphp::coro::async_stack_frame* m_caller_async_stack_frame{}; @@ -30,8 +41,8 @@ class event { awaiter* m_next{}; awaiter* m_prev{}; - explicit awaiter(event& event) noexcept - : m_event(event), + explicit awaiter(event_controller& event_controller) noexcept + : m_controller(event_controller), m_async_stack_root(CoroutineInstanceState::get().coroutine_stack_root) {} awaiter(const awaiter&) = delete; @@ -55,10 +66,28 @@ class event { }; public: - auto set() noexcept -> void; + event() noexcept + : m_controller(std::make_unique()) { + kphp::log::assertion(m_controller != nullptr); + } - auto unset() noexcept -> void; + event(event&& other) noexcept + : m_controller(std::move(other.m_controller)) {} + + event& operator=(event&& other) noexcept { + if (this != std::addressof(other)) { + m_controller = std::move(other.m_controller); + } + return *this; + } + + ~event() = default; + event(const event&) = delete; + event& operator=(const event&) = delete; + + auto set() noexcept -> void; + auto unset() noexcept -> void; auto is_set() const noexcept -> bool; auto operator co_await() noexcept; @@ -72,14 +101,14 @@ inline auto event::awaiter::cancel_awaiter() noexcept -> void { m_prev->m_next = m_next; } else { // we are the head of the awaiters list, so we need to update the head - m_event.m_state = m_next; + m_controller.m_state = m_next; } m_next = nullptr; m_prev = nullptr; } inline auto event::awaiter::await_ready() const noexcept -> bool { - return m_event.is_set(); + return m_controller.is_set(); } template caller_promise_type> @@ -90,15 +119,15 @@ auto event::awaiter::await_suspend(std::coroutine_handle aw m_suspended = true; m_awaiting_coroutine = awaiting_coroutine; - m_next = static_cast(m_event.m_state); + m_next = static_cast(m_controller.m_state); // ensure that the event isn't triggered - kphp::log::assertion(reinterpret_cast(m_next) != std::addressof(m_event)); + kphp::log::assertion(reinterpret_cast(m_next) != std::addressof(m_controller)); if (m_next != nullptr) { m_next->m_prev = this; } - m_event.m_state = this; + m_controller.m_state = this; } inline auto event::awaiter::await_resume() noexcept -> void { @@ -109,7 +138,7 @@ inline auto event::awaiter::await_resume() noexcept -> void { } } -inline auto event::set() noexcept -> void { +inline auto event::event_controller::set() noexcept -> void { void* prev_value{std::exchange(m_state, this)}; if (prev_value == this || prev_value == nullptr) [[unlikely]] { return; @@ -122,18 +151,34 @@ inline auto event::set() noexcept -> void { } } -inline auto event::unset() noexcept -> void { +inline auto event::event_controller::unset() noexcept -> void { if (m_state == this) { m_state = nullptr; } } -inline auto event::is_set() const noexcept -> bool { +inline auto event::event_controller::is_set() const noexcept -> bool { return m_state == this; } +inline auto event::set() noexcept -> void { + kphp::log::assertion(m_controller != nullptr); + m_controller->set(); +} + +inline auto event::unset() noexcept -> void { + kphp::log::assertion(m_controller != nullptr); + m_controller->unset(); +} + +inline auto event::is_set() const noexcept -> bool { + kphp::log::assertion(m_controller != nullptr); + return m_controller->is_set(); +} + inline auto event::operator co_await() noexcept { - return event::awaiter{*this}; + kphp::log::assertion(m_controller != nullptr); + return event::awaiter{*this->m_controller}; } } // namespace kphp::coro diff --git a/runtime-light/k2-platform/k2-api.h b/runtime-light/k2-platform/k2-api.h index 9b0c5f4874..1bbf21333b 100644 --- a/runtime-light/k2-platform/k2-api.h +++ b/runtime-light/k2-platform/k2-api.h @@ -36,6 +36,7 @@ inline constexpr size_t DEFAULT_MEMORY_ALIGN = 16; } // namespace k2_impl_ inline constexpr int32_t errno_ok = 0; +inline constexpr int32_t errno_ebusy = EBUSY; inline constexpr int32_t errno_enodev = ENODEV; inline constexpr int32_t errno_einval = EINVAL; inline constexpr int32_t errno_enodata = ENODATA; @@ -46,6 +47,9 @@ inline constexpr int32_t errno_eshutdown = ESHUTDOWN; inline constexpr int32_t errno_ecanceled = ECANCELED; inline constexpr int32_t errno_erange = ERANGE; inline constexpr int32_t errno_enoent = ENOENT; +inline constexpr int32_t errno_eopnotsupp = EOPNOTSUPP; +inline constexpr int32_t errno_ealready = EALREADY; +inline constexpr int32_t errno_einprogress = EINPROGRESS; using descriptor = uint64_t; inline constexpr k2::descriptor INVALID_PLATFORM_DESCRIPTOR = 0; diff --git a/runtime-light/server/http/http-server-state.h b/runtime-light/server/http/http-server-state.h index 814b5614b8..13d0d1fa94 100644 --- a/runtime-light/server/http/http-server-state.h +++ b/runtime-light/server/http/http-server-state.h @@ -9,13 +9,14 @@ #include #include #include +#include #include "common/mixin/not_copyable.h" #include "runtime-common/core/allocator/script-allocator.h" #include "runtime-common/core/runtime-core.h" #include "runtime-common/core/std/containers.h" #include "runtime-light/coroutine/task.h" -#include "runtime-light/streams/stream.h" +#include "runtime-light/streams/connection.h" namespace kphp::http { @@ -57,7 +58,7 @@ struct HttpServerInstanceState final : private vk::not_copyable { static constexpr auto ENCODING_GZIP = static_cast(1U << 0U); static constexpr auto ENCODING_DEFLATE = static_cast(1U << 1U); - std::optional request_stream; + std::optional connection; std::optional opt_raw_post_data; diff --git a/runtime-light/server/http/init-functions.cpp b/runtime-light/server/http/init-functions.cpp index 5ce9a19343..21d14756c1 100644 --- a/runtime-light/server/http/init-functions.cpp +++ b/runtime-light/server/http/init-functions.cpp @@ -19,11 +19,13 @@ #include #include "common/algorithms/string-algorithms.h" +#include "common/containers/final_action.h" #include "runtime-common/core/allocator/script-allocator.h" #include "runtime-common/core/runtime-core.h" #include "runtime-common/core/std/containers.h" #include "runtime-common/stdlib/server/url-functions.h" #include "runtime-light/core/globals/php-script-globals.h" +#include "runtime-light/coroutine/task.h" #include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/server/http/http-server-state.h" #include "runtime-light/server/http/multipart/multipart.h" @@ -32,7 +34,9 @@ #include "runtime-light/stdlib/diagnostics/logs.h" #include "runtime-light/stdlib/output/output-state.h" #include "runtime-light/stdlib/server/http-functions.h" +#include "runtime-light/stdlib/system/system-functions.h" #include "runtime-light/stdlib/zlib/zlib-functions.h" +#include "runtime-light/streams/connection.h" #include "runtime-light/streams/stream.h" #include "runtime-light/tl/tl-core.h" #include "runtime-light/tl/tl-functions.h" @@ -241,7 +245,26 @@ void init_server(kphp::component::stream&& request_stream, kphp::stl::vector {}", expected_connection.error()); + } + + static constexpr auto user_abort_handler{[] noexcept -> kphp::coro::task<> { + auto& http_server_instance_st{HttpServerInstanceState::get()}; + kphp::log::assertion(http_server_instance_st.connection.has_value()); + if (http_server_instance_st.connection->get_ignore_abort_level() == 0) { + co_await kphp::system::exit(1); + } + }}; + + http_server_instance_st.connection = *std::move(expected_connection); + if (auto expected{http_server_instance_st.connection->register_abort_handler(user_abort_handler)}; !expected) [[unlikely]] { + kphp::log::error("failed to register user abort handler: error code -> {}", expected.error()); + } + } // determine HTTP method if (invoke_http.method.value == GET_METHOD) { @@ -379,6 +402,22 @@ void init_server(kphp::component::stream&& request_stream, kphp::stl::vector finalize_server() noexcept { auto& http_server_instance_st{HttpServerInstanceState::get()}; + kphp::log::assertion(http_server_instance_st.connection.has_value()); + http_server_instance_st.connection->unregister_abort_handler(); + + const auto finalizer{vk::finally([&http_server_instance_st] noexcept { + // TODO pay attention when adding flush + std::ranges::for_each(http_server_instance_st.multipart_temporary_files, [](const auto& multipart_filename) noexcept { + if (const auto expected{k2::unlink(multipart_filename)}; !expected) { + kphp::log::warning("failed to unlink multipart temporary file: error code -> {}", expected.error()); + } + }); + http_server_instance_st.multipart_temporary_files.clear(); + })}; + + if (http_server_instance_st.connection->is_aborted()) { + co_return kphp::log::info("HTTP connection closed"); + } string response_body{}; tl::HttpResponse http_response{}; @@ -422,21 +461,13 @@ kphp::coro::task<> finalize_server() noexcept { tl::storer tls{http_response.footprint()}; http_response.store(tls); - if (!http_server_instance_st.request_stream.has_value()) [[unlikely]] { - kphp::log::error("can't send HTTP response since there is no available stream"); - } - auto& request_stream{*http_server_instance_st.request_stream}; - if (auto expected{co_await kphp::component::send_response(request_stream, tls.view())}; !expected) [[unlikely]] { - kphp::log::error("can't write HTTP response: stream -> {}, error code -> {}", request_stream.descriptor(), expected.error()); + if (auto expected{co_await kphp::component::send_response(http_server_instance_st.connection->get_stream(), tls.view())}; !expected) [[unlikely]] { + kphp::log::error("can't write HTTP response: error code -> {}", expected.error()); } http_server_instance_st.response_state = kphp::http::response_state::completed; [[fallthrough]]; } case kphp::http::response_state::completed: - for (const auto& temporary_file : http_server_instance_st.multipart_temporary_files) { - std::ignore = k2::unlink(temporary_file); - } - http_server_instance_st.multipart_temporary_files.clear(); co_return; } } diff --git a/runtime-light/state/instance-state.h b/runtime-light/state/instance-state.h index 1c91367c30..ddec40a59d 100644 --- a/runtime-light/state/instance-state.h +++ b/runtime-light/state/instance-state.h @@ -89,9 +89,10 @@ struct InstanceState final : vk::not_copyable { } AllocatorState instance_allocator_state{INIT_INSTANCE_ALLOCATOR_SIZE, 0}; - kphp::log::contextual_tags instance_tags; + kphp::log::contextual_tags instance_tags; kphp::coro::io_scheduler io_scheduler; + CoroutineInstanceState coroutine_instance_state; ForkInstanceState fork_instance_state; WaitQueueInstanceState wait_queue_instance_state; diff --git a/runtime-light/stdlib/exit/exit-functions.h b/runtime-light/stdlib/exit/exit-functions.h deleted file mode 100644 index bece1a20b1..0000000000 --- a/runtime-light/stdlib/exit/exit-functions.h +++ /dev/null @@ -1,34 +0,0 @@ -// Compiler for PHP (aka KPHP) -// Copyright (c) 2024 LLC «V Kontakte» -// Distributed under the GPL v3 License, see LICENSE.notice.txt - -#pragma once - -#include - -#include "runtime-common/core/runtime-core.h" -#include "runtime-light/coroutine/task.h" -#include "runtime-light/k2-platform/k2-api.h" -#include "runtime-light/state/instance-state.h" -#include "runtime-light/stdlib/fork/fork-functions.h" -#include "runtime-light/stdlib/output/output-state.h" - -inline kphp::coro::task<> f$exit(mixed v = 0) noexcept { // TODO: make it synchronous - auto& instance_st{InstanceState::get()}; - - int64_t exit_code{}; - if (v.is_string()) { - OutputInstanceState::get().output_buffers.current_buffer().get() << v; - } else if (v.is_int()) { - int64_t v_code{v.to_int()}; - exit_code = v_code >= 0 && v_code <= 254 ? v_code : 1; // valid PHP exit codes: [0..254] - } else { - exit_code = 1; - } - co_await kphp::forks::id_managed(instance_st.run_instance_epilogue()); - k2::exit(static_cast(exit_code)); -} - -inline kphp::coro::task<> f$die(mixed v = 0) noexcept { - co_await f$exit(std::move(v)); -} diff --git a/runtime-light/stdlib/fork/fork-functions.h b/runtime-light/stdlib/fork/fork-functions.h index b22ec2176a..01eb3658c3 100644 --- a/runtime-light/stdlib/fork/fork-functions.h +++ b/runtime-light/stdlib/fork/fork-functions.h @@ -26,6 +26,23 @@ namespace kphp::forks { +class scoped_id_managed { + ForkInstanceState& state{ForkInstanceState::get()}; + int64_t fork_id{state.current_id}; + +public: + scoped_id_managed() noexcept = default; + + ~scoped_id_managed() { + state.current_id = fork_id; + } + + scoped_id_managed(const scoped_id_managed&) = delete; + scoped_id_managed(scoped_id_managed&&) = delete; + scoped_id_managed& operator=(const scoped_id_managed&) = delete; + scoped_id_managed& operator=(scoped_id_managed&&) = delete; +}; + template auto id_managed(awaitable_type awaitable) noexcept -> kphp::coro::task::awaiter_return_type> { auto& fork_instance_st{ForkInstanceState::get()}; diff --git a/runtime-light/stdlib/rpc/rpc-api.cpp b/runtime-light/stdlib/rpc/rpc-api.cpp index 6ac41e3272..d881fa3c11 100644 --- a/runtime-light/stdlib/rpc/rpc-api.cpp +++ b/runtime-light/stdlib/rpc/rpc-api.cpp @@ -345,7 +345,7 @@ kphp::coro::task send_request(std::string_view actor, std }}; static constexpr auto ignore_answer_awaiter_coroutine{ - [](kphp::component::stream stream, std::chrono::milliseconds timeout) noexcept -> kphp::coro::shared_task { + [](kphp::component::stream stream, std::chrono::milliseconds timeout) noexcept -> kphp::coro::shared_task<> { auto fetch_task{kphp::component::fetch_response(stream, [](std::span) noexcept {})}; std::ignore = co_await kphp::coro::io_scheduler::get().schedule(std::move(fetch_task), timeout); }}; diff --git a/runtime-light/stdlib/rpc/rpc-tl-func-base.h b/runtime-light/stdlib/rpc/rpc-tl-func-base.h index 14568b4ac8..98d9262e37 100644 --- a/runtime-light/stdlib/rpc/rpc-tl-func-base.h +++ b/runtime-light/stdlib/rpc/rpc-tl-func-base.h @@ -9,7 +9,7 @@ #include "runtime-light/stdlib/diagnostics/logs.h" #include "runtime-light/stdlib/rpc/rpc-tl-function.h" -struct tl_func_base : ScriptAllocatorManaged { +struct tl_func_base : kphp::memory::script_allocator_managed { virtual mixed fetch() = 0; virtual class_instance typed_fetch() { diff --git a/runtime-light/stdlib/rpc/rpc-tl-request.h b/runtime-light/stdlib/rpc/rpc-tl-request.h index a83e572de9..7dd50bcf0d 100644 --- a/runtime-light/stdlib/rpc/rpc-tl-request.h +++ b/runtime-light/stdlib/rpc/rpc-tl-request.h @@ -11,7 +11,7 @@ #include "runtime-light/stdlib/rpc/rpc-tl-func-base.h" #include "runtime-light/stdlib/rpc/rpc-tl-function.h" -class RpcRequestResult : public ScriptAllocatorManaged { +class RpcRequestResult : public kphp::memory::script_allocator_managed { public: const bool is_typed{}; diff --git a/runtime-light/stdlib/server/common-functions.h b/runtime-light/stdlib/server/common-functions.h new file mode 100644 index 0000000000..e5716cab42 --- /dev/null +++ b/runtime-light/stdlib/server/common-functions.h @@ -0,0 +1,38 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include + +#include "runtime-common/core/runtime-core.h" +#include "runtime-light/coroutine/task.h" +#include "runtime-light/server/http/http-server-state.h" +#include "runtime-light/state/instance-state.h" +#include "runtime-light/stdlib/diagnostics/logs.h" +#include "runtime-light/stdlib/fork/fork-functions.h" +#include "runtime-light/stdlib/system/system-functions.h" + +inline auto f$ignore_user_abort(Optional enable) noexcept -> kphp::coro::task { + if (InstanceState::get().instance_kind() != instance_kind::http_server) { + kphp::log::error("called stub f$ignore_user_abort"); + } + + auto& http_server_instance_st{HttpServerInstanceState::get()}; + kphp::log::assertion(http_server_instance_st.connection.has_value()); + if (enable.is_null()) { + co_return http_server_instance_st.connection->get_ignore_abort_level(); + } else if (enable.val()) { + http_server_instance_st.connection->increase_ignore_abort_level(); + co_return http_server_instance_st.connection->get_ignore_abort_level(); + } else { + const auto prev{http_server_instance_st.connection->get_ignore_abort_level()}; + http_server_instance_st.connection->decrease_ignore_abort_level(); + + if (http_server_instance_st.connection->get_ignore_abort_level() == 0 && http_server_instance_st.connection->is_aborted()) { + co_await kphp::forks::id_managed(kphp::system::exit(1)); + } + co_return prev; + } +} diff --git a/runtime-light/stdlib/system/system-functions.h b/runtime-light/stdlib/system/system-functions.h index a3c4affc62..2bc407c5fc 100644 --- a/runtime-light/stdlib/system/system-functions.h +++ b/runtime-light/stdlib/system/system-functions.h @@ -32,25 +32,32 @@ #include "runtime-light/k2-platform/k2-api.h" #include "runtime-light/state/component-state.h" #include "runtime-light/state/image-state.h" +#include "runtime-light/state/instance-state.h" #include "runtime-light/stdlib/diagnostics/contextual-tags.h" #include "runtime-light/stdlib/diagnostics/logs.h" #include "runtime-light/stdlib/fork/fork-functions.h" +#include "runtime-light/stdlib/output/output-state.h" #include "runtime-light/stdlib/system/system-state.h" namespace kphp::posix::impl { -constexpr std::string_view NAME_PWUID_KEY = "name"; -constexpr std::string_view PASSWD_PWUID_KEY = "passwd"; -constexpr std::string_view UID_PWUID_KEY = "uid"; -constexpr std::string_view GID_PWUID_KEY = "gid"; -constexpr std::string_view GECOS_PWUID_KEY = "gecos"; -constexpr std::string_view DIR_PWUID_KEY = "dir"; -constexpr std::string_view SHELL_PWUID_KEY = "shell"; +inline constexpr std::string_view NAME_PWUID_KEY = "name"; +inline constexpr std::string_view PASSWD_PWUID_KEY = "passwd"; +inline constexpr std::string_view UID_PWUID_KEY = "uid"; +inline constexpr std::string_view GID_PWUID_KEY = "gid"; +inline constexpr std::string_view GECOS_PWUID_KEY = "gecos"; +inline constexpr std::string_view DIR_PWUID_KEY = "dir"; +inline constexpr std::string_view SHELL_PWUID_KEY = "shell"; } // namespace kphp::posix::impl namespace kphp::system { +inline auto exit(int32_t exit_code) noexcept -> kphp::coro::task<> { + co_await InstanceState::get().run_instance_epilogue(); + k2::exit(exit_code); +} + template> output_handler_type = std::identity> auto exec(std::string_view cmd, const output_handler_type& output_handler = {}) noexcept -> std::expected { static constexpr std::string_view program{"sh"}; @@ -78,6 +85,23 @@ auto exec(std::string_view cmd, const output_handler_type& output_handler = {}) } // namespace kphp::system +inline kphp::coro::task<> f$exit(mixed v = 0) noexcept { // TODO: make it synchronous + int64_t exit_code{}; + if (v.is_string()) { + OutputInstanceState::get().output_buffers.current_buffer().get() << v; + } else if (v.is_int()) { + int64_t v_code{v.to_int()}; + exit_code = v_code >= 0 && v_code <= 254 ? v_code : 1; // valid PHP exit codes: [0..254] + } else { + exit_code = 1; + } + co_await kphp::forks::id_managed(kphp::system::exit(static_cast(exit_code))); +} + +inline kphp::coro::task<> f$die(mixed v = 0) noexcept { + co_await f$exit(std::move(v)); +} + template bool f$register_kphp_on_oom_callback(F&& /*callback*/) { kphp::log::warning("called stub register_kphp_on_oom_callback"); diff --git a/runtime-light/streams/connection.h b/runtime-light/streams/connection.h new file mode 100644 index 0000000000..1cdddc49ab --- /dev/null +++ b/runtime-light/streams/connection.h @@ -0,0 +1,178 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "runtime-common/core/runtime-core.h" +#include "runtime-light/coroutine/event.h" +#include "runtime-light/k2-platform/k2-api.h" +#include "runtime-light/stdlib/diagnostics/logs.h" +#include "runtime-light/streams/stream.h" + +namespace kphp::component { + +class connection { + struct shared_state : refcountable_php_classes { + std::optional m_unwatch_event; + + shared_state() noexcept = default; + shared_state(shared_state&&) noexcept = default; + shared_state& operator=(shared_state&&) noexcept = default; + ~shared_state() = default; + + shared_state(const shared_state&) = delete; + shared_state operator=(const shared_state&) = delete; + }; + + class_instance m_shared_state; + kphp::component::stream m_stream; + uint32_t m_ignore_abort_level{}; + + explicit connection(kphp::component::stream&& stream) noexcept; + +public: + connection() = delete; + connection(const connection&) = delete; + auto operator=(const connection&) = delete; + + connection(connection&&) noexcept = default; + auto operator=(connection&& other) noexcept -> connection&; + + ~connection() { + unregister_abort_handler(); + } + + static auto from_stream(kphp::component::stream&& stream) noexcept -> std::expected; + + auto get_stream() noexcept -> kphp::component::stream&; + + auto increase_ignore_abort_level() noexcept -> void; + auto decrease_ignore_abort_level() noexcept -> void; + auto get_ignore_abort_level() const noexcept -> uint32_t; + auto is_aborted() const noexcept -> bool; + + template + auto register_abort_handler(on_abort_handler_type&& h) noexcept -> std::expected; + auto unregister_abort_handler() noexcept -> void; +}; + +// ================================================================================================ + +inline connection::connection(kphp::component::stream&& stream) noexcept + : m_stream(std::move(stream)) { + kphp::log::assertion(!m_shared_state.alloc().is_null()); +} + +inline auto connection::operator=(connection&& other) noexcept -> connection& { + if (this != std::addressof(other)) { + unregister_abort_handler(); + m_shared_state = std::move(other.m_shared_state); + m_stream = std::move(other.m_stream); + m_ignore_abort_level = other.m_ignore_abort_level; + } + return *this; +} + +inline auto connection::from_stream(kphp::component::stream&& stream) noexcept -> std::expected { + if (const auto status{stream.status()}; status.libc_errno != k2::errno_ok) [[unlikely]] { + return std::unexpected{status.libc_errno}; + } + return connection{std::move(stream)}; +} + +inline auto connection::get_stream() noexcept -> kphp::component::stream& { + return m_stream; +} + +inline auto connection::increase_ignore_abort_level() noexcept -> void { + ++m_ignore_abort_level; +} + +inline auto connection::decrease_ignore_abort_level() noexcept -> void { + if (m_ignore_abort_level > 0) { + --m_ignore_abort_level; + } +} + +inline auto connection::get_ignore_abort_level() const noexcept -> uint32_t { + return m_ignore_abort_level; +} + +inline auto connection::is_aborted() const noexcept -> bool { + return m_stream.status().write_status == k2::IOStatus::IOClosed; +} + +template +auto connection::register_abort_handler(on_abort_handler_type&& h) noexcept -> std::expected { + if (m_shared_state.is_null()) [[unlikely]] { + return std::unexpected{k2::errno_eshutdown}; + } + if (m_shared_state.get()->m_unwatch_event.has_value()) [[unlikely]] { // already registered + return std::unexpected{k2::errno_ealready}; + } + + if (const auto status{m_stream.status()}; status.libc_errno != k2::errno_ok || status.write_status == k2::IOStatus::IOClosed) [[unlikely]] { + return std::unexpected{status.libc_errno != k2::errno_ok ? status.libc_errno : k2::errno_ecanceled}; + } + + static constexpr auto watcher{[](k2::descriptor descriptor, class_instance state, on_abort_handler_type h) noexcept -> kphp::coro::task<> { + static constexpr auto unwatch_awaiter{[](class_instance state) noexcept -> kphp::coro::task<> { + kphp::log::assertion(state.get()->m_unwatch_event.has_value()); + co_await *state.get()->m_unwatch_event; + }}; + + static constexpr auto descriptor_awaiter{[](k2::descriptor descriptor) noexcept -> kphp::coro::task { + k2::StreamStatus stream_status{}; + auto& io_scheduler{kphp::coro::io_scheduler::get()}; + for (;;) { // FIXME it should actually use scheduler.poll + k2::stream_status(descriptor, std::addressof(stream_status)); + if (stream_status.write_status == k2::IOStatus::IOClosed) { + co_return std::monostate{}; + } + + using namespace std::chrono_literals; + co_await io_scheduler.schedule(150ms); + } + }}; + + kphp::log::assertion(!state.is_null()); + if (!state.get()->m_unwatch_event.has_value()) { // already unregistered + co_return; + } + + const auto finalizer{vk::finally([state] noexcept { state.get()->m_unwatch_event.reset(); })}; + const auto v{co_await kphp::coro::when_any(unwatch_awaiter(std::move(state)), descriptor_awaiter(descriptor))}; + if (std::holds_alternative(v)) { + if constexpr (kphp::coro::is_async_function_v) { + co_await std::invoke(std::move(h)); + } else { + std::invoke(std::move(h)); + } + } + }}; + + m_shared_state.get()->m_unwatch_event.emplace(); + if (!kphp::coro::io_scheduler::get().spawn(watcher(m_stream.descriptor(), m_shared_state, std::forward(h)))) [[unlikely]] { + m_shared_state.get()->m_unwatch_event.reset(); + return std::unexpected{k2::errno_ebusy}; + } + return {}; +} + +inline auto connection::unregister_abort_handler() noexcept -> void { + if (m_shared_state.is_null() || !m_shared_state.get()->m_unwatch_event.has_value()) { + return; + } + m_shared_state.get()->m_unwatch_event->set(); + m_shared_state.get()->m_unwatch_event.reset(); +} + +} // namespace kphp::component diff --git a/runtime-light/streams/stream.h b/runtime-light/streams/stream.h index 4a556688b5..43b81b9e41 100644 --- a/runtime-light/streams/stream.h +++ b/runtime-light/streams/stream.h @@ -59,6 +59,7 @@ class stream { auto descriptor() const noexcept -> k2::descriptor; auto reset(k2::descriptor descriptor) noexcept -> void; + auto status() const noexcept -> k2::StreamStatus; auto read(std::span buf) const noexcept -> kphp::coro::task>; template> F> @@ -107,6 +108,10 @@ auto stream::accept(duration_type timeout) noexcept -> kphp::coro::task k2::descriptor { + return m_descriptor; +} + inline auto stream::reset(k2::descriptor descriptor) noexcept -> void { if (descriptor == m_descriptor) [[unlikely]] { return; @@ -114,8 +119,10 @@ inline auto stream::reset(k2::descriptor descriptor) noexcept -> void { k2::free_descriptor(std::exchange(m_descriptor, descriptor)); } -inline auto stream::descriptor() const noexcept -> k2::descriptor { - return m_descriptor; +inline auto stream::status() const noexcept -> k2::StreamStatus { + k2::StreamStatus stream_status{}; + k2::stream_status(descriptor(), std::addressof(stream_status)); + return stream_status; } inline auto stream::read(std::span buf) const noexcept -> kphp::coro::task> { diff --git a/tests/python/tests/http_server/php/index.php b/tests/python/tests/http_server/php/index.php index 125d31dbe4..ddbf2477e0 100644 --- a/tests/python/tests/http_server/php/index.php +++ b/tests/python/tests/http_server/php/index.php @@ -29,7 +29,7 @@ function rpc_fetch_responses(array $query_ids): array { * @kphp-required */ function shutdown_function() { - fwrite(STDERR, "shutdown_function was called\n"); + fwrite(fopen("php://stderr", "w"), "shutdown_function was called\n"); } function assert($flag) { @@ -56,7 +56,7 @@ public function work(string $output) { foreach ($responses as $resp) { assert($resp); } - fwrite(STDERR, $output); + fwrite(fopen("php://stderr", "w"), $output); } } @@ -319,6 +319,8 @@ public function work(string $output) { default: echo "ERROR"; return; } + + $stderr = fopen("php://stderr", "w"); switch($_GET["level"]) { case "no_ignore": $worker->work($msg); @@ -326,14 +328,14 @@ public function work(string $output) { case "ignore": ignore_user_abort(true); $worker->work($msg); - fwrite(STDERR, "test_ignore_user_abort/finish_ignore_" . $_GET["type"] . "\n"); + 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($msg); $worker->work($msg); - fwrite(STDERR, "test_ignore_user_abort/finish_multi_ignore_" . $_GET["type"] . "\n"); + fwrite($stderr, "test_ignore_user_abort/finish_multi_ignore_" . $_GET["type"] . "\n"); ignore_user_abort(false); break; case "nested_ignore": @@ -341,7 +343,7 @@ public function work(string $output) { ignore_user_abort(true); $worker->work($msg); ignore_user_abort(false); - fwrite(STDERR, "test_ignore_user_abort/finish_nested_ignore_" . $_GET["type"] . "\n"); + fwrite($stderr, "test_ignore_user_abort/finish_nested_ignore_" . $_GET["type"] . "\n"); ignore_user_abort(false); default: echo "ERROR"; return; diff --git a/tests/python/tests/http_server/test_ignore_user_abort.py b/tests/python/tests/http_server/test_ignore_user_abort.py index 02070720b3..fe7bbe2e6e 100644 --- a/tests/python/tests/http_server/test_ignore_user_abort.py +++ b/tests/python/tests/http_server/test_ignore_user_abort.py @@ -16,7 +16,7 @@ def _send_request(self, uri="/", timeout=0.05): pass """ - Changing the name leads to different tests run order and for some reason it helps to get rid of ASAN warning. + Changing the name leads to different tests run order and for some reason it helps to get rid of ASAN warning. As we decided that the previous ASAN warning was false-positive, this kind of fix might be acceptable for us. Old name was - "test_user_abort_rpc_work" """ @@ -88,3 +88,46 @@ def test_idempotence_ignore_user_abort(self): self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "nested_ignore"], timeout=5) self.web_server.assert_log(["test_ignore_user_abort/finish_nested_ignore_" + "resumable"], timeout=5) self.web_server.assert_log(["shutdown_function was called"], timeout=5) + +@pytest.mark.kphp_skip +class TestIgnoreUserAbortK2(WebServerAutoTestCase): + + def _send_request(self, uri="/", timeout=0.05): + try: + self.web_server.http_request(uri=uri, timeout=timeout) + except Exception: + pass + + def test_user_abort_resumable_work(self): + self._send_request(uri='/test_ignore_user_abort?type=resumable&level=no_ignore') + self.web_server.assert_log(['(.*)hyper\\:\\:Error\\(IncompleteMessage\\)(.*)'], timeout=10) + error = False + try: + self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "no_ignore"], timeout=10) + except Exception: + error = True + self.assertTrue(error) + + def test_ignore_user_abort_resumable_work(self): + self._send_request(uri='/test_ignore_user_abort?type=resumable&level=ignore') + self.web_server.assert_log(['(.*)hyper\\:\\:Error\\(IncompleteMessage\\)(.*)'], timeout=10) + self.web_server.assert_log(['(.*)HTTP connection closed(.*)'], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "ignore"], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_ignore_" + "resumable"], timeout=10) + self.web_server.assert_log(["shutdown_function was called"], timeout=10) + + def test_nested_ignore_user_abort_resumable_work(self): + self._send_request(uri='/test_ignore_user_abort?type=resumable&level=nested_ignore') + self.web_server.assert_log(['(.*)hyper\\:\\:Error\\(IncompleteMessage\\)(.*)'], timeout=10) + self.web_server.assert_log(['(.*)HTTP connection closed(.*)'], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "nested_ignore"], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_nested_ignore_" + "resumable"], timeout=10) + self.web_server.assert_log(["shutdown_function was called"], timeout=10) + + def test_multi_ignore_user_abort_resumable_work(self): + self._send_request(uri='/test_ignore_user_abort?type=resumable&level=multi_ignore') + self.web_server.assert_log(['(.*)hyper\\:\\:Error\\(IncompleteMessage\\)(.*)'], timeout=10) + self.web_server.assert_log(['(.*)HTTP connection closed(.*)'], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_resumable_work_" + "multi_ignore"], timeout=10) + self.web_server.assert_log(["test_ignore_user_abort/finish_multi_ignore_" + "resumable"], timeout=10) + self.web_server.assert_log(["shutdown_function was called"], timeout=10)