diff --git a/Minecraft.Server/Console/ServerCliInput.cpp b/Minecraft.Server/Console/ServerCliInput.cpp index 6c6c548c8..d873980a0 100644 --- a/Minecraft.Server/Console/ServerCliInput.cpp +++ b/Minecraft.Server/Console/ServerCliInput.cpp @@ -6,6 +6,61 @@ #include "..\ServerLogger.h" #include "..\vendor\linenoise\linenoise.h" +#include +#include + +namespace +{ + bool UseStreamInputMode() + { + const char *mode = getenv("SERVER_CLI_INPUT_MODE"); + if (mode != NULL) + { + return _stricmp(mode, "stream") == 0 + || _stricmp(mode, "stdin") == 0; + } + + return false; + } + + int WaitForStdinReadable(HANDLE stdinHandle, DWORD waitMs) + { + if (stdinHandle == NULL || stdinHandle == INVALID_HANDLE_VALUE) + { + return -1; + } + + DWORD fileType = GetFileType(stdinHandle); + if (fileType == FILE_TYPE_PIPE) + { + DWORD available = 0; + if (!PeekNamedPipe(stdinHandle, NULL, 0, NULL, &available, NULL)) + { + return -1; + } + return available > 0 ? 1 : 0; + } + + if (fileType == FILE_TYPE_CHAR) + { + // console/pty char handles are often not waitable across Wine+Docker. + return 1; + } + + DWORD waitResult = WaitForSingleObject(stdinHandle, waitMs); + if (waitResult == WAIT_OBJECT_0) + { + return 1; + } + if (waitResult == WAIT_TIMEOUT) + { + return 0; + } + + return -1; + } +} + namespace ServerRuntime { // C-style completion callback bridge requires a static instance pointer. @@ -49,6 +104,7 @@ namespace ServerRuntime linenoiseRequestStop(); if (m_inputThread.joinable()) { + CancelSynchronousIo((HANDLE)m_inputThread.native_handle()); m_inputThread.join(); } linenoiseSetCompletionCallback(NULL); @@ -68,6 +124,21 @@ namespace ServerRuntime } void ServerCliInput::RunInputLoop() + { + if (UseStreamInputMode()) + { + LogInfo("console", "CLI input mode: stream(file stdin)"); + RunStreamInputLoop(); + return; + } + + RunLinenoiseLoop(); + } + + /** + * use linenoise for interactive console input, with line editing and history support + */ + void ServerCliInput::RunLinenoiseLoop() { while (m_running) { @@ -83,17 +154,111 @@ namespace ServerRuntime continue; } - if (line[0] != 0 && m_engine != NULL) - { - // Keep local history and forward command for main-thread execution. - linenoiseHistoryAdd(line); - m_engine->EnqueueCommandLine(line); - } + EnqueueLine(line); linenoiseFree(line); } } + /** + * use file-based stdin reading instead of linenoise when requested or when stdin is not a console/pty (e.g. piped input or non-interactive docker) + */ + void ServerCliInput::RunStreamInputLoop() + { + HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE); + if (stdinHandle == NULL || stdinHandle == INVALID_HANDLE_VALUE) + { + LogWarn("console", "stream input mode requested but STDIN handle is unavailable; falling back to linenoise."); + RunLinenoiseLoop(); + return; + } + + std::string line; + bool skipNextLf = false; + + printf("server> "); + fflush(stdout); + + while (m_running) + { + int readable = WaitForStdinReadable(stdinHandle, 50); + if (readable <= 0) + { + Sleep(10); + continue; + } + + char ch = 0; + DWORD bytesRead = 0; + if (!ReadFile(stdinHandle, &ch, 1, &bytesRead, NULL) || bytesRead == 0) + { + Sleep(10); + continue; + } + + if (skipNextLf && ch == '\n') + { + skipNextLf = false; + continue; + } + + if (ch == '\r' || ch == '\n') + { + if (ch == '\r') + { + skipNextLf = true; + } + else + { + skipNextLf = false; + } + + if (!line.empty()) + { + EnqueueLine(line.c_str()); + line.clear(); + } + + printf("server> "); + fflush(stdout); + continue; + } + + skipNextLf = false; + + if ((unsigned char)ch == 3) + { + continue; + } + + if ((unsigned char)ch == 8 || (unsigned char)ch == 127) + { + if (!line.empty()) + { + line.resize(line.size() - 1); + } + continue; + } + + if (isprint((unsigned char)ch) && line.size() < 4096) + { + line.push_back(ch); + } + } + } + + void ServerCliInput::EnqueueLine(const char *line) + { + if (line == NULL || line[0] == 0 || m_engine == NULL) + { + return; + } + + // Keep local history and forward command for main-thread execution. + linenoiseHistoryAdd(line); + m_engine->EnqueueCommandLine(line); + } + void ServerCliInput::CompletionThunk(const char *line, linenoiseCompletions *completions) { // Static thunk forwards callback into instance state. diff --git a/Minecraft.Server/Console/ServerCliInput.h b/Minecraft.Server/Console/ServerCliInput.h index 81575179c..83221a2c0 100644 --- a/Minecraft.Server/Console/ServerCliInput.h +++ b/Minecraft.Server/Console/ServerCliInput.h @@ -47,6 +47,9 @@ namespace ServerRuntime private: void RunInputLoop(); + void RunLinenoiseLoop(); + void RunStreamInputLoop(); + void EnqueueLine(const char *line); static void CompletionThunk(const char *line, linenoiseCompletions *completions); void BuildCompletions(const char *line, linenoiseCompletions *completions); diff --git a/docker-compose.dedicated-server.ghcr.yml b/docker-compose.dedicated-server.ghcr.yml index 5a2c28c2b..243e5f492 100644 --- a/docker-compose.dedicated-server.ghcr.yml +++ b/docker-compose.dedicated-server.ghcr.yml @@ -10,6 +10,8 @@ services: WINEARCH: win64 WINEPREFIX: /var/opt/wineprefix64 WINEDEBUG: -all + # linux require use file stdin + SERVER_CLI_INPUT_MODE: ${SERVER_CLI_INPUT_MODE:-stream} # minimum required virtual screen XVFB_DISPLAY: ${XVFB_DISPLAY:-:99} XVFB_SCREEN: ${XVFB_SCREEN:-720x1280x16} diff --git a/docker-compose.dedicated-server.yml b/docker-compose.dedicated-server.yml index 19b6d4313..4a0d33137 100644 --- a/docker-compose.dedicated-server.yml +++ b/docker-compose.dedicated-server.yml @@ -14,6 +14,8 @@ services: WINEARCH: win64 WINEPREFIX: /var/opt/wineprefix64 WINEDEBUG: -all + # linux require use file stdin + SERVER_CLI_INPUT_MODE: ${SERVER_CLI_INPUT_MODE:-stream} # minimum required virtual screen XVFB_DISPLAY: ${XVFB_DISPLAY:-:99} XVFB_SCREEN: ${XVFB_SCREEN:-64x64x16}