Improve FunctionReference trace logging with real user stack origin (#895)

Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
Co-authored-by: root <root@ns3203586.ip-146-59-53.eu>
This commit is contained in:
Michal
2025-06-20 14:28:14 +02:00
committed by GitHub
parent 22f5c06c49
commit 6f663164ee
13 changed files with 1391 additions and 1035 deletions

View File

@@ -2,6 +2,7 @@
"name": "SteamRT Sniper SDK",
"image": "registry.gitlab.steamos.cloud/steamrt/sniper/sdk:latest",
"updateContentCommand": "git submodule update --init --recursive",
"postCreateCommand": "cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build -j$(nproc)",
"customizations": {
"vscode": {
"extensions": [
@@ -16,4 +17,4 @@
"features": {
"ghcr.io/devcontainers/features/dotnet": "8.0"
}
}
}

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM registry.gitlab.steamos.cloud/steamrt/sniper/sdk:latest
WORKDIR /workspace
RUN apt update && apt install -y \
clang-16 \
cmake \
ninja-build \
git \
zlib1g-dev \
libssl-dev \
libprotobuf-dev \
protobuf-compiler \
pkg-config \
curl && \
ln -sf /usr/bin/clang-16 /usr/bin/clang && \
ln -sf /usr/bin/clang++-16 /usr/bin/clang++

View File

@@ -267,10 +267,17 @@
"linux": 0
}
},
"CheckTransmit": {
"signatures": {
"library": "server",
"windows": "48 8B C4 4C 89 48 ? 44 89 40 ? 48 89 50 ? 48 89 48 ? 55",
"linux": "55 48 89 E5 41 57 49 89 CF 41 56 41 55 41 54 53 48 81 EC"
}
},
"CheckTransmitPlayerSlot": {
"offsets": {
"windows": 584,
"linux": 584
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@
*/
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -26,7 +27,7 @@ namespace CounterStrikeSharp.API.Core
/// </summary>
public enum FunctionLifetime
{
/// <summary>Delegate will be removed after the first invocation.</summary>
/// <summary>Delegate will be removed after the first invocation.</summary>
SingleUse,
/// <summary>Delegate will remain in memory for the lifetime of the application (or until <see cref="FunctionReference.Remove"/> is called).</summary>
@@ -57,7 +58,7 @@ namespace CounterStrikeSharp.API.Core
_targetMethod = method;
_nativeCallback = CreateWrappedCallback();
}
/// <summary>
/// <inheritdoc cref="FunctionLifetime"/>
/// </summary>
@@ -73,6 +74,25 @@ namespace CounterStrikeSharp.API.Core
private unsafe CallbackDelegate CreateWrappedCallback()
{
var methodName = _targetMethod.Method.DeclaringType?.FullName + "." + _targetMethod.Method.Name;
var profileName = "ScriptCallback::Execute::" + _targetMethod.Method.Name;
var stackTrace = new StackTrace(2, true);
var firstUserFrame = stackTrace.GetFrames()?.FirstOrDefault(frame =>
{
var declaring = frame.GetMethod()?.DeclaringType?.FullName;
return declaring != null &&
!declaring.StartsWith("CounterStrikeSharp") &&
!declaring.Contains("SafeExecutor") &&
!declaring.Contains("FunctionReference");
});
string caller = firstUserFrame != null
? $"{firstUserFrame.GetMethod()?.DeclaringType?.FullName}.{firstUserFrame.GetMethod()?.Name} @ {firstUserFrame.GetFileName()}:{firstUserFrame.GetFileLineNumber()}"
: "Unknown (no user frame)";
Helpers.RegisterCallbackTrace(methodName, 1, profileName, caller);
return context =>
{
try
@@ -176,4 +196,4 @@ namespace CounterStrikeSharp.API.Core
}
}
}
}
}

View File

@@ -29,5 +29,8 @@ namespace CounterStrikeSharp.API.Core
[SecurityCritical]
[DllImport(dllPath, EntryPoint = "InvokeNative")]
public static extern void InvokeNative(IntPtr ptr);
[DllImport(dllPath, EntryPoint = "RegisterCallbackTrace")]
public static extern void RegisterCallbackTrace(string name, int count, string profile, string callerStack);
}
}
}

