服务端热更新功能

当调用upgrade命令后,会创建更加新的roomthread
(!完全没测试过)
This commit is contained in:
notify 2024-04-07 00:35:57 +08:00 committed by GitHub
parent 873d68363c
commit 1a4da186d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 333 additions and 172 deletions

View File

@ -143,14 +143,12 @@ callbacks["UpdateRoomList"] = (jsonData) => {
const current = mainStack.currentItem; // should be lobby
if (mainStack.depth === 2) {
current.roomModel.clear();
JSON.parse(jsonData).forEach(function (room) {
JSON.parse(jsonData).forEach(room => {
const [roomId, roomName, gameMode, playerNum, capacity, hasPassword,
outdated] = room;
current.roomModel.append({
roomId: room[0],
roomName: room[1],
gameMode: room[2],
playerNum: room[3],
capacity: room[4],
hasPassword: room[5] ? true : false,
roomId, roomName, gameMode, playerNum, capacity,
hasPassword, outdated,
});
});
}

View File

@ -64,7 +64,13 @@ Item {
Text {
horizontalAlignment: Text.AlignLeft
Layout.fillWidth: true
text: roomName
text: {
let ret = roomName;
if (outdated) {
ret = '<font color="grey"><del>' + ret + '</del></font>';
}
return ret;
}
font.pixelSize: 20
elide: Label.ElideRight
}
@ -94,7 +100,7 @@ Item {
text: (playerNum < capacity) ? luatr("Enter") :
luatr("Observe")
enabled: !opTimer.running
enabled: !opTimer.running && !outdated
onClicked: {
opTimer.start();

View File

@ -318,6 +318,10 @@
<source>room password error</source>
<translation></translation>
</message>
<message>
<source>room is outdated</source>
<translation></translation>
</message>
<message>
<source>no such room</source>
<translation></translation>

View File

@ -495,6 +495,7 @@ Fk:loadTranslationTable{
["#ChainStateChange"] = "%from %arg 了武将牌",
["#ChainDamage"] = "%from 处于连环状态,将受到传导的伤害",
["#ChangeKingdom"] = "%from 的国籍从 %arg 变成了 %arg2",
["#RoomOutdated"] = "服务器更新完毕!该房间已过期,将无法再次游玩",
}
-- card footnote

View File

@ -181,7 +181,7 @@ request_handlers["newroom"] = function(s, id)
s:registerRoom(id)
end
request_handlers["reloadpackage"] = function(room, id, reqlist)
request_handlers["reloadpackage"] = function(_, _, reqlist)
if not IsConsoleStart() then return end
local path = reqlist[3]
Fk:reloadPackage(path)

View File

@ -22,6 +22,8 @@ end)
-- 仿照Room接口编写的request协程处理器
local requestRoom = setmetatable({
id = -1,
runningRooms = runningRooms,
-- minDelayTime 是当没有任何就绪房间时,可以睡眠的时间。
-- 因为这个时间是所有房间预期就绪用时的最小值故称为minDelayTime。
@ -130,6 +132,14 @@ local function mainLoop()
-- 调用RoomThread的trySleep函数开始真正的睡眠。会被wakeUp(c++)唤醒。
requestRoom.thread:trySleep(time)
local runningRoomsCount = -1 -- 必有requestRoom从-1开始算
for _ in pairs(runningRooms) do
runningRoomsCount = runningRoomsCount + 1
if runningRoomsCount > 0 then break end
end
if runningRoomsCount == 0 and requestRoom.thread:isOutdated() then
break
end
-- verbose('[!] Waked up after %f ms...', (os.getms() - cur) / 1000)

View File

@ -153,107 +153,6 @@ void Router::abortRequest() {
}
void Router::handlePacket(const QByteArray &rawPacket) {
#ifndef FK_CLIENT_ONLY
static QMap<QString, void (*)(ServerPlayer *, const QString &)> lobby_actions;
if (lobby_actions.size() <= 0) {
lobby_actions["UpdateAvatar"] = [](ServerPlayer *sender,
const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto avatar = arr[0].toString();
if (CheckSqlString(avatar)) {
auto sql = QString("UPDATE userinfo SET avatar='%1' WHERE id=%2;")
.arg(avatar)
.arg(sender->getId());
ExecSQL(ServerInstance->getDatabase(), sql);
sender->setAvatar(avatar);
sender->doNotify("UpdateAvatar", avatar);
}
};
lobby_actions["UpdatePassword"] = [](ServerPlayer *sender,
const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto oldpw = arr[0].toString();
auto newpw = arr[1].toString();
auto sql_find =
QString("SELECT password, salt FROM userinfo WHERE id=%1;")
.arg(sender->getId());
auto passed = false;
auto arr2 = SelectFromDatabase(ServerInstance->getDatabase(), sql_find);
auto result = arr2[0].toObject();
passed = (result["password"].toString() ==
QCryptographicHash::hash(
oldpw.append(result["salt"].toString()).toLatin1(),
QCryptographicHash::Sha256)
.toHex());
if (passed) {
auto sql_update =
QString("UPDATE userinfo SET password='%1' WHERE id=%2;")
.arg(QCryptographicHash::hash(
newpw.append(result["salt"].toString()).toLatin1(),
QCryptographicHash::Sha256)
.toHex())
.arg(sender->getId());
ExecSQL(ServerInstance->getDatabase(), sql_update);
}
sender->doNotify("UpdatePassword", passed ? "1" : "0");
};
lobby_actions["CreateRoom"] = [](ServerPlayer *sender,
const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto name = arr[0].toString();
auto capacity = arr[1].toInt();
auto timeout = arr[2].toInt();
auto settings =
QJsonDocument(arr[3].toObject()).toJson(QJsonDocument::Compact);
ServerInstance->createRoom(sender, name, capacity, timeout, settings);
};
lobby_actions["EnterRoom"] = [](ServerPlayer *sender,
const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto roomId = arr[0].toInt();
auto room = ServerInstance->findRoom(roomId);
if (room) {
auto settings = QJsonDocument::fromJson(room->getSettings());
auto password = settings["password"].toString();
if (password.isEmpty() || arr[1].toString() == password) {
room->addPlayer(sender);
} else {
sender->doNotify("ErrorMsg", "room password error");
}
} else {
sender->doNotify("ErrorMsg", "no such room");
}
};
lobby_actions["ObserveRoom"] = [](ServerPlayer *sender,
const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto roomId = arr[0].toInt();
auto room = ServerInstance->findRoom(roomId);
if (room) {
auto settings = QJsonDocument::fromJson(room->getSettings());
auto password = settings["password"].toString();
if (password.isEmpty() || arr[1].toString() == password) {
room->addObserver(sender);
} else {
sender->doNotify("ErrorMsg", "room password error");
}
} else {
sender->doNotify("ErrorMsg", "no such room");
}
};
lobby_actions["Chat"] = [](ServerPlayer *sender, const QString &jsonData) {
sender->getRoom()->chat(sender, jsonData);
};
lobby_actions["RefreshRoomList"] = [](ServerPlayer *sender,
const QString &jsonData) {
ServerInstance->updateRoomList(sender);
};
}
#endif
QJsonDocument packet = QJsonDocument::fromJson(rawPacket);
if (packet.isNull() || !packet.isArray())
return;
@ -278,35 +177,7 @@ void Router::handlePacket(const QByteArray &rawPacket) {
}
Room *room = player->getRoom();
if (room->isLobby() && lobby_actions.contains(command))
lobby_actions[command](player, jsonData);
else {
if (command == "QuitRoom") {
room->removePlayer(player);
} else if (command == "AddRobot") {
if (ServerInstance->getConfig("enableBots").toBool()) room->addRobot(player);
} else if (command == "KickPlayer") {
int i = jsonData.toInt();
auto p = room->findPlayer(i);
if (p && !room->isStarted()) {
room->removePlayer(p);
room->addRejectId(i);
QTimer::singleShot(30000, this, [=]() {
room->removeRejectId(i);
});
}
} else if (command == "Ready") {
player->setReady(!player->isReady());
room->doBroadcastNotify(room->getPlayers(), "ReadyChanged",
QString("[%1,%2]").arg(player->getId()).arg(player->isReady()));
} else if (command == "StartGame") {
room->manuallyStart();
} else if (command == "Chat") {
room->chat(player, jsonData);
} else if (command == "PushRequest") {
room->pushRequest(QString("%1,").arg(player->getId()) + jsonData);
}
}
room->handlePacket(player, command, jsonData);
}
#endif
} else if (type & TYPE_REQUEST) {

View File

@ -22,7 +22,7 @@ Room::Room(RoomThread *m_thread) {
id = server->nextRoomId;
server->nextRoomId++;
this->server = server;
this->m_thread = m_thread;
setThread(m_thread);
if (m_thread) { // In case of lobby
m_thread->addRoom(this);
}
@ -60,7 +60,12 @@ Server *Room::getServer() const { return server; }
RoomThread *Room::getThread() const { return m_thread; }
void Room::setThread(RoomThread *t) { m_thread = t; }
void Room::setThread(RoomThread *t) {
m_thread = t;
if (t != nullptr) {
md5 = t->getMd5();
}
}
int Room::getId() const { return id; }
@ -366,6 +371,12 @@ int Room::getTimeout() const { return timeout; }
void Room::setTimeout(int timeout) { this->timeout = timeout; }
bool Room::isOutdated() {
bool ret = md5 != server->getMd5();
if (ret) md5 = "";
return ret;
}
bool Room::isStarted() const { return gameStarted; }
void Room::doBroadcastNotify(const QList<ServerPlayer *> targets,
@ -616,3 +627,186 @@ void Room::addRejectId(int id) {
void Room::removeRejectId(int id) {
rejected_players.removeOne(id);
}
// ------------------------------------------------
static void updateAvatar(ServerPlayer *sender, const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto avatar = arr[0].toString();
if (CheckSqlString(avatar)) {
auto sql = QString("UPDATE userinfo SET avatar='%1' WHERE id=%2;")
.arg(avatar)
.arg(sender->getId());
ExecSQL(ServerInstance->getDatabase(), sql);
sender->setAvatar(avatar);
sender->doNotify("UpdateAvatar", avatar);
}
}
static void updatePassword(ServerPlayer *sender, const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto oldpw = arr[0].toString();
auto newpw = arr[1].toString();
auto sql_find =
QString("SELECT password, salt FROM userinfo WHERE id=%1;")
.arg(sender->getId());
auto passed = false;
auto arr2 = SelectFromDatabase(ServerInstance->getDatabase(), sql_find);
auto result = arr2[0].toObject();
passed = (result["password"].toString() ==
QCryptographicHash::hash(
oldpw.append(result["salt"].toString()).toLatin1(),
QCryptographicHash::Sha256)
.toHex());
if (passed) {
auto sql_update =
QString("UPDATE userinfo SET password='%1' WHERE id=%2;")
.arg(QCryptographicHash::hash(
newpw.append(result["salt"].toString()).toLatin1(),
QCryptographicHash::Sha256)
.toHex())
.arg(sender->getId());
ExecSQL(ServerInstance->getDatabase(), sql_update);
}
sender->doNotify("UpdatePassword", passed ? "1" : "0");
}
static void createRoom(ServerPlayer *sender, const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto name = arr[0].toString();
auto capacity = arr[1].toInt();
auto timeout = arr[2].toInt();
auto settings =
QJsonDocument(arr[3].toObject()).toJson(QJsonDocument::Compact);
ServerInstance->createRoom(sender, name, capacity, timeout, settings);
}
static void enterRoom(ServerPlayer *sender, const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto roomId = arr[0].toInt();
auto room = ServerInstance->findRoom(roomId);
if (room) {
auto settings = QJsonDocument::fromJson(room->getSettings());
auto password = settings["password"].toString();
if (password.isEmpty() || arr[1].toString() == password) {
if (room->isOutdated()) {
sender->doNotify("ErrorMsg", "room is outdated");
} else {
room->addPlayer(sender);
}
} else {
sender->doNotify("ErrorMsg", "room password error");
}
} else {
sender->doNotify("ErrorMsg", "no such room");
}
}
static void observeRoom(ServerPlayer *sender, const QString &jsonData) {
auto arr = String2Json(jsonData).array();
auto roomId = arr[0].toInt();
auto room = ServerInstance->findRoom(roomId);
if (room) {
auto settings = QJsonDocument::fromJson(room->getSettings());
auto password = settings["password"].toString();
if (password.isEmpty() || arr[1].toString() == password) {
if (room->isOutdated()) {
sender->doNotify("ErrorMsg", "room is outdated");
} else {
room->addObserver(sender);
}
} else {
sender->doNotify("ErrorMsg", "room password error");
}
} else {
sender->doNotify("ErrorMsg", "no such room");
}
}
static void refreshRoomList(ServerPlayer *sender, const QString &) {
ServerInstance->updateRoomList(sender);
};
static void quitRoom(ServerPlayer *player, const QString &) {
auto room = player->getRoom();
room->removePlayer(player);
if (room->isOutdated()) {
player->kicked();
}
}
static void addRobot(ServerPlayer *player, const QString &) {
auto room = player->getRoom();
if (ServerInstance->getConfig("enableBots").toBool())
room->addRobot(player);
}
static void kickPlayer(ServerPlayer *player, const QString &jsonData) {
auto room = player->getRoom();
int i = jsonData.toInt();
auto p = room->findPlayer(i);
if (p && !room->isStarted()) {
room->removePlayer(p);
room->addRejectId(i);
QTimer::singleShot(30000, room, [=]() {
room->removeRejectId(i);
});
}
}
static void ready(ServerPlayer *player, const QString &) {
auto room = player->getRoom();
player->setReady(!player->isReady());
room->doBroadcastNotify(room->getPlayers(), "ReadyChanged",
QString("[%1,%2]").arg(player->getId()).arg(player->isReady()));
}
static void startGame(ServerPlayer *player, const QString &) {
auto room = player->getRoom();
if (room->isOutdated()) {
foreach (auto p, room->getPlayers()) {
p->doNotify("ErrorMsg", "room is outdated");
p->kicked();
}
} else {
room->manuallyStart();
}
}
typedef void (*room_cb)(ServerPlayer *, const QString &);
static const QMap<QString, room_cb> lobby_actions = {
{"UpdateAvatar", updateAvatar},
{"UpdatePassword", updatePassword},
{"CreateRoom", createRoom},
{"EnterRoom", enterRoom},
{"ObserveRoom", observeRoom},
{"RefreshRoomList", refreshRoomList},
};
static const QMap<QString, room_cb> room_actions = {
{"QuitRoom", quitRoom},
{"AddRobot", addRobot},
{"KickPlayer", kickPlayer},
{"Ready", ready},
{"StartGame", startGame},
};
void Room::handlePacket(ServerPlayer *sender, const QString &command,
const QString &jsonData) {
if (command == "Chat") {
chat(sender, jsonData);
return;
} else if (command == "PushRequest") {
if (!isLobby())
pushRequest(QString("%1,").arg(sender->getId()) + jsonData);
}
auto func_table = lobby_actions;
if (!isLobby()) func_table = room_actions;
auto func = func_table[command];
if (func) {
func(sender, jsonData);
}
}

View File

@ -50,6 +50,8 @@ class Room : public QObject {
int getTimeout() const;
void setTimeout(int timeout);
bool isOutdated();
bool isStarted() const;
// ====================================}
@ -65,6 +67,10 @@ class Room : public QObject {
void addRejectId(int id);
void removeRejectId(int id);
// router用
void handlePacket(ServerPlayer *sender, const QString &command,
const QString &jsonData);
signals:
void abandoned();
@ -90,6 +96,7 @@ class Room : public QObject {
bool m_ready;
int timeout;
QString md5;
void addRunRate(int id, const QString &mode);
void updatePlayerGameData(int id, const QString &mode);

View File

@ -3,6 +3,7 @@
#include "roomthread.h"
#include "server.h"
#include "util.h"
#include <lua.h>
#ifndef FK_SERVER_ONLY
#include "client.h"
@ -13,6 +14,7 @@ RoomThread::RoomThread(Server *m_server) {
this->m_server = m_server;
m_capacity = 100; // TODO: server cfg
terminated = false;
md5 = m_server->getMd5();
L = CreateLuaState();
DoLuaScript(L, "lua/freekill.lua");
@ -26,6 +28,7 @@ RoomThread::~RoomThread() {
wait();
}
lua_close(L);
m_server->removeThread(this);
// foreach (auto room, room_list) {
// room->deleteLater();
// }
@ -40,6 +43,8 @@ bool RoomThread::isFull() const {
return m_capacity <= 0;
}
QString RoomThread::getMd5() const { return md5; }
Room *RoomThread::getRoom(int id) const {
return m_server->findRoom(id);
}
@ -52,6 +57,10 @@ void RoomThread::addRoom(Room *room) {
void RoomThread::removeRoom(Room *room) {
room->setThread(nullptr);
m_capacity++;
if (m_capacity == 100 // TODO: server cfg
&& isOutdated()) {
deleteLater();
}
}
QString RoomThread::fetchRequest() {
@ -118,3 +127,13 @@ bool RoomThread::isConsoleStart() const {
return false;
#endif
}
bool RoomThread::isOutdated() {
bool ret = md5 != m_server->getMd5();
if (ret) {
// 让以后每次都outdate
// 不然反复disable/enable的情况下会出乱子
md5 = "";
}
return ret;
}

View File

@ -16,6 +16,7 @@ class RoomThread : public QThread {
Server *getServer() const;
bool isFull() const;
QString getMd5() const;
Room *getRoom(int id) const;
void addRoom(Room *room);
void removeRoom(Room *room);
@ -32,6 +33,9 @@ class RoomThread : public QThread {
bool isTerminated() const;
bool isConsoleStart() const;
bool isOutdated();
protected:
virtual void run();
@ -39,6 +43,7 @@ class RoomThread : public QThread {
Server *m_server;
// QList<Room *> room_list;
int m_capacity;
QString md5;
lua_State *L;
QMutex request_queue_mutex;

View File

@ -112,14 +112,14 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity,
RoomThread *thread = nullptr;
foreach (auto t, threads) {
if (!t->isFull()) {
if (!t->isFull() && !t->isOutdated()) {
thread = t;
break;
}
}
if (!thread && nextRoomId != 0) {
thread = new RoomThread(this);
threads.append(thread);
thread = createThread();
}
if (!idle_rooms.isEmpty()) {
@ -128,6 +128,7 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity,
nextRoomId++;
room->setAbandoned(false);
room->setThread(thread);
thread->addRoom(room);
rooms.insert(room->getId(), room);
} else {
room = new Room(thread);
@ -151,6 +152,16 @@ Room *Server::findRoom(int id) const { return rooms.value(id); }
Room *Server::lobby() const { return m_lobby; }
RoomThread *Server::createThread() {
RoomThread *thread = new RoomThread(this);
threads.append(thread);
return thread;
}
void Server::removeThread(RoomThread *thread) {
threads.removeOne(thread);
}
ServerPlayer *Server::findPlayer(int id) const { return players.value(id); }
void Server::addPlayer(ServerPlayer *player) {
@ -183,6 +194,7 @@ void Server::updateRoomList(ServerPlayer *teller) {
obj << count;
obj << cap;
obj << !password.isEmpty();
obj << room->isOutdated();
if (count == cap)
arr << obj;
@ -670,6 +682,30 @@ void Server::endTransaction() {
transaction_mutex.unlock();
}
const QString &Server::getMd5() const {
return md5;
}
void Server::refreshMd5() {
md5 = calcFileMD5();
foreach (auto room, rooms) {
if (room->isOutdated()) {
if (!room->isStarted()) {
foreach (auto p, room->getPlayers()) {
p->doNotify("ErrorMsg", "room is outdated");
p->kicked();
}
} else {
room->doBroadcastNotify(room->getPlayers(), "GameLog",
"{\"type\":\"#RoomOutdated\",\"toast\":true}");
}
}
}
foreach (auto p, lobby()->getPlayers()) {
emit p->kicked();
}
}
void Server::readPendingDatagrams() {
while (udpSocket->hasPendingDatagrams()) {
QNetworkDatagram datagram = udpSocket->receiveDatagram();

View File

@ -31,6 +31,9 @@ public:
Room *findRoom(int id) const;
Room *lobby() const;
RoomThread *createThread();
void removeThread(RoomThread *thread);
ServerPlayer *findPlayer(int id) const;
void addPlayer(ServerPlayer *player);
void removePlayer(int id);
@ -50,6 +53,9 @@ public:
void beginTransaction();
void endTransaction();
const QString &getMd5() const;
void refreshMd5();
signals:
void roomCreated(Room *room);
void playerAdded(ServerPlayer *player);

View File

@ -117,11 +117,13 @@ void Shell::upgradeCommand(QStringList &list) {
auto obj = a.toObject();
Pacman->upgradePack(obj["name"].toString());
}
ServerInstance->refreshMd5();
return;
}
auto pack = list[0];
Pacman->upgradePack(pack);
ServerInstance->refreshMd5();
}
void Shell::enableCommand(QStringList &list) {
@ -132,6 +134,7 @@ void Shell::enableCommand(QStringList &list) {
auto pack = list[0];
Pacman->enablePack(pack);
ServerInstance->refreshMd5();
}
void Shell::disableCommand(QStringList &list) {
@ -142,6 +145,7 @@ void Shell::disableCommand(QStringList &list) {
auto pack = list[0];
Pacman->disablePack(pack);
ServerInstance->refreshMd5();
}
void Shell::lspkgCommand(QStringList &) {
@ -380,33 +384,32 @@ Shell::Shell() {
setObjectName("Shell");
signal(SIGINT, sigintHandler);
static QHash<QString, void (Shell::*)(QStringList &)> handlers;
if (handlers.size() == 0) {
handlers["help"] = &Shell::helpCommand;
handlers["?"] = &Shell::helpCommand;
handlers["lsplayer"] = &Shell::lspCommand;
handlers["lsroom"] = &Shell::lsrCommand;
handlers["install"] = &Shell::installCommand;
handlers["remove"] = &Shell::removeCommand;
handlers["upgrade"] = &Shell::upgradeCommand;
handlers["u"] = &Shell::upgradeCommand;
handlers["lspkg"] = &Shell::lspkgCommand;
handlers["enable"] = &Shell::enableCommand;
handlers["disable"] = &Shell::disableCommand;
handlers["kick"] = &Shell::kickCommand;
handlers["msg"] = &Shell::msgCommand;
handlers["m"] = &Shell::msgCommand;
handlers["ban"] = &Shell::banCommand;
handlers["unban"] = &Shell::unbanCommand;
handlers["banip"] = &Shell::banipCommand;
handlers["unbanip"] = &Shell::unbanipCommand;
handlers["banuuid"] = &Shell::banUuidCommand;
handlers["unbanuuid"] = &Shell::unbanUuidCommand;
handlers["reloadconf"] = &Shell::reloadConfCommand;
handlers["r"] = &Shell::reloadConfCommand;
handlers["resetpassword"] = &Shell::resetPasswordCommand;
handlers["rp"] = &Shell::resetPasswordCommand;
}
static const QHash<QString, void (Shell::*)(QStringList &)> handlers = {
{"help", &Shell::helpCommand},
{"?", &Shell::helpCommand},
{"lsplayer", &Shell::lspCommand},
{"lsroom", &Shell::lsrCommand},
{"install", &Shell::installCommand},
{"remove", &Shell::removeCommand},
{"upgrade", &Shell::upgradeCommand},
{"u", &Shell::upgradeCommand},
{"lspkg", &Shell::lspkgCommand},
{"enable", &Shell::enableCommand},
{"disable", &Shell::disableCommand},
{"kick", &Shell::kickCommand},
{"msg", &Shell::msgCommand},
{"m", &Shell::msgCommand},
{"ban", &Shell::banCommand},
{"unban", &Shell::unbanCommand},
{"banip", &Shell::banipCommand},
{"unbanip", &Shell::unbanipCommand},
{"banuuid", &Shell::banUuidCommand},
{"unbanuuid", &Shell::unbanUuidCommand},
{"reloadconf", &Shell::reloadConfCommand},
{"r", &Shell::reloadConfCommand},
{"resetpassword", &Shell::resetPasswordCommand},
{"rp", &Shell::resetPasswordCommand},
};
handler_map = handlers;
}

View File

@ -41,6 +41,7 @@ public:
bool isTerminated() const;
bool isConsoleStart() const;
bool isOutdated();
};
%{