[Feature]: Add on crash uploaded SourceMod forward (#42)
Some checks failed
Release / main-ci (push) Has been cancelled
Release / release (push) Has been cancelled

* Squashed commit of the following:

commit c7efce2d6722b08e2ea719ce1a39f80747502f77
Author: caxanga334 <10157643+caxanga334@users.noreply.github.com>
Date:   Sat Nov 22 22:35:24 2025 -0300

    Fix Error

commit e8acf9f505aa26b007c5837d4075f649099f43c3
Author: caxanga334 <10157643+caxanga334@users.noreply.github.com>
Date:   Sat Nov 22 22:23:57 2025 -0300

    Fix Possible Memory Leak

commit 51e718922726c074c0e7a45f9d13c6a51e4b97eb
Author: caxanga334 <10157643+caxanga334@users.noreply.github.com>
Date:   Sat Nov 22 22:13:55 2025 -0300

    Add Upload Forward

* Update accelerator.inc

* Fix: Missing forwards.cpp in AMBuilder

* Refactor API

Forward notifies plugins when the extension is done uploading.
Plugins can fetch data from uploaded crashes via natives.
Added example plugin.

* Implement Requested Changes

* Fix Error

* Update Example Plugin

Removed timer example.
Added example of how to handle plugin late loads.
This commit is contained in:
caxanga334 2025-12-02 18:32:12 -03:00 committed by GitHub
parent a661a6abb7
commit 87704dc53b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 413 additions and 2 deletions

View File

@ -9,7 +9,9 @@ builder.SetBuildFolder('package')
folder_list = [
'addons/sourcemod/extensions',
'addons/sourcemod/gamedata',
'addons/sourcemod/configs'
'addons/sourcemod/configs',
'addons/sourcemod/scripting',
'addons/sourcemod/scripting/include',
]
# Are we build a x86_64 extension ?
@ -42,6 +44,20 @@ def CopyDirContent(src, dest):
if item.is_file():
builder.AddCopy(item.path, dest_entry)
# Include files
CopyFiles('scripting/include', 'addons/sourcemod/scripting/include',
[
'accelerator.inc',
]
)
# Example files
CopyFiles('scripting', 'addons/sourcemod/scripting',
[
'accelerator_example.sp',
]
)
# Copy binaries.
for task in Accelerator.extension:
if task.target.arch == 'x86_64':

View File

@ -7,6 +7,8 @@ project = builder.LibraryProject('accelerator.ext')
project.sources = [
'extension.cpp',
'MemoryDownloader.cpp',
'forwards.cpp',
'natives.cpp',
os.path.join(Accelerator.sm_root, 'public', 'smsdk_ext.cpp')
]

View File

@ -27,6 +27,8 @@
#include <IWebternet.h>
#include "MemoryDownloader.h"
#include "forwards.h"
#include "natives.h"
#if defined _LINUX
#include "client/linux/handler/exception_handler.h"
@ -464,6 +466,8 @@ class UploadThread: public IThread
count++;
g_pSM->LogError(myself, "Accelerator uploaded crash dump: %s", response);
if (log) fprintf(log, "Uploaded crash dump: %s\n", response);
UploadedCrash crash{ response };
g_accelerator.StoreUploadedCrash(crash);
} else {
failed++;
g_pSM->LogError(myself, "Accelerator failed to upload crash dump: %s", response);
@ -495,6 +499,7 @@ class UploadThread: public IThread
log = nullptr;
}
g_accelerator.MarkAsDoneUploading();
rootconsole->ConsolePrint("Accelerator upload thread finished. (%d skipped, %d uploaded, %d failed)", skip, count, failed);
}
@ -1080,6 +1085,25 @@ class UploadThread: public IThread
}
} uploadThread;
class SourcePawnNotifyThread : public IThread
{
public:
void RunThread(IThreadHandle* pHandle) {
for (;;) {
// Wait until OnMapStart is called once, this should be enough delay to make sure plugins are loaded.
if (g_accelerator.IsMapStarted() && g_accelerator.IsDoneUploading()) {
extforwards::CallOnDoneUploadingForward();
break;
}
}
}
void OnTerminate(IThreadHandle* pHandle, bool cancel) {
}
} spNotifyThread;
class VFuncEmptyClass {};
const char *GetCmdLine()
@ -1117,6 +1141,11 @@ const char *GetCmdLine()
return (const char *)(reinterpret_cast<VFuncEmptyClass*>(cmdline)->*u.mfpnew)();
}
Accelerator::Accelerator() :
m_doneuploading(false), m_maphasstarted(false)
{
}
bool Accelerator::SDK_OnLoad(char *error, size_t maxlength, bool late)
{
sharesys->AddDependency(myself, "webternet.ext", true, true);
@ -1142,6 +1171,7 @@ bool Accelerator::SDK_OnLoad(char *error, size_t maxlength, bool late)
strncpy(crashGameDirectory, g_pSM->GetGameFolderName(), sizeof(crashGameDirectory) - 1);
threader->MakeThread(&uploadThread);
threader->MakeThread(&spNotifyThread); // This thread waits for accelator to be done uploading and for the first OnMapStart call, then fires a SourceMod forward
do {
char gameconfigError[256];
@ -1321,6 +1351,7 @@ bool Accelerator::SDK_OnLoad(char *error, size_t maxlength, bool late)
void Accelerator::SDK_OnUnload()
{
extforwards::Shutdown();
plsys->RemovePluginsListener(this);
#if defined _LINUX
@ -1336,9 +1367,21 @@ void Accelerator::SDK_OnUnload()
delete handler;
}
void Accelerator::SDK_OnAllLoaded()
{
extforwards::Init();
sharesys->RegisterLibrary(myself, "accelerator");
natives::Setup(m_natives);
m_natives.push_back({ nullptr, nullptr }); // SM requires this to signal the end of the native info array
sharesys->AddNatives(myself, m_natives.data());
}
void Accelerator::OnCoreMapStart(edict_t *pEdictList, int edictCount, int clientMax)
{
strncpy(crashMap, gamehelpers->GetCurrentMap(), sizeof(crashMap) - 1);
m_maphasstarted.store(true);
}
/* 010 Editor Template
@ -1499,3 +1542,25 @@ void Accelerator::OnPluginUnloaded(IPlugin *plugin)
SerializePluginContexts();
}
void Accelerator::StoreUploadedCrash(UploadedCrash& crash)
{
std::lock_guard<std::mutex> lock(m_uploadedcrashes_mutex);
m_uploadedcrashes.push_back(std::move(crash));
}
const UploadedCrash* Accelerator::GetUploadedCrash(int element) const
{
std::lock_guard<std::mutex> lock(m_uploadedcrashes_mutex);
if (element < 0 || element >= static_cast<int>(m_uploadedcrashes.size())) {
return nullptr;
}
return &m_uploadedcrashes[element];
}
cell_t Accelerator::GetUploadedCrashCount() const
{
std::lock_guard<std::mutex> lock(m_uploadedcrashes_mutex);
return static_cast<cell_t>(m_uploadedcrashes.size());
}

View File

@ -26,14 +26,36 @@
* @brief Accelerator extension code header.
*/
#include <atomic>
#include <vector>
#include <mutex>
#include "smsdk_ext.h"
/**
* @brief Represents a crash that has been successfully uploaded to Accelerator's backend
*/
class UploadedCrash
{
public:
UploadedCrash(const char* response) :
m_httpresponse(response)
{
}
const std::string& GetHTTPResponse() const { return m_httpresponse; }
private:
std::string m_httpresponse; // HTTP response from the crash upload in free form text.
};
/**
* @brief Sample implementation of the SDK Extension.
*/
class Accelerator : public SDKExtension, IPluginsListener
{
public: // SDKExtension
Accelerator();
/**
* @brief This is called after the initial loading sequence has been processed.
*
@ -49,6 +71,11 @@ public: // SDKExtension
*/
virtual void SDK_OnUnload();
/**
* @brief This is called once all known extensions have been loaded.
*/
virtual void SDK_OnAllLoaded();
/**
* @brief Called on server activation before plugins receive the OnServerLoad forward.
*
@ -68,6 +95,47 @@ public: // IPluginsListener
* @brief Called when a plugin is unloaded (only if fully loaded).
*/
virtual void OnPluginUnloaded(IPlugin *plugin);
/**
* @brief Stores an uploaded crash into the vector.
* @param crash Uploaded crash instance to store.
*/
void StoreUploadedCrash(UploadedCrash& crash);
/**
* @brief Retrieves an uploaded crash from the given element.
* @param element Vector element (index) to read.
* @return Upload crash if found or NULL if out of bounds.
*/
const UploadedCrash* GetUploadedCrash(int element) const;
/**
* @brief Returns the number of uploaded crashes (for SourcePawn plugins).
* @return Number of uploaded crashes.
*/
cell_t GetUploadedCrashCount() const;
/**
* @brief Signals the extension that uploading is done.
*/
void MarkAsDoneUploading() { m_doneuploading.store(true); }
/**
* @brief Is Accelerator done uploading crashes.
* @return Returns true if yes, false otherwise.
*/
bool IsDoneUploading() const { return m_doneuploading.load(); }
/**
* @brief Has the 'OnMapStart' function called at least once.
* @return True if yes, false otherwise.
*/
bool IsMapStarted() const { return m_maphasstarted.load(); }
private:
std::vector<UploadedCrash> m_uploadedcrashes; // Vector of uploaded crashes
std::vector<sp_nativeinfo_t> m_natives; // Vector of SourcePawn natives
mutable std::mutex m_uploadedcrashes_mutex; // mutex for accessing the m_uploadedcrashes vector
std::atomic_bool m_doneuploading; // Signals that Accelerator is done uploading crashes.
std::atomic_bool m_maphasstarted; // Signals that OnMapStart has been called at least once.
};
// Expose the extension singleton.
extern Accelerator g_accelerator;
#endif // _INCLUDE_SOURCEMOD_EXTENSION_PROPER_H_

36
extension/forwards.cpp Normal file
View File

@ -0,0 +1,36 @@
#include <array>
#include <string>
#include "extension.h"
#include "forwards.h"
static SourceMod::IForward* s_ondoneuploadingforward = nullptr;
static void OnDoneUploadingCallback(void* data)
{
if (s_ondoneuploadingforward) {
s_ondoneuploadingforward->Execute();
}
}
void extforwards::Init()
{
s_ondoneuploadingforward = forwards->CreateForward("Accelerator_OnDoneUploadingCrashes", SourceMod::ExecType::ET_Ignore, 0, nullptr);
}
void extforwards::Shutdown()
{
if (s_ondoneuploadingforward) {
forwards->ReleaseForward(s_ondoneuploadingforward);
s_ondoneuploadingforward = nullptr;
}
}
void extforwards::CallOnDoneUploadingForward()
{
// It's obligatory to use AddFrameAction here because SourcePawn forwards can only be called from the server's main thread.
// AddFrameAction will do this for us and is also thread safe.
smutils->AddFrameAction(OnDoneUploadingCallback, nullptr);
}

16
extension/forwards.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef _INCLUDE_FORWARDS_H_
#define _INCLUDE_FORWARDS_H_
namespace extforwards
{
// Initialize the sourcepawn forwards.
void Init();
// Shutdown the sourcepawn forwards.
void Shutdown();
// Calls the on done uploadind forward. (thread safe)
void CallOnDoneUploadingForward();
}
#endif // !_INCLUDE_FORWARDS_H_

54
extension/natives.cpp Normal file
View File

@ -0,0 +1,54 @@
#include <algorithm>
#include <amtl/am-string.h>
#include "extension.h"
#include "natives.h"
static cell_t Native_GetUploadedCrashCount(IPluginContext* context, const cell_t* params)
{
return g_accelerator.GetUploadedCrashCount();
}
static cell_t Native_IsDoneUploadingCrashes(IPluginContext* context, const cell_t* params)
{
if (g_accelerator.IsDoneUploading()) {
return 1;
}
return 0;
}
static cell_t Native_GetCrashHTTPResponse(IPluginContext* context, const cell_t* params)
{
if (!g_accelerator.IsDoneUploading()) {
context->ReportError("Wait until accelerator is done uploading crashes before accessing crash information!");
return 0;
}
int element = static_cast<int>(params[1]);
const UploadedCrash* crash = g_accelerator.GetUploadedCrash(element);
if (!crash) {
context->ReportError("Crash index %i is invalid!", element);
return 0;
}
char* buffer;
context->LocalToString(params[2], &buffer);
size_t maxsize = static_cast<size_t>(params[3]);
ke::SafeStrcpy(buffer, maxsize, crash->GetHTTPResponse().c_str());
return 0;
}
void natives::Setup(std::vector<sp_nativeinfo_t>& vec)
{
sp_nativeinfo_t list[] = {
{"Accelerator_GetUploadedCrashCount", Native_GetUploadedCrashCount},
{"Accelerator_IsDoneUploadingCrashes", Native_IsDoneUploadingCrashes},
{"Accelerator_GetCrashHTTPResponse", Native_GetCrashHTTPResponse},
};
vec.insert(vec.end(), std::begin(list), std::end(list));
}

11
extension/natives.h Normal file
View File

@ -0,0 +1,11 @@
#ifndef _INCLUDE_NATIVES_H_
#define _INCLUDE_NATIVES_H_
namespace natives
{
// Called during the load process to setup the extension natives
void Setup(std::vector<sp_nativeinfo_t>& vec);
}
#endif // !_INCLUDE_NATIVES_H_

View File

@ -58,7 +58,7 @@
//#define SMEXT_CONF_METAMOD
/** Enable interfaces you want to use here by uncommenting lines */
//#define SMEXT_ENABLE_FORWARDSYS
#define SMEXT_ENABLE_FORWARDSYS
//#define SMEXT_ENABLE_HANDLESYS
//#define SMEXT_ENABLE_PLAYERHELPERS
//#define SMEXT_ENABLE_DBMANAGER

View File

@ -0,0 +1,76 @@
#include <sourcemod>
#include <accelerator>
#pragma newdecls required
#pragma semicolon 1
public Plugin myinfo =
{
name = "Accelerator Example",
author = "caxanga334",
description = "Example Accelerator natives plugin.",
version = "1.0.0",
url = "https://github.com/asherkin/accelerator"
};
public void OnPluginStart()
{
RegAdminCmd("sm_listcrashes", Command_ListCrashes, ADMFLAG_RCON, "List all uploaded crash dumps.");
// Call the forward manually. (For late loading plugins)
if (Accelerator_IsDoneUploadingCrashes())
{
Accelerator_OnDoneUploadingCrashes();
}
}
// This is called when the extension is done uploading crashes
public void Accelerator_OnDoneUploadingCrashes()
{
LogMessage("Accelerator is done uploading crashes!");
int max = Accelerator_GetUploadedCrashCount();
if (max == 0)
{
LogMessage("No crashes were uploaded!");
return;
}
char buffer[512];
for (int i = 0; i < max; i++)
{
Accelerator_GetCrashHTTPResponse(i, buffer, sizeof(buffer));
LogMessage("Crash #%i: HTTP reponse: \"%s\".", i, buffer);
}
}
// Admin command to list crashes
Action Command_ListCrashes(int client, int args)
{
if (!Accelerator_IsDoneUploadingCrashes())
{
ReplyToCommand(client, "Accelerator is still uploading crashes, please wait!");
return Plugin_Handled;
}
int max = Accelerator_GetUploadedCrashCount();
if (max == 0)
{
ReplyToCommand(client, "No crashes were uploaded!");
return Plugin_Handled;
}
char buffer[512];
for (int i = 0; i < max; i++)
{
Accelerator_GetCrashHTTPResponse(i, buffer, sizeof(buffer));
ReplyToCommand(client, "Crash #%i: HTTP reponse: \"%s\".", i, buffer);
}
return Plugin_Handled;
}

View File

@ -0,0 +1,67 @@
/**
* Accelerator's SourcePawn include file
*/
#if defined _accelerator_included
#endinput
#endif
#define _accelerator_included
/**
* Called when Accelerator is done uploading crash dumps.
*/
forward void Accelerator_OnDoneUploadingCrashes();
/**
* Returns the number of crashes uploaded.
*
* @return Number of crashes uploaded.
*/
native int Accelerator_GetUploadedCrashCount();
/**
* Returns if Accelerator is done uploading crashes.
*
* @return True if no more crashes will be uploaded, false if there are still crashes pending upload.
*/
native bool Accelerator_IsDoneUploadingCrashes();
/**
* Gets a crash's HTTP response from Accelerator's backend server.
*
* @note The response from the server can be anything and there is no guarantee a crash ID will be present.
* @param index The crash index, starts at 0 and goes up to Accelerator_GetUploadedCrashCount() - 1;
* @param buffer Buffer to store the crash HTTP response.
* @param size Size of the buffer parameter.
* @error Native called before Accelerator_IsDoneUploadingCrashes() returns true or invalid crash index passed.
*/
native void Accelerator_GetCrashHTTPResponse(int index, char[] buffer, int size);
/**
* Do not edit below this line!
*/
public Extension __ext_accelerator =
{
name = "accelerator",
file = "accelerator.ext",
#if defined AUTOLOAD_EXTENSIONS
autoload = 1,
#else
autoload = 0,
#endif
#if defined REQUIRE_EXTENSIONS
required = 1,
#else
required = 0,
#endif
};
#if !defined REQUIRE_EXTENSIONS
public __ext_accelerator_SetNTVOptional()
{
MarkNativeAsOptional("Accelerator_GetUploadedCrashCount");
MarkNativeAsOptional("Accelerator_IsDoneUploadingCrashes");
MarkNativeAsOptional("Accelerator_GetCrashHTTPResponse");
}
#endif