From 84de80649e02bba7101aa7e687668cde27de9f2e Mon Sep 17 00:00:00 2001 From: Roman Nikonov Date: Sun, 19 Jan 2025 21:40:30 +0100 Subject: [PATCH] squash, old 1c468c8c94989f33fdbb239a47ab0831b97ffb84 --- .github/actions/pr_base/action.yml | 5 + .github/workflows/deploy.yml | 3 +- .github/workflows/pr-linux-variants.yml | 1 + aaa-playground/CMakeLists.txt | 10 + aaa-playground/cmakeproj.cmake | 2 + aaa-playground/src/main.cpp | 82 ++ antigo/CMakeLists.txt | 26 + antigo/LICENSE | 165 ++++ antigo/cmakeproj.cmake | 2 + antigo/include/antigo/Context.h | 56 ++ antigo/include/antigo/ExecutionData.h | 16 + antigo/include/antigo/ResolvedContext.h | 35 + antigo/include/antigo/impl/complex_values.h | 34 + antigo/include/antigo/impl/test_helpers.h | 6 + .../include/antigo/third_party/fast_pimpl.hpp | 124 +++ antigo/include_stub/antigo/Context.h | 43 + antigo/include_stub/antigo/ExecutionData.h | 16 + antigo/include_stub/antigo/ResolvedContext.h | 35 + .../include_stub/antigo/impl/complex_values.h | 33 + antigo/src/antigo/Context.cpp | 30 + antigo/src/antigo/ResolvedContext.cpp | 81 ++ antigo/src/antigo/impl/ExecutionData.cpp | 42 + antigo/src/antigo/impl/ExecutionData.h | 22 + antigo/src/antigo/impl/OnstackContextImpl.cpp | 139 ++++ antigo/src/antigo/impl/OnstackContextImpl.h | 226 +++++ antigo/src_stub/antigo_stub.cpp | 34 + antigo/unit/CMakeLists.txt | 38 + antigo/unit/src/ContextTest.cpp | 780 ++++++++++++++++++ antigo/unit/src/main.cpp | 6 + libespm/CMakeLists.txt | 2 +- libespm/include/libespm/CombineBrowser.h | 3 + libespm/src/CombineBrowser.cpp | 11 + misc/deploy/workaround_temporary/run.sh | 2 +- papyrus-vm/CMakeLists.txt | 2 +- .../include/papyrus-vm/ActivePexInstance.h | 2 - papyrus-vm/include/papyrus-vm/FunctionCode.h | 2 + papyrus-vm/include/papyrus-vm/FunctionInfo.h | 1 + papyrus-vm/include/papyrus-vm/Object.h | 1 + .../src/papyrus-vm-lib/ActivePexInstance.cpp | 84 +- papyrus-vm/src/papyrus-vm-lib/VarValue.cpp | 6 + .../src/papyrus-vm-lib/VirtualMachine.cpp | 46 ++ skymp5-server/cpp/CMakeLists.txt | 2 +- skymp5-server/cpp/addon/ScampServer.cpp | 28 +- .../cpp/addon/ScampServerListener.cpp | 3 + .../LocationalDataBinding.cpp | 11 + .../cpp/server_guest_lib/ActionListener.cpp | 17 + .../cpp/server_guest_lib/EvaluateTemplate.h | 40 +- .../cpp/server_guest_lib/MpActor.cpp | 8 + skymp5-server/cpp/server_guest_lib/MpForm.cpp | 17 + .../server_guest_lib/MpObjectReference.cpp | 69 +- .../cpp/server_guest_lib/MpObjectReference.h | 2 +- .../cpp/server_guest_lib/PartOne.cpp | 31 + .../ScriptVariablesHolder.cpp | 8 + .../server_guest_lib/SpSnippetFunctionGen.cpp | 3 + .../cpp/server_guest_lib/WorldState.cpp | 68 +- .../database_drivers/MongoDatabase.cpp | 3 + .../formulas/SweetPieDamageFormula.cpp | 3 + .../save_storages/AsyncSaveStorage.cpp | 11 +- .../script_classes/PapyrusActor.cpp | 35 +- .../script_objects/EspmGameObject.cpp | 34 +- skymp5-server/ts/settings.ts | 1 + vcpkg.json | 1 + 62 files changed, 2587 insertions(+), 62 deletions(-) create mode 100644 aaa-playground/CMakeLists.txt create mode 100644 aaa-playground/cmakeproj.cmake create mode 100644 aaa-playground/src/main.cpp create mode 100644 antigo/CMakeLists.txt create mode 100644 antigo/LICENSE create mode 100644 antigo/cmakeproj.cmake create mode 100644 antigo/include/antigo/Context.h create mode 100644 antigo/include/antigo/ExecutionData.h create mode 100644 antigo/include/antigo/ResolvedContext.h create mode 100644 antigo/include/antigo/impl/complex_values.h create mode 100644 antigo/include/antigo/impl/test_helpers.h create mode 100644 antigo/include/antigo/third_party/fast_pimpl.hpp create mode 100644 antigo/include_stub/antigo/Context.h create mode 100644 antigo/include_stub/antigo/ExecutionData.h create mode 100644 antigo/include_stub/antigo/ResolvedContext.h create mode 100644 antigo/include_stub/antigo/impl/complex_values.h create mode 100644 antigo/src/antigo/Context.cpp create mode 100644 antigo/src/antigo/ResolvedContext.cpp create mode 100644 antigo/src/antigo/impl/ExecutionData.cpp create mode 100644 antigo/src/antigo/impl/ExecutionData.h create mode 100644 antigo/src/antigo/impl/OnstackContextImpl.cpp create mode 100644 antigo/src/antigo/impl/OnstackContextImpl.h create mode 100644 antigo/src_stub/antigo_stub.cpp create mode 100644 antigo/unit/CMakeLists.txt create mode 100644 antigo/unit/src/ContextTest.cpp create mode 100644 antigo/unit/src/main.cpp diff --git a/.github/actions/pr_base/action.yml b/.github/actions/pr_base/action.yml index 564650b545..85ca66115d 100644 --- a/.github/actions/pr_base/action.yml +++ b/.github/actions/pr_base/action.yml @@ -32,6 +32,10 @@ inputs: description: "Set to true if building for Emscripten" required: false default: "false" + WITH_ANTIGO: + description: build antigo + required: false + default: '0' runs: using: composite steps: @@ -197,6 +201,7 @@ runs: $cmake_command_full += "$vcpkg_chainload_toolchain_arg " $cmake_command_full += "$vcpkg_triplet_arg " $cmake_command_full += "$vcpkg_root_arg " + $cmake_command_full += "-DWITH_ANTIGO=${{env.WITH_ANTIGO}} " $cmake_command_full += "-DUNIT_DATA_DIR=skyrim_data_files " $cmake_command_full += "-DBUILD_NODEJS=OFF " $cmake_command_full += "$prepare_nexus_archives_arg " diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2ed86f09ad..e6a9a1e569 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -197,6 +197,7 @@ jobs: run: | cd /src \ && ./build.sh --configure \ + -DWITH_ANTIGO=1 \ -DBUILD_UNIT_TESTS=OFF \ -DBUILD_GAMEMODE=ON \ -DOFFLINE_MODE=OFF \ @@ -219,7 +220,7 @@ jobs: -u skymp run: | cd /src \ - && ./build.sh --build + && ./build.sh --build --target=skymp5-server - name: Deploy env: diff --git a/.github/workflows/pr-linux-variants.yml b/.github/workflows/pr-linux-variants.yml index 7d3f03b31c..1f74f3dd0b 100644 --- a/.github/workflows/pr-linux-variants.yml +++ b/.github/workflows/pr-linux-variants.yml @@ -172,6 +172,7 @@ jobs: run: | cd /src \ && ./build.sh --configure \ + -DWITH_ANTIGO=1 \ -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ -DUNIT_DATA_DIR="/src/skyrim_data_files" diff --git a/aaa-playground/CMakeLists.txt b/aaa-playground/CMakeLists.txt new file mode 100644 index 0000000000..93c793f549 --- /dev/null +++ b/aaa-playground/CMakeLists.txt @@ -0,0 +1,10 @@ +include(${CMAKE_SOURCE_DIR}/cmake/apply_default_settings.cmake) + +file(GLOB_RECURSE aaa_playground_sources "${CMAKE_CURRENT_SOURCE_DIR}/src/*")# "${CMAKE_CURRENT_SOURCE_DIR}/include/*") +add_executable(aaa_playground ${aaa_playground_sources}) +target_include_directories(aaa_playground PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") +apply_default_settings(TARGETS aaa_playground) + + +find_package(cpptrace CONFIG REQUIRED) +target_link_libraries(aaa_playground PRIVATE cpptrace::cpptrace antigo) diff --git a/aaa-playground/cmakeproj.cmake b/aaa-playground/cmakeproj.cmake new file mode 100644 index 0000000000..1fc6aa1fde --- /dev/null +++ b/aaa-playground/cmakeproj.cmake @@ -0,0 +1,2 @@ +list(APPEND CMAKEPROJ_PROJECTS aaa_playground) +set(CMAKEPROJ_PRIORITY_aaa_playground 0) diff --git a/aaa-playground/src/main.cpp b/aaa-playground/src/main.cpp new file mode 100644 index 0000000000..9937c3a5b3 --- /dev/null +++ b/aaa-playground/src/main.cpp @@ -0,0 +1,82 @@ +#include +#include +#include + +// #include +#include + +#include "antigo/Context.h" + +void test3(int x) +{ + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddMessage("checking"); + // ctx.AddMessage(x); + if (x == 7) { + ctx.AddMessage("matched to throw"); + throw std::runtime_error("baibai"); + } +} + +void test2(int start) +{ + ANTIGO_CONTEXT_INIT(ctx); + for (int i = start; i < 10; ++i) { + ctx.AddMessage("hi from loop"); // discouraged + test3(i); + } +} + +void test() +{ + ANTIGO_CONTEXT_INIT(ctx); + std::cout << "hello test\n"; + test2(3); + std::cout << "goodbye test\n"; +} + +int main() +{ + // ANTIGO_CONTEXT_INIT(ctx); + std::cout << "hello world\n"; + + try { + throw std::runtime_error("kek1"); + } catch (const std::exception& e) { + std::cout << "caught " << e.what() << "\n"; + std::cout << "uncaught_exceptions: " << std::uncaught_exceptions() << "\n"; + try { + throw std::runtime_error("kek1"); + } catch (const std::exception& e) { + std::cout << "caught " << e.what() << "\n"; + std::cout << "uncaught_exceptions: " << std::uncaught_exceptions() << "\n"; + } + std::cout << "uncaught_exceptions: " << std::uncaught_exceptions() << "\n"; + // std::cout << "stacktrace :" << "\n"; + // // cpptrace::from_current_exception().print(); + // for (size_t i = 0; Antigo::HasExceptionWitness(); ++i) { + // std::cerr << "Exception witness #" << i << "\n"; + // Antigo::PopExceptionWitness().PrintTrace(); + // } + } + + /* + try { + ::cpptrace ::detail ::try_canary cpptrace_try_canary; + try { + { + test(); + } + } catch (::cpptrace ::detail ::unwind_interceptor&) { + } + } catch (const std ::exception& e) { + std::cout << "caught " << e.what() << "\n"; + std::cout << "stacktrace :" << "\n"; + cpptrace::from_current_exception().print(); + for (size_t i = 0; Antigo::HasExceptionWitness(); ++i) { + std::cerr << "Exception witness #" << i << "\n"; + Antigo::PopExceptionWitness().PrintTrace(); + } + } + */ +} diff --git a/antigo/CMakeLists.txt b/antigo/CMakeLists.txt new file mode 100644 index 0000000000..ff4705fa41 --- /dev/null +++ b/antigo/CMakeLists.txt @@ -0,0 +1,26 @@ +include(${CMAKE_SOURCE_DIR}/cmake/apply_default_settings.cmake) + +if(WITH_ANTIGO) + file(GLOB_RECURSE antigo_sources "${CMAKE_CURRENT_SOURCE_DIR}/src/*" "${CMAKE_CURRENT_SOURCE_DIR}/include/*") + add_library(antigo STATIC ${antigo_sources}) + target_include_directories(antigo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") + target_include_directories(antigo PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") + apply_default_settings(TARGETS antigo) + + find_package(cpptrace CONFIG REQUIRED) + target_link_libraries(antigo PRIVATE cpptrace::cpptrace) + + # XXX remove me + find_package(spdlog CONFIG REQUIRED) + target_link_libraries(antigo PUBLIC spdlog::spdlog) + + add_subdirectory(unit) +else() + file(GLOB_RECURSE antigo_sources "${CMAKE_CURRENT_SOURCE_DIR}/src_stub/*" "${CMAKE_CURRENT_SOURCE_DIR}/include_stub/*") + add_library(antigo STATIC ${antigo_sources}) + target_include_directories(antigo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src_stub") + target_include_directories(antigo PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include_stub") + apply_default_settings(TARGETS antigo) + + set_target_properties(antigo PROPERTIES LINKER_LANGUAGE CXX) +endif() diff --git a/antigo/LICENSE b/antigo/LICENSE new file mode 100644 index 0000000000..0a041280bd --- /dev/null +++ b/antigo/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/antigo/cmakeproj.cmake b/antigo/cmakeproj.cmake new file mode 100644 index 0000000000..9fc178a620 --- /dev/null +++ b/antigo/cmakeproj.cmake @@ -0,0 +1,2 @@ +list(APPEND CMAKEPROJ_PROJECTS antigo) +set(CMAKEPROJ_PRIORITY_antigo 0) diff --git a/antigo/include/antigo/Context.h b/antigo/include/antigo/Context.h new file mode 100644 index 0000000000..cf7eb00da1 --- /dev/null +++ b/antigo/include/antigo/Context.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +#include "third_party/fast_pimpl.hpp" + +#include "antigo/impl/complex_values.h" + +namespace Antigo { + +class ResolvedContext; + +class OnstackContextImpl; + +class OnstackContext +{ +public: + OnstackContext(const char* filename_, size_t linenum_, const char* funcname_); + ~OnstackContext(); + + OnstackContext(OnstackContext&& other) = delete; + OnstackContext(const OnstackContext& other) = delete; + OnstackContext& operator=(OnstackContext&& other) = delete; + OnstackContext& operator=(const OnstackContext& other) = delete; + + void AddMessage(const char* message); + + void AddPtr(const void* ptr); + void AddUnsigned(uint64_t val); + void AddSigned(int64_t val); + + template + void AddPtr(const std::shared_ptr& ptr) { + AddPtr(ptr.get()); + } + + void AddLambdaWithOwned(std::function printerFunc); + [[nodiscard]] impl::ReferencedValueGuard AddLambdaWithRef(std::function printerFunc); + + [[nodiscard]] ResolvedContext Resolve() const; + void Orphan() const; + + // XXX: move to some common headedr? + constexpr static size_t kSize = 1024; + +private: + third_party::userver::utils::FastPimpl pImpl; + + friend ResolvedContext; +}; + +#define ANTIGO_CONTEXT_INIT(ctx) ::Antigo::OnstackContext ctx(__FILE__, __LINE__, __func__) + +} // namespace Antigo diff --git a/antigo/include/antigo/ExecutionData.h b/antigo/include/antigo/ExecutionData.h new file mode 100644 index 0000000000..dbce9f77c9 --- /dev/null +++ b/antigo/include/antigo/ExecutionData.h @@ -0,0 +1,16 @@ +#pragma once + +#include "antigo/Context.h" + +namespace Antigo { + +struct ExecutionData; +ExecutionData& GetCurrentExecutionData(); + +bool HasExceptionWitness(); +ResolvedContext PopExceptionWitness(); + +bool HasExceptionWitnessOrphan(); +ResolvedContext PopExceptionWitnessOrphan(); + +} diff --git a/antigo/include/antigo/ResolvedContext.h b/antigo/include/antigo/ResolvedContext.h new file mode 100644 index 0000000000..b0dfeacefb --- /dev/null +++ b/antigo/include/antigo/ResolvedContext.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace Antigo { + +struct SourceLocationEntry { + const char* filename; + uint16_t line; + const char* func; +}; + +struct ResolvedMessageEntry { + std::string type; + std::string strval; +}; + +struct ResolvedContextEntry { + SourceLocationEntry sourceLoc; + std::vector messages; // 20241218 1 common buffer (for each thread) +}; + +struct ResolvedContext { + std::vector entries; + std::string reason; + + std::string ToString() const; + void Print() const; +}; + +std::ostream& operator<<(std::ostream& os, const ResolvedContext& self); + +} diff --git a/antigo/include/antigo/impl/complex_values.h b/antigo/include/antigo/impl/complex_values.h new file mode 100644 index 0000000000..48854b80db --- /dev/null +++ b/antigo/include/antigo/impl/complex_values.h @@ -0,0 +1,34 @@ + +#pragma once + +#include +#include + +namespace Antigo::impl { + +struct ReferencedValueGuard; + +struct ReferencedValue { + const ReferencedValueGuard* guard; // set nullptr on expiry +}; + +struct ReferencedValueGuard { + ReferencedValue* ctxEntry; + std::function printerFunc; + + void Arm() { + if (!ctxEntry) { + return; + } + ctxEntry->guard = this; + } + + ~ReferencedValueGuard() { + if (!ctxEntry) { + return; + } + ctxEntry->guard = nullptr; + } +}; + +} diff --git a/antigo/include/antigo/impl/test_helpers.h b/antigo/include/antigo/impl/test_helpers.h new file mode 100644 index 0000000000..2d7a58d32b --- /dev/null +++ b/antigo/include/antigo/impl/test_helpers.h @@ -0,0 +1,6 @@ +#pragma once + +namespace Antigo::impl { +// XXX: maybe test should either be able to impot from src dir or idk +bool HasCleanState(); +} diff --git a/antigo/include/antigo/third_party/fast_pimpl.hpp b/antigo/include/antigo/third_party/fast_pimpl.hpp new file mode 100644 index 0000000000..5b7e8226f4 --- /dev/null +++ b/antigo/include/antigo/third_party/fast_pimpl.hpp @@ -0,0 +1,124 @@ +/** + * Copyright 2024 YANDEX LLC + * Copyright 2024 SkyMP contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This file is copied from the usever framework released by YANDEX LLC under Apache-2.0 license +// Original code can be found here: +// https://github.com/userver-framework/userver/blob/develop/universal/include/userver/utils/fast_pimpl.hpp + +// Documentation for this class can be found at: +// https://userver.tech/d8/d4b/classutils_1_1FastPimpl.html + +#pragma once + +#include +// #include +// #include +#include + +namespace third_party::userver::utils { + +/// @brief Helper constant to use with FastPimpl +inline constexpr bool kStrictMatch = true; + +/// @ingroup userver_universal userver_containers +/// +/// @brief Implements pimpl idiom without dynamic memory allocation. +/// +/// FastPimpl doesn't require either memory allocation or indirect memory +/// access. But you have to manually set object size when you instantiate +/// FastPimpl. +/// +/// ## Example usage: +/// Take your class with pimpl via smart pointer and +/// replace the smart pointer with utils::FastPimpl +/// @snippet utils/widget_fast_pimpl_test.hpp FastPimpl - header +/// +/// If the Size and Alignment are unknown - just put a random ones and +/// the compiler would show the right ones in the error message: +/// @code +/// In instantiation of 'void FastPimpl::Validate() +/// [with int ActualSize = 1; int ActualAlignment = 8; T = sample::Widget; +/// int Size = 8; int Alignment = 8]' +/// @endcode +/// +/// Change the initialization in source file to not allocate for pimpl +/// @snippet utils/widget_fast_pimpl_test.cpp FastPimpl - source +/// +/// Done! Now you can use the header without exposing the implementation +/// details: +/// @snippet utils/fast_pimpl_test.cpp FastPimpl - usage +template +class FastPimpl final { +public: + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init,performance-noexcept-move-constructor) + FastPimpl(FastPimpl&& v) noexcept(noexcept(T(std::declval()))) : FastPimpl(std::move(*v)) {} + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) + FastPimpl(const FastPimpl& v) noexcept(noexcept(T(std::declval()))) : FastPimpl(*v) {} + + // NOLINTNEXTLINE(bugprone-unhandled-self-assignment,cert-oop54-cpp) + FastPimpl& operator=(const FastPimpl& rhs) noexcept(noexcept(std::declval() = std::declval())) { + *AsHeld() = *rhs; + return *this; + } + + FastPimpl& operator=(FastPimpl&& rhs) noexcept( + // NOLINTNEXTLINE(performance-noexcept-move-constructor) + noexcept(std::declval() = std::declval()) + ) { + *AsHeld() = std::move(*rhs); + return *this; + } + + template + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) + explicit FastPimpl(Args&&... args) noexcept(noexcept(T(std::declval()...))) { + ::new (AsHeld()) T(std::forward(args)...); + } + + T* operator->() noexcept { return AsHeld(); } + + const T* operator->() const noexcept { return AsHeld(); } + + T& operator*() noexcept { return *AsHeld(); } + + const T& operator*() const noexcept { return *AsHeld(); } + + ~FastPimpl() noexcept { + Validate(); + AsHeld()->~T(); + } + +private: + // Use a template to make actual sizes visible in the compiler error message. + template + static void Validate() noexcept { + static_assert(Size >= ActualSize, "invalid Size: Size >= sizeof(T) failed"); + static_assert(!Strict || Size == ActualSize, "invalid Size: Size == sizeof(T) failed"); + + static_assert(Alignment % ActualAlignment == 0, "invalid Alignment: Alignment % alignof(T) == 0 failed"); + static_assert(!Strict || Alignment == ActualAlignment, "invalid Alignment: Alignment == alignof(T) failed"); + } + + alignas(Alignment) std::byte storage_[Size]; + + T* AsHeld() noexcept { return reinterpret_cast(&storage_); } + + const T* AsHeld() const noexcept { return reinterpret_cast(&storage_); } +}; + +} // namespace third_party::userver::utils diff --git a/antigo/include_stub/antigo/Context.h b/antigo/include_stub/antigo/Context.h new file mode 100644 index 0000000000..aa8d4c7426 --- /dev/null +++ b/antigo/include_stub/antigo/Context.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include "antigo/ResolvedContext.h" +#include "antigo/impl/complex_values.h" + +namespace Antigo { +struct ResolvedContext; + +class OnstackContext +{ +public: + OnstackContext(const char* filename_, size_t linenum_, const char* funcname_) {} + ~OnstackContext() {} + + OnstackContext(OnstackContext&& other) = delete; + OnstackContext(const OnstackContext& other) = delete; + OnstackContext& operator=(OnstackContext&& other) = delete; + OnstackContext& operator=(const OnstackContext& other) = delete; + + void AddMessage(const char* message) {} + + void AddPtr(const void* ptr) {} + void AddUnsigned(uint64_t val) {} + void AddSigned(int64_t val) {} + + template + void AddPtr(const std::shared_ptr& ptr) { + AddPtr(ptr.get()); + } + + void AddLambdaWithOwned(std::function printerFunc) {} + [[nodiscard]] impl::ReferencedValueGuard AddLambdaWithRef(std::function printerFunc) { return {}; } + + [[nodiscard]] ResolvedContext Resolve() const { return {}; } + void Orphan() const {} +}; + +#define ANTIGO_CONTEXT_INIT(ctx) ::Antigo::OnstackContext ctx(__FILE__, __LINE__, __func__) + +} // namespace Antigo diff --git a/antigo/include_stub/antigo/ExecutionData.h b/antigo/include_stub/antigo/ExecutionData.h new file mode 100644 index 0000000000..dbce9f77c9 --- /dev/null +++ b/antigo/include_stub/antigo/ExecutionData.h @@ -0,0 +1,16 @@ +#pragma once + +#include "antigo/Context.h" + +namespace Antigo { + +struct ExecutionData; +ExecutionData& GetCurrentExecutionData(); + +bool HasExceptionWitness(); +ResolvedContext PopExceptionWitness(); + +bool HasExceptionWitnessOrphan(); +ResolvedContext PopExceptionWitnessOrphan(); + +} diff --git a/antigo/include_stub/antigo/ResolvedContext.h b/antigo/include_stub/antigo/ResolvedContext.h new file mode 100644 index 0000000000..d850883885 --- /dev/null +++ b/antigo/include_stub/antigo/ResolvedContext.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace Antigo { + +struct SourceLocationEntry { + const char* filename; + uint16_t line; + const char* func; +}; + +struct ResolvedMessageEntry { + std::string type; + std::string strval; +}; + +struct ResolvedContextEntry { + SourceLocationEntry sourceLoc; + std::vector messages; +}; + +struct ResolvedContext { + std::vector entries; + std::string reason; + + std::string ToString() const; + void Print() const; + + friend std::ostream& operator<<(std::ostream& os, const ResolvedContext& self) { return os; } +}; + +} diff --git a/antigo/include_stub/antigo/impl/complex_values.h b/antigo/include_stub/antigo/impl/complex_values.h new file mode 100644 index 0000000000..fb35ab4f67 --- /dev/null +++ b/antigo/include_stub/antigo/impl/complex_values.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace Antigo::impl { + +struct ReferencedValueGuard; + +struct ReferencedValue { + const ReferencedValueGuard* guard; // set nullptr on expiry +}; + +struct ReferencedValueGuard { + ReferencedValue* ctxEntry; + std::function printerFunc; + + void Arm() { + if (!ctxEntry) { + return; + } + ctxEntry->guard = this; + } + + ~ReferencedValueGuard() { + if (!ctxEntry) { + return; + } + ctxEntry->guard = nullptr; + } +}; + +} diff --git a/antigo/src/antigo/Context.cpp b/antigo/src/antigo/Context.cpp new file mode 100644 index 0000000000..e81a70711d --- /dev/null +++ b/antigo/src/antigo/Context.cpp @@ -0,0 +1,30 @@ +#include "antigo/Context.h" + +#include +#include +#include + +#include + +#include "antigo/impl/complex_values.h" +#include "antigo/impl/ExecutionData.h" +#include "impl/OnstackContextImpl.h" + +namespace Antigo { + +OnstackContext::OnstackContext(const char* filename_, size_t linenum_, const char* funcname_) : pImpl(filename_, linenum_, funcname_) {} +OnstackContext::~OnstackContext() = default; + +void OnstackContext::AddMessage(const char* message) { return pImpl->AddMessage(message); } + +void OnstackContext::AddPtr(const void* ptr) { return pImpl->AddPtr(ptr); } +void OnstackContext::AddUnsigned(uint64_t val) { return pImpl->AddUnsigned(val); } +void OnstackContext::AddSigned(int64_t val) { return pImpl->AddSigned(val); } + +void OnstackContext::AddLambdaWithOwned(std::function printerFunc) { return pImpl->AddLambdaWithOwned(std::move(printerFunc)); } +impl::ReferencedValueGuard OnstackContext::AddLambdaWithRef(std::function printerFunc) { return pImpl->AddLambdaWithRef(std::move(printerFunc)); } + +ResolvedContext OnstackContext::Resolve() const { return pImpl->Resolve(); } +void OnstackContext::Orphan() const { return pImpl->Orphan(); } + +} // namespace Antigo diff --git a/antigo/src/antigo/ResolvedContext.cpp b/antigo/src/antigo/ResolvedContext.cpp new file mode 100644 index 0000000000..35aabf1a54 --- /dev/null +++ b/antigo/src/antigo/ResolvedContext.cpp @@ -0,0 +1,81 @@ +#include "antigo/ResolvedContext.h" + +#include +#include +#include +#include + +namespace Antigo { + +namespace { + +struct ApplyIndent { + const std::string& s; + // size_t indent; + const std::string indentStr; +}; + +std::ostream& operator<<(std::ostream& os, const ApplyIndent& rhs) { + std::string_view s = rhs.s; + // std::string indentStr(rhs.indent, ' '); + + size_t pos = 0; + while (pos < s.size()) { + size_t next = s.find('\n', pos); + next = next == std::string::npos ? s.size() : next + 1; + assert(next > pos); + + if (pos != 0) { + os << rhs.indentStr; + } + os << s.substr(pos, next - pos); + pos = next; + } + + return os; +} + +} + +std::string ResolvedContext::ToString() const { + std::stringstream ss; + ss << *this; + if (!ss.good()) { + return "stringstream error!"; + } + return std::move(ss).str(); +} + +void ResolvedContext::Print() const { + std::cout << *this << "\n"; +} + +std::ostream& operator<<(std::ostream& os, const ResolvedContext& self) { + os << "resolved context with " << self.entries.size() << " entries (reason=" << self.reason << "):\n"; + for (size_t entryIdx = 0; entryIdx < self.entries.size(); ++entryIdx) { + const auto& entry = self.entries[entryIdx]; + if (!entry.messages.empty()) { + os << " ╭--------\n"; + } + for (size_t msgIdx = entry.messages.size(); msgIdx > 0; --msgIdx) { + const auto& msg = entry.messages[msgIdx - 1]; + os << " | (" << msg.type << ") " << ApplyIndent{msg.strval, " " + std::string(msg.type.size(), ' ') + " "} << "\n"; + } + // os << "> " << entry.sourceLoc.func << "\n"; + if (!entry.messages.empty()) { + os << " ╰-- "; + } else { + os << " o-- "; + } + os << entry.sourceLoc.filename << ":" << entry.sourceLoc.line << " - " << entry.sourceLoc.func; + if (entryIdx + 1 != self.entries.size()) { + os << '\n'; + } + } + if (self.entries.empty()) { + os << "(empty)"; + } + return os; +} + +} diff --git a/antigo/src/antigo/impl/ExecutionData.cpp b/antigo/src/antigo/impl/ExecutionData.cpp new file mode 100644 index 0000000000..132e0b230f --- /dev/null +++ b/antigo/src/antigo/impl/ExecutionData.cpp @@ -0,0 +1,42 @@ +#include "antigo/impl/ExecutionData.h" + +// XXX rename namespace, move out of impl? +namespace Antigo { + +class OnstackContextImpl; + +ExecutionData& GetCurrentExecutionData() { + thread_local ExecutionData data; + return data; +} + +bool HasExceptionWitness() { + return !GetCurrentExecutionData().errorWitnesses.empty(); +} + +ResolvedContext PopExceptionWitness() { + auto w = std::move(GetCurrentExecutionData().errorWitnesses.back()); + GetCurrentExecutionData().errorWitnesses.pop_back(); + return w; +} + +bool HasExceptionWitnessOrphan() +{ + return GetCurrentExecutionData().orphans.size(); +} + +ResolvedContext PopExceptionWitnessOrphan() +{ + auto w = std::move(GetCurrentExecutionData().orphans.back()); + GetCurrentExecutionData().orphans.pop_back(); + return w; +} + +namespace impl { +bool HasCleanState() { + auto& d = GetCurrentExecutionData(); + return d.errorWitnesses.empty() && d.orphans.empty() && d.stackCtxChain.empty(); +} +} + +} diff --git a/antigo/src/antigo/impl/ExecutionData.h b/antigo/src/antigo/impl/ExecutionData.h new file mode 100644 index 0000000000..681ea0cae5 --- /dev/null +++ b/antigo/src/antigo/impl/ExecutionData.h @@ -0,0 +1,22 @@ +#pragma once + +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" +#include + +namespace Antigo { + +class OnstackContextImpl; + +struct ExecutionData +{ + std::vector stackCtxChain; + std::vector errorWitnesses; + std::vector orphans; + + std::size_t ticker = 0; +}; + +ExecutionData& GetCurrentExecutionData(); + +} diff --git a/antigo/src/antigo/impl/OnstackContextImpl.cpp b/antigo/src/antigo/impl/OnstackContextImpl.cpp new file mode 100644 index 0000000000..906a6a5897 --- /dev/null +++ b/antigo/src/antigo/impl/OnstackContextImpl.cpp @@ -0,0 +1,139 @@ +#include + +#include +#include +#include +#include + +#include "antigo/Context.h" +#include "antigo/impl/OnstackContextImpl.h" +#include "antigo/impl/ExecutionData.h" + +#ifndef WIN32 +#define ANTIGO_TRY CPPTRACE_TRY +#define ANTIGO_CATCH CPPTRACE_CATCH +#else +#define ANTIGO_TRY try +#define ANTIGO_CATCH catch +#endif + +namespace Antigo { + +OnstackContextImpl::OnstackContextImpl(const char* filename_, size_t linenum_, const char* funcname_): h{}, dataFrames{} { + h.filename = filename_; + h.linenum = std::min(linenum_, std::numeric_limits::max()); + h.dataFramesCnt = 0; + h.skippedDataFramesCnt = 0; + h.destructing = false; + h.errorOnTop = false; + h.funcname = funcname_; + + h.downCtx = nullptr; + if (GetCurrentExecutionData().stackCtxChain.size()) { + assert(GetCurrentExecutionData().stackCtxChain.back()); + h.downCtx = GetCurrentExecutionData().stackCtxChain.back(); + } + + // h.rawTrace = cpptrace::generate_raw_trace(); // XXX disable for emscripten (can't build) + h.uncaughtExceptions = std::uncaught_exceptions(); + + if (h.uncaughtExceptions) { + AddMessage("ctx: has uncaught exceptions; count="); + AddUnsigned(h.uncaughtExceptions); + } + + // 20241227: if debug include message thread id + + GetCurrentExecutionData().stackCtxChain.push_back(this); + + // 20241218 1: push_back & pop_back for reserve storage and noexcept? + // next step: common buffer for prepared message frames? + // exception safety: cancel adding in case of error; also skip all dtor logic in that case + + // 20241227 evg: self-trace - писать в цикличный буфер в таком же +- формате в локальный сторадж потока + в себя же +} + +OnstackContextImpl::~OnstackContextImpl() { + ANTIGO_TRY { + h.destructing = true; + + if (!(++GetCurrentExecutionData().ticker & 0xffff)) { + GetCurrentExecutionData().orphans.emplace_back(ResolveCtxStackImpl("ticker")); + } + + assert(GetCurrentExecutionData().stackCtxChain.size() && GetCurrentExecutionData().stackCtxChain.back() == this); + if (std::uncaught_exceptions() != h.uncaughtExceptions || (h.downCtx && h.downCtx->h.uncaughtExceptions != h.uncaughtExceptions)) { + auto& w = GetCurrentExecutionData().errorWitnesses; + if (!h.errorOnTop) { + // XXX 20250112 1508 related: условие здесь должно как-то учитывать, что мы могли провалиться сверху с другим слоем эксепшена. или пофиг?) + ANTIGO_TRY { + w.emplace_back(ResolveCtxStackImpl("exception")); + } ANTIGO_CATCH (const std::exception& e) { + cpptrace::from_current_exception().print(); + } + } + if (h.downCtx == nullptr) { + while (!w.empty()) { + GetCurrentExecutionData().orphans.push_back(std::move(w.back())); + w.pop_back(); + } + } else { + h.downCtx->h.errorOnTop = true; + } + } else { + auto& w = GetCurrentExecutionData().errorWitnesses; + while (!w.empty()) { + GetCurrentExecutionData().orphans.push_back(std::move(w.back())); + w.pop_back(); + } + } + GetCurrentExecutionData().stackCtxChain.pop_back(); + } ANTIGO_CATCH (const std::exception& e) { + std::cerr << "fatal exception, what: " << e.what() << "\n"; + cpptrace::from_current_exception().print(); + std::terminate(); + // terminating here on purpose, our state could get inconsistent + } +} + +void OnstackContextImpl::ResolveCurrentImpl(ResolvedContext& to) const { + auto& entry = to.entries.emplace_back(); + entry.sourceLoc.filename = h.filename; + entry.sourceLoc.line = h.linenum; + entry.sourceLoc.func = h.funcname; + + for (size_t i = 0; i < h.dataFramesCnt; ++i) { + entry.messages.emplace_back(dataFrames[i].Resolve()); + } + + if (h.skippedDataFramesCnt) { + std::string msg = std::to_string(h.skippedDataFramesCnt); + if (h.skippedDataFramesCnt == std::numeric_limits::max()) { + msg += "+"; + } + msg += " last messages didn't fit into buffer"; + entry.messages.push_back(ResolvedMessageEntry{"meta", std::move(msg)}); + } +} + +ResolvedContext OnstackContextImpl::ResolveCtxStackImpl(std::string reason) const { + ResolvedContext result; + result.reason = std::move(reason); + const auto& chain = GetCurrentExecutionData().stackCtxChain; + for (size_t i = chain.size(); i > 0; --i) { + chain[i - 1]->ResolveCurrentImpl(result); + } + if (!result.entries.empty()) { + result.entries[0].messages.push_back({"stacktrace", cpptrace::generate_trace().to_string()}); + } + return result; +} + +ResolvedContext OnstackContextImpl::Resolve() const { + return ResolveCtxStackImpl("ondemand"); +} + +void OnstackContextImpl::Orphan() const { + GetCurrentExecutionData().orphans.emplace_back(Resolve()); +} +} diff --git a/antigo/src/antigo/impl/OnstackContextImpl.h b/antigo/src/antigo/impl/OnstackContextImpl.h new file mode 100644 index 0000000000..f0b8f3e980 --- /dev/null +++ b/antigo/src/antigo/impl/OnstackContextImpl.h @@ -0,0 +1,226 @@ +#pragma once + +#include "antigo/Context.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "antigo/impl/complex_values.h" +#include "antigo/ResolvedContext.h" + +#ifndef WIN32 +#define ANTIGO_TRY CPPTRACE_TRY +#define ANTIGO_CATCH CPPTRACE_CATCH +#else +#define ANTIGO_TRY try +#define ANTIGO_CATCH catch +#endif + +namespace Antigo { + +namespace { + +struct OwnedValue { + std::function printerFunc; +}; + +std::string Strip(const std::string& s) { + size_t len = s.length(); + while (len > 0 && s[len - 1] == '\n') { + --len; + } + return s.substr(0, len); +} + +// XXX: should be separate for each size +// (uint) uint: 1736811162163836790 | int: 1736811162163836790 | hex: 0x181a648ccab57776 | bin: 0001100000011010011001001000110011001010101101010111011101110110 +std::string IntRepresentations(uint64_t val) { + std::bitset<64> binaryVal(val); + + return fmt::format( + "uint: {} | int: {} | hex: 0x{:x} | bin: {}", + val, + static_cast(val), + val, + binaryVal.to_string() + ); +} + +} // namespace + +struct OnstackDataFrame; + +struct OnstackDataFrame { + using ValueType = std::variant, impl::ReferencedValue>; + ValueType value; + + struct ToPreparedVisitor { + ResolvedMessageEntry operator()(const char* v) const { + return {"cstr", v}; + } + + ResolvedMessageEntry operator()(uint64_t v) const { + return {"uint", IntRepresentations(v)}; + } + + ResolvedMessageEntry operator()(int64_t v) const { + return {"int", IntRepresentations(static_cast(v))}; + } + + ResolvedMessageEntry operator()(const std::unique_ptr& v) const { + return {"custom", Strip(v->printerFunc())}; + } + + ResolvedMessageEntry operator()(const impl::ReferencedValue& v) const { + if (v.guard == nullptr) { + return {"custom", "(reference possibly expired or Arm() not called)"}; + } + return {"custom", Strip(v.guard->printerFunc())}; + } + }; + + ResolvedMessageEntry Resolve() const { + ANTIGO_TRY { + return std::visit(ToPreparedVisitor{}, value); + } ANTIGO_CATCH (const std::exception& e) { + return {"error", "ctx: error while resolving value, variant index " + std::to_string(value.index()) + "\nwhat: " + e.what() + '\n' + cpptrace::from_current_exception().to_string()}; + } + return {"error", "resolve error: this code should be unreachable?"}; + } +}; +static_assert(sizeof(OnstackDataFrame) == 16); + +class OnstackContextImpl { +public: + OnstackContextImpl(const char* filename_, size_t linenum_, const char* funcname_); + + ~OnstackContextImpl(); + + void ResolveCurrentImpl(ResolvedContext& to) const; + + [[nodiscard]] + ResolvedContext ResolveCtxStackImpl(std::string reason) const; + + [[nodiscard]] + OnstackDataFrame* TryEmplaceFrame() { + if (h.dataFramesCnt == kMaxDataFrameCnt) { + static_assert(255 == std::numeric_limits::max()); + if (h.skippedDataFramesCnt != 255) { + ++h.skippedDataFramesCnt; + } + return nullptr; + } + return &dataFrames[h.dataFramesCnt++]; + } + + template + T* TryEmplaceFrameValue() { + // XXX move impl here? + if (auto* frame = TryEmplaceFrame()) { + return &frame->value.emplace(); + } + return nullptr; + } + + // XXX maybe add a check that this message was precompiled? + void AddMessage(const char* message) { + if (auto* frame = TryEmplaceFrame()) { + frame->value = message; + } + } + + void AddPtr(const void* ptr) { + static_assert(sizeof(const void*) == sizeof(uint64_t)); + AddUnsigned(reinterpret_cast(ptr)); + } + + void AddUnsigned(uint64_t val) { + if (auto* frame = TryEmplaceFrame()) { + frame->value = val; + } + } + + void AddSigned(int64_t val) { + if (auto* frame = TryEmplaceFrame()) { + frame->value = val; + } + } + + // XXX как-то так переименовать, чтобы было понятно, что это либо Owned, либо лайфтайм нормальный + void AddLambdaWithOwned(std::function printerFunc) { + if (auto* frame = TryEmplaceFrame()) { + frame->value = std::make_unique(OwnedValue{printerFunc}); + } + } + + [[nodiscard]] impl::ReferencedValueGuard AddLambdaWithRef(std::function printerFunc) { + if (auto* frame = TryEmplaceFrame()) { + auto& value = frame->value.emplace(impl::ReferencedValue{/*guard=*/nullptr}); // to be filled via Arm() + return {&value, std::move(printerFunc)}; + } + return {}; + } + + ResolvedContext Resolve() const; + void Orphan() const; + + // TODO 20241219 2150 + // auto g = ctx.AddMessageGuard(message); + + // for T message: + // ContextStringify(message, indent) + // func + + // XXX: move to some common headedr? + constexpr static size_t kSize = 1024; + +private: + // header, compactly stores some essential data + struct { + // location data + // (also frame cnt bc needs to be tightly packed) + const char* filename; + uint16_t linenum; + uint8_t dataFramesCnt; + uint8_t skippedDataFramesCnt; // XXX: move to a separate struct that holds frames? // also rename to message frames? + bool destructing; + bool errorOnTop; + const char* funcname; + + // back/forward pointers + // const OnstackContext* downCtx; + OnstackContextImpl* downCtx; + + // stack and/or exception data + // XXX: can actually capture in dtor; but keep here or in thread data if light context comes? + // XXX 20241220 remove + // XXX: emscripten off https://github.com/jeremy-rifkin/cpptrace/issues/175 + cpptrace::raw_trace rawTrace; // can be empty + int uncaughtExceptions; + } h; + static_assert(sizeof(h) == 64); + + // calculate how much frames can we store with the remaining space and assert some stuff + static_assert((kSize - sizeof(h)) % sizeof(OnstackDataFrame) == 0); + static constexpr size_t kMaxDataFrameCnt = (kSize - sizeof(h)) / sizeof(OnstackDataFrame); + static_assert(kMaxDataFrameCnt <= std::numeric_limits::max()); + + static_assert(kMaxDataFrameCnt == 60, "update value in tests"); + + // messages that are associated with this context (= logged by the current function or a lightweight context above) + OnstackDataFrame dataFrames[kMaxDataFrameCnt]; + // XXX replace with std::array? + // XXX 20241220 2 replace with a special class that allows push_backs? Or that would probably waste space + // XXX 20250107 0311 это уже было где-то, но пусть и здесь - надо в идеале выделить в отдельный тип, чтобы уметь дебажить сам контекст цикличным буфером +}; + +} // namespace Antigo diff --git a/antigo/src_stub/antigo_stub.cpp b/antigo/src_stub/antigo_stub.cpp new file mode 100644 index 0000000000..5df11b3b77 --- /dev/null +++ b/antigo/src_stub/antigo_stub.cpp @@ -0,0 +1,34 @@ +#include "antigo/ExecutionData.h" + +#include +#include + +namespace Antigo { + +struct ExecutionData {}; + +ExecutionData& GetCurrentExecutionData() { thread_local ExecutionData g_stub; return g_stub; } + +bool HasExceptionWitness() { return false; } +ResolvedContext PopExceptionWitness() {return {};} + +bool HasExceptionWitnessOrphan() { return false; } +ResolvedContext PopExceptionWitnessOrphan() {return {};} + +std::string ResolvedContext::ToString() const { + std::stringstream ss; + ss << *this; + if (!ss.good()) { + return "stringstream error!"; + } + return std::move(ss).str(); +} + +void ResolvedContext::Print() const { + std::cout << *this << "\n"; +} +std::ostream& operator<<(std::ostream& os, const ResolvedContext& self) { + return os << "(no context in this build)"; +} + +} diff --git a/antigo/unit/CMakeLists.txt b/antigo/unit/CMakeLists.txt new file mode 100644 index 0000000000..26fd98fb43 --- /dev/null +++ b/antigo/unit/CMakeLists.txt @@ -0,0 +1,38 @@ +if(WITH_ANTIGO) + file(GLOB src "${CMAKE_CURRENT_SOURCE_DIR}/src/*") + list(APPEND src "${CMAKE_SOURCE_DIR}/.clang-format") + if(TARGET platform_lib) + file(GLOB src_windows "${CMAKE_CURRENT_SOURCE_DIR}/platform_lib_tests/*") + list(APPEND src ${src_windows}) + endif() + + add_executable(antigo_unit ${src}) + + find_package(Catch2 CONFIG REQUIRED) + target_link_libraries(antigo_unit PRIVATE Catch2::Catch2) + + target_link_libraries(antigo_unit PUBLIC antigo) + apply_default_settings(TARGETS antigo_unit) + list(APPEND VCPKG_DEPENDENT antigo_unit) + + if(WIN32) + target_compile_options(antigo_unit PRIVATE "/bigobj") + target_link_libraries(antigo_unit PUBLIC Dbghelp.lib) + endif() + + # + # ctest tests + # + + # run without coverage but with dumps + add_test( + NAME test_unit + COMMAND ${CMAKE_COMMAND} + -DEXE_PATH=$ + -DCOVERAGE_HTML_OUT_DIR=${CMAKE_BINARY_DIR}/__coverage + -DCPPCOV=OFF + -DCPPCOV_PATH=${CPPCOV_PATH} + -DUNIT_WORKING_DIRECTORY=${CMAKE_BINARY_DIR} + -P ${CMAKE_SOURCE_DIR}/cmake/run_test_unit.cmake + ) +endif() diff --git a/antigo/unit/src/ContextTest.cpp b/antigo/unit/src/ContextTest.cpp new file mode 100644 index 0000000000..9ae97afb33 --- /dev/null +++ b/antigo/unit/src/ContextTest.cpp @@ -0,0 +1,780 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" +#include "antigo/ExecutionData.h" +#include "antigo/impl/test_helpers.h" + +namespace { + +// expects messages in order of calls +struct MessageWalker +{ + const std::vector& v; + + size_t i = 0; + bool skipped = false; + + [[nodiscard]] + bool HasCstr(std::string_view msg) { + for (; i < v.size(); ++i) { + if (v[i].type == "cstr" && v[i].strval == msg) { + ++i; + return true; + } + skipped = true; + } + return false; + } + + [[nodiscard]] + bool Has(const std::string& type, const std::string& message) { + for (; i < v.size(); ++i) { + if (v[i].type == type && v[i].strval == message) { + ++i; + return true; + } + skipped = true; + } + return false; + } + + [[nodiscard]] + bool HasStacktrace() { + for (; i < v.size(); ++i) { + if (v[i].type == "stacktrace") { + ++i; + return true; + } + skipped = true; + } + return false; + } + + bool MatchedAll() const { + return i == v.size() && !skipped; + } +}; + +struct CustomTestException : public std::exception { + virtual const char* what() const noexcept override { + return "custom test exception"; + } +}; + +} + +TEST_CASE("Test context simple") +{ + struct { + size_t lineThrowerFunc; + void ThrowerFunc() { + ANTIGO_CONTEXT_INIT(ctx); + lineThrowerFunc = __LINE__ - 1; + ctx.AddMessage("gonna throw an error now"); + throw std::runtime_error("boo!"); + }; + } f; + + REQUIRE_THROWS(f.ThrowerFunc()); + REQUIRE_FALSE(Antigo::HasExceptionWitness()); + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + auto w = Antigo::PopExceptionWitnessOrphan(); + REQUIRE(w.entries.size() == 1); + REQUIRE(w.entries[0].sourceLoc.filename); + REQUIRE(std::string{w.entries[0].sourceLoc.filename} == __FILE__); + REQUIRE(w.entries[0].sourceLoc.line == f.lineThrowerFunc); + REQUIRE(w.entries[0].sourceLoc.func); + REQUIRE(std::string{w.entries[0].sourceLoc.func} == "ThrowerFunc"); + MessageWalker wlk{w.entries[0].messages}; + REQUIRE(wlk.HasCstr("gonna throw an error now")); + + REQUIRE(Antigo::impl::HasCleanState()); +} + +TEST_CASE("Test context multifunc exception (with picked up witnesses)") +{ + struct { + std::vector collected; + + size_t lineThrowerFunc; + void ThrowerFunc(bool doThrow) { + ANTIGO_CONTEXT_INIT(ctx); + lineThrowerFunc = __LINE__ - 1; + if (doThrow) { + throw std::runtime_error("error from Foo"); + } + }; + + size_t lineCatcherFunc; + void CatcherFunc() { + ANTIGO_CONTEXT_INIT(ctx); + lineCatcherFunc = __LINE__ - 1; + ctx.AddMessage("before throw 1"); + try { + ThrowerFunc(true); + ctx.AddMessage("you won't see this message"); + } catch (const std::exception& e) { + if (Antigo::HasExceptionWitness()) { + auto w = Antigo::PopExceptionWitness(); + ctx.AddMessage("caught 1, but this w won't have it"); + collected.push_back(std::move(w)); + } else { + throw std::runtime_error("caught 1, but no witness"); + } + if (Antigo::HasExceptionWitness()) { + throw std::runtime_error("caught 1, only expected one witness"); + } + } + ctx.AddMessage("before throw 2"); + try { + ThrowerFunc(true); + } catch (const std::exception& e) { + if (Antigo::HasExceptionWitness()) { + collected.push_back(Antigo::PopExceptionWitness()); + } else { + throw std::runtime_error("caught 2, only expected one witness"); + } + } + ctx.AddMessage("after throw 2 (you won't see it)"); + ThrowerFunc(false); // won't throw + } + } f; + + f.CatcherFunc(); + REQUIRE_FALSE(Antigo::HasExceptionWitness()); // should be collected + REQUIRE(f.collected.size() == 2); + + { + auto& ent0 = f.collected[0].entries; + REQUIRE(ent0.size() == 2); + + REQUIRE(ent0[0].sourceLoc.line == f.lineThrowerFunc); + REQUIRE(ent0[0].messages.size() == 1); + REQUIRE(ent0[0].messages[0].type == "stacktrace"); + + REQUIRE(ent0[1].sourceLoc.line == f.lineCatcherFunc); + MessageWalker wlk{ent0[1].messages}; + REQUIRE(wlk.HasCstr("before throw 1")); + REQUIRE(wlk.MatchedAll()); + } + + { + auto& ent1 = f.collected[1].entries; + REQUIRE(ent1.size() == 2); + + REQUIRE(ent1[0].sourceLoc.line == f.lineThrowerFunc); + REQUIRE(ent1[0].messages.size() == 1); + REQUIRE(ent1[0].messages[0].type == "stacktrace"); + + REQUIRE(ent1[1].sourceLoc.line == f.lineCatcherFunc); + MessageWalker wlk{ent1[1].messages}; + REQUIRE(wlk.HasCstr("before throw 1")); + REQUIRE(wlk.HasCstr("caught 1, but this w won't have it")); + REQUIRE(wlk.HasCstr("before throw 2")); + REQUIRE(wlk.MatchedAll()); + } + + REQUIRE(Antigo::impl::HasCleanState()); +} + +TEST_CASE("Test detached context") +{ + struct { + std::vector collected; + + size_t lineThrowerFunc; + void ThrowerFunc() { + ANTIGO_CONTEXT_INIT(ctx); + lineThrowerFunc = __LINE__ - 1; + throw std::runtime_error("error from Foo"); + }; + + size_t lineCatcherFunc; + void CatcherFunc() { + ANTIGO_CONTEXT_INIT(ctx); + lineCatcherFunc = __LINE__ - 1; + try { + ThrowerFunc(); + } catch (const std::exception& e) { + // doesn't pop witness + } + } + } f; + + f.CatcherFunc(); + + // XXX do it like this?? + REQUIRE(!Antigo::HasExceptionWitness()); + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + auto w = Antigo::PopExceptionWitnessOrphan(); + REQUIRE(w.entries.size() == 2); + + REQUIRE(Antigo::impl::HasCleanState()); +} + +TEST_CASE("Test recursive") +{ + struct HelperF { + bool popWitness = false; + int catchDepth = -1; + int throwDepth = -1; + int exitDepth = -1; + + struct DepthData { + bool hasWitness{}; + bool hasWitnessOrphan{}; + bool thrown{}; + bool caught{}; + bool exited{}; + }; + std::vector dd; + + int orphanAppearedDepth = -1; + int caughtDepth = -1; + int thrownDepth = -1; + int exitedDepth = -1; + + void UpdateWitnessStats(DepthData& data, size_t currentDepth) { + data.hasWitness = Antigo::HasExceptionWitness(); + data.hasWitnessOrphan = Antigo::HasExceptionWitnessOrphan(); + if (data.hasWitnessOrphan && orphanAppearedDepth == -1) { + orphanAppearedDepth = currentDepth; + } + } + + size_t lineRecursiveHelper; + void RecursiveHelper(size_t currentDepth) { + ANTIGO_CONTEXT_INIT(ctx); + lineRecursiveHelper = __LINE__ - 1; + + assert(dd.size() == currentDepth); + dd.emplace_back(); + + // exit conditions + if (currentDepth == exitDepth) { + exitedDepth = currentDepth; + dd[currentDepth].exited = true; + return; + } + if (currentDepth == throwDepth) { + thrownDepth = currentDepth; + dd[currentDepth].thrown = true; + throw CustomTestException(); + } + + // do next call + if (currentDepth == catchDepth) { + try { + RecursiveHelper(currentDepth + 1); + UpdateWitnessStats(dd[currentDepth], currentDepth); + } catch (const CustomTestException& e) { + caughtDepth = currentDepth; + UpdateWitnessStats(dd[currentDepth], currentDepth); + if (popWitness && Antigo::HasExceptionWitness()) { + Antigo::PopExceptionWitness(); + } + } + return; + } + RecursiveHelper(currentDepth + 1); + UpdateWitnessStats(dd[currentDepth], currentDepth); + } + }; + + { + HelperF f; + f.exitDepth = 5; + f.RecursiveHelper(0); + REQUIRE(f.dd.size() == 6); + REQUIRE(f.orphanAppearedDepth == -1); + REQUIRE(f.thrownDepth == -1); + REQUIRE(f.exitedDepth == 5); + REQUIRE(Antigo::impl::HasCleanState()); + } + + { + HelperF f; + f.exitDepth = 5; + f.RecursiveHelper(0); + REQUIRE(f.dd.size() == 6); + REQUIRE(f.orphanAppearedDepth == -1); + REQUIRE(f.thrownDepth == -1); + REQUIRE(f.exitedDepth == 5); + REQUIRE(Antigo::impl::HasCleanState()); + } + + { + HelperF f; + f.throwDepth = 0; + f.exitDepth = 5; + REQUIRE_THROWS_AS(f.RecursiveHelper(0), CustomTestException); + REQUIRE(f.dd.size() == 1); + REQUIRE(f.orphanAppearedDepth == -1); + REQUIRE(f.thrownDepth == 0); + REQUIRE(f.exitedDepth == -1); + REQUIRE_FALSE(Antigo::HasExceptionWitness()); + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + Antigo::PopExceptionWitnessOrphan(); + REQUIRE_FALSE(Antigo::HasExceptionWitnessOrphan()); + REQUIRE(Antigo::impl::HasCleanState()); + } + + { + HelperF f; + f.throwDepth = 1; + f.catchDepth = 0; + f.exitDepth = 5; + f.RecursiveHelper(0); + REQUIRE(f.dd.size() == 2); + REQUIRE(f.orphanAppearedDepth == -1); // after returning from RecursiveHelper + REQUIRE(f.thrownDepth == 1); + REQUIRE(f.caughtDepth == 0); + REQUIRE(f.exitedDepth == -1); + REQUIRE_FALSE(Antigo::HasExceptionWitness()); + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + Antigo::PopExceptionWitnessOrphan(); + REQUIRE_FALSE(Antigo::HasExceptionWitnessOrphan()); + REQUIRE(Antigo::impl::HasCleanState()); + } + + { + HelperF f; + f.throwDepth = 2; + f.catchDepth = 1; + f.exitDepth = 5; + f.RecursiveHelper(0); + REQUIRE(f.dd.size() == 3); + REQUIRE(f.orphanAppearedDepth == 0); + REQUIRE(f.thrownDepth == 2); + REQUIRE(f.caughtDepth == 1); + REQUIRE(f.exitedDepth == -1); + REQUIRE_FALSE(Antigo::HasExceptionWitness()); + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + Antigo::PopExceptionWitnessOrphan(); + REQUIRE_FALSE(Antigo::HasExceptionWitnessOrphan()); + REQUIRE(Antigo::impl::HasCleanState()); + } + + REQUIRE(Antigo::impl::HasCleanState()); +} + +TEST_CASE("Test several catches with no pop") +{ + struct HelperF { + std::vector popped; + + void ThrowerFunc() { + ANTIGO_CONTEXT_INIT(ctx); + throw CustomTestException(); + } + + void CatcherFunc(bool doPop) { + ANTIGO_CONTEXT_INIT(ctx); + try { + ThrowerFunc(); + } catch (const CustomTestException&) { + // ignore + } + try { + ThrowerFunc(); + } catch (const CustomTestException&) { + // ignore + } + try { + ThrowerFunc(); + } catch (const CustomTestException&) { + // ignore + } + + if (Antigo::HasExceptionWitnessOrphan()) { + throw std::runtime_error("orphan unexpected"); + } + + if (doPop) { + while (Antigo::HasExceptionWitness()) { + popped.push_back(Antigo::PopExceptionWitness()); + } + } + } + }; + + { + HelperF f; + f.CatcherFunc(false); + REQUIRE(f.popped.size() == 0); + size_t witnessCnt = 0; + while (Antigo::HasExceptionWitness()) { + Antigo::PopExceptionWitness(); + ++witnessCnt; + } + REQUIRE(witnessCnt == 0); + size_t orphansCnt = 0; + while (Antigo::HasExceptionWitnessOrphan()) { + Antigo::PopExceptionWitnessOrphan(); + ++orphansCnt; + } + REQUIRE(orphansCnt == 3); + // TODO: check via lines or messages? + REQUIRE(Antigo::impl::HasCleanState()); + } + + { + HelperF f; + f.CatcherFunc(true); + REQUIRE(f.popped.size() == 3); + // TODO: check via lines or messages? + REQUIRE(Antigo::impl::HasCleanState()); + } + + REQUIRE(Antigo::impl::HasCleanState()); +} + +namespace { +struct alignas(64) Kek { + std::mt19937 gen; + std::bernoulli_distribution rngUnlikely; + std::bernoulli_distribution rngCoin; + std::bernoulli_distribution rngLikely; + + std::uint32_t iterations = 0; + + Kek(size_t seed): gen(seed), rngUnlikely(0.2), rngCoin(0.5), rngLikely(0.9), iterations(0) {} + + void RecursiveFunc(std::stop_token stop_token, size_t depth) { + ANTIGO_CONTEXT_INIT(ctx); + if (stop_token.stop_requested()) { + return; + } + if (rngUnlikely(gen)) { + return; + } + iterations++; + if (rngUnlikely(gen)) { + throw CustomTestException(); + } + if (depth < 50 && rngLikely(gen)) { + if (rngUnlikely(gen)) { + try { + RecursiveFunc(stop_token, depth + 1); + } catch (const CustomTestException&) { + // ignore + } + } else { + RecursiveFunc(stop_token, depth + 1); + } + + if (rngUnlikely(gen)) { + while (Antigo::HasExceptionWitness()) { + Antigo::PopExceptionWitness(); + } + } + + if (rngUnlikely(gen)) { + while (Antigo::HasExceptionWitnessOrphan()) { + Antigo::PopExceptionWitnessOrphan(); + } + } + } + } + + void EntryFunc(std::stop_token stop_token) { + ANTIGO_CONTEXT_INIT(ctx); + while (!stop_token.stop_requested()) { + try { + RecursiveFunc(stop_token, 1); + } catch (const CustomTestException&) { + // ignore + } + } + } +}; +} // namespace + +// XXX 20250112 fix Ubuntu build +/* +TEST_CASE("Test recursive random") +{ + uint32_t seed = Catch::getSeed(); + std::vector keks; + std::vector threads; + keks.reserve(4); + threads.reserve(4); + for (size_t i = 0; i < 4; ++i) { + auto& kek = keks.emplace_back(seed + i); + threads.emplace_back(&Kek::EntryFunc, kek); + // XXX ^ fails on clang 15 and MSVC + } + std::this_thread::sleep_for(std::chrono::milliseconds(5000)); + + REQUIRE(threads.size() == keks.size()); + threads.clear(); // jthreads join + + std::vector iterations; + for (size_t i = 0; i < keks.size(); ++i) { + iterations.push_back(keks[i].iterations); + } + CAPTURE(iterations); + + REQUIRE(Antigo::impl::HasCleanState()); +} +*/ + +TEST_CASE("Test context messages") +{ + struct { + size_t evaluated1 = false; + size_t evaluated2 = false; + size_t evaluated3 = false; + size_t evaluated4 = false; + size_t evaluated5 = false; + + void InnerFunc() { + ANTIGO_CONTEXT_INIT(ctx); + + ctx.AddMessage("logging some predefined string"); + ctx.AddUnsigned(1337); + ctx.AddSigned(-1337); + + std::vector v{1, 2, 3}; + ctx.AddLambdaWithOwned([this, v = std::move(v)]() { + evaluated1++; + return "captured a vector that has " + std::to_string(v.size()) + " elements"; + }); + + std::vector v2{1, 2, 3, 4}; + auto g = ctx.AddLambdaWithRef([this, &v2]() { + evaluated2++; + return "vector has " + std::to_string(v2.size()) + " elements; this one won't expire after Orphan but will expire after exception"; + }); + g.Arm(); + + { + ctx.AddLambdaWithOwned([this]() { + evaluated3++; + return "owned, will be copied"; + }); + + auto g2 = ctx.AddLambdaWithRef([this]() { + evaluated4++; + return "ref, will expire"; + }); + g2.Arm(); + } + + ctx.Orphan(); + throw CustomTestException(); + } + + void OuterFunc() { + ANTIGO_CONTEXT_INIT(ctx); + + ctx.AddLambdaWithOwned([this]() { + evaluated5++; + return "some message from the outer scope just to verify proper capture of the whole chain"; + }); + + InnerFunc(); + } + } f; + + REQUIRE_THROWS_AS(f.OuterFunc(), CustomTestException); + + const std::string expiredMessage = "(reference possibly expired or Arm() not called)"; + + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + { + auto w = Antigo::PopExceptionWitnessOrphan(); + REQUIRE(w.reason == "exception"); + auto& e = w.entries; + REQUIRE(e.size() == 2); + { + REQUIRE(std::string_view{e[0].sourceLoc.func} == "InnerFunc"); + MessageWalker wlk{e[0].messages}; + REQUIRE(wlk.Has("cstr", "logging some predefined string")); + REQUIRE(wlk.Has("uint", "1337")); + REQUIRE(wlk.Has("int", "-1337")); + REQUIRE(wlk.Has("custom", "captured a vector that has 3 elements")); + REQUIRE(wlk.Has("custom", expiredMessage)); + REQUIRE(wlk.Has("custom", "owned, will be copied")); + REQUIRE(wlk.Has("custom", expiredMessage)); + REQUIRE(wlk.HasStacktrace()); + REQUIRE(wlk.MatchedAll()); + } + { + REQUIRE(std::string_view{e[1].sourceLoc.func} == "OuterFunc"); + MessageWalker wlk{e[1].messages}; + REQUIRE(wlk.Has("custom", "some message from the outer scope just to verify proper capture of the whole chain")); + REQUIRE(wlk.MatchedAll()); + } + } + + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + { + auto w = Antigo::PopExceptionWitnessOrphan(); + REQUIRE(w.reason == "ondemand"); + auto& e = w.entries; + REQUIRE(e.size() == 2); + { + REQUIRE(std::string_view{e[0].sourceLoc.func} == "InnerFunc"); + MessageWalker wlk{e[0].messages}; + REQUIRE(wlk.Has("cstr", "logging some predefined string")); + REQUIRE(wlk.Has("uint", "1337")); + REQUIRE(wlk.Has("int", "-1337")); + REQUIRE(wlk.Has("custom", "captured a vector that has 3 elements")); + REQUIRE(wlk.Has("custom", "vector has 4 elements; this one won't expire after Orphan but will expire after exception")); + REQUIRE(wlk.Has("custom", "owned, will be copied")); + REQUIRE(wlk.Has("custom", expiredMessage)); + REQUIRE(wlk.HasStacktrace()); + REQUIRE(wlk.MatchedAll()); + } + { + REQUIRE(std::string_view{e[1].sourceLoc.func} == "OuterFunc"); + MessageWalker wlk{e[1].messages}; + REQUIRE(wlk.Has("custom", "some message from the outer scope just to verify proper capture of the whole chain")); + REQUIRE(wlk.MatchedAll()); + } + } + + REQUIRE_FALSE(Antigo::HasExceptionWitnessOrphan()); + REQUIRE_FALSE(Antigo::HasExceptionWitness()); + + REQUIRE(f.evaluated1 == 2); + REQUIRE(f.evaluated2 == 1); + REQUIRE(f.evaluated3 == 2); + REQUIRE(f.evaluated4 == 0); + REQUIRE(f.evaluated5 == 2); + + REQUIRE(Antigo::impl::HasCleanState()); +} + +TEST_CASE("Test context too many messages") +{ + static constexpr size_t kMaxDataFrameCnt = 60; + struct { + void Func(size_t extra) { + ANTIGO_CONTEXT_INIT(ctx); + + for (size_t i = 0; i < kMaxDataFrameCnt + extra; ++i) { + ctx.AddUnsigned(i); + } + ctx.Orphan(); + } + } f; + + { + f.Func(4); + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + auto w = Antigo::PopExceptionWitnessOrphan(); + + auto& e = w.entries; + REQUIRE(e.size() == 1); + + MessageWalker wlk{e[0].messages}; + for (size_t i = 0; i < kMaxDataFrameCnt; ++i) { + REQUIRE(wlk.Has("uint", std::to_string(i))); + } + REQUIRE(wlk.Has("meta", "4 last messages didn't fit into buffer")); + REQUIRE(wlk.HasStacktrace()); + REQUIRE(wlk.MatchedAll()); + + REQUIRE_FALSE(Antigo::HasExceptionWitnessOrphan()); + REQUIRE(Antigo::impl::HasCleanState()); + } + + { + f.Func(1337); + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + auto w = Antigo::PopExceptionWitnessOrphan(); + + auto& e = w.entries; + REQUIRE(e.size() == 1); + + MessageWalker wlk{e[0].messages}; + for (size_t i = 0; i < kMaxDataFrameCnt; ++i) { + REQUIRE(wlk.Has("uint", std::to_string(i))); + } + REQUIRE(wlk.Has("meta", "255+ last messages didn't fit into buffer")); + REQUIRE(wlk.HasStacktrace()); + REQUIRE(wlk.MatchedAll()); + + REQUIRE_FALSE(Antigo::HasExceptionWitnessOrphan()); + REQUIRE(Antigo::impl::HasCleanState()); + } + + REQUIRE(Antigo::impl::HasCleanState()); +} + +TEST_CASE("Test exception during exception handling") +{ + struct { + struct InnerStruct2 { + ~InnerStruct2() { + ANTIGO_CONTEXT_INIT(ctx); // 2 -> 2 + try { + throw CustomTestException(); + } catch (const CustomTestException&) { + // ignore + } + } + }; + + struct InnerStruct { + ~InnerStruct() { + ANTIGO_CONTEXT_INIT(ctx); // 1 -> 1 + try { + InnerStruct2 is2; + throw CustomTestException(); + } catch (const CustomTestException&) { + // ignore + } + } + }; + + void Func() { + ANTIGO_CONTEXT_INIT(ctx); // 0 -> 0 + try { + InnerStruct is; + throw CustomTestException(); + } catch (const CustomTestException&) { + // ignore + } + } + } f; + + f.Func(); + + REQUIRE(Antigo::HasExceptionWitnessOrphan()); + auto w = Antigo::PopExceptionWitnessOrphan(); + REQUIRE(w.reason == "exception"); + REQUIRE(w.entries.size() == 3); + REQUIRE(w.entries[0].sourceLoc.func == std::string_view{"~InnerStruct2"}); + REQUIRE(w.entries[1].sourceLoc.func == std::string_view{"~InnerStruct"}); + REQUIRE(w.entries[2].sourceLoc.func == std::string_view{"Func"}); + + // for now, it would only return one witness, i.e. ~InnerStruct() and Func() wouldn't have a separate one, even though they have separate exceptions + + REQUIRE(Antigo::impl::HasCleanState()); +} + +// XXX test if catch can receive an unrelated context (probably can if we throw and catch in-place with orphan/detached already existing prior to that) + +// XXX uncleaned witnesses can probably stick to a thread pool local storage +// XXX 20250103 0021: probably is only problem for orphans. witnesses don't live longer than the current context does + +// XXX uncaught exceptions might inc/dec between ctor and dtor and return back - is it a problem? +// inc-dec по идее это будет значить либо что мы поймали эксепшн, который возник выше этого контекста - т.е. он остался изолированным в текущем скоупе +// dec-inc - это будет значить, что текущий скоуп как-то погасил эксепшн из фрейма ниже. возможно ли это вообще? +// TODO: почекать, в каких ситуациях uncaught_exceptions > 1 diff --git a/antigo/unit/src/main.cpp b/antigo/unit/src/main.cpp new file mode 100644 index 0000000000..8b501b665e --- /dev/null +++ b/antigo/unit/src/main.cpp @@ -0,0 +1,6 @@ +#include + +int main(int argc, char* argv[]) +{ + return Catch::Session().run(argc, argv); +} diff --git a/libespm/CMakeLists.txt b/libespm/CMakeLists.txt index f04bec1f5e..1866601e1d 100644 --- a/libespm/CMakeLists.txt +++ b/libespm/CMakeLists.txt @@ -10,4 +10,4 @@ apply_default_settings(TARGETS espm) add_library(libespm ALIAS espm) -target_link_libraries(espm PUBLIC viet) +target_link_libraries(espm PUBLIC antigo viet) diff --git a/libespm/include/libespm/CombineBrowser.h b/libespm/include/libespm/CombineBrowser.h index 8393d8d49e..41787d43a9 100644 --- a/libespm/include/libespm/CombineBrowser.h +++ b/libespm/include/libespm/CombineBrowser.h @@ -3,9 +3,12 @@ #include "GroupStack.h" #include "IdMapping.h" #include "LookupResult.h" +#include "antigo/ResolvedContext.h" namespace espm { +extern Antigo::ResolvedContext g_lastForm0Lookup; + class RecordHeader; class Browser; diff --git a/libespm/src/CombineBrowser.cpp b/libespm/src/CombineBrowser.cpp index 7c418b8304..e0f0411555 100644 --- a/libespm/src/CombineBrowser.cpp +++ b/libespm/src/CombineBrowser.cpp @@ -7,6 +7,9 @@ #include #include +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" + namespace espm { int32_t CombineBrowser::Impl::GetFileIndex(const char* fileName) const noexcept @@ -22,8 +25,16 @@ int32_t CombineBrowser::Impl::GetFileIndex(const char* fileName) const noexcept return -1; } +Antigo::ResolvedContext g_lastForm0Lookup; + LookupResult CombineBrowser::LookupById(uint32_t combFormId) const noexcept { + ANTIGO_CONTEXT_INIT(ctx); + if (combFormId == 0) { + ctx.AddMessage("trying to resolve form id 0"); + g_lastForm0Lookup = ctx.Resolve(); + g_lastForm0Lookup.Print(); + } const RecordHeader* resRec = nullptr; uint8_t resFileIdx = 0; for (size_t i = 0; i < pImpl->numSources; ++i) { diff --git a/misc/deploy/workaround_temporary/run.sh b/misc/deploy/workaround_temporary/run.sh index b5842dd58f..ccd4fc484d 100755 --- a/misc/deploy/workaround_temporary/run.sh +++ b/misc/deploy/workaround_temporary/run.sh @@ -3,4 +3,4 @@ set -e set -x -node dist_back/skymp5-server.js +node dist_back/skymp5-server.js --config-type=indev diff --git a/papyrus-vm/CMakeLists.txt b/papyrus-vm/CMakeLists.txt index 5afeff6d3d..8333fa4fcc 100644 --- a/papyrus-vm/CMakeLists.txt +++ b/papyrus-vm/CMakeLists.txt @@ -8,7 +8,7 @@ apply_default_settings(TARGETS papyrus-vm-lib) target_include_directories(papyrus-vm-lib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") target_include_directories(papyrus-vm-lib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") -target_link_libraries(papyrus-vm-lib PUBLIC viet) +target_link_libraries(papyrus-vm-lib PUBLIC antigo viet) find_package(spdlog CONFIG REQUIRED) target_link_libraries(papyrus-vm-lib PUBLIC spdlog::spdlog) diff --git a/papyrus-vm/include/papyrus-vm/ActivePexInstance.h b/papyrus-vm/include/papyrus-vm/ActivePexInstance.h index 78f8f36446..233028fee2 100644 --- a/papyrus-vm/include/papyrus-vm/ActivePexInstance.h +++ b/papyrus-vm/include/papyrus-vm/ActivePexInstance.h @@ -1,5 +1,4 @@ #pragma once -#include #include #include #include @@ -7,7 +6,6 @@ #include #include -#include "FunctionCode.h" #include "FunctionInfo.h" #include "IVariablesHolder.h" #include "PexScript.h" diff --git a/papyrus-vm/include/papyrus-vm/FunctionCode.h b/papyrus-vm/include/papyrus-vm/FunctionCode.h index 41b6e06b9e..8ba93c8335 100644 --- a/papyrus-vm/include/papyrus-vm/FunctionCode.h +++ b/papyrus-vm/include/papyrus-vm/FunctionCode.h @@ -2,6 +2,8 @@ #include #include +#include "papyrus-vm/VarValue.h" + struct FunctionCode { enum diff --git a/papyrus-vm/include/papyrus-vm/FunctionInfo.h b/papyrus-vm/include/papyrus-vm/FunctionInfo.h index 115ef2522d..00ecbf5e13 100644 --- a/papyrus-vm/include/papyrus-vm/FunctionInfo.h +++ b/papyrus-vm/include/papyrus-vm/FunctionInfo.h @@ -1,4 +1,5 @@ #pragma once +#include "papyrus-vm/FunctionCode.h" #include #include #include diff --git a/papyrus-vm/include/papyrus-vm/Object.h b/papyrus-vm/include/papyrus-vm/Object.h index ae0ae5e76a..e45b026946 100644 --- a/papyrus-vm/include/papyrus-vm/Object.h +++ b/papyrus-vm/include/papyrus-vm/Object.h @@ -4,6 +4,7 @@ #include #include "VarValue.h" +#include "papyrus-vm/FunctionInfo.h" struct Object { diff --git a/papyrus-vm/src/papyrus-vm-lib/ActivePexInstance.cpp b/papyrus-vm/src/papyrus-vm-lib/ActivePexInstance.cpp index c8165340d3..6bd0d200f0 100644 --- a/papyrus-vm/src/papyrus-vm-lib/ActivePexInstance.cpp +++ b/papyrus-vm/src/papyrus-vm-lib/ActivePexInstance.cpp @@ -1,9 +1,12 @@ #include "ScopedTask.h" +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" #include "papyrus-vm/OpcodesImplementation.h" #include "papyrus-vm/Utils.h" #include "papyrus-vm/VirtualMachine.h" #include #include // tolower +#include #include #include #include @@ -74,17 +77,20 @@ FunctionInfo ActivePexInstance::GetFunctionByName(const char* name, std::string ActivePexInstance::GetActiveStateName() const { + ANTIGO_CONTEXT_INIT(ctx); + VarValue* var = nullptr; try { var = variables->GetVariableByName("::State", *sourcePex.fn()); - } catch (...) { + } catch (const std::exception& e) { throw std::runtime_error( " Papyrus VM: GetVariableByName must never throw when " - "::State variable is requested"); + "::State variable is requested; prev error: " + std::string{e.what()}); } - if (!var) + if (!var) { throw std::runtime_error( "Papyrus VM: ::State variable doesn't exist in ActivePexInstance"); + } return static_cast(*var); } @@ -189,6 +195,21 @@ bool ActivePexInstance::EnsureCallResultIsSynchronous( void ActivePexInstance::ExecuteOpCode(ExecutionContext* ctx, uint8_t op, const std::vector& args) { + ANTIGO_CONTEXT_INIT(agctx); + agctx.AddMessage("next: op, args.size(), args"); + agctx.AddUnsigned(op); + agctx.AddUnsigned(args.size()); + auto g = agctx.AddLambdaWithRef([&args]() { + std::stringstream ss; + ss << "size = " << args.size() << "\n[\n"; + for (const auto& arg : args) { + ss << " " << (arg ? arg->ToString() : "(null)") << "\n"; + } + ss << "]"; + return std::move(ss).str(); + }); + g.Arm(); + auto argsForCall = GetArgsForCall(op, args); switch (op) { @@ -247,6 +268,8 @@ void ActivePexInstance::ExecuteOpCode(ExecutionContext* ctx, uint8_t op, default: // assert(0); // Triggered by some array stuff in SkyMP, not sure this is OK + agctx.AddMessage("strange cast, had comment \"Triggered by some array stuff in SkyMP, not sure this is OK 4 years ago\" :)"); + agctx.Orphan(); *args[0] = (*args[1]); break; } @@ -433,8 +456,11 @@ void ActivePexInstance::ExecuteOpCode(ExecutionContext* ctx, uint8_t op, if (object && object->ListActivePexInstances().size() > 0) { auto inst = object->ListActivePexInstances().back(); + agctx.AddMessage("next: inst, runProperty"); + agctx.AddPtr(inst); Object::PropInfo* runProperty = GetProperty(*inst, propertyName, Object::PropInfo::kFlags_Read); + agctx.AddPtr(runProperty); if (runProperty != nullptr) { // TODO: use of argsForCall looks incorrect. why use argsForCall // here? shoud be {} (empty args) @@ -518,9 +544,12 @@ void ActivePexInstance::ExecuteOpCode(ExecutionContext* ctx, uint8_t op, } if (object && object->ListActivePexInstances().size() > 0) { + agctx.AddMessage("next: inst, runProperty"); auto inst = object->ListActivePexInstances().back(); + agctx.AddPtr(inst); Object::PropInfo* runProperty = GetProperty(*inst, propertyName, Object::PropInfo::kFlags_Write); + agctx.AddPtr(runProperty); if (runProperty != nullptr) { // TODO: use of argsForCall looks incorrect. // probably should only *args[2] @@ -702,7 +731,28 @@ ActivePexInstance::TransformInstructions( VarValue ActivePexInstance::ExecuteAll( ExecutionContext& ctx, std::optional previousCallResult) { - auto pipex = sourcePex.fn(); + ANTIGO_CONTEXT_INIT(agctx); + agctx.AddMessage("next: stack id, line"); + agctx.AddUnsigned(ctx.stackData->stackIdHolder.GetStackId()); + agctx.AddUnsigned(ctx.line); + auto g = agctx.AddLambdaWithRef([&ctx]() { + std::stringstream ss; + ss << "ExecutionContext:\n"; + if (ctx.locals == nullptr) { + ss << "locals = nullptr\n"; + } else { + ss << "locals = [" << ctx.locals->size() << "] [\n"; + for (size_t i = 0; i < ctx.locals->size(); ++i) { + auto& local = (*ctx.locals)[i]; + ss << " " << local.first << " = " << local.second << "\n"; + } + ss << "]\n"; + } + return std::move(ss).str(); + }); + g.Arm(); + + auto pipex = sourcePex.fn(); // XXX ??? auto opCode = TransformInstructions(ctx.instructions, ctx.locals); @@ -737,10 +787,33 @@ VarValue ActivePexInstance::StartFunction(FunctionInfo& function, std::vector& arguments, std::shared_ptr stackData) { + ANTIGO_CONTEXT_INIT(agctx); + if (!stackData) { throw std::runtime_error("An empty stackData passed to StartFunction"); } + auto g = agctx.AddLambdaWithRef([this, &function]() { + std::stringstream ss; + ss << "source pex = " << GetSourcePexName() << "\n"; + ss << "return = " << function.returnType << "\n"; + ss << "docstring = " << function.docstring << "\n"; + ss << "userFlags = " << std::hex << function.userFlags << "\n"; + ss << "flags = " << std::hex << static_cast(function.flags) << "\n"; + ss << "params: [\n"; + for (const auto& param : function.params) { + ss << " " << param.name << " " << param.type << "\n"; + } + ss << "]\n"; + ss << "locals:\n"; + for (const auto& param : function.locals) { + ss << " " << param.name << " " << param.type << "\n"; + } + ss << "]\n"; + return std::move(ss).str(); + }); + g.Arm(); + thread_local StackDepthHolder g_stackDepthHolder; g_stackDepthHolder.IncreaseStackDepth(); @@ -757,6 +830,7 @@ VarValue ActivePexInstance::StartFunction(FunctionInfo& function, spdlog::error("ActivePexInstance::StartFunction - Stack overflow in " "script {}, returning None", sourcePex.fn()->source); + agctx.Resolve().Print(); return VarValue::None(); } @@ -947,6 +1021,8 @@ VarValue ActivePexInstance::TryCastToBaseClass( const VirtualMachine& vm, const std::string& resultTypeName, VarValue* scriptToCastOwner, std::vector& outClassesStack) { + ANTIGO_CONTEXT_INIT(ctx); + auto object = static_cast(*scriptToCastOwner); if (!object) { return VarValue::None(); diff --git a/papyrus-vm/src/papyrus-vm-lib/VarValue.cpp b/papyrus-vm/src/papyrus-vm-lib/VarValue.cpp index 73c64692f9..30cdf8da81 100644 --- a/papyrus-vm/src/papyrus-vm-lib/VarValue.cpp +++ b/papyrus-vm/src/papyrus-vm-lib/VarValue.cpp @@ -1,3 +1,4 @@ +#include "antigo/Context.h" #include "papyrus-vm/Structures.h" #include "papyrus-vm/VirtualMachine.h" @@ -494,6 +495,11 @@ bool VarValue::operator<(const VarValue& argument2) const bool VarValue::operator<=(const VarValue& argument2) const { + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddMessage("next: type left, type right"); + ctx.AddUnsigned(GetType()); + ctx.AddUnsigned(argument2.GetType()); + switch (this->type) { case VarValue::kType_Integer: return this->CastToInt().data.i <= argument2.CastToInt().data.i; diff --git a/papyrus-vm/src/papyrus-vm-lib/VirtualMachine.cpp b/papyrus-vm/src/papyrus-vm-lib/VirtualMachine.cpp index e69946b252..e5990262d4 100644 --- a/papyrus-vm/src/papyrus-vm-lib/VirtualMachine.cpp +++ b/papyrus-vm/src/papyrus-vm-lib/VirtualMachine.cpp @@ -1,4 +1,6 @@ #include "papyrus-vm/VirtualMachine.h" +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" #include "papyrus-vm/Utils.h" #include #include @@ -164,6 +166,8 @@ bool VirtualMachine::DynamicCast(const VarValue& object, void VirtualMachine::AddObject(std::shared_ptr self, const std::vector& scripts) { + ANTIGO_CONTEXT_INIT(ctx); + std::vector> scriptsForObject; for (auto& s : scripts) { @@ -180,6 +184,7 @@ void VirtualMachine::AddObject(std::shared_ptr self, self->AddScript(script); } gameObjectsHolder.insert(self); + // XXX why } void VirtualMachine::SendEvent(std::shared_ptr self, @@ -187,6 +192,12 @@ void VirtualMachine::SendEvent(std::shared_ptr self, const std::vector& arguments, OnEnter enter) { + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddMessage("next: self, eventName"); + ctx.AddPtr(self); + std::string eventNameS = eventName; + ctx.AddLambdaWithOwned([n = std::move(eventNameS)]() { return n; }); + for (auto& scriptInstance : self->ListActivePexInstances()) { auto name = scriptInstance->GetActiveStateName(); @@ -208,6 +219,11 @@ void VirtualMachine::SendEvent(ActivePexInstance* instance, const char* eventName, const std::vector& arguments) { + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddMessage("next: instance, eventName"); + ctx.AddPtr(instance); + std::string eventNameS = eventName; + ctx.AddLambdaWithOwned([n = std::move(eventNameS)]() { return n; }); auto fn = instance->GetFunctionByName(eventName, instance->GetActiveStateName()); @@ -262,11 +278,33 @@ VarValue VirtualMachine::CallMethod( const std::vector>* activePexInstancesOverride) { + ANTIGO_CONTEXT_INIT(ctx); + auto g = ctx.AddLambdaWithRef([selfObj, methodName, arguments]() { + std::stringstream ss; + ss << "selfObj = "; + if (selfObj) { + ss << selfObj->GetStringID(); + } else { + ss << "nullptr"; + } + ss << "\n"; + ss << "methodName = " << methodName << "\n"; + ss << "arguments = [\n"; + for (const auto& arg : arguments) { + ss << " " << arg.ToString() << "\n"; + } + ss << "]"; + return std::move(ss).str(); + }); + g.Arm(); + if (!stackData) { stackData.reset(new StackData{ StackIdHolder{ *this } }); } if (!selfObj) { + ctx.AddMessage("tried to call a method for null"); + ctx.Resolve().Print(); return VarValue::None(); } @@ -312,6 +350,11 @@ VarValue VirtualMachine::CallMethod( break; } + // ctx.AddMessage("method not found (see adjacent message if this one came from an exception)"); + // ctx.Orphan(); + + ANTIGO_CONTEXT_INIT(ctx_hack); // a way to avoid ref expiry + std::string e = "Method not found - '"; e += base; e += (base[0] ? "." : "") + std::string(methodName) + "'"; @@ -323,6 +366,8 @@ VarValue VirtualMachine::CallStatic(const std::string& className, std::vector& arguments, std::shared_ptr stackData) { + ANTIGO_CONTEXT_INIT(ctx); + if (!stackData) { stackData.reset(new StackData{ StackIdHolder{ *this } }); } @@ -390,6 +435,7 @@ std::shared_ptr VirtualMachine::CreateActivePexInstance( const std::shared_ptr& mapForFillProperties, const std::string& childrenName) { + ANTIGO_CONTEXT_INIT(ctx); auto it = allLoadedScripts.find( CIString{ pexScriptName.begin(), pexScriptName.end() }); diff --git a/skymp5-server/cpp/CMakeLists.txt b/skymp5-server/cpp/CMakeLists.txt index d77fabeee4..92c38ffe4b 100644 --- a/skymp5-server/cpp/CMakeLists.txt +++ b/skymp5-server/cpp/CMakeLists.txt @@ -72,7 +72,7 @@ add_library(server_guest_lib STATIC ${src}) target_include_directories(server_guest_lib PUBLIC "${CMAKE_CURRENT_LIST_DIR}/server_guest_lib") apply_default_settings(TARGETS server_guest_lib) list(APPEND VCPKG_DEPENDENT server_guest_lib) -target_link_libraries(server_guest_lib PUBLIC viet mp_common espm papyrus-vm-lib geo serialization) +target_link_libraries(server_guest_lib PUBLIC viet mp_common espm papyrus-vm-lib geo serialization antigo) # # localization-provider diff --git a/skymp5-server/cpp/addon/ScampServer.cpp b/skymp5-server/cpp/addon/ScampServer.cpp index d5e715c782..4965b69436 100644 --- a/skymp5-server/cpp/addon/ScampServer.cpp +++ b/skymp5-server/cpp/addon/ScampServer.cpp @@ -8,6 +8,9 @@ #include "PacketHistoryWrapper.h" #include "PapyrusUtils.h" #include "ScampServerListener.h" +#include "antigo/Context.h" +#include "antigo/ExecutionData.h" +#include "antigo/ResolvedContext.h" #include "database_drivers/DatabaseFactory.h" #include "formulas/DamageMultFormula.h" #include "formulas/SweetPieDamageFormula.h" @@ -63,6 +66,8 @@ Napi::FunctionReference ScampServer::constructor; Napi::Object ScampServer::Init(Napi::Env env, Napi::Object exports) { + ANTIGO_CONTEXT_INIT(ctx); + Napi::Function func = DefineClass( env, "ScampServer", { InstanceMethod("_setSelf", &ScampServer::_SetSelf), @@ -408,6 +413,7 @@ Napi::Value ScampServer::AttachSaveStorage(const Napi::CallbackInfo& info) Napi::Value ScampServer::Tick(const Napi::CallbackInfo& info) { + ANTIGO_CONTEXT_INIT(ctx); try { tickEnv = info.Env(); @@ -416,15 +422,29 @@ Napi::Value ScampServer::Tick(const Napi::CallbackInfo& info) try { server->Tick(PartOne::HandlePacket, partOne.get()); tickFinished = true; - } catch (std::exception& e) { + } catch (const std::exception& e) { logger->error("{}", e.what()); + while (Antigo::HasExceptionWitness()) { + auto w = Antigo::PopExceptionWitness(); + w.Print(); + } } } partOne->Tick(); - } catch (std::exception& e) { + } catch (const std::exception& e) { + while (Antigo::HasExceptionWitness()) { + auto w = Antigo::PopExceptionWitness(); + w.Print(); + } throw Napi::Error::New(info.Env(), std::string(e.what())); } + + while (Antigo::HasExceptionWitnessOrphan()) { + auto w = Antigo::PopExceptionWitnessOrphan(); + w.Print(); + } + return info.Env().Undefined(); } @@ -861,9 +881,13 @@ Napi::Value ScampServer::MakeEventSource(const Napi::CallbackInfo& info) Napi::Value ScampServer::Get(const Napi::CallbackInfo& info) { + ANTIGO_CONTEXT_INIT(ctx); + try { auto formId = NapiHelper::ExtractUInt32(info[0], "formId"); auto propertyName = NapiHelper::ExtractString(info[1], "propertyName"); + ctx.AddUnsigned(formId); + ctx.AddLambdaWithOwned([propertyName]() { return propertyName; }); static auto g_standardPropertyBindings = PropertyBindingFactory().CreateStandardPropertyBindings(); diff --git a/skymp5-server/cpp/addon/ScampServerListener.cpp b/skymp5-server/cpp/addon/ScampServerListener.cpp index f624e8b09e..639b2d49d5 100644 --- a/skymp5-server/cpp/addon/ScampServerListener.cpp +++ b/skymp5-server/cpp/addon/ScampServerListener.cpp @@ -1,6 +1,7 @@ #include "ScampServerListener.h" #include "PapyrusUtils.h" #include "ScampServer.h" +#include "antigo/Context.h" ScampServerListener::ScampServerListener(ScampServer& scampServer_) : server(scampServer_) @@ -9,6 +10,8 @@ ScampServerListener::ScampServerListener(ScampServer& scampServer_) void ScampServerListener::OnConnect(Networking::UserId userId) { + ANTIGO_CONTEXT_INIT(ctx); + auto& env = server.tickEnv; auto emit = server.emit.Value(); emit.Call( diff --git a/skymp5-server/cpp/addon/property_bindings/LocationalDataBinding.cpp b/skymp5-server/cpp/addon/property_bindings/LocationalDataBinding.cpp index 8ae7801013..117974af85 100644 --- a/skymp5-server/cpp/addon/property_bindings/LocationalDataBinding.cpp +++ b/skymp5-server/cpp/addon/property_bindings/LocationalDataBinding.cpp @@ -1,10 +1,21 @@ #include "LocationalDataBinding.h" #include "NapiHelper.h" +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" +#include Napi::Value LocationalDataBinding::Get(Napi::Env env, ScampServer& scampServer, uint32_t formId) { + ANTIGO_CONTEXT_INIT(ctx); + auto& partOne = scampServer.GetPartOne(); + if (!partOne) { + ctx.AddMessage("fatal: received partOne = nullptr, next: &scampServer"); + ctx.AddPtr(&scampServer); + ctx.Resolve().Print(); + std::terminate(); + } auto& refr = partOne->worldState.GetFormAt(formId); diff --git a/skymp5-server/cpp/server_guest_lib/ActionListener.cpp b/skymp5-server/cpp/server_guest_lib/ActionListener.cpp index 0c0434e7dd..f92fb6db63 100644 --- a/skymp5-server/cpp/server_guest_lib/ActionListener.cpp +++ b/skymp5-server/cpp/server_guest_lib/ActionListener.cpp @@ -13,6 +13,7 @@ #include "MsgType.h" #include "UserMessageOutput.h" #include "WorldState.h" +#include "antigo/ResolvedContext.h" #include "gamemode_events/CraftEvent.h" #include "gamemode_events/CustomEvent.h" #include "gamemode_events/EatItemEvent.h" @@ -21,6 +22,7 @@ #include #include #include +#include "antigo/Context.h" #include "UpdateEquipmentMessage.h" @@ -563,10 +565,16 @@ void ActionListener::OnCraftItem(const RawMessageData& rawMsgData, void ActionListener::OnHostAttempt(const RawMessageData& rawMsgData, uint32_t remoteId) { + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddMessage("next: removeId, me, me->baseId, profileId"); + ctx.AddUnsigned(remoteId); MpActor* me = partOne.serverState.ActorByUser(rawMsgData.userId); + ctx.AddPtr(me); if (!me) { throw std::runtime_error("Unable to host without actor attached"); } + ctx.AddUnsigned(me->GetBaseId()); + ctx.AddUnsigned(me->GetProfileId()); auto& remote = partOne.worldState.GetFormAt(remoteId); @@ -578,6 +586,9 @@ void ActionListener::OnHostAttempt(const RawMessageData& rawMsgData, auto& hoster = partOne.worldState.hosters[remoteId]; const uint32_t prevHoster = hoster; + ctx.AddMessage("next: prev hoster"); + ctx.AddUnsigned(prevHoster); + auto remoteIdx = remote.GetIdx(); std::optional lastRemoteUpdate; @@ -650,6 +661,8 @@ void ActionListener::OnCustomEvent(const RawMessageData& rawMsgData, const char* eventName, simdjson::dom::element& args) { + ANTIGO_CONTEXT_INIT(ctx); + auto ac = partOne.serverState.ActorByUser(rawMsgData.userId); if (!ac) { return; @@ -669,6 +682,8 @@ void ActionListener::OnCustomEvent(const RawMessageData& rawMsgData, void ActionListener::OnChangeValues(const RawMessageData& rawMsgData, const ActorValues& newActorValues) { + ANTIGO_CONTEXT_INIT(ctx); + MpActor* actor = partOne.serverState.ActorByUser(rawMsgData.userId); if (!actor) { throw std::runtime_error("Unable to change values without Actor attached"); @@ -880,6 +895,7 @@ void ActionListener::OnHit(const RawMessageData& rawMsgData_, if (!myActor) { throw std::runtime_error("Unable to change values without Actor attached"); } + ANTIGO_CONTEXT_INIT(ctx); MpActor* aggressor = nullptr; @@ -895,6 +911,7 @@ void ActionListener::OnHit(const RawMessageData& rawMsgData_, spdlog::error("SendToNeighbours - No permission to send OnHit with " "aggressor actor {:x}", aggressor->GetFormId()); + ctx.Resolve().Print(); return; } } diff --git a/skymp5-server/cpp/server_guest_lib/EvaluateTemplate.h b/skymp5-server/cpp/server_guest_lib/EvaluateTemplate.h index 6c71ac3ad6..09732d7de1 100644 --- a/skymp5-server/cpp/server_guest_lib/EvaluateTemplate.h +++ b/skymp5-server/cpp/server_guest_lib/EvaluateTemplate.h @@ -1,16 +1,33 @@ #pragma once #include "FormDesc.h" #include "WorldState.h" +#include "antigo/Context.h" +#include "antigo/ExecutionData.h" +#include "antigo/ResolvedContext.h" +#include "libespm/LookupResult.h" +#include "libespm/NPC_.h" #include "libespm/espm.h" +#include #include #include #include +// template +// auto CallbackWrapper(const Callback& callback, const espm::LookupResult& npcLookupResult, const espm::NPC_::Data& npcData, std::string detailedLog) { +// if constexpr (std::tuple_size::type>::value == 2) { +// return callback(npcLookupResult, npcData); +// } else { +// return callback(npcLookupResult, npcData, std::move(detailedLog)); +// } +// } + template auto EvaluateTemplate(WorldState* worldState, uint32_t baseId, const std::vector& templateChain, const Callback& callback) { + ANTIGO_CONTEXT_INIT(ctx); + const std::vector chainDefault = { FormDesc::FromFormId( baseId, worldState->espmFiles) }; const std::vector& chain = @@ -40,12 +57,23 @@ auto EvaluateTemplate(WorldState* worldState, uint32_t baseId, detailedLog << "Variable npcData: baseTemplate=" << npcData.baseTemplate << ", templateDataFlags=" << npcData.templateDataFlags << "\n"; - if (npcData.baseTemplate == 0) { - return callback(npcLookupResult, npcData); - } - - if (!(npcData.templateDataFlags & TemplateFlag)) { - return callback(npcLookupResult, npcData); + ctx.AddLambdaWithOwned([s = detailedLog.str()] { + return s; + }); + + try { + if (npcData.baseTemplate == 0) { + return callback(npcLookupResult, npcData); + } + + if (!(npcData.templateDataFlags & TemplateFlag)) { + return callback(npcLookupResult, npcData); + } + } catch (const std::exception& e) { + detailedLog << "Callback failed: " << e.what() << "\n"; + while (Antigo::HasExceptionWitness()) { + detailedLog << Antigo::PopExceptionWitness().ToString() << "\n"; + } } } diff --git a/skymp5-server/cpp/server_guest_lib/MpActor.cpp b/skymp5-server/cpp/server_guest_lib/MpActor.cpp index 79d2a1da0f..b8e5a12cfe 100644 --- a/skymp5-server/cpp/server_guest_lib/MpActor.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpActor.cpp @@ -17,6 +17,7 @@ #include "SweetPieScript.h" #include "TimeUtils.h" #include "WorldState.h" +#include "antigo/Context.h" #include "gamemode_events/DeathEvent.h" #include "gamemode_events/DropItemEvent.h" #include "gamemode_events/EatItemEvent.h" @@ -190,6 +191,8 @@ void MpActor::EquipBestWeapon() void MpActor::AddSpell(const uint32_t spellId) { + ANTIGO_CONTEXT_INIT(ctx); + EditChangeForm([&](MpChangeForm& changeForm) { changeForm.learnedSpells.LearnSpell(spellId); }); @@ -197,6 +200,8 @@ void MpActor::AddSpell(const uint32_t spellId) void MpActor::RemoveSpell(const uint32_t spellId) { + ANTIGO_CONTEXT_INIT(ctx); + EditChangeForm([&](MpChangeForm& changeForm) { changeForm.learnedSpells.ForgetSpell(spellId); }); @@ -517,6 +522,9 @@ MpChangeForm MpActor::GetChangeForm() const void MpActor::ApplyChangeForm(const MpChangeForm& newChangeForm) { + ANTIGO_CONTEXT_INIT(ctx); + auto g = ctx.AddLambdaWithRef([&newChangeForm]() { return MpChangeForm::ToJson(newChangeForm).dump(2); }); + g.Arm(); if (newChangeForm.recType != MpChangeForm::ACHR) { throw std::runtime_error( "Expected record type to be ACHR, but found REFR"); diff --git a/skymp5-server/cpp/server_guest_lib/MpForm.cpp b/skymp5-server/cpp/server_guest_lib/MpForm.cpp index 165223af25..560545eccc 100644 --- a/skymp5-server/cpp/server_guest_lib/MpForm.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpForm.cpp @@ -3,8 +3,10 @@ #include "GetWeightFromRecord.h" #include "MpObjectReference.h" #include "WorldState.h" +#include "antigo/Context.h" #include "gamemode_events/PapyrusEventEvent.h" #include "script_objects/MpFormGameObject.h" +#include MpForm::MpForm() { @@ -34,6 +36,21 @@ void MpForm::Update() void MpForm::SendPapyrusEvent(const char* eventName, const VarValue* arguments, size_t argumentsCount) { + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddPtr(arguments); + ctx.AddUnsigned(argumentsCount); + auto g = ctx.AddLambdaWithRef([eventName, arguments, argumentsCount]{ + std::stringstream ss; + ss << "eventName = " << eventName << "\n"; + ss << "arguments = [" << argumentsCount << "] [\n"; + for (size_t i = 0; i < argumentsCount; ++i) { + ss << " " << arguments[i] << "\n"; + } + ss << "]"; + return std::move(ss).str(); + }); + g.Arm(); + PapyrusEventEvent papyrusEventEvent(this, eventName, arguments, argumentsCount); papyrusEventEvent.Fire(parent); diff --git a/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp b/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp index 605855f3f2..8e53d44f9a 100644 --- a/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp @@ -16,12 +16,16 @@ #include "TimeUtils.h" #include "UpdatePropertyMessage.h" #include "WorldState.h" +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" #include "gamemode_events/ActivateEvent.h" #include "gamemode_events/PutItemEvent.h" #include "gamemode_events/TakeItemEvent.h" #include "libespm/CompressedFieldsCache.h" #include "libespm/Convert.h" #include "libespm/GroupUtils.h" +#include "libespm/LookupResult.h" +#include "libespm/NPC_.h" #include "libespm/Utils.h" #include "papyrus-vm/Reader.h" #include "papyrus-vm/Utils.h" // stricmp @@ -31,6 +35,7 @@ #include #include #include +#include #include "OpenContainerMessage.h" #include "TeleportMessage.h" @@ -140,6 +145,7 @@ struct MpObjectReference::Impl std::optional primitive; bool teleportFlag = false; bool setPropertyCalled = false; + Antigo::ResolvedContext setPropertyCaller; }; namespace { @@ -705,6 +711,7 @@ void MpObjectReference::SetProperty(const std::string& propertyName, bool isVisibleByOwner, bool isVisibleByNeighbor) { + ANTIGO_CONTEXT_INIT(ctx); auto msg = CreatePropertyMessage(this, propertyName.c_str(), newValue); EditChangeForm([&](MpChangeFormREFR& changeForm) { changeForm.dynamicFields.Set(propertyName, std::move(newValue)); @@ -717,6 +724,7 @@ void MpObjectReference::SetProperty(const std::string& propertyName, } } pImpl->setPropertyCalled = true; + pImpl->setPropertyCaller = ctx.Resolve(); } void MpObjectReference::SetTeleportFlag(bool value) @@ -1122,6 +1130,8 @@ MpObjectReference::GetNextRelootMoment() const MpChangeForm MpObjectReference::GetChangeForm() const { + ANTIGO_CONTEXT_INIT(ctx); + MpChangeForm res = ChangeForm(); if (GetParent() && !GetParent()->espmFiles.empty()) { @@ -1131,13 +1141,24 @@ MpChangeForm MpObjectReference::GetChangeForm() const res.formDesc = res.baseDesc = FormDesc(GetFormId(), ""); } + // res.noSaveRequestor = ctx.Resolve(); + return res; } void MpObjectReference::ApplyChangeForm(const MpChangeForm& changeForm) { + ANTIGO_CONTEXT_INIT(ctx); + auto g = ctx.AddLambdaWithRef([&changeForm]() { return MpChangeForm::ToJson(changeForm).dump(2); }); + g.Arm(); + if (pImpl->setPropertyCalled) { GetParent()->logger->critical("ApplyChangeForm called after SetProperty"); + auto g = ctx.AddLambdaWithRef([this]() { return pImpl->setPropertyCaller.ToString(); }); + g.Arm(); + ctx.Resolve().Print(); + std::cout << std::flush; + std::cerr << std::flush; std::terminate(); } @@ -1147,6 +1168,9 @@ void MpObjectReference::ApplyChangeForm(const MpChangeForm& changeForm) const auto currentBaseId = GetBaseId(); const auto newBaseId = changeForm.baseDesc.ToFormId(GetParent()->espmFiles); + ctx.AddMessage("next: current, new base id"); + ctx.AddUnsigned(currentBaseId); + ctx.AddUnsigned(newBaseId); if (currentBaseId != newBaseId) { spdlog::error("Anomaly, baseId should never change ({:x} => {:x})", currentBaseId, newBaseId); @@ -1261,6 +1285,7 @@ void MpObjectReference::SendPapyrusEvent(const char* eventName, const VarValue* arguments, size_t argumentsCount) { + ANTIGO_CONTEXT_INIT(ctx); if (!pImpl->scriptsInited) { InitScripts(); pImpl->scriptsInited = true; @@ -1636,6 +1661,8 @@ void MpObjectReference::UnsubscribeFromAll() void MpObjectReference::InitScripts() { + ANTIGO_CONTEXT_INIT(ctx); + auto baseId = GetBaseId(); if (!baseId || !GetParent()->espm) { return; @@ -1684,6 +1711,10 @@ void MpObjectReference::InitScripts() auto& scriptsInStorage = GetParent()->GetScriptStorage()->ListScripts(false); for (auto& script : scriptData->scripts) { + ANTIGO_CONTEXT_INIT(ctx); + auto g = ctx.AddLambdaWithRef([&script]() { return script.scriptName; }); + g.Arm(); + if (scriptsInStorage.count( { script.scriptName.begin(), script.scriptName.end() })) { @@ -1691,9 +1722,13 @@ void MpObjectReference::InitScripts() script.scriptName) == 0) { scriptNames.push_back(script.scriptName); } - } else if (auto wst = GetParent()) - wst->logger->warn("Script '{}' not found in the script storage", - script.scriptName); + } else { + if (auto wst = GetParent()) { + wst->logger->warn("Script '{}' not found in the script storage", + script.scriptName); + } + ctx.Resolve().Print(); + } } } @@ -1721,6 +1756,7 @@ void MpObjectReference::InitScripts() // A hardcoded hack to remove unsupported scripts // TODO: make is a server setting with proper conditions or an API + /* auto isScriptEraseNeeded = [](const std::string& val) { // 1. GetStage in OnTrigger // 2. Unable to determine Actor for 'Game.GetPlayer' in 'OnLoad' @@ -1735,6 +1771,7 @@ void MpObjectReference::InitScripts() scriptNames.erase(std::remove_if(scriptNames.begin(), scriptNames.end(), isScriptEraseNeeded), scriptNames.end()); + */ if (!scriptNames.empty()) { pImpl->scriptState = std::make_unique(); @@ -1796,6 +1833,8 @@ std::vector GetOutfitObjects( WorldState* worldState, const std::vector& templateChain, const espm::LookupResult& lookupRes) { + ANTIGO_CONTEXT_INIT(ctx); + auto& compressedFieldsCache = worldState->GetEspmCache(); std::vector res; @@ -1804,8 +1843,15 @@ std::vector GetOutfitObjects( auto baseId = lookupRes.ToGlobalId(lookupRes.rec->GetId()); auto outfitId = EvaluateTemplate( worldState, baseId, templateChain, - [](const auto& npcLookupRes, const auto& npcData) { - return npcLookupRes.ToGlobalId(npcData.defaultOutfitId); + [](const espm::LookupResult& npcLookupRes, const espm::NPC_::Data& npcData) { + ANTIGO_CONTEXT_INIT(ctx); + auto result = npcLookupRes.ToGlobalId(npcData.defaultOutfitId); + if (result == 0) { + ctx.AddMessage("evaluated NPC template - outfitId (global) is 0; next: npcData.defaultOutfitId"); + ctx.AddUnsigned(npcData.defaultOutfitId); + ctx.Orphan(); + } + return result; }); auto outfitLookupRes = @@ -1866,6 +1912,8 @@ void MpObjectReference::AddContainerObject( void MpObjectReference::EnsureBaseContainerAdded(espm::Loader& espm) { + ANTIGO_CONTEXT_INIT(ctx); + if (ChangeForm().baseContainerAdded) { return; } @@ -1875,12 +1923,23 @@ void MpObjectReference::EnsureBaseContainerAdded(espm::Loader& espm) return; } + ctx.AddMessage("next: AsActor(), templateChain, lookupRes.rec"); + auto actor = AsActor(); + ctx.AddPtr(actor); const std::vector kEmptyTemplateChain; const std::vector& templateChain = actor ? actor->GetTemplateChain() : kEmptyTemplateChain; + ctx.AddLambdaWithOwned([templateChain]{ + std::string s = "[\n"; + for (const auto& elt : templateChain) { + s += " " + elt.ToString() + "\n"; + } + return s + "]"; + }); auto lookupRes = espm.GetBrowser().LookupById(GetBaseId()); + ctx.AddPtr(lookupRes.rec); std::map itemsToAdd, itemsToEquip; diff --git a/skymp5-server/cpp/server_guest_lib/MpObjectReference.h b/skymp5-server/cpp/server_guest_lib/MpObjectReference.h index 87761b3ca7..33b746acd4 100644 --- a/skymp5-server/cpp/server_guest_lib/MpObjectReference.h +++ b/skymp5-server/cpp/server_guest_lib/MpObjectReference.h @@ -74,7 +74,7 @@ using SetAngleMode = SetPosMode; class MpObjectReference : public MpForm - , public FormIndex + , public FormIndex//бля , protected ChangeFormGuard { friend class OccupantDestroyEventSink; diff --git a/skymp5-server/cpp/server_guest_lib/PartOne.cpp b/skymp5-server/cpp/server_guest_lib/PartOne.cpp index 57e7e496f7..e2a575865f 100644 --- a/skymp5-server/cpp/server_guest_lib/PartOne.cpp +++ b/skymp5-server/cpp/server_guest_lib/PartOne.cpp @@ -7,9 +7,12 @@ #include "MessageSerializerFactory.h" #include "MsgType.h" #include "PacketParser.h" +#include "antigo/Context.h" #include #include #include +#include +#include #include #include @@ -103,6 +106,8 @@ bool PartOne::IsConnected(Networking::UserId userId) const void PartOne::Tick() { + ANTIGO_CONTEXT_INIT(ctx); + TickPacketHistoryPlaybacks(); TickDeferredMessages(); worldState.Tick(); @@ -279,6 +284,8 @@ void PartOne::AttachEspm(espm::Loader* espm) void PartOne::AttachSaveStorage(std::shared_ptr saveStorage) { + ANTIGO_CONTEXT_INIT(ctx); + worldState.AttachSaveStorage(saveStorage); auto start = std::chrono::steady_clock::now(); @@ -395,6 +402,24 @@ void PartOne::HandlePacket(void* partOneInstance, Networking::UserId userId, Networking::PacketType packetType, Networking::PacketData data, size_t length) { + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddPtr(partOneInstance); + ctx.AddUnsigned(userId); + ctx.AddUnsigned(static_cast(packetType)); + auto g = ctx.AddLambdaWithRef([&data, &length]() { + std::string s = "byte[" + std::to_string(length) + "] "; + for (size_t i = 0; i < length; ++i) { + if (i == 100) { + s += "..."; + break; + } + s += "0123456789abcdef"[(data[i] & 0xf0) >> 8]; + s += "0123456789abcdef"[data[i] & 0x0f]; + } + return s; + }); + g.Arm(); + auto this_ = reinterpret_cast(partOneInstance); switch (packetType) { @@ -655,6 +680,8 @@ std::vector& PartOne::Messages() void PartOne::Init() { + ANTIGO_CONTEXT_INIT(ctx); + pImpl.reset(new Impl); pImpl->logger.reset(new spdlog::logger{ "empty logger" }); @@ -832,6 +859,8 @@ void PartOne::Init() void PartOne::AddUser(Networking::UserId userId, UserType type, const std::string& guid) { + ANTIGO_CONTEXT_INIT(ctx); + serverState.Connect(userId, guid); for (auto& listener : worldState.listeners) listener->OnConnect(userId); @@ -847,6 +876,8 @@ void PartOne::AddUser(Networking::UserId userId, UserType type, void PartOne::HandleMessagePacket(Networking::UserId userId, Networking::PacketData data, size_t length) { + ANTIGO_CONTEXT_INIT(ctx); + if (!serverState.IsConnected(userId)) { throw std::runtime_error("User with id " + std::to_string(userId) + " doesn't exist"); diff --git a/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.cpp b/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.cpp index 2d234ad2e5..93f7c84418 100644 --- a/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.cpp +++ b/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.cpp @@ -1,6 +1,8 @@ #include "ScriptVariablesHolder.h" #include "WorldState.h" +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" #include "libespm/Property.h" #include "papyrus-vm/Utils.h" #include "script_objects/EspmGameObject.h" @@ -25,6 +27,9 @@ ScriptVariablesHolder::ScriptVariablesHolder( VarValue* ScriptVariablesHolder::GetVariableByName(const char* name, const PexScript& pex) { + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddLambdaWithOwned([name = std::string{name}]() { return name; }); + if (!scriptsCache) { scriptsCache = std::make_unique(); } @@ -128,6 +133,8 @@ VarValue ScriptVariablesHolder::CastPrimitivePropertyValue( const espm::Property::Value& propValue, espm::Property::Type type, const std::function& toGlobalId, WorldState* worldState) { + ANTIGO_CONTEXT_INIT(ctx); + switch (type) { case espm::Property::Type::Object: { if (!propValue.formId) { @@ -164,6 +171,7 @@ VarValue ScriptVariablesHolder::CastPrimitivePropertyValue( "not found in the world. LookupFormById trace:\n{}", myScriptName, type.ToString(), propValueFormIdGlobal, traceStream.str()); + ctx.Resolve().Print(); } } else { spdlog::error( diff --git a/skymp5-server/cpp/server_guest_lib/SpSnippetFunctionGen.cpp b/skymp5-server/cpp/server_guest_lib/SpSnippetFunctionGen.cpp index 054f176a9f..472895a1ed 100644 --- a/skymp5-server/cpp/server_guest_lib/SpSnippetFunctionGen.cpp +++ b/skymp5-server/cpp/server_guest_lib/SpSnippetFunctionGen.cpp @@ -2,6 +2,7 @@ #include "SpSnippet.h" #include "WorldState.h" +#include "antigo/Context.h" #include "script_objects/EspmGameObject.h" #include "script_objects/MpFormGameObject.h" #include @@ -23,6 +24,8 @@ uint32_t SpSnippetFunctionGen::GetFormId(VarValue varValue) std::string SpSnippetFunctionGen::SerializeArguments( const std::vector& arguments, MpActor* actor) { + ANTIGO_CONTEXT_INIT(ctx); + std::stringstream ss; ss << '['; for (auto& arg : arguments) { diff --git a/skymp5-server/cpp/server_guest_lib/WorldState.cpp b/skymp5-server/cpp/server_guest_lib/WorldState.cpp index 81eba3ea56..442d791b59 100644 --- a/skymp5-server/cpp/server_guest_lib/WorldState.cpp +++ b/skymp5-server/cpp/server_guest_lib/WorldState.cpp @@ -8,6 +8,8 @@ #include "MpObjectReference.h" #include "ScopedTask.h" #include "Timer.h" +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" #include "database_drivers/IDatabase.h" // UpsertFailedException #include "libespm/GroupUtils.h" #include "papyrus-vm/Reader.h" @@ -98,6 +100,12 @@ void WorldState::AddForm(std::unique_ptr form, uint32_t formId, bool skipChecks, const MpChangeForm* optionalChangeFormToApply) { + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddMessage("next: formId, skipChecks, optionalChangeFormToApply"); + ctx.AddUnsigned(formId); + ctx.AddUnsigned(skipChecks); + ctx.AddPtr(optionalChangeFormToApply); + if (!skipChecks && forms.find(formId) != forms.end()) { throw std::runtime_error( fmt::format("Form with id {:x} already exists", formId)); @@ -138,7 +146,9 @@ void WorldState::AddForm(std::unique_ptr form, uint32_t formId, void WorldState::Tick() { + ANTIGO_CONTEXT_INIT(ctx); const auto now = std::chrono::system_clock::now(); + ctx.AddUnsigned(now.time_since_epoch().count()); TickSaveStorage(now); TickTimers(now); } @@ -146,6 +156,10 @@ void WorldState::Tick() void WorldState::LoadChangeForm(const MpChangeForm& changeForm, const FormCallbacks& callbacks) { + ANTIGO_CONTEXT_INIT(ctx); + auto g = ctx.AddLambdaWithRef([&changeForm]() { return MpChangeForm::ToJson(changeForm).dump(2); }); + g.Arm(); + Viet::ScopedTask task([](bool& st) { st = false; }, pImpl->formLoadingInProgress); pImpl->formLoadingInProgress = true; @@ -277,6 +291,8 @@ void WorldState::RequestReloot(MpObjectReference& ref, void WorldState::RequestSave(MpObjectReference& ref) { + ANTIGO_CONTEXT_INIT(ctx); + if (pImpl->formLoadingInProgress) { return; } @@ -284,7 +300,7 @@ void WorldState::RequestSave(MpObjectReference& ref) auto idx = ref.GetIdx(); [[unlikely]] if (idx == FormIndex::g_invalidIdx) { - return spdlog::error("RequestSave {:x} - Invalid index", ref.GetFormId()); + return spdlog::error("RequestSave {:x} - Invalid index\n{}", ref.GetFormId(), ctx.Resolve().ToString()); } if (pImpl->changesByIdx.size() <= idx) { @@ -298,6 +314,19 @@ void WorldState::RequestSave(MpObjectReference& ref) const std::shared_ptr& WorldState::LookupFormById( uint32_t formId, std::stringstream* optionalOutTrace) { + ANTIGO_CONTEXT_INIT(ctx); + + struct Deferred { + Antigo::OnstackContext& ctx; + std::stringstream* optionalOutTrace; + + ~Deferred() { + if (optionalOutTrace) { + *optionalOutTrace << "look at my horse, my horse is amazing:\n" << ctx.Resolve(); + } + } + } def{ctx, optionalOutTrace}; + static const std::shared_ptr kNullForm; if (optionalOutTrace) { @@ -310,33 +339,20 @@ const std::shared_ptr& WorldState::LookupFormById( if (LoadForm(formId, optionalOutTrace)) { it = forms.find(formId); if (it != forms.end()) { - if (optionalOutTrace) { - *optionalOutTrace << "found after successful LoadForm" << std::hex - << formId << std::endl; - } + ctx.AddMessage("found after successful LoadForm"); return it->second; } - if (optionalOutTrace) { - *optionalOutTrace << "not found after successful LoadForm" - << std::hex << formId << std::endl; - } + ctx.AddMessage("not found after successful LoadForm"); return kNullForm; } else { - if (optionalOutTrace) { - *optionalOutTrace << "LoadForm returned false " << std::hex << formId - << std::endl; - } + ctx.AddMessage("LoadForm returned false"); } } - if (optionalOutTrace) { - *optionalOutTrace << "not found " << std::hex << formId << std::endl; - } + ctx.AddMessage("not found"); return kNullForm; } - if (optionalOutTrace) { - *optionalOutTrace << "found " << std::hex << formId << std::endl; - } + ctx.AddMessage("found"); return it->second; } @@ -648,18 +664,29 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, bool WorldState::LoadForm(uint32_t formId, std::stringstream* optionalOutTrace) { + ANTIGO_CONTEXT_INIT(ctx); + ctx.AddMessage("next: formId"); + ctx.AddUnsigned(formId); + auto& br = GetEspm().GetBrowser(); auto lookupRes = br.LookupById(formId); + ctx.AddMessage("next: lookupRes.rec, ->id"); + ctx.AddPtr(lookupRes.rec); if (!lookupRes.rec) { return false; } + ctx.AddUnsigned(lookupRes.rec->GetId()); auto mapping = br.GetCombMapping(lookupRes.fileIdx); + ctx.AddMessage("next: mapping"); + ctx.AddPtr(mapping); bool attached = AttachEspmRecord(br, lookupRes.rec, *mapping, optionalOutTrace); + ctx.AddMessage("next: attached"); + ctx.AddUnsigned(attached); if (optionalOutTrace) { *optionalOutTrace << "AttachEspmRecord " << (attached ? "true" : "false") << std::endl; @@ -685,6 +712,8 @@ bool WorldState::LoadForm(uint32_t formId, std::stringstream* optionalOutTrace) void WorldState::TickSaveStorage(const std::chrono::system_clock::time_point&) { + ANTIGO_CONTEXT_INIT(ctx); + if (!pImpl->saveStorage) { return; } @@ -775,6 +804,7 @@ void WorldState::TickSaveStorage(const std::chrono::system_clock::time_point&) void WorldState::TickTimers(const std::chrono::system_clock::time_point&) { + ANTIGO_CONTEXT_INIT(ctx); timerEffects.TickTimers(); timerRegular.TickTimers(); diff --git a/skymp5-server/cpp/server_guest_lib/database_drivers/MongoDatabase.cpp b/skymp5-server/cpp/server_guest_lib/database_drivers/MongoDatabase.cpp index b067d07da4..4b3f8b7b55 100644 --- a/skymp5-server/cpp/server_guest_lib/database_drivers/MongoDatabase.cpp +++ b/skymp5-server/cpp/server_guest_lib/database_drivers/MongoDatabase.cpp @@ -1,6 +1,7 @@ #include "MongoDatabase.h" #include "JsonUtils.h" +#include "antigo/Context.h" #ifndef NO_MONGO # include @@ -106,6 +107,8 @@ std::vector>&& MongoDatabase::UpsertImpl( void MongoDatabase::Iterate(const IterateCallback& iterateCallback) { + ANTIGO_CONTEXT_INIT(ctx); + constexpr int kBatchSize = 1001; mongocxx::options::find findOptions; findOptions.batch_size(kBatchSize); diff --git a/skymp5-server/cpp/server_guest_lib/formulas/SweetPieDamageFormula.cpp b/skymp5-server/cpp/server_guest_lib/formulas/SweetPieDamageFormula.cpp index d1c93ddedf..94b49de5a6 100644 --- a/skymp5-server/cpp/server_guest_lib/formulas/SweetPieDamageFormula.cpp +++ b/skymp5-server/cpp/server_guest_lib/formulas/SweetPieDamageFormula.cpp @@ -11,6 +11,7 @@ #include "HitData.h" #include "MpActor.h" #include "SpellCastData.h" +#include "antigo/Context.h" #include "libespm/Loader.h" #include "libespm/espm.h" @@ -27,6 +28,8 @@ SweetPieDamageFormula::SweetPieDamageFormula( SweetPieDamageFormulaSettings SweetPieDamageFormula::ParseConfig( const nlohmann::json& config) const { + ANTIGO_CONTEXT_INIT(ctx); + SweetPieDamageFormulaSettings result{}; for (const auto& level : config["damageMultByLevel"]) { diff --git a/skymp5-server/cpp/server_guest_lib/save_storages/AsyncSaveStorage.cpp b/skymp5-server/cpp/server_guest_lib/save_storages/AsyncSaveStorage.cpp index 20b67cafa0..21b5c9bf7a 100644 --- a/skymp5-server/cpp/server_guest_lib/save_storages/AsyncSaveStorage.cpp +++ b/skymp5-server/cpp/server_guest_lib/save_storages/AsyncSaveStorage.cpp @@ -1,4 +1,6 @@ #include "AsyncSaveStorage.h" +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" #include #include #include @@ -50,12 +52,19 @@ AsyncSaveStorage::AsyncSaveStorage(const std::shared_ptr& dbImpl, std::string name) : pImpl(new Impl, [](Impl* p) { delete p; }) { + ANTIGO_CONTEXT_INIT(ctx); + pImpl->name = std::move(name); pImpl->logger = logger; pImpl->share.dbImpl = dbImpl; auto p = this->pImpl.get(); - pImpl->thr.reset(new std::thread([p] { SaverThreadMain(p); })); + pImpl->thr.reset(new std::thread([p, callerCtx = ctx.Resolve()] { + ANTIGO_CONTEXT_INIT(ctx); + auto g = ctx.AddLambdaWithRef([&callerCtx]() { return callerCtx.ToString(); }); + g.Arm(); + SaverThreadMain(p); + })); } AsyncSaveStorage::~AsyncSaveStorage() diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.cpp index 607c7828fc..67d482030a 100644 --- a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.cpp @@ -1,6 +1,8 @@ #include "PapyrusActor.h" #include "MpActor.h" +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" #include "script_objects/EspmGameObject.h" #include "script_objects/MpFormGameObject.h" @@ -179,6 +181,8 @@ VarValue PapyrusActor::SetAlpha(VarValue self, VarValue PapyrusActor::EquipItem(VarValue self, const std::vector& arguments) { + ANTIGO_CONTEXT_INIT(ctx); + if (auto actor = GetFormPtr(self)) { auto worldState = actor->GetParent(); if (!worldState) { @@ -225,19 +229,21 @@ VarValue PapyrusActor::EquipItem(VarValue self, VarValue PapyrusActor::EquipSpell(VarValue self, const std::vector& arguments) { + ANTIGO_CONTEXT_INIT(ctx); + if (arguments.size() < 2) { - spdlog::error("EquipSpell requires at least 2 arguments"); + spdlog::error("EquipSpell requires at least 2 arguments\n{}", ctx.Resolve().ToString()); return VarValue::None(); } auto lookupRes = GetRecordPtr(arguments[0]); if (!lookupRes.rec) { - spdlog::error("EquipSpell - invalid form"); + spdlog::error("EquipSpell - invalid form\n{}", ctx.Resolve().ToString()); return VarValue::None(); } if (lookupRes.rec->GetType() != espm::SPEL::kType) { - spdlog::error("EquipSpell - form is not a spell"); + spdlog::error("EquipSpell - form is not a spell\n{}", ctx.Resolve().ToString()); return VarValue::None(); } @@ -260,8 +266,10 @@ VarValue PapyrusActor::EquipSpell(VarValue self, VarValue PapyrusActor::UnequipItem(VarValue self, const std::vector& arguments) { + ANTIGO_CONTEXT_INIT(ctx); + if (arguments.size() < 3) { - spdlog::error("UnequipItem requires at least 3 arguments"); + spdlog::error("UnequipItem requires at least 3 arguments\n{}", ctx.Resolve().ToString()); return VarValue::None(); } @@ -498,32 +506,41 @@ VarValue PapyrusActor::AddSpell(VarValue self, VarValue PapyrusActor::RemoveSpell(VarValue self, const std::vector& arguments) { + ANTIGO_CONTEXT_INIT(ctx); + if (auto actor = GetFormPtr(self)) { + ctx.AddMessage("next: actor form id, base id; spell lookup rec ptr, id"); + ctx.AddUnsigned(actor->GetFormId()); + ctx.AddUnsigned(actor->GetBaseId()); if (arguments.size() < 1) { throw std::runtime_error( "Actor.RemoveSpell requires at least one argument"); } const auto& spell = GetRecordPtr(arguments[0]); + ctx.AddPtr(spell.rec); if (!spell.rec) { - spdlog::error("Actor.RemoveSpell - invalid spell form"); + spdlog::error("Actor.RemoveSpell - invalid spell form\n{}", ctx.Resolve().ToString()); return VarValue(false); } + ctx.AddUnsigned(spell.rec->GetId()); if (spell.rec->GetType().ToString() != "SPEL") { spdlog::error( - "Actor.RemoveSpell - type expected to be SPEL, but it is {}", - spell.rec->GetType().ToString()); + "Actor.RemoveSpell - type expected to be SPEL, but it is {}\n{}", + spell.rec->GetType().ToString(), ctx.Resolve().ToString()); return VarValue(false); } uint32_t spellId = spell.ToGlobalId(spell.rec->GetId()); + ctx.AddMessage("next: spellId"); + ctx.AddUnsigned(spellId); if (actor->IsSpellLearnedFromBase(spellId)) { spdlog::warn("Actor.RemoveSpell - can't remove spells inherited from " - "RACE/NPC_ records"); + "RACE/NPC_ records\n{}", ctx.Resolve().ToString()); } else if (!actor->IsSpellLearned(spellId)) { - spdlog::warn("Actor.RemoveSpell - spell already removed/not learned"); + spdlog::warn("Actor.RemoveSpell - spell already removed/not learned\n{}", ctx.Resolve().ToString()); } else { actor->RemoveSpell(spellId); diff --git a/skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.cpp b/skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.cpp index 93aa797d05..8e4deb18e8 100644 --- a/skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.cpp @@ -1,9 +1,14 @@ #include "EspmGameObject.h" -#include #include #include +#include + +#include "antigo/Context.h" +#include "antigo/ResolvedContext.h" +#include "libespm/CombineBrowser.h" + EspmGameObject::EspmGameObject(const espm::LookupResult& record_) : record(record_) { @@ -11,9 +16,17 @@ EspmGameObject::EspmGameObject(const espm::LookupResult& record_) const char* EspmGameObject::GetParentNativeScript() { + ANTIGO_CONTEXT_INIT(ctx); + + ctx.AddMessage("record ptr = next"); + ctx.AddPtr(record.rec); + if (record.rec) { auto t = record.rec->GetType(); + ctx.AddMessage("record type = next"); + ctx.AddLambdaWithOwned([t]() { return t.ToString(); }); + if (t == "GRUP") return "group"; if (t == "GMST") @@ -283,6 +296,8 @@ const char* EspmGameObject::GetParentNativeScript() // P.S. there is some code outside espm that relies on REFR/ACHR + ctx.AddLambdaWithOwned([b = espm::g_lastForm0Lookup]{ return b.ToString(); }); + throw std::runtime_error("Unable to find native script for record type " + t.ToString()); } @@ -299,29 +314,31 @@ bool EspmGameObject::EqualsByValue(const IGameObject& obj) const const espm::LookupResult& GetRecordPtr(const VarValue& papyrusObject) { + ANTIGO_CONTEXT_INIT(ctx); + static const espm::LookupResult emptyResult; if (papyrusObject.GetType() != VarValue::kType_Object) { std::stringstream papyrusObjectStr; papyrusObjectStr << papyrusObject; - spdlog::warn("GetRecordPtr called with non-object ({})", - papyrusObjectStr.str()); + spdlog::warn("GetRecordPtr called with non-object ({})\n{}", + papyrusObjectStr.str(), ctx.Resolve().ToString()); return emptyResult; } auto gameObject = static_cast(papyrusObject); if (!gameObject) { std::stringstream papyrusObjectStr; papyrusObjectStr << papyrusObject; - spdlog::warn("GetRecordPtr called with null object ({})", - papyrusObjectStr.str()); + spdlog::warn("GetRecordPtr called with null object ({})\n{}", + papyrusObjectStr.str(), ctx.Resolve().ToString()); return emptyResult; } auto espmGameObject = dynamic_cast(gameObject); if (!espmGameObject) { std::stringstream papyrusObjectStr; papyrusObjectStr << papyrusObject; - spdlog::warn("GetRecordPtr called with non-espm object ({})", - papyrusObjectStr.str()); + spdlog::warn("GetRecordPtr called with non-espm object ({})\n{}", + papyrusObjectStr.str(), ctx.Resolve().ToString()); return emptyResult; } return espmGameObject->record; @@ -349,6 +366,7 @@ EspmGameObject::ListActivePexInstances() const void EspmGameObject::AddScript( std::shared_ptr sctipt) noexcept { - spdlog::critical("EspmGameObject::AddScript is not supported"); + ANTIGO_CONTEXT_INIT(ctx); + spdlog::critical("EspmGameObject::AddScript is not supported\n{}", ctx.Resolve().ToString()); std::terminate(); } diff --git a/skymp5-server/ts/settings.ts b/skymp5-server/ts/settings.ts index 3cbaa660ed..fe2db84d9b 100644 --- a/skymp5-server/ts/settings.ts +++ b/skymp5-server/ts/settings.ts @@ -96,6 +96,7 @@ export class Settings { parser.add_argument('--port'); parser.add_argument('--ip'); parser.add_argument('--offlineMode'); + parser.add_argument('--config-type'); return parser.parse_args(); } diff --git a/vcpkg.json b/vcpkg.json index b3be7a0702..adee47fd55 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -16,6 +16,7 @@ "bshoshany-thread-pool", "makeid", "libzippp", + "cpptrace", "slikenet", { "name": "mongo-cxx-driver",