SymphonyElectron/auto_update/ipc.h

822 lines
38 KiB
C

#ifndef ipc_h
#define ipc_h
#include <stdbool.h>
#include <stddef.h>
#define IPC_MESSAGE_MAX_LENGTH 4096
// client
typedef struct ipc_client_t ipc_client_t;
ipc_client_t* ipc_client_connect( char const* pipe_name );
void ipc_client_disconnect( ipc_client_t* connection );
typedef enum ipc_receive_status_t {
IPC_RECEIVE_STATUS_DONE,
IPC_RECEIVE_STATUS_MORE_DATA,
IPC_RECEIVE_STATUS_ERROR,
} ipc_receive_status_t;
ipc_receive_status_t ipc_client_receive( ipc_client_t* connection, char* output, int output_size, int* received_size );
bool ipc_client_send( ipc_client_t* connection, char const* message );
// server
typedef struct ipc_server_t ipc_server_t;
typedef void (*ipc_request_handler_t)( char const* request, void* user_data, char* response, size_t capacity );
ipc_server_t* ipc_server_start( char const* pipe_name, ipc_request_handler_t request_handler, void* user_data );
void ipc_server_stop( ipc_server_t* server );
#endif /* ipc_h */
#ifdef IPC_IMPLEMENTATION
#undef IPC_IMPLEMENTATION
#include <stdio.h>
#include <windows.h>
#include <aclapi.h>
#pragma comment(lib, "advapi32.lib")
#ifndef IPC_LOG_INFO
#define IPC_LOG_INFO printf( "\n" ), printf
#endif
#ifndef IPC_LOG_ERROR
#define IPC_LOG_ERROR printf( "\n" ), printf
#endif
#ifndef IPC_LOG_LAST_ERROR
#define IPC_LOG_LAST_ERROR printf( "\nLastError=%d : ", GetLastError() ), printf
#endif
// Named pipes are on the form "\\.\pipe\name" but we don't want the user to have
// to specify all that, so we expand what they pass in from "name" to "\\.\pipe\name"
bool expand_pipe_name( char const* pipe_name, char* buffer, size_t capacity ) {
int result = snprintf( buffer, capacity, "\\\\.\\pipe\\%s", pipe_name );
return result >= 0 && result < (int) capacity;
}
// Returns true if a pipe of the specified name exists, false if none exists
bool pipe_exists( const char* pipe_name ) {
IPC_LOG_INFO( "Checking if pipe exists: %s", pipe_name );
WIN32_FIND_DATAA data;
memset( &data, 0, sizeof( data ) );
HANDLE hfind = FindFirstFileA( "\\\\.\\pipe\\*", &data );
if( hfind != INVALID_HANDLE_VALUE ) {
do {
char const* filename = data.cFileName;
if( _stricmp( filename, pipe_name ) == 0 ) {
FindClose( hfind );
IPC_LOG_INFO( "Pipe found: %s", filename );
return true;
}
} while( FindNextFileA( hfind, &data ) );
FindClose( hfind );
}
IPC_LOG_ERROR( "Pipe not found" );
return false;
}
// This holds data related to a single client instance
struct ipc_client_t {
HANDLE pipe; // The named pipe to communicate over
};
// Establishes a connection to the specified named pipe
// Returns NULL if a connection could not be established
ipc_client_t* ipc_client_connect( char const* pipe_name ) {
IPC_LOG_INFO( "Connecting to named pipe: %s", pipe_name );
// Make sure a pipe with the specified name exists
if( !pipe_exists( pipe_name ) ) {
// Retry once if pipe was not found - this would be very rare, but will make it more robust
IPC_LOG_INFO( "Pipe was not found, waiting a little and trying again" );
Sleep( 1000 );
if( !pipe_exists( pipe_name ) ) {
IPC_LOG_INFO( "Pipe was still not found after waiting" );
IPC_LOG_ERROR( "Named pipe does not exist" );
return NULL;
}
}
// Expand the pipe name to the valid form eg. "\\.\pipe\name"
char expanded_pipe_name[ MAX_PATH ];
IPC_LOG_INFO( "Expanding to fully qualified pipe name: %s", pipe_name );
if( !expand_pipe_name( pipe_name, expanded_pipe_name, sizeof( expanded_pipe_name ) ) ) {
IPC_LOG_ERROR( "Pipe name too long" );
return NULL;
}
IPC_LOG_INFO( "Expanded pipe name: %s", expanded_pipe_name );
// A named pipe has a maximum number of connections. When a client disconnect, it
// can take a while for the disconnect to register on the server side, so we need
// to handle the case where the pipe is busy. In practice, this should be rare,
// but for robustness we handle it anyway.
HANDLE pipe = NULL;
for( ; ; ) { // This loop will typically not run more than two iterations, due to multiple exit points
IPC_LOG_INFO( "Try to create connection" );
pipe = CreateFileA(
expanded_pipe_name, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL ); // no template file
// Break if the pipe handle is valid - a connection is now established
if( pipe != INVALID_HANDLE_VALUE ) {
IPC_LOG_INFO( "Connection attempt succeeded" );
break;
}
// Retry once if pipe was not found. Very rare that this would happen, but we're going for stability
if( GetLastError() == ERROR_FILE_NOT_FOUND ) {
IPC_LOG_INFO( "The pipe was not found, which was unexpected at this point, so we wait a little and try again" );
Sleep( 1000 );
pipe = CreateFileA(
expanded_pipe_name, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL ); // no template file
// Break if the pipe handle is valid - a connection is now established
if( pipe != INVALID_HANDLE_VALUE ) {
IPC_LOG_INFO( "Second connection attempt succeeded" );
break;
}
}
// If we get an error other than ERROR_PIPE_BUSY, we fail to establish a connection.
// In the case of ERROR_PIPE_BUSY we will wait for the pipe not to be busy (see below)
if( GetLastError() != ERROR_PIPE_BUSY ) {
IPC_LOG_LAST_ERROR( "Could not open pipe: " );
return NULL;
}
// All pipe instances are busy, so wait for 20 seconds.
IPC_LOG_INFO( "All pipe instances are busy, so we wait for 20 seconds and then try again" );
if( !WaitNamedPipeA( expanded_pipe_name, 20000 ) ) {
IPC_LOG_INFO( "Wait failed" );
// In the specific case of getting an ERROR_FILE_NOT_FOUND, we try doing the
// wait one more time. The reason this would happen is if the server was just restarting
// at the start of the call to ipc_client_connect, and thus the check if the pipe exist
// passed, but when we got to the wait, the pipe was closed down and not yet started up
// again. Waiting briefly and then trying again will ensure that we handle this rare case
// of the server being restarted, but it will be very very rare.
if( GetLastError() == ERROR_FILE_NOT_FOUND ) {
// retry once just in case pipe was not created yet
IPC_LOG_INFO( "Try the wait again after a short pause, in case it was just being created" );
Sleep(1000);
if( !WaitNamedPipeA( expanded_pipe_name, 20000 ) ) {
IPC_LOG_LAST_ERROR( "Could not open pipe on second attempt: 20 second wait timed out: " );
return NULL;
}
} else {
IPC_LOG_LAST_ERROR( "Could not open pipe: 20 second wait timed out: " );
return NULL;
}
}
}
// A fully working connection has been set up, return it to the caller
IPC_LOG_INFO( "Connection successful" );
ipc_client_t* connection = (ipc_client_t*) malloc( sizeof( ipc_client_t ) );
connection->pipe = pipe;
return connection;
}
// Disconnect the client from the server, and release the resources used by it
// This will allow the server to eventually recycle and reuse that connection slot,
// but in some cases it can take a brief period of time for that to happen
void ipc_client_disconnect( ipc_client_t* connection ) {
IPC_LOG_INFO( "Disconnecting client" );
FlushFileBuffers( connection->pipe );
DisconnectNamedPipe( connection->pipe );
CloseHandle( connection->pipe );
free( connection );
IPC_LOG_INFO( "Disconnection complete" );
}
// Wait for data to be available on the named pipe, and once it is, read it into the
// provided buffer. Returns a status enum for success or failure, or for the case
// where more data was cued up on the server side than could be received in one call,
// in which case the ipc_client_receive function should be called again to complete
// the retrieval of the message. The function will wait indefinitely, until either
// a message is available, or the pipe is closed.
// TODO: consider a timeout for the wait, to allow for more robust client implementations
ipc_receive_status_t ipc_client_receive( ipc_client_t* connection, char* output, int output_size, int* received_size ) {
IPC_LOG_INFO( "Reading data" );
DWORD size_read = 0;
BOOL success = ReadFile(
connection->pipe, // pipe handle
output, // buffer to receive reply
output_size, // size of buffer
&size_read, // number of bytes read
NULL ); // not overlapped
IPC_LOG_INFO( "Read returned %s", success ? "true" : "false" );
if( !success && GetLastError() != ERROR_MORE_DATA ) {
IPC_LOG_LAST_ERROR( "ReadFile from pipe failed: " );
return IPC_RECEIVE_STATUS_ERROR;
}
IPC_LOG_INFO( "Data size received: %u", size_read );
if( received_size ) {
*received_size = size_read;
}
if( success ) {
IPC_LOG_INFO( "Read done" );
return IPC_RECEIVE_STATUS_DONE;
} else {
IPC_LOG_INFO( "More data to be read" );
return IPC_RECEIVE_STATUS_MORE_DATA;
}
}
// Sends the specified message (as a zero-terminated string) to the server
// Will wait for the server to receive the message, and how long that wait
// is will depend on if the server is busy when the message is sent.
// TODO: consider a timeout for the wait, to allow for more robust client implementations
bool ipc_client_send( ipc_client_t* connection, char const* message ) {
// Send a message to the pipe server.
IPC_LOG_INFO( "Sending data" );
DWORD written = 0;
BOOL success = WriteFile(
connection->pipe, // pipe handle
message, // message
(DWORD) strlen( message ) + 1, // message length
&written, // bytes written
NULL ); // not overlapped
IPC_LOG_INFO( "Write returned %s", success ? "true" : "false" );
if( !success ) {
IPC_LOG_LAST_ERROR( "WriteFile to pipe failed: " );
return false;
}
return true;
}
// This holds the data for a single server-side client thread
typedef struct ipc_client_thread_t {
BOOL recycle; // When a client disconnect, this flag is set to TRUE so the slot can be reused
ipc_request_handler_t request_handler; // When a request is recieved from a client, the server calls this handler
void* user_data; // When the request_handler is called, this user_data field is passed along with it
int exit_flag; // Set by the server to signal that the client thread should exit
HANDLE thread; // Handle to this client thread, used by server to wait for it to exit (on server shutdown)
HANDLE pipe; // The named pipe instance allocated to this client
OVERLAPPED io; // We are using non-blocking I/O so the server can cancel pending read/write operations on shutdown
} ipc_client_thread_t;
// Typically, we should only ever have one connections, so this is probably overkill, but
// it doesn't hurt
#define MAX_CLIENT_CONNECTIONS 32
// This holds the data for an ipc server instance
struct ipc_server_t {
char expanded_pipe_name[ MAX_PATH ]; // Holds the result of expanding from the "name" form to the "\\.\pipe\name" form
HANDLE thread; // Handle to the main server thread, used to wait for thread exit on server shutdown
HANDLE thread_started_event; // When the main server thread is started, ipc_server_start needs to wait until it is ready to accept connections before returning to the caller
HANDLE pipe; // The server pipe instance currently used to listen for connections, will be handed to client thread when connection is made
OVERLAPPED io; // We are using non-blocking I/O so the server can cancel pending ConnectNamedPipe operations on shutdown
int exit_flag; // Set by the ipc_server_stop to signal that the main server thread should exit
ipc_request_handler_t request_handler; // When a request is recieved from a client, the server calls this handler
void* user_data; // When the request_handler is called, this user_data field is passed along with it
ipc_client_thread_t client_threads[ MAX_CLIENT_CONNECTIONS ]; // Array of client instances, an instance is only in use if its `recycle` flag is FALSE
int client_threads_count; // Number of slots used on the `client_threads` array (but a slot may or may not be in use depending on its `recycle` flag)
};
// When a client connects to the server, the server creates a new thread to handle that connection,
// and this is the function running on that thread. It basically just sits in a loop, doing a Read
// from the pipe and waiting until it gets a message. Then it will call the user supplied request
// handler, and then it does a Write on the pipe to send the response it got from the request handler
// to the pipe.
DWORD WINAPI ipc_client_thread( LPVOID param ) {
IPC_LOG_INFO( "[%u] Client thread started", GetCurrentThreadId() );
ipc_client_thread_t* context = (ipc_client_thread_t*) param;
// Create the event used to wait for Read/Write operations to complete
HANDLE io_event = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
NULL // object name
);
// Main request-response loop. Will run until exit requested or an eror occurs
IPC_LOG_INFO( "[%u] Enter client thread main loop", GetCurrentThreadId() );
while( !context->exit_flag ) {
// Read loop, keeps trying to read until data arrives or an error occurs (including a shutdown
// cancelling the read operation)
char request[ IPC_MESSAGE_MAX_LENGTH ]; // buffer to hold incoming data
DWORD bytes_read = 0;
BOOL success = FALSE;
bool read_pending = true;
IPC_LOG_INFO( "[%u] Starting read loop", GetCurrentThreadId() );
while( read_pending ) {
// Set up non-blocking I/O
memset( &context->io, 0, sizeof( context->io ) );
ResetEvent( io_event );
context->io.hEvent = io_event;
// Read client requests from the pipe in a non-blocking call
IPC_LOG_INFO( "[%u] Reading from pipe", GetCurrentThreadId() );
success = ReadFile(
context->pipe, // handle to pipe
request, // buffer to receive data
IPC_MESSAGE_MAX_LENGTH, // size of buffer
&bytes_read, // number of bytes read
&context->io ); // overlapped I/O
IPC_LOG_INFO( "[%u] Read returned: %s", GetCurrentThreadId(), success ? "true" : "false" );
// Check if the Read operation is in progress (ReadFile returns FALSE and the error is ERROR_IO_PENDING )
if( !success && GetLastError() == ERROR_IO_PENDING ) {
IPC_LOG_INFO( "[%u] Pipe is in IO_PENDING state, read is in progress", GetCurrentThreadId() );
// Wait for the event to be triggered, but timeout after half a second and re-issue the Read
// This is so the re-issued Read can detect if the pipe have been closed, and thus exit the thread
IPC_LOG_INFO( "[%u] Wait for read to complete", GetCurrentThreadId() );
if( WaitForSingleObject( io_event, 500 ) == WAIT_TIMEOUT ) {
IPC_LOG_INFO( "[%u] Read timed out, try again", GetCurrentThreadId() );
CancelIoEx( context->pipe, &context->io );
continue; // Make another Read call
}
// The wait did not timeout, so the Read operation should now be completed (or failed)
IPC_LOG_INFO( "[%u] Read completed, checking result", GetCurrentThreadId() );
success = GetOverlappedResult(
context->pipe, // handle to pipe
&context->io, // OVERLAPPED structure
&bytes_read, // bytes transferred
FALSE ); // don't wait
IPC_LOG_INFO( "[%u] Read was %s", GetCurrentThreadId(), success ? "successful" : "unsuccessful" );
IPC_LOG_INFO( "[%u] Bytes read %u", GetCurrentThreadId(), bytes_read );
}
// The read have completed (successfully or not) so exit the read loop
read_pending = false;
}
IPC_LOG_INFO( "[%u] Finished read loop, result was %s", GetCurrentThreadId(), success ? "success" : "failure" );
// If the Read was unsuccessful (or read no data), log the error and exit the thread
// TODO: consider if there are better ways to deal with the error. There might not be,
// but then the user-code calling client send/receive might need some robust retry code
if( !success || bytes_read == 0 ) {
if( GetLastError() == ERROR_BROKEN_PIPE ) {
IPC_LOG_INFO( "[%u] Client disconnected", GetCurrentThreadId() );
} else {
IPC_LOG_LAST_ERROR( "ReadFile failed: " );
}
break;
}
// Check if a server shutdown have requested this thread to be terminated, and exit if that's the case
if( context->exit_flag ) {
IPC_LOG_INFO( "[%u] Server shutdown requested, terminating thread", GetCurrentThreadId() );
break;
}
IPC_LOG_INFO( "[%u] Incoming message: %s", GetCurrentThreadId(), request );
// Process the incoming message by calling the user-supplied request handler function
char response[ IPC_MESSAGE_MAX_LENGTH ];
memset( response, 0, sizeof( response ) );
IPC_LOG_INFO( "[%u] Processing message", GetCurrentThreadId() );
context->request_handler( request, context->user_data, response, sizeof( response ) );
response[ sizeof( response ) - 1 ] = '\0'; // Force zero termination (truncate string)
IPC_LOG_INFO( "[%u] Outgoing response: \"%.32s%s\"", GetCurrentThreadId(), response, strlen( response ) > 32 ? "..." : "" );
DWORD response_length = (DWORD)strlen( response ) + 1;
IPC_LOG_INFO( "[%u] Response length: %u", GetCurrentThreadId(), response_length );
// Write the reply to the pipe
DWORD bytes_written = 0;
IPC_LOG_INFO( "[%u] Sending response", GetCurrentThreadId() );
success = WriteFile(
context->pipe, // handle to pipe
response, // buffer to write from
response_length, // number of bytes to write
&bytes_written, // number of bytes written
&context->io ); // overlapped I/O
IPC_LOG_INFO( "[%u] Write returned: %s", GetCurrentThreadId(), success ? "true" : "false" );
// If the write operation is in progress, we wait until it is done, or aborted due to server shutdown
if( success || GetLastError() == ERROR_IO_PENDING ) {
IPC_LOG_INFO( "[%u] Pipe is in IO_PENDING state, write is in progress", GetCurrentThreadId() );
success = GetOverlappedResult(
context->pipe, // handle to pipe
&context->io, // OVERLAPPED structure
&bytes_written, // bytes transferred
TRUE ); // wait
IPC_LOG_INFO( "[%u] Write was %s", GetCurrentThreadId(), success ? "successful" : "unsuccessful" );
IPC_LOG_INFO( "[%u] Bytes written %u", GetCurrentThreadId(), bytes_written );
}
// If the Write was unsuccessful (or didn't manage to write the whole buffer), log the error and exit the thread
if( !success || bytes_written != response_length ) {
IPC_LOG_LAST_ERROR( "WriteFile failed: " );
break;
}
}
IPC_LOG_INFO( "[%u] Finished client thread main loop", GetCurrentThreadId() );
// Signal that a disconnect has happened
context->request_handler( NULL, context->user_data, NULL, 0 );
// Flush the pipe to allow the client to read the pipe's contents
// before disconnecting. Then disconnect the pipe, and close the
// handle to this pipe instance.
CloseHandle( io_event );
FlushFileBuffers( context->pipe );
DisconnectNamedPipe( context->pipe );
CloseHandle( context->pipe );
// Mark this client slot for recycling for new connections
context->pipe = INVALID_HANDLE_VALUE;
context->recycle = TRUE;
IPC_LOG_INFO( "[%u] Client thread terminated", GetCurrentThreadId() );
return EXIT_SUCCESS;
}
// When the `ipc_server_start` is called, it creates this thread which sits in a loop
// and listens for new client connections, until exit is requested by a call to
// `ipc_server_stop`. When a new connection is made, it will start another thread to
// handle the I/O for that specific client. Then it will open a new listening pipe
// instance for further connections.
DWORD WINAPI ipc_server_thread( LPVOID param ) {
IPC_LOG_INFO( "[%u] Server thread started", GetCurrentThreadId() );
ipc_server_t* server = (ipc_server_t*) param;
// Create security attribs, we need this so the server can run in session 0
// while client runs as a normal user session
SID_IDENTIFIER_AUTHORITY auth = { SECURITY_WORLD_SID_AUTHORITY };
PSID sid;
if( !AllocateAndInitializeSid( &auth, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &sid ) ) {
IPC_LOG_LAST_ERROR( "AllocateAndInitializeSid failed: " );
IPC_LOG_INFO( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
EXPLICIT_ACCESS access = { 0 };
access.grfAccessPermissions = FILE_ALL_ACCESS;
access.grfAccessMode = SET_ACCESS;
access.grfInheritance = NO_INHERITANCE;
access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
access.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
access.Trustee.ptstrName = (LPTSTR)sid;
PACL acl;
if( SetEntriesInAcl(1, &access, NULL, &acl) != ERROR_SUCCESS ) {
IPC_LOG_LAST_ERROR( "SetEntriesInAcl failed: " );
FreeSid(sid);
IPC_LOG_INFO( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
PSECURITY_DESCRIPTOR sd = (PSECURITY_DESCRIPTOR)LocalAlloc( LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH );
if( !sd ) {
IPC_LOG_ERROR( "LocalAlloc failed" );
FreeSid( sid );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
if( !InitializeSecurityDescriptor( sd, SECURITY_DESCRIPTOR_REVISION ) ) {
IPC_LOG_LAST_ERROR( "InitializeSecurityDescriptor failed: " );
LocalFree(sd);
FreeSid(sid);
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
if( !SetSecurityDescriptorDacl( sd, TRUE, acl, FALSE ) ) {
IPC_LOG_LAST_ERROR( "SetSecurityDescriptorDacl failed: " );
LocalFree( sd );
FreeSid( sid );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
SECURITY_ATTRIBUTES attribs;
attribs.nLength = sizeof( SECURITY_ATTRIBUTES );
attribs.lpSecurityDescriptor = sd;
attribs.bInheritHandle = -1;
// Create the event used to wait for ConnectNamedPipe operations to complete
HANDLE io_event = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
NULL // object name
);
// The main loop creates an instance of the named pipe and then waits for a client
// to connect to it. When the client connects, a thread is created to handle
// communications with that client, and this loop is free to wait for the next client
// connect request
IPC_LOG_INFO( "[%u] Enter server thread main loop", GetCurrentThreadId() );
bool event_raised = false; // We make sure to only raise the server-thread-is-ready event once
while( !server->exit_flag ) {
// Create a pipe instance to listen for connections
IPC_LOG_INFO( "[%u] Creating named pipe listening for connections", GetCurrentThreadId() );
server->pipe = CreateNamedPipeA(
server->expanded_pipe_name,// pipe name
PIPE_ACCESS_DUPLEX | // read/write access
FILE_FLAG_OVERLAPPED, // we use async I/O so that we can cancel ConnectNamedPipe operations
PIPE_TYPE_MESSAGE | // message type pipe
PIPE_READMODE_MESSAGE | // message-read mode
PIPE_WAIT, // blocking mode
MAX_CLIENT_CONNECTIONS, // max. instances
IPC_MESSAGE_MAX_LENGTH, // output buffer size
IPC_MESSAGE_MAX_LENGTH, // input buffer size
0, // client time-out
&attribs ); // default security attribute
// If we failed to create the pipe, we log the error and exit
// TODO: Should we handle this some other way? perhaps report the error back to user
if( server->pipe == INVALID_HANDLE_VALUE ) {
// If the failure was due to pipe busy, try again in a bit
if( GetLastError() == ERROR_PIPE_BUSY ) {
IPC_LOG_LAST_ERROR( "CreateNamedPipe failed: " );
IPC_LOG_INFO( "[%u] Pipe was busy, waiting a bit then trying once more.", GetCurrentThreadId() );
Sleep(1000);
server->pipe = CreateNamedPipeA(
server->expanded_pipe_name,// pipe name
PIPE_ACCESS_DUPLEX | // read/write access
FILE_FLAG_OVERLAPPED, // we use async I/O so that we can cancel ConnectNamedPipe operations
PIPE_TYPE_MESSAGE | // message type pipe
PIPE_READMODE_MESSAGE | // message-read mode
PIPE_WAIT, // blocking mode
MAX_CLIENT_CONNECTIONS, // max. instances
IPC_MESSAGE_MAX_LENGTH, // output buffer size
IPC_MESSAGE_MAX_LENGTH, // input buffer size
0, // client time-out
&attribs ); // default security attribute
}
if( server->pipe == INVALID_HANDLE_VALUE ) {
IPC_LOG_LAST_ERROR( "CreateNamedPipe failed: " );
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
}
// Signal to `ipc_server_start` that the server thread is now fully up and
// running and accepting connections
if( !event_raised ) {
IPC_LOG_INFO( "[%u] Signaling to ipc_server_start that init is complete and we are now listening", GetCurrentThreadId() );
SetEvent( server->thread_started_event );
event_raised = true; // Make sure we don't signal the event again
}
// Wait for the client to connect, using async I/O operation, so ConnectNamedPipe returns immediately
memset( &server->io, 0, sizeof( server->io ) );
server->io.hEvent = io_event;
IPC_LOG_INFO( "[%u] Wait for client to connect", GetCurrentThreadId() );
ConnectNamedPipe( server->pipe, &server->io );
if( GetLastError() == ERROR_IO_PENDING ) {
for( ; ; ) {
if( WaitForSingleObject( server->io.hEvent, 100 ) == WAIT_OBJECT_0 ) {
IPC_LOG_INFO( "[%u] Connection completed", GetCurrentThreadId() );
break;
}
if( server->exit_flag ) {
IPC_LOG_INFO( "[%u] Server shutdown requested", GetCurrentThreadId() );
break;
}
}
} else if( GetLastError() != ERROR_PIPE_CONNECTED ) {
if( GetLastError() != ERROR_OPERATION_ABORTED || server->exit_flag == 0 ) {
// The client could not connect, so close the pipe.
IPC_LOG_LAST_ERROR( "Connection failed: " );
break;
}
}
// Check if a server shutdown have requested this thread to be terminated, and exit if that's the case
if( server->exit_flag ) {
IPC_LOG_INFO( "[%u] Server shutdown requested, breaking main loop", GetCurrentThreadId() );
break;
}
IPC_LOG_INFO( "[%u] Client connected, setting up client thread to handle it", GetCurrentThreadId() );
// Find a free client slot to recycle for this new client connection
ipc_client_thread_t* context = NULL;
for( int i = 0; i < server->client_threads_count; ++i ) {
if( server->client_threads[ i ].recycle ) {
context = &server->client_threads[ i ];
}
}
// If there is no free slot to recycle, use a new slot if available
if( !context ) {
IPC_LOG_INFO( "[%u] No free slot to recycle, allocating a new slot", GetCurrentThreadId() );
if( server->client_threads_count < MAX_CLIENT_CONNECTIONS ) {
context = &server->client_threads[ server->client_threads_count++ ];
} else {
IPC_LOG_ERROR( "[%u] Maximum number of connectsions reached - client should have been held in wait state, and this error should never have been triggered", GetCurrentThreadId() );
// If we already reached the maximum number of connections, we have to bail out
// This shouldn't really happen though, as the client should be kept in the wait
// state by the pipe itself which is specified to accept only the same number of
// connections
// TODO: Perhaps better to just silently refuse the connection but stay alive?
// or maybe kill the connection that has been idle for the longest time?
IPC_LOG_INFO( "[%u] Too many connections", GetCurrentThreadId() );
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
if( server->pipe != INVALID_HANDLE_VALUE ) {
CloseHandle( server->pipe );
server->pipe = INVALID_HANDLE_VALUE;
}
CloseHandle( io_event );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
}
// Initialize the client slot
IPC_LOG_INFO( "[%u] Initializing client slot", GetCurrentThreadId() );
memset( context, 0, sizeof( *context ) );
context->request_handler = server->request_handler;
context->user_data = server->user_data;
context->pipe = server->pipe;
// We are handing the pipe over to the client thread, but will be creating a new one on
// the next iteration through the loop
server->pipe = INVALID_HANDLE_VALUE;
// Create a dedicated thread to handle this connection
IPC_LOG_INFO( "[%u] Creating the client thread", GetCurrentThreadId() );
context->thread = CreateThread(
NULL, // no security attribute
0, // default stack size
ipc_client_thread, // thread proc
(LPVOID) context, // thread parameter
0, // not suspended
NULL ); // returns thread ID
// If we failed to create thread, something's gone very wrong, so we need to bail
if( context->thread == NULL ) {
IPC_LOG_LAST_ERROR( "CreateThread failed: " );
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
if( server->pipe != INVALID_HANDLE_VALUE ) {
CloseHandle( server->pipe );
server->pipe = INVALID_HANDLE_VALUE;
}
CloseHandle( io_event );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
}
IPC_LOG_INFO( "[%u] Finished server thread main loop", GetCurrentThreadId() );
// Cleanup thread resources before we exit
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
if( server->pipe != INVALID_HANDLE_VALUE ) {
CloseHandle( server->pipe );
server->pipe = INVALID_HANDLE_VALUE;
}
CloseHandle( io_event );
IPC_LOG_INFO( "[%u] Server thread terminated by request", GetCurrentThreadId() );
return EXIT_SUCCESS;
}
// Starts a named pipe server with the specified pipe name, and starts listening for
// client connections on a separate thread, so will return immediately. The server
// thread will keep listening for connections until `ipc_server_stop` is called.
ipc_server_t* ipc_server_start( char const* pipe_name, ipc_request_handler_t request_handler, void* user_data ) {
IPC_LOG_INFO( "Starting named pipe server: %s", pipe_name );
// Allocate the server instance and initialize it
ipc_server_t* server = (ipc_server_t*) malloc( sizeof( ipc_server_t ) );
memset( server, 0, sizeof( ipc_server_t ) );
server->pipe = INVALID_HANDLE_VALUE;
server->request_handler = request_handler;
server->user_data = user_data;
// Expand the pipe name to the valid form eg. "\\.\pipe\name"
IPC_LOG_INFO( "Expanding to fully qualified pipe name: %s", pipe_name );
if( !expand_pipe_name( pipe_name, server->expanded_pipe_name, sizeof( server->expanded_pipe_name ) ) ) {
IPC_LOG_ERROR( "Pipe name too long" );
free( server );
return NULL;
}
IPC_LOG_INFO( "Expanded pipe name: %s", server->expanded_pipe_name );
// Create the event used by the server thread to signal that it is up and running and accepting connections
server->thread_started_event = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
NULL // object name
);
// Start the server thread which accepts connections and starts dedicated client threads for each new connection
IPC_LOG_INFO( "Starting server thread" );
server->thread = CreateThread(
NULL, // default security attributes
0, // use default stack size
ipc_server_thread, // thread function name
server, // argument to thread function
0, // use default creation flags
NULL ); // returns the thread identifier
// If thread creation failed, return error
if( server->thread == NULL ) {
IPC_LOG_LAST_ERROR( "Failed to create server thread: " );
CloseHandle( server->thread_started_event );
free( server );
return NULL;
}
// Wait for the server thread to be up and running and accepting connections
IPC_LOG_INFO( "Waiting for server thread to be initialized" );
if( WaitForSingleObject( server->thread_started_event, 10000 ) != WAIT_OBJECT_0 ) {
// If it takes more than 10 seconds for the server thread to start up, something
// has gone very wrong so we abort and return an error
IPC_LOG_LAST_ERROR( "Timeout waiting for client thread to start: " );
CloseHandle( server->thread_started_event );
TerminateThread( server->thread, EXIT_FAILURE );
free( server );
return NULL;
}
IPC_LOG_INFO( "Server up and running" );
// Return the fully set up and ready server instance
return server;
}
// Signals the server thread to stop, cancels all pending I/O operations on all
// client threads, and release the resources used by the server
void ipc_server_stop( ipc_server_t* server ) {
IPC_LOG_INFO( "Stopping named pipe server" );
server->exit_flag = 1; // Signal server thread top stop
if( server->pipe != INVALID_HANDLE_VALUE ) {
CancelIoEx( server->pipe, &server->io ); // Cancel pending ConnectNamedPipe operatios, if any
}
IPC_LOG_INFO( "Waiting for server thread to exit" );
WaitForSingleObject( server->thread, INFINITE ); // Wait for server thread to exit
IPC_LOG_INFO( "Server thread stopped" );
// Loop over all clients and terminate each one
IPC_LOG_INFO( "Terminating client connections" );
for( int i = 0; i < server->client_threads_count; ++i ) {
ipc_client_thread_t* client = &server->client_threads[ i ];
if( !client->recycle ) { // A slot is only valid if `recycle` is FALSE
client->exit_flag = 1; // Tell client thread to exit
CancelIoEx( client->pipe, &client->io ); // Cancel any pending Read/Write operation
WaitForSingleObject( client->thread, INFINITE ); // Wait for client thread to exit
}
}
// Free server resources
CloseHandle( server->thread_started_event );
free( server );
IPC_LOG_INFO( "Server stopped and terminated" );
}
#endif /* IPC_IMPLEMENTATION */