./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
1 / 1
100.0%
Branches:
-
-%
Functions:
1 / 1
100.0%

include/DetourModKit/session.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_SESSION_HPP
2 #define DETOURMODKIT_SESSION_HPP
3
4 /**
5 * @file session.hpp
6 * @brief Process-lifecycle surface: the RAII Session, the ModInfo descriptor, and the DllMain bootstrap entry points.
7 */
8
9 #include "DetourModKit/async_logger_config.hpp"
10 #include "DetourModKit/config.hpp"
11 #include "DetourModKit/error.hpp"
12 #include "DetourModKit/input.hpp"
13 #include "DetourModKit/logger.hpp"
14
15 #include <chrono>
16 #include <cstdint>
17 #include <functional>
18 #include <span>
19 #include <string_view>
20
21 struct HINSTANCE__;
22
23 namespace DetourModKit
24 {
25 /** @brief Opaque Win32 module handle, identical to HMODULE. This header does not include <windows.h>. */
26 using ModuleHandle = ::HINSTANCE__ *;
27
28 namespace detail
29 {
30 struct SessionBootstrapAccess;
31 } // namespace detail
32
33 /**
34 * @brief Identity, single-instance gating, process gating, and async-logger settings for a mod.
35 * @details Every entry point copies each field that it keeps before it returns, so a field needs to outlive only
36 * the call. @p name is the mod identity and the logger prefix. A non-empty @p game_process_name must match
37 * the executable basename, or start() returns ErrorCode::ProcessMismatch. The match folds case on UTF-8
38 * through CompareStringOrdinal and ignores the C runtime locale. A non-empty @p instance_mutex_prefix
39 * creates a per-PID named mutex, so a second load of the mod fails with ErrorCode::InstanceAlreadyRunning.
40 */
41 struct ModInfo
42 {
43 std::string_view name{};
44 std::string_view log_file{};
45 std::string_view game_process_name{};
46 std::string_view instance_mutex_prefix{};
47 AsyncLoggerConfig log{};
48 /** @brief Selects the process-default logger's first sink open. See @ref LogOpenMode. */
49 LogOpenMode log_open_mode{LogOpenMode::Truncate};
50 /// The source-location stamp policy for formatted records. See @ref LogSourceStampMode.
51 LogSourceStampMode log_source_stamp_mode{};
52 };
53
54 /**
55 * @brief Owns a mod's process lifetime: single-instance guard, logger configuration, input scope, and teardown.
56 * @details ~Session and active move-assignment run the ordered teardown. First, scope().clear() releases this
57 * session's input bindings in reverse insertion order. The process-wide subsystems then tear down in
58 * reverse dependency order. The order is the config auto-reload watcher, the input poll thread, the memory
59 * cache, the config registry, the diagnostics TLS index, and the logger. Under the loader lock, or in an
60 * attach or unload phase off the bootstrap worker, no subsystem shutdown joins a thread. The Session owns
61 * no Hook: a Hook handle unhooks when the caller drops it.
62 * @note A moved-from or abandon()ed Session is inert: its destructor does nothing. One Session is active at a time.
63 * A second start() returns ErrorCode::SessionAlreadyActive, and a second bootstrap entry returns the code
64 * that identifies the current bootstrap slot owner.
65 * @note The init/teardown thread runs Session::start, on_ready, ~Session, and abandon() single-threaded. Do not
66 * call them from a hook, an input callback, or a config-reload callback.
67 * @warning `[B-100]` Run Session::start, ~Session, and active move-assignment off the loader lock. Teardown invokes
68 * consumer release callbacks and joins worker threads. A DllMain caller must route both phases through
69 * bootstrap_attach and bootstrap_detach. See abandon() for the process-termination-only escape.
70 */
71 class Session
72 {
73 public:
74 /**
75 * @brief Synchronously builds a Session: process gate, single-instance mutex, and logger configuration.
76 * @return The live Session, or ProcessMismatch, InstanceAlreadyRunning, or SessionAlreadyActive (see ModInfo
77 * and Session). A failed Win32 lifecycle call, or an instance_mutex_prefix longer than 32757 bytes,
78 * returns SystemCallFailed with a Win32 error code in Error::detail. A thrown std::bad_alloc maps to
79 * OutOfMemory, and any other exception maps to Unknown.
80 * @note Setup/control-plane only. If the logger refuses async mode, start succeeds with synchronous logging.
81 * @warning See the class `[B-100]` loader-lock warning.
82 */
83 [[nodiscard]] static Result<Session> start(const ModInfo &info) noexcept;
84
85 /** @brief Transfers the live teardown and leaves @p other inert. */
86 Session(Session &&other) noexcept;
87 /**
88 * @brief Runs the ordered teardown if this Session is active, then adopts @p other.
89 * @note Setup/control-plane only.
90 * @warning See the class `[B-100]` loader-lock warning.
91 */
92 Session &operator=(Session &&other) noexcept;
93 /** @brief Deleted: the teardown and the single-instance guard have one owner. */
94 Session(const Session &) = delete;
95 Session &operator=(const Session &) = delete;
96
97 /**
98 * @brief Runs the ordered teardown if this Session is active.
99 * @note Setup/control-plane only.
100 * @warning See the class `[B-100]` loader-lock warning.
101 */
102 ~Session() noexcept;
103
104 /** @brief The process-default logger that this session configured, the same as `DetourModKit::log()`. */
105 [[nodiscard]] Logger &log() const noexcept;
106
107 /** @brief A handle to the process configuration registry. After the binds exist, call `load(path)` on it. */
108 [[nodiscard]] config::Ini ini() const noexcept;
109
110 /** @brief The process input manager, the same as `input::Input::instance()`. */
111 [[nodiscard]] input::Input &input() const noexcept;
112
113 /** @brief This session's input binding scope for BindingGuards. */
114 [[nodiscard]] input::Scope &scope() noexcept;
115
116 /** @brief True while this Session is active, false once it is moved-from or abandon()ed. */
117 7 [[nodiscard]] explicit operator bool() const noexcept { return m_active; }
118
119 /**
120 * @brief Makes the Session inert: its destructor runs no scope clear, teardown, flush, or join.
121 * @details Call it only for DLL_PROCESS_DETACH with `lpReserved != NULL` (process termination), where a
122 * teardown risks a use after free. Never call it for an explicit FreeLibrary (`lpReserved == NULL`).
123 * @note Setup/control-plane only.
124 */
125 void abandon() noexcept;
126
127 private:
128 friend struct detail::SessionBootstrapAccess;
129
130 // The Session owns instance_mutex until release(). Null means that ModInfo requested no guard.
131 explicit Session(void *instance_mutex) noexcept;
132
133 void release() noexcept;
134
135 input::Scope m_scope;
136 void *m_instance_mutex{nullptr};
137 bool m_active{false};
138 };
139
140 /// The plain on_ready callback type of bootstrap_attach().
141 using BootstrapReadyFn = Result<void> (*)(Session &);
142
143 /**
144 * @brief DllMain DLL_PROCESS_ATTACH entry point: runs the loader-safe gates, then starts the Session on a worker.
145 * @details It captures the handle of the mod DLL that links this DetourModKit copy. It calls
146 * DisableThreadLibraryCalls and runs the process and single-instance gates without heap allocation. The
147 * worker configures the logger and runs @p on_ready off the loader lock. There it can allocate, load INIs,
148 * install hooks, and register bindings into session.scope(). It then waits until bootstrap_detach(),
149 * request_shutdown(), or shutdown_and_wait() wakes it, and destroys the Session off the loader lock. If
150 * @p on_ready fails, the worker logs the error and still waits, with the Session live and the DLL mapped.
151 * @param on_ready Called once on the worker thread with the live Session. A null value registers no callback.
152 * @return An empty Result once the worker is published, or ProcessMismatch, InstanceAlreadyRunning,
153 * SessionAlreadyActive, InvalidArg, SessionShutdownInProgress, SessionShutdownUnavailable, or
154 * SystemCallFailed. InvalidArg means that ModInfo::name or ModInfo::log_file is longer than 32768 bytes.
155 * Pre-publication failures roll back the mutex and the bootstrap slot.
156 * @note Setup/control-plane only. The synchronous phase calls no logger, callback, or wait.
157 */
158 [[nodiscard]] Result<void> bootstrap_attach(const ModInfo &info, BootstrapReadyFn on_ready) noexcept;
159
160 /**
161 * @brief The off-DllMain form of bootstrap_attach(), with the same results. Its callable can own move-only state.
162 * @note Setup/control-plane only.
163 * @warning Do not call it from DllMain. The callable conversion can allocate at the call site. A pre-publication
164 * failure destroys the callable and its captures on the current thread.
165 */
166 [[nodiscard]] Result<void>
167 bootstrap(const ModInfo &info, std::move_only_function<Result<void>(Session &)> on_ready) noexcept;
168
169 /**
170 * @brief DllMain DLL_PROCESS_DETACH entry point. Pass DllMain's lpvReserved as @p reserved.
171 * @details If @p reserved is NULL (explicit FreeLibrary), it destroys no callback state. From a Ready slot, it
172 * also signals the worker and retires the slot permanently. While the worker is live, a bare
173 * FreeLibrary cannot reach this call, because the worker holds a counted module reference. For a drained
174 * unload, call shutdown_and_wait() before FreeLibrary.
175 *
176 * If @p reserved is not NULL (process termination), the OS already killed the worker, so this path skips
177 * the teardown, flush, and join. Neither path waits or joins, so both are loader-lock-safe. Both publish
178 * an unload phase, so a later subsystem shutdown off the bootstrap worker joins no thread.
179 * @note Setup/control-plane only. Call it only from DllMain's DLL_PROCESS_DETACH path. A later call only
180 * republishes the unload phase.
181 */
182 void bootstrap_detach(void *reserved) noexcept;
183
184 /**
185 * @brief Requests asynchronous teardown of the bootstrap worker.
186 * @details It does nothing if no bootstrap entry ran or the teardown completed. To drain the module before
187 * FreeLibrary, use shutdown_and_wait().
188 * @note Callback-safe: call it from any thread (a hook, an input callback, or DllMain). It only signals an event
189 * and never allocates, waits, or joins.
190 */
191 void request_shutdown() noexcept;
192
193 /**
194 * @brief Signals the bootstrap worker and waits for its complete off-loader-lock teardown.
195 * @details If a bootstrap worker is live, success means that the worker exited, released its counted module
196 * reference, and drained every Session-owned subsystem. After a completed drain, or with no bootstrap
197 * entry, the call returns success and does nothing.
198 * @return Success after a complete drain. SessionShutdownInProgress if another control thread owns the drain or a
199 * bootstrap entry claims the slot. SessionShutdownUnavailable if DllMain detach claimed the slot.
200 * SessionShutdownWouldBlock if the calling thread holds the loader lock or is the bootstrap worker.
201 * SystemCallFailed with Error::detail = GetLastError() if the worker-handle wait fails.
202 * @note Setup/control-plane only. Call it before FreeLibrary, never from DllMain, a hook, or an input callback.
203 * @warning On the bootstrap worker (in on_ready or a callback that its teardown reaches), use request_shutdown().
204 */
205 [[nodiscard]] Result<void> shutdown_and_wait() noexcept;
206
207 /**
208 * @brief The HMODULE of the module that links this DetourModKit copy, or nullptr outside the session window.
209 * @details Session::start and a bootstrap entry publish it after the process and instance gates pass, before their
210 * fallible setup completes. A concurrent reader can observe it before the call returns, and a failure in
211 * that setup clears it. It is null before either call, after ~Session, after bootstrap_detach(), and after
212 * a successful shutdown_and_wait(). To use the handle after ~Session or a drain, capture it first.
213 * SessionStart.PublishesModuleIdentityForTheSessionLifetime proves the synchronous path.
214 * @note Callback-safe: a lock-free atomic load returns the current identity or null, even during a detach.
215 */
216 [[nodiscard]] ModuleHandle module_handle() noexcept;
217
218 /** @brief The status of a Logic DLL unload preparation for consumer-owned callback state. */
219 enum class LogicDllUnloadStatus : std::uint8_t
220 {
221 /// With the caller-owned preconditions met, DMK holds nothing that blocks the Logic DLL unmap.
222 SafeToUnload,
223 /// The loader lock, or an attach or unload phase off the bootstrap worker, forbids the drain waits and joins.
224 LoaderLock,
225 /// The caller runs inside an input or config callback and cannot drain itself.
226 SelfDelivery,
227 /// Another control thread owns the safe-drain transaction.
228 InProgress,
229 /// Selected input bindings were not retired.
230 RetireFailed,
231 /// The deadline expired while a callback or worker body remained alive.
232 TimedOut
233 };
234
235 /// Default deadline for Logic DLL safe-unload preparation.
236 inline constexpr std::chrono::milliseconds DEFAULT_LOGIC_DLL_DRAIN_TIMEOUT{500};
237
238 /**
239 * @brief Retires the named input bindings and all config callbacks before a Logic DLL is unmapped.
240 * @details A completed config drain also stops auto-reload and the reload hotkey, then runs config::clear().
241 * @param timeout Bounds the waits for in-flight callbacks and worker bodies, not the consumer code in the warning.
242 * @return SafeToUnload only after every callable copy that DMK still owns for the named bindings, and every config
243 * setter from the old lifecycle, is gone. An outstanding BindingGuard does not keep a callback alive. A
244 * still-held Hold binding receives its balancing on_state_change(false) during the drain, while the DLL is
245 * still mapped. The drain includes a balancing edge that a concurrent guard release already started.
246 * @note A guard retained across a successful drain stays valid and still lifts its binding's passthrough
247 * suppression when released.
248 * @note Setup/control-plane only. Before you call it, stop consumer-owned workers. Retire each hook::HookStack
249 * through hook::HookStack::retire. Drop dispatcher subscriptions and the remaining hook handles. Call it from
250 * an off-loader-lock shutdown thread.
251 * @warning The drain runs your balancing callbacks and capture destructors on this thread, after the deadline is
252 * spent. A BindingGuard release that races the drain blocks until they finish. Neither wait is bounded, so
253 * hold no lock and own no join that any of that code can wait on.
254 */
255 [[nodiscard]] LogicDllUnloadStatus prepare_logic_dll_unload(
256 std::span<const std::string_view> binding_names,
257 std::chrono::milliseconds timeout = DEFAULT_LOGIC_DLL_DRAIN_TIMEOUT
258 ) noexcept;
259
260 /**
261 * @brief Retires every input binding and all config callbacks before Logic DLLs are unmapped.
262 * @return SafeToUnload only after every callable copy that DMK still owns, and every config setter, is gone.
263 * @note Setup/control-plane only. Every precondition and rule of prepare_logic_dll_unload applies.
264 * @warning The prepare_logic_dll_unload warning applies. It retires the bindings of every Logic DLL in the host.
265 */
266 [[nodiscard]] LogicDllUnloadStatus
267 prepare_logic_dll_unload_all(std::chrono::milliseconds timeout = DEFAULT_LOGIC_DLL_DRAIN_TIMEOUT) noexcept;
268
269 /**
270 * @brief Calls prepare_logic_dll_unload with the default deadline and discards the status.
271 * @details If blocking teardown is not permitted on this thread, it closes new callback admission, disables config
272 * reloads, and does not wait.
273 * @note Best-effort: the wrapper fails closed.
274 * @warning This void result never authorizes FreeLibrary. Require SafeToUnload from prepare_logic_dll_unload.
275 */
276 void on_logic_dll_unload(std::span<const std::string_view> binding_names) noexcept;
277
278 /**
279 * @brief Acts as on_logic_dll_unload for every binding, through prepare_logic_dll_unload_all.
280 * @note Best-effort: the wrapper fails closed.
281 * @warning This void result never authorizes FreeLibrary. Require SafeToUnload from prepare_logic_dll_unload_all.
282 */
283 void on_logic_dll_unload_all() noexcept;
284 } // namespace DetourModKit
285
286 #endif // DETOURMODKIT_SESSION_HPP
287