完善后端析构逻辑以及优化日志性能
This commit is contained in:
@@ -1,216 +0,0 @@
|
||||
#include <qthread.h>
|
||||
#include <serviceManager.h>
|
||||
#include <sql/qSqlBase.h>
|
||||
#include <missionLoader.h>
|
||||
#include <CLIManager.h>
|
||||
#include <back/missionResultHandle.h>
|
||||
#include <jsonRpcServer.h>
|
||||
class detProcess : public QThread
|
||||
{
|
||||
public:
|
||||
CsvWriter* _csvWriter = nullptr;
|
||||
missionResultHandle* _missionResultHandle = nullptr;
|
||||
ServiceManager _serviceManager;
|
||||
CLIManager* _cliManager; // 命令行接口管理器
|
||||
rpc::JsonRpcServer _rpcServer;
|
||||
bool initOnce = false;
|
||||
detProcess(SQLiteDB* dbIn, nlohmann::ordered_json& jsonIn, qtFuncBridge* bridgeIn):
|
||||
db(dbIn),bridge(bridgeIn), missionJson(jsonIn){
|
||||
|
||||
}
|
||||
|
||||
// 对普通指针的安全删除
|
||||
template<typename T>
|
||||
void safeDelete(T*& ptr) {
|
||||
if (ptr) { delete ptr; ptr = nullptr; }
|
||||
}
|
||||
|
||||
// 尝试对基于 QThread 的 serviceModule 进行关停,然后删除
|
||||
void safeShutdownDelete(serviceModule* ptr) {
|
||||
if (!ptr) return;
|
||||
// 如果对象正在运行,尝试停止
|
||||
if (ptr->isRunning()) {
|
||||
// 如果存在 stop 槽,则尝试调用
|
||||
QMetaObject::invokeMethod(ptr, "stop", Qt::DirectConnection);
|
||||
// 作为后备,尝试退出事件循环
|
||||
QMetaObject::invokeMethod(ptr, "quit", Qt::DirectConnection);
|
||||
// 等待其退出,给予一定时间
|
||||
if (!ptr->wait(2000)) {
|
||||
// 仍在运行时,作为最后手段强制终止
|
||||
ptr->terminate();
|
||||
ptr->wait(1000);
|
||||
}
|
||||
}
|
||||
delete ptr; ptr = nullptr;
|
||||
}
|
||||
|
||||
void init() {
|
||||
// 创建/重建需要的服务模块,避免重复创建
|
||||
if (!_serviceManager._imgSaveThreadManager) {
|
||||
_serviceManager._imgSaveThreadManager = new imgSaveThreadManager(missionJson["imgSaveBasePath"]);
|
||||
}
|
||||
if (!_serviceManager._cameraManager) {
|
||||
_serviceManager._cameraManager = new cameraManager(missionJson, bridge);
|
||||
}
|
||||
if (!_serviceManager._triggerManager) {
|
||||
_serviceManager._triggerManager = new triggerManager(this, bridge, missionJson["Triggers"]);
|
||||
}
|
||||
if (initOnce == false) {
|
||||
initOnce = true;
|
||||
_csvWriter = new CsvWriter(db);
|
||||
_serviceManager._detDataManager = new detDataManager();
|
||||
_missionResultHandle = new missionResultHandle(bridge);
|
||||
_serviceManager._missionResultHandle = _missionResultHandle;
|
||||
_serviceManager.dataBase = db;
|
||||
_serviceManager._csvWriter = _csvWriter;
|
||||
// handleAiModelManager 与 CLI 等只在首次初始化创建
|
||||
if (!_serviceManager._handleAiModelManager) {
|
||||
_serviceManager._handleAiModelManager = new handleAiModelManager(missionJson, bridge);
|
||||
}
|
||||
connect(bridge, &qtFuncBridge::reInitConfig, this, [=]() {reinitConfig(); });
|
||||
_cliManager = new CLIManager(&_serviceManager, bridge);
|
||||
}
|
||||
}
|
||||
|
||||
bool waitrForInitResult(serviceModule* ptr, int overtimeS = 999999) {
|
||||
int overTimeMs = overtimeS * 1000;
|
||||
int timecnt = 0;
|
||||
while (timecnt <= overTimeMs) {
|
||||
if (ptr->status != NotInit)
|
||||
break;
|
||||
QThread::msleep(100);
|
||||
timecnt += 100;
|
||||
}
|
||||
if (timecnt > overTimeMs)
|
||||
return false;
|
||||
if (ptr->status == InitFailure)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 重新初始化配置
|
||||
void reinitConfig() {
|
||||
// 停止并删除线程型模块
|
||||
safeShutdownDelete(_serviceManager._cameraManager);
|
||||
safeShutdownDelete(_serviceManager._triggerManager);
|
||||
if (_serviceManager._handleAiModelManager) {
|
||||
// try invoking stop if exists
|
||||
QMetaObject::invokeMethod(_serviceManager._handleAiModelManager, "stop", Qt::DirectConnection);
|
||||
safeDelete(_serviceManager._handleAiModelManager);
|
||||
}
|
||||
init();
|
||||
if (!this->isRunning()) {
|
||||
this->start();
|
||||
} else {
|
||||
qDebug() << "detProcess already running, init refreshed. run() should handle reloading if needed.";
|
||||
}
|
||||
}
|
||||
// 初始化并运行主流程
|
||||
void run() {
|
||||
#if 1
|
||||
_serviceManager._cameraManager->start();
|
||||
_serviceManager._handleAiModelManager->start();
|
||||
_serviceManager._triggerManager->start();
|
||||
bool res1 = waitrForInitResult(_serviceManager._cameraManager);
|
||||
bool res2 = waitrForInitResult(_serviceManager._handleAiModelManager);
|
||||
bool res3 = waitrForInitResult(_serviceManager._triggerManager);
|
||||
|
||||
if (res1 == true && res2 == true && res3 == true) {
|
||||
_missionLoader.loadMission(missionJson["missionDllPath"]);
|
||||
_missionLoader.getmission(missionJson["missionDllPath"])->initBasic(
|
||||
&_serviceManager,
|
||||
missionJson["missionSettings"], bridge
|
||||
);
|
||||
_cliManager->initBasicPreset(_missionLoader.getmission(missionJson["missionDllPath"]));
|
||||
_serviceManager._detDataManager->setTriggerManager(_serviceManager._triggerManager);
|
||||
initJsonRpc();
|
||||
emit bridge->initResult(InitOK);
|
||||
}
|
||||
else {
|
||||
emit bridge->initResult(InitFailure);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
void initJsonRpc() {
|
||||
//QObject::connect(&_rpcServer, &JsonRpcServer::clientConnected,
|
||||
// [](const QString& address) {
|
||||
// qInfo() << "New client connected:" << address;
|
||||
// });
|
||||
|
||||
//QObject::connect(&_rpcServer, &JsonRpcServer::requestReceived,
|
||||
// [](const QString& method, const json& params, const QString& address) {
|
||||
// qDebug() << "Request from" << address << "- Method:" << method;
|
||||
// });
|
||||
|
||||
/* _rpcServer.registerMethod("getServerInfo", [](const json& params) -> json {
|
||||
return json{
|
||||
{"name", "JSON-RPC Server"},
|
||||
{"version", "1.0.0"},
|
||||
{"timestamp", QDateTime::currentSecsSinceEpoch()}
|
||||
};
|
||||
});*/
|
||||
//_rpcServer.registerMethod("getMachineInfo", [](const json& params) -> json {
|
||||
// auto& config = appConfig::getInstance();
|
||||
// return json{
|
||||
// {"name", config.softwareTile.c_str()},
|
||||
// {"timestamp", QDateTime::currentSecsSinceEpoch()}
|
||||
// };
|
||||
// });
|
||||
//_rpcServer.registerMethod("reloadConfig", [this](const json& params) -> json {
|
||||
// _cliManager->cliCommand("reloadConfig");
|
||||
// return json{
|
||||
// {"result", "OK"} };
|
||||
// });
|
||||
//// 添加连接信号用于调试
|
||||
//QObject::connect(&_rpcServer, &JsonRpcServer::clientConnected,
|
||||
// [](const QString& address) {
|
||||
// qInfo() << "RPC Client connected:" << address;
|
||||
// });
|
||||
|
||||
//QObject::connect(&_rpcServer, &JsonRpcServer::clientDisconnected,
|
||||
// [](const QString& address) {
|
||||
// qInfo() << "RPC Client disconnected:" << address;
|
||||
// });
|
||||
|
||||
//QObject::connect(&_rpcServer, &JsonRpcServer::requestReceived,
|
||||
// [](const QString& method, const json& params, const QString& address) {
|
||||
// qDebug() << "RPC Request from" << address << "- Method:" << method;
|
||||
// });
|
||||
|
||||
//QObject::connect(&_rpcServer, &JsonRpcServer::errorOccurred,
|
||||
// [](const QString& error, const QString& address) {
|
||||
// qWarning() << "RPC Error from" << address << ":" << error;
|
||||
// });
|
||||
|
||||
_rpcServer.registerMethod(QStringLiteral("reloadConfig"), [this](const nlohmann::json&) -> nlohmann::json {
|
||||
_cliManager->cliCommand("reloadConfig");
|
||||
std::cout << "接收到远程命令->重新加载配置文件" << std::endl;
|
||||
return nlohmann::json{
|
||||
{"result", "OK"}
|
||||
};
|
||||
});
|
||||
// 检查服务器启动结果
|
||||
_rpcServer.start(2367);
|
||||
//if (_rpcServer.start(2367)) {
|
||||
// qInfo() << "JSON-RPC server started successfully on port" << _rpcServer.serverPort();
|
||||
//}
|
||||
//else {
|
||||
// qCritical() << "Failed to start JSON-RPC server on port 2367";
|
||||
// // 尝试其他端口
|
||||
// for (int port = 2368; port <= 2370; ++port) {
|
||||
// if (_rpcServer.start(port)) {
|
||||
// qInfo() << "JSON-RPC server started on alternative port" << port;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
private:
|
||||
nlohmann::ordered_json missionJson;
|
||||
SQLiteDB* db;
|
||||
qtFuncBridge* bridge;
|
||||
MissionLoader _missionLoader; // 任务加载器,用于加载任务配置
|
||||
|
||||
};
|
||||
|
||||
#pragma once
|
||||
@@ -66,7 +66,6 @@ public:
|
||||
~triggerManager(){
|
||||
runFlag = false;
|
||||
this->stop();
|
||||
this->terminate();
|
||||
delete trigger;
|
||||
}
|
||||
triggerManager(detProcess*, qtFuncBridge* bridgeIn, nlohmann::ordered_json& triggerJson);
|
||||
|
||||
@@ -20,10 +20,16 @@ public:
|
||||
|
||||
}
|
||||
void cleanup() override {
|
||||
|
||||
if (_detProcess != nullptr){
|
||||
delete _detProcess;
|
||||
_detProcess = nullptr;
|
||||
}
|
||||
}
|
||||
~Backend() {
|
||||
delete _detProcess;
|
||||
if (_detProcess != nullptr) {
|
||||
delete _detProcess;
|
||||
_detProcess = nullptr;
|
||||
}
|
||||
}
|
||||
bool excuteCliCommand(const QString& command) {
|
||||
return _detProcess->_cliManager->cliCommand(command);
|
||||
@@ -33,7 +39,7 @@ public:
|
||||
}
|
||||
private:
|
||||
bool initOnce = false;
|
||||
detProcess* _detProcess;
|
||||
detProcess* _detProcess = nullptr;
|
||||
qtFuncBridge* bridge = nullptr;
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@ public:
|
||||
db(dbIn),bridge(bridgeIn), missionJson(jsonIn){
|
||||
|
||||
}
|
||||
|
||||
~detProcess() {
|
||||
safeShutdownDelete(_serviceManager._triggerManager);
|
||||
safeShutdownDelete(_serviceManager._cameraManager);
|
||||
safeDelete(_serviceManager._handleAiModelManager);
|
||||
}
|
||||
// 对普通指针的安全删除
|
||||
template<typename T>
|
||||
void safeDelete(T*& ptr) {
|
||||
|
||||
@@ -24,9 +24,16 @@
|
||||
#include <QMessageBox>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextCharFormat>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QTimer>
|
||||
|
||||
//行数上限
|
||||
const int MAX_LOGWIDGET_LINES = 1000;
|
||||
//内存中存储的消息数量上限
|
||||
const int MAX_STORED_MESSAGES = 5000;
|
||||
//批量处理延迟时间(毫秒)
|
||||
const int BATCH_UPDATE_DELAY = 100;
|
||||
|
||||
class logWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
@@ -45,9 +52,15 @@ private:
|
||||
QPlainTextEdit* textWidget;
|
||||
QStringList allMessages; // 存储所有原始消息
|
||||
bool autoScroll = true;
|
||||
mutable QMutex mutex; // 保护此状态的互斥锁
|
||||
QStringList pendingMessages; // 待处理的消息队列
|
||||
};
|
||||
|
||||
std::unordered_map<int, FilterState> filterStates;
|
||||
|
||||
// 线程安全和性能优化相关
|
||||
mutable QMutex globalMutex; // 全局保护锁
|
||||
QTimer* batchUpdateTimer; // 批量更新定时器
|
||||
|
||||
public:
|
||||
QVBoxLayout* Layout;
|
||||
@@ -63,6 +76,12 @@ public:
|
||||
palette.setColor(QPalette::WindowText, Qt::white);
|
||||
QFont font("微软雅黑", 24, QFont::Bold);
|
||||
|
||||
// 初始化批量处理定时器
|
||||
batchUpdateTimer = new QTimer(this);
|
||||
batchUpdateTimer->setSingleShot(false);
|
||||
batchUpdateTimer->setInterval(BATCH_UPDATE_DELAY);
|
||||
connect(batchUpdateTimer, &QTimer::timeout, this, &logWidget::processPendingMessages);
|
||||
|
||||
widgets = new QStackedWidget();
|
||||
widgets->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
|
||||
@@ -125,6 +144,9 @@ public:
|
||||
btnPtr->toDefault();
|
||||
btn->pressed();
|
||||
widgets->setCurrentIndex(index);
|
||||
|
||||
// 切换页面时应用过滤器以显示最新内容
|
||||
applyFilters(index);
|
||||
});
|
||||
|
||||
QWidget* widget = new QWidget();
|
||||
@@ -235,11 +257,67 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
// 应用过滤器
|
||||
void applyFilters(int index) {
|
||||
// 线程安全地添加消息
|
||||
void addMessageSafe(int index, const QString& message) {
|
||||
QMutexLocker globalLocker(&globalMutex);
|
||||
|
||||
if (filterStates.find(index) == filterStates.end()) return;
|
||||
|
||||
FilterState& state = filterStates[index];
|
||||
QMutexLocker stateLocker(&state.mutex);
|
||||
|
||||
// 限制消息数量,防止内存泄漏
|
||||
if (state.allMessages.size() >= MAX_STORED_MESSAGES) {
|
||||
// 删除最老的消息(前面的1/4)
|
||||
int removeCount = MAX_STORED_MESSAGES / 4;
|
||||
for (int i = 0; i < removeCount && !state.allMessages.isEmpty(); ++i) {
|
||||
state.allMessages.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
state.allMessages.append(message);
|
||||
state.pendingMessages.append(message);
|
||||
|
||||
// 启动批量更新定时器
|
||||
if (!batchUpdateTimer->isActive()) {
|
||||
batchUpdateTimer->start();
|
||||
}
|
||||
}
|
||||
|
||||
// 批量处理待处理的消息
|
||||
|
||||
// 判断消息是否应该显示
|
||||
bool shouldShowMessage(const QString& message, const FilterState& state) {
|
||||
QString levelFilter = state.levelFilter->currentText();
|
||||
QString regexPattern = state.regexFilter->text();
|
||||
|
||||
// 级别过滤
|
||||
if (levelFilter != "全部") {
|
||||
if (!message.contains(levelFilter, Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 正则过滤
|
||||
if (!regexPattern.isEmpty()) {
|
||||
QRegularExpression regex(regexPattern);
|
||||
if (regex.isValid() && !regex.match(message).hasMatch()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 应用过滤器
|
||||
void applyFilters(int index) {
|
||||
QMutexLocker globalLocker(&globalMutex);
|
||||
|
||||
if (filterStates.find(index) == filterStates.end()) return;
|
||||
|
||||
FilterState& state = filterStates[index];
|
||||
QMutexLocker stateLocker(&state.mutex);
|
||||
|
||||
QString levelFilter = state.levelFilter->currentText();
|
||||
QString regexPattern = state.regexFilter->text();
|
||||
|
||||
@@ -247,6 +325,7 @@ private:
|
||||
state.textWidget->clear();
|
||||
|
||||
QRegularExpression regex;
|
||||
bool regexValid = true;
|
||||
if (!regexPattern.isEmpty()) {
|
||||
regex.setPattern(regexPattern);
|
||||
if (!regex.isValid()) {
|
||||
@@ -257,28 +336,15 @@ private:
|
||||
|
||||
// 过滤并显示消息
|
||||
for (const QString& message : state.allMessages) {
|
||||
bool shouldShow = true;
|
||||
|
||||
// 级别过滤
|
||||
if (levelFilter != "全部") {
|
||||
if (!message.contains(levelFilter, Qt::CaseInsensitive)) {
|
||||
shouldShow = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 正则过滤
|
||||
if (shouldShow && !regexPattern.isEmpty()) {
|
||||
if (!regex.match(message).hasMatch()) {
|
||||
shouldShow = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldShow) {
|
||||
if (shouldShowMessage(message, state)) {
|
||||
// 根据日志级别设置颜色
|
||||
appendColoredMessage(state.textWidget, message);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空待处理消息,因为已经全部重新过滤了
|
||||
state.pendingMessages.clear();
|
||||
|
||||
// 自动滚动到底部
|
||||
if (state.autoScroll) {
|
||||
scrollToBottom(state.textWidget);
|
||||
@@ -321,8 +387,12 @@ private:
|
||||
|
||||
// 保存日志到文件
|
||||
void saveLogToFile(int index) {
|
||||
QMutexLocker globalLocker(&globalMutex);
|
||||
|
||||
if (filterStates.find(index) == filterStates.end()) return;
|
||||
|
||||
FilterState& state = filterStates[index];
|
||||
|
||||
QString fileName = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
"保存日志文件",
|
||||
@@ -335,7 +405,15 @@ private:
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QTextStream out(&file);
|
||||
out.setCodec("UTF-8");
|
||||
out << filterStates[index].textWidget->toPlainText();
|
||||
|
||||
// 复制当前显示的内容以避免在写入时内容发生变化
|
||||
QString content;
|
||||
{
|
||||
QMutexLocker stateLocker(&state.mutex);
|
||||
content = state.textWidget->toPlainText();
|
||||
}
|
||||
|
||||
out << content;
|
||||
file.close();
|
||||
QMessageBox::information(this, "成功", "日志已保存到: " + fileName);
|
||||
} else {
|
||||
@@ -346,55 +424,72 @@ private:
|
||||
|
||||
// 清空日志
|
||||
void clearLog(int index) {
|
||||
QMutexLocker globalLocker(&globalMutex);
|
||||
|
||||
if (filterStates.find(index) == filterStates.end()) return;
|
||||
|
||||
auto reply = QMessageBox::question(this, "确认", "确定要清空当前日志吗?",
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
if (reply == QMessageBox::Yes) {
|
||||
filterStates[index].allMessages.clear();
|
||||
filterStates[index].textWidget->clear();
|
||||
FilterState& state = filterStates[index];
|
||||
QMutexLocker stateLocker(&state.mutex);
|
||||
|
||||
state.allMessages.clear();
|
||||
state.pendingMessages.clear();
|
||||
state.textWidget->clear();
|
||||
}
|
||||
}
|
||||
|
||||
public slots:
|
||||
void recvStdLogMsg(QString logMsg) {
|
||||
QString text = getCurrentDate() + " " + getCurrentTime() + " " + logMsg;
|
||||
|
||||
// 添加到标准控制台 (索引0)
|
||||
if (filterStates.find(0) != filterStates.end()) {
|
||||
filterStates[0].allMessages.append(text);
|
||||
|
||||
// 如果当前显示的是标准控制台,应用过滤器显示
|
||||
if (widgets->currentIndex() == 0) {
|
||||
applyFilters(0);
|
||||
}
|
||||
} else {
|
||||
// 兜底:直接添加到文本框
|
||||
stdConsoleOutput.appendPlainText(text);
|
||||
if (filterStates.size() > 0 && filterStates[0].autoScroll) {
|
||||
scrollToBottom(&stdConsoleOutput);
|
||||
}
|
||||
}
|
||||
addMessageSafe(0, text); // 使用线程安全的方法
|
||||
}
|
||||
|
||||
void recvQtLogMsg(QString logMsg) {
|
||||
QString text = getCurrentDate() + " " + getCurrentTime() + " " + logMsg;
|
||||
addMessageSafe(1, text); // 使用线程安全的方法
|
||||
}
|
||||
|
||||
private slots:
|
||||
void processPendingMessages() {
|
||||
QMutexLocker globalLocker(&globalMutex);
|
||||
|
||||
// 添加到Qt日志 (索引1)
|
||||
if (filterStates.find(1) != filterStates.end()) {
|
||||
filterStates[1].allMessages.append(text);
|
||||
bool hasUpdates = false;
|
||||
int currentIndex = widgets->currentIndex();
|
||||
|
||||
for (auto& pair : filterStates) {
|
||||
int index = pair.first;
|
||||
FilterState& state = pair.second;
|
||||
|
||||
// 如果当前显示的是Qt日志,应用过滤器显示
|
||||
if (widgets->currentIndex() == 1) {
|
||||
applyFilters(1);
|
||||
}
|
||||
} else {
|
||||
// 兜底:直接添加到文本框
|
||||
qtConsoleOutput.appendPlainText(text);
|
||||
if (filterStates.size() > 1 && filterStates[1].autoScroll) {
|
||||
scrollToBottom(&qtConsoleOutput);
|
||||
QMutexLocker stateLocker(&state.mutex);
|
||||
|
||||
if (!state.pendingMessages.isEmpty()) {
|
||||
hasUpdates = true;
|
||||
|
||||
// 只更新当前显示的页面,提升性能
|
||||
if (index == currentIndex) {
|
||||
// 增量添加消息而不是重新过滤所有消息
|
||||
for (const QString& message : state.pendingMessages) {
|
||||
if (shouldShowMessage(message, state)) {
|
||||
appendColoredMessage(state.textWidget, message);
|
||||
}
|
||||
}
|
||||
|
||||
// 自动滚动到底部
|
||||
if (state.autoScroll) {
|
||||
scrollToBottom(state.textWidget);
|
||||
}
|
||||
}
|
||||
|
||||
state.pendingMessages.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有待处理消息,停止定时器
|
||||
if (!hasUpdates) {
|
||||
batchUpdateTimer->stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
#pragma once
|
||||
@@ -64,9 +64,10 @@ public:
|
||||
initConfig();
|
||||
}
|
||||
~appCore(){
|
||||
_backEnd.releaseCore();
|
||||
auto& config = appConfig::getInstance();
|
||||
if(config.dataBase) delete config.dataBase;
|
||||
//backend.releaseCore();
|
||||
|
||||
}
|
||||
void initConfig() {
|
||||
auto& config = appConfig::getInstance();
|
||||
|
||||
Reference in New Issue
Block a user