Shell enhance (#359)
Some checks failed
Check Whitespace and New Line / check (push) Has been cancelled
Deploy Sphinx documentation to Pages / pages (push) Has been cancelled

- 修复了Log淹没正在输入命令的bug
- 增加Tab补全:命令补全、install链接推荐、用户名补全、拓展包名补全
- 为Windows端增加基于cstdio的getline函数的丐版shell
This commit is contained in:
notify 2024-07-01 00:28:03 +08:00 committed by GitHub
parent bdfcf805e4
commit 79ede70b6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 335 additions and 57 deletions

View File

@ -16,6 +16,7 @@ set(freekill_SRCS
"server/room.cpp" "server/room.cpp"
"server/roomthread.cpp" "server/roomthread.cpp"
"server/scheduler.cpp" "server/scheduler.cpp"
"server/shell.cpp"
"ui/qmlbackend.cpp" "ui/qmlbackend.cpp"
"swig/freekill-wrap.cxx" "swig/freekill-wrap.cxx"
) )
@ -67,7 +68,6 @@ else ()
set(SQLITE3_LIB sqlite3) set(SQLITE3_LIB sqlite3)
set(CRYPTO_LIB OpenSSL::Crypto) set(CRYPTO_LIB OpenSSL::Crypto)
set(READLINE_LIB readline) set(READLINE_LIB readline)
list(APPEND freekill_SRCS "server/shell.cpp")
set(GIT_LIB git2) set(GIT_LIB git2)
endif () endif ()

View File

@ -120,9 +120,12 @@ static int callback(void *jsonDoc, int argc, char **argv, char **cols) {
} }
QJsonArray SelectFromDatabase(sqlite3 *db, const QString &sql) { QJsonArray SelectFromDatabase(sqlite3 *db, const QString &sql) {
static QMutex select_lock;
QJsonArray arr; QJsonArray arr;
auto bytes = sql.toUtf8(); auto bytes = sql.toUtf8();
select_lock.lock();
sqlite3_exec(db, bytes.data(), callback, (void *)&arr, nullptr); sqlite3_exec(db, bytes.data(), callback, (void *)&arr, nullptr);
select_lock.unlock();
return arr; return arr;
} }

View File

