Commit be948594 authored by jehan's avatar jehan

Fix presence server memory leaks

parent 8552cd8d
......@@ -80,12 +80,16 @@ ListSubscription::ListSubscription(unsigned int expires, belle_sip_server_transa
}
list<shared_ptr<PresentityPresenceInformationListener>> &ListSubscription::getListeners() {
shared_ptr<PresentityPresenceInformationListener> toto =
make_shared<PresentityResourceListener>(*this, belle_sip_uri_new());
return mListeners;
}
ListSubscription::~ListSubscription() {
if (mTimer) {
belle_sip_main_loop_cancel_source( belle_sip_stack_get_main_loop(belle_sip_provider_get_sip_stack(mProv))
, belle_sip_source_get_id(mTimer));
belle_sip_object_unref(this->mTimer);
}
belle_sip_object_unref((void *)mName);
SLOGD << "List souscription ["<< this <<"] deleted";
};
void ListSubscription::addInstanceToResource(rlmi::Resource &resource, list<belle_sip_body_handler_t *> &multipartList,
......@@ -225,6 +229,7 @@ void ListSubscription::onInformationChanged(PresentityPresenceInformation &prese
belle_sip_source_cpp_func_t *func = new belle_sip_source_cpp_func_t([this](unsigned int events) {
this->notify(FALSE);
SLOGD << "defered notify sent on [" << this << "]";
belle_sip_object_unref(this->mTimer);
this->mTimer = NULL;
return BELLE_SIP_STOP;
});
......@@ -232,9 +237,10 @@ void ListSubscription::onInformationChanged(PresentityPresenceInformation &prese
chrono::milliseconds timeout(chrono::duration_cast<chrono::milliseconds>(
mMinNotifyIntervale - (chrono::system_clock::now() - mLastNotify)));
mTimer = belle_sip_main_loop_create_timeout(
belle_sip_stack_get_main_loop(belle_sip_provider_get_sip_stack(mProv)),
(belle_sip_source_func_t)belle_sip_source_cpp_func, func, timeout.count(), "timer for list notify");
mTimer = belle_sip_main_loop_create_cpp_timeout( belle_sip_stack_get_main_loop(belle_sip_provider_get_sip_stack(mProv))
, func
, timeout.count()
, "timer for list notify");
}
if (mVersion > 0) {
......
......@@ -19,9 +19,7 @@
#include "presence-server.h"
#include "belle-sip/belle-sip.h"
#include "pidf+xml.hxx"
//#include "application_pidf+xml/pidf+xml-pimpl.hxx"
#include "resource-lists.hxx"
//#include "resource-list/resource-lists-pimpl.hxx"
#include "presentity-presenceinformation.hh"
#include "list-subscription.hh"
#include "bellesip-signaling-exception.hh"
......@@ -70,7 +68,7 @@ PresenceServer::Init::Init() {
}
PresenceServer::PresenceServer(std::string configFile) throw(FlexisipException)
: mStarted(true), mStack(belle_sip_stack_new(NULL)), mProvider(belle_sip_stack_create_provider(mStack, NULL)),
: mStarted((belle_sip_object_enable_leak_detector(TRUE),true)), mStack(belle_sip_stack_new(NULL)), mProvider(belle_sip_stack_create_provider(mStack, NULL)),
mIterateThread([this]() {
while (mStarted)
belle_sip_stack_sleep(this->mStack, 100);
......@@ -107,16 +105,32 @@ PresenceServer::PresenceServer(std::string configFile) throw(FlexisipException)
GenericManager::get()->getRoot()->get<GenericStruct>("presence-server")->get<ConfigInt>("expires")->read();
SLOGD << "Presence server configuration file [" << configFile << "] Successfully loaded ";
}
static void remove_listening_point(belle_sip_listening_point_t* lp,belle_sip_provider_t* prov) {
belle_sip_provider_remove_listening_point(prov,lp);
}
PresenceServer::~PresenceServer() {
belle_sip_provider_clean_channels(mProvider);
const belle_sip_list_t * lps = belle_sip_provider_get_listening_points(mProvider);
belle_sip_list_t * tmp_list = belle_sip_list_copy(lps);
belle_sip_list_for_each2 (tmp_list,(void (*)(void*,void*))remove_listening_point,mProvider);
belle_sip_list_free(tmp_list);
mStarted = false;
mIterateThread.join();
belle_sip_object_unref(mProvider);
belle_sip_object_unref(mStack);
belle_sip_object_unref(mListener);
// must be done before cleaning xerces
SLOGD << "Still ["<<mPresenceInformations.size()<<"] PresenceInformations referenced, clearing";
mPresenceInformations.clear();
SLOGD << "Still ["<<mPresenceInformationsByEtag.size()<<"] PresenceInformationsByEtag referenced, clearing";
mPresenceInformationsByEtag.clear();
xercesc::XMLPlatformUtils::Terminate();
belle_sip_object_dump_active_objects();
belle_sip_object_flush_active_objects();
SLOGD << "Presence server destroyed";
}
void PresenceServer::start() throw(FlexisipException) {
......@@ -140,11 +154,11 @@ void PresenceServer::start() throw(FlexisipException) {
}
}
void PresenceServer::processDialogTerminated(PresenceServer *thiz, const belle_sip_dialog_terminated_event_t *event) {
// belle_sip_dialog_t *dialog = belle_sip_dialog_terminated_event_get_dialog(event);
// Subscription *sub = static_cast<Subscription*>(belle_sip_dialog_get_application_data(dialog));
// thiz->removeSubscription(sub);
// nothing to be done for now because expire is performed at SubscriptionLevel
belle_sip_dialog_t *dialog = belle_sip_dialog_terminated_event_get_dialog(event);
Subscription *sub = static_cast<Subscription*>(belle_sip_dialog_get_application_data(dialog));
if (dynamic_cast<ListSubscription *>(sub)) {
thiz->removeSubscription(sub);
} //else nothing to be done for now because expire is performed at SubscriptionLevel
}
void PresenceServer::processIoError(PresenceServer *thiz, const belle_sip_io_error_event_t *event) {
SLOGD << "PresenceServer::processIoError not implemented yet";
......@@ -369,7 +383,7 @@ void PresenceServer::processPublishRequestEvent(const belle_sip_request_event_t
if (!(presenceInfo = getPresenceInfo(entity))) {
presenceInfo.reset(new PresentityPresenceInformation(entity, *this, belle_sip_stack_get_main_loop(mStack)));
SLOGD << "New Presentity [" << *presenceInfo << "] created";
SLOGD << "New Presentity [" << *presenceInfo << "] created from PUBLISH";
// for (const belle_sip_uri_t* : mPresenceInformations.keys())
addPresenceInfo(presenceInfo);
} else {
......@@ -397,6 +411,7 @@ void PresenceServer::processPublishRequestEvent(const belle_sip_request_event_t
if (expires == 0) {
if (presenceInfo)
presenceInfo->removeTuplesForEtag(eTag);
invalidateETag(eTag);
/*else already expired*/
} else {
if (presenceInfo)
......@@ -557,7 +572,7 @@ void PresenceServer::processSubscribeRequestEvent(const belle_sip_request_event_
SLOGD << "Subscribe for resource list "
<< "for dialog [" << BELLE_SIP_OBJECT(dialog) << "]";
ListSubscription *listSubscription = new ListSubscription(expires, server_transaction, mProvider);
ListSubscription *listSubscription = new ListSubscription(expires, server_transaction, mProvider); // will be release when last PresentityPresenceInformationListener is released
if (acceptEncodingHeader) listSubscription->setAcceptEncodingHeader(acceptEncodingHeader);
// send 200ok late to allow deeper anylise of request
belle_sip_server_transaction_send_response(server_transaction, resp);
......@@ -613,8 +628,6 @@ void PresenceServer::processSubscribeRequestEvent(const belle_sip_request_event_
// 3.1.4.1.)
if (!subscription || subscription->getState() == Subscription::State::terminated) {
// fixme
if (subscription) delete subscription;
belle_sip_dialog_set_application_data(dialog, NULL);
throw BELLESIP_SIGNALING_EXCEPTION(481) << "Subscription [" << std::hex << (long)subscription << "] for dialog ["
<< BELLE_SIP_OBJECT(dialog) << "] already in terminated state";
......@@ -689,7 +702,17 @@ std::shared_ptr<PresentityPresenceInformation> PresenceServer::getPresenceInfo(c
}
void PresenceServer::invalidateETag(const string &eTag) {
mPresenceInformationsByEtag.erase(eTag);
auto presenceInformationsByEtagIt = mPresenceInformationsByEtag.find(eTag);
if (presenceInformationsByEtagIt != mPresenceInformationsByEtag.end()) {
const std::shared_ptr<PresentityPresenceInformation> presenceInfo = presenceInformationsByEtagIt->second;
if (presenceInfo->getNumberOfListeners() == 0 && presenceInfo->getNumberOfInformationElements() == 0) {
SLOGD << "Presentity [" << *presenceInfo << "] no longuer referenced by any SUBSCRIBE nor PUBLISH, removing";
mPresenceInformations.erase(presenceInfo->getEntity());
}
mPresenceInformationsByEtag.erase(eTag);
SLOGD <<"Etag manager size ["<<mPresenceInformationsByEtag.size()<<"]";
}
}
void PresenceServer::modifyEtag(const string &oldEtag, const string &newEtag) throw(FlexisipException) {
auto presenceInformationsByEtagIt = mPresenceInformationsByEtag.find(oldEtag);
......@@ -704,6 +727,7 @@ void PresenceServer::addEtag(const std::shared_ptr<PresentityPresenceInformation
if (presenceInformationsByEtagIt != mPresenceInformationsByEtag.end())
throw FLEXISIP_EXCEPTION << "Already existing etag [" << etag << "] use PresenceServer::modifyEtag instead ";
mPresenceInformationsByEtag[etag] = info;
SLOGD <<"Etag manager size ["<<mPresenceInformationsByEtag.size()<<"]";
}
void PresenceServer::addOrUpdateListener(shared_ptr<PresentityPresenceInformationListener> &listener, int expires) {
......@@ -712,15 +736,19 @@ void PresenceServer::addOrUpdateListener(shared_ptr<PresentityPresenceInformatio
/*no information available yet, but creating entry to be able to register subscribers*/
presenceInfo.reset(new PresentityPresenceInformation(listener->getPresentityUri(), *this,
belle_sip_stack_get_main_loop(mStack)));
SLOGD << "New Presentity [" << *presenceInfo << "] created";
SLOGD << "New Presentity [" << *presenceInfo << "] created from SUBSCRIBE";
addPresenceInfo(presenceInfo);
}
presenceInfo->addOrUpdateListener(listener, expires);
}
void PresenceServer::removeListener(shared_ptr<PresentityPresenceInformationListener> &listener) {
void PresenceServer::removeListener(const shared_ptr<PresentityPresenceInformationListener> &listener) {
const std::shared_ptr<PresentityPresenceInformation> presenceInfo = getPresenceInfo(listener->getPresentityUri());
if (presenceInfo) {
presenceInfo->removeListener(listener);
if (presenceInfo->getNumberOfListeners() == 0 && presenceInfo->getNumberOfInformationElements() == 0) {
SLOGD << "Presentity [" << *presenceInfo << "] no longuer referenced by any SUBSCRIBE nor PUBLISH, removing";
mPresenceInformations.erase(presenceInfo->getEntity());
}
} else
SLOGW << "No presence info for this entity [" << listener->getPresentityUri() << "]/[" << std::hex
<< (long)&listener << "]";
......@@ -739,6 +767,6 @@ void PresenceServer::removeSubscription(Subscription *subscription) throw() {
removeListener(listener);
}
dynamic_cast<Subscription *>(listSubscription)->notify(NULL); // to trigger final notify
// fixme de delete listSubscription ???
delete listSubscription;
}
}
......@@ -52,7 +52,7 @@ class Subscription;
class PresentityPresenceInformation;
class Listener;
class PresenceServer : EtagManager,PresentityManager {
class PresenceServer : PresentityManager {
public:
PresenceServer(std::string configFile) throw (FlexisipException);
~PresenceServer();
......@@ -105,7 +105,7 @@ private:
*/
void addOrUpdateListener(shared_ptr<PresentityPresenceInformationListener>& listerner,int expires);
void removeListener(shared_ptr<PresentityPresenceInformationListener>& listerner);
void removeListener(const shared_ptr<PresentityPresenceInformationListener>& listerner);
void removeSubscription(Subscription* identity) throw();
//void notify(Subscription& subscription,PresentityPresenceInformation& presenceInformation) throw (FlexisipException);
......
......@@ -20,14 +20,15 @@
#define PRESENTITY_MANAGER_HH_
#include "string"
#include "utils/flexisip-exception.hh"
#include "etag-manager.hh"
namespace flexisip {
class PresentityPresenceInformationListener;
class PresentityManager {
class PresentityManager : public EtagManager {
public:
virtual void addOrUpdateListener(shared_ptr<PresentityPresenceInformationListener> &listerner, int exires) = 0;
virtual void removeListener(shared_ptr<PresentityPresenceInformationListener> &listerner) = 0;
virtual void removeListener(const shared_ptr<PresentityPresenceInformationListener> &listerner) = 0;
};
}
#endif /* PRESENTITY_MANAGER_HH_ */
......@@ -23,6 +23,7 @@
#include "etag-manager.hh"
#include "pidf+xml.hxx"
#include <memory>
#include "presentity-manager.hh"
#define ETAG_SIZE 8
using namespace pidf;
......@@ -36,9 +37,9 @@ FlexisipException &operator<<(FlexisipException &e, const xml_schema::Exception
return e;
}
PresentityPresenceInformation::PresentityPresenceInformation(const belle_sip_uri_t *entity, EtagManager &etagManager,
PresentityPresenceInformation::PresentityPresenceInformation(const belle_sip_uri_t *entity, PresentityManager &presentityManager,
belle_sip_main_loop_t *mainloop)
: mEntity((belle_sip_uri_t *)belle_sip_object_clone(BELLE_SIP_OBJECT(entity))), mEtagManager(etagManager),
: mEntity((belle_sip_uri_t *)belle_sip_object_clone(BELLE_SIP_OBJECT(entity))), mPresentityManager(presentityManager),
mBelleSipMainloop(mainloop) {
belle_sip_object_ref(mainloop);
belle_sip_object_ref((void *)mEntity);
......@@ -53,10 +54,11 @@ PresentityPresenceInformation::~PresentityPresenceInformation() {
belle_sip_object_unref((void *)mBelleSipMainloop);
SLOGD << "Presence information [" << this << "] deleted";
}
static int source_func(std::function<int(unsigned int)> *user_data, unsigned int events) {
int result = (*user_data)(events);
delete user_data;
return result;
size_t PresentityPresenceInformation::getNumberOfListeners() const {
return mSubscribers.size();
}
size_t PresentityPresenceInformation::getNumberOfInformationElements() const {
return mInformationElements.size();
}
string PresentityPresenceInformation::putTuples(pidf::Presence::TupleSequence &tuples,
pidf::Presence::AnySequence &extensions, int expires) {
......@@ -116,28 +118,29 @@ string PresentityPresenceInformation::setOrUpdate(pidf::Presence::TupleSequence
informationElement->setEtag(generatedETag);
// cb function to invalidate an unrefreshed etag;
std::function<int(unsigned int)> *func =
new std::function<int(unsigned int)>([this, generatedETag](unsigned int events) {
belle_sip_source_cpp_func_t *func =
new belle_sip_source_cpp_func_t([this, generatedETag](unsigned int events) {
// find information element
this->removeTuplesForEtag(generatedETag);
mEtagManager.invalidateETag(generatedETag);
mPresentityManager.invalidateETag(generatedETag);
SLOGD << "eTag [" << generatedETag << "] has expired";
return BELLE_SIP_STOP;
});
// create timer
belle_sip_source_t *timer =
belle_sip_main_loop_create_timeout(mBelleSipMainloop, (belle_sip_source_func_t)belle_sip_source_cpp_func, func,
expires * 1000, "timer for presence Info");
belle_sip_source_t *timer = belle_sip_main_loop_create_cpp_timeout( mBelleSipMainloop
, func
, expires * 1000
, "timer for presence Info");
// set expiration timer
informationElement->setExpiresTimer(timer);
// modify global etag list
if (eTag && eTag->size() > 0) {
mEtagManager.modifyEtag(*eTag, generatedETag);
mPresentityManager.modifyEtag(*eTag, generatedETag);
mInformationElements.erase(*eTag);
} else {
mEtagManager.addEtag(shared_from_this(), generatedETag);
mPresentityManager.addEtag(shared_from_this(), generatedETag);
}
// modify etag list for this presenceInfo
......@@ -201,17 +204,17 @@ void PresentityPresenceInformation::addOrUpdateListener(shared_ptr<PresentityPre
SLOGD << op << " listener [" << listener.get() << "] on [" << *this << "] for [" << expires << "] seconds";
// PresentityPresenceInformationListener* listener_ptr=listener.get();
// cb function to invalidate an unrefreshed etag;
std::function<int(unsigned int)> *func =
new std::function<int(unsigned int)>([this, listener /*_ptr*/](unsigned int events) {
listener->onExpired(*this);
this->removeListener(listener);
belle_sip_source_cpp_func_t *func =
new belle_sip_source_cpp_func_t([this, listener/*_ptr*/](unsigned int events) {
SLOGD << "Listener [" << listener.get() << "] on [" << *this << "] has expired";
listener->onExpired(*this);
this->mPresentityManager.removeListener(listener);
return BELLE_SIP_STOP;
});
// create timer
belle_sip_source_t *timer =
belle_sip_main_loop_create_timeout(mBelleSipMainloop, (belle_sip_source_func_t)source_func, func,
expires * 1000, "timer for presence info listener");
belle_sip_source_t *timer = belle_sip_main_loop_create_cpp_timeout(mBelleSipMainloop
, func
, expires * 1000, "timer for presence info listener");
// set expiration timer
listener->setExpiresTimer(mBelleSipMainloop, timer);
......@@ -256,7 +259,7 @@ string PresentityPresenceInformation::getPidf() throw(FlexisipException) {
for (const unique_ptr<pidf::Tuple> &tup : element.second->getTuples()) {
// check for multiple tupple id, may happend with buggy presence publisher
if (find(tupleList.begin(), tupleList.end(), tup.get()->getId()) == tupleList.end()) {
presence.getTuple().push_back(*tup->_clone());
presence.getTuple().push_back(*tup);
tupleList.push_back(tup.get()->getId());
} else {
SLOGW << "Already existing tuple id [" << tup.get()->getId() << " for [" << *this << "], skipping";
......@@ -270,7 +273,7 @@ string PresentityPresenceInformation::getPidf() throw(FlexisipException) {
}
if (mInformationElements.size() == 0 && mDefaultInformationElement != nullptr) {
// insering default tuple
presence.getTuple().push_back(*mDefaultInformationElement->getTuples().begin()->get()->_clone());
presence.getTuple().push_back(*mDefaultInformationElement->getTuples().begin()->get());
}
pidf::Note value;
namespace_::Lang lang("en");
......@@ -306,19 +309,16 @@ void PresentityPresenceInformation::notifyAll() {
PresentityPresenceInformationListener::PresentityPresenceInformationListener() : mTimer(NULL) {
}
PresentityPresenceInformationListener::~PresentityPresenceInformationListener() {
if (mTimer) {
belle_sip_object_unref(mTimer);
}
setExpiresTimer(mBelleSipMainloop, NULL);
}
void PresentityPresenceInformationListener::setExpiresTimer(belle_sip_main_loop_t *ml, belle_sip_source_t *timer) {
if (mTimer) {
// canceling previous timer
belle_sip_main_loop_remove_source(ml, mTimer);
belle_sip_main_loop_cancel_source(ml, belle_sip_source_get_id(mTimer));;
belle_sip_object_unref(mTimer);
}
mBelleSipMainloop=ml;
mTimer = timer;
if (mTimer)
belle_sip_object_ref(mTimer);
}
// PresenceInformationElement
......@@ -337,8 +337,10 @@ PresenceInformationElement::PresenceInformationElement(pidf::Presence::TupleSequ
for (pidf::Presence::AnySequence::iterator domElement = extensions->begin(); domElement != extensions->end();
domElement++) {
SLOGD << "Adding extension element [" << xercesc::XMLString::transcode(domElement->getNodeName())
char * transcodedString = xercesc::XMLString::transcode(domElement->getNodeName());
SLOGD << "Adding extension element [" << transcodedString
<< "] to presence info element [" << this << "]";
xercesc::XMLString::release(&transcodedString);
mExtensions.push_back(dynamic_cast<xercesc::DOMElement *>(mDomDocument->importNode(&*domElement, true)));
}
}
......@@ -373,21 +375,23 @@ PresenceInformationElement::PresenceInformationElement(const belle_sip_uri_t *co
now->tm_min, now->tm_sec));
tup->setContact(::pidf::Contact(contact_as_string));
mTuples.push_back(std::unique_ptr<Tuple>(tup.release()));
belle_sip_free(contact_as_string);
}
PresenceInformationElement::~PresenceInformationElement() {
if (mBelleSipMainloop && mTimer)
belle_sip_main_loop_remove_source(mBelleSipMainloop, mTimer);
if (mBelleSipMainloop)
setExpiresTimer(NULL);
SLOGD << "Presence information element [" << std::hex << (long)this << "] deleted";
}
void PresenceInformationElement::setExpiresTimer(belle_sip_source_t *timer) {
if (mTimer) {
// canceling previous timer
belle_sip_main_loop_remove_source(mBelleSipMainloop, mTimer);
belle_sip_main_loop_cancel_source(mBelleSipMainloop, belle_sip_source_get_id(mTimer));
belle_sip_object_unref(mTimer);
}
mTimer = timer;
belle_sip_object_ref(mTimer);
}
const std::unique_ptr<pidf::Tuple> &PresenceInformationElement::getTuple(const string &id) const {
for (const std::unique_ptr<Tuple> &tup : mTuples) {
......
......@@ -30,7 +30,7 @@ typedef struct belle_sip_source belle_sip_source_t;
typedef struct belle_sip_main_loop belle_sip_main_loop_t;
using namespace std;
namespace flexisip {
class EtagManager;
class PresentityManager;
class PresenceInformationElement {
public:
PresenceInformationElement(pidf::Presence::TupleSequence *tuples, pidf::Presence::AnySequence *extensions,
......@@ -79,6 +79,7 @@ class PresentityPresenceInformationListener : public enable_shared_from_this<Pre
virtual void onExpired(PresentityPresenceInformation &presenceInformation) = 0;
private:
belle_sip_main_loop_t *mBelleSipMainloop;
belle_sip_source_t *mTimer;
};
......@@ -119,7 +120,7 @@ class PresentityPresenceInformation : public std::enable_shared_from_this<Presen
* */
void removeTuplesForEtag(const string &eTag);
PresentityPresenceInformation(const belle_sip_uri_t *entity, EtagManager &etagManager, belle_sip_main_loop_t *ml);
PresentityPresenceInformation(const belle_sip_uri_t *entity, PresentityManager &presentityManager, belle_sip_main_loop_t *ml);
virtual ~PresentityPresenceInformation();
const belle_sip_uri_t *getEntity() const;
......@@ -142,6 +143,16 @@ class PresentityPresenceInformation : public std::enable_shared_from_this<Presen
* return true if a presence info is already known from a publish
*/
bool isKnown();
/*
* return number of current listeners (I.E subscriber)
*/
size_t getNumberOfListeners() const;
/*
* return current number of information elements (I.E from PUBLISH)
*/
size_t getNumberOfInformationElements() const;
private:
/*
......@@ -155,7 +166,7 @@ class PresentityPresenceInformation : public std::enable_shared_from_this<Presen
void notifyAll();
const belle_sip_uri_t *mEntity;
EtagManager &mEtagManager;
PresentityManager &mPresentityManager;
belle_sip_main_loop_t *mBelleSipMainloop;
// Tuples ordered by Etag.
std::map<std::string /*Etag*/, PresenceInformationElement *> mInformationElements;
......
......@@ -138,6 +138,7 @@ void Subscription::notify(belle_sip_header_content_type_t *content_type, const s
Subscription::~Subscription() {
belle_sip_object_unref(mDialog);
belle_sip_object_unref(mProv);
setAcceptEncodingHeader(NULL);
}
Subscription::State Subscription::getState() const {
......@@ -164,6 +165,7 @@ PresenceSubscription::PresenceSubscription(unsigned int expires, const belle_sip
}
PresenceSubscription::~PresenceSubscription() {
belle_sip_object_unref((void *)mPresentity);
SLOGD << "PresenceSubscription ["<<this<<"] deleted";
}
const belle_sip_uri_t *PresenceSubscription::getPresentityUri() const {
return mPresentity;
......
belle-sip @ 5137bf79
Subproject commit 0d7d95df4806cf4751355f577437ab2da1e3ea72
Subproject commit 5137bf79cc1d82103c4c5da5c6299ef6fb700a00
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment