SDA-3148 refactoring, cleanup and comments

This commit is contained in:
Mattias Gustavsson 2021-05-20 15:21:31 +02:00
parent 3ddcbe1bd5
commit 2011b02bbc
2 changed files with 355 additions and 273 deletions

View File

@ -47,12 +47,18 @@ void ipc_server_stop( ipc_server_t* server );
#pragma comment(lib, "advapi32.lib")
// TODO: placeholder until implementing logging
#define IPC_LOG printf
// 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 ) {
WIN32_FIND_DATAA data;
memset( &data, 0, sizeof( data ) );
@ -73,29 +79,39 @@ bool pipe_exists( const char* pipe_name ) {
}
// This holds data related to a single client instance
struct ipc_client_t {
HANDLE pipe;
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 ) {
// Make sure a pipe with the specified name exists
if( !pipe_exists( pipe_name ) ) {
// Retry once if pipe was not found
// Retry once if pipe was not found - this would be very rare, but will make it more robust
Sleep( 1000 );
if( !pipe_exists( pipe_name ) ) {
printf( "Named pipe does not exist\n" );
IPC_LOG( "Named pipe does not exist\n" );
return NULL;
}
}
// Expand the pipe name to the valid form eg. "\\.\pipe\name"
char expanded_pipe_name[ MAX_PATH ];
if( !expand_pipe_name( pipe_name, expanded_pipe_name, sizeof( expanded_pipe_name ) ) ) {
printf( "Pipe name too long\n" );
IPC_LOG( "Pipe name too long\n" );
return NULL;
}
// 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( ; ; ) { // Keep trying to connect while pipe is busy
for( ; ; ) { // This loop will typically not run more than two iterations, due to multiple exit points
pipe = CreateFileA(
expanded_pipe_name, // pipe name
GENERIC_READ | // read and write access
@ -106,12 +122,12 @@ ipc_client_t* ipc_client_connect( char const* pipe_name ) {
0, // default attributes
NULL ); // no template file
// Break if the pipe handle is valid.
// Break if the pipe handle is valid - a connection is now established
if( pipe != INVALID_HANDLE_VALUE ) {
break;
}
// Retry once if pipe was not found
// 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 ) {
Sleep( 1000 );
pipe = CreateFileA(
@ -123,40 +139,53 @@ ipc_client_t* ipc_client_connect( char const* pipe_name ) {
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL ); // no template file
// Break if the pipe handle is valid.
// Break if the pipe handle is valid - a connection is now established
if( pipe != INVALID_HANDLE_VALUE ) {
break;
}
}
// Exit if an error other than ERROR_PIPE_BUSY occurs.
// 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 ) {
printf( "Could not open pipe. LastError=%d\n", GetLastError() );
IPC_LOG( "Could not open pipe. LastError=%d\n", GetLastError() );
return NULL;
}
// All pipe instances are busy, so wait for 20 seconds.
if( !WaitNamedPipeA( expanded_pipe_name, 20000 ) ) {
// 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
Sleep(1000);
if( !WaitNamedPipeA( expanded_pipe_name, 20000 ) ) {
printf( "Could not open pipe on second attempt: 20 second wait timed out. LastError=%d\n", GetLastError() );
IPC_LOG( "Could not open pipe on second attempt: 20 second wait timed out. LastError=%d\n", GetLastError() );
return NULL;
}
} else {
printf( "Could not open pipe: 20 second wait timed out. LastError=%d\n", GetLastError() );
IPC_LOG( "Could not open pipe: 20 second wait timed out. LastError=%d\n", GetLastError() );
return NULL;
}
}
}
// A fully working connection has been set up, return it to the caller
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 ) {
FlushFileBuffers( connection->pipe );
DisconnectNamedPipe( connection->pipe );
@ -165,6 +194,13 @@ void ipc_client_disconnect( ipc_client_t* connection ) {
}
// 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 ) {
DWORD size_read = 0;
BOOL success = ReadFile(
@ -175,7 +211,7 @@ ipc_receive_status_t ipc_client_receive( ipc_client_t* connection, char* output,
NULL ); // not overlapped
if( !success && GetLastError() != ERROR_MORE_DATA ) {
printf( "ReadFile from pipe failed. LastError=%d\n", GetLastError() );
IPC_LOG( "ReadFile from pipe failed. LastError=%d\n", GetLastError() );
return IPC_RECEIVE_STATUS_ERROR;
}
@ -191,21 +227,14 @@ ipc_receive_status_t ipc_client_receive( ipc_client_t* connection, char* output,
}
// 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 ) {
DWORD mode = PIPE_READMODE_MESSAGE;
BOOL success = SetNamedPipeHandleState(
connection->pipe, // pipe handle
&mode, // new pipe mode
NULL, // don't set maximum bytes
NULL ); // don't set maximum time
if( !success ) {
printf( "SetNamedPipeHandleState failed. LastError=%d\n", GetLastError() );
return false;
}
// Send a message to the pipe server.
DWORD written = 0;
success = WriteFile(
BOOL success = WriteFile(
connection->pipe, // pipe handle
message, // message
(DWORD) strlen( message ) + 1, // message length
@ -213,7 +242,7 @@ bool ipc_client_send( ipc_client_t* connection, char const* message ) {
NULL ); // not overlapped
if( !success ) {
printf( "WriteFile to pipe failed. LastError=%d\n", GetLastError() );
IPC_LOG( "WriteFile to pipe failed. LastError=%d\n", GetLastError() );
return false;
}
@ -221,186 +250,229 @@ bool ipc_client_send( ipc_client_t* connection, char const* message ) {
}
// This holds the data for a single server-side client thread
typedef struct ipc_client_thread_t {
BOOL recycle;
ipc_server_t* server;
HANDLE thread;
HANDLE thread_started_event;
HANDLE pipe;
OVERLAPPED io;
int exit_flag;
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 ];
PSID sid;
PACL acl;
PSECURITY_DESCRIPTOR sd;
HANDLE thread;
HANDLE thread_started_event;
HANDLE pipe;
OVERLAPPED io;
int exit_flag;
ipc_request_handler_t request_handler;
void* user_data;
ipc_client_thread_t client_threads[ MAX_CLIENT_CONNECTIONS ];
int client_threads_count;
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)
};
// This routine is a thread processing function to read from and reply to a client
// via the open pipe connection passed from the main loop. Note this allows
// the main loop to continue executing, potentially creating more threads of
// this procedure to run concurrently, depending on the number of incoming
// client connections.
// 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 ) {
// Print verbose messages. In production code, this should be for debugging only.
//printf( "ipc_client_thread created, receiving and processing messages.\n" );
ipc_client_thread_t* context = (ipc_client_thread_t*) param;
// The thread's parameter is a handle to a pipe object instance.
ipc_client_thread_t* context= (ipc_client_thread_t*) param;
ipc_server_t* server = context->server;
HANDLE hPipe = context->pipe;
HANDLE hEvent = CreateEvent(
// 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
);
//printf( "Thread started\n" );
SetEvent( context->thread_started_event );
// Loop until done reading
for( ; ; ) {
CHAR pchRequest[ IPC_MESSAGE_MAX_LENGTH ];
for( ; ; ) {
// Read client requests from the pipe. This simplistic code only allows messages
// up to IPC_MESSAGE_MAX_LENGTH characters in length.
//printf( "ipc_client_thread: reading.\n" );
DWORD cbBytesRead = 0;
// Main request-response loop. Will run until exit requested or an eror occurs
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;
while( read_pending ) {
// Set up non-blocking I/O
memset( &context->io, 0, sizeof( context->io ) );
ResetEvent( hEvent );
context->io.hEvent = hEvent;
BOOL fSuccess = ReadFile(
hPipe, // handle to pipe
pchRequest, // buffer to receive data
IPC_MESSAGE_MAX_LENGTH * sizeof( CHAR ), // size of buffer
&cbBytesRead, // number of bytes read
ResetEvent( io_event );
context->io.hEvent = io_event;
// Read client requests from the pipe in a non-blocking call
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
//printf( "ipc_client_thread: read.\n" );
if( !fSuccess && GetLastError() == ERROR_IO_PENDING ) {
if( WaitForSingleObject( hEvent, 500 ) == WAIT_TIMEOUT ) {
// Check if the Read operation is in progress (ReadFile returns FALSE and the error is ERROR_IO_PENDING )
if( !success && GetLastError() == ERROR_IO_PENDING ) {
// 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
if( WaitForSingleObject( io_event, 500 ) == WAIT_TIMEOUT ) {
CancelIoEx( context->pipe, &context->io );
continue;
continue; // Make another Read call
}
fSuccess = GetOverlappedResult(
hPipe, // handle to pipe
// The wait did not timeout, so the Read operation should now be completed (or failed)
success = GetOverlappedResult(
context->pipe, // handle to pipe
&context->io, // OVERLAPPED structure
&cbBytesRead, // bytes transferred
FALSE ); // wait
&bytes_read, // bytes transferred
FALSE ); // don't wait
}
if( !fSuccess || cbBytesRead == 0 ) {
// The read have completed (successfully or not) so exit the read loop
read_pending = false;
}
// 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) {
//printf( "ipc_client_thread: client disconnected.\n" );
//IPC_LOG( "ipc_client_thread: client disconnected.\n" );
} else {
printf( "ipc_client_thread ReadFile failed, LastError=%d.\n", GetLastError() );
IPC_LOG( "ipc_client_thread ReadFile failed, LastError=%d.\n", GetLastError() );
}
goto exit_client_thread;
}
if( context->exit_flag ) {
goto exit_client_thread;
}
break;
}
// Process the incoming message.
char pchReply[ IPC_MESSAGE_MAX_LENGTH ];
memset( pchReply, 0, sizeof( pchReply ) );
server->request_handler( pchRequest, server->user_data, pchReply, sizeof( pchReply ) );
pchReply[ IPC_MESSAGE_MAX_LENGTH - 1 ] = '\0'; // Force zero termination (truncate string)
DWORD cbReplyBytes = (DWORD)strlen(pchReply) + 1;
// Check if a server shutdown have requested this thread to be terminated, and exit if that's the case
if( context->exit_flag ) {
break;
}
// Write the reply to the pipe.
//printf( "ipc_client_thread: writing.\n" );
DWORD cbWritten = 0;
BOOL fSuccess = WriteFile(
hPipe, // handle to pipe
pchReply, // buffer to write from
cbReplyBytes, // number of bytes to write
&cbWritten, // number of bytes written
// Process the incoming message by calling the user-supplied request handler function
char response[ IPC_MESSAGE_MAX_LENGTH ];
memset( response, 0, sizeof( response ) );
context->request_handler( request, context->user_data, response, sizeof( response ) );
response[ sizeof( response ) - 1 ] = '\0'; // Force zero termination (truncate string)
// TODO: Do we need to handle this better? Log it?
DWORD response_length = (DWORD)strlen( response ) + 1;
// Write the reply to the pipe
DWORD bytes_written = 0;
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
//printf( "ipc_client_thread: writ.\n" );
if( fSuccess || GetLastError() == ERROR_IO_PENDING ) {
fSuccess = GetOverlappedResult(
hPipe, // handle to pipe
// 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 ) {
success = GetOverlappedResult(
context->pipe, // handle to pipe
&context->io, // OVERLAPPED structure
&cbWritten, // bytes transferred
FALSE); // wait
&bytes_written, // bytes transferred
TRUE ); // wait
}
if( !fSuccess || cbReplyBytes != cbWritten ) {
printf( ("ipc_client_thread WriteFile failed, LastError=%d.\n"), GetLastError());
break;
}
if( context->exit_flag ) {
// 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( ("ipc_client_thread WriteFile failed, LastError=%d.\n"), GetLastError());
break;
}
}
exit_client_thread:
//printf( "ipc_client_thread cleanup.\n" );
// 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( hEvent );
FlushFileBuffers( hPipe );
DisconnectNamedPipe( hPipe );
CloseHandle( hPipe );
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;
//printf( "ipc_client_thread exiting.\n" );
return EXIT_SUCCESS;
}
void ipc_stop_client_thread( ipc_client_thread_t* thread_context ) {
thread_context->exit_flag = 1;
CancelIoEx( thread_context->pipe, &thread_context->io );
WaitForSingleObject( thread_context->thread, INFINITE );
}
// 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_server_t* server = (ipc_server_t*) param;
// TODO: logging
//fp = fopen( "C:\\auto_update_poc\\log.txt", "w" );
//setvbuf(fp, NULL, _IONBF, 0);
// 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 ) ) {
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 ) {
FreeSid(sid);
return EXIT_FAILURE;
}
PSECURITY_DESCRIPTOR sd = (PSECURITY_DESCRIPTOR)LocalAlloc( LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH );
if( !sd ) {
FreeSid( sid );
return EXIT_FAILURE;
}
if( !InitializeSecurityDescriptor( sd, SECURITY_DESCRIPTOR_REVISION ) ) {
LocalFree(sd);
FreeSid(sid);
return EXIT_FAILURE;
}
if( !SetSecurityDescriptorDacl( sd, TRUE, acl, FALSE ) ) {
LocalFree( sd );
FreeSid( sid );
return EXIT_FAILURE;
}
SECURITY_ATTRIBUTES attribs;
attribs.nLength = sizeof( SECURITY_ATTRIBUTES );
attribs.lpSecurityDescriptor = server->sd;
attribs.lpSecurityDescriptor = sd;
attribs.bInheritHandle = -1;
// 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. It is an infinite loop.
bool event_raised = false;
// 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
bool event_raised = false; // We make sure to only raise the server-thread-is-ready event once
while( !server->exit_flag ) {
//printf( "\nPipe Server: Main thread awaiting client connection on %s\n", server->expanded_pipe_name );
// Create a pipe instance to listen for connections
server->pipe = CreateNamedPipeA(
server->expanded_pipe_name,// pipe name
PIPE_ACCESS_DUPLEX | // read/write access
FILE_FLAG_OVERLAPPED,
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
@ -410,26 +482,26 @@ DWORD WINAPI ipc_server_thread( LPVOID param ) {
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 ) {
printf( "CreateNamedPipe failed, LastError=%d.\n", GetLastError() );
IPC_LOG( "CreateNamedPipe failed, LastError=%d.\n", GetLastError() );
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
return EXIT_FAILURE;
}
// Signal to `ipc_server_start` that the server thread is now fully up and
// running and accepting connections
if( !event_raised ) {
SetEvent( server->thread_started_event );
event_raised = true;
event_raised = true; // Make sure we don't signal the event again
}
// Wait for the client to connect; if it succeeds,
// the function returns a nonzero value. If the function
// returns zero, GetLastError returns ERROR_PIPE_CONNECTED.
// 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 = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
NULL // object name
);
server->io.hEvent = io_event;
ConnectNamedPipe( server->pipe, &server->io );
if( GetLastError() == ERROR_IO_PENDING ) {
for( ; ; ) {
@ -443,48 +515,58 @@ DWORD WINAPI ipc_server_thread( LPVOID param ) {
} else if( GetLastError() != ERROR_PIPE_CONNECTED ) {
if( GetLastError() != ERROR_OPERATION_ABORTED || server->exit_flag == 0 ) {
// The client could not connect, so close the pipe.
printf( "Connection failed. LastError=%d\n",GetLastError() );
CloseHandle( server->io.hEvent );
IPC_LOG( "Connection failed. LastError=%d\n",GetLastError() );
break;
}
}
CloseHandle( server->io.hEvent );
// Check if a server shutdown have requested this thread to be terminated, and exit if that's the case
if( server->exit_flag ) {
break;
}
//printf( "Client connected, creating a processing thread.\n" );
// Create a thread for this client.
// 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 ) {
if( server->client_threads_count == MAX_CLIENT_CONNECTIONS ) {
printf( "Too many connections\n" );
if( server->client_threads_count < MAX_CLIENT_CONNECTIONS ) {
context = &server->client_threads[ server->client_threads_count++ ];
} else {
// 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( "Too many connections\n" );
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
if( server->pipe != INVALID_HANDLE_VALUE ) {
CloseHandle( server->pipe );
server->pipe = INVALID_HANDLE_VALUE;
}
CloseHandle( io_event );
return EXIT_FAILURE;
} else {
context = &server->client_threads[ server->client_threads_count++ ];
}
}
memset( context, 0, sizeof( *context ) );
context->server = server;
context->pipe = server->pipe;
server->pipe = INVALID_HANDLE_VALUE;
context->thread_started_event = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
NULL // object name
);
// Initialize the client slot
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
context->thread = CreateThread(
NULL, // no security attribute
0, // default stack size
@ -493,126 +575,117 @@ DWORD WINAPI ipc_server_thread( LPVOID param ) {
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 ) {
printf( "CreateThread failed, LastError=%d.\n", GetLastError() );
IPC_LOG( "CreateThread failed, LastError=%d.\n", GetLastError() );
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
if( server->pipe != INVALID_HANDLE_VALUE ) {
CloseHandle( server->pipe );
server->pipe = INVALID_HANDLE_VALUE;
}
CloseHandle( io_event );
return EXIT_FAILURE;
}
//printf( "Waiting for thread\n" );
WaitForSingleObject( context->thread_started_event, INFINITE );
}
// 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 );
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 ) {
// 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;
if( !expand_pipe_name( pipe_name, server->expanded_pipe_name, sizeof( server->expanded_pipe_name ) ) ) {
printf( "Pipe name too long\n" );
free( server );
return NULL;
}
// Create security attribs
SID_IDENTIFIER_AUTHORITY auth = { SECURITY_WORLD_SID_AUTHORITY };
if( !AllocateAndInitializeSid( &auth, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &server->sid ) ) {
free( server );
return NULL;
}
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)server->sid;
if( SetEntriesInAcl(1, &access, NULL, &server->acl) != ERROR_SUCCESS ) {
FreeSid( server->sid );
free( server );
return NULL;
}
server->sd = (PSECURITY_DESCRIPTOR)LocalAlloc( LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH );
if( !server->sd ) {
FreeSid( server->sid );
free( server );
return NULL;
}
if( !InitializeSecurityDescriptor( server->sd, SECURITY_DESCRIPTOR_REVISION ) ) {
LocalFree(server->sd);
FreeSid( server->sid );
free( server );
return NULL;
}
if( !SetSecurityDescriptorDacl( server->sd, TRUE, server->acl, FALSE ) ) {
LocalFree( server->sd );
FreeSid( server->sid );
free( server );
return NULL;
}
server->request_handler = request_handler;
server->user_data = user_data;
// Expand the pipe name to the valid form eg. "\\.\pipe\name"
if( !expand_pipe_name( pipe_name, server->expanded_pipe_name, sizeof( server->expanded_pipe_name ) ) ) {
IPC_LOG( "Pipe name too long\n" );
free( server );
return NULL;
}
// 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
);
DWORD threadId = 0;
// Start the server thread which accepts connections and starts dedicated client threads for each new connection
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
&threadId ); // returns the thread identifier
NULL ); // returns the thread identifier
// If thread creation failed, return error
if( server->thread == NULL ) {
printf( "Failed to create server thread\n" );
LocalFree( server->acl );
LocalFree( server->sd );
FreeSid( server->sid );
IPC_LOG( "Failed to create server thread\n" );
CloseHandle( server->thread_started_event );
free( server );
return NULL;
}
// Wait for the server thread to be up and running and accepting connections
if( WaitForSingleObject( server->thread_started_event, 10000 ) != WAIT_OBJECT_0 ) {
printf( "Timeout waiting for client thread to start\n" );
LocalFree( server->acl );
LocalFree( server->sd );
FreeSid( server->sid );
// 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( "Timeout waiting for client thread to start\n" );
CloseHandle( server->thread_started_event );
TerminateThread( server->thread, EXIT_FAILURE );
free( server );
return NULL;
}
// 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 ) {
server->exit_flag = 1;
server->exit_flag = 1; // Signal server thread top stop
if( server->pipe != INVALID_HANDLE_VALUE ) {
CancelIoEx( server->pipe, &server->io );
CancelIoEx( server->pipe, &server->io ); // Cancel pending ConnectNamedPipe operatios, if any
}
WaitForSingleObject( server->thread, INFINITE );
LocalFree( server->acl );
LocalFree( server->sd );
FreeSid( server->sid );
WaitForSingleObject( server->thread, INFINITE ); // Wait for server thread to exit
// Loop over all clients and terminate each one
for( int i = 0; i < server->client_threads_count; ++i ) {
if( !server->client_threads[ i ].recycle ) {
ipc_stop_client_thread( &server->client_threads[ 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 );
}

View File

@ -1,8 +1,17 @@
REM runs the tests in an infinite loop
REM intended to be run manually and left running for a long time,
REM as a final sanity check to make sure the tests are stable
REM will exit if any tests fail
@echo off
echo runs the tests in an infinite loop
echo intended to be run manually and left running for a long time,
echo as a final sanity check to make sure the tests are stable
echo will exit if any tests fail
set started=%date% %time%
:loop
echo INITIATED AT %started%
echo CURRENTLY AT %date% %time%
call npm run test
if %ERRORLEVEL% NEQ 0 goto :eof
if %ERRORLEVEL% NEQ 0 (
echo.
echo INITIATED AT %started%
echo TERMINATED AT %date% %time%
goto :eof
)
goto :loop