6295: Implement FIFO on Windows using Named Pipes r=def- a=Robyt3

Reimplement the Linux FIFO file server and client controls on Windows by using Named Pipes.

The DDNet server/client acts as a named pipe server and receives messages.
Messages can be posted to the named pipe server by connecting to it as a client.
The named pipe client can for instance be controlled from the command line with PowerShell.

The PowerShell script `scripts/send_named_pipe.ps1` is added for this purpose.
For example the PowerShell command `./send_named_pipe.ps1 "testpipe" "echo a"` sends the command `echo a` to the pipe named `testpipe`.
Multiple commands can be sent at the same time by separating them with semicolons or newlines.

## Checklist

- [X] Tested the change ingame
- [ ] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test (especially base/) or added coverage to integration test
- [ ] Considered possible null pointers and out of bounds array indexing
- [ ] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: Robert Müller <robytemueller@gmail.com>
This commit is contained in:
bors[bot] 2023-01-22 21:40:17 +00:00 committed by GitHub
commit 3ffd4205ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 184 additions and 32 deletions

View file

@ -0,0 +1,33 @@
# This PowerShell script connects to a Named Pipe server,
# sends one message and then disconnects again.
# The first argument is the name of the pipe.
# The second argument is the message to send.
if ($args.length -lt 2) {
Write-Output "Usage: ./send_named_pipe.ps1 <pipename> <message> [message] ... [message]"
return
}
$Wrapper = [pscustomobject]@{
Pipe = new-object System.IO.Pipes.NamedPipeClientStream(
".",
$args[0],
[System.IO.Pipes.PipeDirection]::InOut,
[System.IO.Pipes.PipeOptions]::None,
[System.Security.Principal.TokenImpersonationLevel]::Impersonation
)
Reader = $null
Writer = $null
}
$Wrapper.Pipe.Connect(5000)
if (!$?) {
return
}
$Wrapper.Reader = New-Object System.IO.StreamReader($Wrapper.Pipe)
$Wrapper.Writer = New-Object System.IO.StreamWriter($Wrapper.Pipe)
$Wrapper.Writer.AutoFlush = $true
for ($i = 1; $i -lt $args.length; $i++) {
$Wrapper.Writer.WriteLine($args[$i])
}
# We need to wait because the lines will not be written if we close the pipe immediately
Start-Sleep -Seconds 1.5
$Wrapper.Pipe.Close()

View file