View File

@@ -8,7 +8,7 @@ namespace CounterStrikeSharp.API.Core;
public enum CSWeaponType : uint
{
WEAPONTYPE_KNIFE = 0x0,
WEAPONTYPE_KNIFE = 0x0,
WEAPONTYPE_PISTOL = 0x1,
WEAPONTYPE_SUBMACHINEGUN = 0x2,
WEAPONTYPE_RIFLE = 0x3,
@@ -20,5 +20,12 @@ public enum CSWeaponType : uint
WEAPONTYPE_GRENADE = 0x9,
WEAPONTYPE_EQUIPMENT = 0xA,
WEAPONTYPE_STACKABLEITEM = 0xB,
WEAPONTYPE_UNKNOWN = 0xC,
WEAPONTYPE_FISTS = 0xC,
WEAPONTYPE_BREACHCHARGE = 0xD,
WEAPONTYPE_BUMPMINE = 0xE,
WEAPONTYPE_TABLET = 0xF,
WEAPONTYPE_MELEE = 0x10,
WEAPONTYPE_SHIELD = 0x11,
WEAPONTYPE_ZONE_REPULSOR = 0x12,
WEAPONTYPE_UNKNOWN = 0x13,
}

View File

@@ -25,6 +25,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;

View File

@@ -124,6 +124,9 @@ public abstract class BaseMenuInstance : IMenuInstance
var menuItemIndex = CurrentOffset + desiredValue - 1;
if (Menu?.MenuOptions == null)
return;
if (menuItemIndex >= 0 && menuItemIndex < Menu.MenuOptions.Count)
{
var menuOption = Menu.MenuOptions[menuItemIndex];

View File

@@ -100452,8 +100452,36 @@
"value": 11
},
{
"name": "WEAPONTYPE_UNKNOWN",
"name": "WEAPONTYPE_FISTS",
"value": 12
},
{
"name": "WEAPONTYPE_BREACHCHARGE",
"value": 13
},
{
"name": "WEAPONTYPE_BUMPMINE",
"value": 14
},
{
"name": "WEAPONTYPE_TABLET",
"value": 15
},
{
"name": "WEAPONTYPE_MELEE",
"value": 16
},
{
"name": "WEAPONTYPE_SHIELD",
"value": 17
},
{
"name": "WEAPONTYPE_ZONE_REPULSOR",
"value": 18
},
{
"name": "WEAPONTYPE_UNKNOWN",
"value": 19
}
]
},

View File

@@ -270,8 +270,15 @@ bool EventManager::OnFireEventPost(IGameEvent* pEvent, bool bDontBroadcast)
pCallback->ScriptContext().Push(&override);
pCallback->Execute();
globals::gameEventManager->FreeEvent(pEventCopy);
m_EventCopies.pop();
if (pEventCopy)
{
globals::gameEventManager->FreeEvent(pEventCopy);
m_EventCopies.pop();
}
else
{
CSSHARP_CORE_WARN("OnFireEventPost: pEventCopy is nullptr, cannot free event");
}
}
}

View File

