Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .clang-format-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Repository-relative exclusions for shared FormatValidation.
# Only src/, include/, and tests/ are selected; dependencies and artifacts are excluded.
**/thirdparty/**
**/build/**
**/install/**
31 changes: 31 additions & 0 deletions .github/workflows/clang-format.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: clang-format

on:
push:
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: clang-format-${{ github.ref }}
cancel-in-progress: true

jobs:
clang-format:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install pinned formatter
run: python -m pip install clang-format==23.1.3
- name: Configure formatting targets
run: cmake -S . -B build/format -DFORMAT_VALIDATION_ONLY=ON
- name: Check all owned C/C++ sources
run: cmake --build build/format --target format-check
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
/thirdparty/cache/
*.user
__pycache__/

# Generated from the shared FormatValidation source during CMake configuration.
/.clang-format
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
cmake_minimum_required(VERSION 3.21)
option(FORMAT_VALIDATION_ONLY "Configure only clang-format targets without native dependencies" OFF)
if(FORMAT_VALIDATION_ONLY)
project(FormatValidation LANGUAGES NONE)
include(cmake/FormatValidation.cmake)
return()
endif()
cmake_policy(SET CMP0091 NEW)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
project(ThreadGuard LANGUAGES CXX)
Expand Down Expand Up @@ -95,3 +101,6 @@ if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
metahooksv_add_launch_game()
endif()
endif()

# Shared formatting targets are explicit; ordinary native builds do not run them.
include(cmake/FormatValidation.cmake)
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,36 @@ resolution; those require live game verification. The handler test includes 100
stop/recreate cycles with idle sockets and continuous packets, creation failure,
suspended creation, non-target forwarding and handle counts. Debugger output
records the real stop request, safe sleep exit and signaled wait in a game run.

## C/C++ formatting

Formatting uses [MetaHookSv/FormatValidation](https://github.com/MetaHookSv/FormatValidation)
and clang-format **23.1.3**, with the DiligentCore style (4 spaces, preserved include
order). Install the formatter for the Python interpreter used by CMake:

```sh
python -m pip install clang-format==23.1.3
cmake -S . -B build/format "-DFORMAT_VALIDATION_ONLY=ON"
cmake --build build/format --target format-check
cmake --build build/format --target format
```

The format-only configuration needs CMake 3.21+, Git, Python 3.9+ (CI uses 3.12),
and a build generator; `-G Ninja` works without Visual Studio. It prepares no native
SDK or game dependencies. Formatting targets are explicit and are not part of a
normal DLL build. With a Visual Studio generator, add `--config Debug` or
`--config Release` when building a formatting target.

The aggregate provides `FORMAT_VALIDATION_SOURCE_PATH=thirdparty/FormatValidation`.
Standalone components accept that CMake variable or its environment counterpart;
if empty, FetchContent downloads the fixed tooling commit. Quote relative paths,
for example `"-DFORMAT_VALIDATION_SOURCE_PATH=../../thirdparty/FormatValidation"`.
Configuration generates the ignored root `.clang-format` for editors; change the
shared style rather than that generated copy. An optional
`FORMAT_VALIDATION_CLANG_FORMAT_EXECUTABLE` selects an explicit formatter, whose
version must still match the pin.

Checks cover owned C/C++ files in `src/`, `include/`, and `tests/`, including
non-ignored new files. Repository-relative exclusions live in `.clang-format-ignore`.
Third-party sources and build artifacts are excluded. The `clang-format` workflow
checks the full scope on pushes, pull requests, and manual runs.
30 changes: 30 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,33 @@ ctest --test-dir build/tests -C Release --output-on-failure
```

将 `Release` 换成 `Debug` 可测试调试配置。独立测试使用真实 Win32 线程、socket、锁和等待函数,但只提供宿主的致命错误回调,用于验证 handler 行为;真实 IAT 安装及 gamedata 解析仍需游戏实测。handler 测试包含空闲 socket 和持续取包条件下的 100 次停止/重建,以及创建失败、挂起创建、非目标转发和句柄计数检查。实机调试输出记录退出请求、安全 Sleep 退出及真实等待 signaled 三个阶段。

## C/C++ 格式化

使用 [MetaHookSv/FormatValidation](https://github.com/MetaHookSv/FormatValidation)
共享工具及固定版本 **clang-format 23.1.3**,采用 DiligentCore 风格(4 空格,保留
include 顺序)。为 CMake 使用的 Python 解释器安装格式工具:

```sh
python -m pip install clang-format==23.1.3
cmake -S . -B build/format "-DFORMAT_VALIDATION_ONLY=ON"
cmake --build build/format --target format-check
cmake --build build/format --target format
```

格式专用配置需要 CMake 3.21+、Git、Python 3.9+(CI 使用 3.12)及构建生成器;
使用 `-G Ninja` 可无需 Visual Studio。它不准备原生 SDK 或游戏依赖。
格式目标需显式执行,不加入普通 DLL 构建。使用 Visual Studio 生成器时,执行目标
需追加 `--config Debug` 或 `--config Release`。

聚合仓库注入 `FORMAT_VALIDATION_SOURCE_PATH=thirdparty/FormatValidation`。
独立组件支持该 CMake 参数及同名环境变量;为空时通过 FetchContent 获取固定工具
提交。相对路径应加引号,例如
`"-DFORMAT_VALIDATION_SOURCE_PATH=../../thirdparty/FormatValidation"`。
配置时在仓库根目录生成被 gitignore 的 `.clang-format` 供编辑器使用;格式规则应
在共享仓库修改,不修改生成副本。可通过 `FORMAT_VALIDATION_CLANG_FORMAT_EXECUTABLE`
指定工具路径,但版本仍须与固定版本一致。

检查覆盖 `src/`、`include/`、`tests/` 中维护的 C/C++ 文件,包括未被 Git 忽略的新文件。
相对仓库根目录的排除规则位于 `.clang-format-ignore`;第三方源和构建产物不纳入检查。
`clang-format` workflow 在 push、pull request 和手动运行时执行全量检查。
26 changes: 26 additions & 0 deletions cmake/FormatValidation.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Shared tool implementation; only this dependency bootstrap belongs to the consumer.
if(NOT DEFINED FORMAT_VALIDATION_SOURCE_PATH)
set(FORMAT_VALIDATION_SOURCE_PATH "$ENV{FORMAT_VALIDATION_SOURCE_PATH}" CACHE PATH
"FormatValidation source tree; empty fetches the pinned tooling")
endif()
if(FORMAT_VALIDATION_SOURCE_PATH)
get_filename_component(_format_validation_source "${FORMAT_VALIDATION_SOURCE_PATH}"
ABSOLUTE BASE_DIR "${CMAKE_CURRENT_LIST_DIR}/..")
else()
include(FetchContent)
FetchContent_Declare(threadguard_format_validation
GIT_REPOSITORY https://github.com/MetaHookSv/FormatValidation.git
GIT_TAG 13c9fabe058e1f887ad1b03bb6884de911192c6a # v1.0.0; retained immutable tooling commit
GIT_SUBMODULES ""
# Populate the tooling without configuring its own test project.
SOURCE_SUBDIR cmake)
FetchContent_MakeAvailable(threadguard_format_validation)
set(_format_validation_source "${threadguard_format_validation_SOURCE_DIR}")
endif()
foreach(_format_validation_file .clang-format clang-format-validate.py cmake/FormatValidation.cmake)
if(NOT EXISTS "${_format_validation_source}/${_format_validation_file}")
message(FATAL_ERROR "FORMAT_VALIDATION_SOURCE_PATH is missing ${_format_validation_file}: ${_format_validation_source}")
endif()
endforeach()
include("${_format_validation_source}/cmake/FormatValidation.cmake")
format_validation_add_component(ThreadGuard "${CMAKE_CURRENT_SOURCE_DIR}")
187 changes: 95 additions & 92 deletions src/NetworkThreadState.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,108 +9,111 @@
class NetworkThreadState
{
public:
enum class StopResult { NotTarget, Requested, Stopped, SelfWait, Failed };
enum class StopResult
{
NotTarget,
Requested,
Stopped,
SelfWait,
Failed
};

bool PrepareCreate() { return Reset(); }
bool PrepareCreate() { return Reset(); }

bool Track(HANDLE thread)
{
std::lock_guard<std::mutex> lock(m_lock);
const DWORD id = GetThreadId(thread);
if (m_thread || !id)
return false;
if (!DuplicateHandle(GetCurrentProcess(), thread, GetCurrentProcess(),
&m_thread, 0, FALSE, DUPLICATE_SAME_ACCESS))
return false;
m_exitRequested.store(false);
m_threadId.store(id);
return true;
}
bool Track(HANDLE thread)
{
std::lock_guard<std::mutex> lock(m_lock);
const DWORD id = GetThreadId(thread);
if (m_thread || !id)
return false;
if (!DuplicateHandle(GetCurrentProcess(), thread, GetCurrentProcess(),
&m_thread, 0, FALSE, DUPLICATE_SAME_ACCESS))
return false;
m_exitRequested.store(false);
m_threadId.store(id);
return true;
}

bool IsThread(DWORD currentId, DWORD engineId) const
{
return currentId != 0 && currentId == engineId && currentId == m_threadId.load();
}
bool IsThread(DWORD currentId, DWORD engineId) const
{
return currentId != 0 && currentId == engineId && currentId == m_threadId.load();
}

bool ShouldExit(DWORD currentId, DWORD engineId) const
{
return IsThread(currentId, engineId) && m_exitRequested.load();
}
bool ShouldExit(DWORD currentId, DWORD engineId) const
{
return IsThread(currentId, engineId) && m_exitRequested.load();
}

void RetireCurrentThread()
{
DWORD id = GetCurrentThreadId();
m_threadId.compare_exchange_strong(id, 0);
}
void RetireCurrentThread()
{
DWORD id = GetCurrentThreadId();
m_threadId.compare_exchange_strong(id, 0);
}

StopResult RequestStop(HANDLE thread, DWORD engineId, DWORD currentId = GetCurrentThreadId())
{
std::lock_guard<std::mutex> lock(m_lock);
return RequestStopLocked(thread, engineId, currentId);
}
StopResult RequestStop(HANDLE thread, DWORD engineId, DWORD currentId = GetCurrentThreadId())
{
std::lock_guard<std::mutex> lock(m_lock);
return RequestStopLocked(thread, engineId, currentId);
}

StopResult RequestTrackedStop(DWORD engineId, HANDLE* waitHandle = NULL)
{
std::lock_guard<std::mutex> lock(m_lock);
if (waitHandle)
*waitHandle = NULL;
if (!m_thread)
return StopResult::NotTarget;
// A repeated shutdown may already have cleared the engine's ID. An
// exited object is safe; a live identity mismatch must block cleanup.
const auto result = WaitForSingleObject(m_thread, 0) == WAIT_OBJECT_0
? StopResult::Stopped : RequestStopLocked(m_thread, engineId, GetCurrentThreadId());
if (result == StopResult::NotTarget)
return StopResult::Failed;
if (waitHandle && (result == StopResult::Requested || result == StopResult::Stopped)
&& !DuplicateHandle(GetCurrentProcess(), m_thread, GetCurrentProcess(),
waitHandle, 0, FALSE, DUPLICATE_SAME_ACCESS))
return StopResult::Failed;
return result;
}
StopResult RequestTrackedStop(DWORD engineId, HANDLE* waitHandle = NULL)
{
std::lock_guard<std::mutex> lock(m_lock);
if (waitHandle)
*waitHandle = NULL;
if (!m_thread)
return StopResult::NotTarget;
// A repeated shutdown may already have cleared the engine's ID. An
// exited object is safe; a live identity mismatch must block cleanup.
const auto result = WaitForSingleObject(m_thread, 0) == WAIT_OBJECT_0 ? StopResult::Stopped : RequestStopLocked(m_thread, engineId, GetCurrentThreadId());
if (result == StopResult::NotTarget)
return StopResult::Failed;
if (waitHandle && (result == StopResult::Requested || result == StopResult::Stopped) && !DuplicateHandle(GetCurrentProcess(), m_thread, GetCurrentProcess(), waitHandle, 0, FALSE, DUPLICATE_SAME_ACCESS))
return StopResult::Failed;
return result;
}

bool Reset()
{
std::lock_guard<std::mutex> lock(m_lock);
if (m_thread && WaitForSingleObject(m_thread, 0) != WAIT_OBJECT_0)
return false;
m_threadId.store(0);
m_exitRequested.store(false);
if (m_thread)
CloseHandle(m_thread);
m_thread = NULL;
return true;
}
bool Reset()
{
std::lock_guard<std::mutex> lock(m_lock);
if (m_thread && WaitForSingleObject(m_thread, 0) != WAIT_OBJECT_0)
return false;
m_threadId.store(0);
m_exitRequested.store(false);
if (m_thread)
CloseHandle(m_thread);
m_thread = NULL;
return true;
}

private:
StopResult RequestStopLocked(HANDLE thread, DWORD engineId, DWORD currentId)
{
if (!m_thread)
return StopResult::NotTarget;
const DWORD id = GetThreadId(thread);
// Keep the object identity for repeated stops even after the worker
// retires its active ID immediately before ExitThread.
if (!id || id != engineId || id != GetThreadId(m_thread))
return StopResult::NotTarget;
if (id == currentId)
return StopResult::SelfWait;
StopResult RequestStopLocked(HANDLE thread, DWORD engineId, DWORD currentId)
{
if (!m_thread)
return StopResult::NotTarget;
const DWORD id = GetThreadId(thread);
// Keep the object identity for repeated stops even after the worker
// retires its active ID immediately before ExitThread.
if (!id || id != engineId || id != GetThreadId(m_thread))
return StopResult::NotTarget;
if (id == currentId)
return StopResult::SelfWait;

const DWORD state = WaitForSingleObject(m_thread, 0);
if (state == WAIT_OBJECT_0)
{
// A finished thread's ID can be reused. Never request exit for a new
// live thread merely because it has the old numeric ID.
return WaitForSingleObject(thread, 0) == WAIT_OBJECT_0
? StopResult::Stopped : StopResult::NotTarget;
}
if (state != WAIT_TIMEOUT)
return StopResult::Failed;
m_exitRequested.store(true);
return StopResult::Requested;
}
const DWORD state = WaitForSingleObject(m_thread, 0);
if (state == WAIT_OBJECT_0)
{
// A finished thread's ID can be reused. Never request exit for a new
// live thread merely because it has the old numeric ID.
return WaitForSingleObject(thread, 0) == WAIT_OBJECT_0 ? StopResult::Stopped : StopResult::NotTarget;
}
if (state != WAIT_TIMEOUT)
return StopResult::Failed;
m_exitRequested.store(true);
return StopResult::Requested;
}

std::mutex m_lock;
HANDLE m_thread = NULL;
std::atomic<DWORD> m_threadId{ 0 };
std::atomic<bool> m_exitRequested{ false };
std::mutex m_lock;
HANDLE m_thread = NULL;
std::atomic<DWORD> m_threadId{0};
std::atomic<bool> m_exitRequested{false};
};
Loading
Loading