@ -1437,7 +1437,7 @@ static int priv_net_close_all_sockets(NETSOCKET sock)
}
#if defined(CONF_FAMILY_WINDOWS)
static char *windows_format_system_message(unsigned long error)
char *windows_format_system_message(unsigned long error)
{
WCHAR *wide_message;
const DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_MAX_WIDTH_MASK;

View file

@ -1147,6 +1147,20 @@ void net_unix_set_addr(UNIXSOCKETADDR *addr, const char *path);
*/
void net_unix_close(UNIXSOCKET sock);
#elif defined(CONF_FAMILY_WINDOWS)
/**
* Formats a Windows error code as a human-readable string.
*
* @param error The Windows error code.
*
* @return A new string representing the error code.
*
* @remark Guarantees that result will contain zero-termination.
* @remark The result must be freed after it has been used.
*/
char *windows_format_system_message(unsigned long error);
#endif
/**

View file

@ -3078,9 +3078,7 @@ void CClient::Run()
// process pending commands
m_pConsole->StoreCommands(false);
#if defined(CONF_FAMILY_UNIX)
m_Fifo.Init(m_pConsole, g_Config.m_ClInputFifo, CFGFLAG_CLIENT);
#endif
InitChecksum();
m_pConsole->InitChecksum(ChecksumData());
@ -3341,9 +3339,7 @@ void CClient::Run()
break;
}
#if defined(CONF_FAMILY_UNIX)
m_Fifo.Update();
#endif
// beNice
auto Now = time_get_nanoseconds();
@ -3395,9 +3391,7 @@ void CClient::Run()
m_GlobalTime = (time_get() - m_GlobalStartTime) / (float)time_freq();
}
#if defined(CONF_FAMILY_UNIX)
m_Fifo.Shutdown();
#endif
GameClient()->OnShutdown();
Disconnect();

View file

@ -281,9 +281,7 @@ class CClient : public IClient, public CDemoPlayer::IListener
std::vector<SWarning> m_vWarnings;
#if defined(CONF_FAMILY_UNIX)
CFifo m_Fifo;
#endif
IOHANDLE m_BenchmarkFile;
int64_t m_BenchmarkStopTime;

View file

@ -2598,9 +2598,7 @@ int CServer::Run()
m_Econ.Init(Config(), Console(), &m_ServerBan);
#if defined(CONF_FAMILY_UNIX)
m_Fifo.Init(Console(), Config()->m_SvInputFifo, CFGFLAG_SERVER);
#endif
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "server name is '%s'", Config()->m_SvName);
@ -2798,9 +2796,7 @@ int CServer::Run()
UpdateClientRconCommands();
#if defined(CONF_FAMILY_UNIX)
m_Fifo.Update();
#endif
}
// master server stuff
@ -2879,9 +2875,7 @@ int CServer::Run()
m_Econ.Shutdown();
#if defined(CONF_FAMILY_UNIX)
m_Fifo.Shutdown();
#endif
GameServer()->OnShutdown();
m_pMap->Unload();

View file

@ -221,9 +221,7 @@ public:
CSnapIDPool m_IDPool;
CNetServer m_NetServer;
CEcon m_Econ;
#if defined(CONF_FAMILY_UNIX)
CFifo m_Fifo;
#endif
CServerBan m_ServerBan;
IEngineMap *m_pMap;

View file

@ -356,7 +356,7 @@ MACRO_CONFIG_INT(SvVotePause, sv_vote_pause, 1, 0, 1, CFGFLAG_SERVER, "Allow vot
MACRO_CONFIG_INT(SvVotePauseTime, sv_vote_pause_time, 10, 0, 360, CFGFLAG_SERVER, "The time (in seconds) players have to wait in pause when paused by vote")
MACRO_CONFIG_INT(SvTuneReset, sv_tune_reset, 1, 0, 1, CFGFLAG_SERVER, "Whether tuning is reset after each map change or not")
MACRO_CONFIG_STR(SvResetFile, sv_reset_file, 128, "reset.cfg", CFGFLAG_SERVER, "File to execute on map change or reload to set the default server settings")
MACRO_CONFIG_STR(SvInputFifo, sv_input_fifo, 128, "", CFGFLAG_SERVER, "Fifo file to use as input for server console")
MACRO_CONFIG_STR(SvInputFifo, sv_input_fifo, 128, "", CFGFLAG_SERVER, "Fifo file (non-Windows) or Named Pipe (Windows) to use as input for server console")
MACRO_CONFIG_INT(SvDDRaceTuneReset, sv_ddrace_tune_reset, 1, 0, 1, CFGFLAG_SERVER, "Whether DDRace tuning (sv_hit, sv_endless_drag and sv_old_laser) is reset after each map change or not")
MACRO_CONFIG_INT(SvNamelessScore, sv_nameless_score, 1, 0, 1, CFGFLAG_SERVER, "Whether nameless tee has a score or not")
MACRO_CONFIG_INT(SvTimeInBroadcastInterval, sv_time_in_broadcast_interval, 1, 0, 60, CFGFLAG_SERVER, "How often to update the broadcast time")
@ -412,7 +412,7 @@ MACRO_CONFIG_INT(ClConfirmQuitTime, cl_confirm_quit_time, 20, -1, 1440, CFGFLAG_
MACRO_CONFIG_STR(ClTimeoutCode, cl_timeout_code, 64, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Timeout code to use")
MACRO_CONFIG_STR(ClDummyTimeoutCode, cl_dummy_timeout_code, 64, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Dummy Timeout code to use")
MACRO_CONFIG_STR(ClTimeoutSeed, cl_timeout_seed, 64, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Timeout seed")
MACRO_CONFIG_STR(ClInputFifo, cl_input_fifo, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Fifo file to use as input for client console")
MACRO_CONFIG_STR(ClInputFifo, cl_input_fifo, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Fifo file (non-Windows) or Named Pipe (Windows) to use as input for client console")
MACRO_CONFIG_INT(ClConfigVersion, cl_config_version, 0, 0, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE, "The config version. Helps newer clients fix bugs with older configs.")
// demo editor

View file

@ -3,8 +3,6 @@
#include <base/system.h>
#if defined(CONF_FAMILY_UNIX)
#include <engine/shared/config.h>
#include <cstdlib>
#include <fcntl.h>
#include <sys/stat.h>
@ -36,8 +34,8 @@ void CFifo::Init(IConsole *pConsole, char *pFifoFile, int Flag)
if(!S_ISFIFO(Attribute.st_mode))
{
dbg_msg("fifo", "can't remove file '%s', quitting", m_aFilename);
exit(2);
dbg_msg("fifo", "can't remove file '%s'", m_aFilename);
return;
}
}
@ -48,11 +46,11 @@ void CFifo::Init(IConsole *pConsole, char *pFifoFile, int Flag)
void CFifo::Shutdown()
{
if(m_File >= 0)
{
close(m_File);
fs_remove(m_aFilename);
}
if(m_File < 0)
return;
close(m_File);
fs_remove(m_aFilename);
}
void CFifo::Update()
@ -78,4 +76,127 @@ void CFifo::Update()
if(pCur < aBuf + Length) // missed the last line
m_pConsole->ExecuteLineFlag(pCur, m_Flag, -1);
}
#elif defined(CONF_FAMILY_WINDOWS)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
void CFifo::Init(IConsole *pConsole, char *pFifoFile, int Flag)
{
m_pConsole = pConsole;
if(pFifoFile[0] == '\0')
{
m_pPipe = INVALID_HANDLE_VALUE;
return;
}
str_copy(m_aFilename, "\\\\.\\pipe\\");
str_append(m_aFilename, pFifoFile, sizeof(m_aFilename));
m_Flag = Flag;
const int WLen = MultiByteToWideChar(CP_UTF8, 0, m_aFilename, -1, NULL, 0);
dbg_assert(WLen > 0, "MultiByteToWideChar failure");
wchar_t *pWide = static_cast<wchar_t *>(malloc(WLen * sizeof(*pWide)));
dbg_assert(MultiByteToWideChar(CP_UTF8, 0, m_aFilename, -1, pWide, WLen) == WLen, "MultiByteToWideChar failure");
m_pPipe = CreateNamedPipeW(pWide,
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_NOWAIT | PIPE_REJECT_REMOTE_CLIENTS,
PIPE_UNLIMITED_INSTANCES,
8192,
8192,
NMPWAIT_USE_DEFAULT_WAIT,
NULL);
free(pWide);
if(m_pPipe == INVALID_HANDLE_VALUE)
{
const DWORD LastError = GetLastError();
char *pErrorMsg = windows_format_system_message(LastError);
dbg_msg("fifo", "failed to create named pipe '%s' (%ld %s)", m_aFilename, LastError, pErrorMsg);
free(pErrorMsg);
}
else
dbg_msg("fifo", "created named pipe '%s'", m_aFilename);
}
void CFifo::Shutdown()
{
if(m_pPipe == INVALID_HANDLE_VALUE)
return;
DisconnectNamedPipe(m_pPipe);
CloseHandle(m_pPipe);
m_pPipe = INVALID_HANDLE_VALUE;
}
void CFifo::Update()
{
if(m_pPipe == INVALID_HANDLE_VALUE)
return;
if(!ConnectNamedPipe(m_pPipe, NULL))
{
const DWORD LastError = GetLastError();
if(LastError == ERROR_PIPE_LISTENING) // waiting for clients to connect
return;
if(LastError == ERROR_NO_DATA) // pipe was disconnected from the other end, also disconnect it from this end
{
// disconnect the previous client so we can connect to a new one
DisconnectNamedPipe(m_pPipe);
return;
}
if(LastError != ERROR_PIPE_CONNECTED) // pipe already connected, not an error
{
char *pErrorMsg = windows_format_system_message(LastError);
dbg_msg("fifo", "failed to connect named pipe '%s' (%ld %s)", m_aFilename, LastError, pErrorMsg);
free(pErrorMsg);
return;
}
}
while(true) // read all messages from the pipe
{
DWORD BytesAvailable;
if(!PeekNamedPipe(m_pPipe, NULL, 0, NULL, &BytesAvailable, NULL))
{
const DWORD LastError = GetLastError();
if(LastError != ERROR_BAD_PIPE) // pipe not connected, not an error
{
char *pErrorMsg = windows_format_system_message(LastError);
dbg_msg("fifo", "failed to peek at pipe '%s' (%ld %s)", m_aFilename, LastError, pErrorMsg);
free(pErrorMsg);
}
return;
}
if(BytesAvailable == 0) // pipe connected but no data available
return;
char *pBuf = static_cast<char *>(malloc(BytesAvailable + 1));
DWORD Length;
if(!ReadFile(m_pPipe, pBuf, BytesAvailable, &Length, NULL))
{
const DWORD LastError = GetLastError();
char *pErrorMsg = windows_format_system_message(LastError);
dbg_msg("fifo", "failed to read from pipe '%s' (%ld %s)", m_aFilename, LastError, pErrorMsg);
free(pErrorMsg);
free(pBuf);
return;
}
pBuf[Length] = '\0';
char *pCur = pBuf;
for(DWORD i = 0; i < Length; ++i)
{
if(pBuf[i] != '\n')
continue;
pBuf[i] = '\0';
m_pConsole->ExecuteLineFlag(pCur, m_Flag, -1);
pCur = pBuf + i + 1;
}
if(pCur < pBuf + Length) // missed the last line
m_pConsole->ExecuteLineFlag(pCur, m_Flag, -1);
free(pBuf);
}
}
#endif

View file

@ -3,14 +3,16 @@
#include <engine/console.h>
#if defined(CONF_FAMILY_UNIX)
class CFifo
{
IConsole *m_pConsole;
char m_aFilename[IO_MAX_PATH_LENGTH];
int m_Flag;
#if defined(CONF_FAMILY_UNIX)
int m_File;
#elif defined(CONF_FAMILY_WINDOWS)
void *m_pPipe;
#endif
public:
void Init(IConsole *pConsole, char *pFifoFile, int Flag);
@ -19,5 +21,3 @@ public:
};
#endif
#endif