Reputation: 3521
When one thread is blocked by a call to TcpListener.AcceptTcpClient() and the TcpListener is Stop()'d by a second thread, the expected behavior is a SocketException thrown from the call to AcceptTcpClient().
This seems to be affected by a call to Process.Start(StartupInfo) when the created process's input stream is redirected. This can be shown by the code below.
void Main() {
TcpListener server = new TcpListener(IPAddress.Any, 1339);
server.Start();
Stopwatch sw = new Stopwatch();
Thread t = new Thread(() => {
Thread.Sleep(1000);
String cmdExe = Environment.ExpandEnvironmentVariables(@"%SYSTEMROOT%\system32\cmd.exe");
ProcessStartInfo info = new ProcessStartInfo(cmdExe, "/Q");
// This problem does not show up when this true
info.UseShellExecute = false;
// The exception is only delayed when this is false
info.RedirectStandardInput = true;
info.RedirectStandardOutput = true;
info.RedirectStandardError = true;
Process p = Process.Start(info);
server.Stop();
//Start a timer as soon as the server is stopped
sw.Start();
});
t.Start();
try {
server.AcceptTcpClient();
}
catch (Exception) { }
// Print how long between the server stopping and the exception being thrown
sw.Stop();
sw.Elapsed.Dump();
}
When UseShellExecute is true, everything works as expected. When UseShellExecute is false, there is a delay between stopping the listener and the exception being thrown of ~25 - 30 seconds. When UseShell Execute is false and RedirectStandardInput is true, the exception seems to never be thrown until the process is terminated.
After the call to Stop(), the debugger shows that tthe listener really is stopped and the socket is no longer bound. But any incomming connection throws a different SocketExceptions saying that the "Object is not a socket".
I have solved the problem by switching to async methods which seem unaffected by all this, but I cannot wrap my head around how these two calls are connected.
Update Using the information provided by RbMm below, I have re-solved the problem by changing the inherit flags of the listening socket. The flags used to create the socket is hard coded into the framework, however the inherit flag can be changed by p/Invoking SetHandleInformation() immediately after creating the listener. Note that a new socket is created anytime Stop() is called, so this will need to be called again if the listener is to ever be restarted.
TcpListener server = new TcpListener(IPAddress.Any, 1339);
SetHandleInformation(server.Server.Handle, HANDLE_FLAGS.INHERIT, HANDLE_FLAGS.None);
server.Start();
Upvotes: 2
Views: 234
Reputation: 33794
the TcpListener.AcceptTcpClient begin I/O request on socket file object. internally IRP
allocated and associated with this file object.
the TcpListener.Stop
close the socket file handle. when the last handle for a file is closed - the IRP_MJ_CLEANUP
handler is called. DispatchCleanup routine cancel every IRPs (I/O requests) that associated with file object for which cleanup is called.
so usually exist only one file (socket) handle which you use in call AcceptTcpClient
. and when Stop
is called (before client connect) - it close this handle. if handle is single - this is last handle closed, as result all I/O request for it canceled and AcceptTcpClient
finished with error (canceled).
but if handle to this socket will be duplicated - close not last handle in Stop
not give effect, I/O will be not canceled.
how and why socket handle is duplicated ? by unknown reason all socket handles is inheritable by default. only begin from Windows 7 with SP1 added flag WSA_FLAG_NO_HANDLE_INHERIT
which let create a socket that is non-inheritable.
until you not call CreateProcess
with bInheritHandles
set to true
this not play role. but after such call - all inheritable handles (including all your socket handles) will be duplicated to child process.
the redirecting input stream implementation use inheritable named pipes for input/output/error stream. and started process with bInheritHandles
set to true
. this is have fatal effect for network code - listened socket handle is duplicated to child and Stop
close not last socket handle (else one will be in child process - cmd in your case). as result AcceptTcpClient
will be not finished.
the exception seems to never be thrown until the process is terminated.
of course. when child process is terminated - the last handle to your socket will be closed and AcceptTcpClient
is finished.
what is solution ? on c++ begin from win7 sp1 - always create sockets with WSA_FLAG_NO_HANDLE_INHERIT
. on early systems - call SetHandleInformation
for remove HANDLE_FLAG_INHERIT
.
also begin from vista, when we need start child process with some duplicated handles, instead set bInheritHandles
to true, which duplicated all inheritable handle to child process we can explicit set array of handles to be inherited by the child process by using PROC_THREAD_ATTRIBUTE_HANDLE_LIST
.
by switching to async methods which seem unaffected by all this
no. absolute does not matter synchronous or asynchronous I/O (socket handle) you use here. the I/O request anyway will be not canceled. simply when you use synchronous call - this is very visible - call not return. if you use asynchronous call and managed environment - here more problematic note this. if you use callback, which must be called when AcceptTcpClient
finished - this callback will be not called. if you associate event with this io operation - event will be not set. etc.
Upvotes: 3