1

My application streams (write) data into a named pipe, with the name of the pipe specified as an CLI argument when invoking the application. Streaming data is irregular with phases where there might not be any data to send down the pipe.

I would like to detect when the other process reading from the pipe has closed its end, in order to quickly release the resources my application has allocated for streaming. My issue is now to even detect that the reading end of the pipe has been closed without writing anything into the pipe.

As the stream data format is fixed and allows for no empty writes or pings, I cannot simply try to write some pipe data even if I have nothing to stream in order to see if the pipe reader is still reading.

I have a working solution on Linux, which unfortunately doesn't work on Windows, because Windows named pipes cannot be uniformly handled in select() like on Linux. On Linux, I simply check for the writing end of the pipe to become readable as this signals a pipe error, and then close the pipe and release my allocated resources.

On Windows, this isn't possible. I've opened the pipe for writing as such:

fifo = open('//./pipe/somepipe', 'wb')

Trying to fifo.read() from the pipe doesn't work (as is to be expected) and immediately throws an OSException.

As I said, I cannot try some empty/nul write; fifo.write(b'') does nothing, not even poking the pipe for writability at all.

Is there any way on Windows to test the writing end of a named pipe to see if the reader (client) is still connected?

TheDiveO
  • 2,183
  • 2
  • 19
  • 38
  • 1
    I assume you're opening "//./pipe/" in Windows. Anyway, your idea will work if you call WINAPI `WriteFile` directly. This will fail with `ERROR_NO_DATA` (232) if the pipe is closing. – Eryk Sun Apr 10 '19 at 10:27
  • 1
    Alternatively, if you're familiar with the NT API, you can query this directly without an empty write. Call `NtQueryInformationFile` to get the [`FilePipeLocalInformation`](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/ns-ntifs-_file_pipe_local_information). Python `open` (in particular, WINAPI `CreateFile`) always requests read-attributes access, so we're allowed to read this info on the client side even if it's an inbound pipe (i.e. the client only has write data access). `NamedPipeState` will be `FILE_PIPE_CLOSING_STATE` (4). – Eryk Sun Apr 10 '19 at 10:33
  • @eryksun: sounds very interesting, will try this; for the moment I found a workaround in that the external application protocol has an extension point which I can reuse for checking the pipe state by writing a special record, which the receiving application silently ignores. – TheDiveO Apr 10 '19 at 12:32

1 Answers1

1

As @eryksun pointed out above, it is actually possible to write a probing zero-length byte string using the Win32 API WriteFile directly: if the pipe is closed, then this will return "no success" (false/0), if the pipe is alive, then "success" (true/!=0). Exactly what I was asking for. As I find this to be simpler than using NtQueryInformationFile I'm now using the empty write method; here's a simplified example:

import ctypes
from ctypes import byref, c_ulong, c_char_p
import msvcrt
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)

fifo = open('//./pipe/somepipe', 'wb')
data = b''
written = c_ulong(0)
if not kernel32.WriteFile(
        msvcrt.get_osfhandle(fifo.fileno()),
        c_char_p(data), 0, 
        byref(written), 
        None):
    last_error = ctypes.get_last_error()
    if last_error in (
            0x000000E8,  # ERROR_NO_DATA
            # enable as required: 0x000000E9,  # ERROR_PIPE_NOT_CONNECTED
    ):
        # pipe broken
        pass
    else:
        # something else is wrong...
        pass
else:
    # pipe still okay
    pass

Helpful resources:

TheDiveO
  • 2,183
  • 2
  • 19
  • 38
  • 1
    I suggest changing this to use `kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)`. If `kernel32.WriteFile` fails, get `last_error = ctypes.get_last_error()`. If `last_error != ERROR_NO_DATA`, raise an exception via raise `ctypes.WinError(last_error)`. Otherwise you're masking unexpected errors. – Eryk Sun Apr 15 '19 at 10:41
  • 1
    Also, using `ctypes.WinDLL` isolates your package from other Python libraries. `ctypes.windll` was a bad idea from the start. It caches `WinDLL` instances, which cache function pointers. A package that uses `windll` can thus set custom `argtypes` and `restype` on commonly used API functions, which potentially breaks simultaneous use of other packages that need the same functions. This has happened previously with colorama and pyreadline, which both use the Windows console API. – Eryk Sun Apr 15 '19 at 10:47
  • Oh, many thanks! Updated my example code; I've found out from my working code that in my case I won't see the pipe _closing_ but only being _closed_ already, so checking for 0xe8 and 0xe9 (`ERROR_NO_DATA` as well as `ERROR_PIPE_NOT_CONNECTED`). Since we pipe was opened synchronously and blockingly, we know that `ERROR_PIPE_NOT_CONNECTED` means that the pipe has closed. – TheDiveO Apr 15 '19 at 13:33
  • `ERROR_PIPE_NOT_CONNECTED` means the server disconnected the pipe instance via `DisconnectNamedPipe`. A query would say the pipe state is `FILE_PIPE_DISCONNECTED_STATE`. The server may or may not have closed its end of the instance. More likely it just disconnected and will reuse the instance for the next connection. – Eryk Sun Apr 15 '19 at 13:58
  • Good to know; in my case the server won't reconnect (this is defined in the overall protocol between server and client). – TheDiveO Apr 15 '19 at 14:55