mirror of
https://github.com/Qsgs-Fans/FreeKill.git
synced 2024-11-16 03:32:34 +08:00
Shell enhance (#359)
- 修复了Log淹没正在输入命令的bug - 增加Tab补全:命令补全、install链接推荐、用户名补全、拓展包名补全 - 为Windows端增加基于cstdio的getline函数的丐版shell
This commit is contained in:
parent
bdfcf805e4
commit
79ede70b6b
|
@ -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 ()
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
31
src/main.cpp
31
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();
|
||||
}
|
||||
|
|
|
@ -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 <QApplication>
|
||||
|
|
|
@ -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 <readline/history.h>
|
||||
#include <readline/readline.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#else
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#endif
|
||||
#include <QJsonDocument>
|
||||
|
||||
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 <url>.", "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<QString, void (Shell::*)(QStringList &)> 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,37 +433,31 @@ Shell::Shell() {
|
|||
{"r", &Shell::reloadConfCommand},
|
||||
{"resetpassword", &Shell::resetPasswordCommand},
|
||||
{"rp", &Shell::resetPasswordCommand},
|
||||
// special command
|
||||
{"quit", &Shell::helpCommand},
|
||||
{"crash", &Shell::helpCommand},
|
||||
};
|
||||
handler_map = handlers;
|
||||
}
|
||||
|
||||
void Shell::run() {
|
||||
printf("\rFreeKill, Copyright (C) 2022-2023, 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);
|
||||
|
||||
while (true) {
|
||||
char *bytes = readline("fk> ");
|
||||
if (!bytes || !strcmp(bytes, "quit")) {
|
||||
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 (!strcmp(bytes, "crash")) {
|
||||
if (!strncmp(bytes, "crash", 5)) {
|
||||
qFatal("Crashing."); // should dump core
|
||||
return;
|
||||
}
|
||||
|
||||
if (*bytes)
|
||||
#ifdef FK_USE_READLINE
|
||||
add_history(bytes);
|
||||
#endif
|
||||
|
||||
auto command = QString(bytes);
|
||||
auto command_list = command.split(' ');
|
||||
|
@ -455,6 +473,237 @@ void Shell::run() {
|
|||
|
||||
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-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] Welcome to CLI. Enter \"help\" for usage hints.\n", FK_VERSION);
|
||||
|
||||
while (true) {
|
||||
#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 {
|
||||
bytes[strlen(bytes) - 1] = '\0'; // remove \n
|
||||
}
|
||||
#endif
|
||||
handleLine(bytes);
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,13 @@ class Shell: public QThread {
|
|||
public:
|
||||
Shell();
|
||||
|
||||
void handleLine(char *);
|
||||
|
||||
protected:
|
||||
virtual void run();
|
||||
|
||||
private:
|
||||
bool done = false;
|
||||
QHash<QString, void (Shell::*)(QStringList &)> 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
|
||||
|
|
Loading…
Reference in New Issue
Block a user