@@ -21,6 +21,33 @@
#include "core/log.h"
#include "vprof.h"
DLL_EXPORT void RegisterCallbackTrace(const char* name, size_t count, const char* profile, const char* callerStack)
{
// Dummy logic to prevent compiler from optimizing this function away
volatile size_t hash = 5381;
if (name)
{
for (const char* c = name; *c; ++c)
hash = ((hash << 5) + hash) + *c;
}
if (profile)
{
for (const char* c = profile; *c; ++c)
hash = ((hash << 5) + hash) + *c;
}
if (callerStack)
{
for (int i = 0; callerStack[i] && i < 128; ++i)
hash ^= callerStack[i];
}
hash ^= count;
(void)hash;
}
namespace counterstrikesharp {
ScriptCallback::ScriptCallback(const char* szName) : m_root_context(fxNativeContext{})
@@ -36,22 +63,55 @@ void ScriptCallback::AddListener(CallbackT fnPluginFunction) { m_functions.push_
bool ScriptCallback::RemoveListener(CallbackT fnPluginFunction)
{
bool bSuccess = true;
size_t nOriginalSize = m_functions.size();
m_functions.erase(std::ranges::remove(m_functions, fnPluginFunction).begin(), m_functions.end());
return m_functions.size() != nOriginalSize;
}
m_functions.erase(std::remove(m_functions.begin(), m_functions.end(), fnPluginFunction), m_functions.end());
return bSuccess;
bool ScriptCallback::IsContextSafe()
{
try
{
auto& Ctx = ScriptContext();
Ctx.GetResult<void*>();
return true;
}
catch (...)
{
CSSHARP_CORE_WARN("Context is invalid (exception during access)");
return false;
}
}
void ScriptCallback::Execute(bool bResetContext)
{
if (!IsContextSafe())
{
ScriptContext().ThrowNativeError("ScriptCallback::Execute aborted due to invalid context");
CSSHARP_CORE_WARN("ScriptCallback::Execute aborted due to invalid context (callback: '{}')", m_name);
return;
}
VPROF_BUDGET(m_profile_name.c_str(), "CS# Script Callbacks");
for (auto fnMethodToCall : m_functions)
for (size_t nI = 0; nI < m_functions.size(); ++nI)
{
if (fnMethodToCall)
if (auto fnMethodToCall = m_functions[nI])
{
fnMethodToCall(&ScriptContextStruct());
try
{
fnMethodToCall(&ScriptContextStruct());
}
catch (...)
{
ScriptContext().ThrowNativeError("Exception in callback execution");
CSSHARP_CORE_ERROR("Exception thrown inside callback '{}', index {}", m_name, nI);
}
}
else
{
ScriptContext().ThrowNativeError("Null listener in callback");
CSSHARP_CORE_ERROR("Null function pointer in callback '{}', index {}", m_name, nI);
}
}
@@ -89,9 +149,9 @@ ScriptCallback* CallbackManager::FindCallback(const char* szName)
void CallbackManager::ReleaseCallback(ScriptCallback* pCallback)
{
auto I = std::remove_if(m_managed.begin(), m_managed.end(), [pCallback](ScriptCallback* pI) {
auto I = std::ranges::remove_if(m_managed, [pCallback](const ScriptCallback* pI) {
return pCallback == pI;
});
}).begin();
if (I != m_managed.end()) m_managed.erase(I, m_managed.end());
delete pCallback;
@@ -99,8 +159,7 @@ void CallbackManager::ReleaseCallback(ScriptCallback* pCallback)
bool CallbackManager::TryAddFunction(const char* szName, CallbackT fnCallable)
{
auto* pCallback = FindCallback(szName);
if (pCallback)
if (auto* pCallback = FindCallback(szName))
{
pCallback->AddListener(fnCallable);
return true;
@@ -111,8 +170,7 @@ bool CallbackManager::TryAddFunction(const char* szName, CallbackT fnCallable)
bool CallbackManager::TryRemoveFunction(const char* szName, CallbackT fnCallable)
{
auto* pCallback = FindCallback(szName);
if (pCallback)
if (auto* pCallback = FindCallback(szName))
{
return pCallback->RemoveListener(fnCallable);
}

View File

@@ -31,8 +31,9 @@ class ScriptCallback
~ScriptCallback();
void AddListener(CallbackT fnPluginFunction);
bool RemoveListener(CallbackT fnPluginFunction);
bool IsContextSafe();
std::string GetName() { return m_name; }
unsigned int GetFunctionCount() { return m_functions.size(); }
unsigned int GetFunctionCount() const { return m_functions.size(); }
std::vector<CallbackT> GetFunctions() { return m_functions; }
void Execute(bool bResetContext = true);
@@ -72,7 +73,6 @@ class CallbackPair
~CallbackPair();
bool HasCallbacks() const { return pre->GetFunctionCount() > 0 || post->GetFunctionCount() > 0; }
public:
ScriptCallback* pre;
ScriptCallback* post;
};