MinecraftConsoles/Minecraft.Server/Console/ServerCliInput.cpp
Riley M. c0da06e4ee
major: Switch to forward slashes(+more) to fix compilation on Linux (#1403)
Notably also adds some metadata files for NixOS 

* add support for linux clang cross compiles

* add linux clang instructions

* un-capitalize Mob.horse.*

* update the description in flake.nix

---------

Co-authored-by: Loki <lokirautio@gmail.com>
2026-04-14 16:47:37 -05:00

286 lines
5.4 KiB
C++

#include "stdafx.h"
#include "ServerCliInput.h"
#include "ServerCliEngine.h"
#include "../ServerLogger.h"
#include "../vendor/linenoise/linenoise.h"
#include <ctype.h>
#include <stdlib.h>
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.
ServerCliInput *ServerCliInput::s_instance = NULL;
ServerCliInput::ServerCliInput()
: m_running(false)
, m_engine(NULL)
{
}
ServerCliInput::~ServerCliInput()
{
Stop();
}
void ServerCliInput::Start(ServerCliEngine *engine)
{
if (engine == NULL || m_running.exchange(true))
{
return;
}
m_engine = engine;
s_instance = this;
linenoiseResetStop();
linenoiseHistorySetMaxLen(128);
linenoiseSetCompletionCallback(&ServerCliInput::CompletionThunk);
m_inputThread = std::thread(&ServerCliInput::RunInputLoop, this);
LogInfo("console", "CLI input thread started.");
}
void ServerCliInput::Stop()
{
if (!m_running.exchange(false))
{
return;
}
// Ask linenoise to break out first, then join thread safely.
linenoiseRequestStop();
if (m_inputThread.joinable())
{
CancelSynchronousIo((HANDLE)m_inputThread.native_handle());
m_inputThread.join();
}
linenoiseSetCompletionCallback(NULL);
if (s_instance == this)
{
s_instance = NULL;
}
m_engine = NULL;
LogInfo("console", "CLI input thread stopped.");
}
bool ServerCliInput::IsRunning() const
{
return m_running.load();
}
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)
{
char *line = linenoise("server> ");
if (line == NULL)
{
// NULL is expected on stop request (or Ctrl+C inside linenoise).
if (!m_running)
{
break;
}
Sleep(10);
continue;
}
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.
if (s_instance != NULL)
{
s_instance->BuildCompletions(line, completions);
}
}
void ServerCliInput::BuildCompletions(const char *line, linenoiseCompletions *completions)
{
if (line == NULL || completions == NULL || m_engine == NULL)
{
return;
}
std::vector<std::string> suggestions;
m_engine->BuildCompletions(line, &suggestions);
for (size_t i = 0; i < suggestions.size(); ++i)
{
linenoiseAddCompletion(completions, suggestions[i].c_str());
}
}
}