diff --git a/auto_update/ipc.h b/auto_update/ipc.h index ee561f20..caa63b21 100644 --- a/auto_update/ipc.h +++ b/auto_update/ipc.h @@ -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 ) { + + ipc_client_thread_t* context = (ipc_client_thread_t*) param; - // Print verbose messages. In production code, this should be for debugging only. - //printf( "ipc_client_thread created, receiving and processing messages.\n" ); - - // 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( - NULL, // default security attributes - TRUE, // manual-reset event - FALSE, // initial state is nonsignaled - NULL // object name + // 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 - &context->io ); // overlapped I/O - //printf( "ipc_client_thread: read.\n" ); - - if( !fSuccess && GetLastError() == ERROR_IO_PENDING ) { - if( WaitForSingleObject( hEvent, 500 ) == WAIT_TIMEOUT ) { + 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 + + // 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 - &context->io, // OVERLAPPED structure - &cbBytesRead, // bytes transferred - FALSE ); // wait + + // 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 + &bytes_read, // bytes transferred + FALSE ); // don't wait } + + // The read have completed (successfully or not) so exit the read loop + read_pending = false; + } - if( !fSuccess || cbBytesRead == 0 ) { - if (GetLastError() == ERROR_BROKEN_PIPE) { - //printf( "ipc_client_thread: client disconnected.\n" ); - } else { - printf( "ipc_client_thread ReadFile failed, LastError=%d.\n", GetLastError() ); - } - goto exit_client_thread; + // 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( "ipc_client_thread: client disconnected.\n" ); + } else { + IPC_LOG( "ipc_client_thread ReadFile failed, LastError=%d.\n", GetLastError() ); } - - 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; - - // 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 - &context->io ); // overlapped I/O - - //printf( "ipc_client_thread: writ.\n" ); - if( fSuccess || GetLastError() == ERROR_IO_PENDING ) { - fSuccess = GetOverlappedResult( - hPipe, // handle to pipe - &context->io, // OVERLAPPED structure - &cbWritten, // bytes transferred - FALSE); // wait - } - - if( !fSuccess || cbReplyBytes != cbWritten ) { - printf( ("ipc_client_thread WriteFile failed, LastError=%d.\n"), GetLastError()); break; } + // Check if a server shutdown have requested this thread to be terminated, and exit if that's the case if( context->exit_flag ) { break; } + + // 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 + + // 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 + &bytes_written, // bytes transferred + TRUE ); // wait + } + + // 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, + 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 @@ -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 + 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 + 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 ); } diff --git a/auto_update/run_loop.bat b/auto_update/run_loop.bat index ba203905..88a7b91e 100644 --- a/auto_update/run_loop.bat +++ b/auto_update/run_loop.bat @@ -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 \ No newline at end of file