@ -6,10 +6,7 @@ using namespace fkShell;
#include "core/packman.h" #include "core/packman.h"
#include "server/server.h" #include "server/server.h"
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
#include "server/shell.h" #include "server/shell.h"
#endif
#if defined(Q_OS_WIN32) #if defined(Q_OS_WIN32)
#include "applink.c" #include "applink.c"
@ -113,8 +110,13 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context,
break; break;
} }
fprintf(stderr, "%02d/%02d ", date.month(), date.day()); #ifdef FK_USE_READLINE
fprintf(stderr, "%s ", ShellInstance->clearLine();
#else
printf("\r");
#endif
printf("%02d/%02d ", date.month(), date.day());
printf("%s ",
QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); QTime::currentTime().toString("hh:mm:ss").toLatin1().constData());
fprintf(file, "%02d/%02d ", date.month(), date.day()); fprintf(file, "%02d/%02d ", date.month(), date.day());
fprintf(file, "%s ", fprintf(file, "%s ",
@ -125,26 +127,26 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context,
switch (type) { switch (type) {
case QtDebugMsg: case QtDebugMsg:
fprintf(stderr, "%s[D] %s\n", threadName.constData(), printf("%s[D] %s\n", threadName.constData(),
localMsg.constData()); localMsg.constData());
fprintf(file, "%s[D] %s\n", threadName.constData(), fprintf(file, "%s[D] %s\n", threadName.constData(),
localMsg.constData()); localMsg.constData());
break; break;
case QtInfoMsg: case QtInfoMsg:
fprintf(stderr, "%s[%s] %s\n", threadName.constData(), printf("%s[%s] %s\n", threadName.constData(),
Color("I", Green).toUtf8().constData(), localMsg.constData()); Color("I", Green).toUtf8().constData(), localMsg.constData());
fprintf(file, "%s[%s] %s\n", threadName.constData(), fprintf(file, "%s[%s] %s\n", threadName.constData(),
"I", localMsg.constData()); "I", localMsg.constData());
break; break;
case QtWarningMsg: case QtWarningMsg:
fprintf(stderr, "%s[%s] %s\n", threadName.constData(), printf("%s[%s] %s\n", threadName.constData(),
Color("W", Yellow, Bold).toUtf8().constData(), Color("W", Yellow, Bold).toUtf8().constData(),
localMsg.constData()); localMsg.constData());
fprintf(file, "%s[%s] %s\n", threadName.constData(), fprintf(file, "%s[%s] %s\n", threadName.constData(),
"W", localMsg.constData()); "W", localMsg.constData());
break; break;
case QtCriticalMsg: case QtCriticalMsg:
fprintf(stderr, "%s[%s] %s\n", threadName.constData(), printf("%s[%s] %s\n", threadName.constData(),
Color("C", Red, Bold).toUtf8().constData(), localMsg.constData()); Color("C", Red, Bold).toUtf8().constData(), localMsg.constData());
fprintf(file, "%s[%s] %s\n", threadName.constData(), fprintf(file, "%s[%s] %s\n", threadName.constData(),
"C", localMsg.constData()); "C", localMsg.constData());
@ -156,12 +158,18 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context,
#endif #endif
break; break;
case QtFatalMsg: case QtFatalMsg:
fprintf(stderr, "%s[%s] %s\n", threadName.constData(), printf("%s[%s] %s\n", threadName.constData(),
Color("E", Red, Bold).toUtf8().constData(), localMsg.constData()); Color("E", Red, Bold).toUtf8().constData(), localMsg.constData());
fprintf(file, "%s[%s] %s\n", threadName.constData(), fprintf(file, "%s[%s] %s\n", threadName.constData(),
"E", localMsg.constData()); "E", localMsg.constData());
break; break;
} }
#ifdef FK_USE_READLINE
if (ShellInstance && !ShellInstance->lineDone()) {
ShellInstance->redisplay();
}
#endif
} }
// FreeKill 的程序主入口。整个程序就是从这里开始执行的。 // FreeKill 的程序主入口。整个程序就是从这里开始执行的。
@ -230,11 +238,8 @@ int main(int argc, char *argv[]) {
app->exit(1); app->exit(1);
} else { } else {
qInfo("Server is listening on port %d", serverPort); qInfo("Server is listening on port %d", serverPort);
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
// Linux 服务器的话可以启用一个 Shell 来操作服务器。
auto shell = new Shell; auto shell = new Shell;
shell->start(); shell->start();
#endif
} }
return app->exec(); return app->exec();
} }

View File

@ -21,6 +21,10 @@ typedef int LuaFunction;
#define DESKTOP_BUILD #define DESKTOP_BUILD
#endif #endif
#if defined (Q_OS_LINUX) && !defined (Q_OS_ANDROID)
#define FK_USE_READLINE
#endif
// You may define FK_SERVER_ONLY with cmake .. -D... // You may define FK_SERVER_ONLY with cmake .. -D...
#ifndef FK_SERVER_ONLY #ifndef FK_SERVER_ONLY
#include <QApplication> #include <QApplication>

View File

