-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Crash due to ConVar overlap and release. #421 #430
Comments
Sorry, for joning the discussion so late, but here is my opinion on this topic. Deleting ConVars:The issue we have with deleting ConVars is that we don't have a reference counter. With a reference counter we could easily keep track whether or not a ConVar can be deleted, but without one we have a little problem. We could implement our own reference counter for ConVars we created on our own, but that would only work in a world where we are the only VSP. This isn't the case, obviously. So, we should think about a different solution as I don't want to have any avoidable incompatibilities with SM. And the only solution I can think of (currently) is not deleting any ConVars. This isn't the best solution, but it's not like we have a constant memory leak that grows with every function call. Once the ConVar is created the memory is reserved and it's not going to grow further. This will never cause a server to reserve all available RAM, which then results in a crash due to unsufficient memory. I wouldn't even delete the ConVar if SP is being unloaded, because we don't know if there is another VSP or SM plugin that relies on the same ConVar (e. g. because of an unfortune coincidence or because the foreign plugin extended an SP plugin). ConCommands overriding ConVarsI'm not really sure about this one. I would probably raise an exception if someone tries to create a ConCommand with the same name like a ConVar or vice-versa. This has two simple reasons:
|
Actually, we already do have a reference counter ( ../cvars/cvars.h/**
* =============================================================================
* Source Python
* Copyright (C) 2012-2015 Source Python Development Team. All rights reserved.
* =============================================================================
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, version 3.0, as published by the
* Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, the Source Python Team gives you permission
* to link the code of this program (as well as its derivative works) to
* "Half-Life 2," the "Source Engine," and any Game MODs that run on software
* by the Valve Corporation. You must obey the GNU General Public License in
* all respects for all other code used. Additionally, the Source.Python
* Development Team grants this exception to all derivative works.
*/
#ifndef _CVARS_H
#define _CVARS_H
//-----------------------------------------------------------------------------
// Includes.
//-----------------------------------------------------------------------------
#include "icvar.h"
#include "utilities/convar.h"
#include "utilities/sp_util.h"
#include "modules/commands/commands_server.h"
//-----------------------------------------------------------------------------
// ICvar shared extension class.
//-----------------------------------------------------------------------------
class ICVarSharedExt
{
public:
static ConVar *FindVar(ICvar *pCVar, const char *szName)
{
ConCommandBase *pBase = pCVar->FindCommandBase(szName);
if (pBase)
{
if (pBase->IsCommand()) {
CServerCommandManager *pManager = dynamic_cast<CServerCommandManager *>(pBase);
if (pManager && pManager->m_pOldCommand && !pManager->m_pOldCommand->IsCommand()) {
return static_cast<ConVar *>(pManager->m_pOldCommand);
}
}
else {
return static_cast<ConVar *>(pBase);
}
}
return NULL;
};
};
#include "boost/smart_ptr/weak_ptr.hpp"
#include "boost/unordered_map.hpp"
inline boost::unordered_map<ConVar *, boost::weak_ptr<ConVar> > *owned_references()
{
static boost::unordered_map<ConVar *, boost::weak_ptr<ConVar> > *s = new boost::unordered_map<ConVar *, boost::weak_ptr<ConVar> >();
return s;
}
//-----------------------------------------------------------------------------
// ConVar extension class.
//-----------------------------------------------------------------------------
class ConVarExt
{
public:
static boost::shared_ptr<ConVar> __init__(const char* name, const char* value,
const char* description, int flags, object min_value, object max_value)
{
if (!name || name[0] == '\0')
BOOST_RAISE_EXCEPTION(PyExc_ValueError, "An empty string is not a valid ConVar name.")
float fMin = 0;
float fMax = 0;
try {
fMin = extract<float>(min_value);
}
catch (...) {
PyErr_Clear();
}
try {
fMax = extract<float>(max_value);
}
catch (...) {
PyErr_Clear();
}
ConVar *pConVar = ICVarSharedExt::FindVar(g_pCVar, name);
if (pConVar) {
boost::unordered_map<ConVar *, boost::weak_ptr<ConVar> >::const_iterator it = owned_references()->find(pConVar);
if (it != owned_references()->end()) {
printf(">> Returning existing reference for %s (%d)\n", pConVar->GetName(), pConVar);
return boost::shared_ptr<ConVar>(it->second);
}
}
else
{
ConVar* pConVar = new ConVar(strdup(name), strdup(value), flags,
strdup(description), !min_value.is_none(), fMin, !max_value.is_none(), fMax);
printf(">> Creating new owned reference for %s (%d)\n", pConVar->GetName(), pConVar);
boost::shared_ptr<ConVar> s = boost::shared_ptr<ConVar>(pConVar, &Deleter);
owned_references()->insert(std::make_pair(pConVar, boost::weak_ptr<ConVar>(s)));
return s;
}
return boost::shared_ptr<ConVar>(pConVar, &NeverDeleteDeleter<ConVar *>);
}
static void Deleter(ConVar *pConVar)
{
ConCommandBase *pBase = g_pCVar->FindCommandBase(pConVar->GetName());
if (pBase) {
CServerCommandManager *pManager = dynamic_cast<CServerCommandManager *>(pBase);
if (pManager && pManager->m_pOldCommand == pConVar) {
pManager->m_pOldCommand = NULL;
}
else if (pBase == pConVar) {
printf(">> Unregistering %s (%d)\n", pConVar->GetName(), pConVar);
g_pCVar->UnregisterConCommand(pConVar);
}
}
boost::unordered_map<ConVar *, boost::weak_ptr<ConVar> >::const_iterator it = owned_references()->find(pConVar);
if (it != owned_references()->end()) {
printf(">> Erasing weak reference for %s (%d)\n", pConVar->GetName(), pConVar);
owned_references()->erase(it);
}
printf(">> Deleting owned reference for %s (%d)\n", pConVar->GetName(), pConVar);
delete pConVar;
}
static bool HasMin(ConVar* pConVar)
{
float fMin;
return pConVar->GetMin(fMin);
}
static bool HasMax(ConVar* pConVar)
{
float fMax;
return pConVar->GetMax(fMax);
}
static float GetMin(ConVar* pConVar)
{
float fMin;
pConVar->GetMin(fMin);
return fMin;
}
static bool GetMax(ConVar* pConVar)
{
float fMax;
pConVar->GetMax(fMax);
return fMax;
}
static void SetValue(ConVar* pConVar, bool bValue)
{
pConVar->SetValue(bValue);
}
static void MakePublic(ConVar* pConVar)
{
AddConCommandFlags(pConVar, FCVAR_NOTIFY);
g_pCVar->CallGlobalChangeCallbacks(pConVar, pConVar->GetString(), pConVar->GetFloat());
}
static void RemovePublic(ConVar* pConVar)
{
RemoveConCommandFlags(pConVar, FCVAR_NOTIFY);
g_pCVar->CallGlobalChangeCallbacks(pConVar, pConVar->GetString(), pConVar->GetFloat());
}
};
#endif // _CVARS_H
../test1/test1.pyfrom cvars import ConVar
test_convar = ConVar("test_convar", "1", "Test Convar(test1).") ../test2/test2.pyfrom commands.server import ServerCommand
from cvars import ConVar
test_convar = ConVar("test_convar", "2", "Test Convar(test2).")
@ServerCommand("set_test")
def test_command(*args):
test_convar.set_string("3") ../collect/collect.pyimport gc
gc.collect()
Despite the assumptions that have been presented as facts countless times; they are not affected whether we release our That said, as also stated into the other thread, our commands act the same exact way. For example: ../test1/test1.pyfrom commands.server import *
@ServerCommand('test1')
def test1(command):
... ../test2/test2.pyfrom commands.server import *
from commands import *
test1 = get_server_command('test1')
@ServerCommand('test2')
def test2(command):
test1.dispatch(Command()) Then running Anyways, I just wanted to clarify these points. You guys go ahead and take the final decision and make the appropriate changes. |
This is an issue derived from #421 and introduced by 02e3490. There is a long discussion going on in #421, and I've created an issue for the record.
The main issue is due to Source.Python releasing the ConVar object created by ConVar(str) when the plugin is unloaded. This will cause other Source.Python/SourceMod plugins to crash if they hold the same ConVar object.
Code:
Plugin1 (test1)
Plugin2 (test2) Or a SourceMod plugin with equivalent functionality.
Output;
The same problem occurs with the wrapping plugin for ConVars.
The root of the problem lies in the fact that ConVar(str) returns an existing registered ConVar.
Source.Python/src/core/modules/cvars/cvars.h
Lines 95 to 104 in 02e3490
In the Valve Server Plugin, the ConVar used by the plugin is created each time it is loaded, and released when it is unloaded. Also, when accessing a ConVar that already exists, the plugin retrieves the ConVar from ICvar.FindVar(g_pCVar->FindVar) and checks if it is valid before accessing it. However, SourcePython's ConVar(str) and SourceMod's CreateConVar return an existing ConVar, so if there are overlapping ConVars, intentionally or unintentionally, the plugin will own a single ConVar, resulting in a crash.
Unfortunately, ConVar(str) is used by many plugins to access an existing ConVar instead of cvar.find_var which should be used, and it is not possible to not return an existing ConVar without causing a major compatibility break.
But either way, when ConVar overlaps, the parent is set and the children access the parent, so when the parent is released, this also causes a crash. (This has been disabled in 02e3490, but should not be disabled for warnings.)
Source.Python/src/core/modules/commands/commands_server.cpp
Lines 54 to 62 in 02e3490
We should stop releasing ConVar when unloading plugins, and if we do release it, at least it should be done when unloading Source.Python VSP, so that plugins can work safely. However, if we want to be compatible with SourceMod, I think we should prohibit release of ConVar itself.
The text was updated successfully, but these errors were encountered: