Search code examples
pythonwindowsnamed-pipespywin32python-3.7

Why does os.path.exists() stop windows named pipes from connecting?


It seems that a successful test of the existence of a windows named pipe by using os.path.exists() prevents the pipe from working. Why would this be?

Here is successfully working windows named-pipe code:

import time
import multiprocessing as mp
import win32pipe, win32file

PIPENAME = r'\\.\pipe\Foo'

def producer(pipe_name: str):
    print('producer')
#    if not os.path.exists(pipe_name):
#        print(f'No pipe {pipe_name}')
#        return
    pipe = win32file.CreateFile(pipe_name,
                                win32file.GENERIC_READ | win32file.GENERIC_WRITE,  # dwDesiredAccess
                                0,  # dwShareMode
                                None,  # lpSecurityAttributes
                                win32file.OPEN_EXISTING,  # dwCreationDisposition
                                0,  # dwFlagsAndAttributes
                                None
                                )
    win32pipe.SetNamedPipeHandleState(pipe, win32pipe.PIPE_READMODE_MESSAGE, None, None)
    win32file.WriteFile(pipe, b'foobar')


def receiver(pipe_name: str):
    print('receiver')
    pipe = win32pipe.CreateNamedPipe(pipe_name,
                                     win32pipe.PIPE_ACCESS_DUPLEX,
                                     win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT,
                                     1,  # nMaxInstances
                                     65536,  # nOutBufferSize
                                     65536,  # nInBufferSize
                                     0, # 50ms timeout (the default)
                                     None) # securityAttributes
    win32pipe.ConnectNamedPipe(pipe)
    msg = win32file.ReadFile(pipe, 65536)
    print(f'msg: {msg}')

if __name__ == '__main__':
    recv_p = mp.Process(target=receiver, args=(PIPENAME,))
    prod_p = mp.Process(target=producer, args=(PIPENAME,))
    recv_p.start()
    time.sleep(0.1)
    prod_p.start()
    prod_p.join()
    recv_p.join()

This works as expected, with the receiver printing the received message.

But if the three commented-out lines in the producer are uncommented, the os.path.exists(pipe_name) call somehow breaks the pipe so the output becomes:

receiver
producer
Process Process-2:
Process Process-1:
Traceback (most recent call last):
  File "C:\Users\redacted\AppData\Local\Programs\Python\Python37\lib\multiprocessing\process.py", line 297, in _bootstrap
    self.run()
  File "C:\Users\redacted\AppData\Local\Programs\Python\Python37\lib\multiprocessing\process.py", line 99, in run
    self._target(*self._args, **self._kwargs)
  File "C:\git\redacted\named_pipe_mqtt_test.py", line 18, in producer
    None
pywintypes.error: (231, 'CreateFile', 'All pipe instances are busy.')
Traceback (most recent call last):
  File "C:\Users\redacted\AppData\Local\Programs\Python\Python37\lib\multiprocessing\process.py", line 297, in _bootstrap
    self.run()
  File "C:\Users\redacted\AppData\Local\Programs\Python\Python37\lib\multiprocessing\process.py", line 99, in run
    self._target(*self._args, **self._kwargs)
  File "C:\git\redacted\named_pipe_mqtt_test.py", line 35, in receiver
    msg = win32file.ReadFile(pipe, 65536)
pywintypes.error: (109, 'ReadFile', 'The pipe has been ended.')

Why would os.path.exists break windows named pipes?

I've ruled out the python multiprocessing library. I've tried a delay after os.path.exists.

This is not a blocking problem for me, but I am curious.


Solution

  • os.path.exists delegates to os.stat, and on Windows, os.stat tries to open the file:

    hFile = CreateFileW(path, access, 0, NULL, OPEN_EXISTING, flags, NULL);
    

    Then it closes the file. At this point, the client can't reopen the pipe unless the server side first calls DisconnectNamedPipe, which doesn't happen in this code. Also, when the server side tries to read from the pipe, it's reading from os.stat's connection attempt, which closed the pipe without writing any data.

    I would consider this a bug. os.stat should not have such side effects.