@ -1,23 +1,24 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
#include "server/shell.h" #include "server/shell.h"
#include "core/packman.h" #include "core/packman.h"
#include "server/server.h" #include "server/server.h"
#include "server/serverplayer.h" #include "server/serverplayer.h"
#include "core/util.h" #include "core/util.h"
#ifdef FK_USE_READLINE
#include <readline/history.h> #include <readline/history.h>
#include <readline/readline.h> #include <readline/readline.h>
#include <signal.h> #include <signal.h>
#include <unistd.h>
#include <sys/ioctl.h>
#else
#include <cstdio>
#include <cstring>
#endif
#include <QJsonDocument> #include <QJsonDocument>
static void sigintHandler(int) { Shell *ShellInstance = nullptr;
fprintf(stderr, "\n"); static const char *prompt = "Fk> ";
rl_reset_line_state();
rl_replace_line("", 0);
rl_crlf();
rl_redisplay();
}
void Shell::helpCommand(QStringList &) { void Shell::helpCommand(QStringList &) {
qInfo("Frequently used commands:"); qInfo("Frequently used commands:");
@ -55,7 +56,7 @@ void Shell::helpCommand(QStringList &) {
qInfo("===== Package commands ====="); qInfo("===== Package commands =====");
HELP_MSG("%s: Install a new package from <url>.", "install"); HELP_MSG("%s: Install a new package from <url>.", "install");
HELP_MSG("%s: Remove a package.", "remove"); HELP_MSG("%s: Remove a package.", "remove");
HELP_MSG("%s: List all packages.", "lspkg"); HELP_MSG("%s: List all packages.", "pkgs");
HELP_MSG("%s: Enable a package.", "enable"); HELP_MSG("%s: Enable a package.", "enable");
HELP_MSG("%s: Disable a package.", "disable"); HELP_MSG("%s: Disable a package.", "disable");
HELP_MSG("%s: Upgrade a package. Leave empty to upgrade all.", "upgrade/u"); HELP_MSG("%s: Upgrade a package. Leave empty to upgrade all.", "upgrade/u");
@ -380,9 +381,32 @@ void Shell::resetPasswordCommand(QStringList &list) {
} }
} }
#ifdef FK_USE_READLINE
static void sigintHandler(int) {
rl_reset_line_state();
rl_replace_line("", 0);
rl_crlf();
rl_forced_update_display();
}
static char **fk_completion(const char *text, int start, int end);
static char *null_completion(const char *, int) { return NULL; }
#endif
Shell::Shell() { Shell::Shell() {
ShellInstance = this;
setObjectName("Shell"); setObjectName("Shell");
#ifdef FK_USE_READLINE
// Setup readline here
// 别管Ctrl+C了
//rl_catch_signals = 1;
//rl_catch_sigwinch = 1;
//rl_persistent_signal_handlers = 1;
//rl_set_signals();
signal(SIGINT, sigintHandler); signal(SIGINT, sigintHandler);
rl_attempted_completion_function = fk_completion;
rl_completion_entry_function = null_completion;
#endif
static const QHash<QString, void (Shell::*)(QStringList &)> handlers = { static const QHash<QString, void (Shell::*)(QStringList &)> handlers = {
{"help", &Shell::helpCommand}, {"help", &Shell::helpCommand},
@ -393,7 +417,7 @@ Shell::Shell() {
{"remove", &Shell::removeCommand}, {"remove", &Shell::removeCommand},
{"upgrade", &Shell::upgradeCommand}, {"upgrade", &Shell::upgradeCommand},
{"u", &Shell::upgradeCommand}, {"u", &Shell::upgradeCommand},
{"lspkg", &Shell::lspkgCommand}, {"pkgs", &Shell::lspkgCommand},
{"enable", &Shell::enableCommand}, {"enable", &Shell::enableCommand},
{"disable", &Shell::disableCommand}, {"disable", &Shell::disableCommand},
{"kick", &Shell::kickCommand}, {"kick", &Shell::kickCommand},
@ -409,52 +433,277 @@ Shell::Shell() {
{"r", &Shell::reloadConfCommand}, {"r", &Shell::reloadConfCommand},
{"resetpassword", &Shell::resetPasswordCommand}, {"resetpassword", &Shell::resetPasswordCommand},
{"rp", &Shell::resetPasswordCommand}, {"rp", &Shell::resetPasswordCommand},
// special command
{"quit", &Shell::helpCommand},
{"crash", &Shell::helpCommand},
}; };
handler_map = handlers; handler_map = handlers;
} }
void Shell::handleLine(char *bytes) {
if (!bytes || !strncmp(bytes, "quit", 4)) {
qInfo("Server is shutting down.");
qApp->quit();
done = true;
return;
}
qInfo("Running command: \"%s\"", bytes);
if (!strncmp(bytes, "crash", 5)) {
qFatal("Crashing."); // should dump core
return;
}
#ifdef FK_USE_READLINE
add_history(bytes);
#endif
auto command = QString(bytes);
auto command_list = command.split(' ');
auto func = handler_map[command_list.first()];
if (!func) {
auto bytes = command_list.first().toUtf8();
qWarning("Unknown command \"%s\". Type \"help\" for hints.",
bytes.constData());
} else {
command_list.removeFirst();
(this->*func)(command_list);
}
free(bytes);
}
#ifdef FK_USE_READLINE
void Shell::redisplay() {
QString tmp = syntaxHighlight(rl_line_buffer);
rl_clear_visible_line();
rl_forced_update_display();
//moveCursorToStart();
//printf("\r%s%s", prompt, tmp.toUtf8().constData());
}
void Shell::moveCursorToStart() {
winsize sz;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &sz);
int lines = (rl_end + strlen(prompt) - 1) / sz.ws_col;
printf("\e[%d;%dH", sz.ws_row - lines, 0);
}
void Shell::clearLine() {
rl_clear_visible_line();
}
bool Shell::lineDone() const {
return (bool)rl_done;
}
// 最简单的语法高亮,若命令可执行就涂绿,否则涂红
QString Shell::syntaxHighlight(char *bytes) {
QString ret(bytes);
auto command = ret.split(' ').first();
auto func = handler_map[command];
auto colored_command = command;
if (!func) {
colored_command = Color(command, fkShell::Red, fkShell::Bold);
} else {
colored_command = Color(command, fkShell::Green);
}
ret.replace(0, command.length(), colored_command);
return ret;
}
static void linehandler(char *bytes) {
ShellInstance->handleLine(bytes);
}
char *Shell::generateCommand(const char *text, int state) {
static int list_index, len;
static auto keys = handler_map.keys();
const char *name;
if (state == 0) {
list_index = 0;
len = strlen(text);
}
while (list_index < keys.length()) {
name = keys[list_index].toUtf8().constData();
++list_index;
if (strncmp(name, text, len) == 0) {
return strdup(name);
}
}
return NULL;
}
static char *command_generator(const char *text, int state) {
return ShellInstance->generateCommand(text, state);
}
static char *repo_generator(const char *text, int state) {
static QStringList recommend_repos = {
"https://gitee.com/Qsgs-Fans/standard_ex",
"https://gitee.com/Qsgs-Fans/shzl",
"https://gitee.com/Qsgs-Fans/sp",
"https://gitee.com/Qsgs-Fans/yj",
"https://gitee.com/Qsgs-Fans/ol",
"https://gitee.com/Qsgs-Fans/mougong",
"https://gitee.com/Qsgs-Fans/mobile",
"https://gitee.com/Qsgs-Fans/tenyear",
"https://gitee.com/Qsgs-Fans/overseas",
"https://gitee.com/Qsgs-Fans/jsrg",
"https://gitee.com/Qsgs-Fans/qsgs",
"https://gitee.com/Qsgs-Fans/mini",
"https://gitee.com/Qsgs-Fans/gamemode",
"https://gitee.com/Qsgs-Fans/utility",
"https://gitee.com/Qsgs-Fans/freekill-core",
"https://gitee.com/Qsgs-Fans/offline",
"https://gitee.com/Qsgs-Fans/hegemony",
"https://gitee.com/Qsgs-Fans/lunar",
};
static int list_index, len;
const char *name;
if (state == 0) {
list_index = 0;
len = strlen(text);
}
while (list_index < recommend_repos.count()) {
name = recommend_repos[list_index].toUtf8().constData();
++list_index;
if (strncmp(name, text, len) == 0) {
return strdup(name);
}
}
return NULL;
}
static char *package_generator(const char *text, int state) {
static QJsonArray arr;
static int list_index, len;
const char *name;
if (state == 0) {
arr = QJsonDocument::fromJson(Pacman->listPackages().toUtf8()).array();
list_index = 0;
len = strlen(text);
}
while (list_index < arr.count()) {
name = arr[list_index].toObject().value("name").toString().toUtf8().constData();
++list_index;
if (strncmp(name, text, len) == 0) {
return strdup(name);
}
}
return NULL;
}
static char *user_generator(const char *text, int state) {
// TODO: userinfo表需要一个cache机制
static QJsonArray arr;
static int list_index, len;
const char *name;
if (state == 0) {
arr = SelectFromDatabase(ServerInstance->getDatabase(),
"SELECT name FROM userinfo;");
list_index = 0;
len = strlen(text);
}
while (list_index < arr.count()) {
name = arr[list_index].toObject().value("name").toString().toUtf8().constData();
++list_index;
if (strncmp(name, text, len) == 0) {
return strdup(name);
}
}
return NULL;
};
static char *banned_user_generator(const char *text, int state) {
// TODO: userinfo表需要一个cache机制
static QJsonArray arr;
static int list_index, len;
const char *name;
if (state == 0) {
arr = SelectFromDatabase(ServerInstance->getDatabase(),
"SELECT name FROM userinfo WHERE banned = 1;");
list_index = 0;
len = strlen(text);
}
while (list_index < arr.count()) {
name = arr[list_index].toObject().value("name").toString().toUtf8().constData();
++list_index;
if (strncmp(name, text, len) == 0) {
return strdup(name);
}
}
return NULL;
};
static char **fk_completion(const char* text, int start, int end) {
char **matches = NULL;
if (start == 0) {
matches = rl_completion_matches(text, command_generator);
} else {
auto command_list = QString(rl_line_buffer).split(' ');
if (command_list.length() > 2) return NULL;
auto command = command_list[0];
if (command == "install") {
matches = rl_completion_matches(text, repo_generator);
} else if (command == "remove" || command == "upgrade"
|| command == "enable" || command == "disable") {
matches = rl_completion_matches(text, package_generator);
} else if (command.startsWith("ban") || command == "resetpassword" || command == "rp") {
matches = rl_completion_matches(text, user_generator);
} else if (command.startsWith("unban")) {
matches = rl_completion_matches(text, banned_user_generator);
}
}
return matches;
}
#endif
void Shell::run() { void Shell::run() {
printf("\rFreeKill, Copyright (C) 2022-2023, GNU GPL'd, by Notify et al.\n"); printf("\rFreeKill, Copyright (C) 2022-2024, GNU GPL'd, by Notify et al.\n");
printf("This program comes with ABSOLUTELY NO WARRANTY.\n"); printf("This program comes with ABSOLUTELY NO WARRANTY.\n");
printf( printf(
"This is free software, and you are welcome to redistribute it under\n"); "This is free software, and you are welcome to redistribute it under\n");
printf("certain conditions; For more information visit " printf("certain conditions; For more information visit "
"http://www.gnu.org/licenses.\n\n"); "http://www.gnu.org/licenses.\n\n");
printf("[v%s] This is server cli. Enter \"help\" for usage hints.\n", FK_VERSION); printf("[v%s] Welcome to CLI. Enter \"help\" for usage hints.\n", FK_VERSION);
while (true) { while (true) {
char *bytes = readline("fk> "); #ifdef FK_USE_READLINE
if (!bytes || !strcmp(bytes, "quit")) { char *bytes = readline(prompt);
qInfo("Server is shutting down."); #else
qApp->quit(); char *bytes = NULL;
return; size_t bufsize = 512;
} printf("\rfk> ");
fflush(stdin);
qInfo("Running command: \"%s\"", bytes); int ret = getline(&bytes, &bufsize, stdin);
if (ret == -1 || ret == 0) {
if (!strcmp(bytes, "crash")) { free(bytes);
qFatal("Crashing."); // should dump core bytes = NULL;
return;
}
if (*bytes)
add_history(bytes);
auto command = QString(bytes);
auto command_list = command.split(' ');
auto func = handler_map[command_list.first()];
if (!func) {
auto bytes = command_list.first().toUtf8();
qWarning("Unknown command \"%s\". Type \"help\" for hints.",
bytes.constData());
} else { } else {
command_list.removeFirst(); bytes[strlen(bytes) - 1] = '\0'; // remove \n
(this->*func)(command_list);
} }
#endif
free(bytes); handleLine(bytes);
if (done) break;
} }
} }
#endif

View File

@ -8,10 +8,13 @@ class Shell: public QThread {
public: public:
Shell(); Shell();
void handleLine(char *);
protected: protected:
virtual void run(); virtual void run();
private: private:
bool done = false;
QHash<QString, void (Shell::*)(QStringList &)> handler_map; QHash<QString, void (Shell::*)(QStringList &)> handler_map;
void helpCommand(QStringList &); void helpCommand(QStringList &);
void quitCommand(QStringList &); void quitCommand(QStringList &);
@ -33,6 +36,20 @@ private:
void unbanUuidCommand(QStringList &); void unbanUuidCommand(QStringList &);
void reloadConfCommand(QStringList &); void reloadConfCommand(QStringList &);
void resetPasswordCommand(QStringList &); void resetPasswordCommand(QStringList &);
};
#ifdef FK_USE_READLINE
private:
QString syntaxHighlight(char *);
public:
void redisplay();
void moveCursorToStart();
void clearLine();
bool lineDone() const;
char *generateCommand(const char *, int);
#endif
};
extern Shell *ShellInstance;
#endif #endif