From 79ede70b6b183ac3112dfb3c668070924718e881 Mon Sep 17 00:00:00 2001 From: notify Date: Mon, 1 Jul 2024 00:28:03 +0800 Subject: [PATCH] Shell enhance (#359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复了Log淹没正在输入命令的bug - 增加Tab补全:命令补全、install链接推荐、用户名补全、拓展包名补全 - 为Windows端增加基于cstdio的getline函数的丐版shell --- src/CMakeLists.txt | 2 +- src/core/util.cpp | 3 + src/main.cpp | 31 ++-- src/pch.h | 4 + src/server/shell.cpp | 333 +++++++++++++++++++++++++++++++++++++------ src/server/shell.h | 19 ++- 6 files changed, 335 insertions(+), 57 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2d6473b8..566bcec7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,6 +16,7 @@ set(freekill_SRCS "server/room.cpp" "server/roomthread.cpp" "server/scheduler.cpp" + "server/shell.cpp" "ui/qmlbackend.cpp" "swig/freekill-wrap.cxx" ) @@ -67,7 +68,6 @@ else () set(SQLITE3_LIB sqlite3) set(CRYPTO_LIB OpenSSL::Crypto) set(READLINE_LIB readline) - list(APPEND freekill_SRCS "server/shell.cpp") set(GIT_LIB git2) endif () diff --git a/src/core/util.cpp b/src/core/util.cpp index 4decfa29..1d1b1d5c 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -120,9 +120,12 @@ static int callback(void *jsonDoc, int argc, char **argv, char **cols) { } QJsonArray SelectFromDatabase(sqlite3 *db, const QString &sql) { + static QMutex select_lock; QJsonArray arr; auto bytes = sql.toUtf8(); + select_lock.lock(); sqlite3_exec(db, bytes.data(), callback, (void *)&arr, nullptr); + select_lock.unlock(); return arr; } diff --git a/src/main.cpp b/src/main.cpp index aa8be159..b4d58585 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,10 +6,7 @@ using namespace fkShell; #include "core/packman.h" #include "server/server.h" - -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) #include "server/shell.h" -#endif #if defined(Q_OS_WIN32) #include "applink.c" @@ -113,8 +110,13 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, break; } - fprintf(stderr, "%02d/%02d ", date.month(), date.day()); - fprintf(stderr, "%s ", +#ifdef FK_USE_READLINE + ShellInstance->clearLine(); +#else + printf("\r"); +#endif + printf("%02d/%02d ", date.month(), date.day()); + printf("%s ", QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); fprintf(file, "%02d/%02d ", date.month(), date.day()); fprintf(file, "%s ", @@ -125,26 +127,26 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, switch (type) { case QtDebugMsg: - fprintf(stderr, "%s[D] %s\n", threadName.constData(), + printf("%s[D] %s\n", threadName.constData(), localMsg.constData()); fprintf(file, "%s[D] %s\n", threadName.constData(), localMsg.constData()); break; case QtInfoMsg: - fprintf(stderr, "%s[%s] %s\n", threadName.constData(), + printf("%s[%s] %s\n", threadName.constData(), Color("I", Green).toUtf8().constData(), localMsg.constData()); fprintf(file, "%s[%s] %s\n", threadName.constData(), "I", localMsg.constData()); break; case QtWarningMsg: - fprintf(stderr, "%s[%s] %s\n", threadName.constData(), + printf("%s[%s] %s\n", threadName.constData(), Color("W", Yellow, Bold).toUtf8().constData(), localMsg.constData()); fprintf(file, "%s[%s] %s\n", threadName.constData(), "W", localMsg.constData()); break; 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()); fprintf(file, "%s[%s] %s\n", threadName.constData(), "C", localMsg.constData()); @@ -156,12 +158,18 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, #endif break; 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()); fprintf(file, "%s[%s] %s\n", threadName.constData(), "E", localMsg.constData()); break; } + +#ifdef FK_USE_READLINE + if (ShellInstance && !ShellInstance->lineDone()) { + ShellInstance->redisplay(); + } +#endif } // FreeKill 的程序主入口。整个程序就是从这里开始执行的。 @@ -230,11 +238,8 @@ int main(int argc, char *argv[]) { app->exit(1); } else { qInfo("Server is listening on port %d", serverPort); -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - // Linux 服务器的话可以启用一个 Shell 来操作服务器。 auto shell = new Shell; shell->start(); -#endif } return app->exec(); } diff --git a/src/pch.h b/src/pch.h index 1616d069..d78dc81a 100644 --- a/src/pch.h +++ b/src/pch.h @@ -21,6 +21,10 @@ typedef int LuaFunction; #define DESKTOP_BUILD #endif +#if defined (Q_OS_LINUX) && !defined (Q_OS_ANDROID) +#define FK_USE_READLINE +#endif + // You may define FK_SERVER_ONLY with cmake .. -D... #ifndef FK_SERVER_ONLY #include diff --git a/src/server/shell.cpp b/src/server/shell.cpp index f1168adf..468af198 100644 --- a/src/server/shell.cpp +++ b/src/server/shell.cpp @@ -1,23 +1,24 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) #include "server/shell.h" #include "core/packman.h" #include "server/server.h" #include "server/serverplayer.h" #include "core/util.h" +#ifdef FK_USE_READLINE #include #include #include +#include +#include +#else +#include +#include +#endif #include -static void sigintHandler(int) { - fprintf(stderr, "\n"); - rl_reset_line_state(); - rl_replace_line("", 0); - rl_crlf(); - rl_redisplay(); -} +Shell *ShellInstance = nullptr; +static const char *prompt = "Fk> "; void Shell::helpCommand(QStringList &) { qInfo("Frequently used commands:"); @@ -55,7 +56,7 @@ void Shell::helpCommand(QStringList &) { qInfo("===== Package commands ====="); HELP_MSG("%s: Install a new package from .", "install"); 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: Disable a package.", "disable"); 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() { + ShellInstance = this; 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); + rl_attempted_completion_function = fk_completion; + rl_completion_entry_function = null_completion; +#endif static const QHash handlers = { {"help", &Shell::helpCommand}, @@ -393,7 +417,7 @@ Shell::Shell() { {"remove", &Shell::removeCommand}, {"upgrade", &Shell::upgradeCommand}, {"u", &Shell::upgradeCommand}, - {"lspkg", &Shell::lspkgCommand}, + {"pkgs", &Shell::lspkgCommand}, {"enable", &Shell::enableCommand}, {"disable", &Shell::disableCommand}, {"kick", &Shell::kickCommand}, @@ -409,52 +433,277 @@ Shell::Shell() { {"r", &Shell::reloadConfCommand}, {"resetpassword", &Shell::resetPasswordCommand}, {"rp", &Shell::resetPasswordCommand}, + // special command + {"quit", &Shell::helpCommand}, + {"crash", &Shell::helpCommand}, }; 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() { - 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 is free software, and you are welcome to redistribute it under\n"); printf("certain conditions; For more information visit " "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) { - char *bytes = readline("fk> "); - if (!bytes || !strcmp(bytes, "quit")) { - qInfo("Server is shutting down."); - qApp->quit(); - return; - } - - qInfo("Running command: \"%s\"", bytes); - - if (!strcmp(bytes, "crash")) { - qFatal("Crashing."); // should dump core - 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()); +#ifdef FK_USE_READLINE + char *bytes = readline(prompt); +#else + char *bytes = NULL; + size_t bufsize = 512; + printf("\rfk> "); + fflush(stdin); + int ret = getline(&bytes, &bufsize, stdin); + if (ret == -1 || ret == 0) { + free(bytes); + bytes = NULL; } else { - command_list.removeFirst(); - (this->*func)(command_list); + bytes[strlen(bytes) - 1] = '\0'; // remove \n } - - free(bytes); +#endif + handleLine(bytes); + if (done) break; } } - -#endif diff --git a/src/server/shell.h b/src/server/shell.h index 6ebea4b3..dbc533ea 100644 --- a/src/server/shell.h +++ b/src/server/shell.h @@ -8,10 +8,13 @@ class Shell: public QThread { public: Shell(); + void handleLine(char *); + protected: virtual void run(); private: + bool done = false; QHash handler_map; void helpCommand(QStringList &); void quitCommand(QStringList &); @@ -33,6 +36,20 @@ private: void unbanUuidCommand(QStringList &); void reloadConfCommand(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