From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-02-17 21:50:28
From: Jeff Hostetler <redacted>
Brief design documentation for new IPC mechanism allowing
foreground Git client to talk with an existing daemon process
at a known location using a named pipe or unix domain socket.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
Documentation/technical/api-simple-ipc.txt | 34 ++++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 Documentation/technical/api-simple-ipc.txt
@@ -0,0 +1,34 @@+simple-ipc API+==============++The simple-ipc API is used to send an IPC message and response between+a (presumably) foreground Git client process to a background server or+daemon process. The server process must already be running. Multiple+client processes can simultaneously communicate with the server+process.++Communication occurs over a named pipe on Windows and a Unix domain+socket on other platforms. Clients and the server rendezvous at a+previously agreed-to application-specific pathname (which is outside+the scope of this design).++This IPC mechanism differs from the existing `sub-process.c` model+(Documentation/technical/long-running-process-protocol.txt) and used+by applications like Git-LFS. In the simple-ipc model the server is+assumed to be a very long-running system service. In contrast, in the+LFS-style sub-process model the helper is started with the foreground+process and exits when the foreground process terminates.++How the simple-ipc server is started is also outside the scope of the+IPC mechanism. For example, the server might be started during+maintenance operations.++The IPC protocol consists of a single request message from the client and+an optional request message from the server. For simplicity, pkt-line+routines are used to hide chunking and buffering concerns. Each side+terminates their message with a flush packet.+(Documentation/technical/protocol-common.txt)++The actual format of the client and server messages is application+specific. The IPC layer transmits and receives an opaque buffer without+any concern for the content within.
@@ -0,0 +1,749 @@+#include"cache.h"+#include"simple-ipc.h"+#include"strbuf.h"+#include"pkt-line.h"+#include"thread-utils.h"++#ifndef GIT_WINDOWS_NATIVE+#error This file can only be compiled on Windows+#endif++staticintinitialize_pipe_name(constchar*path,wchar_t*wpath,size_talloc)+{+intoff=0;+structstrbufrealpath=STRBUF_INIT;++if(!strbuf_realpath(&realpath,path,0))+return-1;++off=swprintf(wpath,alloc,L"\\\\.\\pipe\\");+if(xutftowcs(wpath+off,realpath.buf,alloc-off)<0)+return-1;++/* Handle drive prefix */+if(wpath[off]&&wpath[off+1]==L':'){+wpath[off+1]=L'_';+off+=2;+}++for(;wpath[off];off++)+if(wpath[off]==L'/')+wpath[off]=L'\\';++strbuf_release(&realpath);+return0;+}++staticenumipc_active_stateget_active_state(wchar_t*pipe_path)+{+if(WaitNamedPipeW(pipe_path,NMPWAIT_USE_DEFAULT_WAIT))+returnIPC_STATE__LISTENING;++if(GetLastError()==ERROR_SEM_TIMEOUT)+returnIPC_STATE__NOT_LISTENING;++if(GetLastError()==ERROR_FILE_NOT_FOUND)+returnIPC_STATE__PATH_NOT_FOUND;++returnIPC_STATE__OTHER_ERROR;+}++enumipc_active_stateipc_get_active_state(constchar*path)+{+wchar_tpipe_path[MAX_PATH];++if(initialize_pipe_name(path,pipe_path,ARRAY_SIZE(pipe_path))<0)+returnIPC_STATE__INVALID_PATH;++returnget_active_state(pipe_path);+}++#define WAIT_STEP_MS (50)++staticenumipc_active_stateconnect_to_server(+constwchar_t*wpath,+DWORDtimeout_ms,+conststructipc_client_connect_options*options,+int*pfd)+{+DWORDt_start_ms,t_waited_ms;+DWORDstep_ms;+HANDLEhPipe=INVALID_HANDLE_VALUE;+DWORDmode=PIPE_READMODE_BYTE;+DWORDgle;++*pfd=-1;++for(;;){+hPipe=CreateFileW(wpath,GENERIC_READ|GENERIC_WRITE,+0,NULL,OPEN_EXISTING,0,NULL);+if(hPipe!=INVALID_HANDLE_VALUE)+break;++gle=GetLastError();++switch(gle){+caseERROR_FILE_NOT_FOUND:+if(!options->wait_if_not_found)+returnIPC_STATE__PATH_NOT_FOUND;+if(!timeout_ms)+returnIPC_STATE__PATH_NOT_FOUND;++step_ms=(timeout_ms<WAIT_STEP_MS)?+timeout_ms:WAIT_STEP_MS;+sleep_millisec(step_ms);++timeout_ms-=step_ms;+break;/* try again */++caseERROR_PIPE_BUSY:+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;+if(!timeout_ms)+returnIPC_STATE__NOT_LISTENING;++t_start_ms=(DWORD)(getnanotime()/1000000);++if(!WaitNamedPipeW(wpath,timeout_ms)){+if(GetLastError()==ERROR_SEM_TIMEOUT)+returnIPC_STATE__NOT_LISTENING;++returnIPC_STATE__OTHER_ERROR;+}++/*+*Apipeserverinstancebecameavailable.+*Raceotherclientprocessestoconnectto+*it.+*+*Butfirstdecrementouroveralltimeoutso+*thatwedon'tstarveifwekeeplosingthe+*race.Butalsoguardagainstspecial+*NPMWAIT_values(0and-1).+*/+t_waited_ms=(DWORD)(getnanotime()/1000000)-t_start_ms;+if(t_waited_ms<timeout_ms)+timeout_ms-=t_waited_ms;+else+timeout_ms=1;+break;/* try again */++default:+returnIPC_STATE__OTHER_ERROR;+}+}++if(!SetNamedPipeHandleState(hPipe,&mode,NULL,NULL)){+CloseHandle(hPipe);+returnIPC_STATE__OTHER_ERROR;+}++*pfd=_open_osfhandle((intptr_t)hPipe,O_RDWR|O_BINARY);+if(*pfd<0){+CloseHandle(hPipe);+returnIPC_STATE__OTHER_ERROR;+}++/* fd now owns hPipe */++returnIPC_STATE__LISTENING;+}++/*+*ThedefaultconnectiontimeoutforWindowsclients.+*+*Thisisnotcurrentlypartoftheipc_API(northeconfigsettings)+*becauseofdifferencesbetweenWindowsandotherplatforms.+*+*Thisvaluewaschosenatrandom.+*/+#define WINDOWS_CONNECTION_TIMEOUT_MS (30000)++enumipc_active_stateipc_client_try_connect(+constchar*path,+conststructipc_client_connect_options*options,+structipc_client_connection**p_connection)+{+wchar_twpath[MAX_PATH];+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+intfd=-1;++*p_connection=NULL;++trace2_region_enter("ipc-client","try-connect",NULL);+trace2_data_string("ipc-client",NULL,"try-connect/path",path);++if(initialize_pipe_name(path,wpath,ARRAY_SIZE(wpath))<0)+state=IPC_STATE__INVALID_PATH;+else+state=connect_to_server(wpath,WINDOWS_CONNECTION_TIMEOUT_MS,+options,&fd);++trace2_data_intmax("ipc-client",NULL,"try-connect/state",+(intmax_t)state);+trace2_region_leave("ipc-client","try-connect",NULL);++if(state==IPC_STATE__LISTENING){+(*p_connection)=xcalloc(1,sizeof(structipc_client_connection));+(*p_connection)->fd=fd;+}++returnstate;+}++voidipc_client_close_connection(structipc_client_connection*connection)+{+if(!connection)+return;++if(connection->fd!=-1)+close(connection->fd);++free(connection);+}++intipc_client_send_command_to_connection(+structipc_client_connection*connection,+constchar*message,structstrbuf*answer)+{+intret=0;++strbuf_setlen(answer,0);++trace2_region_enter("ipc-client","send-command",NULL);++if(write_packetized_from_buf_no_flush(message,strlen(message),+connection->fd)<0||+packet_flush_gently(connection->fd)<0){+ret=error(_("could not send IPC command"));+gotodone;+}++FlushFileBuffers((HANDLE)_get_osfhandle(connection->fd));++if(read_packetized_to_strbuf(+connection->fd,answer,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_NEVER_DIE)<0){+ret=error(_("could not read IPC response"));+gotodone;+}++done:+trace2_region_leave("ipc-client","send-command",NULL);+returnret;+}++intipc_client_send_command(constchar*path,+conststructipc_client_connect_options*options,+constchar*message,structstrbuf*response)+{+intret=-1;+enumipc_active_statestate;+structipc_client_connection*connection=NULL;++state=ipc_client_try_connect(path,options,&connection);++if(state!=IPC_STATE__LISTENING)+returnret;++ret=ipc_client_send_command_to_connection(connection,message,response);++ipc_client_close_connection(connection);++returnret;+}++/*+*Duplicatethegivenpipehandleandwrapitinafiledescriptorso+*thatwecanusepkt-lineonit.+*/+staticintdup_fd_from_pipe(constHANDLEpipe)+{+HANDLEprocess=GetCurrentProcess();+HANDLEhandle;+intfd;++if(!DuplicateHandle(process,pipe,process,&handle,0,FALSE,+DUPLICATE_SAME_ACCESS)){+errno=err_win_to_posix(GetLastError());+return-1;+}++fd=_open_osfhandle((intptr_t)handle,O_RDWR|O_BINARY);+if(fd<0){+errno=err_win_to_posix(GetLastError());+CloseHandle(handle);+return-1;+}++/*+*`handle`isnowownedby`fd`andwillbeautomaticallyclosed+*whenthedescriptorisclosed.+*/++returnfd;+}++/*+*Magicnumbersusedtoannotatecallbackinstancedata.+*Theseareusedtohelpguardagainstaccidentallypassingthe+*wronginstancedataacrossmultiplelevelsofcallbacks(which+*iseasytodoifthereare`void*`arguments).+*/+enummagic{+MAGIC_SERVER_REPLY_DATA,+MAGIC_SERVER_THREAD_DATA,+MAGIC_SERVER_DATA,+};++structipc_server_reply_data{+enummagicmagic;+intfd;+structipc_server_thread_data*server_thread_data;+};++structipc_server_thread_data{+enummagicmagic;+structipc_server_thread_data*next_thread;+structipc_server_data*server_data;+pthread_tpthread_id;+HANDLEhPipe;+};++/*+*OnWindows,theconceptual"ipc-server"isimplementedasapoolof+*nidential/peer"server-thread"threads.Thatis,thereisno+*hierarchyofthreads;andthereforenocontrollerthreadmanaging+*thepool.Eachthreadhasanindependenthandletothenamedpipe,+*receivesincomingconnections,processestheclient,andre-uses+*thepipeforthenextclientconnection.+*+*Therefore,the"ipc-server"onlyneedstomaintainalistofthe+*spawnedthreadsforeventual"join"purposes.+*+*Asingle"stop-event"isvisibletoalloftheserverthreadsto+*tellthemtoshutdown(whenidle).+*/+structipc_server_data{+enummagicmagic;+ipc_server_application_cb*application_cb;+void*application_data;+structstrbufbuf_path;+wchar_twpath[MAX_PATH];++HANDLEhEventStopRequested;+structipc_server_thread_data*thread_list;+intis_stopped;+};++enumconnect_result{+CR_CONNECTED=0,+CR_CONNECT_PENDING,+CR_CONNECT_ERROR,+CR_WAIT_ERROR,+CR_SHUTDOWN,+};++staticenumconnect_resultqueue_overlapped_connect(+structipc_server_thread_data*server_thread_data,+OVERLAPPED*lpo)+{+if(ConnectNamedPipe(server_thread_data->hPipe,lpo))+gotofailed;++switch(GetLastError()){+caseERROR_IO_PENDING:+returnCR_CONNECT_PENDING;++caseERROR_PIPE_CONNECTED:+SetEvent(lpo->hEvent);+returnCR_CONNECTED;++default:+break;+}++failed:+error(_("ConnectNamedPipe failed for '%s' (%lu)"),+server_thread_data->server_data->buf_path.buf,+GetLastError());+returnCR_CONNECT_ERROR;+}++/*+*UseWindowsOverlappedIOtowaitforaconnectionorforourevent+*tobesignalled.+*/+staticenumconnect_resultwait_for_connection(+structipc_server_thread_data*server_thread_data,+OVERLAPPED*lpo)+{+enumconnect_resultr;+HANDLEwaitHandles[2];+DWORDdwWaitResult;++r=queue_overlapped_connect(server_thread_data,lpo);+if(r!=CR_CONNECT_PENDING)+returnr;++waitHandles[0]=server_thread_data->server_data->hEventStopRequested;+waitHandles[1]=lpo->hEvent;++dwWaitResult=WaitForMultipleObjects(2,waitHandles,FALSE,INFINITE);+switch(dwWaitResult){+caseWAIT_OBJECT_0+0:+returnCR_SHUTDOWN;++caseWAIT_OBJECT_0+1:+ResetEvent(lpo->hEvent);+returnCR_CONNECTED;++default:+returnCR_WAIT_ERROR;+}+}++/*+*Forwarddeclareourreplycallbackfunctionsothatanycompiler+*errorsarereportedwhenweactuallydefinethefunction(inaddition+*toanyerrorsreportedwhenwetrytopassthiscallbackfunctionas+*aparameterinafunctioncall).Theformerareeasiertounderstand.+*/+staticipc_server_reply_cbdo_io_reply_callback;++/*+*Relayapplication'sresponsemessagetotheclientprocess.+*(Wedonotflushatthispointbecauseweallowthecaller+*tochunkdatatotheclientthruus.)+*/+staticintdo_io_reply_callback(structipc_server_reply_data*reply_data,+constchar*response,size_tresponse_len)+{+if(reply_data->magic!=MAGIC_SERVER_REPLY_DATA)+BUG("reply_cb called with wrong instance data");++returnwrite_packetized_from_buf_no_flush(response,response_len,+reply_data->fd);+}++/*+*Receivetherequest/commandfromtheclientandpassittothe+*registeredrequest-callback.Therequest-callbackwillcompose+*aresponseandcallourreply-callbacktosendittotheclient.+*+*Simple-IPConlycontainsoneroundtrip,soweflushandclose+*hereaftertheresponse.+*/+staticintdo_io(structipc_server_thread_data*server_thread_data)+{+structstrbufbuf=STRBUF_INIT;+structipc_server_reply_datareply_data;+intret=0;++reply_data.magic=MAGIC_SERVER_REPLY_DATA;+reply_data.server_thread_data=server_thread_data;++reply_data.fd=dup_fd_from_pipe(server_thread_data->hPipe);+if(reply_data.fd<0)+returnerror(_("could not create fd from pipe for '%s'"),+server_thread_data->server_data->buf_path.buf);++ret=read_packetized_to_strbuf(+reply_data.fd,&buf,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_NEVER_DIE);+if(ret>=0){+ret=server_thread_data->server_data->application_cb(+server_thread_data->server_data->application_data,+buf.buf,do_io_reply_callback,&reply_data);++packet_flush_gently(reply_data.fd);++FlushFileBuffers((HANDLE)_get_osfhandle((reply_data.fd)));+}+else{+/*+*Theclientprobablydisconnected/shutdownbeforeit+*couldsendawell-formedmessage.Ignoreit.+*/+}++strbuf_release(&buf);+close(reply_data.fd);++returnret;+}++/*+*HandleIPCrequestandresponsewiththisconnectedclient.Andreset+*thepipetoprepareforthenextclient.+*/+staticintuse_connection(structipc_server_thread_data*server_thread_data)+{+intret;++ret=do_io(server_thread_data);++FlushFileBuffers(server_thread_data->hPipe);+DisconnectNamedPipe(server_thread_data->hPipe);++returnret;+}++/*+*ThreadprocforanIPCserverworkerthread.Ithandlesaseriesof+*connectionsfromclients.ItcleansandreusesthehPipebetweeneach+*client.+*/+staticvoid*server_thread_proc(void*_server_thread_data)+{+structipc_server_thread_data*server_thread_data=_server_thread_data;+HANDLEhEventConnected=INVALID_HANDLE_VALUE;+OVERLAPPEDoConnect;+enumconnect_resultcr;+intret;++assert(server_thread_data->hPipe!=INVALID_HANDLE_VALUE);++trace2_thread_start("ipc-server");+trace2_data_string("ipc-server",NULL,"pipe",+server_thread_data->server_data->buf_path.buf);++hEventConnected=CreateEventW(NULL,TRUE,FALSE,NULL);++memset(&oConnect,0,sizeof(oConnect));+oConnect.hEvent=hEventConnected;++for(;;){+cr=wait_for_connection(server_thread_data,&oConnect);++switch(cr){+caseCR_SHUTDOWN:+gotofinished;++caseCR_CONNECTED:+ret=use_connection(server_thread_data);+if(ret==SIMPLE_IPC_QUIT){+ipc_server_stop_async(+server_thread_data->server_data);+gotofinished;+}+if(ret>0){+/*+*Ignore(transient)IOerrorswiththis+*clientandresetforthenextclient.+*/+}+break;++caseCR_CONNECT_PENDING:+/* By construction, this should not happen. */+BUG("ipc-server[%s]: unexpeced CR_CONNECT_PENDING",+server_thread_data->server_data->buf_path.buf);++caseCR_CONNECT_ERROR:+caseCR_WAIT_ERROR:+/*+*Ignorethesetheoreticalerrors.+*/+DisconnectNamedPipe(server_thread_data->hPipe);+break;++default:+BUG("unandled case after wait_for_connection");+}+}++finished:+CloseHandle(server_thread_data->hPipe);+CloseHandle(hEventConnected);++trace2_thread_exit();+returnNULL;+}++staticHANDLEcreate_new_pipe(wchar_t*wpath,intis_first)+{+HANDLEhPipe;+DWORDdwOpenMode,dwPipeMode;+LPSECURITY_ATTRIBUTESlpsa=NULL;++dwOpenMode=PIPE_ACCESS_INBOUND|PIPE_ACCESS_OUTBOUND|+FILE_FLAG_OVERLAPPED;++dwPipeMode=PIPE_TYPE_MESSAGE|PIPE_READMODE_BYTE|PIPE_WAIT|+PIPE_REJECT_REMOTE_CLIENTS;++if(is_first){+dwOpenMode|=FILE_FLAG_FIRST_PIPE_INSTANCE;++/*+*OnWindows,thefirstserverpipeinstancegetsto+*settheACL/SecurityAttributesonthenamed+*pipe;subsequentinstancesinheritandcannot+*changethem.+*+*TODOShouldweallowtheapplicationlayerto+*specifysecurityattributes,suchas`LocalService`+*or`LocalSystem`,whenwecreatethenamedpipe?+*Thisquestionisprobablynotimportantwhenthe+*daemonisstartedbyaforegrounduserprocessand+*onlyneedstotalktothecurrentuser,butmaybe+*ifthedaemonisrunviatheControlPanelasa+*SystemService.+*/+}++hPipe=CreateNamedPipeW(wpath,dwOpenMode,dwPipeMode,+PIPE_UNLIMITED_INSTANCES,1024,1024,0,lpsa);++returnhPipe;+}++intipc_server_run_async(structipc_server_data**returned_server_data,+constchar*path,conststructipc_server_opts*opts,+ipc_server_application_cb*application_cb,+void*application_data)+{+structipc_server_data*server_data;+wchar_twpath[MAX_PATH];+HANDLEhPipeFirst=INVALID_HANDLE_VALUE;+intk;+intret=0;+intnr_threads=opts->nr_threads;++*returned_server_data=NULL;++ret=initialize_pipe_name(path,wpath,ARRAY_SIZE(wpath));+if(ret<0)+returnerror(+_("could not create normalized wchar_t path for '%s'"),+path);++hPipeFirst=create_new_pipe(wpath,1);+if(hPipeFirst==INVALID_HANDLE_VALUE)+returnerror(_("IPC server already running on '%s'"),path);++server_data=xcalloc(1,sizeof(*server_data));+server_data->magic=MAGIC_SERVER_DATA;+server_data->application_cb=application_cb;+server_data->application_data=application_data;+server_data->hEventStopRequested=CreateEvent(NULL,TRUE,FALSE,NULL);+strbuf_init(&server_data->buf_path,0);+strbuf_addstr(&server_data->buf_path,path);+wcscpy(server_data->wpath,wpath);++if(nr_threads<1)+nr_threads=1;++for(k=0;k<nr_threads;k++){+structipc_server_thread_data*std;++std=xcalloc(1,sizeof(*std));+std->magic=MAGIC_SERVER_THREAD_DATA;+std->server_data=server_data;+std->hPipe=INVALID_HANDLE_VALUE;++std->hPipe=(k==0)+?hPipeFirst+:create_new_pipe(server_data->wpath,0);++if(std->hPipe==INVALID_HANDLE_VALUE){+/*+*Ifwe'vereachedapipeinstancelimitfor+*thispath,justusefewerthreads.+*/+free(std);+break;+}++if(pthread_create(&std->pthread_id,NULL,+server_thread_proc,std)){+/*+*Likewise,ifwe'reoutofthreads,justuse+*fewerthreadsthanrequested.+*+*However,wejustgiveupifwecan'tevenget+*onethread.Thisshouldnothappen.+*/+if(k==0)+die(_("could not start thread[0] for '%s'"),+path);++CloseHandle(std->hPipe);+free(std);+break;+}++std->next_thread=server_data->thread_list;+server_data->thread_list=std;+}++*returned_server_data=server_data;+return0;+}++intipc_server_stop_async(structipc_server_data*server_data)+{+if(!server_data)+return0;++/*+*Gentlytellalloftheipc_serverthreadstoshutdown.+*Thiswillbeseenthenexttimetheyareidle(andwaiting+*foraconnection).+*+*WeDONOTattempttoforcethemtodropanactiveconnection.+*/+SetEvent(server_data->hEventStopRequested);+return0;+}++intipc_server_await(structipc_server_data*server_data)+{+DWORDdwWaitResult;++if(!server_data)+return0;++dwWaitResult=WaitForSingleObject(server_data->hEventStopRequested,INFINITE);+if(dwWaitResult!=WAIT_OBJECT_0)+returnerror(_("wait for hEvent failed for '%s'"),+server_data->buf_path.buf);++while(server_data->thread_list){+structipc_server_thread_data*std=server_data->thread_list;++pthread_join(std->pthread_id,NULL);++server_data->thread_list=std->next_thread;+free(std);+}++server_data->is_stopped=1;++return0;+}++voidipc_server_free(structipc_server_data*server_data)+{+if(!server_data)+return;++if(!server_data->is_stopped)+BUG("cannot free ipc-server while running for '%s'",+server_data->buf_path.buf);++strbuf_release(&server_data->buf_path);++if(server_data->hEventStopRequested!=INVALID_HANDLE_VALUE)+CloseHandle(server_data->hEventStopRequested);++while(server_data->thread_list){+structipc_server_thread_data*std=server_data->thread_list;++server_data->thread_list=std->next_thread;+free(std);+}++free(server_data);+}
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-02-17 21:50:34
From: Jeff Hostetler <redacted>
The static helper function `unix_stream_socket()` calls `die()`. This
is not appropriate for all callers. Eliminate the wrapper function
and make the callers propagate the error.
Signed-off-by: Jeff Hostetler <redacted>
---
unix-socket.c | 27 +++++++++++++--------------
1 file changed, 13 insertions(+), 14 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-02-17 21:50:51
From: Jeff Hostetler <redacted>
Update `unix_stream_listen()` to take an options structure to override
default behaviors. This commit includes the size of the `listen()` backlog.
Signed-off-by: Jeff Hostetler <redacted>
---
builtin/credential-cache--daemon.c | 3 ++-
unix-socket.c | 9 +++++++--
unix-socket.h | 14 +++++++++++++-
3 files changed, 22 insertions(+), 4 deletions(-)
@@ -203,9 +203,10 @@ static int serve_cache_loop(int fd)staticvoidserve_cache(constchar*socket_path,intdebug){+structunix_stream_listen_optsopts=UNIX_STREAM_LISTEN_OPTS_INIT;intfd;-fd=unix_stream_listen(socket_path);+fd=unix_stream_listen(socket_path,&opts);if(fd<0)die_errno("unable to bind to '%s'",socket_path);
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-02-17 21:50:55
From: Jeff Hostetler <redacted>
Calls to `chdir()` are dangerous in a multi-threaded context. If
`unix_stream_listen()` or `unix_stream_connect()` is given a socket
pathname that is too long to fit in a `sockaddr_un` structure, it will
`chdir()` to the parent directory of the requested socket pathname,
create the socket using a relative pathname, and then `chdir()` back.
This is not thread-safe.
Teach `unix_sockaddr_init()` to not allow calls to `chdir()` when this
flag is set.
Signed-off-by: Jeff Hostetler <redacted>
---
builtin/credential-cache.c | 2 +-
unix-socket.c | 17 ++++++++++++-----
unix-socket.h | 4 +++-
3 files changed, 16 insertions(+), 7 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-02-17 21:51:02
From: Jeff Hostetler <redacted>
Create a version of `unix_stream_listen()` that uses a ".lock" lockfile
to create the unix domain socket in a race-free manner.
Unix domain sockets have a fundamental problem on Unix systems because
they persist in the filesystem until they are deleted. This is
independent of whether a server is actually listening for connections.
Well-behaved servers are expected to delete the socket when they
shutdown. A new server cannot easily tell if a found socket is
attached to an active server or is leftover cruft from a dead server.
The traditional solution used by `unix_stream_listen()` is to force
delete the socket pathname and then create a new socket. This solves
the latter (cruft) problem, but in the case of the former, it orphans
the existing server (by stealing the pathname associated with the
socket it is listening on).
We cannot directly use a .lock lockfile to create the socket because
the socket is created by `bind(2)` rather than the `open(2)` mechanism
used by `tempfile.c`.
As an alternative, we hold a plain lockfile ("<path>.lock") as a
mutual exclusion device. Under the lock, we test if an existing
socket ("<path>") is has an active server. If not, create a new
socket and begin listening. Then we rollback the lockfile in all
cases.
Signed-off-by: Jeff Hostetler <redacted>
---
unix-socket.c | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++
unix-socket.h | 29 +++++++++++++
2 files changed, 144 insertions(+)
@@ -132,3 +133,117 @@ int unix_stream_listen(const char *path,errno=saved_errno;return-1;}++staticintis_another_server_alive(constchar*path,+conststructunix_stream_listen_opts*opts)+{+structstatst;+intfd;++if(!lstat(path,&st)&&S_ISSOCK(st.st_mode)){+/*+*Asocket-inodeexistsondiskat`path`,butwe+*don'tknowwhetheritbelongstoanactiveserver+*orwhetherthelastserverdiedwithoutcleaning+*up.+*+*Pokeitwithatrivialconnectiontotrytofind+*out.+*/+fd=unix_stream_connect(path,opts->disallow_chdir);+if(fd>=0){+close(fd);+return1;+}+}++return0;+}++structunix_stream_server_socket*unix_stream_server__listen_with_lock(+constchar*path,+conststructunix_stream_listen_opts*opts)+{+structlock_filelock=LOCK_INIT;+intfd_socket;+structunix_stream_server_socket*server_socket;++/*+*Createalockat"<path>.lock"ifwecan.+*/+if(hold_lock_file_for_update_timeout(&lock,path,0,+opts->timeout_ms)<0){+error_errno(_("could not lock listener socket '%s'"),path);+returnNULL;+}++/*+*Ifanotherserverislisteningon"<path>"giveup.Wedonot+*wanttocreateasocketandstealfutureconnectionsfromthem.+*/+if(is_another_server_alive(path,opts)){+errno=EADDRINUSE;+error_errno(_("listener socket already in use '%s'"),path);+rollback_lock_file(&lock);+returnNULL;+}++/*+*CreateandbindtoaUnixdomainsocketat"<path>".+*/+fd_socket=unix_stream_listen(path,opts);+if(fd_socket<0){+error_errno(_("could not create listener socket '%s'"),path);+rollback_lock_file(&lock);+returnNULL;+}++server_socket=xcalloc(1,sizeof(*server_socket));+server_socket->path_socket=strdup(path);+server_socket->fd_socket=fd_socket;+lstat(path,&server_socket->st_socket);++/*+*Alwaysrollback(justdelete)"<path>.lock"becausewealreadycreated+*"<path>"asasocketanddonotwanttocommit_locktodotheatomic+*renametrick.+*/+rollback_lock_file(&lock);++returnserver_socket;+}++voidunix_stream_server__free(+structunix_stream_server_socket*server_socket)+{+if(!server_socket)+return;++if(server_socket->fd_socket>=0){+if(!unix_stream_server__was_stolen(server_socket))+unlink(server_socket->path_socket);+close(server_socket->fd_socket);+}++free(server_socket->path_socket);+free(server_socket);+}++intunix_stream_server__was_stolen(+structunix_stream_server_socket*server_socket)+{+structstatst_now;++if(!server_socket)+return0;++if(lstat(server_socket->path_socket,&st_now)==-1)+return1;++if(st_now.st_ino!=server_socket->st_socket.st_ino)+return1;++/* We might also consider the ctime on some platforms. */++return0;+}
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-02-17 21:51:10
From: Jeff Hostetler <redacted>
Create t0052-simple-ipc.sh with unit tests for the "simple-ipc" mechanism.
Create t/helper/test-simple-ipc test tool to exercise the "simple-ipc"
functions.
When the tool is invoked with "run-daemon", it runs a server to listen
for "simple-ipc" connections on a test socket or named pipe and
responds to a set of commands to exercise/stress the communication
setup.
When the tool is invoked with "start-daemon", it spawns a "run-daemon"
command in the background and waits for the server to become ready
before exiting. (This helps make unit tests in t0052 more predictable
and avoids the need for arbitrary sleeps in the test script.)
The tool also has a series of client "send" commands to send commands
and data to a server instance.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 1 +
t/helper/test-simple-ipc.c | 773 +++++++++++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/t0052-simple-ipc.sh | 122 ++++++
5 files changed, 898 insertions(+)
create mode 100644 t/helper/test-simple-ipc.c
create mode 100755 t/t0052-simple-ipc.sh
@@ -0,0 +1,773 @@+/*+*test-simple-ipc.c:verifythattheInter-ProcessCommunicationworks.+*/++#include"test-tool.h"+#include"cache.h"+#include"strbuf.h"+#include"simple-ipc.h"+#include"parse-options.h"+#include"thread-utils.h"+#include"strvec.h"++#ifndef SUPPORTS_SIMPLE_IPC+intcmd__simple_ipc(intargc,constchar**argv)+{+die("simple IPC not available on this platform");+}+#else++/*+*Thetestdaemondefinesan"application callback"thatsupportsa+*seriesofcommands(see`test_app_cb()`).+*+*Unknowncommandsarecaughthereandwesendanerrormessageback+*totheclientprocess.+*/+staticintapp__unhandled_command(constchar*command,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+intret;++strbuf_addf(&buf,"unhandled command: %s",command);+ret=reply_cb(reply_data,buf.buf,buf.len);+strbuf_release(&buf);++returnret;+}++/*+*Replywithasingleverylargebuffer.Thisistoensurethat+*longresponseareproperlyhandled--whetherthechunkingoccurs+*inthekernelorinthe(probablypkt-line)layer.+*/+#define BIG_ROWS (10000)+staticintapp__big_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<BIG_ROWS;row++)+strbuf_addf(&buf,"big: %.75d\n",row);++ret=reply_cb(reply_data,buf.buf,buf.len);+strbuf_release(&buf);++returnret;+}++/*+*Replywithaseriesoflines.Thisistoensurethatwecanincrementally+*computetheresponseandchunkittotheclient.+*/+#define CHUNK_ROWS (10000)+staticintapp__chunk_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<CHUNK_ROWS;row++){+strbuf_setlen(&buf,0);+strbuf_addf(&buf,"big: %.75d\n",row);+ret=reply_cb(reply_data,buf.buf,buf.len);+}++strbuf_release(&buf);++returnret;+}++/*+*Slowlyreplywithaseriesoflines.Thisistomodelanexpensiveto+*computechunkedresponse(whichmighthappenifthiscallbackisrunning+*inathreadandisfightingforalockwithotherthreads).+*/+#define SLOW_ROWS (1000)+#define SLOW_DELAY_MS (10)+staticintapp__slow_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<SLOW_ROWS;row++){+strbuf_setlen(&buf,0);+strbuf_addf(&buf,"big: %.75d\n",row);+ret=reply_cb(reply_data,buf.buf,buf.len);+sleep_millisec(SLOW_DELAY_MS);+}++strbuf_release(&buf);++returnret;+}++/*+*Theclientsentacommandfollowedbya(possiblyvery)largebuffer.+*/+staticintapp__sendbytes_command(constchar*received,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf_resp=STRBUF_INIT;+constchar*p="?";+intlen_ballast=0;+intk;+interrs=0;+intret;++if(skip_prefix(received,"sendbytes ",&p))+len_ballast=strlen(p);++/*+*Verifythattheballastisncopiesofasingleletter.+*Andthatthemulti-threadedIOlayerdidn'tcrossthestreams.+*/+for(k=1;k<len_ballast;k++)+if(p[k]!=p[0])+errs++;++if(errs)+strbuf_addf(&buf_resp,"errs:%d\n",errs);+else+strbuf_addf(&buf_resp,"rcvd:%c%08d\n",p[0],len_ballast);++ret=reply_cb(reply_data,buf_resp.buf,buf_resp.len);++strbuf_release(&buf_resp);++returnret;+}++/*+*Anarbitraryfixedaddresstoverifythattheapplicationinstance+*dataishandledproperly.+*/+staticintmy_app_data=42;++staticipc_server_application_cbtest_app_cb;++/*+*Thisisthe"application callback"thatsitsontopofthe+*"ipc-server".Itcompletelydefinesthesetofcommandssupported+*bythisapplication.+*/+staticinttest_app_cb(void*application_data,+constchar*command,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+/*+*Verifythatwereceivedtheapplication-datathatwepassed+*whenwestartedtheipc-server.(Wehaveseverallayersof+*callbackscallingcallbacksandit'seasytogetthingsmixed+*up(especiallywhensomeare"void*").)+*/+if(application_data!=(void*)&my_app_data)+BUG("application_cb: application_data pointer wrong");++if(!strcmp(command,"quit")){+/*+*Theclientsenta"quit"command.Thisisanasync+*requestfortheservertoshutdown.+*+*WeDONOTsendtheclientaresponsemessage+*(becausewehavenothingtosayandtheother+*serverthreadshavenotyetstopped).+*+*Telltheipc-serverlayertostartshuttingdown.+*Thisincludes:stoplisteningfornewconnections+*onthesocket/pipeandtellingallworkerthreads+*tofinish/draintheiroutgoingresponsestoother+*clients.+*+*ThisDOESNOTforceanimmediatesyncshutdown.+*/+returnSIMPLE_IPC_QUIT;+}++if(!strcmp(command,"ping")){+constchar*answer="pong";+returnreply_cb(reply_data,answer,strlen(answer));+}++if(!strcmp(command,"big"))+returnapp__big_command(reply_cb,reply_data);++if(!strcmp(command,"chunk"))+returnapp__chunk_command(reply_cb,reply_data);++if(!strcmp(command,"slow"))+returnapp__slow_command(reply_cb,reply_data);++if(starts_with(command,"sendbytes "))+returnapp__sendbytes_command(command,reply_cb,reply_data);++returnapp__unhandled_command(command,reply_cb,reply_data);+}++/*+*Thisprocesswillrunasasimple-ipcserverandlistenforIPCcommands+*fromclientprocesses.+*/+staticintdaemon__run_server(constchar*path,intargc,constchar**argv)+{+structipc_server_optsopts={+.nr_threads=5+};++constchar*constdaemon_usage[]={+N_("test-helper simple-ipc run-daemon [<options>"),+NULL+};+structoptiondaemon_options[]={+OPT_INTEGER(0,"threads",&opts.nr_threads,+N_("number of threads in server thread pool")),+OPT_END()+};++argc=parse_options(argc,argv,NULL,daemon_options,daemon_usage,0);++if(opts.nr_threads<1)+opts.nr_threads=1;++/*+*Synchronouslyruntheipc-server.Wedon'tneedanyapplication+*instancedata,sopassanarbitrarypointer(thatwe'lllater+*verifymadetheroundtrip).+*/+returnipc_server_run(path,&opts,test_app_cb,(void*)&my_app_data);+}++#ifndef GIT_WINDOWS_NATIVE+/*+*Thisisadaptedfrom`daemonize()`.Use`fork()`todirectlycreateand+*runthedaemoninachildprocess.+*/+staticintspawn_server(constchar*path,+conststructipc_server_opts*opts,+pid_t*pid)+{+*pid=fork();++switch(*pid){+case0:+if(setsid()==-1)+error_errno(_("setsid failed"));+close(0);+close(1);+close(2);+sanitize_stdfds();++returnipc_server_run(path,opts,test_app_cb,(void*)&my_app_data);++case-1:+returnerror_errno(_("could not spawn daemon in the background"));++default:+return0;+}+}+#else+/*+*Conceptuallylike`daemonize()`butdifferentbecauseWindowsdoesnot+*have`fork(2)`.SpawnanormalWindowschildprocessbutwithoutthe+*limitationsof`start_command()`and`finish_command()`.+*/+staticintspawn_server(constchar*path,+conststructipc_server_opts*opts,+pid_t*pid)+{+chartest_tool_exe[MAX_PATH];+structstrvecargs=STRVEC_INIT;+intin,out;++GetModuleFileNameA(NULL,test_tool_exe,MAX_PATH);++in=open("/dev/null",O_RDONLY);+out=open("/dev/null",O_WRONLY);++strvec_push(&args,test_tool_exe);+strvec_push(&args,"simple-ipc");+strvec_push(&args,"run-daemon");+strvec_pushf(&args,"--threads=%d",opts->nr_threads);++*pid=mingw_spawnvpe(args.v[0],args.v,NULL,NULL,in,out,out);+close(in);+close(out);++strvec_clear(&args);++if(*pid<0)+returnerror(_("could not spawn daemon in the background"));++return0;+}+#endif++/*+*Thisisadaptedfrom`wait_or_whine()`.Watchthechildprocessand+*letitgetstartedandbeginlisteningforrequestsonthesocket+*beforereportingoursuccess.+*/+staticintwait_for_server_startup(constchar*path,pid_tpid_child,+intmax_wait_sec)+{+intstatus;+pid_tpid_seen;+enumipc_active_states;+time_ttime_limit,now;++time(&time_limit);+time_limit+=max_wait_sec;++for(;;){+pid_seen=waitpid(pid_child,&status,WNOHANG);++if(pid_seen==-1)+returnerror_errno(_("waitpid failed"));++elseif(pid_seen==0){+/*+*Thechildisstillrunning(thisshouldbe+*thenormalcase).Trytoconnecttoiton+*thesocketandseeifitisreadyfor+*business.+*+*Ifthereisanotherdaemonalreadyrunning,+*ourchildwillfailtostart(possibly+*afteratimeoutonthelock),butwedon't+*care(whoresponds)ifthesocketislive.+*/+s=ipc_get_active_state(path);+if(s==IPC_STATE__LISTENING)+return0;++time(&now);+if(now>time_limit)+returnerror(_("daemon not online yet"));++continue;+}++elseif(pid_seen==pid_child){+/*+*Thenewchilddaemonprocessshutdownwhile+*itwasstartingup,soitisnotlistening+*onthesocket.+*+*Trytopingthesocketintheoddchance+*thatanotherdaemonstarted(orwasalready+*running)whileourchildwasstarting.+*+*Again,wedon'tcarewhoservicesthesocket.+*/+s=ipc_get_active_state(path);+if(s==IPC_STATE__LISTENING)+return0;++/*+*Wedon'tcareabouttheWEXITSTATUS()nor+*anyoftheWIF*(status)valuesbecause+*`cmd__simple_ipc()`doesthe`!!result`+*trickonallfunctionreturnvalues.+*+*Soitissufficienttojustreportthe+*earlyshutdownasanerror.+*/+returnerror(_("daemon failed to start"));+}++else+returnerror(_("waitpid is confused"));+}+}++/*+*Thisprocesswillstartasimple-ipcserverinabackgroundprocessand+*waitforittobecomeready.Thisislike`daemonize()`butgivesus+*morecontrolandbettererrorreporting(andmakesiteasiertowrite+*unittests).+*/+staticintdaemon__start_server(constchar*path,intargc,constchar**argv)+{+pid_tpid_child;+intret;+intmax_wait_sec=60;+structipc_server_optsopts={+.nr_threads=5+};++constchar*constdaemon_usage[]={+N_("test-helper simple-ipc start-daemon [<options>"),+NULL+};++structoptiondaemon_options[]={+OPT_INTEGER(0,"max-wait",&max_wait_sec,+N_("seconds to wait for daemon to startup")),+OPT_INTEGER(0,"threads",&opts.nr_threads,+N_("number of threads in server thread pool")),+OPT_END()+};++argc=parse_options(argc,argv,NULL,daemon_options,daemon_usage,0);++if(max_wait_sec<0)+max_wait_sec=0;+if(opts.nr_threads<1)+opts.nr_threads=1;++/*+*Runtheactualdaemoninabackgroundprocess.+*/+ret=spawn_server(path,&opts,&pid_child);+if(pid_child<=0)+returnret;++/*+*Lettheparentwaitforthechildprocesstogetstarted+*andbeginlisteningforrequestsonthesocket.+*/+ret=wait_for_server_startup(path,pid_child,max_wait_sec);++returnret;+}++/*+*Thisprocesswillrunaquickprobetoseeifasimple-ipcserver+*isactiveonthispath.+*+*Returns0iftheserverisalive.+*/+staticintclient__probe_server(constchar*path)+{+enumipc_active_states;++s=ipc_get_active_state(path);+switch(s){+caseIPC_STATE__LISTENING:+return0;++caseIPC_STATE__NOT_LISTENING:+returnerror("no server listening at '%s'",path);++caseIPC_STATE__PATH_NOT_FOUND:+returnerror("path not found '%s'",path);++caseIPC_STATE__INVALID_PATH:+returnerror("invalid pipe/socket name '%s'",path);++caseIPC_STATE__OTHER_ERROR:+default:+returnerror("other error for '%s'",path);+}+}++/*+*SendanIPCcommandtoanalready-runningserverdaemonandprintthe+*response.+*+*argv[2]containsasimple(1word)commandthat`test_app_cb()`(in+*thedaemonprocess)willunderstand.+*/+staticintclient__send_ipc(intargc,constchar**argv,constchar*path)+{+constchar*command=argc>2?argv[2]:"(no command)";+structstrbufbuf=STRBUF_INIT;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++options.wait_if_busy=1;+options.wait_if_not_found=0;++if(!ipc_client_send_command(path,&options,command,&buf)){+if(buf.len){+printf("%s\n",buf.buf);+fflush(stdout);+}+strbuf_release(&buf);++return0;+}++returnerror("failed to send '%s' to '%s'",command,path);+}++/*+*SendanIPCcommandtoanalready-runningserverandaskitto+*shutdown."send quit"isanasyncrequestandqueuesashutdown+*eventintheserver,sowespinandwaithereforittoactually+*shutdowntomaketheunittestsalittleeasiertowrite.+*/+staticintclient__stop_server(intargc,constchar**argv,constchar*path)+{+constchar*send_quit[]={argv[0],"send","quit",NULL};+intmax_wait_sec=60;+intret;+time_ttime_limit,now;+enumipc_active_states;++constchar*conststop_usage[]={+N_("test-helper simple-ipc stop-daemon [<options>]"),+NULL+};++structoptionstop_options[]={+OPT_INTEGER(0,"max-wait",&max_wait_sec,+N_("seconds to wait for daemon to stop")),+OPT_END()+};++argc=parse_options(argc,argv,NULL,stop_options,stop_usage,0);++if(max_wait_sec<0)+max_wait_sec=0;++time(&time_limit);+time_limit+=max_wait_sec;++ret=client__send_ipc(3,send_quit,path);+if(ret)+returnret;++for(;;){+sleep_millisec(100);++s=ipc_get_active_state(path);++if(s!=IPC_STATE__LISTENING){+/*+*Thesocket/pipeisgoneand/orhasstopped+*responding.Letsassumethatthedaemon+*processhasexitedtoo.+*/+return0;+}++time(&now);+if(now>time_limit)+returnerror(_("daemon has not shutdown yet"));+}+}++/*+*SendanIPCcommandfollowedbyballasttoconfirmthatalarge+*messagecanbesentandthatthekernelorpkt-linelayerswill+*properlychunkitandthatthedaemonreceivestheentiremessage.+*/+staticintdo_sendbytes(intbytecount,charbyte,constchar*path,+conststructipc_client_connect_options*options)+{+structstrbufbuf_send=STRBUF_INIT;+structstrbufbuf_resp=STRBUF_INIT;++strbuf_addstr(&buf_send,"sendbytes ");+strbuf_addchars(&buf_send,byte,bytecount);++if(!ipc_client_send_command(path,options,buf_send.buf,&buf_resp)){+strbuf_rtrim(&buf_resp);+printf("sent:%c%08d %s\n",byte,bytecount,buf_resp.buf);+fflush(stdout);+strbuf_release(&buf_send);+strbuf_release(&buf_resp);++return0;+}++returnerror("client failed to sendbytes(%d, '%c') to '%s'",+bytecount,byte,path);+}++/*+*SendanIPCcommandwithballasttoanalready-runningserverdaemon.+*/+staticintclient__sendbytes(intargc,constchar**argv,constchar*path)+{+intbytecount=1024;+char*string="x";+constchar*constsendbytes_usage[]={+N_("test-helper simple-ipc sendbytes [<options>]"),+NULL+};+structoptionsendbytes_options[]={+OPT_INTEGER(0,"bytecount",&bytecount,N_("number of bytes")),+OPT_STRING(0,"byte",&string,N_("byte"),N_("ballast")),+OPT_END()+};+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++options.wait_if_busy=1;+options.wait_if_not_found=0;+options.uds_disallow_chdir=0;++argc=parse_options(argc,argv,NULL,sendbytes_options,sendbytes_usage,0);++returndo_sendbytes(bytecount,string[0],path,&options);+}++structmultiple_thread_data{+pthread_tpthread_id;+structmultiple_thread_data*next;+constchar*path;+intbytecount;+intbatchsize;+intsum_errors;+intsum_good;+charletter;+};++staticvoid*multiple_thread_proc(void*_multiple_thread_data)+{+structmultiple_thread_data*d=_multiple_thread_data;+intk;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++options.wait_if_busy=1;+options.wait_if_not_found=0;+/*+*Amulti-threadedclientshouldnotberandomlycallingchdir().+*Thetestwillpasswithoutthisrestrictionbecausethetestis+*nototherwiseaccessingthefilesystem,butitmakesushonest.+*/+options.uds_disallow_chdir=1;++trace2_thread_start("multiple");++for(k=0;k<d->batchsize;k++){+if(do_sendbytes(d->bytecount+k,d->letter,d->path,&options))+d->sum_errors++;+else+d->sum_good++;+}++trace2_thread_exit();+returnNULL;+}++/*+*Startaclient-sidethreadpool.Eachthreadsendsaseriesof+*IPCrequests.Eachrequestisonanewconnectiontotheserver.+*/+staticintclient__multiple(intargc,constchar**argv,constchar*path)+{+structmultiple_thread_data*list=NULL;+intk;+intnr_threads=5;+intbytecount=1;+intbatchsize=10;+intsum_join_errors=0;+intsum_thread_errors=0;+intsum_good=0;++constchar*constmultiple_usage[]={+N_("test-helper simple-ipc multiple [<options>]"),+NULL+};+structoptionmultiple_options[]={+OPT_INTEGER(0,"bytecount",&bytecount,N_("number of bytes")),+OPT_INTEGER(0,"threads",&nr_threads,N_("number of threads")),+OPT_INTEGER(0,"batchsize",&batchsize,N_("number of requests per thread")),+OPT_END()+};++argc=parse_options(argc,argv,NULL,multiple_options,multiple_usage,0);++if(bytecount<1)+bytecount=1;+if(nr_threads<1)+nr_threads=1;+if(batchsize<1)+batchsize=1;++for(k=0;k<nr_threads;k++){+structmultiple_thread_data*d=xcalloc(1,sizeof(*d));+d->next=list;+d->path=path;+d->bytecount=bytecount+batchsize*(k/26);+d->batchsize=batchsize;+d->sum_errors=0;+d->sum_good=0;+d->letter='A'+(k%26);++if(pthread_create(&d->pthread_id,NULL,multiple_thread_proc,d)){+warning("failed to create thread[%d] skipping remainder",k);+free(d);+break;+}++list=d;+}++while(list){+structmultiple_thread_data*d=list;++if(pthread_join(d->pthread_id,NULL))+sum_join_errors++;++sum_thread_errors+=d->sum_errors;+sum_good+=d->sum_good;++list=d->next;+free(d);+}++printf("client (good %d) (join %d), (errors %d)\n",+sum_good,sum_join_errors,sum_thread_errors);++return(sum_join_errors+sum_thread_errors)?1:0;+}++intcmd__simple_ipc(intargc,constchar**argv)+{+constchar*path="ipc-test";++if(argc==2&&!strcmp(argv[1],"SUPPORTS_SIMPLE_IPC"))+return0;++/*+*Use'!!'onalldispatchfunctionstomapfrom`error()`style+*(returns-1)styleto`test_must_fail`style(expects1).This+*makesshellerrormessageslessconfusing.+*/++if(argc==2&&!strcmp(argv[1],"is-active"))+return!!client__probe_server(path);++if(argc>=2&&!strcmp(argv[1],"run-daemon"))+return!!daemon__run_server(path,argc,argv);++if(argc>=2&&!strcmp(argv[1],"start-daemon"))+return!!daemon__start_server(path,argc,argv);++/*+*Clientcommandsfollow.Ensureaserverisrunningbefore+*goinganyfurther.+*/+if(client__probe_server(path))+return1;++if(argc>=2&&!strcmp(argv[1],"stop-daemon"))+return!!client__stop_server(argc,argv,path);++if((argc==2||argc==3)&&!strcmp(argv[1],"send"))+return!!client__send_ipc(argc,argv,path);++if(argc>=2&&!strcmp(argv[1],"sendbytes"))+return!!client__sendbytes(argc,argv,path);++if(argc>=2&&!strcmp(argv[1],"multiple"))+return!!client__multiple(argc,argv,path);++die("Unhandled argv[1]: '%s'",argv[1]);+}+#endif
@@ -0,0 +1,122 @@+#!/bin/sh++test_description='simple command server'++../test-lib.sh++test-toolsimple-ipcSUPPORTS_SIMPLE_IPC||{+skip_all='simple IPC not supported on this platform'+test_done+}++stop_simple_IPC_server(){+test-toolsimple-ipcstop-daemon+}++test_expect_success'start simple command server''+test_atexitstop_simple_IPC_server&&+test-toolsimple-ipcstart-daemon--threads=8&&+test-toolsimple-ipcis-active+'++test_expect_success'simple command server''+test-toolsimple-ipcsendping>actual&&+echopong>expect&&+test_cmpexpectactual+'++test_expect_success'servers cannot share the same path''+test_must_failtest-toolsimple-ipcrun-daemon&&+test-toolsimple-ipcis-active+'++test_expect_success'big response''+test-toolsimple-ipcsendbig>actual&&+test_line_count-ge10000actual&&+grep-q"big: [0]*9999\$"actual+'++test_expect_success'chunk response''+test-toolsimple-ipcsendchunk>actual&&+test_line_count-ge10000actual&&+grep-q"big: [0]*9999\$"actual+'++test_expect_success'slow response''+test-toolsimple-ipcsendslow>actual&&+test_line_count-ge100actual&&+grep-q"big: [0]*99\$"actual+'++# Send an IPC with n=100,000 bytes of ballast. This should be large enough+# to force both the kernel and the pkt-line layer to chunk the message to the+# daemon and for the daemon to receive it in chunks.+#+test_expect_success'sendbytes''+test-toolsimple-ipcsendbytes--bytecount=100000--byte=A>actual&&+grep"sent:A00100000 rcvd:A00100000"actual+'++# Start a series of <threads> client threads that each make <batchsize>+# IPC requests to the server. Each (<threads> * <batchsize>) request+# will open a new connection to the server and randomly bind to a server+# thread. Each client thread exits after completing its batch. So the+# total number of live client threads will be smaller than the total.+# Each request will send a message containing at least <bytecount> bytes+# of ballast. (Responses are small.)+#+# The purpose here is to test threading in the server and responding to+# many concurrent client requests (regardless of whether they come from+# 1 client process or many). And to test that the server side of the+# named pipe/socket is stable. (On Windows this means that the server+# pipe is properly recycled.)+#+# On Windows it also lets us adjust the connection timeout in the+# `ipc_client_send_command()`.+#+# Note it is easy to drive the system into failure by requesting an+# insane number of threads on client or server and/or increasing the+# per-thread batchsize or the per-request bytecount (ballast).+# On Windows these failures look like "pipe is busy" errors.+# So I've chosen fairly conservative values for now.+#+# We expect output of the form "sent:<letter><length> ..."+# With terms (7, 19, 13) we expect:+# <letter> in [A-G]+# <length> in [19+0 .. 19+(13-1)]+# and (7 * 13) successful responses.+#+test_expect_success'stress test threads''+test-toolsimple-ipcmultiple\+--threads=7\+--bytecount=19\+--batchsize=13\+>actual&&+test_line_count=92actual&&+grep"good 91"actual&&+grep"sent:A"<actual>actual_a&&+cat>expect_a<<-EOF&&+sent:A00000019rcvd:A00000019+sent:A00000020rcvd:A00000020+sent:A00000021rcvd:A00000021+sent:A00000022rcvd:A00000022+sent:A00000023rcvd:A00000023+sent:A00000024rcvd:A00000024+sent:A00000025rcvd:A00000025+sent:A00000026rcvd:A00000026+sent:A00000027rcvd:A00000027+sent:A00000028rcvd:A00000028+sent:A00000029rcvd:A00000029+sent:A00000030rcvd:A00000030+sent:A00000031rcvd:A00000031+EOF+test_cmpexpect_aactual_a+'++test_expect_success'stop-daemon works''+test-toolsimple-ipcstop-daemon&&+test_must_failtest-toolsimple-ipcis-active&&+test_must_failtest-toolsimple-ipcsendping+'++test_done
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-02-17 21:51:30
From: Jeff Hostetler <redacted>
Create Unix domain socket based implementation of "simple-ipc".
A set of `ipc_client` routines implement a client library to connect
to an `ipc_server` over a Unix domain socket, send a simple request,
and receive a single response. Clients use blocking IO on the socket.
A set of `ipc_server` routines implement a thread pool to listen for
and concurrently service client connections.
The server creates a new Unix domain socket at a known location. If a
socket already exists with that name, the server tries to determine if
another server is already listening on the socket or if the socket is
dead. If socket is busy, the server exits with an error rather than
stealing the socket. If the socket is dead, the server creates a new
one and starts up.
If while running, the server detects that its socket has been stolen
by another server, it automatically exits.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 2 +
compat/simple-ipc/ipc-unix-socket.c | 979 ++++++++++++++++++++++++++++
contrib/buildsystems/CMakeLists.txt | 2 +
simple-ipc.h | 13 +-
4 files changed, 995 insertions(+), 1 deletion(-)
create mode 100644 compat/simple-ipc/ipc-unix-socket.c
@@ -0,0 +1,979 @@+#include"cache.h"+#include"simple-ipc.h"+#include"strbuf.h"+#include"pkt-line.h"+#include"thread-utils.h"+#include"unix-socket.h"++#ifdef NO_UNIX_SOCKETS+#error compat/simple-ipc/ipc-unix-socket.c requires Unix sockets+#endif++enumipc_active_stateipc_get_active_state(constchar*path)+{+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;+structstatst;+structipc_client_connection*connection_test=NULL;++options.wait_if_busy=0;+options.wait_if_not_found=0;++if(lstat(path,&st)==-1){+switch(errno){+caseENOENT:+caseENOTDIR:+returnIPC_STATE__NOT_LISTENING;+default:+returnIPC_STATE__INVALID_PATH;+}+}++/* also complain if a plain file is in the way */+if((st.st_mode&S_IFMT)!=S_IFSOCK)+returnIPC_STATE__INVALID_PATH;++/*+*JustbecausethefilesystemhasaS_IFSOCKtypeinode+*at`path`,doesn'tmeanitthatthereisaserverlistening.+*Pingittobesure.+*/+state=ipc_client_try_connect(path,&options,&connection_test);+ipc_client_close_connection(connection_test);++returnstate;+}++/*+*Thisvaluewaschosenatrandom.+*/+#define WAIT_STEP_MS (50)++/*+*Trytoconnecttotheserver.Iftheserverisjuststartingupor+*isverybusy,wemaynotgetaconnectionthefirsttime.+*/+staticenumipc_active_stateconnect_to_server(+constchar*path,+inttimeout_ms,+conststructipc_client_connect_options*options,+int*pfd)+{+intwait_ms=50;+intk;++*pfd=-1;++for(k=0;k<timeout_ms;k+=wait_ms){+intfd=unix_stream_connect(path,options->uds_disallow_chdir);++if(fd!=-1){+*pfd=fd;+returnIPC_STATE__LISTENING;+}++if(errno==ENOENT){+if(!options->wait_if_not_found)+returnIPC_STATE__PATH_NOT_FOUND;++gotosleep_and_try_again;+}++if(errno==ETIMEDOUT){+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;++gotosleep_and_try_again;+}++if(errno==ECONNREFUSED){+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;++gotosleep_and_try_again;+}++returnIPC_STATE__OTHER_ERROR;++sleep_and_try_again:+sleep_millisec(wait_ms);+}++returnIPC_STATE__NOT_LISTENING;+}++/*+*Arandomlychosentimeoutvalue.+*/+#define MY_CONNECTION_TIMEOUT_MS (1000)++enumipc_active_stateipc_client_try_connect(+constchar*path,+conststructipc_client_connect_options*options,+structipc_client_connection**p_connection)+{+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+intfd=-1;++*p_connection=NULL;++trace2_region_enter("ipc-client","try-connect",NULL);+trace2_data_string("ipc-client",NULL,"try-connect/path",path);++state=connect_to_server(path,MY_CONNECTION_TIMEOUT_MS,+options,&fd);++trace2_data_intmax("ipc-client",NULL,"try-connect/state",+(intmax_t)state);+trace2_region_leave("ipc-client","try-connect",NULL);++if(state==IPC_STATE__LISTENING){+(*p_connection)=xcalloc(1,sizeof(structipc_client_connection));+(*p_connection)->fd=fd;+}++returnstate;+}++voidipc_client_close_connection(structipc_client_connection*connection)+{+if(!connection)+return;++if(connection->fd!=-1)+close(connection->fd);++free(connection);+}++intipc_client_send_command_to_connection(+structipc_client_connection*connection,+constchar*message,structstrbuf*answer)+{+intret=0;++strbuf_setlen(answer,0);++trace2_region_enter("ipc-client","send-command",NULL);++if(write_packetized_from_buf_no_flush(message,strlen(message),+connection->fd)<0||+packet_flush_gently(connection->fd)<0){+ret=error(_("could not send IPC command"));+gotodone;+}++if(read_packetized_to_strbuf(+connection->fd,answer,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_NEVER_DIE)<0){+ret=error(_("could not read IPC response"));+gotodone;+}++done:+trace2_region_leave("ipc-client","send-command",NULL);+returnret;+}++intipc_client_send_command(constchar*path,+conststructipc_client_connect_options*options,+constchar*message,structstrbuf*answer)+{+intret=-1;+enumipc_active_statestate;+structipc_client_connection*connection=NULL;++state=ipc_client_try_connect(path,options,&connection);++if(state!=IPC_STATE__LISTENING)+returnret;++ret=ipc_client_send_command_to_connection(connection,message,answer);++ipc_client_close_connection(connection);++returnret;+}++staticintset_socket_blocking_flag(intfd,intmake_nonblocking)+{+intflags;++flags=fcntl(fd,F_GETFL,NULL);++if(flags<0)+return-1;++if(make_nonblocking)+flags|=O_NONBLOCK;+else+flags&=~O_NONBLOCK;++returnfcntl(fd,F_SETFL,flags);+}++/*+*Magicnumbersusedtoannotatecallbackinstancedata.+*Theseareusedtohelpguardagainstaccidentallypassingthe+*wronginstancedataacrossmultiplelevelsofcallbacks(which+*iseasytodoifthereare`void*`arguments).+*/+enummagic{+MAGIC_SERVER_REPLY_DATA,+MAGIC_WORKER_THREAD_DATA,+MAGIC_ACCEPT_THREAD_DATA,+MAGIC_SERVER_DATA,+};++structipc_server_reply_data{+enummagicmagic;+intfd;+structipc_worker_thread_data*worker_thread_data;+};++structipc_worker_thread_data{+enummagicmagic;+structipc_worker_thread_data*next_thread;+structipc_server_data*server_data;+pthread_tpthread_id;+};++structipc_accept_thread_data{+enummagicmagic;+structipc_server_data*server_data;++structunix_stream_server_socket*server_socket;++intfd_send_shutdown;+intfd_wait_shutdown;+pthread_tpthread_id;+};++/*+*Withunix-sockets,theconceptual"ipc-server"isimplementedasasingle+*controller"accept-thread"threadandapoolof"worker-thread"threads.+*Theformerdoestheusual`accept()`loopanddispatchesconnections+*toanidleworkerthread.Theworkerthreadswaitinanidleloopfor+*anewconnection,communicatewiththeclientandrelaydatato/from+*the`application_cb`andthenwaitforanotherconnectionfromthe+*serverthread.Thisavoidstheoverheadofconstantlycreatingand+*destroyingthreads.+*/+structipc_server_data{+enummagicmagic;+ipc_server_application_cb*application_cb;+void*application_data;+structstrbufbuf_path;++structipc_accept_thread_data*accept_thread;+structipc_worker_thread_data*worker_thread_list;++pthread_mutex_twork_available_mutex;+pthread_cond_twork_available_cond;++/*+*Acceptedbutnotyetprocessedclientconnectionsarekept+*inacircularbufferFIFO.Thequeueisemptywhenthe+*positionsareequal.+*/+int*fifo_fds;+intqueue_size;+intback_pos;+intfront_pos;++intshutdown_requested;+intis_stopped;+};++/*+*Removeandreturntheoldestqueuedconnection.+*+*Returns-1ifempty.+*/+staticintfifo_dequeue(structipc_server_data*server_data)+{+/* ASSERT holding mutex */++intfd;++if(server_data->back_pos==server_data->front_pos)+return-1;++fd=server_data->fifo_fds[server_data->front_pos];+server_data->fifo_fds[server_data->front_pos]=-1;++server_data->front_pos++;+if(server_data->front_pos==server_data->queue_size)+server_data->front_pos=0;++returnfd;+}++/*+*Pushanewfdontothebackofthequeue.+*+*Dropitandreturn-1ifqueueisalreadyfull.+*/+staticintfifo_enqueue(structipc_server_data*server_data,intfd)+{+/* ASSERT holding mutex */++intnext_back_pos;++next_back_pos=server_data->back_pos+1;+if(next_back_pos==server_data->queue_size)+next_back_pos=0;++if(next_back_pos==server_data->front_pos){+/* Queue is full. Just drop it. */+close(fd);+return-1;+}++server_data->fifo_fds[server_data->back_pos]=fd;+server_data->back_pos=next_back_pos;++returnfd;+}++/*+*WaitforaconnectiontobequeuedtotheFIFOandreturnit.+*+*Returns-1ifsomeonehasalreadyrequestedashutdown.+*/+staticintworker_thread__wait_for_connection(+structipc_worker_thread_data*worker_thread_data)+{+/* ASSERT NOT holding mutex */++structipc_server_data*server_data=worker_thread_data->server_data;+intfd=-1;++pthread_mutex_lock(&server_data->work_available_mutex);+for(;;){+if(server_data->shutdown_requested)+break;++fd=fifo_dequeue(server_data);+if(fd>=0)+break;++pthread_cond_wait(&server_data->work_available_cond,+&server_data->work_available_mutex);+}+pthread_mutex_unlock(&server_data->work_available_mutex);++returnfd;+}++/*+*Forwarddeclareourreplycallbackfunctionsothatanycompiler+*errorsarereportedwhenweactuallydefinethefunction(inaddition+*toanyerrorsreportedwhenwetrytopassthiscallbackfunctionas+*aparameterinafunctioncall).Theformerareeasiertounderstand.+*/+staticipc_server_reply_cbdo_io_reply_callback;++/*+*Relayapplication'sresponsemessagetotheclientprocess.+*(Wedonotflushatthispointbecauseweallowthecaller+*tochunkdatatotheclientthruus.)+*/+staticintdo_io_reply_callback(structipc_server_reply_data*reply_data,+constchar*response,size_tresponse_len)+{+if(reply_data->magic!=MAGIC_SERVER_REPLY_DATA)+BUG("reply_cb called with wrong instance data");++returnwrite_packetized_from_buf_no_flush(response,response_len,+reply_data->fd);+}++/* A randomly chosen value. */+#define MY_WAIT_POLL_TIMEOUT_MS (10)++/*+*Iftheclienthangsupwithoutsendinganydataonthewire,just+*quietlyclosethesocketandignorethisclient.+*+*ThisworkerthreadiscommittedtoreadingtheIPCrequestdata+*fromtheclientattheotherendofthisfd.Waithereforthe+*clienttoactuallyputsomethingonthewire--becauseifthe+*clientjustdoesaping(connectandhangupwithoutsendingany+*data),ouruseofthepkt-linereadroutineswillspewanerror+*message.+*+*Return-1iftheclienthungup.+*Return0ifdata(possiblyincomplete)isready.+*/+staticintworker_thread__wait_for_io_start(+structipc_worker_thread_data*worker_thread_data,+intfd)+{+structipc_server_data*server_data=worker_thread_data->server_data;+structpollfdpollfd[1];+intresult;++for(;;){+pollfd[0].fd=fd;+pollfd[0].events=POLLIN;++result=poll(pollfd,1,MY_WAIT_POLL_TIMEOUT_MS);+if(result<0){+if(errno==EINTR)+continue;+gotocleanup;+}++if(result==0){+/* a timeout */++intin_shutdown;++pthread_mutex_lock(&server_data->work_available_mutex);+in_shutdown=server_data->shutdown_requested;+pthread_mutex_unlock(&server_data->work_available_mutex);++/*+*Ifashutdownisalreadyinprogressandthis+*clienthasnotstartedtalkingyet,justdropit.+*/+if(in_shutdown)+gotocleanup;+continue;+}++if(pollfd[0].revents&POLLHUP)+gotocleanup;++if(pollfd[0].revents&POLLIN)+return0;++gotocleanup;+}++cleanup:+close(fd);+return-1;+}++/*+*Receivetherequest/commandfromtheclientandpassittothe+*registeredrequest-callback.Therequest-callbackwillcompose+*aresponseandcallourreply-callbacktosendittotheclient.+*/+staticintworker_thread__do_io(+structipc_worker_thread_data*worker_thread_data,+intfd)+{+/* ASSERT NOT holding lock */++structstrbufbuf=STRBUF_INIT;+structipc_server_reply_datareply_data;+intret=0;++reply_data.magic=MAGIC_SERVER_REPLY_DATA;+reply_data.worker_thread_data=worker_thread_data;++reply_data.fd=fd;++ret=read_packetized_to_strbuf(+reply_data.fd,&buf,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_NEVER_DIE);+if(ret>=0){+ret=worker_thread_data->server_data->application_cb(+worker_thread_data->server_data->application_data,+buf.buf,do_io_reply_callback,&reply_data);++packet_flush_gently(reply_data.fd);+}+else{+/*+*Theclientprobablydisconnected/shutdownbeforeit+*couldsendawell-formedmessage.Ignoreit.+*/+}++strbuf_release(&buf);+close(reply_data.fd);++returnret;+}++/*+*BlockSIGPIPEonthecurrentthread(sothatwegetEPIPEfrom+*write()ratherthananactualsignal).+*+*Notethatusingsigchain_push()and_pop()tocontrolSIGPIPE+*aroundourIOcallsisnotthreadsafe:+*[]Itusesaglobalstackofhandlerframes.+*[]ItusesALLOC_GROW()toresizeit.+*[]Finally,accordingtothe`signal(2)`man-page:+*"The effects of `signal()` in a multithreaded process are unspecified."+*/+staticvoidthread_block_sigpipe(sigset_t*old_set)+{+sigset_tnew_set;++sigemptyset(&new_set);+sigaddset(&new_set,SIGPIPE);++sigemptyset(old_set);+pthread_sigmask(SIG_BLOCK,&new_set,old_set);+}++/*+*ThreadprocforanIPCworkerthread.Ithandlesaseriesof+*connectionsfromclients.Itpullsthenextfdfromthequeue+*processesit,andthenwaitsforthenextclient.+*+*BlockSIGPIPEinthisworkerthreadforthelifeofthethread.+*Thisavoidsstray(andsometimesdelayed)SIGPIPEsignalscaused+*byclienterrorsand/orwhenweareunderextremelyheavyIOload.+*+*ThismeansthattheapplicationcallbackwillhaveSIGPIPEblocked.+*Thecallbackshouldnotchangeit.+*/+staticvoid*worker_thread_proc(void*_worker_thread_data)+{+structipc_worker_thread_data*worker_thread_data=_worker_thread_data;+structipc_server_data*server_data=worker_thread_data->server_data;+sigset_told_set;+intfd,io;+intret;++trace2_thread_start("ipc-worker");++thread_block_sigpipe(&old_set);++for(;;){+fd=worker_thread__wait_for_connection(worker_thread_data);+if(fd==-1)+break;/* in shutdown */++io=worker_thread__wait_for_io_start(worker_thread_data,fd);+if(io==-1)+continue;/* client hung up without sending anything */++ret=worker_thread__do_io(worker_thread_data,fd);++if(ret==SIMPLE_IPC_QUIT){+trace2_data_string("ipc-worker",NULL,"queue_stop_async",+"application_quit");+/*+*Theapplicationlayeristellingtheipc-server+*layertoshutdown.+*+*WeDONOThavearesponsetosendtotheclient.+*+*Queueanasyncstop(tostoptheotherthreads)and+*allowthisworkerthreadtoexitnow(nosensewaiting+*forthethread-poolshutdownsignal).+*+*Othernon-idleworkerthreadsareallowedtofinish+*respondingtotheircurrentclients.+*/+ipc_server_stop_async(server_data);+break;+}+}++trace2_thread_exit();+returnNULL;+}++/* A randomly chosen value. */+#define MY_ACCEPT_POLL_TIMEOUT_MS (60 * 1000)++/*+*Acceptanewclientconnectiononoursocket.Thisusesnon-blocking+*IOsothatwecanalsowaitforshutdownrequestsonoursocket-pair+*withoutactuallyspinningonafasttimeout.+*/+staticintaccept_thread__wait_for_connection(+structipc_accept_thread_data*accept_thread_data)+{+structpollfdpollfd[2];+intresult;++for(;;){+pollfd[0].fd=accept_thread_data->fd_wait_shutdown;+pollfd[0].events=POLLIN;++pollfd[1].fd=accept_thread_data->server_socket->fd_socket;+pollfd[1].events=POLLIN;++result=poll(pollfd,2,MY_ACCEPT_POLL_TIMEOUT_MS);+if(result<0){+if(errno==EINTR)+continue;+returnresult;+}++if(result==0){+/* a timeout */++/*+*Ifsomeonedeletesorforce-createsanewunix+*domainsocketatourpath,allfutureclients+*willberoutedelsewhereandwesilentlystarve.+*Ifthathappens,justqueueashutdown.+*/+if(unix_stream_server__was_stolen(+accept_thread_data->server_socket)){+trace2_data_string("ipc-accept",NULL,+"queue_stop_async",+"socket_stolen");+ipc_server_stop_async(+accept_thread_data->server_data);+}+continue;+}++if(pollfd[0].revents&POLLIN){+/* shutdown message queued to socketpair */+return-1;+}++if(pollfd[1].revents&POLLIN){+/* a connection is available on server_socket */++intclient_fd=+accept(accept_thread_data->server_socket->fd_socket,+NULL,NULL);+if(client_fd>=0)+returnclient_fd;++/*+*Anerrorhereisunlikely--itprobably+*indicatesthattheconnectingprocesshas+*alreadydroppedtheconnection.+*/+continue;+}++BUG("unandled poll result errno=%d r[0]=%d r[1]=%d",+errno,pollfd[0].revents,pollfd[1].revents);+}+}++/*+*ThreadprocfortheIPCserver"accept thread".Thiswaitsfor+*anincomingsocketconnection,appendsittothequeueofavailable+*connections,andnotifiesaworkerthreadtoprocessit.+*+*BlockSIGPIPEinthisthreadforthelifeofthethread.This+*avoidsanystraySIGPIPEsignalswhenclosingpipefdsunder+*extremelyheavyloads(suchaswhenthefifoqueueisfullandwe+*dropincommingconnections).+*/+staticvoid*accept_thread_proc(void*_accept_thread_data)+{+structipc_accept_thread_data*accept_thread_data=_accept_thread_data;+structipc_server_data*server_data=accept_thread_data->server_data;+sigset_told_set;++trace2_thread_start("ipc-accept");++thread_block_sigpipe(&old_set);++for(;;){+intclient_fd=accept_thread__wait_for_connection(+accept_thread_data);++pthread_mutex_lock(&server_data->work_available_mutex);+if(server_data->shutdown_requested){+pthread_mutex_unlock(&server_data->work_available_mutex);+if(client_fd>=0)+close(client_fd);+break;+}++if(client_fd<0){+/* ignore transient accept() errors */+}+else{+fifo_enqueue(server_data,client_fd);+pthread_cond_broadcast(&server_data->work_available_cond);+}+pthread_mutex_unlock(&server_data->work_available_mutex);+}++trace2_thread_exit();+returnNULL;+}++/*+*Wecan'tpredicttheconnectionarrivalraterelativetotheworker+*processingrate,thereforeweallowthe"accept-thread"toqueueup+*agenerousnumberofconnections,sincewe'dratherhavetheclient+*notunnecessarilytimeoutifwecanavoidit.(Theassumptionis+*thatthiswillbeusedforFSMonitorandafewsecondwaitona+*connectionisbetterthanhavingtheclienttimeoutanddothefull+*computationitself.)+*+*TheFIFOqueuesizeissettoamultipleoftheworkerpoolsize.+*Thisvaluechosenatrandom.+*/+#define FIFO_SCALE (100)++/*+*Thebacklogvaluefor`listen(2)`.Thisdoesn'tneedtohuge,+*ratherjustlargeenoughforour"accept-thread"towakeupand+*queueincomingconnectionsontotheFIFOwithoutthekernel+*droppingany.+*+*Thisvaluechosenatrandom.+*/+#define LISTEN_BACKLOG (50)++staticstructunix_stream_server_socket*create_listener_socket(+constchar*path,+conststructipc_server_opts*ipc_opts)+{+structunix_stream_server_socket*server_socket=NULL;+structunix_stream_listen_optsuslg_opts=UNIX_STREAM_LISTEN_OPTS_INIT;++uslg_opts.listen_backlog_size=LISTEN_BACKLOG;+uslg_opts.disallow_chdir=ipc_opts->uds_disallow_chdir;++server_socket=unix_stream_server__listen_with_lock(path,&uslg_opts);+if(!server_socket)+returnNULL;++if(set_socket_blocking_flag(server_socket->fd_socket,1)){+intsaved_errno=errno;+error_errno(_("could not set listener socket nonblocking '%s'"),+path);+unix_stream_server__free(server_socket);+errno=saved_errno;+returnNULL;+}++trace2_data_string("ipc-server",NULL,"listen-with-lock",path);+returnserver_socket;+}++staticstructunix_stream_server_socket*setup_listener_socket(+constchar*path,+conststructipc_server_opts*ipc_opts)+{+structunix_stream_server_socket*server_socket;++trace2_region_enter("ipc-server","create-listener_socket",NULL);+server_socket=create_listener_socket(path,ipc_opts);+trace2_region_leave("ipc-server","create-listener_socket",NULL);++returnserver_socket;+}++/*+*StartIPCserverinapoolofbackgroundthreads.+*/+intipc_server_run_async(structipc_server_data**returned_server_data,+constchar*path,conststructipc_server_opts*opts,+ipc_server_application_cb*application_cb,+void*application_data)+{+structunix_stream_server_socket*server_socket=NULL;+structipc_server_data*server_data;+intsv[2];+intk;+intnr_threads=opts->nr_threads;++*returned_server_data=NULL;++/*+*Createasocketpairandsetsv[1]tonon-blocking.This+*willusedtosendashutdownmessagetotheaccept-thread+*andallowstheaccept-threadtowaitonEITHERaclient+*connectionorashutdownrequestwithoutspinning.+*/+if(socketpair(AF_UNIX,SOCK_STREAM,0,sv)<0)+returnerror_errno(_("could not create socketpair for '%s'"),+path);++if(set_socket_blocking_flag(sv[1],1)){+intsaved_errno=errno;+close(sv[0]);+close(sv[1]);+errno=saved_errno;+returnerror_errno(_("making socketpair nonblocking '%s'"),+path);+}++server_socket=setup_listener_socket(path,opts);+if(!server_socket){+intsaved_errno=errno;+close(sv[0]);+close(sv[1]);+errno=saved_errno;+return-1;+}++server_data=xcalloc(1,sizeof(*server_data));+server_data->magic=MAGIC_SERVER_DATA;+server_data->application_cb=application_cb;+server_data->application_data=application_data;+strbuf_init(&server_data->buf_path,0);+strbuf_addstr(&server_data->buf_path,path);++if(nr_threads<1)+nr_threads=1;++pthread_mutex_init(&server_data->work_available_mutex,NULL);+pthread_cond_init(&server_data->work_available_cond,NULL);++server_data->queue_size=nr_threads*FIFO_SCALE;+server_data->fifo_fds=xcalloc(server_data->queue_size,+sizeof(*server_data->fifo_fds));++server_data->accept_thread=+xcalloc(1,sizeof(*server_data->accept_thread));+server_data->accept_thread->magic=MAGIC_ACCEPT_THREAD_DATA;+server_data->accept_thread->server_data=server_data;+server_data->accept_thread->server_socket=server_socket;+server_data->accept_thread->fd_send_shutdown=sv[0];+server_data->accept_thread->fd_wait_shutdown=sv[1];++if(pthread_create(&server_data->accept_thread->pthread_id,NULL,+accept_thread_proc,server_data->accept_thread))+die_errno(_("could not start accept_thread '%s'"),path);++for(k=0;k<nr_threads;k++){+structipc_worker_thread_data*wtd;++wtd=xcalloc(1,sizeof(*wtd));+wtd->magic=MAGIC_WORKER_THREAD_DATA;+wtd->server_data=server_data;++if(pthread_create(&wtd->pthread_id,NULL,worker_thread_proc,+wtd)){+if(k==0)+die(_("could not start worker[0] for '%s'"),+path);+/*+*Limpalongwiththethreadpoolthatwehave.+*/+break;+}++wtd->next_thread=server_data->worker_thread_list;+server_data->worker_thread_list=wtd;+}++*returned_server_data=server_data;+return0;+}++/*+*GentlytelltheIPCservertreadstoshutdown.+*Canberunonanythread.+*/+intipc_server_stop_async(structipc_server_data*server_data)+{+/* ASSERT NOT holding mutex */++intfd;++if(!server_data)+return0;++trace2_region_enter("ipc-server","server-stop-async",NULL);++pthread_mutex_lock(&server_data->work_available_mutex);++server_data->shutdown_requested=1;++/*+*Writeabytetotheshutdownsocketpairtowakeupthe+*accept-thread.+*/+if(write(server_data->accept_thread->fd_send_shutdown,"Q",1)<0)+error_errno("could not write to fd_send_shutdown");++/*+*Drainthequeueofexistingconnections.+*/+while((fd=fifo_dequeue(server_data))!=-1)+close(fd);++/*+*Gentlytellworkerthreadstostopprocessingnewconnections+*andexit.(Thisdoesnotabortin-processconversations.)+*/+pthread_cond_broadcast(&server_data->work_available_cond);++pthread_mutex_unlock(&server_data->work_available_mutex);++trace2_region_leave("ipc-server","server-stop-async",NULL);++return0;+}++/*+*WaitforallIPCserverthreadstostop.+*/+intipc_server_await(structipc_server_data*server_data)+{+pthread_join(server_data->accept_thread->pthread_id,NULL);++if(!server_data->shutdown_requested)+BUG("ipc-server: accept-thread stopped for '%s'",+server_data->buf_path.buf);++while(server_data->worker_thread_list){+structipc_worker_thread_data*wtd=+server_data->worker_thread_list;++pthread_join(wtd->pthread_id,NULL);++server_data->worker_thread_list=wtd->next_thread;+free(wtd);+}++server_data->is_stopped=1;++return0;+}++voidipc_server_free(structipc_server_data*server_data)+{+structipc_accept_thread_data*accept_thread_data;++if(!server_data)+return;++if(!server_data->is_stopped)+BUG("cannot free ipc-server while running for '%s'",+server_data->buf_path.buf);++accept_thread_data=server_data->accept_thread;+if(accept_thread_data){+unix_stream_server__free(accept_thread_data->server_socket);++if(accept_thread_data->fd_send_shutdown!=-1)+close(accept_thread_data->fd_send_shutdown);+if(accept_thread_data->fd_wait_shutdown!=-1)+close(accept_thread_data->fd_wait_shutdown);++free(server_data->accept_thread);+}++while(server_data->worker_thread_list){+structipc_worker_thread_data*wtd=+server_data->worker_thread_list;++server_data->worker_thread_list=wtd->next_thread;+free(wtd);+}++pthread_cond_destroy(&server_data->work_available_cond);+pthread_mutex_destroy(&server_data->work_available_mutex);++strbuf_release(&server_data->buf_path);++free(server_data->fifo_fds);+free(server_data);+}
Isn't this just putting the buffer onto the stack anyway? Your
scratch_space struct is really just a big array. You'd want to make
it static here, but then we haven't really solved anything. :)
I think instead that:
quoted hunk
-int write_packetized_from_fd(int fd_in, int fd_out)+int write_packetized_from_fd(int fd_in, int fd_out,+ struct packet_scratch_space *scratch) {- static char buf[LARGE_PACKET_DATA_MAX]; int err = 0; ssize_t bytes_to_write; while (!err) {- bytes_to_write = xread(fd_in, buf, sizeof(buf));+ bytes_to_write = xread(fd_in, scratch->buffer,+ sizeof(scratch->buffer)); if (bytes_to_write < 0) return COPY_READ_ERROR; if (bytes_to_write == 0) break;- err = packet_write_gently(fd_out, buf, bytes_to_write);+ err = packet_write_gently(fd_out, scratch->buffer,+ bytes_to_write); }
...just heap-allocating the buffer in this function would be fine. It's
one malloc for the whole sequence of pktlines, which is unlikely to be a
problem.
-Peff
From: Jeff King <hidden> Date: 2021-02-26 07:26:33
On Wed, Feb 17, 2021 at 09:48:43PM +0000, Jeff Hostetler via GitGitGadget wrote:
From: Jeff Hostetler <redacted>
The static helper function `unix_stream_socket()` calls `die()`. This
is not appropriate for all callers. Eliminate the wrapper function
and make the callers propagate the error.
Thanks for breaking it up this way. It's (IMHO) much easier to see the
motivation and impact of the changes now.
There's a small typo in the subject:
Subject: unix-socket: elimiate static unix_stream_socket() helper function
...but I thought the plan was to drop this initialization in favor of a
zero-initialization. What you have certainly wouldn't do the wrong
thing, but it just seems weirdly redundant. Unless some caller really
wants to know what the default will be?
-Peff
From: Jeff King <hidden> Date: 2021-02-26 07:57:15
On Wed, Feb 17, 2021 at 09:48:46PM +0000, Jeff Hostetler via GitGitGadget wrote:
From: Jeff Hostetler <redacted>
Create a version of `unix_stream_listen()` that uses a ".lock" lockfile
to create the unix domain socket in a race-free manner.
The "unix_stream_server__listen_with_lock" name is quite a mouthful. My
first question was: don't we have an "options" struct that we can use to
tell it we're interested in using the locking strategy?
But I do find it a little weird for the feature to be at this layer at
all. I'd have thought it would make more sense in the simple-ipc layer
that implements the unix-socket backend, where app-level logic like
"it's OK to just connect to this socket and hang up in order to ping it"
might be more appropriate. We might even want to have a more robust
check (e.g., an actual "ping" that expects the server to say "yes, I'm
here").
(But also see below where I am less certain about this...)
Unix domain sockets have a fundamental problem on Unix systems because
they persist in the filesystem until they are deleted. This is
independent of whether a server is actually listening for connections.
Well-behaved servers are expected to delete the socket when they
shutdown. A new server cannot easily tell if a found socket is
attached to an active server or is leftover cruft from a dead server.
The traditional solution used by `unix_stream_listen()` is to force
delete the socket pathname and then create a new socket. This solves
the latter (cruft) problem, but in the case of the former, it orphans
the existing server (by stealing the pathname associated with the
socket it is listening on).
Nicely explained.
We cannot directly use a .lock lockfile to create the socket because
the socket is created by `bind(2)` rather than the `open(2)` mechanism
used by `tempfile.c`.
As an alternative, we hold a plain lockfile ("<path>.lock") as a
mutual exclusion device. Under the lock, we test if an existing
socket ("<path>") is has an active server. If not, create a new
socket and begin listening. Then we rollback the lockfile in all
cases.
Make sense.
quoted hunk
+static int is_another_server_alive(const char *path,+ const struct unix_stream_listen_opts *opts)+{+ struct stat st;+ int fd;++ if (!lstat(path, &st) && S_ISSOCK(st.st_mode)) {+ /*+ * A socket-inode exists on disk at `path`, but we+ * don't know whether it belongs to an active server+ * or whether the last server died without cleaning+ * up.+ *+ * Poke it with a trivial connection to try to find+ * out.+ */+ fd = unix_stream_connect(path, opts->disallow_chdir);+ if (fd >= 0) {+ close(fd);+ return 1;+ }+ }
The lstat() seems redundant here. unix_stream_connect() will tell us
whether there is something to connect to or not. (It's also racy with
respect to the actual connect, but since you're doing this under lock, I
don't think that matters).
quoted hunk
+struct unix_stream_server_socket *unix_stream_server__listen_with_lock(+ const char *path,+ const struct unix_stream_listen_opts *opts)+{+ struct lock_file lock = LOCK_INIT;+ int fd_socket;+ struct unix_stream_server_socket *server_socket;++ /*+ * Create a lock at "<path>.lock" if we can.+ */+ if (hold_lock_file_for_update_timeout(&lock, path, 0,+ opts->timeout_ms) < 0) {+ error_errno(_("could not lock listener socket '%s'"), path);+ return NULL;+ }
Would you want to ping to see if it's alive before creating the lock?
That would be the fast-path if we assume that a server will usually be
there once started. Or is that supposed to happen in the caller (in
which case I'd again wonder if this really should be happening in the
simple-ipc code).
quoted hunk
+ /*+ * If another server is listening on "<path>" give up. We do not+ * want to create a socket and steal future connections from them.+ */+ if (is_another_server_alive(path, opts)) {+ errno = EADDRINUSE;+ error_errno(_("listener socket already in use '%s'"), path);+ rollback_lock_file(&lock);+ return NULL;+ }
Wouldn't this be a "success" case for a caller? They did not open the
server themselves, but they are presumably happy that there is one there
now to talk to. So do we actually want to print an error to stderr?
Likewise, how do they tell the difference between this NULL and the NULL
we returned above because we couldn't take the lock? Or the NULL we
return below because there is some error creating a listening socket?
I'd think in those three cases you'd want:
- if lock contention, pause a moment and wait for the winner to spin
up and serve requests
- if another server is live while we hold the lock, then we raced them
and they won. Release the lock and start using them.
- if we really tried to call unix_stream_listen() and that failed,
give up now. There is some system error that is not likely to be
fixed by trying anything more (e.g., ENAMETOOLONG).
What do we need this server_socket for? The caller already knows the
path; they fed it to us. We do need to return the descriptor, but we
could do that directly.
+ lstat(path, &server_socket->st_socket);
This lstat I guess is part of your "periodically check to see if we're
still the one holding the socket" strategy. We _shouldn't_ need that
anymore, with the dotlocking, but I'm OK with it as a
belt-and-suspenders check. But why are we filling in the lstat here?
This seems like something that the unix-socket code doesn't really need
to know about (though you do at least provide the complementary
"was_stolen" function here, so that part makes sense).
Again, I guess I'd find it less weird if it were happening at a layer
above. Maybe I'm really just complaining that this is in unix-socket.c.
I guess it is a separate unix_stream_server data type. Arguably that
should go in a separate file, but I guess the whole conditional
compilation of unix-socket.c makes that awkward. So maybe this is the
least-bad thing.
quoted hunk
+ /*+ * Always rollback (just delete) "<path>.lock" because we already created+ * "<path>" as a socket and do not want to commit_lock to do the atomic+ * rename trick.+ */+ rollback_lock_file(&lock);++ return server_socket;+}
OK, this part makes sense to me.
quoted hunk
+void unix_stream_server__free(+ struct unix_stream_server_socket *server_socket)+{+ if (!server_socket)+ return;++ if (server_socket->fd_socket >= 0) {+ if (!unix_stream_server__was_stolen(server_socket))+ unlink(server_socket->path_socket);+ close(server_socket->fd_socket);+ }++ free(server_socket->path_socket);+ free(server_socket);+}
OK, this makes sense. We only remove it if we're still the ones holding
it. That's not done under lock, though, so it's possibly racy (somebody
steals from us while _they_ hold the lock; we check and see "not stolen"
right before they steal it, and then we unlink their stolen copy).
quoted hunk
+int unix_stream_server__was_stolen(+ struct unix_stream_server_socket *server_socket)+{+ struct stat st_now;++ if (!server_socket)+ return 0;++ if (lstat(server_socket->path_socket, &st_now) == -1)+ return 1;++ if (st_now.st_ino != server_socket->st_socket.st_ino)+ return 1;++ /* We might also consider the ctime on some platforms. */++ return 0;+}
You probably should confirm that st.dev matches, too, since that is the
namespace for st.ino. Maybe also double check that it's still a socket
with S_ISSOCK(st_mode)?
-Peff
Isn't this just putting the buffer onto the stack anyway? Your
scratch_space struct is really just a big array. You'd want to make
it static here, but then we haven't really solved anything. :)
Yeah, I was letting the caller decide how to provide the buffer.
They could put it on the stack or allocate it once across a whole
set of files or use a static buffer -- the caller has context for
what works best that we don't have here. For example, the caller
may know that is not in threaded code at all, but we cannot assume
that here.
I think instead that:
quoted
-int write_packetized_from_fd(int fd_in, int fd_out)+int write_packetized_from_fd(int fd_in, int fd_out,+ struct packet_scratch_space *scratch) {- static char buf[LARGE_PACKET_DATA_MAX]; int err = 0; ssize_t bytes_to_write; while (!err) {- bytes_to_write = xread(fd_in, buf, sizeof(buf));+ bytes_to_write = xread(fd_in, scratch->buffer,+ sizeof(scratch->buffer)); if (bytes_to_write < 0) return COPY_READ_ERROR; if (bytes_to_write == 0) break;- err = packet_write_gently(fd_out, buf, bytes_to_write);+ err = packet_write_gently(fd_out, scratch->buffer,+ bytes_to_write); }
...just heap-allocating the buffer in this function would be fine. It's
one malloc for the whole sequence of pktlines, which is unlikely to be a
problem.
Right, I think it would be fine to malloc it here, but I didn't
want to assume that everyone would think that.
I'll change it.
Thanks
Jeff
Isn't this just putting the buffer onto the stack anyway? Your
scratch_space struct is really just a big array. You'd want to make
it static here, but then we haven't really solved anything. :)
Yeah, I was letting the caller decide how to provide the buffer.
They could put it on the stack or allocate it once across a whole
set of files or use a static buffer -- the caller has context for
what works best that we don't have here. For example, the caller
may know that is not in threaded code at all, but we cannot assume
that here.
Yeah, I think it's successfully pushed the problem up to the caller. But
it introduced a _new_ problem in putting the large buffer on the stack.
So if this were "static struct packet_scratch_space scratch", I think
we'd be OK.
And perhaps that would meet your needs (if you just need to call
write_packed_from_fd() in a thread, and not this other caller).
But I do think the heap approach is nice in that it keeps the interface
clean, and I think the performance should be comparable.
Right, I think it would be fine to malloc it here, but I didn't
want to assume that everyone would think that.
I'll change it.
From: Jeff King <hidden> Date: 2021-03-02 15:28:36
On Wed, Feb 17, 2021 at 09:48:48PM +0000, Jeff Hostetler via GitGitGadget wrote:
Create t/helper/test-simple-ipc test tool to exercise the "simple-ipc"
functions.
BTW, one oddity I noticed in this (because of my -Wunused-parameters
branch):
quoted hunk
+#ifndef GIT_WINDOWS_NATIVE+/*+ * This is adapted from `daemonize()`. Use `fork()` to directly create and+ * run the daemon in a child process.+ */+static int spawn_server(const char *path,+ const struct ipc_server_opts *opts,+ pid_t *pid)+{+ *pid = fork();++ switch (*pid) {+ case 0:+ if (setsid() == -1)+ error_errno(_("setsid failed"));+ close(0);+ close(1);+ close(2);+ sanitize_stdfds();++ return ipc_server_run(path, opts, test_app_cb, (void*)&my_app_data);++ case -1:+ return error_errno(_("could not spawn daemon in the background"));++ default:+ return 0;+ }+}
In the non-Windows version, we spawn a server using the "path" parameter
we got from the caller.
But in the Windows version:
quoted hunk
+#else+/*+ * Conceptually like `daemonize()` but different because Windows does not+ * have `fork(2)`. Spawn a normal Windows child process but without the+ * limitations of `start_command()` and `finish_command()`.+ */+static int spawn_server(const char *path,+ const struct ipc_server_opts *opts,+ pid_t *pid)+{+ char test_tool_exe[MAX_PATH];+ struct strvec args = STRVEC_INIT;+ int in, out;++ GetModuleFileNameA(NULL, test_tool_exe, MAX_PATH);++ in = open("/dev/null", O_RDONLY);+ out = open("/dev/null", O_WRONLY);++ strvec_push(&args, test_tool_exe);+ strvec_push(&args, "simple-ipc");+ strvec_push(&args, "run-daemon");+ strvec_pushf(&args, "--threads=%d", opts->nr_threads);++ *pid = mingw_spawnvpe(args.v[0], args.v, NULL, NULL, in, out, out);+ close(in);+ close(out);++ strvec_clear(&args);++ if (*pid < 0)+ return error(_("could not spawn daemon in the background"));++ return 0;+}+#endif
We ignore the "path" parameter entirely. Should we be passing it along
as an option to the child process? I think it doesn't really matter at
this point because both the parent and child processes will use the
hard-coded string "ipc-test", but it seems like something the test
script might want to be able to specify.
-Peff
From: Jeff Hostetler <hidden> Date: 2021-03-03 06:42:17
On 2/26/21 2:56 AM, Jeff King wrote:
On Wed, Feb 17, 2021 at 09:48:46PM +0000, Jeff Hostetler via GitGitGadget wrote:
quoted
From: Jeff Hostetler <redacted>
Create a version of `unix_stream_listen()` that uses a ".lock" lockfile
to create the unix domain socket in a race-free manner.
The "unix_stream_server__listen_with_lock" name is quite a mouthful. My
first question was: don't we have an "options" struct that we can use to
tell it we're interested in using the locking strategy?
But I do find it a little weird for the feature to be at this layer at
all. I'd have thought it would make more sense in the simple-ipc layer
that implements the unix-socket backend, where app-level logic like
"it's OK to just connect to this socket and hang up in order to ping it"
might be more appropriate. We might even want to have a more robust
check (e.g., an actual "ping" that expects the server to say "yes, I'm
here").
I think when I started this, the "safe listen" was much closer to the
original `unix_stream_listen()` and it made sense to keep it nearby,
but as it evolved (and we added lockfiles and etc.) it grew to be more
like its own level between the original socket code and the simple-ipc
layer. Pulling it out into its own source file is probably a good idea
for clarity.
I was thinking that the "ping" is just to see if a server is listening
or not. (And I viewed that as kind of a hack, but it works.) If we
start sending data back and forth, we get into protocols and blocking
and stuff that this layer (even if we move it up a level) doesn't know
about.
I'll pull this out into a new file.
(But also see below where I am less certain about this...)
quoted
Unix domain sockets have a fundamental problem on Unix systems because
they persist in the filesystem until they are deleted. This is
independent of whether a server is actually listening for connections.
Well-behaved servers are expected to delete the socket when they
shutdown. A new server cannot easily tell if a found socket is
attached to an active server or is leftover cruft from a dead server.
The traditional solution used by `unix_stream_listen()` is to force
delete the socket pathname and then create a new socket. This solves
the latter (cruft) problem, but in the case of the former, it orphans
the existing server (by stealing the pathname associated with the
socket it is listening on).
Nicely explained.
quoted
We cannot directly use a .lock lockfile to create the socket because
the socket is created by `bind(2)` rather than the `open(2)` mechanism
used by `tempfile.c`.
As an alternative, we hold a plain lockfile ("<path>.lock") as a
mutual exclusion device. Under the lock, we test if an existing
socket ("<path>") is has an active server. If not, create a new
socket and begin listening. Then we rollback the lockfile in all
cases.
Make sense.
quoted
+static int is_another_server_alive(const char *path,+ const struct unix_stream_listen_opts *opts)+{+ struct stat st;+ int fd;++ if (!lstat(path, &st) && S_ISSOCK(st.st_mode)) {+ /*+ * A socket-inode exists on disk at `path`, but we+ * don't know whether it belongs to an active server+ * or whether the last server died without cleaning+ * up.+ *+ * Poke it with a trivial connection to try to find+ * out.+ */+ fd = unix_stream_connect(path, opts->disallow_chdir);+ if (fd >= 0) {+ close(fd);+ return 1;+ }+ }
The lstat() seems redundant here. unix_stream_connect() will tell us
whether there is something to connect to or not. (It's also racy with
respect to the actual connect, but since you're doing this under lock, I
don't think that matters).
I agree. I'll get rid of the lstat().
quoted
+struct unix_stream_server_socket *unix_stream_server__listen_with_lock(+ const char *path,+ const struct unix_stream_listen_opts *opts)+{+ struct lock_file lock = LOCK_INIT;+ int fd_socket;+ struct unix_stream_server_socket *server_socket;++ /*+ * Create a lock at "<path>.lock" if we can.+ */+ if (hold_lock_file_for_update_timeout(&lock, path, 0,+ opts->timeout_ms) < 0) {+ error_errno(_("could not lock listener socket '%s'"), path);+ return NULL;+ }
Would you want to ping to see if it's alive before creating the lock?
That would be the fast-path if we assume that a server will usually be
there once started. Or is that supposed to happen in the caller (in
which case I'd again wonder if this really should be happening in the
simple-ipc code).
Starting a server should not happen that often, so I'm not sure it
matters. And yes, a server once started should run for a long time.
Pinging without the lock puts us back in another race, so we might as
well lock first.
quoted
+ /*+ * If another server is listening on "<path>" give up. We do not+ * want to create a socket and steal future connections from them.+ */+ if (is_another_server_alive(path, opts)) {+ errno = EADDRINUSE;+ error_errno(_("listener socket already in use '%s'"), path);+ rollback_lock_file(&lock);+ return NULL;+ }
Wouldn't this be a "success" case for a caller? They did not open the
server themselves, but they are presumably happy that there is one there
now to talk to. So do we actually want to print an error to stderr?
Likewise, how do they tell the difference between this NULL and the NULL
we returned above because we couldn't take the lock? Or the NULL we
return below because there is some error creating a listening socket?
I'd think in those three cases you'd want:
- if lock contention, pause a moment and wait for the winner to spin
up and serve requests
- if another server is live while we hold the lock, then we raced them
and they won. Release the lock and start using them.
- if we really tried to call unix_stream_listen() and that failed,
give up now. There is some system error that is not likely to be
fixed by trying anything more (e.g., ENAMETOOLONG).
Yes, I want to move the error messages out of these library layers.
And yes, if another server is running, our server instance should
shutdown gracefully. Other client processes can just talk to them
rather than us.
What do we need this server_socket for? The caller already knows the
path; they fed it to us. We do need to return the descriptor, but we
could do that directly.
I wanted a wrapper struct to persist a copy of the pathname near
the fd. Later when we get ready to shutdown, we can close and unlink
without worrying whether our caller kept their copy of the path buffer.
This also lets me have the pathname to poll and check for theft during
the accept thread's event loop.
quoted
+ lstat(path, &server_socket->st_socket);
This lstat I guess is part of your "periodically check to see if we're
still the one holding the socket" strategy. We _shouldn't_ need that
anymore, with the dotlocking, but I'm OK with it as a
belt-and-suspenders check. But why are we filling in the lstat here?
This seems like something that the unix-socket code doesn't really need
to know about (though you do at least provide the complementary
"was_stolen" function here, so that part makes sense).
The dotlock is only on disk for the duration of the socket setup.
We do the rollback (to delete the lockfile) once we have the socket
open and ready for business.
The lstat gives me the inode of the socket on disk and we can watch
it with future lstat's in the event loop and see if it changes and
detect theft and auto-shutdown.
Again, I guess I'd find it less weird if it were happening at a layer
above. Maybe I'm really just complaining that this is in unix-socket.c.
I guess it is a separate unix_stream_server data type. Arguably that
should go in a separate file, but I guess the whole conditional
compilation of unix-socket.c makes that awkward. So maybe this is the
least-bad thing.
Yeah, I'll move it out.
And yes, the whole conditional compilation thing was something I was
hesitating on, but it really isn't that bad. (But I should not brag
here until all of the build servers have had their say....)
quoted
+ /*+ * Always rollback (just delete) "<path>.lock" because we already created+ * "<path>" as a socket and do not want to commit_lock to do the atomic+ * rename trick.+ */+ rollback_lock_file(&lock);++ return server_socket;+}
OK, this part makes sense to me.
quoted
+void unix_stream_server__free(+ struct unix_stream_server_socket *server_socket)+{+ if (!server_socket)+ return;++ if (server_socket->fd_socket >= 0) {+ if (!unix_stream_server__was_stolen(server_socket))+ unlink(server_socket->path_socket);+ close(server_socket->fd_socket);+ }++ free(server_socket->path_socket);+ free(server_socket);+}
OK, this makes sense. We only remove it if we're still the ones holding
it. That's not done under lock, though, so it's possibly racy (somebody
steals from us while _they_ hold the lock; we check and see "not stolen"
right before they steal it, and then we unlink their stolen copy).
Right, I didn't bother with the lock here. I don't think we need it.
We technically still have the socket open and are listening on it when
we lstat and unlink it. The other process should create the lock and
try to connect. That should hang in the kernel because of the accept()
grace period. Then we close the socket and the client's connection
request errors because we didn't accept it. They will see the error
as no one is listening and then create their own socket.
quoted
+int unix_stream_server__was_stolen(+ struct unix_stream_server_socket *server_socket)+{+ struct stat st_now;++ if (!server_socket)+ return 0;++ if (lstat(server_socket->path_socket, &st_now) == -1)+ return 1;++ if (st_now.st_ino != server_socket->st_socket.st_ino)+ return 1;++ /* We might also consider the ctime on some platforms. */++ return 0;+}
You probably should confirm that st.dev matches, too, since that is the
namespace for st.ino. Maybe also double check that it's still a socket
with S_ISSOCK(st_mode)?
Good point.
-Peff
Thanks for all the careful study. I'll push up a new series to
address them shortly.
Jeff
From: Jeff Hostetler <hidden> Date: 2021-03-04 00:23:31
On 3/2/21 4:44 AM, Jeff King wrote:
On Wed, Feb 17, 2021 at 09:48:48PM +0000, Jeff Hostetler via GitGitGadget wrote:
quoted
Create t/helper/test-simple-ipc test tool to exercise the "simple-ipc"
functions.
BTW, one oddity I noticed in this (because of my -Wunused-parameters
branch):
quoted
+#ifndef GIT_WINDOWS_NATIVE+/*+ * This is adapted from `daemonize()`. Use `fork()` to directly create and+ * run the daemon in a child process.+ */+static int spawn_server(const char *path,+ const struct ipc_server_opts *opts,+ pid_t *pid)+{+ *pid = fork();++ switch (*pid) {+ case 0:+ if (setsid() == -1)+ error_errno(_("setsid failed"));+ close(0);+ close(1);+ close(2);+ sanitize_stdfds();++ return ipc_server_run(path, opts, test_app_cb, (void*)&my_app_data);++ case -1:+ return error_errno(_("could not spawn daemon in the background"));++ default:+ return 0;+ }+}
In the non-Windows version, we spawn a server using the "path" parameter
we got from the caller.
But in the Windows version:
quoted
+#else+/*+ * Conceptually like `daemonize()` but different because Windows does not+ * have `fork(2)`. Spawn a normal Windows child process but without the+ * limitations of `start_command()` and `finish_command()`.+ */+static int spawn_server(const char *path,+ const struct ipc_server_opts *opts,+ pid_t *pid)+{+ char test_tool_exe[MAX_PATH];+ struct strvec args = STRVEC_INIT;+ int in, out;++ GetModuleFileNameA(NULL, test_tool_exe, MAX_PATH);++ in = open("/dev/null", O_RDONLY);+ out = open("/dev/null", O_WRONLY);++ strvec_push(&args, test_tool_exe);+ strvec_push(&args, "simple-ipc");+ strvec_push(&args, "run-daemon");+ strvec_pushf(&args, "--threads=%d", opts->nr_threads);++ *pid = mingw_spawnvpe(args.v[0], args.v, NULL, NULL, in, out, out);+ close(in);+ close(out);++ strvec_clear(&args);++ if (*pid < 0)+ return error(_("could not spawn daemon in the background"));++ return 0;+}+#endif
We ignore the "path" parameter entirely. Should we be passing it along
as an option to the child process? I think it doesn't really matter at
this point because both the parent and child processes will use the
hard-coded string "ipc-test", but it seems like something the test
script might want to be able to specify.
-Peff
Yeah, since it was a test helper I hesitated to add a command line
arg to pass it to the child process (when all callers were right here
and using the same default value). However it would be good to do so
in case we want to write more complicated tests.
Jeff
From: Jeff King <hidden> Date: 2021-03-04 15:15:48
On Tue, Mar 02, 2021 at 06:50:51PM -0500, Jeff Hostetler wrote:
I was thinking that the "ping" is just to see if a server is listening
or not. (And I viewed that as kind of a hack, but it works.) If we
start sending data back and forth, we get into protocols and blocking
and stuff that this layer (even if we move it up a level) doesn't know
about.
Right. Definitely the higher up the stack the ping happens, the more
value it has. But I also see the appeal of keeping this as its own
layer.
quoted
quoted
+ if (hold_lock_file_for_update_timeout(&lock, path, 0,+ opts->timeout_ms) < 0) {+ error_errno(_("could not lock listener socket '%s'"), path);+ return NULL;+ }
Would you want to ping to see if it's alive before creating the lock?
That would be the fast-path if we assume that a server will usually be
there once started. Or is that supposed to happen in the caller (in
which case I'd again wonder if this really should be happening in the
simple-ipc code).
Starting a server should not happen that often, so I'm not sure it
matters. And yes, a server once started should run for a long time.
Pinging without the lock puts us back in another race, so we might as
well lock first.
Definitely you need to ping under lock to avoid races. But I was
thinking of an additional optimistic ping before we take the lock. I
agree that starting the server should be rare, which is why I think
there's value in seeing "is it up" before taking any lock.
But I suspect your thinking is that this ping happens in the caller
anyway, before we hit any of this unix_socket_listen() code at all. And
that makes sense to me. In fact, I guess it has to happen that way,
because "try to connect" and "try to spin up a server" are likely
happening in two separate processes entirely (we only spawn the second
one if the first one failed its ping).
quoted
I'd think in those three cases you'd want:
- if lock contention, pause a moment and wait for the winner to spin
up and serve requests
- if another server is live while we hold the lock, then we raced them
and they won. Release the lock and start using them.
- if we really tried to call unix_stream_listen() and that failed,
give up now. There is some system error that is not likely to be
fixed by trying anything more (e.g., ENAMETOOLONG).
Yes, I want to move the error messages out of these library layers.
And yes, if another server is running, our server instance should
shutdown gracefully. Other client processes can just talk to them
rather than us.
Right, that makes sense. Again, I was thinking earlier of the whole "try
to connect, but spin up a server otherwise" thing happening in a single
process. But by the time we get to the listen code, we have probably
already spawned a server process, and have redirected its stderr
somewhere. And likewise the caller doesn't even care that much if the
server reports an error because it somebody else won the race. It only
cares that after a few connect attempts it manages to talk to
_somebody_.
quoted
quoted
+ lstat(path, &server_socket->st_socket);
This lstat I guess is part of your "periodically check to see if we're
still the one holding the socket" strategy. We _shouldn't_ need that
anymore, with the dotlocking, but I'm OK with it as a
belt-and-suspenders check. But why are we filling in the lstat here?
This seems like something that the unix-socket code doesn't really need
to know about (though you do at least provide the complementary
"was_stolen" function here, so that part makes sense).
The dotlock is only on disk for the duration of the socket setup.
We do the rollback (to delete the lockfile) once we have the socket
open and ready for business.
The lstat gives me the inode of the socket on disk and we can watch
it with future lstat's in the event loop and see if it changes and
detect theft and auto-shutdown.
Right, I gradually came to the understanding of what your extra layer
was trying to accomplish while reading (sometimes I'll go back and edit
earlier comments in my review before sending out the mail, but in this
case it seemed less confusing to leave my train of thought in place.
That might not have been correct, though. ;) ).
I think if everybody is abiding by the lock system to create the socket,
we probably don't strictly _need_ the theft detection. But it might not
hurt as a belt-and-suspenders, or for cases where somebody thinks the
socket is stale but it isn't (perhaps due to listen backlog or
something while trying to do the connect() ping).
quoted
quoted
+void unix_stream_server__free(+ struct unix_stream_server_socket *server_socket)+{+ if (!server_socket)+ return;++ if (server_socket->fd_socket >= 0) {+ if (!unix_stream_server__was_stolen(server_socket))+ unlink(server_socket->path_socket);+ close(server_socket->fd_socket);+ }++ free(server_socket->path_socket);+ free(server_socket);+}
OK, this makes sense. We only remove it if we're still the ones holding
it. That's not done under lock, though, so it's possibly racy (somebody
steals from us while _they_ hold the lock; we check and see "not stolen"
right before they steal it, and then we unlink their stolen copy).
Right, I didn't bother with the lock here. I don't think we need it.
We technically still have the socket open and are listening on it when
we lstat and unlink it. The other process should create the lock and
try to connect. That should hang in the kernel because of the accept()
grace period. Then we close the socket and the client's connection
request errors because we didn't accept it. They will see the error
as no one is listening and then create their own socket.
I think there are still some races (at least if we believe that anything
can be stolen in the first place). Something like:
- process A holds the socket but plans to exit
- process B takes the lock
- process B tries to ping us, but it doesn't work for some reason
(this part is vague, but it's also the thing that makes stealing
possible at all)
- process A calls was_stolen(), which says "no"
- process B decides nobody is there, so it unlinks the socket and
creates its own
- process A calls unlink(), removing B's socket
A is OK with this; it was exiting anyway. But it just stranded B, who
_thinks_ it owns the socket, but doesn't.
Again, there's a vagueness to "B somehow doesn't see A as listening" in
the middle step. But without that step, I don't see how you'd really
have stealing in the first place.
-Peff
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-09 15:03:22
From: Jeff Hostetler <redacted>
Teach `packet_write_gently()` to write the pkt-line header and the actual
buffer in 2 separate calls to `write_in_full()` and avoid the need for a
static buffer, thread-safe scratch space, or an excessively large stack
buffer.
Change `write_packetized_from_fd()` to allocate a temporary buffer rather
than using a static buffer to avoid similar issues here.
These changes are intended to make it easier to use pkt-line routines in
a multi-threaded context with multiple concurrent writers writing to
different streams.
Signed-off-by: Jeff Hostetler <redacted>
---
pkt-line.c | 27 +++++++++++++++++++--------
1 file changed, 19 insertions(+), 8 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-09 15:03:52
From: Jeff Hostetler <redacted>
Brief design documentation for new IPC mechanism allowing
foreground Git client to talk with an existing daemon process
at a known location using a named pipe or unix domain socket.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
Documentation/technical/api-simple-ipc.txt | 105 +++++++++++++++++++++
1 file changed, 105 insertions(+)
create mode 100644 Documentation/technical/api-simple-ipc.txt
@@ -0,0 +1,105 @@+Simple-IPC API+==============++The Simple-IPC API is a collection of `ipc_` prefixed library routines+and a basic communication protocol that allow an IPC-client process to+send an application-specific IPC-request message to an IPC-server+process and receive an application-specific IPC-response message.++Communication occurs over a named pipe on Windows and a Unix domain+socket on other platforms. IPC-clients and IPC-servers rendezvous at+a previously agreed-to application-specific pathname (which is outside+the scope of this design) that is local to the computer system.++The IPC-server routines within the server application process create a+thread pool to listen for connections and receive request messages+from multiple concurrent IPC-clients. When received, these messages+are dispatched up to the server application callbacks for handling.+IPC-server routines then incrementally relay responses back to the+IPC-client.++The IPC-client routines within a client application process connect+to the IPC-server and send a request message and wait for a response.+When received, the response is returned back the caller.++For example, the `fsmonitor--daemon` feature will be built as a server+application on top of the IPC-server library routines. It will have+threads watching for file system events and a thread pool waiting for+client connections. Clients, such as `git status` will request a list+of file system events since a point in time and the server will+respond with a list of changed files and directories. The formats of+the request and response are application-specific; the IPC-client and+IPC-server routines treat them as opaque byte streams.+++Comparison with sub-process model+---------------------------------++The Simple-IPC mechanism differs from the existing `sub-process.c`+model (Documentation/technical/long-running-process-protocol.txt) and+used by applications like Git-LFS. In the LFS-style sub-process model+the helper is started by the foreground process, communication happens+via a pair of file descriptors bound to the stdin/stdout of the+sub-process, the sub-process only serves the current foreground+process, and the sub-process exits when the foreground process+terminates.++In the Simple-IPC model the server is a very long-running service. It+can service many clients at the same time and has a private socket or+named pipe connection to each active client. It might be started+(on-demand) by the current client process or it might have been+started by a previous client or by the OS at boot time. The server+process is not associated with a terminal and it persists after+clients terminate. Clients do not have access to the stdin/stdout of+the server process and therefore must communicate over sockets or+named pipes.+++Server startup and shutdown+---------------------------++How an application server based upon IPC-server is started is also+outside the scope of the Simple-IPC design and is a property of the+application using it. For example, the server might be started or+restarted during routine maintenance operations, or it might be+started as a system service during the system boot-up sequence, or it+might be started on-demand by a foreground Git command when needed.++Similarly, server shutdown is a property of the application using+the simple-ipc routines. For example, the server might decide to+shutdown when idle or only upon explicit request.+++Simple-IPC protocol+-------------------++The Simple-IPC protocol consists of a single request message from the+client and an optional response message from the server. Both the+client and server messages are unlimited in length and are terminated+with a flush packet.++The pkt-line routines (Documentation/technical/protocol-common.txt)+are used to simplify buffer management during message generation,+transmission, and reception. A flush packet is used to mark the end+of the message. This allows the sender to incrementally generate and+transmit the message. It allows the receiver to incrementally receive+the message in chunks and to know when they have received the entire+message.++The actual byte format of the client request and server response+messages are application specific. The IPC layer transmits and+receives them as opaque byte buffers without any concern for the+content within. It is the job of the calling application layer to+understand the contents of the request and response messages.+++Summary+-------++Conceptually, the Simple-IPC protocol is similar to an HTTP REST+request. Clients connect, make an application-specific and+stateless request, receive an application-specific+response, and disconnect. It is a one round trip facility for+querying the server. The Simple-IPC routines hide the socket,+named pipe, and thread pool details and allow the application+layer to focus on the application at hand.
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-09 15:03:53
From: Jeff Hostetler <redacted>
The static helper function `unix_stream_socket()` calls `die()`. This
is not appropriate for all callers. Eliminate the wrapper function
and make the callers propagate the error.
Signed-off-by: Jeff Hostetler <redacted>
---
unix-socket.c | 27 +++++++++++++--------------
1 file changed, 13 insertions(+), 14 deletions(-)
@@ -0,0 +1,751 @@+#include"cache.h"+#include"simple-ipc.h"+#include"strbuf.h"+#include"pkt-line.h"+#include"thread-utils.h"++#ifndef GIT_WINDOWS_NATIVE+#error This file can only be compiled on Windows+#endif++staticintinitialize_pipe_name(constchar*path,wchar_t*wpath,size_talloc)+{+intoff=0;+structstrbufrealpath=STRBUF_INIT;++if(!strbuf_realpath(&realpath,path,0))+return-1;++off=swprintf(wpath,alloc,L"\\\\.\\pipe\\");+if(xutftowcs(wpath+off,realpath.buf,alloc-off)<0)+return-1;++/* Handle drive prefix */+if(wpath[off]&&wpath[off+1]==L':'){+wpath[off+1]=L'_';+off+=2;+}++for(;wpath[off];off++)+if(wpath[off]==L'/')+wpath[off]=L'\\';++strbuf_release(&realpath);+return0;+}++staticenumipc_active_stateget_active_state(wchar_t*pipe_path)+{+if(WaitNamedPipeW(pipe_path,NMPWAIT_USE_DEFAULT_WAIT))+returnIPC_STATE__LISTENING;++if(GetLastError()==ERROR_SEM_TIMEOUT)+returnIPC_STATE__NOT_LISTENING;++if(GetLastError()==ERROR_FILE_NOT_FOUND)+returnIPC_STATE__PATH_NOT_FOUND;++returnIPC_STATE__OTHER_ERROR;+}++enumipc_active_stateipc_get_active_state(constchar*path)+{+wchar_tpipe_path[MAX_PATH];++if(initialize_pipe_name(path,pipe_path,ARRAY_SIZE(pipe_path))<0)+returnIPC_STATE__INVALID_PATH;++returnget_active_state(pipe_path);+}++#define WAIT_STEP_MS (50)++staticenumipc_active_stateconnect_to_server(+constwchar_t*wpath,+DWORDtimeout_ms,+conststructipc_client_connect_options*options,+int*pfd)+{+DWORDt_start_ms,t_waited_ms;+DWORDstep_ms;+HANDLEhPipe=INVALID_HANDLE_VALUE;+DWORDmode=PIPE_READMODE_BYTE;+DWORDgle;++*pfd=-1;++for(;;){+hPipe=CreateFileW(wpath,GENERIC_READ|GENERIC_WRITE,+0,NULL,OPEN_EXISTING,0,NULL);+if(hPipe!=INVALID_HANDLE_VALUE)+break;++gle=GetLastError();++switch(gle){+caseERROR_FILE_NOT_FOUND:+if(!options->wait_if_not_found)+returnIPC_STATE__PATH_NOT_FOUND;+if(!timeout_ms)+returnIPC_STATE__PATH_NOT_FOUND;++step_ms=(timeout_ms<WAIT_STEP_MS)?+timeout_ms:WAIT_STEP_MS;+sleep_millisec(step_ms);++timeout_ms-=step_ms;+break;/* try again */++caseERROR_PIPE_BUSY:+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;+if(!timeout_ms)+returnIPC_STATE__NOT_LISTENING;++t_start_ms=(DWORD)(getnanotime()/1000000);++if(!WaitNamedPipeW(wpath,timeout_ms)){+if(GetLastError()==ERROR_SEM_TIMEOUT)+returnIPC_STATE__NOT_LISTENING;++returnIPC_STATE__OTHER_ERROR;+}++/*+*Apipeserverinstancebecameavailable.+*Raceotherclientprocessestoconnectto+*it.+*+*Butfirstdecrementouroveralltimeoutso+*thatwedon'tstarveifwekeeplosingthe+*race.Butalsoguardagainstspecial+*NPMWAIT_values(0and-1).+*/+t_waited_ms=(DWORD)(getnanotime()/1000000)-t_start_ms;+if(t_waited_ms<timeout_ms)+timeout_ms-=t_waited_ms;+else+timeout_ms=1;+break;/* try again */++default:+returnIPC_STATE__OTHER_ERROR;+}+}++if(!SetNamedPipeHandleState(hPipe,&mode,NULL,NULL)){+CloseHandle(hPipe);+returnIPC_STATE__OTHER_ERROR;+}++*pfd=_open_osfhandle((intptr_t)hPipe,O_RDWR|O_BINARY);+if(*pfd<0){+CloseHandle(hPipe);+returnIPC_STATE__OTHER_ERROR;+}++/* fd now owns hPipe */++returnIPC_STATE__LISTENING;+}++/*+*ThedefaultconnectiontimeoutforWindowsclients.+*+*Thisisnotcurrentlypartoftheipc_API(northeconfigsettings)+*becauseofdifferencesbetweenWindowsandotherplatforms.+*+*Thisvaluewaschosenatrandom.+*/+#define WINDOWS_CONNECTION_TIMEOUT_MS (30000)++enumipc_active_stateipc_client_try_connect(+constchar*path,+conststructipc_client_connect_options*options,+structipc_client_connection**p_connection)+{+wchar_twpath[MAX_PATH];+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+intfd=-1;++*p_connection=NULL;++trace2_region_enter("ipc-client","try-connect",NULL);+trace2_data_string("ipc-client",NULL,"try-connect/path",path);++if(initialize_pipe_name(path,wpath,ARRAY_SIZE(wpath))<0)+state=IPC_STATE__INVALID_PATH;+else+state=connect_to_server(wpath,WINDOWS_CONNECTION_TIMEOUT_MS,+options,&fd);++trace2_data_intmax("ipc-client",NULL,"try-connect/state",+(intmax_t)state);+trace2_region_leave("ipc-client","try-connect",NULL);++if(state==IPC_STATE__LISTENING){+(*p_connection)=xcalloc(1,sizeof(structipc_client_connection));+(*p_connection)->fd=fd;+}++returnstate;+}++voidipc_client_close_connection(structipc_client_connection*connection)+{+if(!connection)+return;++if(connection->fd!=-1)+close(connection->fd);++free(connection);+}++intipc_client_send_command_to_connection(+structipc_client_connection*connection,+constchar*message,structstrbuf*answer)+{+intret=0;++strbuf_setlen(answer,0);++trace2_region_enter("ipc-client","send-command",NULL);++if(write_packetized_from_buf_no_flush(message,strlen(message),+connection->fd)<0||+packet_flush_gently(connection->fd)<0){+ret=error(_("could not send IPC command"));+gotodone;+}++FlushFileBuffers((HANDLE)_get_osfhandle(connection->fd));++if(read_packetized_to_strbuf(+connection->fd,answer,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR)<0){+ret=error(_("could not read IPC response"));+gotodone;+}++done:+trace2_region_leave("ipc-client","send-command",NULL);+returnret;+}++intipc_client_send_command(constchar*path,+conststructipc_client_connect_options*options,+constchar*message,structstrbuf*response)+{+intret=-1;+enumipc_active_statestate;+structipc_client_connection*connection=NULL;++state=ipc_client_try_connect(path,options,&connection);++if(state!=IPC_STATE__LISTENING)+returnret;++ret=ipc_client_send_command_to_connection(connection,message,response);++ipc_client_close_connection(connection);++returnret;+}++/*+*Duplicatethegivenpipehandleandwrapitinafiledescriptorso+*thatwecanusepkt-lineonit.+*/+staticintdup_fd_from_pipe(constHANDLEpipe)+{+HANDLEprocess=GetCurrentProcess();+HANDLEhandle;+intfd;++if(!DuplicateHandle(process,pipe,process,&handle,0,FALSE,+DUPLICATE_SAME_ACCESS)){+errno=err_win_to_posix(GetLastError());+return-1;+}++fd=_open_osfhandle((intptr_t)handle,O_RDWR|O_BINARY);+if(fd<0){+errno=err_win_to_posix(GetLastError());+CloseHandle(handle);+return-1;+}++/*+*`handle`isnowownedby`fd`andwillbeautomaticallyclosed+*whenthedescriptorisclosed.+*/++returnfd;+}++/*+*Magicnumbersusedtoannotatecallbackinstancedata.+*Theseareusedtohelpguardagainstaccidentallypassingthe+*wronginstancedataacrossmultiplelevelsofcallbacks(which+*iseasytodoifthereare`void*`arguments).+*/+enummagic{+MAGIC_SERVER_REPLY_DATA,+MAGIC_SERVER_THREAD_DATA,+MAGIC_SERVER_DATA,+};++structipc_server_reply_data{+enummagicmagic;+intfd;+structipc_server_thread_data*server_thread_data;+};++structipc_server_thread_data{+enummagicmagic;+structipc_server_thread_data*next_thread;+structipc_server_data*server_data;+pthread_tpthread_id;+HANDLEhPipe;+};++/*+*OnWindows,theconceptual"ipc-server"isimplementedasapoolof+*nidential/peer"server-thread"threads.Thatis,thereisno+*hierarchyofthreads;andthereforenocontrollerthreadmanaging+*thepool.Eachthreadhasanindependenthandletothenamedpipe,+*receivesincomingconnections,processestheclient,andre-uses+*thepipeforthenextclientconnection.+*+*Therefore,the"ipc-server"onlyneedstomaintainalistofthe+*spawnedthreadsforeventual"join"purposes.+*+*Asingle"stop-event"isvisibletoalloftheserverthreadsto+*tellthemtoshutdown(whenidle).+*/+structipc_server_data{+enummagicmagic;+ipc_server_application_cb*application_cb;+void*application_data;+structstrbufbuf_path;+wchar_twpath[MAX_PATH];++HANDLEhEventStopRequested;+structipc_server_thread_data*thread_list;+intis_stopped;+};++enumconnect_result{+CR_CONNECTED=0,+CR_CONNECT_PENDING,+CR_CONNECT_ERROR,+CR_WAIT_ERROR,+CR_SHUTDOWN,+};++staticenumconnect_resultqueue_overlapped_connect(+structipc_server_thread_data*server_thread_data,+OVERLAPPED*lpo)+{+if(ConnectNamedPipe(server_thread_data->hPipe,lpo))+gotofailed;++switch(GetLastError()){+caseERROR_IO_PENDING:+returnCR_CONNECT_PENDING;++caseERROR_PIPE_CONNECTED:+SetEvent(lpo->hEvent);+returnCR_CONNECTED;++default:+break;+}++failed:+error(_("ConnectNamedPipe failed for '%s' (%lu)"),+server_thread_data->server_data->buf_path.buf,+GetLastError());+returnCR_CONNECT_ERROR;+}++/*+*UseWindowsOverlappedIOtowaitforaconnectionorforourevent+*tobesignalled.+*/+staticenumconnect_resultwait_for_connection(+structipc_server_thread_data*server_thread_data,+OVERLAPPED*lpo)+{+enumconnect_resultr;+HANDLEwaitHandles[2];+DWORDdwWaitResult;++r=queue_overlapped_connect(server_thread_data,lpo);+if(r!=CR_CONNECT_PENDING)+returnr;++waitHandles[0]=server_thread_data->server_data->hEventStopRequested;+waitHandles[1]=lpo->hEvent;++dwWaitResult=WaitForMultipleObjects(2,waitHandles,FALSE,INFINITE);+switch(dwWaitResult){+caseWAIT_OBJECT_0+0:+returnCR_SHUTDOWN;++caseWAIT_OBJECT_0+1:+ResetEvent(lpo->hEvent);+returnCR_CONNECTED;++default:+returnCR_WAIT_ERROR;+}+}++/*+*Forwarddeclareourreplycallbackfunctionsothatanycompiler+*errorsarereportedwhenweactuallydefinethefunction(inaddition+*toanyerrorsreportedwhenwetrytopassthiscallbackfunctionas+*aparameterinafunctioncall).Theformerareeasiertounderstand.+*/+staticipc_server_reply_cbdo_io_reply_callback;++/*+*Relayapplication'sresponsemessagetotheclientprocess.+*(Wedonotflushatthispointbecauseweallowthecaller+*tochunkdatatotheclientthruus.)+*/+staticintdo_io_reply_callback(structipc_server_reply_data*reply_data,+constchar*response,size_tresponse_len)+{+if(reply_data->magic!=MAGIC_SERVER_REPLY_DATA)+BUG("reply_cb called with wrong instance data");++returnwrite_packetized_from_buf_no_flush(response,response_len,+reply_data->fd);+}++/*+*Receivetherequest/commandfromtheclientandpassittothe+*registeredrequest-callback.Therequest-callbackwillcompose+*aresponseandcallourreply-callbacktosendittotheclient.+*+*Simple-IPConlycontainsoneroundtrip,soweflushandclose+*hereaftertheresponse.+*/+staticintdo_io(structipc_server_thread_data*server_thread_data)+{+structstrbufbuf=STRBUF_INIT;+structipc_server_reply_datareply_data;+intret=0;++reply_data.magic=MAGIC_SERVER_REPLY_DATA;+reply_data.server_thread_data=server_thread_data;++reply_data.fd=dup_fd_from_pipe(server_thread_data->hPipe);+if(reply_data.fd<0)+returnerror(_("could not create fd from pipe for '%s'"),+server_thread_data->server_data->buf_path.buf);++ret=read_packetized_to_strbuf(+reply_data.fd,&buf,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR);+if(ret>=0){+ret=server_thread_data->server_data->application_cb(+server_thread_data->server_data->application_data,+buf.buf,do_io_reply_callback,&reply_data);++packet_flush_gently(reply_data.fd);++FlushFileBuffers((HANDLE)_get_osfhandle((reply_data.fd)));+}+else{+/*+*Theclientprobablydisconnected/shutdownbeforeit+*couldsendawell-formedmessage.Ignoreit.+*/+}++strbuf_release(&buf);+close(reply_data.fd);++returnret;+}++/*+*HandleIPCrequestandresponsewiththisconnectedclient.Andreset+*thepipetoprepareforthenextclient.+*/+staticintuse_connection(structipc_server_thread_data*server_thread_data)+{+intret;++ret=do_io(server_thread_data);++FlushFileBuffers(server_thread_data->hPipe);+DisconnectNamedPipe(server_thread_data->hPipe);++returnret;+}++/*+*ThreadprocforanIPCserverworkerthread.Ithandlesaseriesof+*connectionsfromclients.ItcleansandreusesthehPipebetweeneach+*client.+*/+staticvoid*server_thread_proc(void*_server_thread_data)+{+structipc_server_thread_data*server_thread_data=_server_thread_data;+HANDLEhEventConnected=INVALID_HANDLE_VALUE;+OVERLAPPEDoConnect;+enumconnect_resultcr;+intret;++assert(server_thread_data->hPipe!=INVALID_HANDLE_VALUE);++trace2_thread_start("ipc-server");+trace2_data_string("ipc-server",NULL,"pipe",+server_thread_data->server_data->buf_path.buf);++hEventConnected=CreateEventW(NULL,TRUE,FALSE,NULL);++memset(&oConnect,0,sizeof(oConnect));+oConnect.hEvent=hEventConnected;++for(;;){+cr=wait_for_connection(server_thread_data,&oConnect);++switch(cr){+caseCR_SHUTDOWN:+gotofinished;++caseCR_CONNECTED:+ret=use_connection(server_thread_data);+if(ret==SIMPLE_IPC_QUIT){+ipc_server_stop_async(+server_thread_data->server_data);+gotofinished;+}+if(ret>0){+/*+*Ignore(transient)IOerrorswiththis+*clientandresetforthenextclient.+*/+}+break;++caseCR_CONNECT_PENDING:+/* By construction, this should not happen. */+BUG("ipc-server[%s]: unexpeced CR_CONNECT_PENDING",+server_thread_data->server_data->buf_path.buf);++caseCR_CONNECT_ERROR:+caseCR_WAIT_ERROR:+/*+*Ignorethesetheoreticalerrors.+*/+DisconnectNamedPipe(server_thread_data->hPipe);+break;++default:+BUG("unandled case after wait_for_connection");+}+}++finished:+CloseHandle(server_thread_data->hPipe);+CloseHandle(hEventConnected);++trace2_thread_exit();+returnNULL;+}++staticHANDLEcreate_new_pipe(wchar_t*wpath,intis_first)+{+HANDLEhPipe;+DWORDdwOpenMode,dwPipeMode;+LPSECURITY_ATTRIBUTESlpsa=NULL;++dwOpenMode=PIPE_ACCESS_INBOUND|PIPE_ACCESS_OUTBOUND|+FILE_FLAG_OVERLAPPED;++dwPipeMode=PIPE_TYPE_MESSAGE|PIPE_READMODE_BYTE|PIPE_WAIT|+PIPE_REJECT_REMOTE_CLIENTS;++if(is_first){+dwOpenMode|=FILE_FLAG_FIRST_PIPE_INSTANCE;++/*+*OnWindows,thefirstserverpipeinstancegetsto+*settheACL/SecurityAttributesonthenamed+*pipe;subsequentinstancesinheritandcannot+*changethem.+*+*TODOShouldweallowtheapplicationlayerto+*specifysecurityattributes,suchas`LocalService`+*or`LocalSystem`,whenwecreatethenamedpipe?+*Thisquestionisprobablynotimportantwhenthe+*daemonisstartedbyaforegrounduserprocessand+*onlyneedstotalktothecurrentuser,butmaybe+*ifthedaemonisrunviatheControlPanelasa+*SystemService.+*/+}++hPipe=CreateNamedPipeW(wpath,dwOpenMode,dwPipeMode,+PIPE_UNLIMITED_INSTANCES,1024,1024,0,lpsa);++returnhPipe;+}++intipc_server_run_async(structipc_server_data**returned_server_data,+constchar*path,conststructipc_server_opts*opts,+ipc_server_application_cb*application_cb,+void*application_data)+{+structipc_server_data*server_data;+wchar_twpath[MAX_PATH];+HANDLEhPipeFirst=INVALID_HANDLE_VALUE;+intk;+intret=0;+intnr_threads=opts->nr_threads;++*returned_server_data=NULL;++ret=initialize_pipe_name(path,wpath,ARRAY_SIZE(wpath));+if(ret<0){+errno=EINVAL;+return-1;+}++hPipeFirst=create_new_pipe(wpath,1);+if(hPipeFirst==INVALID_HANDLE_VALUE){+errno=EADDRINUSE;+return-2;+}++server_data=xcalloc(1,sizeof(*server_data));+server_data->magic=MAGIC_SERVER_DATA;+server_data->application_cb=application_cb;+server_data->application_data=application_data;+server_data->hEventStopRequested=CreateEvent(NULL,TRUE,FALSE,NULL);+strbuf_init(&server_data->buf_path,0);+strbuf_addstr(&server_data->buf_path,path);+wcscpy(server_data->wpath,wpath);++if(nr_threads<1)+nr_threads=1;++for(k=0;k<nr_threads;k++){+structipc_server_thread_data*std;++std=xcalloc(1,sizeof(*std));+std->magic=MAGIC_SERVER_THREAD_DATA;+std->server_data=server_data;+std->hPipe=INVALID_HANDLE_VALUE;++std->hPipe=(k==0)+?hPipeFirst+:create_new_pipe(server_data->wpath,0);++if(std->hPipe==INVALID_HANDLE_VALUE){+/*+*Ifwe'vereachedapipeinstancelimitfor+*thispath,justusefewerthreads.+*/+free(std);+break;+}++if(pthread_create(&std->pthread_id,NULL,+server_thread_proc,std)){+/*+*Likewise,ifwe'reoutofthreads,justuse+*fewerthreadsthanrequested.+*+*However,wejustgiveupifwecan'tevenget+*onethread.Thisshouldnothappen.+*/+if(k==0)+die(_("could not start thread[0] for '%s'"),+path);++CloseHandle(std->hPipe);+free(std);+break;+}++std->next_thread=server_data->thread_list;+server_data->thread_list=std;+}++*returned_server_data=server_data;+return0;+}++intipc_server_stop_async(structipc_server_data*server_data)+{+if(!server_data)+return0;++/*+*Gentlytellalloftheipc_serverthreadstoshutdown.+*Thiswillbeseenthenexttimetheyareidle(andwaiting+*foraconnection).+*+*WeDONOTattempttoforcethemtodropanactiveconnection.+*/+SetEvent(server_data->hEventStopRequested);+return0;+}++intipc_server_await(structipc_server_data*server_data)+{+DWORDdwWaitResult;++if(!server_data)+return0;++dwWaitResult=WaitForSingleObject(server_data->hEventStopRequested,INFINITE);+if(dwWaitResult!=WAIT_OBJECT_0)+returnerror(_("wait for hEvent failed for '%s'"),+server_data->buf_path.buf);++while(server_data->thread_list){+structipc_server_thread_data*std=server_data->thread_list;++pthread_join(std->pthread_id,NULL);++server_data->thread_list=std->next_thread;+free(std);+}++server_data->is_stopped=1;++return0;+}++voidipc_server_free(structipc_server_data*server_data)+{+if(!server_data)+return;++if(!server_data->is_stopped)+BUG("cannot free ipc-server while running for '%s'",+server_data->buf_path.buf);++strbuf_release(&server_data->buf_path);++if(server_data->hEventStopRequested!=INVALID_HANDLE_VALUE)+CloseHandle(server_data->hEventStopRequested);++while(server_data->thread_list){+structipc_server_thread_data*std=server_data->thread_list;++server_data->thread_list=std->next_thread;+free(std);+}++free(server_data);+}
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-09 15:03:53
From: Jeff Hostetler <redacted>
Create Unix domain socket based implementation of "simple-ipc".
A set of `ipc_client` routines implement a client library to connect
to an `ipc_server` over a Unix domain socket, send a simple request,
and receive a single response. Clients use blocking IO on the socket.
A set of `ipc_server` routines implement a thread pool to listen for
and concurrently service client connections.
The server creates a new Unix domain socket at a known location. If a
socket already exists with that name, the server tries to determine if
another server is already listening on the socket or if the socket is
dead. If socket is busy, the server exits with an error rather than
stealing the socket. If the socket is dead, the server creates a new
one and starts up.
If while running, the server detects that its socket has been stolen
by another server, it automatically exits.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 2 +
compat/simple-ipc/ipc-unix-socket.c | 986 ++++++++++++++++++++++++++++
contrib/buildsystems/CMakeLists.txt | 2 +
simple-ipc.h | 13 +-
4 files changed, 1002 insertions(+), 1 deletion(-)
create mode 100644 compat/simple-ipc/ipc-unix-socket.c
@@ -0,0 +1,986 @@+#include"cache.h"+#include"simple-ipc.h"+#include"strbuf.h"+#include"pkt-line.h"+#include"thread-utils.h"+#include"unix-socket.h"+#include"unix-stream-server.h"++#ifdef NO_UNIX_SOCKETS+#error compat/simple-ipc/ipc-unix-socket.c requires Unix sockets+#endif++enumipc_active_stateipc_get_active_state(constchar*path)+{+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;+structstatst;+structipc_client_connection*connection_test=NULL;++options.wait_if_busy=0;+options.wait_if_not_found=0;++if(lstat(path,&st)==-1){+switch(errno){+caseENOENT:+caseENOTDIR:+returnIPC_STATE__NOT_LISTENING;+default:+returnIPC_STATE__INVALID_PATH;+}+}++/* also complain if a plain file is in the way */+if((st.st_mode&S_IFMT)!=S_IFSOCK)+returnIPC_STATE__INVALID_PATH;++/*+*JustbecausethefilesystemhasaS_IFSOCKtypeinode+*at`path`,doesn'tmeanitthatthereisaserverlistening.+*Pingittobesure.+*/+state=ipc_client_try_connect(path,&options,&connection_test);+ipc_client_close_connection(connection_test);++returnstate;+}++/*+*Thisvaluewaschosenatrandom.+*/+#define WAIT_STEP_MS (50)++/*+*Trytoconnecttotheserver.Iftheserverisjuststartingupor+*isverybusy,wemaynotgetaconnectionthefirsttime.+*/+staticenumipc_active_stateconnect_to_server(+constchar*path,+inttimeout_ms,+conststructipc_client_connect_options*options,+int*pfd)+{+intwait_ms=50;+intk;++*pfd=-1;++for(k=0;k<timeout_ms;k+=wait_ms){+intfd=unix_stream_connect(path,options->uds_disallow_chdir);++if(fd!=-1){+*pfd=fd;+returnIPC_STATE__LISTENING;+}++if(errno==ENOENT){+if(!options->wait_if_not_found)+returnIPC_STATE__PATH_NOT_FOUND;++gotosleep_and_try_again;+}++if(errno==ETIMEDOUT){+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;++gotosleep_and_try_again;+}++if(errno==ECONNREFUSED){+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;++gotosleep_and_try_again;+}++returnIPC_STATE__OTHER_ERROR;++sleep_and_try_again:+sleep_millisec(wait_ms);+}++returnIPC_STATE__NOT_LISTENING;+}++/*+*Arandomlychosentimeoutvalue.+*/+#define MY_CONNECTION_TIMEOUT_MS (1000)++enumipc_active_stateipc_client_try_connect(+constchar*path,+conststructipc_client_connect_options*options,+structipc_client_connection**p_connection)+{+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+intfd=-1;++*p_connection=NULL;++trace2_region_enter("ipc-client","try-connect",NULL);+trace2_data_string("ipc-client",NULL,"try-connect/path",path);++state=connect_to_server(path,MY_CONNECTION_TIMEOUT_MS,+options,&fd);++trace2_data_intmax("ipc-client",NULL,"try-connect/state",+(intmax_t)state);+trace2_region_leave("ipc-client","try-connect",NULL);++if(state==IPC_STATE__LISTENING){+(*p_connection)=xcalloc(1,sizeof(structipc_client_connection));+(*p_connection)->fd=fd;+}++returnstate;+}++voidipc_client_close_connection(structipc_client_connection*connection)+{+if(!connection)+return;++if(connection->fd!=-1)+close(connection->fd);++free(connection);+}++intipc_client_send_command_to_connection(+structipc_client_connection*connection,+constchar*message,structstrbuf*answer)+{+intret=0;++strbuf_setlen(answer,0);++trace2_region_enter("ipc-client","send-command",NULL);++if(write_packetized_from_buf_no_flush(message,strlen(message),+connection->fd)<0||+packet_flush_gently(connection->fd)<0){+ret=error(_("could not send IPC command"));+gotodone;+}++if(read_packetized_to_strbuf(+connection->fd,answer,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR)<0){+ret=error(_("could not read IPC response"));+gotodone;+}++done:+trace2_region_leave("ipc-client","send-command",NULL);+returnret;+}++intipc_client_send_command(constchar*path,+conststructipc_client_connect_options*options,+constchar*message,structstrbuf*answer)+{+intret=-1;+enumipc_active_statestate;+structipc_client_connection*connection=NULL;++state=ipc_client_try_connect(path,options,&connection);++if(state!=IPC_STATE__LISTENING)+returnret;++ret=ipc_client_send_command_to_connection(connection,message,answer);++ipc_client_close_connection(connection);++returnret;+}++staticintset_socket_blocking_flag(intfd,intmake_nonblocking)+{+intflags;++flags=fcntl(fd,F_GETFL,NULL);++if(flags<0)+return-1;++if(make_nonblocking)+flags|=O_NONBLOCK;+else+flags&=~O_NONBLOCK;++returnfcntl(fd,F_SETFL,flags);+}++/*+*Magicnumbersusedtoannotatecallbackinstancedata.+*Theseareusedtohelpguardagainstaccidentallypassingthe+*wronginstancedataacrossmultiplelevelsofcallbacks(which+*iseasytodoifthereare`void*`arguments).+*/+enummagic{+MAGIC_SERVER_REPLY_DATA,+MAGIC_WORKER_THREAD_DATA,+MAGIC_ACCEPT_THREAD_DATA,+MAGIC_SERVER_DATA,+};++structipc_server_reply_data{+enummagicmagic;+intfd;+structipc_worker_thread_data*worker_thread_data;+};++structipc_worker_thread_data{+enummagicmagic;+structipc_worker_thread_data*next_thread;+structipc_server_data*server_data;+pthread_tpthread_id;+};++structipc_accept_thread_data{+enummagicmagic;+structipc_server_data*server_data;++structunix_stream_server_socket*server_socket;++intfd_send_shutdown;+intfd_wait_shutdown;+pthread_tpthread_id;+};++/*+*Withunix-sockets,theconceptual"ipc-server"isimplementedasasingle+*controller"accept-thread"threadandapoolof"worker-thread"threads.+*Theformerdoestheusual`accept()`loopanddispatchesconnections+*toanidleworkerthread.Theworkerthreadswaitinanidleloopfor+*anewconnection,communicatewiththeclientandrelaydatato/from+*the`application_cb`andthenwaitforanotherconnectionfromthe+*serverthread.Thisavoidstheoverheadofconstantlycreatingand+*destroyingthreads.+*/+structipc_server_data{+enummagicmagic;+ipc_server_application_cb*application_cb;+void*application_data;+structstrbufbuf_path;++structipc_accept_thread_data*accept_thread;+structipc_worker_thread_data*worker_thread_list;++pthread_mutex_twork_available_mutex;+pthread_cond_twork_available_cond;++/*+*Acceptedbutnotyetprocessedclientconnectionsarekept+*inacircularbufferFIFO.Thequeueisemptywhenthe+*positionsareequal.+*/+int*fifo_fds;+intqueue_size;+intback_pos;+intfront_pos;++intshutdown_requested;+intis_stopped;+};++/*+*Removeandreturntheoldestqueuedconnection.+*+*Returns-1ifempty.+*/+staticintfifo_dequeue(structipc_server_data*server_data)+{+/* ASSERT holding mutex */++intfd;++if(server_data->back_pos==server_data->front_pos)+return-1;++fd=server_data->fifo_fds[server_data->front_pos];+server_data->fifo_fds[server_data->front_pos]=-1;++server_data->front_pos++;+if(server_data->front_pos==server_data->queue_size)+server_data->front_pos=0;++returnfd;+}++/*+*Pushanewfdontothebackofthequeue.+*+*Dropitandreturn-1ifqueueisalreadyfull.+*/+staticintfifo_enqueue(structipc_server_data*server_data,intfd)+{+/* ASSERT holding mutex */++intnext_back_pos;++next_back_pos=server_data->back_pos+1;+if(next_back_pos==server_data->queue_size)+next_back_pos=0;++if(next_back_pos==server_data->front_pos){+/* Queue is full. Just drop it. */+close(fd);+return-1;+}++server_data->fifo_fds[server_data->back_pos]=fd;+server_data->back_pos=next_back_pos;++returnfd;+}++/*+*WaitforaconnectiontobequeuedtotheFIFOandreturnit.+*+*Returns-1ifsomeonehasalreadyrequestedashutdown.+*/+staticintworker_thread__wait_for_connection(+structipc_worker_thread_data*worker_thread_data)+{+/* ASSERT NOT holding mutex */++structipc_server_data*server_data=worker_thread_data->server_data;+intfd=-1;++pthread_mutex_lock(&server_data->work_available_mutex);+for(;;){+if(server_data->shutdown_requested)+break;++fd=fifo_dequeue(server_data);+if(fd>=0)+break;++pthread_cond_wait(&server_data->work_available_cond,+&server_data->work_available_mutex);+}+pthread_mutex_unlock(&server_data->work_available_mutex);++returnfd;+}++/*+*Forwarddeclareourreplycallbackfunctionsothatanycompiler+*errorsarereportedwhenweactuallydefinethefunction(inaddition+*toanyerrorsreportedwhenwetrytopassthiscallbackfunctionas+*aparameterinafunctioncall).Theformerareeasiertounderstand.+*/+staticipc_server_reply_cbdo_io_reply_callback;++/*+*Relayapplication'sresponsemessagetotheclientprocess.+*(Wedonotflushatthispointbecauseweallowthecaller+*tochunkdatatotheclientthruus.)+*/+staticintdo_io_reply_callback(structipc_server_reply_data*reply_data,+constchar*response,size_tresponse_len)+{+if(reply_data->magic!=MAGIC_SERVER_REPLY_DATA)+BUG("reply_cb called with wrong instance data");++returnwrite_packetized_from_buf_no_flush(response,response_len,+reply_data->fd);+}++/* A randomly chosen value. */+#define MY_WAIT_POLL_TIMEOUT_MS (10)++/*+*Iftheclienthangsupwithoutsendinganydataonthewire,just+*quietlyclosethesocketandignorethisclient.+*+*ThisworkerthreadiscommittedtoreadingtheIPCrequestdata+*fromtheclientattheotherendofthisfd.Waithereforthe+*clienttoactuallyputsomethingonthewire--becauseifthe+*clientjustdoesaping(connectandhangupwithoutsendingany+*data),ouruseofthepkt-linereadroutineswillspewanerror+*message.+*+*Return-1iftheclienthungup.+*Return0ifdata(possiblyincomplete)isready.+*/+staticintworker_thread__wait_for_io_start(+structipc_worker_thread_data*worker_thread_data,+intfd)+{+structipc_server_data*server_data=worker_thread_data->server_data;+structpollfdpollfd[1];+intresult;++for(;;){+pollfd[0].fd=fd;+pollfd[0].events=POLLIN;++result=poll(pollfd,1,MY_WAIT_POLL_TIMEOUT_MS);+if(result<0){+if(errno==EINTR)+continue;+gotocleanup;+}++if(result==0){+/* a timeout */++intin_shutdown;++pthread_mutex_lock(&server_data->work_available_mutex);+in_shutdown=server_data->shutdown_requested;+pthread_mutex_unlock(&server_data->work_available_mutex);++/*+*Ifashutdownisalreadyinprogressandthis+*clienthasnotstartedtalkingyet,justdropit.+*/+if(in_shutdown)+gotocleanup;+continue;+}++if(pollfd[0].revents&POLLHUP)+gotocleanup;++if(pollfd[0].revents&POLLIN)+return0;++gotocleanup;+}++cleanup:+close(fd);+return-1;+}++/*+*Receivetherequest/commandfromtheclientandpassittothe+*registeredrequest-callback.Therequest-callbackwillcompose+*aresponseandcallourreply-callbacktosendittotheclient.+*/+staticintworker_thread__do_io(+structipc_worker_thread_data*worker_thread_data,+intfd)+{+/* ASSERT NOT holding lock */++structstrbufbuf=STRBUF_INIT;+structipc_server_reply_datareply_data;+intret=0;++reply_data.magic=MAGIC_SERVER_REPLY_DATA;+reply_data.worker_thread_data=worker_thread_data;++reply_data.fd=fd;++ret=read_packetized_to_strbuf(+reply_data.fd,&buf,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR);+if(ret>=0){+ret=worker_thread_data->server_data->application_cb(+worker_thread_data->server_data->application_data,+buf.buf,do_io_reply_callback,&reply_data);++packet_flush_gently(reply_data.fd);+}+else{+/*+*Theclientprobablydisconnected/shutdownbeforeit+*couldsendawell-formedmessage.Ignoreit.+*/+}++strbuf_release(&buf);+close(reply_data.fd);++returnret;+}++/*+*BlockSIGPIPEonthecurrentthread(sothatwegetEPIPEfrom+*write()ratherthananactualsignal).+*+*Notethatusingsigchain_push()and_pop()tocontrolSIGPIPE+*aroundourIOcallsisnotthreadsafe:+*[]Itusesaglobalstackofhandlerframes.+*[]ItusesALLOC_GROW()toresizeit.+*[]Finally,accordingtothe`signal(2)`man-page:+*"The effects of `signal()` in a multithreaded process are unspecified."+*/+staticvoidthread_block_sigpipe(sigset_t*old_set)+{+sigset_tnew_set;++sigemptyset(&new_set);+sigaddset(&new_set,SIGPIPE);++sigemptyset(old_set);+pthread_sigmask(SIG_BLOCK,&new_set,old_set);+}++/*+*ThreadprocforanIPCworkerthread.Ithandlesaseriesof+*connectionsfromclients.Itpullsthenextfdfromthequeue+*processesit,andthenwaitsforthenextclient.+*+*BlockSIGPIPEinthisworkerthreadforthelifeofthethread.+*Thisavoidsstray(andsometimesdelayed)SIGPIPEsignalscaused+*byclienterrorsand/orwhenweareunderextremelyheavyIOload.+*+*ThismeansthattheapplicationcallbackwillhaveSIGPIPEblocked.+*Thecallbackshouldnotchangeit.+*/+staticvoid*worker_thread_proc(void*_worker_thread_data)+{+structipc_worker_thread_data*worker_thread_data=_worker_thread_data;+structipc_server_data*server_data=worker_thread_data->server_data;+sigset_told_set;+intfd,io;+intret;++trace2_thread_start("ipc-worker");++thread_block_sigpipe(&old_set);++for(;;){+fd=worker_thread__wait_for_connection(worker_thread_data);+if(fd==-1)+break;/* in shutdown */++io=worker_thread__wait_for_io_start(worker_thread_data,fd);+if(io==-1)+continue;/* client hung up without sending anything */++ret=worker_thread__do_io(worker_thread_data,fd);++if(ret==SIMPLE_IPC_QUIT){+trace2_data_string("ipc-worker",NULL,"queue_stop_async",+"application_quit");+/*+*Theapplicationlayeristellingtheipc-server+*layertoshutdown.+*+*WeDONOThavearesponsetosendtotheclient.+*+*Queueanasyncstop(tostoptheotherthreads)and+*allowthisworkerthreadtoexitnow(nosensewaiting+*forthethread-poolshutdownsignal).+*+*Othernon-idleworkerthreadsareallowedtofinish+*respondingtotheircurrentclients.+*/+ipc_server_stop_async(server_data);+break;+}+}++trace2_thread_exit();+returnNULL;+}++/* A randomly chosen value. */+#define MY_ACCEPT_POLL_TIMEOUT_MS (60 * 1000)++/*+*Acceptanewclientconnectiononoursocket.Thisusesnon-blocking+*IOsothatwecanalsowaitforshutdownrequestsonoursocket-pair+*withoutactuallyspinningonafasttimeout.+*/+staticintaccept_thread__wait_for_connection(+structipc_accept_thread_data*accept_thread_data)+{+structpollfdpollfd[2];+intresult;++for(;;){+pollfd[0].fd=accept_thread_data->fd_wait_shutdown;+pollfd[0].events=POLLIN;++pollfd[1].fd=accept_thread_data->server_socket->fd_socket;+pollfd[1].events=POLLIN;++result=poll(pollfd,2,MY_ACCEPT_POLL_TIMEOUT_MS);+if(result<0){+if(errno==EINTR)+continue;+returnresult;+}++if(result==0){+/* a timeout */++/*+*Ifsomeonedeletesorforce-createsanewunix+*domainsocketatourpath,allfutureclients+*willberoutedelsewhereandwesilentlystarve.+*Ifthathappens,justqueueashutdown.+*/+if(unix_stream_server__was_stolen(+accept_thread_data->server_socket)){+trace2_data_string("ipc-accept",NULL,+"queue_stop_async",+"socket_stolen");+ipc_server_stop_async(+accept_thread_data->server_data);+}+continue;+}++if(pollfd[0].revents&POLLIN){+/* shutdown message queued to socketpair */+return-1;+}++if(pollfd[1].revents&POLLIN){+/* a connection is available on server_socket */++intclient_fd=+accept(accept_thread_data->server_socket->fd_socket,+NULL,NULL);+if(client_fd>=0)+returnclient_fd;++/*+*Anerrorhereisunlikely--itprobably+*indicatesthattheconnectingprocesshas+*alreadydroppedtheconnection.+*/+continue;+}++BUG("unandled poll result errno=%d r[0]=%d r[1]=%d",+errno,pollfd[0].revents,pollfd[1].revents);+}+}++/*+*ThreadprocfortheIPCserver"accept thread".Thiswaitsfor+*anincomingsocketconnection,appendsittothequeueofavailable+*connections,andnotifiesaworkerthreadtoprocessit.+*+*BlockSIGPIPEinthisthreadforthelifeofthethread.This+*avoidsanystraySIGPIPEsignalswhenclosingpipefdsunder+*extremelyheavyloads(suchaswhenthefifoqueueisfullandwe+*dropincommingconnections).+*/+staticvoid*accept_thread_proc(void*_accept_thread_data)+{+structipc_accept_thread_data*accept_thread_data=_accept_thread_data;+structipc_server_data*server_data=accept_thread_data->server_data;+sigset_told_set;++trace2_thread_start("ipc-accept");++thread_block_sigpipe(&old_set);++for(;;){+intclient_fd=accept_thread__wait_for_connection(+accept_thread_data);++pthread_mutex_lock(&server_data->work_available_mutex);+if(server_data->shutdown_requested){+pthread_mutex_unlock(&server_data->work_available_mutex);+if(client_fd>=0)+close(client_fd);+break;+}++if(client_fd<0){+/* ignore transient accept() errors */+}+else{+fifo_enqueue(server_data,client_fd);+pthread_cond_broadcast(&server_data->work_available_cond);+}+pthread_mutex_unlock(&server_data->work_available_mutex);+}++trace2_thread_exit();+returnNULL;+}++/*+*Wecan'tpredicttheconnectionarrivalraterelativetotheworker+*processingrate,thereforeweallowthe"accept-thread"toqueueup+*agenerousnumberofconnections,sincewe'dratherhavetheclient+*notunnecessarilytimeoutifwecanavoidit.(Theassumptionis+*thatthiswillbeusedforFSMonitorandafewsecondwaitona+*connectionisbetterthanhavingtheclienttimeoutanddothefull+*computationitself.)+*+*TheFIFOqueuesizeissettoamultipleoftheworkerpoolsize.+*Thisvaluechosenatrandom.+*/+#define FIFO_SCALE (100)++/*+*Thebacklogvaluefor`listen(2)`.Thisdoesn'tneedtohuge,+*ratherjustlargeenoughforour"accept-thread"towakeupand+*queueincomingconnectionsontotheFIFOwithoutthekernel+*droppingany.+*+*Thisvaluechosenatrandom.+*/+#define LISTEN_BACKLOG (50)++staticintcreate_listener_socket(+constchar*path,+conststructipc_server_opts*ipc_opts,+structunix_stream_server_socket**new_server_socket)+{+structunix_stream_server_socket*server_socket=NULL;+structunix_stream_listen_optsuslg_opts=UNIX_STREAM_LISTEN_OPTS_INIT;+intret;++uslg_opts.listen_backlog_size=LISTEN_BACKLOG;+uslg_opts.disallow_chdir=ipc_opts->uds_disallow_chdir;++ret=unix_stream_server__create(path,&uslg_opts,-1,&server_socket);+if(ret)+returnret;++if(set_socket_blocking_flag(server_socket->fd_socket,1)){+intsaved_errno=errno;+unix_stream_server__free(server_socket);+errno=saved_errno;+return-1;+}++*new_server_socket=server_socket;++trace2_data_string("ipc-server",NULL,"listen-with-lock",path);+return0;+}++staticintsetup_listener_socket(+constchar*path,+conststructipc_server_opts*ipc_opts,+structunix_stream_server_socket**new_server_socket)+{+intret,saved_errno;++trace2_region_enter("ipc-server","create-listener_socket",NULL);++ret=create_listener_socket(path,ipc_opts,new_server_socket);++saved_errno=errno;+trace2_region_leave("ipc-server","create-listener_socket",NULL);+errno=saved_errno;++returnret;+}++/*+*StartIPCserverinapoolofbackgroundthreads.+*/+intipc_server_run_async(structipc_server_data**returned_server_data,+constchar*path,conststructipc_server_opts*opts,+ipc_server_application_cb*application_cb,+void*application_data)+{+structunix_stream_server_socket*server_socket=NULL;+structipc_server_data*server_data;+intsv[2];+intk;+intret;+intnr_threads=opts->nr_threads;++*returned_server_data=NULL;++/*+*Createasocketpairandsetsv[1]tonon-blocking.This+*willusedtosendashutdownmessagetotheaccept-thread+*andallowstheaccept-threadtowaitonEITHERaclient+*connectionorashutdownrequestwithoutspinning.+*/+if(socketpair(AF_UNIX,SOCK_STREAM,0,sv)<0)+return-1;++if(set_socket_blocking_flag(sv[1],1)){+intsaved_errno=errno;+close(sv[0]);+close(sv[1]);+errno=saved_errno;+return-1;+}++ret=setup_listener_socket(path,opts,&server_socket);+if(ret){+intsaved_errno=errno;+close(sv[0]);+close(sv[1]);+errno=saved_errno;+returnret;+}++server_data=xcalloc(1,sizeof(*server_data));+server_data->magic=MAGIC_SERVER_DATA;+server_data->application_cb=application_cb;+server_data->application_data=application_data;+strbuf_init(&server_data->buf_path,0);+strbuf_addstr(&server_data->buf_path,path);++if(nr_threads<1)+nr_threads=1;++pthread_mutex_init(&server_data->work_available_mutex,NULL);+pthread_cond_init(&server_data->work_available_cond,NULL);++server_data->queue_size=nr_threads*FIFO_SCALE;+server_data->fifo_fds=xcalloc(server_data->queue_size,+sizeof(*server_data->fifo_fds));++server_data->accept_thread=+xcalloc(1,sizeof(*server_data->accept_thread));+server_data->accept_thread->magic=MAGIC_ACCEPT_THREAD_DATA;+server_data->accept_thread->server_data=server_data;+server_data->accept_thread->server_socket=server_socket;+server_data->accept_thread->fd_send_shutdown=sv[0];+server_data->accept_thread->fd_wait_shutdown=sv[1];++if(pthread_create(&server_data->accept_thread->pthread_id,NULL,+accept_thread_proc,server_data->accept_thread))+die_errno(_("could not start accept_thread '%s'"),path);++for(k=0;k<nr_threads;k++){+structipc_worker_thread_data*wtd;++wtd=xcalloc(1,sizeof(*wtd));+wtd->magic=MAGIC_WORKER_THREAD_DATA;+wtd->server_data=server_data;++if(pthread_create(&wtd->pthread_id,NULL,worker_thread_proc,+wtd)){+if(k==0)+die(_("could not start worker[0] for '%s'"),+path);+/*+*Limpalongwiththethreadpoolthatwehave.+*/+break;+}++wtd->next_thread=server_data->worker_thread_list;+server_data->worker_thread_list=wtd;+}++*returned_server_data=server_data;+return0;+}++/*+*GentlytelltheIPCservertreadstoshutdown.+*Canberunonanythread.+*/+intipc_server_stop_async(structipc_server_data*server_data)+{+/* ASSERT NOT holding mutex */++intfd;++if(!server_data)+return0;++trace2_region_enter("ipc-server","server-stop-async",NULL);++pthread_mutex_lock(&server_data->work_available_mutex);++server_data->shutdown_requested=1;++/*+*Writeabytetotheshutdownsocketpairtowakeupthe+*accept-thread.+*/+if(write(server_data->accept_thread->fd_send_shutdown,"Q",1)<0)+error_errno("could not write to fd_send_shutdown");++/*+*Drainthequeueofexistingconnections.+*/+while((fd=fifo_dequeue(server_data))!=-1)+close(fd);++/*+*Gentlytellworkerthreadstostopprocessingnewconnections+*andexit.(Thisdoesnotabortin-processconversations.)+*/+pthread_cond_broadcast(&server_data->work_available_cond);++pthread_mutex_unlock(&server_data->work_available_mutex);++trace2_region_leave("ipc-server","server-stop-async",NULL);++return0;+}++/*+*WaitforallIPCserverthreadstostop.+*/+intipc_server_await(structipc_server_data*server_data)+{+pthread_join(server_data->accept_thread->pthread_id,NULL);++if(!server_data->shutdown_requested)+BUG("ipc-server: accept-thread stopped for '%s'",+server_data->buf_path.buf);++while(server_data->worker_thread_list){+structipc_worker_thread_data*wtd=+server_data->worker_thread_list;++pthread_join(wtd->pthread_id,NULL);++server_data->worker_thread_list=wtd->next_thread;+free(wtd);+}++server_data->is_stopped=1;++return0;+}++voidipc_server_free(structipc_server_data*server_data)+{+structipc_accept_thread_data*accept_thread_data;++if(!server_data)+return;++if(!server_data->is_stopped)+BUG("cannot free ipc-server while running for '%s'",+server_data->buf_path.buf);++accept_thread_data=server_data->accept_thread;+if(accept_thread_data){+unix_stream_server__free(accept_thread_data->server_socket);++if(accept_thread_data->fd_send_shutdown!=-1)+close(accept_thread_data->fd_send_shutdown);+if(accept_thread_data->fd_wait_shutdown!=-1)+close(accept_thread_data->fd_wait_shutdown);++free(server_data->accept_thread);+}++while(server_data->worker_thread_list){+structipc_worker_thread_data*wtd=+server_data->worker_thread_list;++server_data->worker_thread_list=wtd->next_thread;+free(wtd);+}++pthread_cond_destroy(&server_data->work_available_cond);+pthread_mutex_destroy(&server_data->work_available_mutex);++strbuf_release(&server_data->buf_path);++free(server_data->fifo_fds);+free(server_data);+}
From: Johannes Schindelin via GitGitGadget <hidden> Date: 2021-03-09 15:03:54
From: Johannes Schindelin <redacted>
Introduce PACKET_READ_GENTLE_ON_READ_ERROR option to help libify the
packet readers.
So far, the (possibly indirect) callers of `get_packet_data()` can ask
that function to return an error instead of `die()`ing upon end-of-file.
However, random read errors will still cause the process to die.
So let's introduce an explicit option to tell the packet reader
machinery to please be nice and only return an error on read errors.
This change prepares pkt-line for use by long-running daemon processes.
Such processes should be able to serve multiple concurrent clients and
and survive random IO errors. If there is an error on one connection,
a daemon should be able to drop that connection and continue serving
existing and future connections.
This ability will be used by a Git-aware "Builtin FSMonitor" feature
in a later patch series.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
pkt-line.c | 19 +++++++++++++++++--
pkt-line.h | 11 ++++++++---
2 files changed, 25 insertions(+), 5 deletions(-)
@@ -305,8 +305,11 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,*src_size-=ret;}else{ret=read_in_full(fd,dst,size);-if(ret<0)+if(ret<0){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror_errno(_("read error"));die_errno(_("read error"));+}}/* And complain if we didn't get enough bytes to satisfy the read. */
@@ -314,6 +317,8 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,if(options&PACKET_READ_GENTLE_ON_EOF)return-1;+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("the remote end hung up unexpectedly"));die(_("the remote end hung up unexpectedly"));}
@@ -342,6 +347,9 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer,len=packet_length(linelen);if(len<0){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("protocol error: bad line length "+"character: %.4s"),linelen);die(_("protocol error: bad line length character: %.4s"),linelen);}elseif(!len){packet_trace("0000",4,0);
@@ -356,12 +364,19 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer,*pktlen=0;returnPACKET_READ_RESPONSE_END;}elseif(len<4){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("protocol error: bad line length %d"),+len);die(_("protocol error: bad line length %d"),len);}len-=4;-if((unsigned)len>=size)+if((unsigned)len>=size){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("protocol error: bad line length %d"),+len);die(_("protocol error: bad line length %d"),len);+}if(get_packet_data(fd,src_buffer,src_len,buffer,len,options)<0){*pktlen=-1;
From: Johannes Schindelin via GitGitGadget <hidden> Date: 2021-03-09 15:03:54
From: Johannes Schindelin <redacted>
Remove the `packet_flush_gently()` call in `write_packetized_from_buf() and
`write_packetized_from_fd()` and require the caller to call it if desired.
Rename both functions to `write_packetized_from_*_no_flush()` to prevent
later merge accidents.
`write_packetized_from_buf()` currently only has one caller:
`apply_multi_file_filter()` in `convert.c`. It always wants a flush packet
to be written after writing the payload.
However, we are about to introduce a caller that wants to write many
packets before a final flush packet, so let's make the caller responsible
for emitting the flush packet.
Signed-off-by: Jeff Hostetler <redacted>
Signed-off-by: Johannes Schindelin <redacted>
---
convert.c | 8 ++++++--
pkt-line.c | 8 ++------
pkt-line.h | 4 ++--
3 files changed, 10 insertions(+), 10 deletions(-)
@@ -266,13 +266,11 @@ int write_packetized_from_fd(int fd_in, int fd_out)break;err=packet_write_gently(fd_out,buf,bytes_to_write);}-if(!err)-err=packet_flush_gently(fd_out);free(buf);returnerr;}-intwrite_packetized_from_buf(constchar*src_in,size_tlen,intfd_out)+intwrite_packetized_from_buf_no_flush(constchar*src_in,size_tlen,intfd_out){interr=0;size_tbytes_written=0;
@@ -288,8 +286,6 @@ int write_packetized_from_buf(const char *src_in, size_t len, int fd_out)err=packet_write_gently(fd_out,src_in+bytes_written,bytes_to_write);bytes_written+=bytes_to_write;}-if(!err)-err=packet_flush_gently(fd_out);returnerr;}
From: Johannes Schindelin via GitGitGadget <hidden> Date: 2021-03-09 15:03:54
From: Johannes Schindelin <redacted>
Update the calling sequence of `read_packetized_to_strbuf()` to take
an options argument and not assume a fixed set of options. Update the
only existing caller accordingly to explicitly pass the
formerly-assumed flags.
The `read_packetized_to_strbuf()` function calls `packet_read()` with
a fixed set of assumed options (`PACKET_READ_GENTLE_ON_EOF`). This
assumption has been fine for the single existing caller
`apply_multi_file_filter()` in `convert.c`.
In a later commit we would like to add other callers to
`read_packetized_to_strbuf()` that need a different set of options.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
convert.c | 3 ++-
pkt-line.c | 4 ++--
pkt-line.h | 2 +-
3 files changed, 5 insertions(+), 4 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-09 15:03:54
From: Jeff Hostetler <redacted>
Update `unix_stream_listen()` to take an options structure to override
default behaviors. This commit includes the size of the `listen()` backlog.
Signed-off-by: Jeff Hostetler <redacted>
---
builtin/credential-cache--daemon.c | 3 ++-
unix-socket.c | 11 +++++++++--
unix-socket.h | 9 ++++++++-
3 files changed, 19 insertions(+), 4 deletions(-)
@@ -203,9 +203,10 @@ static int serve_cache_loop(int fd)staticvoidserve_cache(constchar*socket_path,intdebug){+structunix_stream_listen_optsopts=UNIX_STREAM_LISTEN_OPTS_INIT;intfd;-fd=unix_stream_listen(socket_path);+fd=unix_stream_listen(socket_path,&opts);if(fd<0)die_errno("unable to bind to '%s'",socket_path);
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-09 15:03:54
From: Jeff Hostetler <redacted>
Create t0052-simple-ipc.sh with unit tests for the "simple-ipc" mechanism.
Create t/helper/test-simple-ipc test tool to exercise the "simple-ipc"
functions.
When the tool is invoked with "run-daemon", it runs a server to listen
for "simple-ipc" connections on a test socket or named pipe and
responds to a set of commands to exercise/stress the communication
setup.
When the tool is invoked with "start-daemon", it spawns a "run-daemon"
command in the background and waits for the server to become ready
before exiting. (This helps make unit tests in t0052 more predictable
and avoids the need for arbitrary sleeps in the test script.)
The tool also has a series of client "send" commands to send commands
and data to a server instance.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 1 +
t/helper/test-simple-ipc.c | 787 +++++++++++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/t0052-simple-ipc.sh | 122 ++++++
5 files changed, 912 insertions(+)
create mode 100644 t/helper/test-simple-ipc.c
create mode 100755 t/t0052-simple-ipc.sh
@@ -0,0 +1,787 @@+/*+*test-simple-ipc.c:verifythattheInter-ProcessCommunicationworks.+*/++#include"test-tool.h"+#include"cache.h"+#include"strbuf.h"+#include"simple-ipc.h"+#include"parse-options.h"+#include"thread-utils.h"+#include"strvec.h"++#ifndef SUPPORTS_SIMPLE_IPC+intcmd__simple_ipc(intargc,constchar**argv)+{+die("simple IPC not available on this platform");+}+#else++/*+*Thetestdaemondefinesan"application callback"thatsupportsa+*seriesofcommands(see`test_app_cb()`).+*+*Unknowncommandsarecaughthereandwesendanerrormessageback+*totheclientprocess.+*/+staticintapp__unhandled_command(constchar*command,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+intret;++strbuf_addf(&buf,"unhandled command: %s",command);+ret=reply_cb(reply_data,buf.buf,buf.len);+strbuf_release(&buf);++returnret;+}++/*+*Replywithasingleverylargebuffer.Thisistoensurethat+*longresponseareproperlyhandled--whetherthechunkingoccurs+*inthekernelorinthe(probablypkt-line)layer.+*/+#define BIG_ROWS (10000)+staticintapp__big_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<BIG_ROWS;row++)+strbuf_addf(&buf,"big: %.75d\n",row);++ret=reply_cb(reply_data,buf.buf,buf.len);+strbuf_release(&buf);++returnret;+}++/*+*Replywithaseriesoflines.Thisistoensurethatwecanincrementally+*computetheresponseandchunkittotheclient.+*/+#define CHUNK_ROWS (10000)+staticintapp__chunk_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<CHUNK_ROWS;row++){+strbuf_setlen(&buf,0);+strbuf_addf(&buf,"big: %.75d\n",row);+ret=reply_cb(reply_data,buf.buf,buf.len);+}++strbuf_release(&buf);++returnret;+}++/*+*Slowlyreplywithaseriesoflines.Thisistomodelanexpensiveto+*computechunkedresponse(whichmighthappenifthiscallbackisrunning+*inathreadandisfightingforalockwithotherthreads).+*/+#define SLOW_ROWS (1000)+#define SLOW_DELAY_MS (10)+staticintapp__slow_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<SLOW_ROWS;row++){+strbuf_setlen(&buf,0);+strbuf_addf(&buf,"big: %.75d\n",row);+ret=reply_cb(reply_data,buf.buf,buf.len);+sleep_millisec(SLOW_DELAY_MS);+}++strbuf_release(&buf);++returnret;+}++/*+*Theclientsentacommandfollowedbya(possiblyvery)largebuffer.+*/+staticintapp__sendbytes_command(constchar*received,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf_resp=STRBUF_INIT;+constchar*p="?";+intlen_ballast=0;+intk;+interrs=0;+intret;++if(skip_prefix(received,"sendbytes ",&p))+len_ballast=strlen(p);++/*+*Verifythattheballastisncopiesofasingleletter.+*Andthatthemulti-threadedIOlayerdidn'tcrossthestreams.+*/+for(k=1;k<len_ballast;k++)+if(p[k]!=p[0])+errs++;++if(errs)+strbuf_addf(&buf_resp,"errs:%d\n",errs);+else+strbuf_addf(&buf_resp,"rcvd:%c%08d\n",p[0],len_ballast);++ret=reply_cb(reply_data,buf_resp.buf,buf_resp.len);++strbuf_release(&buf_resp);++returnret;+}++/*+*Anarbitraryfixedaddresstoverifythattheapplicationinstance+*dataishandledproperly.+*/+staticintmy_app_data=42;++staticipc_server_application_cbtest_app_cb;++/*+*Thisisthe"application callback"thatsitsontopofthe+*"ipc-server".Itcompletelydefinesthesetofcommandssupported+*bythisapplication.+*/+staticinttest_app_cb(void*application_data,+constchar*command,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+/*+*Verifythatwereceivedtheapplication-datathatwepassed+*whenwestartedtheipc-server.(Wehaveseverallayersof+*callbackscallingcallbacksandit'seasytogetthingsmixed+*up(especiallywhensomeare"void*").)+*/+if(application_data!=(void*)&my_app_data)+BUG("application_cb: application_data pointer wrong");++if(!strcmp(command,"quit")){+/*+*Theclientsenta"quit"command.Thisisanasync+*requestfortheservertoshutdown.+*+*WeDONOTsendtheclientaresponsemessage+*(becausewehavenothingtosayandtheother+*serverthreadshavenotyetstopped).+*+*Telltheipc-serverlayertostartshuttingdown.+*Thisincludes:stoplisteningfornewconnections+*onthesocket/pipeandtellingallworkerthreads+*tofinish/draintheiroutgoingresponsestoother+*clients.+*+*ThisDOESNOTforceanimmediatesyncshutdown.+*/+returnSIMPLE_IPC_QUIT;+}++if(!strcmp(command,"ping")){+constchar*answer="pong";+returnreply_cb(reply_data,answer,strlen(answer));+}++if(!strcmp(command,"big"))+returnapp__big_command(reply_cb,reply_data);++if(!strcmp(command,"chunk"))+returnapp__chunk_command(reply_cb,reply_data);++if(!strcmp(command,"slow"))+returnapp__slow_command(reply_cb,reply_data);++if(starts_with(command,"sendbytes "))+returnapp__sendbytes_command(command,reply_cb,reply_data);++returnapp__unhandled_command(command,reply_cb,reply_data);+}++structcl_args+{+constchar*subcommand;+constchar*path;+constchar*token;++intnr_threads;+intmax_wait_sec;+intbytecount;+intbatchsize;++charbytevalue;+};++staticstructcl_argscl_args={+.subcommand=NULL,+.path="ipc-test",+.token=NULL,++.nr_threads=5,+.max_wait_sec=60,+.bytecount=1024,+.batchsize=10,++.bytevalue='x',+};++/*+*Thisprocesswillrunasasimple-ipcserverandlistenforIPCcommands+*fromclientprocesses.+*/+staticintdaemon__run_server(void)+{+intret;++structipc_server_optsopts={+.nr_threads=cl_args.nr_threads,+};++/*+*Synchronouslyruntheipc-server.Wedon'tneedanyapplication+*instancedata,sopassanarbitrarypointer(thatwe'lllater+*verifymadetheroundtrip).+*/+ret=ipc_server_run(cl_args.path,&opts,test_app_cb,(void*)&my_app_data);+if(ret==-2)+error(_("socket/pipe already in use: '%s'"),cl_args.path);+elseif(ret==-1)+error_errno(_("could not start server on: '%s'"),cl_args.path);++returnret;+}++#ifndef GIT_WINDOWS_NATIVE+/*+*Thisisadaptedfrom`daemonize()`.Use`fork()`todirectlycreateand+*runthedaemoninachildprocess.+*/+staticintspawn_server(pid_t*pid)+{+structipc_server_optsopts={+.nr_threads=cl_args.nr_threads,+};++*pid=fork();++switch(*pid){+case0:+if(setsid()==-1)+error_errno(_("setsid failed"));+close(0);+close(1);+close(2);+sanitize_stdfds();++returnipc_server_run(cl_args.path,&opts,test_app_cb,+(void*)&my_app_data);++case-1:+returnerror_errno(_("could not spawn daemon in the background"));++default:+return0;+}+}+#else+/*+*Conceptuallylike`daemonize()`butdifferentbecauseWindowsdoesnot+*have`fork(2)`.SpawnanormalWindowschildprocessbutwithoutthe+*limitationsof`start_command()`and`finish_command()`.+*/+staticintspawn_server(pid_t*pid)+{+chartest_tool_exe[MAX_PATH];+structstrvecargs=STRVEC_INIT;+intin,out;++GetModuleFileNameA(NULL,test_tool_exe,MAX_PATH);++in=open("/dev/null",O_RDONLY);+out=open("/dev/null",O_WRONLY);++strvec_push(&args,test_tool_exe);+strvec_push(&args,"simple-ipc");+strvec_push(&args,"run-daemon");+strvec_pushf(&args,"--name=%s",cl_args.path);+strvec_pushf(&args,"--threads=%d",cl_args.nr_threads);++*pid=mingw_spawnvpe(args.v[0],args.v,NULL,NULL,in,out,out);+close(in);+close(out);++strvec_clear(&args);++if(*pid<0)+returnerror(_("could not spawn daemon in the background"));++return0;+}+#endif++/*+*Thisisadaptedfrom`wait_or_whine()`.Watchthechildprocessand+*letitgetstartedandbeginlisteningforrequestsonthesocket+*beforereportingoursuccess.+*/+staticintwait_for_server_startup(pid_tpid_child)+{+intstatus;+pid_tpid_seen;+enumipc_active_states;+time_ttime_limit,now;++time(&time_limit);+time_limit+=cl_args.max_wait_sec;++for(;;){+pid_seen=waitpid(pid_child,&status,WNOHANG);++if(pid_seen==-1)+returnerror_errno(_("waitpid failed"));++elseif(pid_seen==0){+/*+*Thechildisstillrunning(thisshouldbe+*thenormalcase).Trytoconnecttoiton+*thesocketandseeifitisreadyfor+*business.+*+*Ifthereisanotherdaemonalreadyrunning,+*ourchildwillfailtostart(possibly+*afteratimeoutonthelock),butwedon't+*care(whoresponds)ifthesocketislive.+*/+s=ipc_get_active_state(cl_args.path);+if(s==IPC_STATE__LISTENING)+return0;++time(&now);+if(now>time_limit)+returnerror(_("daemon not online yet"));++continue;+}++elseif(pid_seen==pid_child){+/*+*Thenewchilddaemonprocessshutdownwhile+*itwasstartingup,soitisnotlistening+*onthesocket.+*+*Trytopingthesocketintheoddchance+*thatanotherdaemonstarted(orwasalready+*running)whileourchildwasstarting.+*+*Again,wedon'tcarewhoservicesthesocket.+*/+s=ipc_get_active_state(cl_args.path);+if(s==IPC_STATE__LISTENING)+return0;++/*+*Wedon'tcareabouttheWEXITSTATUS()nor+*anyoftheWIF*(status)valuesbecause+*`cmd__simple_ipc()`doesthe`!!result`+*trickonallfunctionreturnvalues.+*+*Soitissufficienttojustreportthe+*earlyshutdownasanerror.+*/+returnerror(_("daemon failed to start"));+}++else+returnerror(_("waitpid is confused"));+}+}++/*+*Thisprocesswillstartasimple-ipcserverinabackgroundprocessand+*waitforittobecomeready.Thisislike`daemonize()`butgivesus+*morecontrolandbettererrorreporting(andmakesiteasiertowrite+*unittests).+*/+staticintdaemon__start_server(void)+{+pid_tpid_child;+intret;++/*+*Runtheactualdaemoninabackgroundprocess.+*/+ret=spawn_server(&pid_child);+if(pid_child<=0)+returnret;++/*+*Lettheparentwaitforthechildprocesstogetstarted+*andbeginlisteningforrequestsonthesocket.+*/+ret=wait_for_server_startup(pid_child);++returnret;+}++/*+*Thisprocesswillrunaquickprobetoseeifasimple-ipcserver+*isactiveonthispath.+*+*Returns0iftheserverisalive.+*/+staticintclient__probe_server(void)+{+enumipc_active_states;++s=ipc_get_active_state(cl_args.path);+switch(s){+caseIPC_STATE__LISTENING:+return0;++caseIPC_STATE__NOT_LISTENING:+returnerror("no server listening at '%s'",cl_args.path);++caseIPC_STATE__PATH_NOT_FOUND:+returnerror("path not found '%s'",cl_args.path);++caseIPC_STATE__INVALID_PATH:+returnerror("invalid pipe/socket name '%s'",cl_args.path);++caseIPC_STATE__OTHER_ERROR:+default:+returnerror("other error for '%s'",cl_args.path);+}+}++/*+*SendanIPCcommandtokentoanalready-runningserverdaemonand+*printtheresponse.+*+*Thisisasimple1wordcommand/tokenthat`test_app_cb()`(inthe+*daemonprocess)willunderstand.+*/+staticintclient__send_ipc(void)+{+constchar*command="(no-command)";+structstrbufbuf=STRBUF_INIT;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++if(cl_args.token&&*cl_args.token)+command=cl_args.token;++options.wait_if_busy=1;+options.wait_if_not_found=0;++if(!ipc_client_send_command(cl_args.path,&options,command,&buf)){+if(buf.len){+printf("%s\n",buf.buf);+fflush(stdout);+}+strbuf_release(&buf);++return0;+}++returnerror("failed to send '%s' to '%s'",command,cl_args.path);+}++/*+*SendanIPCcommandtoanalready-runningserverandaskitto+*shutdown."send quit"isanasyncrequestandqueuesashutdown+*eventintheserver,sowespinandwaithereforittoactually+*shutdowntomaketheunittestsalittleeasiertowrite.+*/+staticintclient__stop_server(void)+{+intret;+time_ttime_limit,now;+enumipc_active_states;++time(&time_limit);+time_limit+=cl_args.max_wait_sec;++cl_args.token="quit";++ret=client__send_ipc();+if(ret)+returnret;++for(;;){+sleep_millisec(100);++s=ipc_get_active_state(cl_args.path);++if(s!=IPC_STATE__LISTENING){+/*+*Thesocket/pipeisgoneand/orhasstopped+*responding.Letsassumethatthedaemon+*processhasexitedtoo.+*/+return0;+}++time(&now);+if(now>time_limit)+returnerror(_("daemon has not shutdown yet"));+}+}++/*+*SendanIPCcommandfollowedbyballasttoconfirmthatalarge+*messagecanbesentandthatthekernelorpkt-linelayerswill+*properlychunkitandthatthedaemonreceivestheentiremessage.+*/+staticintdo_sendbytes(intbytecount,charbyte,constchar*path,+conststructipc_client_connect_options*options)+{+structstrbufbuf_send=STRBUF_INIT;+structstrbufbuf_resp=STRBUF_INIT;++strbuf_addstr(&buf_send,"sendbytes ");+strbuf_addchars(&buf_send,byte,bytecount);++if(!ipc_client_send_command(path,options,buf_send.buf,&buf_resp)){+strbuf_rtrim(&buf_resp);+printf("sent:%c%08d %s\n",byte,bytecount,buf_resp.buf);+fflush(stdout);+strbuf_release(&buf_send);+strbuf_release(&buf_resp);++return0;+}++returnerror("client failed to sendbytes(%d, '%c') to '%s'",+bytecount,byte,path);+}++/*+*SendanIPCcommandwithballasttoanalready-runningserverdaemon.+*/+staticintclient__sendbytes(void)+{+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++options.wait_if_busy=1;+options.wait_if_not_found=0;+options.uds_disallow_chdir=0;++returndo_sendbytes(cl_args.bytecount,cl_args.bytevalue,cl_args.path,+&options);+}++structmultiple_thread_data{+pthread_tpthread_id;+structmultiple_thread_data*next;+constchar*path;+intbytecount;+intbatchsize;+intsum_errors;+intsum_good;+charletter;+};++staticvoid*multiple_thread_proc(void*_multiple_thread_data)+{+structmultiple_thread_data*d=_multiple_thread_data;+intk;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++options.wait_if_busy=1;+options.wait_if_not_found=0;+/*+*Amulti-threadedclientshouldnotberandomlycallingchdir().+*Thetestwillpasswithoutthisrestrictionbecausethetestis+*nototherwiseaccessingthefilesystem,butitmakesushonest.+*/+options.uds_disallow_chdir=1;++trace2_thread_start("multiple");++for(k=0;k<d->batchsize;k++){+if(do_sendbytes(d->bytecount+k,d->letter,d->path,&options))+d->sum_errors++;+else+d->sum_good++;+}++trace2_thread_exit();+returnNULL;+}++/*+*Startaclient-sidethreadpool.Eachthreadsendsaseriesof+*IPCrequests.Eachrequestisonanewconnectiontotheserver.+*/+staticintclient__multiple(void)+{+structmultiple_thread_data*list=NULL;+intk;+intsum_join_errors=0;+intsum_thread_errors=0;+intsum_good=0;++for(k=0;k<cl_args.nr_threads;k++){+structmultiple_thread_data*d=xcalloc(1,sizeof(*d));+d->next=list;+d->path=cl_args.path;+d->bytecount=cl_args.bytecount+cl_args.batchsize*(k/26);+d->batchsize=cl_args.batchsize;+d->sum_errors=0;+d->sum_good=0;+d->letter='A'+(k%26);++if(pthread_create(&d->pthread_id,NULL,multiple_thread_proc,d)){+warning("failed to create thread[%d] skipping remainder",k);+free(d);+break;+}++list=d;+}++while(list){+structmultiple_thread_data*d=list;++if(pthread_join(d->pthread_id,NULL))+sum_join_errors++;++sum_thread_errors+=d->sum_errors;+sum_good+=d->sum_good;++list=d->next;+free(d);+}++printf("client (good %d) (join %d), (errors %d)\n",+sum_good,sum_join_errors,sum_thread_errors);++return(sum_join_errors+sum_thread_errors)?1:0;+}++intcmd__simple_ipc(intargc,constchar**argv)+{+constchar*constsimple_ipc_usage[]={+N_("test-helper simple-ipc is-active [<name>] [<options>]"),+N_("test-helper simple-ipc run-daemon [<name>] [<threads>]"),+N_("test-helper simple-ipc start-daemon [<name>] [<threads>] [<max-wait>]"),+N_("test-helper simple-ipc stop-daemon [<name>] [<max-wait>]"),+N_("test-helper simple-ipc send [<name>] [<token>]"),+N_("test-helper simple-ipc sendbytes [<name>] [<bytecount>] [<byte>]"),+N_("test-helper simple-ipc multiple [<name>] [<threads>] [<bytecount>] [<batchsize>]"),+NULL+};++constchar*bytevalue=NULL;++structoptionoptions[]={+#ifndef GIT_WINDOWS_NATIVE+OPT_STRING(0,"name",&cl_args.path,N_("name"),N_("name or pathname of unix domain socket")),+#else+OPT_STRING(0,"name",&cl_args.path,N_("name"),N_("named-pipe name")),+#endif+OPT_INTEGER(0,"threads",&cl_args.nr_threads,N_("number of threads in server thread pool")),+OPT_INTEGER(0,"max-wait",&cl_args.max_wait_sec,N_("seconds to wait for daemon to start or stop")),++OPT_INTEGER(0,"bytecount",&cl_args.bytecount,N_("number of bytes")),+OPT_INTEGER(0,"batchsize",&cl_args.batchsize,N_("number of requests per thread")),++OPT_STRING(0,"byte",&bytevalue,N_("byte"),N_("ballast character")),+OPT_STRING(0,"token",&cl_args.token,N_("token"),N_("command token to send to the server")),++OPT_END()+};++if(argc<2)+usage_with_options(simple_ipc_usage,options);++if(argc==2&&!strcmp(argv[1],"-h"))+usage_with_options(simple_ipc_usage,options);++if(argc==2&&!strcmp(argv[1],"SUPPORTS_SIMPLE_IPC"))+return0;++cl_args.subcommand=argv[1];++argc--;+argv++;++argc=parse_options(argc,argv,NULL,options,simple_ipc_usage,0);++if(cl_args.nr_threads<1)+cl_args.nr_threads=1;+if(cl_args.max_wait_sec<0)+cl_args.max_wait_sec=0;+if(cl_args.bytecount<1)+cl_args.bytecount=1;+if(cl_args.batchsize<1)+cl_args.batchsize=1;++if(bytevalue&&*bytevalue)+cl_args.bytevalue=bytevalue[0];++/*+*Use'!!'onalldispatchfunctionstomapfrom`error()`style+*(returns-1)styleto`test_must_fail`style(expects1).This+*makesshellerrormessageslessconfusing.+*/++if(!strcmp(cl_args.subcommand,"is-active"))+return!!client__probe_server();++if(!strcmp(cl_args.subcommand,"run-daemon"))+return!!daemon__run_server();++if(!strcmp(cl_args.subcommand,"start-daemon"))+return!!daemon__start_server();++/*+*Clientcommandsfollow.Ensureaserverisrunningbefore+*sendinganydata.Thismightbeoverkill,butthenagain+*thisisatestharness.+*/++if(!strcmp(cl_args.subcommand,"stop-daemon")){+if(client__probe_server())+return1;+return!!client__stop_server();+}++if(!strcmp(cl_args.subcommand,"send")){+if(client__probe_server())+return1;+return!!client__send_ipc();+}++if(!strcmp(cl_args.subcommand,"sendbytes")){+if(client__probe_server())+return1;+return!!client__sendbytes();+}++if(!strcmp(cl_args.subcommand,"multiple")){+if(client__probe_server())+return1;+return!!client__multiple();+}++die("Unhandled subcommand: '%s'",cl_args.subcommand);+}+#endif
@@ -0,0 +1,122 @@+#!/bin/sh++test_description='simple command server'++../test-lib.sh++test-toolsimple-ipcSUPPORTS_SIMPLE_IPC||{+skip_all='simple IPC not supported on this platform'+test_done+}++stop_simple_IPC_server(){+test-toolsimple-ipcstop-daemon+}++test_expect_success'start simple command server''+test_atexitstop_simple_IPC_server&&+test-toolsimple-ipcstart-daemon--threads=8&&+test-toolsimple-ipcis-active+'++test_expect_success'simple command server''+test-toolsimple-ipcsend--token=ping>actual&&+echopong>expect&&+test_cmpexpectactual+'++test_expect_success'servers cannot share the same path''+test_must_failtest-toolsimple-ipcrun-daemon&&+test-toolsimple-ipcis-active+'++test_expect_success'big response''+test-toolsimple-ipcsend--token=big>actual&&+test_line_count-ge10000actual&&+grep-q"big: [0]*9999\$"actual+'++test_expect_success'chunk response''+test-toolsimple-ipcsend--token=chunk>actual&&+test_line_count-ge10000actual&&+grep-q"big: [0]*9999\$"actual+'++test_expect_success'slow response''+test-toolsimple-ipcsend--token=slow>actual&&+test_line_count-ge100actual&&+grep-q"big: [0]*99\$"actual+'++# Send an IPC with n=100,000 bytes of ballast. This should be large enough+# to force both the kernel and the pkt-line layer to chunk the message to the+# daemon and for the daemon to receive it in chunks.+#+test_expect_success'sendbytes''+test-toolsimple-ipcsendbytes--bytecount=100000--byte=A>actual&&+grep"sent:A00100000 rcvd:A00100000"actual+'++# Start a series of <threads> client threads that each make <batchsize>+# IPC requests to the server. Each (<threads> * <batchsize>) request+# will open a new connection to the server and randomly bind to a server+# thread. Each client thread exits after completing its batch. So the+# total number of live client threads will be smaller than the total.+# Each request will send a message containing at least <bytecount> bytes+# of ballast. (Responses are small.)+#+# The purpose here is to test threading in the server and responding to+# many concurrent client requests (regardless of whether they come from+# 1 client process or many). And to test that the server side of the+# named pipe/socket is stable. (On Windows this means that the server+# pipe is properly recycled.)+#+# On Windows it also lets us adjust the connection timeout in the+# `ipc_client_send_command()`.+#+# Note it is easy to drive the system into failure by requesting an+# insane number of threads on client or server and/or increasing the+# per-thread batchsize or the per-request bytecount (ballast).+# On Windows these failures look like "pipe is busy" errors.+# So I've chosen fairly conservative values for now.+#+# We expect output of the form "sent:<letter><length> ..."+# With terms (7, 19, 13) we expect:+# <letter> in [A-G]+# <length> in [19+0 .. 19+(13-1)]+# and (7 * 13) successful responses.+#+test_expect_success'stress test threads''+test-toolsimple-ipcmultiple\+--threads=7\+--bytecount=19\+--batchsize=13\+>actual&&+test_line_count=92actual&&+grep"good 91"actual&&+grep"sent:A"<actual>actual_a&&+cat>expect_a<<-EOF&&+sent:A00000019rcvd:A00000019+sent:A00000020rcvd:A00000020+sent:A00000021rcvd:A00000021+sent:A00000022rcvd:A00000022+sent:A00000023rcvd:A00000023+sent:A00000024rcvd:A00000024+sent:A00000025rcvd:A00000025+sent:A00000026rcvd:A00000026+sent:A00000027rcvd:A00000027+sent:A00000028rcvd:A00000028+sent:A00000029rcvd:A00000029+sent:A00000030rcvd:A00000030+sent:A00000031rcvd:A00000031+EOF+test_cmpexpect_aactual_a+'++test_expect_success'stop-daemon works''+test-toolsimple-ipcstop-daemon&&+test_must_failtest-toolsimple-ipcis-active&&+test_must_failtest-toolsimple-ipcsend--token=ping+'++test_done
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-09 15:03:54
From: Jeff Hostetler <redacted>
Create a wrapper class for `unix_stream_listen()` that uses a ".lock"
lockfile to create the unix domain socket in a race-free manner.
Unix domain sockets have a fundamental problem on Unix systems because
they persist in the filesystem until they are deleted. This is
independent of whether a server is actually listening for connections.
Well-behaved servers are expected to delete the socket when they
shutdown. A new server cannot easily tell if a found socket is
attached to an active server or is leftover cruft from a dead server.
The traditional solution used by `unix_stream_listen()` is to force
delete the socket pathname and then create a new socket. This solves
the latter (cruft) problem, but in the case of the former, it orphans
the existing server (by stealing the pathname associated with the
socket it is listening on).
We cannot directly use a .lock lockfile to create the socket because
the socket is created by `bind(2)` rather than the `open(2)` mechanism
used by `tempfile.c`.
As an alternative, we hold a plain lockfile ("<path>.lock") as a
mutual exclusion device. Under the lock, we test if an existing
socket ("<path>") is has an active server. If not, we create a new
socket and begin listening. Then we use "rollback" to delete the
lockfile in all cases.
This wrapper code conceptually exists at a higher-level than the core
unix_stream_connect() and unix_stream_listen() routines that it
consumes. It is isolated in a wrapper class for clarity.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 1 +
contrib/buildsystems/CMakeLists.txt | 2 +-
unix-stream-server.c | 128 ++++++++++++++++++++++++++++
unix-stream-server.h | 36 ++++++++
4 files changed, 166 insertions(+), 1 deletion(-)
create mode 100644 unix-stream-server.c
create mode 100644 unix-stream-server.h
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-09 15:03:54
From: Jeff Hostetler <redacted>
Calls to `chdir()` are dangerous in a multi-threaded context. If
`unix_stream_listen()` or `unix_stream_connect()` is given a socket
pathname that is too long to fit in a `sockaddr_un` structure, it will
`chdir()` to the parent directory of the requested socket pathname,
create the socket using a relative pathname, and then `chdir()` back.
This is not thread-safe.
Teach `unix_sockaddr_init()` to not allow calls to `chdir()` when this
flag is set.
Signed-off-by: Jeff Hostetler <redacted>
---
builtin/credential-cache.c | 2 +-
unix-socket.c | 17 ++++++++++++-----
unix-socket.h | 3 ++-
3 files changed, 15 insertions(+), 7 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-15 21:09:20
From: Jeff Hostetler <redacted>
Teach `packet_write_gently()` to write the pkt-line header and the actual
buffer in 2 separate calls to `write_in_full()` and avoid the need for a
static buffer, thread-safe scratch space, or an excessively large stack
buffer.
Change `write_packetized_from_fd()` to allocate a temporary buffer rather
than using a static buffer to avoid similar issues here.
These changes are intended to make it easier to use pkt-line routines in
a multi-threaded context with multiple concurrent writers writing to
different streams.
Signed-off-by: Jeff Hostetler <redacted>
---
pkt-line.c | 28 ++++++++++++++++++++--------
1 file changed, 20 insertions(+), 8 deletions(-)
From: Johannes Schindelin via GitGitGadget <hidden> Date: 2021-03-15 21:09:22
From: Johannes Schindelin <redacted>
Introduce PACKET_READ_GENTLE_ON_READ_ERROR option to help libify the
packet readers.
So far, the (possibly indirect) callers of `get_packet_data()` can ask
that function to return an error instead of `die()`ing upon end-of-file.
However, random read errors will still cause the process to die.
So let's introduce an explicit option to tell the packet reader
machinery to please be nice and only return an error on read errors.
This change prepares pkt-line for use by long-running daemon processes.
Such processes should be able to serve multiple concurrent clients and
and survive random IO errors. If there is an error on one connection,
a daemon should be able to drop that connection and continue serving
existing and future connections.
This ability will be used by a Git-aware "Builtin FSMonitor" feature
in a later patch series.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
pkt-line.c | 19 +++++++++++++++++--
pkt-line.h | 11 ++++++++---
2 files changed, 25 insertions(+), 5 deletions(-)
@@ -306,8 +306,11 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,*src_size-=ret;}else{ret=read_in_full(fd,dst,size);-if(ret<0)+if(ret<0){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror_errno(_("read error"));die_errno(_("read error"));+}}/* And complain if we didn't get enough bytes to satisfy the read. */
@@ -315,6 +318,8 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,if(options&PACKET_READ_GENTLE_ON_EOF)return-1;+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("the remote end hung up unexpectedly"));die(_("the remote end hung up unexpectedly"));}
@@ -343,6 +348,9 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer,len=packet_length(linelen);if(len<0){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("protocol error: bad line length "+"character: %.4s"),linelen);die(_("protocol error: bad line length character: %.4s"),linelen);}elseif(!len){packet_trace("0000",4,0);
@@ -357,12 +365,19 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer,*pktlen=0;returnPACKET_READ_RESPONSE_END;}elseif(len<4){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("protocol error: bad line length %d"),+len);die(_("protocol error: bad line length %d"),len);}len-=4;-if((unsigned)len>=size)+if((unsigned)len>=size){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("protocol error: bad line length %d"),+len);die(_("protocol error: bad line length %d"),len);+}if(get_packet_data(fd,src_buffer,src_len,buffer,len,options)<0){*pktlen=-1;
From: Johannes Schindelin via GitGitGadget <hidden> Date: 2021-03-15 21:09:22
From: Johannes Schindelin <redacted>
Remove the `packet_flush_gently()` call in `write_packetized_from_buf() and
`write_packetized_from_fd()` and require the caller to call it if desired.
Rename both functions to `write_packetized_from_*_no_flush()` to prevent
later merge accidents.
`write_packetized_from_buf()` currently only has one caller:
`apply_multi_file_filter()` in `convert.c`. It always wants a flush packet
to be written after writing the payload.
However, we are about to introduce a caller that wants to write many
packets before a final flush packet, so let's make the caller responsible
for emitting the flush packet.
Signed-off-by: Jeff Hostetler <redacted>
Signed-off-by: Johannes Schindelin <redacted>
---
convert.c | 8 ++++++--
pkt-line.c | 8 ++------
pkt-line.h | 4 ++--
3 files changed, 10 insertions(+), 10 deletions(-)
@@ -267,13 +267,11 @@ int write_packetized_from_fd(int fd_in, int fd_out)break;err=packet_write_gently(fd_out,buf,bytes_to_write);}-if(!err)-err=packet_flush_gently(fd_out);free(buf);returnerr;}-intwrite_packetized_from_buf(constchar*src_in,size_tlen,intfd_out)+intwrite_packetized_from_buf_no_flush(constchar*src_in,size_tlen,intfd_out){interr=0;size_tbytes_written=0;
@@ -289,8 +287,6 @@ int write_packetized_from_buf(const char *src_in, size_t len, int fd_out)err=packet_write_gently(fd_out,src_in+bytes_written,bytes_to_write);bytes_written+=bytes_to_write;}-if(!err)-err=packet_flush_gently(fd_out);returnerr;}
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-15 21:09:22
From: Jeff Hostetler <redacted>
Brief design documentation for new IPC mechanism allowing
foreground Git client to talk with an existing daemon process
at a known location using a named pipe or unix domain socket.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
Documentation/technical/api-simple-ipc.txt | 105 +++++++++++++++++++++
1 file changed, 105 insertions(+)
create mode 100644 Documentation/technical/api-simple-ipc.txt
@@ -0,0 +1,105 @@+Simple-IPC API+==============++The Simple-IPC API is a collection of `ipc_` prefixed library routines+and a basic communication protocol that allow an IPC-client process to+send an application-specific IPC-request message to an IPC-server+process and receive an application-specific IPC-response message.++Communication occurs over a named pipe on Windows and a Unix domain+socket on other platforms. IPC-clients and IPC-servers rendezvous at+a previously agreed-to application-specific pathname (which is outside+the scope of this design) that is local to the computer system.++The IPC-server routines within the server application process create a+thread pool to listen for connections and receive request messages+from multiple concurrent IPC-clients. When received, these messages+are dispatched up to the server application callbacks for handling.+IPC-server routines then incrementally relay responses back to the+IPC-client.++The IPC-client routines within a client application process connect+to the IPC-server and send a request message and wait for a response.+When received, the response is returned back the caller.++For example, the `fsmonitor--daemon` feature will be built as a server+application on top of the IPC-server library routines. It will have+threads watching for file system events and a thread pool waiting for+client connections. Clients, such as `git status` will request a list+of file system events since a point in time and the server will+respond with a list of changed files and directories. The formats of+the request and response are application-specific; the IPC-client and+IPC-server routines treat them as opaque byte streams.+++Comparison with sub-process model+---------------------------------++The Simple-IPC mechanism differs from the existing `sub-process.c`+model (Documentation/technical/long-running-process-protocol.txt) and+used by applications like Git-LFS. In the LFS-style sub-process model+the helper is started by the foreground process, communication happens+via a pair of file descriptors bound to the stdin/stdout of the+sub-process, the sub-process only serves the current foreground+process, and the sub-process exits when the foreground process+terminates.++In the Simple-IPC model the server is a very long-running service. It+can service many clients at the same time and has a private socket or+named pipe connection to each active client. It might be started+(on-demand) by the current client process or it might have been+started by a previous client or by the OS at boot time. The server+process is not associated with a terminal and it persists after+clients terminate. Clients do not have access to the stdin/stdout of+the server process and therefore must communicate over sockets or+named pipes.+++Server startup and shutdown+---------------------------++How an application server based upon IPC-server is started is also+outside the scope of the Simple-IPC design and is a property of the+application using it. For example, the server might be started or+restarted during routine maintenance operations, or it might be+started as a system service during the system boot-up sequence, or it+might be started on-demand by a foreground Git command when needed.++Similarly, server shutdown is a property of the application using+the simple-ipc routines. For example, the server might decide to+shutdown when idle or only upon explicit request.+++Simple-IPC protocol+-------------------++The Simple-IPC protocol consists of a single request message from the+client and an optional response message from the server. Both the+client and server messages are unlimited in length and are terminated+with a flush packet.++The pkt-line routines (Documentation/technical/protocol-common.txt)+are used to simplify buffer management during message generation,+transmission, and reception. A flush packet is used to mark the end+of the message. This allows the sender to incrementally generate and+transmit the message. It allows the receiver to incrementally receive+the message in chunks and to know when they have received the entire+message.++The actual byte format of the client request and server response+messages are application specific. The IPC layer transmits and+receives them as opaque byte buffers without any concern for the+content within. It is the job of the calling application layer to+understand the contents of the request and response messages.+++Summary+-------++Conceptually, the Simple-IPC protocol is similar to an HTTP REST+request. Clients connect, make an application-specific and+stateless request, receive an application-specific+response, and disconnect. It is a one round trip facility for+querying the server. The Simple-IPC routines hide the socket,+named pipe, and thread pool details and allow the application+layer to focus on the application at hand.
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-15 21:09:22
From: Jeff Hostetler <redacted>
Update `unix_stream_listen()` to take an options structure to override
default behaviors. This commit includes the size of the `listen()` backlog.
Signed-off-by: Jeff Hostetler <redacted>
---
builtin/credential-cache--daemon.c | 3 ++-
unix-socket.c | 11 +++++++++--
unix-socket.h | 9 ++++++++-
3 files changed, 19 insertions(+), 4 deletions(-)
@@ -203,9 +203,10 @@ static int serve_cache_loop(int fd)staticvoidserve_cache(constchar*socket_path,intdebug){+structunix_stream_listen_optsopts=UNIX_STREAM_LISTEN_OPTS_INIT;intfd;-fd=unix_stream_listen(socket_path);+fd=unix_stream_listen(socket_path,&opts);if(fd<0)die_errno("unable to bind to '%s'",socket_path);
From: Johannes Schindelin via GitGitGadget <hidden> Date: 2021-03-15 21:09:22
From: Johannes Schindelin <redacted>
Update the calling sequence of `read_packetized_to_strbuf()` to take
an options argument and not assume a fixed set of options. Update the
only existing caller accordingly to explicitly pass the
formerly-assumed flags.
The `read_packetized_to_strbuf()` function calls `packet_read()` with
a fixed set of assumed options (`PACKET_READ_GENTLE_ON_EOF`). This
assumption has been fine for the single existing caller
`apply_multi_file_filter()` in `convert.c`.
In a later commit we would like to add other callers to
`read_packetized_to_strbuf()` that need a different set of options.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
convert.c | 3 ++-
pkt-line.c | 4 ++--
pkt-line.h | 2 +-
3 files changed, 5 insertions(+), 4 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-15 21:09:23
From: Jeff Hostetler <redacted>
Create a wrapper class for `unix_stream_listen()` that uses a ".lock"
lockfile to create the unix domain socket in a race-free manner.
Unix domain sockets have a fundamental problem on Unix systems because
they persist in the filesystem until they are deleted. This is
independent of whether a server is actually listening for connections.
Well-behaved servers are expected to delete the socket when they
shutdown. A new server cannot easily tell if a found socket is
attached to an active server or is leftover cruft from a dead server.
The traditional solution used by `unix_stream_listen()` is to force
delete the socket pathname and then create a new socket. This solves
the latter (cruft) problem, but in the case of the former, it orphans
the existing server (by stealing the pathname associated with the
socket it is listening on).
We cannot directly use a .lock lockfile to create the socket because
the socket is created by `bind(2)` rather than the `open(2)` mechanism
used by `tempfile.c`.
As an alternative, we hold a plain lockfile ("<path>.lock") as a
mutual exclusion device. Under the lock, we test if an existing
socket ("<path>") is has an active server. If not, we create a new
socket and begin listening. Then we use "rollback" to delete the
lockfile in all cases.
This wrapper code conceptually exists at a higher-level than the core
unix_stream_connect() and unix_stream_listen() routines that it
consumes. It is isolated in a wrapper class for clarity.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 1 +
contrib/buildsystems/CMakeLists.txt | 2 +-
unix-stream-server.c | 125 ++++++++++++++++++++++++++++
unix-stream-server.h | 33 ++++++++
4 files changed, 160 insertions(+), 1 deletion(-)
create mode 100644 unix-stream-server.c
create mode 100644 unix-stream-server.h
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-15 21:09:23
From: Jeff Hostetler <redacted>
Calls to `chdir()` are dangerous in a multi-threaded context. If
`unix_stream_listen()` or `unix_stream_connect()` is given a socket
pathname that is too long to fit in a `sockaddr_un` structure, it will
`chdir()` to the parent directory of the requested socket pathname,
create the socket using a relative pathname, and then `chdir()` back.
This is not thread-safe.
Teach `unix_sockaddr_init()` to not allow calls to `chdir()` when this
flag is set.
Signed-off-by: Jeff Hostetler <redacted>
---
builtin/credential-cache.c | 2 +-
unix-socket.c | 17 ++++++++++++-----
unix-socket.h | 3 ++-
3 files changed, 15 insertions(+), 7 deletions(-)
@@ -0,0 +1,751 @@+#include"cache.h"+#include"simple-ipc.h"+#include"strbuf.h"+#include"pkt-line.h"+#include"thread-utils.h"++#ifndef GIT_WINDOWS_NATIVE+#error This file can only be compiled on Windows+#endif++staticintinitialize_pipe_name(constchar*path,wchar_t*wpath,size_talloc)+{+intoff=0;+structstrbufrealpath=STRBUF_INIT;++if(!strbuf_realpath(&realpath,path,0))+return-1;++off=swprintf(wpath,alloc,L"\\\\.\\pipe\\");+if(xutftowcs(wpath+off,realpath.buf,alloc-off)<0)+return-1;++/* Handle drive prefix */+if(wpath[off]&&wpath[off+1]==L':'){+wpath[off+1]=L'_';+off+=2;+}++for(;wpath[off];off++)+if(wpath[off]==L'/')+wpath[off]=L'\\';++strbuf_release(&realpath);+return0;+}++staticenumipc_active_stateget_active_state(wchar_t*pipe_path)+{+if(WaitNamedPipeW(pipe_path,NMPWAIT_USE_DEFAULT_WAIT))+returnIPC_STATE__LISTENING;++if(GetLastError()==ERROR_SEM_TIMEOUT)+returnIPC_STATE__NOT_LISTENING;++if(GetLastError()==ERROR_FILE_NOT_FOUND)+returnIPC_STATE__PATH_NOT_FOUND;++returnIPC_STATE__OTHER_ERROR;+}++enumipc_active_stateipc_get_active_state(constchar*path)+{+wchar_tpipe_path[MAX_PATH];++if(initialize_pipe_name(path,pipe_path,ARRAY_SIZE(pipe_path))<0)+returnIPC_STATE__INVALID_PATH;++returnget_active_state(pipe_path);+}++#define WAIT_STEP_MS (50)++staticenumipc_active_stateconnect_to_server(+constwchar_t*wpath,+DWORDtimeout_ms,+conststructipc_client_connect_options*options,+int*pfd)+{+DWORDt_start_ms,t_waited_ms;+DWORDstep_ms;+HANDLEhPipe=INVALID_HANDLE_VALUE;+DWORDmode=PIPE_READMODE_BYTE;+DWORDgle;++*pfd=-1;++for(;;){+hPipe=CreateFileW(wpath,GENERIC_READ|GENERIC_WRITE,+0,NULL,OPEN_EXISTING,0,NULL);+if(hPipe!=INVALID_HANDLE_VALUE)+break;++gle=GetLastError();++switch(gle){+caseERROR_FILE_NOT_FOUND:+if(!options->wait_if_not_found)+returnIPC_STATE__PATH_NOT_FOUND;+if(!timeout_ms)+returnIPC_STATE__PATH_NOT_FOUND;++step_ms=(timeout_ms<WAIT_STEP_MS)?+timeout_ms:WAIT_STEP_MS;+sleep_millisec(step_ms);++timeout_ms-=step_ms;+break;/* try again */++caseERROR_PIPE_BUSY:+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;+if(!timeout_ms)+returnIPC_STATE__NOT_LISTENING;++t_start_ms=(DWORD)(getnanotime()/1000000);++if(!WaitNamedPipeW(wpath,timeout_ms)){+if(GetLastError()==ERROR_SEM_TIMEOUT)+returnIPC_STATE__NOT_LISTENING;++returnIPC_STATE__OTHER_ERROR;+}++/*+*Apipeserverinstancebecameavailable.+*Raceotherclientprocessestoconnectto+*it.+*+*Butfirstdecrementouroveralltimeoutso+*thatwedon'tstarveifwekeeplosingthe+*race.Butalsoguardagainstspecial+*NPMWAIT_values(0and-1).+*/+t_waited_ms=(DWORD)(getnanotime()/1000000)-t_start_ms;+if(t_waited_ms<timeout_ms)+timeout_ms-=t_waited_ms;+else+timeout_ms=1;+break;/* try again */++default:+returnIPC_STATE__OTHER_ERROR;+}+}++if(!SetNamedPipeHandleState(hPipe,&mode,NULL,NULL)){+CloseHandle(hPipe);+returnIPC_STATE__OTHER_ERROR;+}++*pfd=_open_osfhandle((intptr_t)hPipe,O_RDWR|O_BINARY);+if(*pfd<0){+CloseHandle(hPipe);+returnIPC_STATE__OTHER_ERROR;+}++/* fd now owns hPipe */++returnIPC_STATE__LISTENING;+}++/*+*ThedefaultconnectiontimeoutforWindowsclients.+*+*Thisisnotcurrentlypartoftheipc_API(northeconfigsettings)+*becauseofdifferencesbetweenWindowsandotherplatforms.+*+*Thisvaluewaschosenatrandom.+*/+#define WINDOWS_CONNECTION_TIMEOUT_MS (30000)++enumipc_active_stateipc_client_try_connect(+constchar*path,+conststructipc_client_connect_options*options,+structipc_client_connection**p_connection)+{+wchar_twpath[MAX_PATH];+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+intfd=-1;++*p_connection=NULL;++trace2_region_enter("ipc-client","try-connect",NULL);+trace2_data_string("ipc-client",NULL,"try-connect/path",path);++if(initialize_pipe_name(path,wpath,ARRAY_SIZE(wpath))<0)+state=IPC_STATE__INVALID_PATH;+else+state=connect_to_server(wpath,WINDOWS_CONNECTION_TIMEOUT_MS,+options,&fd);++trace2_data_intmax("ipc-client",NULL,"try-connect/state",+(intmax_t)state);+trace2_region_leave("ipc-client","try-connect",NULL);++if(state==IPC_STATE__LISTENING){+(*p_connection)=xcalloc(1,sizeof(structipc_client_connection));+(*p_connection)->fd=fd;+}++returnstate;+}++voidipc_client_close_connection(structipc_client_connection*connection)+{+if(!connection)+return;++if(connection->fd!=-1)+close(connection->fd);++free(connection);+}++intipc_client_send_command_to_connection(+structipc_client_connection*connection,+constchar*message,structstrbuf*answer)+{+intret=0;++strbuf_setlen(answer,0);++trace2_region_enter("ipc-client","send-command",NULL);++if(write_packetized_from_buf_no_flush(message,strlen(message),+connection->fd)<0||+packet_flush_gently(connection->fd)<0){+ret=error(_("could not send IPC command"));+gotodone;+}++FlushFileBuffers((HANDLE)_get_osfhandle(connection->fd));++if(read_packetized_to_strbuf(+connection->fd,answer,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR)<0){+ret=error(_("could not read IPC response"));+gotodone;+}++done:+trace2_region_leave("ipc-client","send-command",NULL);+returnret;+}++intipc_client_send_command(constchar*path,+conststructipc_client_connect_options*options,+constchar*message,structstrbuf*response)+{+intret=-1;+enumipc_active_statestate;+structipc_client_connection*connection=NULL;++state=ipc_client_try_connect(path,options,&connection);++if(state!=IPC_STATE__LISTENING)+returnret;++ret=ipc_client_send_command_to_connection(connection,message,response);++ipc_client_close_connection(connection);++returnret;+}++/*+*Duplicatethegivenpipehandleandwrapitinafiledescriptorso+*thatwecanusepkt-lineonit.+*/+staticintdup_fd_from_pipe(constHANDLEpipe)+{+HANDLEprocess=GetCurrentProcess();+HANDLEhandle;+intfd;++if(!DuplicateHandle(process,pipe,process,&handle,0,FALSE,+DUPLICATE_SAME_ACCESS)){+errno=err_win_to_posix(GetLastError());+return-1;+}++fd=_open_osfhandle((intptr_t)handle,O_RDWR|O_BINARY);+if(fd<0){+errno=err_win_to_posix(GetLastError());+CloseHandle(handle);+return-1;+}++/*+*`handle`isnowownedby`fd`andwillbeautomaticallyclosed+*whenthedescriptorisclosed.+*/++returnfd;+}++/*+*Magicnumbersusedtoannotatecallbackinstancedata.+*Theseareusedtohelpguardagainstaccidentallypassingthe+*wronginstancedataacrossmultiplelevelsofcallbacks(which+*iseasytodoifthereare`void*`arguments).+*/+enummagic{+MAGIC_SERVER_REPLY_DATA,+MAGIC_SERVER_THREAD_DATA,+MAGIC_SERVER_DATA,+};++structipc_server_reply_data{+enummagicmagic;+intfd;+structipc_server_thread_data*server_thread_data;+};++structipc_server_thread_data{+enummagicmagic;+structipc_server_thread_data*next_thread;+structipc_server_data*server_data;+pthread_tpthread_id;+HANDLEhPipe;+};++/*+*OnWindows,theconceptual"ipc-server"isimplementedasapoolof+*nidential/peer"server-thread"threads.Thatis,thereisno+*hierarchyofthreads;andthereforenocontrollerthreadmanaging+*thepool.Eachthreadhasanindependenthandletothenamedpipe,+*receivesincomingconnections,processestheclient,andre-uses+*thepipeforthenextclientconnection.+*+*Therefore,the"ipc-server"onlyneedstomaintainalistofthe+*spawnedthreadsforeventual"join"purposes.+*+*Asingle"stop-event"isvisibletoalloftheserverthreadsto+*tellthemtoshutdown(whenidle).+*/+structipc_server_data{+enummagicmagic;+ipc_server_application_cb*application_cb;+void*application_data;+structstrbufbuf_path;+wchar_twpath[MAX_PATH];++HANDLEhEventStopRequested;+structipc_server_thread_data*thread_list;+intis_stopped;+};++enumconnect_result{+CR_CONNECTED=0,+CR_CONNECT_PENDING,+CR_CONNECT_ERROR,+CR_WAIT_ERROR,+CR_SHUTDOWN,+};++staticenumconnect_resultqueue_overlapped_connect(+structipc_server_thread_data*server_thread_data,+OVERLAPPED*lpo)+{+if(ConnectNamedPipe(server_thread_data->hPipe,lpo))+gotofailed;++switch(GetLastError()){+caseERROR_IO_PENDING:+returnCR_CONNECT_PENDING;++caseERROR_PIPE_CONNECTED:+SetEvent(lpo->hEvent);+returnCR_CONNECTED;++default:+break;+}++failed:+error(_("ConnectNamedPipe failed for '%s' (%lu)"),+server_thread_data->server_data->buf_path.buf,+GetLastError());+returnCR_CONNECT_ERROR;+}++/*+*UseWindowsOverlappedIOtowaitforaconnectionorforourevent+*tobesignalled.+*/+staticenumconnect_resultwait_for_connection(+structipc_server_thread_data*server_thread_data,+OVERLAPPED*lpo)+{+enumconnect_resultr;+HANDLEwaitHandles[2];+DWORDdwWaitResult;++r=queue_overlapped_connect(server_thread_data,lpo);+if(r!=CR_CONNECT_PENDING)+returnr;++waitHandles[0]=server_thread_data->server_data->hEventStopRequested;+waitHandles[1]=lpo->hEvent;++dwWaitResult=WaitForMultipleObjects(2,waitHandles,FALSE,INFINITE);+switch(dwWaitResult){+caseWAIT_OBJECT_0+0:+returnCR_SHUTDOWN;++caseWAIT_OBJECT_0+1:+ResetEvent(lpo->hEvent);+returnCR_CONNECTED;++default:+returnCR_WAIT_ERROR;+}+}++/*+*Forwarddeclareourreplycallbackfunctionsothatanycompiler+*errorsarereportedwhenweactuallydefinethefunction(inaddition+*toanyerrorsreportedwhenwetrytopassthiscallbackfunctionas+*aparameterinafunctioncall).Theformerareeasiertounderstand.+*/+staticipc_server_reply_cbdo_io_reply_callback;++/*+*Relayapplication'sresponsemessagetotheclientprocess.+*(Wedonotflushatthispointbecauseweallowthecaller+*tochunkdatatotheclientthruus.)+*/+staticintdo_io_reply_callback(structipc_server_reply_data*reply_data,+constchar*response,size_tresponse_len)+{+if(reply_data->magic!=MAGIC_SERVER_REPLY_DATA)+BUG("reply_cb called with wrong instance data");++returnwrite_packetized_from_buf_no_flush(response,response_len,+reply_data->fd);+}++/*+*Receivetherequest/commandfromtheclientandpassittothe+*registeredrequest-callback.Therequest-callbackwillcompose+*aresponseandcallourreply-callbacktosendittotheclient.+*+*Simple-IPConlycontainsoneroundtrip,soweflushandclose+*hereaftertheresponse.+*/+staticintdo_io(structipc_server_thread_data*server_thread_data)+{+structstrbufbuf=STRBUF_INIT;+structipc_server_reply_datareply_data;+intret=0;++reply_data.magic=MAGIC_SERVER_REPLY_DATA;+reply_data.server_thread_data=server_thread_data;++reply_data.fd=dup_fd_from_pipe(server_thread_data->hPipe);+if(reply_data.fd<0)+returnerror(_("could not create fd from pipe for '%s'"),+server_thread_data->server_data->buf_path.buf);++ret=read_packetized_to_strbuf(+reply_data.fd,&buf,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR);+if(ret>=0){+ret=server_thread_data->server_data->application_cb(+server_thread_data->server_data->application_data,+buf.buf,do_io_reply_callback,&reply_data);++packet_flush_gently(reply_data.fd);++FlushFileBuffers((HANDLE)_get_osfhandle((reply_data.fd)));+}+else{+/*+*Theclientprobablydisconnected/shutdownbeforeit+*couldsendawell-formedmessage.Ignoreit.+*/+}++strbuf_release(&buf);+close(reply_data.fd);++returnret;+}++/*+*HandleIPCrequestandresponsewiththisconnectedclient.Andreset+*thepipetoprepareforthenextclient.+*/+staticintuse_connection(structipc_server_thread_data*server_thread_data)+{+intret;++ret=do_io(server_thread_data);++FlushFileBuffers(server_thread_data->hPipe);+DisconnectNamedPipe(server_thread_data->hPipe);++returnret;+}++/*+*ThreadprocforanIPCserverworkerthread.Ithandlesaseriesof+*connectionsfromclients.ItcleansandreusesthehPipebetweeneach+*client.+*/+staticvoid*server_thread_proc(void*_server_thread_data)+{+structipc_server_thread_data*server_thread_data=_server_thread_data;+HANDLEhEventConnected=INVALID_HANDLE_VALUE;+OVERLAPPEDoConnect;+enumconnect_resultcr;+intret;++assert(server_thread_data->hPipe!=INVALID_HANDLE_VALUE);++trace2_thread_start("ipc-server");+trace2_data_string("ipc-server",NULL,"pipe",+server_thread_data->server_data->buf_path.buf);++hEventConnected=CreateEventW(NULL,TRUE,FALSE,NULL);++memset(&oConnect,0,sizeof(oConnect));+oConnect.hEvent=hEventConnected;++for(;;){+cr=wait_for_connection(server_thread_data,&oConnect);++switch(cr){+caseCR_SHUTDOWN:+gotofinished;++caseCR_CONNECTED:+ret=use_connection(server_thread_data);+if(ret==SIMPLE_IPC_QUIT){+ipc_server_stop_async(+server_thread_data->server_data);+gotofinished;+}+if(ret>0){+/*+*Ignore(transient)IOerrorswiththis+*clientandresetforthenextclient.+*/+}+break;++caseCR_CONNECT_PENDING:+/* By construction, this should not happen. */+BUG("ipc-server[%s]: unexpeced CR_CONNECT_PENDING",+server_thread_data->server_data->buf_path.buf);++caseCR_CONNECT_ERROR:+caseCR_WAIT_ERROR:+/*+*Ignorethesetheoreticalerrors.+*/+DisconnectNamedPipe(server_thread_data->hPipe);+break;++default:+BUG("unandled case after wait_for_connection");+}+}++finished:+CloseHandle(server_thread_data->hPipe);+CloseHandle(hEventConnected);++trace2_thread_exit();+returnNULL;+}++staticHANDLEcreate_new_pipe(wchar_t*wpath,intis_first)+{+HANDLEhPipe;+DWORDdwOpenMode,dwPipeMode;+LPSECURITY_ATTRIBUTESlpsa=NULL;++dwOpenMode=PIPE_ACCESS_INBOUND|PIPE_ACCESS_OUTBOUND|+FILE_FLAG_OVERLAPPED;++dwPipeMode=PIPE_TYPE_MESSAGE|PIPE_READMODE_BYTE|PIPE_WAIT|+PIPE_REJECT_REMOTE_CLIENTS;++if(is_first){+dwOpenMode|=FILE_FLAG_FIRST_PIPE_INSTANCE;++/*+*OnWindows,thefirstserverpipeinstancegetsto+*settheACL/SecurityAttributesonthenamed+*pipe;subsequentinstancesinheritandcannot+*changethem.+*+*TODOShouldweallowtheapplicationlayerto+*specifysecurityattributes,suchas`LocalService`+*or`LocalSystem`,whenwecreatethenamedpipe?+*Thisquestionisprobablynotimportantwhenthe+*daemonisstartedbyaforegrounduserprocessand+*onlyneedstotalktothecurrentuser,butmaybe+*ifthedaemonisrunviatheControlPanelasa+*SystemService.+*/+}++hPipe=CreateNamedPipeW(wpath,dwOpenMode,dwPipeMode,+PIPE_UNLIMITED_INSTANCES,1024,1024,0,lpsa);++returnhPipe;+}++intipc_server_run_async(structipc_server_data**returned_server_data,+constchar*path,conststructipc_server_opts*opts,+ipc_server_application_cb*application_cb,+void*application_data)+{+structipc_server_data*server_data;+wchar_twpath[MAX_PATH];+HANDLEhPipeFirst=INVALID_HANDLE_VALUE;+intk;+intret=0;+intnr_threads=opts->nr_threads;++*returned_server_data=NULL;++ret=initialize_pipe_name(path,wpath,ARRAY_SIZE(wpath));+if(ret<0){+errno=EINVAL;+return-1;+}++hPipeFirst=create_new_pipe(wpath,1);+if(hPipeFirst==INVALID_HANDLE_VALUE){+errno=EADDRINUSE;+return-2;+}++server_data=xcalloc(1,sizeof(*server_data));+server_data->magic=MAGIC_SERVER_DATA;+server_data->application_cb=application_cb;+server_data->application_data=application_data;+server_data->hEventStopRequested=CreateEvent(NULL,TRUE,FALSE,NULL);+strbuf_init(&server_data->buf_path,0);+strbuf_addstr(&server_data->buf_path,path);+wcscpy(server_data->wpath,wpath);++if(nr_threads<1)+nr_threads=1;++for(k=0;k<nr_threads;k++){+structipc_server_thread_data*std;++std=xcalloc(1,sizeof(*std));+std->magic=MAGIC_SERVER_THREAD_DATA;+std->server_data=server_data;+std->hPipe=INVALID_HANDLE_VALUE;++std->hPipe=(k==0)+?hPipeFirst+:create_new_pipe(server_data->wpath,0);++if(std->hPipe==INVALID_HANDLE_VALUE){+/*+*Ifwe'vereachedapipeinstancelimitfor+*thispath,justusefewerthreads.+*/+free(std);+break;+}++if(pthread_create(&std->pthread_id,NULL,+server_thread_proc,std)){+/*+*Likewise,ifwe'reoutofthreads,justuse+*fewerthreadsthanrequested.+*+*However,wejustgiveupifwecan'tevenget+*onethread.Thisshouldnothappen.+*/+if(k==0)+die(_("could not start thread[0] for '%s'"),+path);++CloseHandle(std->hPipe);+free(std);+break;+}++std->next_thread=server_data->thread_list;+server_data->thread_list=std;+}++*returned_server_data=server_data;+return0;+}++intipc_server_stop_async(structipc_server_data*server_data)+{+if(!server_data)+return0;++/*+*Gentlytellalloftheipc_serverthreadstoshutdown.+*Thiswillbeseenthenexttimetheyareidle(andwaiting+*foraconnection).+*+*WeDONOTattempttoforcethemtodropanactiveconnection.+*/+SetEvent(server_data->hEventStopRequested);+return0;+}++intipc_server_await(structipc_server_data*server_data)+{+DWORDdwWaitResult;++if(!server_data)+return0;++dwWaitResult=WaitForSingleObject(server_data->hEventStopRequested,INFINITE);+if(dwWaitResult!=WAIT_OBJECT_0)+returnerror(_("wait for hEvent failed for '%s'"),+server_data->buf_path.buf);++while(server_data->thread_list){+structipc_server_thread_data*std=server_data->thread_list;++pthread_join(std->pthread_id,NULL);++server_data->thread_list=std->next_thread;+free(std);+}++server_data->is_stopped=1;++return0;+}++voidipc_server_free(structipc_server_data*server_data)+{+if(!server_data)+return;++if(!server_data->is_stopped)+BUG("cannot free ipc-server while running for '%s'",+server_data->buf_path.buf);++strbuf_release(&server_data->buf_path);++if(server_data->hEventStopRequested!=INVALID_HANDLE_VALUE)+CloseHandle(server_data->hEventStopRequested);++while(server_data->thread_list){+structipc_server_thread_data*std=server_data->thread_list;++server_data->thread_list=std->next_thread;+free(std);+}++free(server_data);+}
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-15 21:09:23
From: Jeff Hostetler <redacted>
The static helper function `unix_stream_socket()` calls `die()`. This
is not appropriate for all callers. Eliminate the wrapper function
and make the callers propagate the error.
Signed-off-by: Jeff Hostetler <redacted>
---
unix-socket.c | 27 +++++++++++++--------------
1 file changed, 13 insertions(+), 14 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-15 21:09:23
From: Jeff Hostetler <redacted>
Create Unix domain socket based implementation of "simple-ipc".
A set of `ipc_client` routines implement a client library to connect
to an `ipc_server` over a Unix domain socket, send a simple request,
and receive a single response. Clients use blocking IO on the socket.
A set of `ipc_server` routines implement a thread pool to listen for
and concurrently service client connections.
The server creates a new Unix domain socket at a known location. If a
socket already exists with that name, the server tries to determine if
another server is already listening on the socket or if the socket is
dead. If socket is busy, the server exits with an error rather than
stealing the socket. If the socket is dead, the server creates a new
one and starts up.
If while running, the server detects that its socket has been stolen
by another server, it automatically exits.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 2 +
compat/simple-ipc/ipc-unix-socket.c | 1000 +++++++++++++++++++++++++++
contrib/buildsystems/CMakeLists.txt | 2 +
simple-ipc.h | 13 +-
4 files changed, 1016 insertions(+), 1 deletion(-)
create mode 100644 compat/simple-ipc/ipc-unix-socket.c
@@ -0,0 +1,1000 @@+#include"cache.h"+#include"simple-ipc.h"+#include"strbuf.h"+#include"pkt-line.h"+#include"thread-utils.h"+#include"unix-socket.h"+#include"unix-stream-server.h"++#ifdef NO_UNIX_SOCKETS+#error compat/simple-ipc/ipc-unix-socket.c requires Unix sockets+#endif++enumipc_active_stateipc_get_active_state(constchar*path)+{+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;+structstatst;+structipc_client_connection*connection_test=NULL;++options.wait_if_busy=0;+options.wait_if_not_found=0;++if(lstat(path,&st)==-1){+switch(errno){+caseENOENT:+caseENOTDIR:+returnIPC_STATE__NOT_LISTENING;+default:+returnIPC_STATE__INVALID_PATH;+}+}++/* also complain if a plain file is in the way */+if((st.st_mode&S_IFMT)!=S_IFSOCK)+returnIPC_STATE__INVALID_PATH;++/*+*JustbecausethefilesystemhasaS_IFSOCKtypeinode+*at`path`,doesn'tmeanitthatthereisaserverlistening.+*Pingittobesure.+*/+state=ipc_client_try_connect(path,&options,&connection_test);+ipc_client_close_connection(connection_test);++returnstate;+}++/*+*Retryfrequencywhentryingtoconnecttoaserver.+*+*Thisvalueshouldbeshortenoughthatwedon'tseriouslydelayour+*caller,butnotfastenoughthatourspinningputspressureonthe+*system.+*/+#define WAIT_STEP_MS (50)++/*+*Trytoconnecttotheserver.Iftheserverisjuststartingupor+*isverybusy,wemaynotgetaconnectionthefirsttime.+*/+staticenumipc_active_stateconnect_to_server(+constchar*path,+inttimeout_ms,+conststructipc_client_connect_options*options,+int*pfd)+{+intk;++*pfd=-1;++for(k=0;k<timeout_ms;k+=WAIT_STEP_MS){+intfd=unix_stream_connect(path,options->uds_disallow_chdir);++if(fd!=-1){+*pfd=fd;+returnIPC_STATE__LISTENING;+}++if(errno==ENOENT){+if(!options->wait_if_not_found)+returnIPC_STATE__PATH_NOT_FOUND;++gotosleep_and_try_again;+}++if(errno==ETIMEDOUT){+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;++gotosleep_and_try_again;+}++if(errno==ECONNREFUSED){+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;++gotosleep_and_try_again;+}++returnIPC_STATE__OTHER_ERROR;++sleep_and_try_again:+sleep_millisec(WAIT_STEP_MS);+}++returnIPC_STATE__NOT_LISTENING;+}++/*+*Thetotalamountoftimethatwearewillingtowaitwhentryingto+*connecttoaserver.+*+*Whentheserverisfirststarted,itmighttakealittlewhilefor+*ittobecomereadytoservicerequests.Likewise,theservermay+*bevery(temporarily)busyandnotrespondtoourconnections.+*+*Weshouldgracefullyandsilentlyhandlethoseconditionsandtry+*againforareasonabletimeperiod.+*+*Thevaluechosenhereshouldbelongenoughfortheserver+*toreliablyhealfromtheaboveconditions.+*/+#define MY_CONNECTION_TIMEOUT_MS (1000)++enumipc_active_stateipc_client_try_connect(+constchar*path,+conststructipc_client_connect_options*options,+structipc_client_connection**p_connection)+{+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+intfd=-1;++*p_connection=NULL;++trace2_region_enter("ipc-client","try-connect",NULL);+trace2_data_string("ipc-client",NULL,"try-connect/path",path);++state=connect_to_server(path,MY_CONNECTION_TIMEOUT_MS,+options,&fd);++trace2_data_intmax("ipc-client",NULL,"try-connect/state",+(intmax_t)state);+trace2_region_leave("ipc-client","try-connect",NULL);++if(state==IPC_STATE__LISTENING){+(*p_connection)=xcalloc(1,sizeof(structipc_client_connection));+(*p_connection)->fd=fd;+}++returnstate;+}++voidipc_client_close_connection(structipc_client_connection*connection)+{+if(!connection)+return;++if(connection->fd!=-1)+close(connection->fd);++free(connection);+}++intipc_client_send_command_to_connection(+structipc_client_connection*connection,+constchar*message,structstrbuf*answer)+{+intret=0;++strbuf_setlen(answer,0);++trace2_region_enter("ipc-client","send-command",NULL);++if(write_packetized_from_buf_no_flush(message,strlen(message),+connection->fd)<0||+packet_flush_gently(connection->fd)<0){+ret=error(_("could not send IPC command"));+gotodone;+}++if(read_packetized_to_strbuf(+connection->fd,answer,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR)<0){+ret=error(_("could not read IPC response"));+gotodone;+}++done:+trace2_region_leave("ipc-client","send-command",NULL);+returnret;+}++intipc_client_send_command(constchar*path,+conststructipc_client_connect_options*options,+constchar*message,structstrbuf*answer)+{+intret=-1;+enumipc_active_statestate;+structipc_client_connection*connection=NULL;++state=ipc_client_try_connect(path,options,&connection);++if(state!=IPC_STATE__LISTENING)+returnret;++ret=ipc_client_send_command_to_connection(connection,message,answer);++ipc_client_close_connection(connection);++returnret;+}++staticintset_socket_blocking_flag(intfd,intmake_nonblocking)+{+intflags;++flags=fcntl(fd,F_GETFL,NULL);++if(flags<0)+return-1;++if(make_nonblocking)+flags|=O_NONBLOCK;+else+flags&=~O_NONBLOCK;++returnfcntl(fd,F_SETFL,flags);+}++/*+*Magicnumbersusedtoannotatecallbackinstancedata.+*Theseareusedtohelpguardagainstaccidentallypassingthe+*wronginstancedataacrossmultiplelevelsofcallbacks(which+*iseasytodoifthereare`void*`arguments).+*/+enummagic{+MAGIC_SERVER_REPLY_DATA,+MAGIC_WORKER_THREAD_DATA,+MAGIC_ACCEPT_THREAD_DATA,+MAGIC_SERVER_DATA,+};++structipc_server_reply_data{+enummagicmagic;+intfd;+structipc_worker_thread_data*worker_thread_data;+};++structipc_worker_thread_data{+enummagicmagic;+structipc_worker_thread_data*next_thread;+structipc_server_data*server_data;+pthread_tpthread_id;+};++structipc_accept_thread_data{+enummagicmagic;+structipc_server_data*server_data;++structunix_ss_socket*server_socket;++intfd_send_shutdown;+intfd_wait_shutdown;+pthread_tpthread_id;+};++/*+*Withunix-sockets,theconceptual"ipc-server"isimplementedasasingle+*controller"accept-thread"threadandapoolof"worker-thread"threads.+*Theformerdoestheusual`accept()`loopanddispatchesconnections+*toanidleworkerthread.Theworkerthreadswaitinanidleloopfor+*anewconnection,communicatewiththeclientandrelaydatato/from+*the`application_cb`andthenwaitforanotherconnectionfromthe+*serverthread.Thisavoidstheoverheadofconstantlycreatingand+*destroyingthreads.+*/+structipc_server_data{+enummagicmagic;+ipc_server_application_cb*application_cb;+void*application_data;+structstrbufbuf_path;++structipc_accept_thread_data*accept_thread;+structipc_worker_thread_data*worker_thread_list;++pthread_mutex_twork_available_mutex;+pthread_cond_twork_available_cond;++/*+*Acceptedbutnotyetprocessedclientconnectionsarekept+*inacircularbufferFIFO.Thequeueisemptywhenthe+*positionsareequal.+*/+int*fifo_fds;+intqueue_size;+intback_pos;+intfront_pos;++intshutdown_requested;+intis_stopped;+};++/*+*Removeandreturntheoldestqueuedconnection.+*+*Returns-1ifempty.+*/+staticintfifo_dequeue(structipc_server_data*server_data)+{+/* ASSERT holding mutex */++intfd;++if(server_data->back_pos==server_data->front_pos)+return-1;++fd=server_data->fifo_fds[server_data->front_pos];+server_data->fifo_fds[server_data->front_pos]=-1;++server_data->front_pos++;+if(server_data->front_pos==server_data->queue_size)+server_data->front_pos=0;++returnfd;+}++/*+*Pushanewfdontothebackofthequeue.+*+*Dropitandreturn-1ifqueueisalreadyfull.+*/+staticintfifo_enqueue(structipc_server_data*server_data,intfd)+{+/* ASSERT holding mutex */++intnext_back_pos;++next_back_pos=server_data->back_pos+1;+if(next_back_pos==server_data->queue_size)+next_back_pos=0;++if(next_back_pos==server_data->front_pos){+/* Queue is full. Just drop it. */+close(fd);+return-1;+}++server_data->fifo_fds[server_data->back_pos]=fd;+server_data->back_pos=next_back_pos;++returnfd;+}++/*+*WaitforaconnectiontobequeuedtotheFIFOandreturnit.+*+*Returns-1ifsomeonehasalreadyrequestedashutdown.+*/+staticintworker_thread__wait_for_connection(+structipc_worker_thread_data*worker_thread_data)+{+/* ASSERT NOT holding mutex */++structipc_server_data*server_data=worker_thread_data->server_data;+intfd=-1;++pthread_mutex_lock(&server_data->work_available_mutex);+for(;;){+if(server_data->shutdown_requested)+break;++fd=fifo_dequeue(server_data);+if(fd>=0)+break;++pthread_cond_wait(&server_data->work_available_cond,+&server_data->work_available_mutex);+}+pthread_mutex_unlock(&server_data->work_available_mutex);++returnfd;+}++/*+*Forwarddeclareourreplycallbackfunctionsothatanycompiler+*errorsarereportedwhenweactuallydefinethefunction(inaddition+*toanyerrorsreportedwhenwetrytopassthiscallbackfunctionas+*aparameterinafunctioncall).Theformerareeasiertounderstand.+*/+staticipc_server_reply_cbdo_io_reply_callback;++/*+*Relayapplication'sresponsemessagetotheclientprocess.+*(Wedonotflushatthispointbecauseweallowthecaller+*tochunkdatatotheclientthruus.)+*/+staticintdo_io_reply_callback(structipc_server_reply_data*reply_data,+constchar*response,size_tresponse_len)+{+if(reply_data->magic!=MAGIC_SERVER_REPLY_DATA)+BUG("reply_cb called with wrong instance data");++returnwrite_packetized_from_buf_no_flush(response,response_len,+reply_data->fd);+}++/* A randomly chosen value. */+#define MY_WAIT_POLL_TIMEOUT_MS (10)++/*+*Iftheclienthangsupwithoutsendinganydataonthewire,just+*quietlyclosethesocketandignorethisclient.+*+*ThisworkerthreadiscommittedtoreadingtheIPCrequestdata+*fromtheclientattheotherendofthisfd.Waithereforthe+*clienttoactuallyputsomethingonthewire--becauseifthe+*clientjustdoesaping(connectandhangupwithoutsendingany+*data),ouruseofthepkt-linereadroutineswillspewanerror+*message.+*+*Return-1iftheclienthungup.+*Return0ifdata(possiblyincomplete)isready.+*/+staticintworker_thread__wait_for_io_start(+structipc_worker_thread_data*worker_thread_data,+intfd)+{+structipc_server_data*server_data=worker_thread_data->server_data;+structpollfdpollfd[1];+intresult;++for(;;){+pollfd[0].fd=fd;+pollfd[0].events=POLLIN;++result=poll(pollfd,1,MY_WAIT_POLL_TIMEOUT_MS);+if(result<0){+if(errno==EINTR)+continue;+gotocleanup;+}++if(result==0){+/* a timeout */++intin_shutdown;++pthread_mutex_lock(&server_data->work_available_mutex);+in_shutdown=server_data->shutdown_requested;+pthread_mutex_unlock(&server_data->work_available_mutex);++/*+*Ifashutdownisalreadyinprogressandthis+*clienthasnotstartedtalkingyet,justdropit.+*/+if(in_shutdown)+gotocleanup;+continue;+}++if(pollfd[0].revents&POLLHUP)+gotocleanup;++if(pollfd[0].revents&POLLIN)+return0;++gotocleanup;+}++cleanup:+close(fd);+return-1;+}++/*+*Receivetherequest/commandfromtheclientandpassittothe+*registeredrequest-callback.Therequest-callbackwillcompose+*aresponseandcallourreply-callbacktosendittotheclient.+*/+staticintworker_thread__do_io(+structipc_worker_thread_data*worker_thread_data,+intfd)+{+/* ASSERT NOT holding lock */++structstrbufbuf=STRBUF_INIT;+structipc_server_reply_datareply_data;+intret=0;++reply_data.magic=MAGIC_SERVER_REPLY_DATA;+reply_data.worker_thread_data=worker_thread_data;++reply_data.fd=fd;++ret=read_packetized_to_strbuf(+reply_data.fd,&buf,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR);+if(ret>=0){+ret=worker_thread_data->server_data->application_cb(+worker_thread_data->server_data->application_data,+buf.buf,do_io_reply_callback,&reply_data);++packet_flush_gently(reply_data.fd);+}+else{+/*+*Theclientprobablydisconnected/shutdownbeforeit+*couldsendawell-formedmessage.Ignoreit.+*/+}++strbuf_release(&buf);+close(reply_data.fd);++returnret;+}++/*+*BlockSIGPIPEonthecurrentthread(sothatwegetEPIPEfrom+*write()ratherthananactualsignal).+*+*Notethatusingsigchain_push()and_pop()tocontrolSIGPIPE+*aroundourIOcallsisnotthreadsafe:+*[]Itusesaglobalstackofhandlerframes.+*[]ItusesALLOC_GROW()toresizeit.+*[]Finally,accordingtothe`signal(2)`man-page:+*"The effects of `signal()` in a multithreaded process are unspecified."+*/+staticvoidthread_block_sigpipe(sigset_t*old_set)+{+sigset_tnew_set;++sigemptyset(&new_set);+sigaddset(&new_set,SIGPIPE);++sigemptyset(old_set);+pthread_sigmask(SIG_BLOCK,&new_set,old_set);+}++/*+*ThreadprocforanIPCworkerthread.Ithandlesaseriesof+*connectionsfromclients.Itpullsthenextfdfromthequeue+*processesit,andthenwaitsforthenextclient.+*+*BlockSIGPIPEinthisworkerthreadforthelifeofthethread.+*Thisavoidsstray(andsometimesdelayed)SIGPIPEsignalscaused+*byclienterrorsand/orwhenweareunderextremelyheavyIOload.+*+*ThismeansthattheapplicationcallbackwillhaveSIGPIPEblocked.+*Thecallbackshouldnotchangeit.+*/+staticvoid*worker_thread_proc(void*_worker_thread_data)+{+structipc_worker_thread_data*worker_thread_data=_worker_thread_data;+structipc_server_data*server_data=worker_thread_data->server_data;+sigset_told_set;+intfd,io;+intret;++trace2_thread_start("ipc-worker");++thread_block_sigpipe(&old_set);++for(;;){+fd=worker_thread__wait_for_connection(worker_thread_data);+if(fd==-1)+break;/* in shutdown */++io=worker_thread__wait_for_io_start(worker_thread_data,fd);+if(io==-1)+continue;/* client hung up without sending anything */++ret=worker_thread__do_io(worker_thread_data,fd);++if(ret==SIMPLE_IPC_QUIT){+trace2_data_string("ipc-worker",NULL,"queue_stop_async",+"application_quit");+/*+*Theapplicationlayeristellingtheipc-server+*layertoshutdown.+*+*WeDONOThavearesponsetosendtotheclient.+*+*Queueanasyncstop(tostoptheotherthreads)and+*allowthisworkerthreadtoexitnow(nosensewaiting+*forthethread-poolshutdownsignal).+*+*Othernon-idleworkerthreadsareallowedtofinish+*respondingtotheircurrentclients.+*/+ipc_server_stop_async(server_data);+break;+}+}++trace2_thread_exit();+returnNULL;+}++/* A randomly chosen value. */+#define MY_ACCEPT_POLL_TIMEOUT_MS (60 * 1000)++/*+*Acceptanewclientconnectiononoursocket.Thisusesnon-blocking+*IOsothatwecanalsowaitforshutdownrequestsonoursocket-pair+*withoutactuallyspinningonafasttimeout.+*/+staticintaccept_thread__wait_for_connection(+structipc_accept_thread_data*accept_thread_data)+{+structpollfdpollfd[2];+intresult;++for(;;){+pollfd[0].fd=accept_thread_data->fd_wait_shutdown;+pollfd[0].events=POLLIN;++pollfd[1].fd=accept_thread_data->server_socket->fd_socket;+pollfd[1].events=POLLIN;++result=poll(pollfd,2,MY_ACCEPT_POLL_TIMEOUT_MS);+if(result<0){+if(errno==EINTR)+continue;+returnresult;+}++if(result==0){+/* a timeout */++/*+*Ifsomeonedeletesorforce-createsanewunix+*domainsocketatourpath,allfutureclients+*willberoutedelsewhereandwesilentlystarve.+*Ifthathappens,justqueueashutdown.+*/+if(unix_ss_was_stolen(+accept_thread_data->server_socket)){+trace2_data_string("ipc-accept",NULL,+"queue_stop_async",+"socket_stolen");+ipc_server_stop_async(+accept_thread_data->server_data);+}+continue;+}++if(pollfd[0].revents&POLLIN){+/* shutdown message queued to socketpair */+return-1;+}++if(pollfd[1].revents&POLLIN){+/* a connection is available on server_socket */++intclient_fd=+accept(accept_thread_data->server_socket->fd_socket,+NULL,NULL);+if(client_fd>=0)+returnclient_fd;++/*+*Anerrorhereisunlikely--itprobably+*indicatesthattheconnectingprocesshas+*alreadydroppedtheconnection.+*/+continue;+}++BUG("unandled poll result errno=%d r[0]=%d r[1]=%d",+errno,pollfd[0].revents,pollfd[1].revents);+}+}++/*+*ThreadprocfortheIPCserver"accept thread".Thiswaitsfor+*anincomingsocketconnection,appendsittothequeueofavailable+*connections,andnotifiesaworkerthreadtoprocessit.+*+*BlockSIGPIPEinthisthreadforthelifeofthethread.This+*avoidsanystraySIGPIPEsignalswhenclosingpipefdsunder+*extremelyheavyloads(suchaswhenthefifoqueueisfullandwe+*dropincommingconnections).+*/+staticvoid*accept_thread_proc(void*_accept_thread_data)+{+structipc_accept_thread_data*accept_thread_data=_accept_thread_data;+structipc_server_data*server_data=accept_thread_data->server_data;+sigset_told_set;++trace2_thread_start("ipc-accept");++thread_block_sigpipe(&old_set);++for(;;){+intclient_fd=accept_thread__wait_for_connection(+accept_thread_data);++pthread_mutex_lock(&server_data->work_available_mutex);+if(server_data->shutdown_requested){+pthread_mutex_unlock(&server_data->work_available_mutex);+if(client_fd>=0)+close(client_fd);+break;+}++if(client_fd<0){+/* ignore transient accept() errors */+}+else{+fifo_enqueue(server_data,client_fd);+pthread_cond_broadcast(&server_data->work_available_cond);+}+pthread_mutex_unlock(&server_data->work_available_mutex);+}++trace2_thread_exit();+returnNULL;+}++/*+*Wecan'tpredicttheconnectionarrivalraterelativetotheworker+*processingrate,thereforeweallowthe"accept-thread"toqueueup+*agenerousnumberofconnections,sincewe'dratherhavetheclient+*notunnecessarilytimeoutifwecanavoidit.(Theassumptionis+*thatthiswillbeusedforFSMonitorandafewsecondwaitona+*connectionisbetterthanhavingtheclienttimeoutanddothefull+*computationitself.)+*+*TheFIFOqueuesizeissettoamultipleoftheworkerpoolsize.+*Thisvaluechosenatrandom.+*/+#define FIFO_SCALE (100)++/*+*Thebacklogvaluefor`listen(2)`.Thisdoesn'tneedtohuge,+*ratherjustlargeenoughforour"accept-thread"towakeupand+*queueincomingconnectionsontotheFIFOwithoutthekernel+*droppingany.+*+*Thisvaluechosenatrandom.+*/+#define LISTEN_BACKLOG (50)++staticintcreate_listener_socket(+constchar*path,+conststructipc_server_opts*ipc_opts,+structunix_ss_socket**new_server_socket)+{+structunix_ss_socket*server_socket=NULL;+structunix_stream_listen_optsuslg_opts=UNIX_STREAM_LISTEN_OPTS_INIT;+intret;++uslg_opts.listen_backlog_size=LISTEN_BACKLOG;+uslg_opts.disallow_chdir=ipc_opts->uds_disallow_chdir;++ret=unix_ss_create(path,&uslg_opts,-1,&server_socket);+if(ret)+returnret;++if(set_socket_blocking_flag(server_socket->fd_socket,1)){+intsaved_errno=errno;+unix_ss_free(server_socket);+errno=saved_errno;+return-1;+}++*new_server_socket=server_socket;++trace2_data_string("ipc-server",NULL,"listen-with-lock",path);+return0;+}++staticintsetup_listener_socket(+constchar*path,+conststructipc_server_opts*ipc_opts,+structunix_ss_socket**new_server_socket)+{+intret,saved_errno;++trace2_region_enter("ipc-server","create-listener_socket",NULL);++ret=create_listener_socket(path,ipc_opts,new_server_socket);++saved_errno=errno;+trace2_region_leave("ipc-server","create-listener_socket",NULL);+errno=saved_errno;++returnret;+}++/*+*StartIPCserverinapoolofbackgroundthreads.+*/+intipc_server_run_async(structipc_server_data**returned_server_data,+constchar*path,conststructipc_server_opts*opts,+ipc_server_application_cb*application_cb,+void*application_data)+{+structunix_ss_socket*server_socket=NULL;+structipc_server_data*server_data;+intsv[2];+intk;+intret;+intnr_threads=opts->nr_threads;++*returned_server_data=NULL;++/*+*Createasocketpairandsetsv[1]tonon-blocking.This+*willusedtosendashutdownmessagetotheaccept-thread+*andallowstheaccept-threadtowaitonEITHERaclient+*connectionorashutdownrequestwithoutspinning.+*/+if(socketpair(AF_UNIX,SOCK_STREAM,0,sv)<0)+return-1;++if(set_socket_blocking_flag(sv[1],1)){+intsaved_errno=errno;+close(sv[0]);+close(sv[1]);+errno=saved_errno;+return-1;+}++ret=setup_listener_socket(path,opts,&server_socket);+if(ret){+intsaved_errno=errno;+close(sv[0]);+close(sv[1]);+errno=saved_errno;+returnret;+}++server_data=xcalloc(1,sizeof(*server_data));+server_data->magic=MAGIC_SERVER_DATA;+server_data->application_cb=application_cb;+server_data->application_data=application_data;+strbuf_init(&server_data->buf_path,0);+strbuf_addstr(&server_data->buf_path,path);++if(nr_threads<1)+nr_threads=1;++pthread_mutex_init(&server_data->work_available_mutex,NULL);+pthread_cond_init(&server_data->work_available_cond,NULL);++server_data->queue_size=nr_threads*FIFO_SCALE;+server_data->fifo_fds=xcalloc(server_data->queue_size,+sizeof(*server_data->fifo_fds));++server_data->accept_thread=+xcalloc(1,sizeof(*server_data->accept_thread));+server_data->accept_thread->magic=MAGIC_ACCEPT_THREAD_DATA;+server_data->accept_thread->server_data=server_data;+server_data->accept_thread->server_socket=server_socket;+server_data->accept_thread->fd_send_shutdown=sv[0];+server_data->accept_thread->fd_wait_shutdown=sv[1];++if(pthread_create(&server_data->accept_thread->pthread_id,NULL,+accept_thread_proc,server_data->accept_thread))+die_errno(_("could not start accept_thread '%s'"),path);++for(k=0;k<nr_threads;k++){+structipc_worker_thread_data*wtd;++wtd=xcalloc(1,sizeof(*wtd));+wtd->magic=MAGIC_WORKER_THREAD_DATA;+wtd->server_data=server_data;++if(pthread_create(&wtd->pthread_id,NULL,worker_thread_proc,+wtd)){+if(k==0)+die(_("could not start worker[0] for '%s'"),+path);+/*+*Limpalongwiththethreadpoolthatwehave.+*/+break;+}++wtd->next_thread=server_data->worker_thread_list;+server_data->worker_thread_list=wtd;+}++*returned_server_data=server_data;+return0;+}++/*+*GentlytelltheIPCservertreadstoshutdown.+*Canberunonanythread.+*/+intipc_server_stop_async(structipc_server_data*server_data)+{+/* ASSERT NOT holding mutex */++intfd;++if(!server_data)+return0;++trace2_region_enter("ipc-server","server-stop-async",NULL);++pthread_mutex_lock(&server_data->work_available_mutex);++server_data->shutdown_requested=1;++/*+*Writeabytetotheshutdownsocketpairtowakeupthe+*accept-thread.+*/+if(write(server_data->accept_thread->fd_send_shutdown,"Q",1)<0)+error_errno("could not write to fd_send_shutdown");++/*+*Drainthequeueofexistingconnections.+*/+while((fd=fifo_dequeue(server_data))!=-1)+close(fd);++/*+*Gentlytellworkerthreadstostopprocessingnewconnections+*andexit.(Thisdoesnotabortin-processconversations.)+*/+pthread_cond_broadcast(&server_data->work_available_cond);++pthread_mutex_unlock(&server_data->work_available_mutex);++trace2_region_leave("ipc-server","server-stop-async",NULL);++return0;+}++/*+*WaitforallIPCserverthreadstostop.+*/+intipc_server_await(structipc_server_data*server_data)+{+pthread_join(server_data->accept_thread->pthread_id,NULL);++if(!server_data->shutdown_requested)+BUG("ipc-server: accept-thread stopped for '%s'",+server_data->buf_path.buf);++while(server_data->worker_thread_list){+structipc_worker_thread_data*wtd=+server_data->worker_thread_list;++pthread_join(wtd->pthread_id,NULL);++server_data->worker_thread_list=wtd->next_thread;+free(wtd);+}++server_data->is_stopped=1;++return0;+}++voidipc_server_free(structipc_server_data*server_data)+{+structipc_accept_thread_data*accept_thread_data;++if(!server_data)+return;++if(!server_data->is_stopped)+BUG("cannot free ipc-server while running for '%s'",+server_data->buf_path.buf);++accept_thread_data=server_data->accept_thread;+if(accept_thread_data){+unix_ss_free(accept_thread_data->server_socket);++if(accept_thread_data->fd_send_shutdown!=-1)+close(accept_thread_data->fd_send_shutdown);+if(accept_thread_data->fd_wait_shutdown!=-1)+close(accept_thread_data->fd_wait_shutdown);++free(server_data->accept_thread);+}++while(server_data->worker_thread_list){+structipc_worker_thread_data*wtd=+server_data->worker_thread_list;++server_data->worker_thread_list=wtd->next_thread;+free(wtd);+}++pthread_cond_destroy(&server_data->work_available_cond);+pthread_mutex_destroy(&server_data->work_available_mutex);++strbuf_release(&server_data->buf_path);++free(server_data->fifo_fds);+free(server_data);+}
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-15 21:09:23
From: Jeff Hostetler <redacted>
Create t0052-simple-ipc.sh with unit tests for the "simple-ipc" mechanism.
Create t/helper/test-simple-ipc test tool to exercise the "simple-ipc"
functions.
When the tool is invoked with "run-daemon", it runs a server to listen
for "simple-ipc" connections on a test socket or named pipe and
responds to a set of commands to exercise/stress the communication
setup.
When the tool is invoked with "start-daemon", it spawns a "run-daemon"
command in the background and waits for the server to become ready
before exiting. (This helps make unit tests in t0052 more predictable
and avoids the need for arbitrary sleeps in the test script.)
The tool also has a series of client "send" commands to send commands
and data to a server instance.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 1 +
t/helper/test-simple-ipc.c | 787 +++++++++++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/t0052-simple-ipc.sh | 122 ++++++
5 files changed, 912 insertions(+)
create mode 100644 t/helper/test-simple-ipc.c
create mode 100755 t/t0052-simple-ipc.sh
@@ -0,0 +1,787 @@+/*+*test-simple-ipc.c:verifythattheInter-ProcessCommunicationworks.+*/++#include"test-tool.h"+#include"cache.h"+#include"strbuf.h"+#include"simple-ipc.h"+#include"parse-options.h"+#include"thread-utils.h"+#include"strvec.h"++#ifndef SUPPORTS_SIMPLE_IPC+intcmd__simple_ipc(intargc,constchar**argv)+{+die("simple IPC not available on this platform");+}+#else++/*+*Thetestdaemondefinesan"application callback"thatsupportsa+*seriesofcommands(see`test_app_cb()`).+*+*Unknowncommandsarecaughthereandwesendanerrormessageback+*totheclientprocess.+*/+staticintapp__unhandled_command(constchar*command,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+intret;++strbuf_addf(&buf,"unhandled command: %s",command);+ret=reply_cb(reply_data,buf.buf,buf.len);+strbuf_release(&buf);++returnret;+}++/*+*Replywithasingleverylargebuffer.Thisistoensurethat+*longresponseareproperlyhandled--whetherthechunkingoccurs+*inthekernelorinthe(probablypkt-line)layer.+*/+#define BIG_ROWS (10000)+staticintapp__big_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<BIG_ROWS;row++)+strbuf_addf(&buf,"big: %.75d\n",row);++ret=reply_cb(reply_data,buf.buf,buf.len);+strbuf_release(&buf);++returnret;+}++/*+*Replywithaseriesoflines.Thisistoensurethatwecanincrementally+*computetheresponseandchunkittotheclient.+*/+#define CHUNK_ROWS (10000)+staticintapp__chunk_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<CHUNK_ROWS;row++){+strbuf_setlen(&buf,0);+strbuf_addf(&buf,"big: %.75d\n",row);+ret=reply_cb(reply_data,buf.buf,buf.len);+}++strbuf_release(&buf);++returnret;+}++/*+*Slowlyreplywithaseriesoflines.Thisistomodelanexpensiveto+*computechunkedresponse(whichmighthappenifthiscallbackisrunning+*inathreadandisfightingforalockwithotherthreads).+*/+#define SLOW_ROWS (1000)+#define SLOW_DELAY_MS (10)+staticintapp__slow_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<SLOW_ROWS;row++){+strbuf_setlen(&buf,0);+strbuf_addf(&buf,"big: %.75d\n",row);+ret=reply_cb(reply_data,buf.buf,buf.len);+sleep_millisec(SLOW_DELAY_MS);+}++strbuf_release(&buf);++returnret;+}++/*+*Theclientsentacommandfollowedbya(possiblyvery)largebuffer.+*/+staticintapp__sendbytes_command(constchar*received,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf_resp=STRBUF_INIT;+constchar*p="?";+intlen_ballast=0;+intk;+interrs=0;+intret;++if(skip_prefix(received,"sendbytes ",&p))+len_ballast=strlen(p);++/*+*Verifythattheballastisncopiesofasingleletter.+*Andthatthemulti-threadedIOlayerdidn'tcrossthestreams.+*/+for(k=1;k<len_ballast;k++)+if(p[k]!=p[0])+errs++;++if(errs)+strbuf_addf(&buf_resp,"errs:%d\n",errs);+else+strbuf_addf(&buf_resp,"rcvd:%c%08d\n",p[0],len_ballast);++ret=reply_cb(reply_data,buf_resp.buf,buf_resp.len);++strbuf_release(&buf_resp);++returnret;+}++/*+*Anarbitraryfixedaddresstoverifythattheapplicationinstance+*dataishandledproperly.+*/+staticintmy_app_data=42;++staticipc_server_application_cbtest_app_cb;++/*+*Thisisthe"application callback"thatsitsontopofthe+*"ipc-server".Itcompletelydefinesthesetofcommandssupported+*bythisapplication.+*/+staticinttest_app_cb(void*application_data,+constchar*command,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+/*+*Verifythatwereceivedtheapplication-datathatwepassed+*whenwestartedtheipc-server.(Wehaveseverallayersof+*callbackscallingcallbacksandit'seasytogetthingsmixed+*up(especiallywhensomeare"void*").)+*/+if(application_data!=(void*)&my_app_data)+BUG("application_cb: application_data pointer wrong");++if(!strcmp(command,"quit")){+/*+*Theclientsenta"quit"command.Thisisanasync+*requestfortheservertoshutdown.+*+*WeDONOTsendtheclientaresponsemessage+*(becausewehavenothingtosayandtheother+*serverthreadshavenotyetstopped).+*+*Telltheipc-serverlayertostartshuttingdown.+*Thisincludes:stoplisteningfornewconnections+*onthesocket/pipeandtellingallworkerthreads+*tofinish/draintheiroutgoingresponsestoother+*clients.+*+*ThisDOESNOTforceanimmediatesyncshutdown.+*/+returnSIMPLE_IPC_QUIT;+}++if(!strcmp(command,"ping")){+constchar*answer="pong";+returnreply_cb(reply_data,answer,strlen(answer));+}++if(!strcmp(command,"big"))+returnapp__big_command(reply_cb,reply_data);++if(!strcmp(command,"chunk"))+returnapp__chunk_command(reply_cb,reply_data);++if(!strcmp(command,"slow"))+returnapp__slow_command(reply_cb,reply_data);++if(starts_with(command,"sendbytes "))+returnapp__sendbytes_command(command,reply_cb,reply_data);++returnapp__unhandled_command(command,reply_cb,reply_data);+}++structcl_args+{+constchar*subcommand;+constchar*path;+constchar*token;++intnr_threads;+intmax_wait_sec;+intbytecount;+intbatchsize;++charbytevalue;+};++staticstructcl_argscl_args={+.subcommand=NULL,+.path="ipc-test",+.token=NULL,++.nr_threads=5,+.max_wait_sec=60,+.bytecount=1024,+.batchsize=10,++.bytevalue='x',+};++/*+*Thisprocesswillrunasasimple-ipcserverandlistenforIPCcommands+*fromclientprocesses.+*/+staticintdaemon__run_server(void)+{+intret;++structipc_server_optsopts={+.nr_threads=cl_args.nr_threads,+};++/*+*Synchronouslyruntheipc-server.Wedon'tneedanyapplication+*instancedata,sopassanarbitrarypointer(thatwe'lllater+*verifymadetheroundtrip).+*/+ret=ipc_server_run(cl_args.path,&opts,test_app_cb,(void*)&my_app_data);+if(ret==-2)+error(_("socket/pipe already in use: '%s'"),cl_args.path);+elseif(ret==-1)+error_errno(_("could not start server on: '%s'"),cl_args.path);++returnret;+}++#ifndef GIT_WINDOWS_NATIVE+/*+*Thisisadaptedfrom`daemonize()`.Use`fork()`todirectlycreateand+*runthedaemoninachildprocess.+*/+staticintspawn_server(pid_t*pid)+{+structipc_server_optsopts={+.nr_threads=cl_args.nr_threads,+};++*pid=fork();++switch(*pid){+case0:+if(setsid()==-1)+error_errno(_("setsid failed"));+close(0);+close(1);+close(2);+sanitize_stdfds();++returnipc_server_run(cl_args.path,&opts,test_app_cb,+(void*)&my_app_data);++case-1:+returnerror_errno(_("could not spawn daemon in the background"));++default:+return0;+}+}+#else+/*+*Conceptuallylike`daemonize()`butdifferentbecauseWindowsdoesnot+*have`fork(2)`.SpawnanormalWindowschildprocessbutwithoutthe+*limitationsof`start_command()`and`finish_command()`.+*/+staticintspawn_server(pid_t*pid)+{+chartest_tool_exe[MAX_PATH];+structstrvecargs=STRVEC_INIT;+intin,out;++GetModuleFileNameA(NULL,test_tool_exe,MAX_PATH);++in=open("/dev/null",O_RDONLY);+out=open("/dev/null",O_WRONLY);++strvec_push(&args,test_tool_exe);+strvec_push(&args,"simple-ipc");+strvec_push(&args,"run-daemon");+strvec_pushf(&args,"--name=%s",cl_args.path);+strvec_pushf(&args,"--threads=%d",cl_args.nr_threads);++*pid=mingw_spawnvpe(args.v[0],args.v,NULL,NULL,in,out,out);+close(in);+close(out);++strvec_clear(&args);++if(*pid<0)+returnerror(_("could not spawn daemon in the background"));++return0;+}+#endif++/*+*Thisisadaptedfrom`wait_or_whine()`.Watchthechildprocessand+*letitgetstartedandbeginlisteningforrequestsonthesocket+*beforereportingoursuccess.+*/+staticintwait_for_server_startup(pid_tpid_child)+{+intstatus;+pid_tpid_seen;+enumipc_active_states;+time_ttime_limit,now;++time(&time_limit);+time_limit+=cl_args.max_wait_sec;++for(;;){+pid_seen=waitpid(pid_child,&status,WNOHANG);++if(pid_seen==-1)+returnerror_errno(_("waitpid failed"));++elseif(pid_seen==0){+/*+*Thechildisstillrunning(thisshouldbe+*thenormalcase).Trytoconnecttoiton+*thesocketandseeifitisreadyfor+*business.+*+*Ifthereisanotherdaemonalreadyrunning,+*ourchildwillfailtostart(possibly+*afteratimeoutonthelock),butwedon't+*care(whoresponds)ifthesocketislive.+*/+s=ipc_get_active_state(cl_args.path);+if(s==IPC_STATE__LISTENING)+return0;++time(&now);+if(now>time_limit)+returnerror(_("daemon not online yet"));++continue;+}++elseif(pid_seen==pid_child){+/*+*Thenewchilddaemonprocessshutdownwhile+*itwasstartingup,soitisnotlistening+*onthesocket.+*+*Trytopingthesocketintheoddchance+*thatanotherdaemonstarted(orwasalready+*running)whileourchildwasstarting.+*+*Again,wedon'tcarewhoservicesthesocket.+*/+s=ipc_get_active_state(cl_args.path);+if(s==IPC_STATE__LISTENING)+return0;++/*+*Wedon'tcareabouttheWEXITSTATUS()nor+*anyoftheWIF*(status)valuesbecause+*`cmd__simple_ipc()`doesthe`!!result`+*trickonallfunctionreturnvalues.+*+*Soitissufficienttojustreportthe+*earlyshutdownasanerror.+*/+returnerror(_("daemon failed to start"));+}++else+returnerror(_("waitpid is confused"));+}+}++/*+*Thisprocesswillstartasimple-ipcserverinabackgroundprocessand+*waitforittobecomeready.Thisislike`daemonize()`butgivesus+*morecontrolandbettererrorreporting(andmakesiteasiertowrite+*unittests).+*/+staticintdaemon__start_server(void)+{+pid_tpid_child;+intret;++/*+*Runtheactualdaemoninabackgroundprocess.+*/+ret=spawn_server(&pid_child);+if(pid_child<=0)+returnret;++/*+*Lettheparentwaitforthechildprocesstogetstarted+*andbeginlisteningforrequestsonthesocket.+*/+ret=wait_for_server_startup(pid_child);++returnret;+}++/*+*Thisprocesswillrunaquickprobetoseeifasimple-ipcserver+*isactiveonthispath.+*+*Returns0iftheserverisalive.+*/+staticintclient__probe_server(void)+{+enumipc_active_states;++s=ipc_get_active_state(cl_args.path);+switch(s){+caseIPC_STATE__LISTENING:+return0;++caseIPC_STATE__NOT_LISTENING:+returnerror("no server listening at '%s'",cl_args.path);++caseIPC_STATE__PATH_NOT_FOUND:+returnerror("path not found '%s'",cl_args.path);++caseIPC_STATE__INVALID_PATH:+returnerror("invalid pipe/socket name '%s'",cl_args.path);++caseIPC_STATE__OTHER_ERROR:+default:+returnerror("other error for '%s'",cl_args.path);+}+}++/*+*SendanIPCcommandtokentoanalready-runningserverdaemonand+*printtheresponse.+*+*Thisisasimple1wordcommand/tokenthat`test_app_cb()`(inthe+*daemonprocess)willunderstand.+*/+staticintclient__send_ipc(void)+{+constchar*command="(no-command)";+structstrbufbuf=STRBUF_INIT;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++if(cl_args.token&&*cl_args.token)+command=cl_args.token;++options.wait_if_busy=1;+options.wait_if_not_found=0;++if(!ipc_client_send_command(cl_args.path,&options,command,&buf)){+if(buf.len){+printf("%s\n",buf.buf);+fflush(stdout);+}+strbuf_release(&buf);++return0;+}++returnerror("failed to send '%s' to '%s'",command,cl_args.path);+}++/*+*SendanIPCcommandtoanalready-runningserverandaskitto+*shutdown."send quit"isanasyncrequestandqueuesashutdown+*eventintheserver,sowespinandwaithereforittoactually+*shutdowntomaketheunittestsalittleeasiertowrite.+*/+staticintclient__stop_server(void)+{+intret;+time_ttime_limit,now;+enumipc_active_states;++time(&time_limit);+time_limit+=cl_args.max_wait_sec;++cl_args.token="quit";++ret=client__send_ipc();+if(ret)+returnret;++for(;;){+sleep_millisec(100);++s=ipc_get_active_state(cl_args.path);++if(s!=IPC_STATE__LISTENING){+/*+*Thesocket/pipeisgoneand/orhasstopped+*responding.Letsassumethatthedaemon+*processhasexitedtoo.+*/+return0;+}++time(&now);+if(now>time_limit)+returnerror(_("daemon has not shutdown yet"));+}+}++/*+*SendanIPCcommandfollowedbyballasttoconfirmthatalarge+*messagecanbesentandthatthekernelorpkt-linelayerswill+*properlychunkitandthatthedaemonreceivestheentiremessage.+*/+staticintdo_sendbytes(intbytecount,charbyte,constchar*path,+conststructipc_client_connect_options*options)+{+structstrbufbuf_send=STRBUF_INIT;+structstrbufbuf_resp=STRBUF_INIT;++strbuf_addstr(&buf_send,"sendbytes ");+strbuf_addchars(&buf_send,byte,bytecount);++if(!ipc_client_send_command(path,options,buf_send.buf,&buf_resp)){+strbuf_rtrim(&buf_resp);+printf("sent:%c%08d %s\n",byte,bytecount,buf_resp.buf);+fflush(stdout);+strbuf_release(&buf_send);+strbuf_release(&buf_resp);++return0;+}++returnerror("client failed to sendbytes(%d, '%c') to '%s'",+bytecount,byte,path);+}++/*+*SendanIPCcommandwithballasttoanalready-runningserverdaemon.+*/+staticintclient__sendbytes(void)+{+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++options.wait_if_busy=1;+options.wait_if_not_found=0;+options.uds_disallow_chdir=0;++returndo_sendbytes(cl_args.bytecount,cl_args.bytevalue,cl_args.path,+&options);+}++structmultiple_thread_data{+pthread_tpthread_id;+structmultiple_thread_data*next;+constchar*path;+intbytecount;+intbatchsize;+intsum_errors;+intsum_good;+charletter;+};++staticvoid*multiple_thread_proc(void*_multiple_thread_data)+{+structmultiple_thread_data*d=_multiple_thread_data;+intk;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++options.wait_if_busy=1;+options.wait_if_not_found=0;+/*+*Amulti-threadedclientshouldnotberandomlycallingchdir().+*Thetestwillpasswithoutthisrestrictionbecausethetestis+*nototherwiseaccessingthefilesystem,butitmakesushonest.+*/+options.uds_disallow_chdir=1;++trace2_thread_start("multiple");++for(k=0;k<d->batchsize;k++){+if(do_sendbytes(d->bytecount+k,d->letter,d->path,&options))+d->sum_errors++;+else+d->sum_good++;+}++trace2_thread_exit();+returnNULL;+}++/*+*Startaclient-sidethreadpool.Eachthreadsendsaseriesof+*IPCrequests.Eachrequestisonanewconnectiontotheserver.+*/+staticintclient__multiple(void)+{+structmultiple_thread_data*list=NULL;+intk;+intsum_join_errors=0;+intsum_thread_errors=0;+intsum_good=0;++for(k=0;k<cl_args.nr_threads;k++){+structmultiple_thread_data*d=xcalloc(1,sizeof(*d));+d->next=list;+d->path=cl_args.path;+d->bytecount=cl_args.bytecount+cl_args.batchsize*(k/26);+d->batchsize=cl_args.batchsize;+d->sum_errors=0;+d->sum_good=0;+d->letter='A'+(k%26);++if(pthread_create(&d->pthread_id,NULL,multiple_thread_proc,d)){+warning("failed to create thread[%d] skipping remainder",k);+free(d);+break;+}++list=d;+}++while(list){+structmultiple_thread_data*d=list;++if(pthread_join(d->pthread_id,NULL))+sum_join_errors++;++sum_thread_errors+=d->sum_errors;+sum_good+=d->sum_good;++list=d->next;+free(d);+}++printf("client (good %d) (join %d), (errors %d)\n",+sum_good,sum_join_errors,sum_thread_errors);++return(sum_join_errors+sum_thread_errors)?1:0;+}++intcmd__simple_ipc(intargc,constchar**argv)+{+constchar*constsimple_ipc_usage[]={+N_("test-helper simple-ipc is-active [<name>] [<options>]"),+N_("test-helper simple-ipc run-daemon [<name>] [<threads>]"),+N_("test-helper simple-ipc start-daemon [<name>] [<threads>] [<max-wait>]"),+N_("test-helper simple-ipc stop-daemon [<name>] [<max-wait>]"),+N_("test-helper simple-ipc send [<name>] [<token>]"),+N_("test-helper simple-ipc sendbytes [<name>] [<bytecount>] [<byte>]"),+N_("test-helper simple-ipc multiple [<name>] [<threads>] [<bytecount>] [<batchsize>]"),+NULL+};++constchar*bytevalue=NULL;++structoptionoptions[]={+#ifndef GIT_WINDOWS_NATIVE+OPT_STRING(0,"name",&cl_args.path,N_("name"),N_("name or pathname of unix domain socket")),+#else+OPT_STRING(0,"name",&cl_args.path,N_("name"),N_("named-pipe name")),+#endif+OPT_INTEGER(0,"threads",&cl_args.nr_threads,N_("number of threads in server thread pool")),+OPT_INTEGER(0,"max-wait",&cl_args.max_wait_sec,N_("seconds to wait for daemon to start or stop")),++OPT_INTEGER(0,"bytecount",&cl_args.bytecount,N_("number of bytes")),+OPT_INTEGER(0,"batchsize",&cl_args.batchsize,N_("number of requests per thread")),++OPT_STRING(0,"byte",&bytevalue,N_("byte"),N_("ballast character")),+OPT_STRING(0,"token",&cl_args.token,N_("token"),N_("command token to send to the server")),++OPT_END()+};++if(argc<2)+usage_with_options(simple_ipc_usage,options);++if(argc==2&&!strcmp(argv[1],"-h"))+usage_with_options(simple_ipc_usage,options);++if(argc==2&&!strcmp(argv[1],"SUPPORTS_SIMPLE_IPC"))+return0;++cl_args.subcommand=argv[1];++argc--;+argv++;++argc=parse_options(argc,argv,NULL,options,simple_ipc_usage,0);++if(cl_args.nr_threads<1)+cl_args.nr_threads=1;+if(cl_args.max_wait_sec<0)+cl_args.max_wait_sec=0;+if(cl_args.bytecount<1)+cl_args.bytecount=1;+if(cl_args.batchsize<1)+cl_args.batchsize=1;++if(bytevalue&&*bytevalue)+cl_args.bytevalue=bytevalue[0];++/*+*Use'!!'onalldispatchfunctionstomapfrom`error()`style+*(returns-1)styleto`test_must_fail`style(expects1).This+*makesshellerrormessageslessconfusing.+*/++if(!strcmp(cl_args.subcommand,"is-active"))+return!!client__probe_server();++if(!strcmp(cl_args.subcommand,"run-daemon"))+return!!daemon__run_server();++if(!strcmp(cl_args.subcommand,"start-daemon"))+return!!daemon__start_server();++/*+*Clientcommandsfollow.Ensureaserverisrunningbefore+*sendinganydata.Thismightbeoverkill,butthenagain+*thisisatestharness.+*/++if(!strcmp(cl_args.subcommand,"stop-daemon")){+if(client__probe_server())+return1;+return!!client__stop_server();+}++if(!strcmp(cl_args.subcommand,"send")){+if(client__probe_server())+return1;+return!!client__send_ipc();+}++if(!strcmp(cl_args.subcommand,"sendbytes")){+if(client__probe_server())+return1;+return!!client__sendbytes();+}++if(!strcmp(cl_args.subcommand,"multiple")){+if(client__probe_server())+return1;+return!!client__multiple();+}++die("Unhandled subcommand: '%s'",cl_args.subcommand);+}+#endif
@@ -0,0 +1,122 @@+#!/bin/sh++test_description='simple command server'++../test-lib.sh++test-toolsimple-ipcSUPPORTS_SIMPLE_IPC||{+skip_all='simple IPC not supported on this platform'+test_done+}++stop_simple_IPC_server(){+test-toolsimple-ipcstop-daemon+}++test_expect_success'start simple command server''+test_atexitstop_simple_IPC_server&&+test-toolsimple-ipcstart-daemon--threads=8&&+test-toolsimple-ipcis-active+'++test_expect_success'simple command server''+test-toolsimple-ipcsend--token=ping>actual&&+echopong>expect&&+test_cmpexpectactual+'++test_expect_success'servers cannot share the same path''+test_must_failtest-toolsimple-ipcrun-daemon&&+test-toolsimple-ipcis-active+'++test_expect_success'big response''+test-toolsimple-ipcsend--token=big>actual&&+test_line_count-ge10000actual&&+grep-q"big: [0]*9999\$"actual+'++test_expect_success'chunk response''+test-toolsimple-ipcsend--token=chunk>actual&&+test_line_count-ge10000actual&&+grep-q"big: [0]*9999\$"actual+'++test_expect_success'slow response''+test-toolsimple-ipcsend--token=slow>actual&&+test_line_count-ge100actual&&+grep-q"big: [0]*99\$"actual+'++# Send an IPC with n=100,000 bytes of ballast. This should be large enough+# to force both the kernel and the pkt-line layer to chunk the message to the+# daemon and for the daemon to receive it in chunks.+#+test_expect_success'sendbytes''+test-toolsimple-ipcsendbytes--bytecount=100000--byte=A>actual&&+grep"sent:A00100000 rcvd:A00100000"actual+'++# Start a series of <threads> client threads that each make <batchsize>+# IPC requests to the server. Each (<threads> * <batchsize>) request+# will open a new connection to the server and randomly bind to a server+# thread. Each client thread exits after completing its batch. So the+# total number of live client threads will be smaller than the total.+# Each request will send a message containing at least <bytecount> bytes+# of ballast. (Responses are small.)+#+# The purpose here is to test threading in the server and responding to+# many concurrent client requests (regardless of whether they come from+# 1 client process or many). And to test that the server side of the+# named pipe/socket is stable. (On Windows this means that the server+# pipe is properly recycled.)+#+# On Windows it also lets us adjust the connection timeout in the+# `ipc_client_send_command()`.+#+# Note it is easy to drive the system into failure by requesting an+# insane number of threads on client or server and/or increasing the+# per-thread batchsize or the per-request bytecount (ballast).+# On Windows these failures look like "pipe is busy" errors.+# So I've chosen fairly conservative values for now.+#+# We expect output of the form "sent:<letter><length> ..."+# With terms (7, 19, 13) we expect:+# <letter> in [A-G]+# <length> in [19+0 .. 19+(13-1)]+# and (7 * 13) successful responses.+#+test_expect_success'stress test threads''+test-toolsimple-ipcmultiple\+--threads=7\+--bytecount=19\+--batchsize=13\+>actual&&+test_line_count=92actual&&+grep"good 91"actual&&+grep"sent:A"<actual>actual_a&&+cat>expect_a<<-EOF&&+sent:A00000019rcvd:A00000019+sent:A00000020rcvd:A00000020+sent:A00000021rcvd:A00000021+sent:A00000022rcvd:A00000022+sent:A00000023rcvd:A00000023+sent:A00000024rcvd:A00000024+sent:A00000025rcvd:A00000025+sent:A00000026rcvd:A00000026+sent:A00000027rcvd:A00000027+sent:A00000028rcvd:A00000028+sent:A00000029rcvd:A00000029+sent:A00000030rcvd:A00000030+sent:A00000031rcvd:A00000031+EOF+test_cmpexpect_aactual_a+'++test_expect_success'stop-daemon works''+test-toolsimple-ipcstop-daemon&&+test_must_failtest-toolsimple-ipcis-active&&+test_must_failtest-toolsimple-ipcsend--token=ping+'++test_done
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-22 10:30:29
From: Jeff Hostetler <redacted>
Teach `packet_write_gently()` to write the pkt-line header and the actual
buffer in 2 separate calls to `write_in_full()` and avoid the need for a
static buffer, thread-safe scratch space, or an excessively large stack
buffer.
Change `write_packetized_from_fd()` to allocate a temporary buffer rather
than using a static buffer to avoid similar issues here.
These changes are intended to make it easier to use pkt-line routines in
a multi-threaded context with multiple concurrent writers writing to
different streams.
Signed-off-by: Jeff Hostetler <redacted>
---
pkt-line.c | 28 ++++++++++++++++++++--------
1 file changed, 20 insertions(+), 8 deletions(-)
From: Johannes Schindelin via GitGitGadget <hidden> Date: 2021-03-22 10:31:02
From: Johannes Schindelin <redacted>
Introduce PACKET_READ_GENTLE_ON_READ_ERROR option to help libify the
packet readers.
So far, the (possibly indirect) callers of `get_packet_data()` can ask
that function to return an error instead of `die()`ing upon end-of-file.
However, random read errors will still cause the process to die.
So let's introduce an explicit option to tell the packet reader
machinery to please be nice and only return an error on read errors.
This change prepares pkt-line for use by long-running daemon processes.
Such processes should be able to serve multiple concurrent clients and
and survive random IO errors. If there is an error on one connection,
a daemon should be able to drop that connection and continue serving
existing and future connections.
This ability will be used by a Git-aware "Builtin FSMonitor" feature
in a later patch series.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
pkt-line.c | 19 +++++++++++++++++--
pkt-line.h | 11 ++++++++---
2 files changed, 25 insertions(+), 5 deletions(-)
@@ -306,8 +306,11 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,*src_size-=ret;}else{ret=read_in_full(fd,dst,size);-if(ret<0)+if(ret<0){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror_errno(_("read error"));die_errno(_("read error"));+}}/* And complain if we didn't get enough bytes to satisfy the read. */
@@ -315,6 +318,8 @@ static int get_packet_data(int fd, char **src_buf, size_t *src_size,if(options&PACKET_READ_GENTLE_ON_EOF)return-1;+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("the remote end hung up unexpectedly"));die(_("the remote end hung up unexpectedly"));}
@@ -343,6 +348,9 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer,len=packet_length(linelen);if(len<0){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("protocol error: bad line length "+"character: %.4s"),linelen);die(_("protocol error: bad line length character: %.4s"),linelen);}elseif(!len){packet_trace("0000",4,0);
@@ -357,12 +365,19 @@ enum packet_read_status packet_read_with_status(int fd, char **src_buffer,*pktlen=0;returnPACKET_READ_RESPONSE_END;}elseif(len<4){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("protocol error: bad line length %d"),+len);die(_("protocol error: bad line length %d"),len);}len-=4;-if((unsigned)len>=size)+if((unsigned)len>=size){+if(options&PACKET_READ_GENTLE_ON_READ_ERROR)+returnerror(_("protocol error: bad line length %d"),+len);die(_("protocol error: bad line length %d"),len);+}if(get_packet_data(fd,src_buffer,src_len,buffer,len,options)<0){*pktlen=-1;
From: Johannes Schindelin via GitGitGadget <hidden> Date: 2021-03-22 10:31:02
From: Johannes Schindelin <redacted>
Remove the `packet_flush_gently()` call in `write_packetized_from_buf() and
`write_packetized_from_fd()` and require the caller to call it if desired.
Rename both functions to `write_packetized_from_*_no_flush()` to prevent
later merge accidents.
`write_packetized_from_buf()` currently only has one caller:
`apply_multi_file_filter()` in `convert.c`. It always wants a flush packet
to be written after writing the payload.
However, we are about to introduce a caller that wants to write many
packets before a final flush packet, so let's make the caller responsible
for emitting the flush packet.
Signed-off-by: Jeff Hostetler <redacted>
Signed-off-by: Johannes Schindelin <redacted>
---
convert.c | 8 ++++++--
pkt-line.c | 8 ++------
pkt-line.h | 4 ++--
3 files changed, 10 insertions(+), 10 deletions(-)
@@ -267,13 +267,11 @@ int write_packetized_from_fd(int fd_in, int fd_out)break;err=packet_write_gently(fd_out,buf,bytes_to_write);}-if(!err)-err=packet_flush_gently(fd_out);free(buf);returnerr;}-intwrite_packetized_from_buf(constchar*src_in,size_tlen,intfd_out)+intwrite_packetized_from_buf_no_flush(constchar*src_in,size_tlen,intfd_out){interr=0;size_tbytes_written=0;
@@ -289,8 +287,6 @@ int write_packetized_from_buf(const char *src_in, size_t len, int fd_out)err=packet_write_gently(fd_out,src_in+bytes_written,bytes_to_write);bytes_written+=bytes_to_write;}-if(!err)-err=packet_flush_gently(fd_out);returnerr;}
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-22 10:31:02
From: Jeff Hostetler <redacted>
Brief design documentation for new IPC mechanism allowing
foreground Git client to talk with an existing daemon process
at a known location using a named pipe or unix domain socket.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
Documentation/technical/api-simple-ipc.txt | 105 +++++++++++++++++++++
1 file changed, 105 insertions(+)
create mode 100644 Documentation/technical/api-simple-ipc.txt
@@ -0,0 +1,105 @@+Simple-IPC API+==============++The Simple-IPC API is a collection of `ipc_` prefixed library routines+and a basic communication protocol that allow an IPC-client process to+send an application-specific IPC-request message to an IPC-server+process and receive an application-specific IPC-response message.++Communication occurs over a named pipe on Windows and a Unix domain+socket on other platforms. IPC-clients and IPC-servers rendezvous at+a previously agreed-to application-specific pathname (which is outside+the scope of this design) that is local to the computer system.++The IPC-server routines within the server application process create a+thread pool to listen for connections and receive request messages+from multiple concurrent IPC-clients. When received, these messages+are dispatched up to the server application callbacks for handling.+IPC-server routines then incrementally relay responses back to the+IPC-client.++The IPC-client routines within a client application process connect+to the IPC-server and send a request message and wait for a response.+When received, the response is returned back the caller.++For example, the `fsmonitor--daemon` feature will be built as a server+application on top of the IPC-server library routines. It will have+threads watching for file system events and a thread pool waiting for+client connections. Clients, such as `git status` will request a list+of file system events since a point in time and the server will+respond with a list of changed files and directories. The formats of+the request and response are application-specific; the IPC-client and+IPC-server routines treat them as opaque byte streams.+++Comparison with sub-process model+---------------------------------++The Simple-IPC mechanism differs from the existing `sub-process.c`+model (Documentation/technical/long-running-process-protocol.txt) and+used by applications like Git-LFS. In the LFS-style sub-process model+the helper is started by the foreground process, communication happens+via a pair of file descriptors bound to the stdin/stdout of the+sub-process, the sub-process only serves the current foreground+process, and the sub-process exits when the foreground process+terminates.++In the Simple-IPC model the server is a very long-running service. It+can service many clients at the same time and has a private socket or+named pipe connection to each active client. It might be started+(on-demand) by the current client process or it might have been+started by a previous client or by the OS at boot time. The server+process is not associated with a terminal and it persists after+clients terminate. Clients do not have access to the stdin/stdout of+the server process and therefore must communicate over sockets or+named pipes.+++Server startup and shutdown+---------------------------++How an application server based upon IPC-server is started is also+outside the scope of the Simple-IPC design and is a property of the+application using it. For example, the server might be started or+restarted during routine maintenance operations, or it might be+started as a system service during the system boot-up sequence, or it+might be started on-demand by a foreground Git command when needed.++Similarly, server shutdown is a property of the application using+the simple-ipc routines. For example, the server might decide to+shutdown when idle or only upon explicit request.+++Simple-IPC protocol+-------------------++The Simple-IPC protocol consists of a single request message from the+client and an optional response message from the server. Both the+client and server messages are unlimited in length and are terminated+with a flush packet.++The pkt-line routines (Documentation/technical/protocol-common.txt)+are used to simplify buffer management during message generation,+transmission, and reception. A flush packet is used to mark the end+of the message. This allows the sender to incrementally generate and+transmit the message. It allows the receiver to incrementally receive+the message in chunks and to know when they have received the entire+message.++The actual byte format of the client request and server response+messages are application specific. The IPC layer transmits and+receives them as opaque byte buffers without any concern for the+content within. It is the job of the calling application layer to+understand the contents of the request and response messages.+++Summary+-------++Conceptually, the Simple-IPC protocol is similar to an HTTP REST+request. Clients connect, make an application-specific and+stateless request, receive an application-specific+response, and disconnect. It is a one round trip facility for+querying the server. The Simple-IPC routines hide the socket,+named pipe, and thread pool details and allow the application+layer to focus on the application at hand.
From: Johannes Schindelin via GitGitGadget <hidden> Date: 2021-03-22 10:31:02
From: Johannes Schindelin <redacted>
Update the calling sequence of `read_packetized_to_strbuf()` to take
an options argument and not assume a fixed set of options. Update the
only existing caller accordingly to explicitly pass the
formerly-assumed flags.
The `read_packetized_to_strbuf()` function calls `packet_read()` with
a fixed set of assumed options (`PACKET_READ_GENTLE_ON_EOF`). This
assumption has been fine for the single existing caller
`apply_multi_file_filter()` in `convert.c`.
In a later commit we would like to add other callers to
`read_packetized_to_strbuf()` that need a different set of options.
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Jeff Hostetler <redacted>
---
convert.c | 3 ++-
pkt-line.c | 4 ++--
pkt-line.h | 2 +-
3 files changed, 5 insertions(+), 4 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-22 10:31:02
From: Jeff Hostetler <redacted>
Update `unix_stream_listen()` to take an options structure to override
default behaviors. This commit includes the size of the `listen()` backlog.
Signed-off-by: Jeff Hostetler <redacted>
---
builtin/credential-cache--daemon.c | 3 ++-
unix-socket.c | 11 +++++++++--
unix-socket.h | 9 ++++++++-
3 files changed, 19 insertions(+), 4 deletions(-)
@@ -203,9 +203,10 @@ static int serve_cache_loop(int fd)staticvoidserve_cache(constchar*socket_path,intdebug){+structunix_stream_listen_optsopts=UNIX_STREAM_LISTEN_OPTS_INIT;intfd;-fd=unix_stream_listen(socket_path);+fd=unix_stream_listen(socket_path,&opts);if(fd<0)die_errno("unable to bind to '%s'",socket_path);
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-22 10:31:02
From: Jeff Hostetler <redacted>
Calls to `chdir()` are dangerous in a multi-threaded context. If
`unix_stream_listen()` or `unix_stream_connect()` is given a socket
pathname that is too long to fit in a `sockaddr_un` structure, it will
`chdir()` to the parent directory of the requested socket pathname,
create the socket using a relative pathname, and then `chdir()` back.
This is not thread-safe.
Teach `unix_sockaddr_init()` to not allow calls to `chdir()` when this
flag is set.
Signed-off-by: Jeff Hostetler <redacted>
---
builtin/credential-cache.c | 2 +-
unix-socket.c | 17 ++++++++++++-----
unix-socket.h | 3 ++-
3 files changed, 15 insertions(+), 7 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-22 10:31:03
From: Jeff Hostetler <redacted>
The static helper function `unix_stream_socket()` calls `die()`. This
is not appropriate for all callers. Eliminate the wrapper function
and make the callers propagate the error.
Signed-off-by: Jeff Hostetler <redacted>
---
unix-socket.c | 27 +++++++++++++--------------
1 file changed, 13 insertions(+), 14 deletions(-)
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-22 10:31:03
From: Jeff Hostetler <redacted>
Create a wrapper class for `unix_stream_listen()` that uses a ".lock"
lockfile to create the unix domain socket in a race-free manner.
Unix domain sockets have a fundamental problem on Unix systems because
they persist in the filesystem until they are deleted. This is
independent of whether a server is actually listening for connections.
Well-behaved servers are expected to delete the socket when they
shutdown. A new server cannot easily tell if a found socket is
attached to an active server or is leftover cruft from a dead server.
The traditional solution used by `unix_stream_listen()` is to force
delete the socket pathname and then create a new socket. This solves
the latter (cruft) problem, but in the case of the former, it orphans
the existing server (by stealing the pathname associated with the
socket it is listening on).
We cannot directly use a .lock lockfile to create the socket because
the socket is created by `bind(2)` rather than the `open(2)` mechanism
used by `tempfile.c`.
As an alternative, we hold a plain lockfile ("<path>.lock") as a
mutual exclusion device. Under the lock, we test if an existing
socket ("<path>") is has an active server. If not, we create a new
socket and begin listening. Then we use "rollback" to delete the
lockfile in all cases.
This wrapper code conceptually exists at a higher-level than the core
unix_stream_connect() and unix_stream_listen() routines that it
consumes. It is isolated in a wrapper class for clarity.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 1 +
contrib/buildsystems/CMakeLists.txt | 2 +-
unix-stream-server.c | 125 ++++++++++++++++++++++++++++
unix-stream-server.h | 33 ++++++++
4 files changed, 160 insertions(+), 1 deletion(-)
create mode 100644 unix-stream-server.c
create mode 100644 unix-stream-server.h
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-22 10:31:03
From: Jeff Hostetler <redacted>
Create t0052-simple-ipc.sh with unit tests for the "simple-ipc" mechanism.
Create t/helper/test-simple-ipc test tool to exercise the "simple-ipc"
functions.
When the tool is invoked with "run-daemon", it runs a server to listen
for "simple-ipc" connections on a test socket or named pipe and
responds to a set of commands to exercise/stress the communication
setup.
When the tool is invoked with "start-daemon", it spawns a "run-daemon"
command in the background and waits for the server to become ready
before exiting. (This helps make unit tests in t0052 more predictable
and avoids the need for arbitrary sleeps in the test script.)
The tool also has a series of client "send" commands to send commands
and data to a server instance.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 1 +
t/helper/test-simple-ipc.c | 787 +++++++++++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/t0052-simple-ipc.sh | 122 ++++++
5 files changed, 912 insertions(+)
create mode 100644 t/helper/test-simple-ipc.c
create mode 100755 t/t0052-simple-ipc.sh
@@ -0,0 +1,787 @@+/*+*test-simple-ipc.c:verifythattheInter-ProcessCommunicationworks.+*/++#include"test-tool.h"+#include"cache.h"+#include"strbuf.h"+#include"simple-ipc.h"+#include"parse-options.h"+#include"thread-utils.h"+#include"strvec.h"++#ifndef SUPPORTS_SIMPLE_IPC+intcmd__simple_ipc(intargc,constchar**argv)+{+die("simple IPC not available on this platform");+}+#else++/*+*Thetestdaemondefinesan"application callback"thatsupportsa+*seriesofcommands(see`test_app_cb()`).+*+*Unknowncommandsarecaughthereandwesendanerrormessageback+*totheclientprocess.+*/+staticintapp__unhandled_command(constchar*command,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+intret;++strbuf_addf(&buf,"unhandled command: %s",command);+ret=reply_cb(reply_data,buf.buf,buf.len);+strbuf_release(&buf);++returnret;+}++/*+*Replywithasingleverylargebuffer.Thisistoensurethat+*longresponseareproperlyhandled--whetherthechunkingoccurs+*inthekernelorinthe(probablypkt-line)layer.+*/+#define BIG_ROWS (10000)+staticintapp__big_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<BIG_ROWS;row++)+strbuf_addf(&buf,"big: %.75d\n",row);++ret=reply_cb(reply_data,buf.buf,buf.len);+strbuf_release(&buf);++returnret;+}++/*+*Replywithaseriesoflines.Thisistoensurethatwecanincrementally+*computetheresponseandchunkittotheclient.+*/+#define CHUNK_ROWS (10000)+staticintapp__chunk_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<CHUNK_ROWS;row++){+strbuf_setlen(&buf,0);+strbuf_addf(&buf,"big: %.75d\n",row);+ret=reply_cb(reply_data,buf.buf,buf.len);+}++strbuf_release(&buf);++returnret;+}++/*+*Slowlyreplywithaseriesoflines.Thisistomodelanexpensiveto+*computechunkedresponse(whichmighthappenifthiscallbackisrunning+*inathreadandisfightingforalockwithotherthreads).+*/+#define SLOW_ROWS (1000)+#define SLOW_DELAY_MS (10)+staticintapp__slow_command(ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf=STRBUF_INIT;+introw;+intret;++for(row=0;row<SLOW_ROWS;row++){+strbuf_setlen(&buf,0);+strbuf_addf(&buf,"big: %.75d\n",row);+ret=reply_cb(reply_data,buf.buf,buf.len);+sleep_millisec(SLOW_DELAY_MS);+}++strbuf_release(&buf);++returnret;+}++/*+*Theclientsentacommandfollowedbya(possiblyvery)largebuffer.+*/+staticintapp__sendbytes_command(constchar*received,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+structstrbufbuf_resp=STRBUF_INIT;+constchar*p="?";+intlen_ballast=0;+intk;+interrs=0;+intret;++if(skip_prefix(received,"sendbytes ",&p))+len_ballast=strlen(p);++/*+*Verifythattheballastisncopiesofasingleletter.+*Andthatthemulti-threadedIOlayerdidn'tcrossthestreams.+*/+for(k=1;k<len_ballast;k++)+if(p[k]!=p[0])+errs++;++if(errs)+strbuf_addf(&buf_resp,"errs:%d\n",errs);+else+strbuf_addf(&buf_resp,"rcvd:%c%08d\n",p[0],len_ballast);++ret=reply_cb(reply_data,buf_resp.buf,buf_resp.len);++strbuf_release(&buf_resp);++returnret;+}++/*+*Anarbitraryfixedaddresstoverifythattheapplicationinstance+*dataishandledproperly.+*/+staticintmy_app_data=42;++staticipc_server_application_cbtest_app_cb;++/*+*Thisisthe"application callback"thatsitsontopofthe+*"ipc-server".Itcompletelydefinesthesetofcommandssupported+*bythisapplication.+*/+staticinttest_app_cb(void*application_data,+constchar*command,+ipc_server_reply_cb*reply_cb,+structipc_server_reply_data*reply_data)+{+/*+*Verifythatwereceivedtheapplication-datathatwepassed+*whenwestartedtheipc-server.(Wehaveseverallayersof+*callbackscallingcallbacksandit'seasytogetthingsmixed+*up(especiallywhensomeare"void*").)+*/+if(application_data!=(void*)&my_app_data)+BUG("application_cb: application_data pointer wrong");++if(!strcmp(command,"quit")){+/*+*Theclientsenta"quit"command.Thisisanasync+*requestfortheservertoshutdown.+*+*WeDONOTsendtheclientaresponsemessage+*(becausewehavenothingtosayandtheother+*serverthreadshavenotyetstopped).+*+*Telltheipc-serverlayertostartshuttingdown.+*Thisincludes:stoplisteningfornewconnections+*onthesocket/pipeandtellingallworkerthreads+*tofinish/draintheiroutgoingresponsestoother+*clients.+*+*ThisDOESNOTforceanimmediatesyncshutdown.+*/+returnSIMPLE_IPC_QUIT;+}++if(!strcmp(command,"ping")){+constchar*answer="pong";+returnreply_cb(reply_data,answer,strlen(answer));+}++if(!strcmp(command,"big"))+returnapp__big_command(reply_cb,reply_data);++if(!strcmp(command,"chunk"))+returnapp__chunk_command(reply_cb,reply_data);++if(!strcmp(command,"slow"))+returnapp__slow_command(reply_cb,reply_data);++if(starts_with(command,"sendbytes "))+returnapp__sendbytes_command(command,reply_cb,reply_data);++returnapp__unhandled_command(command,reply_cb,reply_data);+}++structcl_args+{+constchar*subcommand;+constchar*path;+constchar*token;++intnr_threads;+intmax_wait_sec;+intbytecount;+intbatchsize;++charbytevalue;+};++staticstructcl_argscl_args={+.subcommand=NULL,+.path="ipc-test",+.token=NULL,++.nr_threads=5,+.max_wait_sec=60,+.bytecount=1024,+.batchsize=10,++.bytevalue='x',+};++/*+*Thisprocesswillrunasasimple-ipcserverandlistenforIPCcommands+*fromclientprocesses.+*/+staticintdaemon__run_server(void)+{+intret;++structipc_server_optsopts={+.nr_threads=cl_args.nr_threads,+};++/*+*Synchronouslyruntheipc-server.Wedon'tneedanyapplication+*instancedata,sopassanarbitrarypointer(thatwe'lllater+*verifymadetheroundtrip).+*/+ret=ipc_server_run(cl_args.path,&opts,test_app_cb,(void*)&my_app_data);+if(ret==-2)+error(_("socket/pipe already in use: '%s'"),cl_args.path);+elseif(ret==-1)+error_errno(_("could not start server on: '%s'"),cl_args.path);++returnret;+}++#ifndef GIT_WINDOWS_NATIVE+/*+*Thisisadaptedfrom`daemonize()`.Use`fork()`todirectlycreateand+*runthedaemoninachildprocess.+*/+staticintspawn_server(pid_t*pid)+{+structipc_server_optsopts={+.nr_threads=cl_args.nr_threads,+};++*pid=fork();++switch(*pid){+case0:+if(setsid()==-1)+error_errno(_("setsid failed"));+close(0);+close(1);+close(2);+sanitize_stdfds();++returnipc_server_run(cl_args.path,&opts,test_app_cb,+(void*)&my_app_data);++case-1:+returnerror_errno(_("could not spawn daemon in the background"));++default:+return0;+}+}+#else+/*+*Conceptuallylike`daemonize()`butdifferentbecauseWindowsdoesnot+*have`fork(2)`.SpawnanormalWindowschildprocessbutwithoutthe+*limitationsof`start_command()`and`finish_command()`.+*/+staticintspawn_server(pid_t*pid)+{+chartest_tool_exe[MAX_PATH];+structstrvecargs=STRVEC_INIT;+intin,out;++GetModuleFileNameA(NULL,test_tool_exe,MAX_PATH);++in=open("/dev/null",O_RDONLY);+out=open("/dev/null",O_WRONLY);++strvec_push(&args,test_tool_exe);+strvec_push(&args,"simple-ipc");+strvec_push(&args,"run-daemon");+strvec_pushf(&args,"--name=%s",cl_args.path);+strvec_pushf(&args,"--threads=%d",cl_args.nr_threads);++*pid=mingw_spawnvpe(args.v[0],args.v,NULL,NULL,in,out,out);+close(in);+close(out);++strvec_clear(&args);++if(*pid<0)+returnerror(_("could not spawn daemon in the background"));++return0;+}+#endif++/*+*Thisisadaptedfrom`wait_or_whine()`.Watchthechildprocessand+*letitgetstartedandbeginlisteningforrequestsonthesocket+*beforereportingoursuccess.+*/+staticintwait_for_server_startup(pid_tpid_child)+{+intstatus;+pid_tpid_seen;+enumipc_active_states;+time_ttime_limit,now;++time(&time_limit);+time_limit+=cl_args.max_wait_sec;++for(;;){+pid_seen=waitpid(pid_child,&status,WNOHANG);++if(pid_seen==-1)+returnerror_errno(_("waitpid failed"));++elseif(pid_seen==0){+/*+*Thechildisstillrunning(thisshouldbe+*thenormalcase).Trytoconnecttoiton+*thesocketandseeifitisreadyfor+*business.+*+*Ifthereisanotherdaemonalreadyrunning,+*ourchildwillfailtostart(possibly+*afteratimeoutonthelock),butwedon't+*care(whoresponds)ifthesocketislive.+*/+s=ipc_get_active_state(cl_args.path);+if(s==IPC_STATE__LISTENING)+return0;++time(&now);+if(now>time_limit)+returnerror(_("daemon not online yet"));++continue;+}++elseif(pid_seen==pid_child){+/*+*Thenewchilddaemonprocessshutdownwhile+*itwasstartingup,soitisnotlistening+*onthesocket.+*+*Trytopingthesocketintheoddchance+*thatanotherdaemonstarted(orwasalready+*running)whileourchildwasstarting.+*+*Again,wedon'tcarewhoservicesthesocket.+*/+s=ipc_get_active_state(cl_args.path);+if(s==IPC_STATE__LISTENING)+return0;++/*+*Wedon'tcareabouttheWEXITSTATUS()nor+*anyoftheWIF*(status)valuesbecause+*`cmd__simple_ipc()`doesthe`!!result`+*trickonallfunctionreturnvalues.+*+*Soitissufficienttojustreportthe+*earlyshutdownasanerror.+*/+returnerror(_("daemon failed to start"));+}++else+returnerror(_("waitpid is confused"));+}+}++/*+*Thisprocesswillstartasimple-ipcserverinabackgroundprocessand+*waitforittobecomeready.Thisislike`daemonize()`butgivesus+*morecontrolandbettererrorreporting(andmakesiteasiertowrite+*unittests).+*/+staticintdaemon__start_server(void)+{+pid_tpid_child;+intret;++/*+*Runtheactualdaemoninabackgroundprocess.+*/+ret=spawn_server(&pid_child);+if(pid_child<=0)+returnret;++/*+*Lettheparentwaitforthechildprocesstogetstarted+*andbeginlisteningforrequestsonthesocket.+*/+ret=wait_for_server_startup(pid_child);++returnret;+}++/*+*Thisprocesswillrunaquickprobetoseeifasimple-ipcserver+*isactiveonthispath.+*+*Returns0iftheserverisalive.+*/+staticintclient__probe_server(void)+{+enumipc_active_states;++s=ipc_get_active_state(cl_args.path);+switch(s){+caseIPC_STATE__LISTENING:+return0;++caseIPC_STATE__NOT_LISTENING:+returnerror("no server listening at '%s'",cl_args.path);++caseIPC_STATE__PATH_NOT_FOUND:+returnerror("path not found '%s'",cl_args.path);++caseIPC_STATE__INVALID_PATH:+returnerror("invalid pipe/socket name '%s'",cl_args.path);++caseIPC_STATE__OTHER_ERROR:+default:+returnerror("other error for '%s'",cl_args.path);+}+}++/*+*SendanIPCcommandtokentoanalready-runningserverdaemonand+*printtheresponse.+*+*Thisisasimple1wordcommand/tokenthat`test_app_cb()`(inthe+*daemonprocess)willunderstand.+*/+staticintclient__send_ipc(void)+{+constchar*command="(no-command)";+structstrbufbuf=STRBUF_INIT;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++if(cl_args.token&&*cl_args.token)+command=cl_args.token;++options.wait_if_busy=1;+options.wait_if_not_found=0;++if(!ipc_client_send_command(cl_args.path,&options,command,&buf)){+if(buf.len){+printf("%s\n",buf.buf);+fflush(stdout);+}+strbuf_release(&buf);++return0;+}++returnerror("failed to send '%s' to '%s'",command,cl_args.path);+}++/*+*SendanIPCcommandtoanalready-runningserverandaskitto+*shutdown."send quit"isanasyncrequestandqueuesashutdown+*eventintheserver,sowespinandwaithereforittoactually+*shutdowntomaketheunittestsalittleeasiertowrite.+*/+staticintclient__stop_server(void)+{+intret;+time_ttime_limit,now;+enumipc_active_states;++time(&time_limit);+time_limit+=cl_args.max_wait_sec;++cl_args.token="quit";++ret=client__send_ipc();+if(ret)+returnret;++for(;;){+sleep_millisec(100);++s=ipc_get_active_state(cl_args.path);++if(s!=IPC_STATE__LISTENING){+/*+*Thesocket/pipeisgoneand/orhasstopped+*responding.Letsassumethatthedaemon+*processhasexitedtoo.+*/+return0;+}++time(&now);+if(now>time_limit)+returnerror(_("daemon has not shutdown yet"));+}+}++/*+*SendanIPCcommandfollowedbyballasttoconfirmthatalarge+*messagecanbesentandthatthekernelorpkt-linelayerswill+*properlychunkitandthatthedaemonreceivestheentiremessage.+*/+staticintdo_sendbytes(intbytecount,charbyte,constchar*path,+conststructipc_client_connect_options*options)+{+structstrbufbuf_send=STRBUF_INIT;+structstrbufbuf_resp=STRBUF_INIT;++strbuf_addstr(&buf_send,"sendbytes ");+strbuf_addchars(&buf_send,byte,bytecount);++if(!ipc_client_send_command(path,options,buf_send.buf,&buf_resp)){+strbuf_rtrim(&buf_resp);+printf("sent:%c%08d %s\n",byte,bytecount,buf_resp.buf);+fflush(stdout);+strbuf_release(&buf_send);+strbuf_release(&buf_resp);++return0;+}++returnerror("client failed to sendbytes(%d, '%c') to '%s'",+bytecount,byte,path);+}++/*+*SendanIPCcommandwithballasttoanalready-runningserverdaemon.+*/+staticintclient__sendbytes(void)+{+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++options.wait_if_busy=1;+options.wait_if_not_found=0;+options.uds_disallow_chdir=0;++returndo_sendbytes(cl_args.bytecount,cl_args.bytevalue,cl_args.path,+&options);+}++structmultiple_thread_data{+pthread_tpthread_id;+structmultiple_thread_data*next;+constchar*path;+intbytecount;+intbatchsize;+intsum_errors;+intsum_good;+charletter;+};++staticvoid*multiple_thread_proc(void*_multiple_thread_data)+{+structmultiple_thread_data*d=_multiple_thread_data;+intk;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;++options.wait_if_busy=1;+options.wait_if_not_found=0;+/*+*Amulti-threadedclientshouldnotberandomlycallingchdir().+*Thetestwillpasswithoutthisrestrictionbecausethetestis+*nototherwiseaccessingthefilesystem,butitmakesushonest.+*/+options.uds_disallow_chdir=1;++trace2_thread_start("multiple");++for(k=0;k<d->batchsize;k++){+if(do_sendbytes(d->bytecount+k,d->letter,d->path,&options))+d->sum_errors++;+else+d->sum_good++;+}++trace2_thread_exit();+returnNULL;+}++/*+*Startaclient-sidethreadpool.Eachthreadsendsaseriesof+*IPCrequests.Eachrequestisonanewconnectiontotheserver.+*/+staticintclient__multiple(void)+{+structmultiple_thread_data*list=NULL;+intk;+intsum_join_errors=0;+intsum_thread_errors=0;+intsum_good=0;++for(k=0;k<cl_args.nr_threads;k++){+structmultiple_thread_data*d=xcalloc(1,sizeof(*d));+d->next=list;+d->path=cl_args.path;+d->bytecount=cl_args.bytecount+cl_args.batchsize*(k/26);+d->batchsize=cl_args.batchsize;+d->sum_errors=0;+d->sum_good=0;+d->letter='A'+(k%26);++if(pthread_create(&d->pthread_id,NULL,multiple_thread_proc,d)){+warning("failed to create thread[%d] skipping remainder",k);+free(d);+break;+}++list=d;+}++while(list){+structmultiple_thread_data*d=list;++if(pthread_join(d->pthread_id,NULL))+sum_join_errors++;++sum_thread_errors+=d->sum_errors;+sum_good+=d->sum_good;++list=d->next;+free(d);+}++printf("client (good %d) (join %d), (errors %d)\n",+sum_good,sum_join_errors,sum_thread_errors);++return(sum_join_errors+sum_thread_errors)?1:0;+}++intcmd__simple_ipc(intargc,constchar**argv)+{+constchar*constsimple_ipc_usage[]={+N_("test-helper simple-ipc is-active [<name>] [<options>]"),+N_("test-helper simple-ipc run-daemon [<name>] [<threads>]"),+N_("test-helper simple-ipc start-daemon [<name>] [<threads>] [<max-wait>]"),+N_("test-helper simple-ipc stop-daemon [<name>] [<max-wait>]"),+N_("test-helper simple-ipc send [<name>] [<token>]"),+N_("test-helper simple-ipc sendbytes [<name>] [<bytecount>] [<byte>]"),+N_("test-helper simple-ipc multiple [<name>] [<threads>] [<bytecount>] [<batchsize>]"),+NULL+};++constchar*bytevalue=NULL;++structoptionoptions[]={+#ifndef GIT_WINDOWS_NATIVE+OPT_STRING(0,"name",&cl_args.path,N_("name"),N_("name or pathname of unix domain socket")),+#else+OPT_STRING(0,"name",&cl_args.path,N_("name"),N_("named-pipe name")),+#endif+OPT_INTEGER(0,"threads",&cl_args.nr_threads,N_("number of threads in server thread pool")),+OPT_INTEGER(0,"max-wait",&cl_args.max_wait_sec,N_("seconds to wait for daemon to start or stop")),++OPT_INTEGER(0,"bytecount",&cl_args.bytecount,N_("number of bytes")),+OPT_INTEGER(0,"batchsize",&cl_args.batchsize,N_("number of requests per thread")),++OPT_STRING(0,"byte",&bytevalue,N_("byte"),N_("ballast character")),+OPT_STRING(0,"token",&cl_args.token,N_("token"),N_("command token to send to the server")),++OPT_END()+};++if(argc<2)+usage_with_options(simple_ipc_usage,options);++if(argc==2&&!strcmp(argv[1],"-h"))+usage_with_options(simple_ipc_usage,options);++if(argc==2&&!strcmp(argv[1],"SUPPORTS_SIMPLE_IPC"))+return0;++cl_args.subcommand=argv[1];++argc--;+argv++;++argc=parse_options(argc,argv,NULL,options,simple_ipc_usage,0);++if(cl_args.nr_threads<1)+cl_args.nr_threads=1;+if(cl_args.max_wait_sec<0)+cl_args.max_wait_sec=0;+if(cl_args.bytecount<1)+cl_args.bytecount=1;+if(cl_args.batchsize<1)+cl_args.batchsize=1;++if(bytevalue&&*bytevalue)+cl_args.bytevalue=bytevalue[0];++/*+*Use'!!'onalldispatchfunctionstomapfrom`error()`style+*(returns-1)styleto`test_must_fail`style(expects1).This+*makesshellerrormessageslessconfusing.+*/++if(!strcmp(cl_args.subcommand,"is-active"))+return!!client__probe_server();++if(!strcmp(cl_args.subcommand,"run-daemon"))+return!!daemon__run_server();++if(!strcmp(cl_args.subcommand,"start-daemon"))+return!!daemon__start_server();++/*+*Clientcommandsfollow.Ensureaserverisrunningbefore+*sendinganydata.Thismightbeoverkill,butthenagain+*thisisatestharness.+*/++if(!strcmp(cl_args.subcommand,"stop-daemon")){+if(client__probe_server())+return1;+return!!client__stop_server();+}++if(!strcmp(cl_args.subcommand,"send")){+if(client__probe_server())+return1;+return!!client__send_ipc();+}++if(!strcmp(cl_args.subcommand,"sendbytes")){+if(client__probe_server())+return1;+return!!client__sendbytes();+}++if(!strcmp(cl_args.subcommand,"multiple")){+if(client__probe_server())+return1;+return!!client__multiple();+}++die("Unhandled subcommand: '%s'",cl_args.subcommand);+}+#endif
@@ -0,0 +1,122 @@+#!/bin/sh++test_description='simple command server'++../test-lib.sh++test-toolsimple-ipcSUPPORTS_SIMPLE_IPC||{+skip_all='simple IPC not supported on this platform'+test_done+}++stop_simple_IPC_server(){+test-toolsimple-ipcstop-daemon+}++test_expect_success'start simple command server''+test_atexitstop_simple_IPC_server&&+test-toolsimple-ipcstart-daemon--threads=8&&+test-toolsimple-ipcis-active+'++test_expect_success'simple command server''+test-toolsimple-ipcsend--token=ping>actual&&+echopong>expect&&+test_cmpexpectactual+'++test_expect_success'servers cannot share the same path''+test_must_failtest-toolsimple-ipcrun-daemon&&+test-toolsimple-ipcis-active+'++test_expect_success'big response''+test-toolsimple-ipcsend--token=big>actual&&+test_line_count-ge10000actual&&+grep-q"big: [0]*9999\$"actual+'++test_expect_success'chunk response''+test-toolsimple-ipcsend--token=chunk>actual&&+test_line_count-ge10000actual&&+grep-q"big: [0]*9999\$"actual+'++test_expect_success'slow response''+test-toolsimple-ipcsend--token=slow>actual&&+test_line_count-ge100actual&&+grep-q"big: [0]*99\$"actual+'++# Send an IPC with n=100,000 bytes of ballast. This should be large enough+# to force both the kernel and the pkt-line layer to chunk the message to the+# daemon and for the daemon to receive it in chunks.+#+test_expect_success'sendbytes''+test-toolsimple-ipcsendbytes--bytecount=100000--byte=A>actual&&+grep"sent:A00100000 rcvd:A00100000"actual+'++# Start a series of <threads> client threads that each make <batchsize>+# IPC requests to the server. Each (<threads> * <batchsize>) request+# will open a new connection to the server and randomly bind to a server+# thread. Each client thread exits after completing its batch. So the+# total number of live client threads will be smaller than the total.+# Each request will send a message containing at least <bytecount> bytes+# of ballast. (Responses are small.)+#+# The purpose here is to test threading in the server and responding to+# many concurrent client requests (regardless of whether they come from+# 1 client process or many). And to test that the server side of the+# named pipe/socket is stable. (On Windows this means that the server+# pipe is properly recycled.)+#+# On Windows it also lets us adjust the connection timeout in the+# `ipc_client_send_command()`.+#+# Note it is easy to drive the system into failure by requesting an+# insane number of threads on client or server and/or increasing the+# per-thread batchsize or the per-request bytecount (ballast).+# On Windows these failures look like "pipe is busy" errors.+# So I've chosen fairly conservative values for now.+#+# We expect output of the form "sent:<letter><length> ..."+# With terms (7, 19, 13) we expect:+# <letter> in [A-G]+# <length> in [19+0 .. 19+(13-1)]+# and (7 * 13) successful responses.+#+test_expect_success'stress test threads''+test-toolsimple-ipcmultiple\+--threads=7\+--bytecount=19\+--batchsize=13\+>actual&&+test_line_count=92actual&&+grep"good 91"actual&&+grep"sent:A"<actual>actual_a&&+cat>expect_a<<-EOF&&+sent:A00000019rcvd:A00000019+sent:A00000020rcvd:A00000020+sent:A00000021rcvd:A00000021+sent:A00000022rcvd:A00000022+sent:A00000023rcvd:A00000023+sent:A00000024rcvd:A00000024+sent:A00000025rcvd:A00000025+sent:A00000026rcvd:A00000026+sent:A00000027rcvd:A00000027+sent:A00000028rcvd:A00000028+sent:A00000029rcvd:A00000029+sent:A00000030rcvd:A00000030+sent:A00000031rcvd:A00000031+EOF+test_cmpexpect_aactual_a+'++test_expect_success'stop-daemon works''+test-toolsimple-ipcstop-daemon&&+test_must_failtest-toolsimple-ipcis-active&&+test_must_failtest-toolsimple-ipcsend--token=ping+'++test_done
From: Jeff Hostetler via GitGitGadget <hidden> Date: 2021-03-22 10:31:03
From: Jeff Hostetler <redacted>
Create Unix domain socket based implementation of "simple-ipc".
A set of `ipc_client` routines implement a client library to connect
to an `ipc_server` over a Unix domain socket, send a simple request,
and receive a single response. Clients use blocking IO on the socket.
A set of `ipc_server` routines implement a thread pool to listen for
and concurrently service client connections.
The server creates a new Unix domain socket at a known location. If a
socket already exists with that name, the server tries to determine if
another server is already listening on the socket or if the socket is
dead. If socket is busy, the server exits with an error rather than
stealing the socket. If the socket is dead, the server creates a new
one and starts up.
If while running, the server detects that its socket has been stolen
by another server, it automatically exits.
Signed-off-by: Jeff Hostetler <redacted>
---
Makefile | 2 +
compat/simple-ipc/ipc-unix-socket.c | 999 ++++++++++++++++++++++++++++
contrib/buildsystems/CMakeLists.txt | 2 +
simple-ipc.h | 13 +-
4 files changed, 1015 insertions(+), 1 deletion(-)
create mode 100644 compat/simple-ipc/ipc-unix-socket.c
@@ -0,0 +1,999 @@+#include"cache.h"+#include"simple-ipc.h"+#include"strbuf.h"+#include"pkt-line.h"+#include"thread-utils.h"+#include"unix-socket.h"+#include"unix-stream-server.h"++#ifdef NO_UNIX_SOCKETS+#error compat/simple-ipc/ipc-unix-socket.c requires Unix sockets+#endif++enumipc_active_stateipc_get_active_state(constchar*path)+{+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+structipc_client_connect_optionsoptions+=IPC_CLIENT_CONNECT_OPTIONS_INIT;+structstatst;+structipc_client_connection*connection_test=NULL;++options.wait_if_busy=0;+options.wait_if_not_found=0;++if(lstat(path,&st)==-1){+switch(errno){+caseENOENT:+caseENOTDIR:+returnIPC_STATE__NOT_LISTENING;+default:+returnIPC_STATE__INVALID_PATH;+}+}++/* also complain if a plain file is in the way */+if((st.st_mode&S_IFMT)!=S_IFSOCK)+returnIPC_STATE__INVALID_PATH;++/*+*JustbecausethefilesystemhasaS_IFSOCKtypeinode+*at`path`,doesn'tmeanitthatthereisaserverlistening.+*Pingittobesure.+*/+state=ipc_client_try_connect(path,&options,&connection_test);+ipc_client_close_connection(connection_test);++returnstate;+}++/*+*Retryfrequencywhentryingtoconnecttoaserver.+*+*Thisvalueshouldbeshortenoughthatwedon'tseriouslydelayour+*caller,butnotfastenoughthatourspinningputspressureonthe+*system.+*/+#define WAIT_STEP_MS (50)++/*+*Trytoconnecttotheserver.Iftheserverisjuststartingupor+*isverybusy,wemaynotgetaconnectionthefirsttime.+*/+staticenumipc_active_stateconnect_to_server(+constchar*path,+inttimeout_ms,+conststructipc_client_connect_options*options,+int*pfd)+{+intk;++*pfd=-1;++for(k=0;k<timeout_ms;k+=WAIT_STEP_MS){+intfd=unix_stream_connect(path,options->uds_disallow_chdir);++if(fd!=-1){+*pfd=fd;+returnIPC_STATE__LISTENING;+}++if(errno==ENOENT){+if(!options->wait_if_not_found)+returnIPC_STATE__PATH_NOT_FOUND;++gotosleep_and_try_again;+}++if(errno==ETIMEDOUT){+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;++gotosleep_and_try_again;+}++if(errno==ECONNREFUSED){+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;++gotosleep_and_try_again;+}++returnIPC_STATE__OTHER_ERROR;++sleep_and_try_again:+sleep_millisec(WAIT_STEP_MS);+}++returnIPC_STATE__NOT_LISTENING;+}++/*+*Thetotalamountoftimethatwearewillingtowaitwhentryingto+*connecttoaserver.+*+*Whentheserverisfirststarted,itmighttakealittlewhilefor+*ittobecomereadytoservicerequests.Likewise,theservermay+*bevery(temporarily)busyandnotrespondtoourconnections.+*+*Weshouldgracefullyandsilentlyhandlethoseconditionsandtry+*againforareasonabletimeperiod.+*+*Thevaluechosenhereshouldbelongenoughfortheserver+*toreliablyhealfromtheaboveconditions.+*/+#define MY_CONNECTION_TIMEOUT_MS (1000)++enumipc_active_stateipc_client_try_connect(+constchar*path,+conststructipc_client_connect_options*options,+structipc_client_connection**p_connection)+{+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+intfd=-1;++*p_connection=NULL;++trace2_region_enter("ipc-client","try-connect",NULL);+trace2_data_string("ipc-client",NULL,"try-connect/path",path);++state=connect_to_server(path,MY_CONNECTION_TIMEOUT_MS,+options,&fd);++trace2_data_intmax("ipc-client",NULL,"try-connect/state",+(intmax_t)state);+trace2_region_leave("ipc-client","try-connect",NULL);++if(state==IPC_STATE__LISTENING){+(*p_connection)=xcalloc(1,sizeof(structipc_client_connection));+(*p_connection)->fd=fd;+}++returnstate;+}++voidipc_client_close_connection(structipc_client_connection*connection)+{+if(!connection)+return;++if(connection->fd!=-1)+close(connection->fd);++free(connection);+}++intipc_client_send_command_to_connection(+structipc_client_connection*connection,+constchar*message,structstrbuf*answer)+{+intret=0;++strbuf_setlen(answer,0);++trace2_region_enter("ipc-client","send-command",NULL);++if(write_packetized_from_buf_no_flush(message,strlen(message),+connection->fd)<0||+packet_flush_gently(connection->fd)<0){+ret=error(_("could not send IPC command"));+gotodone;+}++if(read_packetized_to_strbuf(+connection->fd,answer,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR)<0){+ret=error(_("could not read IPC response"));+gotodone;+}++done:+trace2_region_leave("ipc-client","send-command",NULL);+returnret;+}++intipc_client_send_command(constchar*path,+conststructipc_client_connect_options*options,+constchar*message,structstrbuf*answer)+{+intret=-1;+enumipc_active_statestate;+structipc_client_connection*connection=NULL;++state=ipc_client_try_connect(path,options,&connection);++if(state!=IPC_STATE__LISTENING)+returnret;++ret=ipc_client_send_command_to_connection(connection,message,answer);++ipc_client_close_connection(connection);++returnret;+}++staticintset_socket_blocking_flag(intfd,intmake_nonblocking)+{+intflags;++flags=fcntl(fd,F_GETFL,NULL);++if(flags<0)+return-1;++if(make_nonblocking)+flags|=O_NONBLOCK;+else+flags&=~O_NONBLOCK;++returnfcntl(fd,F_SETFL,flags);+}++/*+*Magicnumbersusedtoannotatecallbackinstancedata.+*Theseareusedtohelpguardagainstaccidentallypassingthe+*wronginstancedataacrossmultiplelevelsofcallbacks(which+*iseasytodoifthereare`void*`arguments).+*/+enummagic{+MAGIC_SERVER_REPLY_DATA,+MAGIC_WORKER_THREAD_DATA,+MAGIC_ACCEPT_THREAD_DATA,+MAGIC_SERVER_DATA,+};++structipc_server_reply_data{+enummagicmagic;+intfd;+structipc_worker_thread_data*worker_thread_data;+};++structipc_worker_thread_data{+enummagicmagic;+structipc_worker_thread_data*next_thread;+structipc_server_data*server_data;+pthread_tpthread_id;+};++structipc_accept_thread_data{+enummagicmagic;+structipc_server_data*server_data;++structunix_ss_socket*server_socket;++intfd_send_shutdown;+intfd_wait_shutdown;+pthread_tpthread_id;+};++/*+*Withunix-sockets,theconceptual"ipc-server"isimplementedasasingle+*controller"accept-thread"threadandapoolof"worker-thread"threads.+*Theformerdoestheusual`accept()`loopanddispatchesconnections+*toanidleworkerthread.Theworkerthreadswaitinanidleloopfor+*anewconnection,communicatewiththeclientandrelaydatato/from+*the`application_cb`andthenwaitforanotherconnectionfromthe+*serverthread.Thisavoidstheoverheadofconstantlycreatingand+*destroyingthreads.+*/+structipc_server_data{+enummagicmagic;+ipc_server_application_cb*application_cb;+void*application_data;+structstrbufbuf_path;++structipc_accept_thread_data*accept_thread;+structipc_worker_thread_data*worker_thread_list;++pthread_mutex_twork_available_mutex;+pthread_cond_twork_available_cond;++/*+*Acceptedbutnotyetprocessedclientconnectionsarekept+*inacircularbufferFIFO.Thequeueisemptywhenthe+*positionsareequal.+*/+int*fifo_fds;+intqueue_size;+intback_pos;+intfront_pos;++intshutdown_requested;+intis_stopped;+};++/*+*Removeandreturntheoldestqueuedconnection.+*+*Returns-1ifempty.+*/+staticintfifo_dequeue(structipc_server_data*server_data)+{+/* ASSERT holding mutex */++intfd;++if(server_data->back_pos==server_data->front_pos)+return-1;++fd=server_data->fifo_fds[server_data->front_pos];+server_data->fifo_fds[server_data->front_pos]=-1;++server_data->front_pos++;+if(server_data->front_pos==server_data->queue_size)+server_data->front_pos=0;++returnfd;+}++/*+*Pushanewfdontothebackofthequeue.+*+*Dropitandreturn-1ifqueueisalreadyfull.+*/+staticintfifo_enqueue(structipc_server_data*server_data,intfd)+{+/* ASSERT holding mutex */++intnext_back_pos;++next_back_pos=server_data->back_pos+1;+if(next_back_pos==server_data->queue_size)+next_back_pos=0;++if(next_back_pos==server_data->front_pos){+/* Queue is full. Just drop it. */+close(fd);+return-1;+}++server_data->fifo_fds[server_data->back_pos]=fd;+server_data->back_pos=next_back_pos;++returnfd;+}++/*+*WaitforaconnectiontobequeuedtotheFIFOandreturnit.+*+*Returns-1ifsomeonehasalreadyrequestedashutdown.+*/+staticintworker_thread__wait_for_connection(+structipc_worker_thread_data*worker_thread_data)+{+/* ASSERT NOT holding mutex */++structipc_server_data*server_data=worker_thread_data->server_data;+intfd=-1;++pthread_mutex_lock(&server_data->work_available_mutex);+for(;;){+if(server_data->shutdown_requested)+break;++fd=fifo_dequeue(server_data);+if(fd>=0)+break;++pthread_cond_wait(&server_data->work_available_cond,+&server_data->work_available_mutex);+}+pthread_mutex_unlock(&server_data->work_available_mutex);++returnfd;+}++/*+*Forwarddeclareourreplycallbackfunctionsothatanycompiler+*errorsarereportedwhenweactuallydefinethefunction(inaddition+*toanyerrorsreportedwhenwetrytopassthiscallbackfunctionas+*aparameterinafunctioncall).Theformerareeasiertounderstand.+*/+staticipc_server_reply_cbdo_io_reply_callback;++/*+*Relayapplication'sresponsemessagetotheclientprocess.+*(Wedonotflushatthispointbecauseweallowthecaller+*tochunkdatatotheclientthruus.)+*/+staticintdo_io_reply_callback(structipc_server_reply_data*reply_data,+constchar*response,size_tresponse_len)+{+if(reply_data->magic!=MAGIC_SERVER_REPLY_DATA)+BUG("reply_cb called with wrong instance data");++returnwrite_packetized_from_buf_no_flush(response,response_len,+reply_data->fd);+}++/* A randomly chosen value. */+#define MY_WAIT_POLL_TIMEOUT_MS (10)++/*+*Iftheclienthangsupwithoutsendinganydataonthewire,just+*quietlyclosethesocketandignorethisclient.+*+*ThisworkerthreadiscommittedtoreadingtheIPCrequestdata+*fromtheclientattheotherendofthisfd.Waithereforthe+*clienttoactuallyputsomethingonthewire--becauseifthe+*clientjustdoesaping(connectandhangupwithoutsendingany+*data),ouruseofthepkt-linereadroutineswillspewanerror+*message.+*+*Return-1iftheclienthungup.+*Return0ifdata(possiblyincomplete)isready.+*/+staticintworker_thread__wait_for_io_start(+structipc_worker_thread_data*worker_thread_data,+intfd)+{+structipc_server_data*server_data=worker_thread_data->server_data;+structpollfdpollfd[1];+intresult;++for(;;){+pollfd[0].fd=fd;+pollfd[0].events=POLLIN;++result=poll(pollfd,1,MY_WAIT_POLL_TIMEOUT_MS);+if(result<0){+if(errno==EINTR)+continue;+gotocleanup;+}++if(result==0){+/* a timeout */++intin_shutdown;++pthread_mutex_lock(&server_data->work_available_mutex);+in_shutdown=server_data->shutdown_requested;+pthread_mutex_unlock(&server_data->work_available_mutex);++/*+*Ifashutdownisalreadyinprogressandthis+*clienthasnotstartedtalkingyet,justdropit.+*/+if(in_shutdown)+gotocleanup;+continue;+}++if(pollfd[0].revents&POLLHUP)+gotocleanup;++if(pollfd[0].revents&POLLIN)+return0;++gotocleanup;+}++cleanup:+close(fd);+return-1;+}++/*+*Receivetherequest/commandfromtheclientandpassittothe+*registeredrequest-callback.Therequest-callbackwillcompose+*aresponseandcallourreply-callbacktosendittotheclient.+*/+staticintworker_thread__do_io(+structipc_worker_thread_data*worker_thread_data,+intfd)+{+/* ASSERT NOT holding lock */++structstrbufbuf=STRBUF_INIT;+structipc_server_reply_datareply_data;+intret=0;++reply_data.magic=MAGIC_SERVER_REPLY_DATA;+reply_data.worker_thread_data=worker_thread_data;++reply_data.fd=fd;++ret=read_packetized_to_strbuf(+reply_data.fd,&buf,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR);+if(ret>=0){+ret=worker_thread_data->server_data->application_cb(+worker_thread_data->server_data->application_data,+buf.buf,do_io_reply_callback,&reply_data);++packet_flush_gently(reply_data.fd);+}+else{+/*+*Theclientprobablydisconnected/shutdownbeforeit+*couldsendawell-formedmessage.Ignoreit.+*/+}++strbuf_release(&buf);+close(reply_data.fd);++returnret;+}++/*+*BlockSIGPIPEonthecurrentthread(sothatwegetEPIPEfrom+*write()ratherthananactualsignal).+*+*Notethatusingsigchain_push()and_pop()tocontrolSIGPIPE+*aroundourIOcallsisnotthreadsafe:+*[]Itusesaglobalstackofhandlerframes.+*[]ItusesALLOC_GROW()toresizeit.+*[]Finally,accordingtothe`signal(2)`man-page:+*"The effects of `signal()` in a multithreaded process are unspecified."+*/+staticvoidthread_block_sigpipe(sigset_t*old_set)+{+sigset_tnew_set;++sigemptyset(&new_set);+sigaddset(&new_set,SIGPIPE);++sigemptyset(old_set);+pthread_sigmask(SIG_BLOCK,&new_set,old_set);+}++/*+*ThreadprocforanIPCworkerthread.Ithandlesaseriesof+*connectionsfromclients.Itpullsthenextfdfromthequeue+*processesit,andthenwaitsforthenextclient.+*+*BlockSIGPIPEinthisworkerthreadforthelifeofthethread.+*Thisavoidsstray(andsometimesdelayed)SIGPIPEsignalscaused+*byclienterrorsand/orwhenweareunderextremelyheavyIOload.+*+*ThismeansthattheapplicationcallbackwillhaveSIGPIPEblocked.+*Thecallbackshouldnotchangeit.+*/+staticvoid*worker_thread_proc(void*_worker_thread_data)+{+structipc_worker_thread_data*worker_thread_data=_worker_thread_data;+structipc_server_data*server_data=worker_thread_data->server_data;+sigset_told_set;+intfd,io;+intret;++trace2_thread_start("ipc-worker");++thread_block_sigpipe(&old_set);++for(;;){+fd=worker_thread__wait_for_connection(worker_thread_data);+if(fd==-1)+break;/* in shutdown */++io=worker_thread__wait_for_io_start(worker_thread_data,fd);+if(io==-1)+continue;/* client hung up without sending anything */++ret=worker_thread__do_io(worker_thread_data,fd);++if(ret==SIMPLE_IPC_QUIT){+trace2_data_string("ipc-worker",NULL,"queue_stop_async",+"application_quit");+/*+*Theapplicationlayeristellingtheipc-server+*layertoshutdown.+*+*WeDONOThavearesponsetosendtotheclient.+*+*Queueanasyncstop(tostoptheotherthreads)and+*allowthisworkerthreadtoexitnow(nosensewaiting+*forthethread-poolshutdownsignal).+*+*Othernon-idleworkerthreadsareallowedtofinish+*respondingtotheircurrentclients.+*/+ipc_server_stop_async(server_data);+break;+}+}++trace2_thread_exit();+returnNULL;+}++/* A randomly chosen value. */+#define MY_ACCEPT_POLL_TIMEOUT_MS (60 * 1000)++/*+*Acceptanewclientconnectiononoursocket.Thisusesnon-blocking+*IOsothatwecanalsowaitforshutdownrequestsonoursocket-pair+*withoutactuallyspinningonafasttimeout.+*/+staticintaccept_thread__wait_for_connection(+structipc_accept_thread_data*accept_thread_data)+{+structpollfdpollfd[2];+intresult;++for(;;){+pollfd[0].fd=accept_thread_data->fd_wait_shutdown;+pollfd[0].events=POLLIN;++pollfd[1].fd=accept_thread_data->server_socket->fd_socket;+pollfd[1].events=POLLIN;++result=poll(pollfd,2,MY_ACCEPT_POLL_TIMEOUT_MS);+if(result<0){+if(errno==EINTR)+continue;+returnresult;+}++if(result==0){+/* a timeout */++/*+*Ifsomeonedeletesorforce-createsanewunix+*domainsocketatourpath,allfutureclients+*willberoutedelsewhereandwesilentlystarve.+*Ifthathappens,justqueueashutdown.+*/+if(unix_ss_was_stolen(+accept_thread_data->server_socket)){+trace2_data_string("ipc-accept",NULL,+"queue_stop_async",+"socket_stolen");+ipc_server_stop_async(+accept_thread_data->server_data);+}+continue;+}++if(pollfd[0].revents&POLLIN){+/* shutdown message queued to socketpair */+return-1;+}++if(pollfd[1].revents&POLLIN){+/* a connection is available on server_socket */++intclient_fd=+accept(accept_thread_data->server_socket->fd_socket,+NULL,NULL);+if(client_fd>=0)+returnclient_fd;++/*+*Anerrorhereisunlikely--itprobably+*indicatesthattheconnectingprocesshas+*alreadydroppedtheconnection.+*/+continue;+}++BUG("unandled poll result errno=%d r[0]=%d r[1]=%d",+errno,pollfd[0].revents,pollfd[1].revents);+}+}++/*+*ThreadprocfortheIPCserver"accept thread".Thiswaitsfor+*anincomingsocketconnection,appendsittothequeueofavailable+*connections,andnotifiesaworkerthreadtoprocessit.+*+*BlockSIGPIPEinthisthreadforthelifeofthethread.This+*avoidsanystraySIGPIPEsignalswhenclosingpipefdsunder+*extremelyheavyloads(suchaswhenthefifoqueueisfullandwe+*dropincommingconnections).+*/+staticvoid*accept_thread_proc(void*_accept_thread_data)+{+structipc_accept_thread_data*accept_thread_data=_accept_thread_data;+structipc_server_data*server_data=accept_thread_data->server_data;+sigset_told_set;++trace2_thread_start("ipc-accept");++thread_block_sigpipe(&old_set);++for(;;){+intclient_fd=accept_thread__wait_for_connection(+accept_thread_data);++pthread_mutex_lock(&server_data->work_available_mutex);+if(server_data->shutdown_requested){+pthread_mutex_unlock(&server_data->work_available_mutex);+if(client_fd>=0)+close(client_fd);+break;+}++if(client_fd<0){+/* ignore transient accept() errors */+}+else{+fifo_enqueue(server_data,client_fd);+pthread_cond_broadcast(&server_data->work_available_cond);+}+pthread_mutex_unlock(&server_data->work_available_mutex);+}++trace2_thread_exit();+returnNULL;+}++/*+*Wecan'tpredicttheconnectionarrivalraterelativetotheworker+*processingrate,thereforeweallowthe"accept-thread"toqueueup+*agenerousnumberofconnections,sincewe'dratherhavetheclient+*notunnecessarilytimeoutifwecanavoidit.(Theassumptionis+*thatthiswillbeusedforFSMonitorandafewsecondwaitona+*connectionisbetterthanhavingtheclienttimeoutanddothefull+*computationitself.)+*+*TheFIFOqueuesizeissettoamultipleoftheworkerpoolsize.+*Thisvaluechosenatrandom.+*/+#define FIFO_SCALE (100)++/*+*Thebacklogvaluefor`listen(2)`.Thisdoesn'tneedtohuge,+*ratherjustlargeenoughforour"accept-thread"towakeupand+*queueincomingconnectionsontotheFIFOwithoutthekernel+*droppingany.+*+*Thisvaluechosenatrandom.+*/+#define LISTEN_BACKLOG (50)++staticintcreate_listener_socket(+constchar*path,+conststructipc_server_opts*ipc_opts,+structunix_ss_socket**new_server_socket)+{+structunix_ss_socket*server_socket=NULL;+structunix_stream_listen_optsuslg_opts=UNIX_STREAM_LISTEN_OPTS_INIT;+intret;++uslg_opts.listen_backlog_size=LISTEN_BACKLOG;+uslg_opts.disallow_chdir=ipc_opts->uds_disallow_chdir;++ret=unix_ss_create(path,&uslg_opts,-1,&server_socket);+if(ret)+returnret;++if(set_socket_blocking_flag(server_socket->fd_socket,1)){+intsaved_errno=errno;+unix_ss_free(server_socket);+errno=saved_errno;+return-1;+}++*new_server_socket=server_socket;++trace2_data_string("ipc-server",NULL,"listen-with-lock",path);+return0;+}++staticintsetup_listener_socket(+constchar*path,+conststructipc_server_opts*ipc_opts,+structunix_ss_socket**new_server_socket)+{+intret,saved_errno;++trace2_region_enter("ipc-server","create-listener_socket",NULL);++ret=create_listener_socket(path,ipc_opts,new_server_socket);++saved_errno=errno;+trace2_region_leave("ipc-server","create-listener_socket",NULL);+errno=saved_errno;++returnret;+}++/*+*StartIPCserverinapoolofbackgroundthreads.+*/+intipc_server_run_async(structipc_server_data**returned_server_data,+constchar*path,conststructipc_server_opts*opts,+ipc_server_application_cb*application_cb,+void*application_data)+{+structunix_ss_socket*server_socket=NULL;+structipc_server_data*server_data;+intsv[2];+intk;+intret;+intnr_threads=opts->nr_threads;++*returned_server_data=NULL;++/*+*Createasocketpairandsetsv[1]tonon-blocking.This+*willusedtosendashutdownmessagetotheaccept-thread+*andallowstheaccept-threadtowaitonEITHERaclient+*connectionorashutdownrequestwithoutspinning.+*/+if(socketpair(AF_UNIX,SOCK_STREAM,0,sv)<0)+return-1;++if(set_socket_blocking_flag(sv[1],1)){+intsaved_errno=errno;+close(sv[0]);+close(sv[1]);+errno=saved_errno;+return-1;+}++ret=setup_listener_socket(path,opts,&server_socket);+if(ret){+intsaved_errno=errno;+close(sv[0]);+close(sv[1]);+errno=saved_errno;+returnret;+}++server_data=xcalloc(1,sizeof(*server_data));+server_data->magic=MAGIC_SERVER_DATA;+server_data->application_cb=application_cb;+server_data->application_data=application_data;+strbuf_init(&server_data->buf_path,0);+strbuf_addstr(&server_data->buf_path,path);++if(nr_threads<1)+nr_threads=1;++pthread_mutex_init(&server_data->work_available_mutex,NULL);+pthread_cond_init(&server_data->work_available_cond,NULL);++server_data->queue_size=nr_threads*FIFO_SCALE;+CALLOC_ARRAY(server_data->fifo_fds,server_data->queue_size);++server_data->accept_thread=+xcalloc(1,sizeof(*server_data->accept_thread));+server_data->accept_thread->magic=MAGIC_ACCEPT_THREAD_DATA;+server_data->accept_thread->server_data=server_data;+server_data->accept_thread->server_socket=server_socket;+server_data->accept_thread->fd_send_shutdown=sv[0];+server_data->accept_thread->fd_wait_shutdown=sv[1];++if(pthread_create(&server_data->accept_thread->pthread_id,NULL,+accept_thread_proc,server_data->accept_thread))+die_errno(_("could not start accept_thread '%s'"),path);++for(k=0;k<nr_threads;k++){+structipc_worker_thread_data*wtd;++wtd=xcalloc(1,sizeof(*wtd));+wtd->magic=MAGIC_WORKER_THREAD_DATA;+wtd->server_data=server_data;++if(pthread_create(&wtd->pthread_id,NULL,worker_thread_proc,+wtd)){+if(k==0)+die(_("could not start worker[0] for '%s'"),+path);+/*+*Limpalongwiththethreadpoolthatwehave.+*/+break;+}++wtd->next_thread=server_data->worker_thread_list;+server_data->worker_thread_list=wtd;+}++*returned_server_data=server_data;+return0;+}++/*+*GentlytelltheIPCservertreadstoshutdown.+*Canberunonanythread.+*/+intipc_server_stop_async(structipc_server_data*server_data)+{+/* ASSERT NOT holding mutex */++intfd;++if(!server_data)+return0;++trace2_region_enter("ipc-server","server-stop-async",NULL);++pthread_mutex_lock(&server_data->work_available_mutex);++server_data->shutdown_requested=1;++/*+*Writeabytetotheshutdownsocketpairtowakeupthe+*accept-thread.+*/+if(write(server_data->accept_thread->fd_send_shutdown,"Q",1)<0)+error_errno("could not write to fd_send_shutdown");++/*+*Drainthequeueofexistingconnections.+*/+while((fd=fifo_dequeue(server_data))!=-1)+close(fd);++/*+*Gentlytellworkerthreadstostopprocessingnewconnections+*andexit.(Thisdoesnotabortin-processconversations.)+*/+pthread_cond_broadcast(&server_data->work_available_cond);++pthread_mutex_unlock(&server_data->work_available_mutex);++trace2_region_leave("ipc-server","server-stop-async",NULL);++return0;+}++/*+*WaitforallIPCserverthreadstostop.+*/+intipc_server_await(structipc_server_data*server_data)+{+pthread_join(server_data->accept_thread->pthread_id,NULL);++if(!server_data->shutdown_requested)+BUG("ipc-server: accept-thread stopped for '%s'",+server_data->buf_path.buf);++while(server_data->worker_thread_list){+structipc_worker_thread_data*wtd=+server_data->worker_thread_list;++pthread_join(wtd->pthread_id,NULL);++server_data->worker_thread_list=wtd->next_thread;+free(wtd);+}++server_data->is_stopped=1;++return0;+}++voidipc_server_free(structipc_server_data*server_data)+{+structipc_accept_thread_data*accept_thread_data;++if(!server_data)+return;++if(!server_data->is_stopped)+BUG("cannot free ipc-server while running for '%s'",+server_data->buf_path.buf);++accept_thread_data=server_data->accept_thread;+if(accept_thread_data){+unix_ss_free(accept_thread_data->server_socket);++if(accept_thread_data->fd_send_shutdown!=-1)+close(accept_thread_data->fd_send_shutdown);+if(accept_thread_data->fd_wait_shutdown!=-1)+close(accept_thread_data->fd_wait_shutdown);++free(server_data->accept_thread);+}++while(server_data->worker_thread_list){+structipc_worker_thread_data*wtd=+server_data->worker_thread_list;++server_data->worker_thread_list=wtd->next_thread;+free(wtd);+}++pthread_cond_destroy(&server_data->work_available_cond);+pthread_mutex_destroy(&server_data->work_available_mutex);++strbuf_release(&server_data->buf_path);++free(server_data->fifo_fds);+free(server_data);+}
@@ -0,0 +1,751 @@+#include"cache.h"+#include"simple-ipc.h"+#include"strbuf.h"+#include"pkt-line.h"+#include"thread-utils.h"++#ifndef GIT_WINDOWS_NATIVE+#error This file can only be compiled on Windows+#endif++staticintinitialize_pipe_name(constchar*path,wchar_t*wpath,size_talloc)+{+intoff=0;+structstrbufrealpath=STRBUF_INIT;++if(!strbuf_realpath(&realpath,path,0))+return-1;++off=swprintf(wpath,alloc,L"\\\\.\\pipe\\");+if(xutftowcs(wpath+off,realpath.buf,alloc-off)<0)+return-1;++/* Handle drive prefix */+if(wpath[off]&&wpath[off+1]==L':'){+wpath[off+1]=L'_';+off+=2;+}++for(;wpath[off];off++)+if(wpath[off]==L'/')+wpath[off]=L'\\';++strbuf_release(&realpath);+return0;+}++staticenumipc_active_stateget_active_state(wchar_t*pipe_path)+{+if(WaitNamedPipeW(pipe_path,NMPWAIT_USE_DEFAULT_WAIT))+returnIPC_STATE__LISTENING;++if(GetLastError()==ERROR_SEM_TIMEOUT)+returnIPC_STATE__NOT_LISTENING;++if(GetLastError()==ERROR_FILE_NOT_FOUND)+returnIPC_STATE__PATH_NOT_FOUND;++returnIPC_STATE__OTHER_ERROR;+}++enumipc_active_stateipc_get_active_state(constchar*path)+{+wchar_tpipe_path[MAX_PATH];++if(initialize_pipe_name(path,pipe_path,ARRAY_SIZE(pipe_path))<0)+returnIPC_STATE__INVALID_PATH;++returnget_active_state(pipe_path);+}++#define WAIT_STEP_MS (50)++staticenumipc_active_stateconnect_to_server(+constwchar_t*wpath,+DWORDtimeout_ms,+conststructipc_client_connect_options*options,+int*pfd)+{+DWORDt_start_ms,t_waited_ms;+DWORDstep_ms;+HANDLEhPipe=INVALID_HANDLE_VALUE;+DWORDmode=PIPE_READMODE_BYTE;+DWORDgle;++*pfd=-1;++for(;;){+hPipe=CreateFileW(wpath,GENERIC_READ|GENERIC_WRITE,+0,NULL,OPEN_EXISTING,0,NULL);+if(hPipe!=INVALID_HANDLE_VALUE)+break;++gle=GetLastError();++switch(gle){+caseERROR_FILE_NOT_FOUND:+if(!options->wait_if_not_found)+returnIPC_STATE__PATH_NOT_FOUND;+if(!timeout_ms)+returnIPC_STATE__PATH_NOT_FOUND;++step_ms=(timeout_ms<WAIT_STEP_MS)?+timeout_ms:WAIT_STEP_MS;+sleep_millisec(step_ms);++timeout_ms-=step_ms;+break;/* try again */++caseERROR_PIPE_BUSY:+if(!options->wait_if_busy)+returnIPC_STATE__NOT_LISTENING;+if(!timeout_ms)+returnIPC_STATE__NOT_LISTENING;++t_start_ms=(DWORD)(getnanotime()/1000000);++if(!WaitNamedPipeW(wpath,timeout_ms)){+if(GetLastError()==ERROR_SEM_TIMEOUT)+returnIPC_STATE__NOT_LISTENING;++returnIPC_STATE__OTHER_ERROR;+}++/*+*Apipeserverinstancebecameavailable.+*Raceotherclientprocessestoconnectto+*it.+*+*Butfirstdecrementouroveralltimeoutso+*thatwedon'tstarveifwekeeplosingthe+*race.Butalsoguardagainstspecial+*NPMWAIT_values(0and-1).+*/+t_waited_ms=(DWORD)(getnanotime()/1000000)-t_start_ms;+if(t_waited_ms<timeout_ms)+timeout_ms-=t_waited_ms;+else+timeout_ms=1;+break;/* try again */++default:+returnIPC_STATE__OTHER_ERROR;+}+}++if(!SetNamedPipeHandleState(hPipe,&mode,NULL,NULL)){+CloseHandle(hPipe);+returnIPC_STATE__OTHER_ERROR;+}++*pfd=_open_osfhandle((intptr_t)hPipe,O_RDWR|O_BINARY);+if(*pfd<0){+CloseHandle(hPipe);+returnIPC_STATE__OTHER_ERROR;+}++/* fd now owns hPipe */++returnIPC_STATE__LISTENING;+}++/*+*ThedefaultconnectiontimeoutforWindowsclients.+*+*Thisisnotcurrentlypartoftheipc_API(northeconfigsettings)+*becauseofdifferencesbetweenWindowsandotherplatforms.+*+*Thisvaluewaschosenatrandom.+*/+#define WINDOWS_CONNECTION_TIMEOUT_MS (30000)++enumipc_active_stateipc_client_try_connect(+constchar*path,+conststructipc_client_connect_options*options,+structipc_client_connection**p_connection)+{+wchar_twpath[MAX_PATH];+enumipc_active_statestate=IPC_STATE__OTHER_ERROR;+intfd=-1;++*p_connection=NULL;++trace2_region_enter("ipc-client","try-connect",NULL);+trace2_data_string("ipc-client",NULL,"try-connect/path",path);++if(initialize_pipe_name(path,wpath,ARRAY_SIZE(wpath))<0)+state=IPC_STATE__INVALID_PATH;+else+state=connect_to_server(wpath,WINDOWS_CONNECTION_TIMEOUT_MS,+options,&fd);++trace2_data_intmax("ipc-client",NULL,"try-connect/state",+(intmax_t)state);+trace2_region_leave("ipc-client","try-connect",NULL);++if(state==IPC_STATE__LISTENING){+(*p_connection)=xcalloc(1,sizeof(structipc_client_connection));+(*p_connection)->fd=fd;+}++returnstate;+}++voidipc_client_close_connection(structipc_client_connection*connection)+{+if(!connection)+return;++if(connection->fd!=-1)+close(connection->fd);++free(connection);+}++intipc_client_send_command_to_connection(+structipc_client_connection*connection,+constchar*message,structstrbuf*answer)+{+intret=0;++strbuf_setlen(answer,0);++trace2_region_enter("ipc-client","send-command",NULL);++if(write_packetized_from_buf_no_flush(message,strlen(message),+connection->fd)<0||+packet_flush_gently(connection->fd)<0){+ret=error(_("could not send IPC command"));+gotodone;+}++FlushFileBuffers((HANDLE)_get_osfhandle(connection->fd));++if(read_packetized_to_strbuf(+connection->fd,answer,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR)<0){+ret=error(_("could not read IPC response"));+gotodone;+}++done:+trace2_region_leave("ipc-client","send-command",NULL);+returnret;+}++intipc_client_send_command(constchar*path,+conststructipc_client_connect_options*options,+constchar*message,structstrbuf*response)+{+intret=-1;+enumipc_active_statestate;+structipc_client_connection*connection=NULL;++state=ipc_client_try_connect(path,options,&connection);++if(state!=IPC_STATE__LISTENING)+returnret;++ret=ipc_client_send_command_to_connection(connection,message,response);++ipc_client_close_connection(connection);++returnret;+}++/*+*Duplicatethegivenpipehandleandwrapitinafiledescriptorso+*thatwecanusepkt-lineonit.+*/+staticintdup_fd_from_pipe(constHANDLEpipe)+{+HANDLEprocess=GetCurrentProcess();+HANDLEhandle;+intfd;++if(!DuplicateHandle(process,pipe,process,&handle,0,FALSE,+DUPLICATE_SAME_ACCESS)){+errno=err_win_to_posix(GetLastError());+return-1;+}++fd=_open_osfhandle((intptr_t)handle,O_RDWR|O_BINARY);+if(fd<0){+errno=err_win_to_posix(GetLastError());+CloseHandle(handle);+return-1;+}++/*+*`handle`isnowownedby`fd`andwillbeautomaticallyclosed+*whenthedescriptorisclosed.+*/++returnfd;+}++/*+*Magicnumbersusedtoannotatecallbackinstancedata.+*Theseareusedtohelpguardagainstaccidentallypassingthe+*wronginstancedataacrossmultiplelevelsofcallbacks(which+*iseasytodoifthereare`void*`arguments).+*/+enummagic{+MAGIC_SERVER_REPLY_DATA,+MAGIC_SERVER_THREAD_DATA,+MAGIC_SERVER_DATA,+};++structipc_server_reply_data{+enummagicmagic;+intfd;+structipc_server_thread_data*server_thread_data;+};++structipc_server_thread_data{+enummagicmagic;+structipc_server_thread_data*next_thread;+structipc_server_data*server_data;+pthread_tpthread_id;+HANDLEhPipe;+};++/*+*OnWindows,theconceptual"ipc-server"isimplementedasapoolof+*nidential/peer"server-thread"threads.Thatis,thereisno+*hierarchyofthreads;andthereforenocontrollerthreadmanaging+*thepool.Eachthreadhasanindependenthandletothenamedpipe,+*receivesincomingconnections,processestheclient,andre-uses+*thepipeforthenextclientconnection.+*+*Therefore,the"ipc-server"onlyneedstomaintainalistofthe+*spawnedthreadsforeventual"join"purposes.+*+*Asingle"stop-event"isvisibletoalloftheserverthreadsto+*tellthemtoshutdown(whenidle).+*/+structipc_server_data{+enummagicmagic;+ipc_server_application_cb*application_cb;+void*application_data;+structstrbufbuf_path;+wchar_twpath[MAX_PATH];++HANDLEhEventStopRequested;+structipc_server_thread_data*thread_list;+intis_stopped;+};++enumconnect_result{+CR_CONNECTED=0,+CR_CONNECT_PENDING,+CR_CONNECT_ERROR,+CR_WAIT_ERROR,+CR_SHUTDOWN,+};++staticenumconnect_resultqueue_overlapped_connect(+structipc_server_thread_data*server_thread_data,+OVERLAPPED*lpo)+{+if(ConnectNamedPipe(server_thread_data->hPipe,lpo))+gotofailed;++switch(GetLastError()){+caseERROR_IO_PENDING:+returnCR_CONNECT_PENDING;++caseERROR_PIPE_CONNECTED:+SetEvent(lpo->hEvent);+returnCR_CONNECTED;++default:+break;+}++failed:+error(_("ConnectNamedPipe failed for '%s' (%lu)"),+server_thread_data->server_data->buf_path.buf,+GetLastError());+returnCR_CONNECT_ERROR;+}++/*+*UseWindowsOverlappedIOtowaitforaconnectionorforourevent+*tobesignalled.+*/+staticenumconnect_resultwait_for_connection(+structipc_server_thread_data*server_thread_data,+OVERLAPPED*lpo)+{+enumconnect_resultr;+HANDLEwaitHandles[2];+DWORDdwWaitResult;++r=queue_overlapped_connect(server_thread_data,lpo);+if(r!=CR_CONNECT_PENDING)+returnr;++waitHandles[0]=server_thread_data->server_data->hEventStopRequested;+waitHandles[1]=lpo->hEvent;++dwWaitResult=WaitForMultipleObjects(2,waitHandles,FALSE,INFINITE);+switch(dwWaitResult){+caseWAIT_OBJECT_0+0:+returnCR_SHUTDOWN;++caseWAIT_OBJECT_0+1:+ResetEvent(lpo->hEvent);+returnCR_CONNECTED;++default:+returnCR_WAIT_ERROR;+}+}++/*+*Forwarddeclareourreplycallbackfunctionsothatanycompiler+*errorsarereportedwhenweactuallydefinethefunction(inaddition+*toanyerrorsreportedwhenwetrytopassthiscallbackfunctionas+*aparameterinafunctioncall).Theformerareeasiertounderstand.+*/+staticipc_server_reply_cbdo_io_reply_callback;++/*+*Relayapplication'sresponsemessagetotheclientprocess.+*(Wedonotflushatthispointbecauseweallowthecaller+*tochunkdatatotheclientthruus.)+*/+staticintdo_io_reply_callback(structipc_server_reply_data*reply_data,+constchar*response,size_tresponse_len)+{+if(reply_data->magic!=MAGIC_SERVER_REPLY_DATA)+BUG("reply_cb called with wrong instance data");++returnwrite_packetized_from_buf_no_flush(response,response_len,+reply_data->fd);+}++/*+*Receivetherequest/commandfromtheclientandpassittothe+*registeredrequest-callback.Therequest-callbackwillcompose+*aresponseandcallourreply-callbacktosendittotheclient.+*+*Simple-IPConlycontainsoneroundtrip,soweflushandclose+*hereaftertheresponse.+*/+staticintdo_io(structipc_server_thread_data*server_thread_data)+{+structstrbufbuf=STRBUF_INIT;+structipc_server_reply_datareply_data;+intret=0;++reply_data.magic=MAGIC_SERVER_REPLY_DATA;+reply_data.server_thread_data=server_thread_data;++reply_data.fd=dup_fd_from_pipe(server_thread_data->hPipe);+if(reply_data.fd<0)+returnerror(_("could not create fd from pipe for '%s'"),+server_thread_data->server_data->buf_path.buf);++ret=read_packetized_to_strbuf(+reply_data.fd,&buf,+PACKET_READ_GENTLE_ON_EOF|PACKET_READ_GENTLE_ON_READ_ERROR);+if(ret>=0){+ret=server_thread_data->server_data->application_cb(+server_thread_data->server_data->application_data,+buf.buf,do_io_reply_callback,&reply_data);++packet_flush_gently(reply_data.fd);++FlushFileBuffers((HANDLE)_get_osfhandle((reply_data.fd)));+}+else{+/*+*Theclientprobablydisconnected/shutdownbeforeit+*couldsendawell-formedmessage.Ignoreit.+*/+}++strbuf_release(&buf);+close(reply_data.fd);++returnret;+}++/*+*HandleIPCrequestandresponsewiththisconnectedclient.Andreset+*thepipetoprepareforthenextclient.+*/+staticintuse_connection(structipc_server_thread_data*server_thread_data)+{+intret;++ret=do_io(server_thread_data);++FlushFileBuffers(server_thread_data->hPipe);+DisconnectNamedPipe(server_thread_data->hPipe);++returnret;+}++/*+*ThreadprocforanIPCserverworkerthread.Ithandlesaseriesof+*connectionsfromclients.ItcleansandreusesthehPipebetweeneach+*client.+*/+staticvoid*server_thread_proc(void*_server_thread_data)+{+structipc_server_thread_data*server_thread_data=_server_thread_data;+HANDLEhEventConnected=INVALID_HANDLE_VALUE;+OVERLAPPEDoConnect;+enumconnect_resultcr;+intret;++assert(server_thread_data->hPipe!=INVALID_HANDLE_VALUE);++trace2_thread_start("ipc-server");+trace2_data_string("ipc-server",NULL,"pipe",+server_thread_data->server_data->buf_path.buf);++hEventConnected=CreateEventW(NULL,TRUE,FALSE,NULL);++memset(&oConnect,0,sizeof(oConnect));+oConnect.hEvent=hEventConnected;++for(;;){+cr=wait_for_connection(server_thread_data,&oConnect);++switch(cr){+caseCR_SHUTDOWN:+gotofinished;++caseCR_CONNECTED:+ret=use_connection(server_thread_data);+if(ret==SIMPLE_IPC_QUIT){+ipc_server_stop_async(+server_thread_data->server_data);+gotofinished;+}+if(ret>0){+/*+*Ignore(transient)IOerrorswiththis+*clientandresetforthenextclient.+*/+}+break;++caseCR_CONNECT_PENDING:+/* By construction, this should not happen. */+BUG("ipc-server[%s]: unexpeced CR_CONNECT_PENDING",+server_thread_data->server_data->buf_path.buf);++caseCR_CONNECT_ERROR:+caseCR_WAIT_ERROR:+/*+*Ignorethesetheoreticalerrors.+*/+DisconnectNamedPipe(server_thread_data->hPipe);+break;++default:+BUG("unandled case after wait_for_connection");+}+}++finished:+CloseHandle(server_thread_data->hPipe);+CloseHandle(hEventConnected);++trace2_thread_exit();+returnNULL;+}++staticHANDLEcreate_new_pipe(wchar_t*wpath,intis_first)+{+HANDLEhPipe;+DWORDdwOpenMode,dwPipeMode;+LPSECURITY_ATTRIBUTESlpsa=NULL;++dwOpenMode=PIPE_ACCESS_INBOUND|PIPE_ACCESS_OUTBOUND|+FILE_FLAG_OVERLAPPED;++dwPipeMode=PIPE_TYPE_MESSAGE|PIPE_READMODE_BYTE|PIPE_WAIT|+PIPE_REJECT_REMOTE_CLIENTS;++if(is_first){+dwOpenMode|=FILE_FLAG_FIRST_PIPE_INSTANCE;++/*+*OnWindows,thefirstserverpipeinstancegetsto+*settheACL/SecurityAttributesonthenamed+*pipe;subsequentinstancesinheritandcannot+*changethem.+*+*TODOShouldweallowtheapplicationlayerto+*specifysecurityattributes,suchas`LocalService`+*or`LocalSystem`,whenwecreatethenamedpipe?+*Thisquestionisprobablynotimportantwhenthe+*daemonisstartedbyaforegrounduserprocessand+*onlyneedstotalktothecurrentuser,butmaybe+*ifthedaemonisrunviatheControlPanelasa+*SystemService.+*/+}++hPipe=CreateNamedPipeW(wpath,dwOpenMode,dwPipeMode,+PIPE_UNLIMITED_INSTANCES,1024,1024,0,lpsa);++returnhPipe;+}++intipc_server_run_async(structipc_server_data**returned_server_data,+constchar*path,conststructipc_server_opts*opts,+ipc_server_application_cb*application_cb,+void*application_data)+{+structipc_server_data*server_data;+wchar_twpath[MAX_PATH];+HANDLEhPipeFirst=INVALID_HANDLE_VALUE;+intk;+intret=0;+intnr_threads=opts->nr_threads;++*returned_server_data=NULL;++ret=initialize_pipe_name(path,wpath,ARRAY_SIZE(wpath));+if(ret<0){+errno=EINVAL;+return-1;+}++hPipeFirst=create_new_pipe(wpath,1);+if(hPipeFirst==INVALID_HANDLE_VALUE){+errno=EADDRINUSE;+return-2;+}++server_data=xcalloc(1,sizeof(*server_data));+server_data->magic=MAGIC_SERVER_DATA;+server_data->application_cb=application_cb;+server_data->application_data=application_data;+server_data->hEventStopRequested=CreateEvent(NULL,TRUE,FALSE,NULL);+strbuf_init(&server_data->buf_path,0);+strbuf_addstr(&server_data->buf_path,path);+wcscpy(server_data->wpath,wpath);++if(nr_threads<1)+nr_threads=1;++for(k=0;k<nr_threads;k++){+structipc_server_thread_data*std;++std=xcalloc(1,sizeof(*std));+std->magic=MAGIC_SERVER_THREAD_DATA;+std->server_data=server_data;+std->hPipe=INVALID_HANDLE_VALUE;++std->hPipe=(k==0)+?hPipeFirst+:create_new_pipe(server_data->wpath,0);++if(std->hPipe==INVALID_HANDLE_VALUE){+/*+*Ifwe'vereachedapipeinstancelimitfor+*thispath,justusefewerthreads.+*/+free(std);+break;+}++if(pthread_create(&std->pthread_id,NULL,+server_thread_proc,std)){+/*+*Likewise,ifwe'reoutofthreads,justuse+*fewerthreadsthanrequested.+*+*However,wejustgiveupifwecan'tevenget+*onethread.Thisshouldnothappen.+*/+if(k==0)+die(_("could not start thread[0] for '%s'"),+path);++CloseHandle(std->hPipe);+free(std);+break;+}++std->next_thread=server_data->thread_list;+server_data->thread_list=std;+}++*returned_server_data=server_data;+return0;+}++intipc_server_stop_async(structipc_server_data*server_data)+{+if(!server_data)+return0;++/*+*Gentlytellalloftheipc_serverthreadstoshutdown.+*Thiswillbeseenthenexttimetheyareidle(andwaiting+*foraconnection).+*+*WeDONOTattempttoforcethemtodropanactiveconnection.+*/+SetEvent(server_data->hEventStopRequested);+return0;+}++intipc_server_await(structipc_server_data*server_data)+{+DWORDdwWaitResult;++if(!server_data)+return0;++dwWaitResult=WaitForSingleObject(server_data->hEventStopRequested,INFINITE);+if(dwWaitResult!=WAIT_OBJECT_0)+returnerror(_("wait for hEvent failed for '%s'"),+server_data->buf_path.buf);++while(server_data->thread_list){+structipc_server_thread_data*std=server_data->thread_list;++pthread_join(std->pthread_id,NULL);++server_data->thread_list=std->next_thread;+free(std);+}++server_data->is_stopped=1;++return0;+}++voidipc_server_free(structipc_server_data*server_data)+{+if(!server_data)+return;++if(!server_data->is_stopped)+BUG("cannot free ipc-server while running for '%s'",+server_data->buf_path.buf);++strbuf_release(&server_data->buf_path);++if(server_data->hEventStopRequested!=INVALID_HANDLE_VALUE)+CloseHandle(server_data->hEventStopRequested);++while(server_data->thread_list){+structipc_server_thread_data*std=server_data->thread_list;++server_data->thread_list=std->next_thread;+free(std);+}++free(server_data);+}