ファイル書き込みするプロセスを制御できない場合に、そのファイルを非同期にReadする(Python on Windows10)

# やりたいこと
あるプロセスがWrite(以後Writeプロセス)しているファイルを、
Writeプロセスがファイルを閉じる前に非同期に別のプロセス(以後Readプロセス)でReadしたい。
ただし、Writeプロセスは誰かが作ったもので、ソースコードもないし、修正もできない。

これが簡単にできなかったので、いろいろ試した記録です。

# TL;DR

* Writeプロセス次第でファイルシェアできない場合がある
 * 単に自分のスキル不足で、実は出来る方法があるのかもしれない...

# 問題設定

* 1つのファイルにWriteするプロセスとReadするプロセスがいる
* ↑のファイル名は決まっている
* Writeプロセスは、ファイルをオープンしたらデータを書き続ける。
 * ビデオストリームを書いているような感じ。
* Readプロセスは、非同期(タイマーなどで)にWriteされたデータを読み出す。
 * Writeプロセスがファイルをオープンしている間もReadする
* Writeプロセスが、ファイルをオープンするときの権限設定やモード(当然Writeは付きますが)は管理できない。
 * 要は、Writeするプロセスのソースコードは入手できない
* ファイルはReadプロセス、またはWriteプロセスがオープンしたときに初期化されてもよい

なお、問題設定ではWriteするプロセスのソースコードは入手できないことになっていますが、検証のためにWriteするプロセスも実装し、権限設定やモード変更して試しています。
また、結果的にWin32 APIを呼び出したりしているので、Python固有の事情はあまりないと思います。
Writesするプロセスのソースコードを書けるならば、PIPEが実装としては簡単な気がします。


# 問題再現

WriteプロセスがWriteしているファイルを、
Writeプロセスがファイルを閉じる前に非同期にReadプロセスでReadしたいが、
できないという状態を再現してみる。


```python:simple_read.py
import os
import time

f = open("test.bin", "rb", buffering=0)
for i in range(100):
    rdata = f.read(128)
    print(i, len(rdata), rdata[:10])
    time.sleep(1)
```

```python:simple_write.py
import os
import time
import struct

f = open("test.bin", "wb", buffering=0)
wdata = struct.pack("256B", *list(range(256)))
for i in range(100):
    print(i)
    f.write(wdata)
    time.sleep(1)
```

"b" はバイナリモードで開くの意味です。
これはsimple_read.py, simple_write.pyのどちらを先に実行してもうまくいく。

```python:simple_write.py
import os
import time
import struct

# f = open("test.bin", "wb", buffering=0)
f = open("test.bin", "xb", buffering=0)
wdata = struct.pack("256B", *list(range(256)))
for i in range(100):
    print(i)
    f.write(wdata)
    time.sleep(1)
```

"x" は、「排他的な生成に開き、ファイルが存在する場合は失敗する」という挙動になる。
これも simple_write.py を先に実行すればうまくいく。(これは想像と違った)
ちなみにsimple_read.py を先に実行すると test.bin が存在しないので失敗する。

まだ問題再現できないので、少し低レベルに降りて os.open() を使ってみる。

```python:os_write.py
import os
import time
import struct

if os.path.exists("test.bin"):
    os.remove("test.bin")

fd = os.open(
    "test.bin", os.O_BINARY | os.O_WRONLY | os.O_CREAT | os.O_NOINHERIT, mode=0o222
)
wdata = struct.pack("256B", *list(range(256)))
for i in range(100):
    print(i)
    os.write(fd, wdata)
    time.sleep(1)
```

これでもまだReadプロセスがファイルを読める。
ちなみにmodeもwrite onlyでつけてみたが、1回closeされないと意味がないようです。

https://docs.python.org/ja/3/library/os.html#os.open

https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/open-wopen?redirectedfrom=MSDN&view=msvc-160

> The pmode argument is required only when _O_CREAT is specified. If the file already exists, pmode is ignored. Otherwise, pmode specifies the file permission settings, **which are set when the new file is closed the first time**.

まだ再現できないのでもう少し低レベルに降りてみる。

https://stackoverflow.com/questions/41007045/in-python-how-to-pass-ctypes-file-descriptor-to-open-a-file

を参考にWin32 APIをつかってみる。

```python:handle_common.py
from os import path
from ctypes import *
from ctypes.wintypes import *

GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
GENERIC_RW = GENERIC_READ | GENERIC_WRITE

FILE_SHARE_DELETE = 0x00000004
FILE_SHARE_READ = 0x00000001
FILE_SHARE_WRITE = 0x00000002
FILE_SHARE_READ_WRITE = FILE_SHARE_READ | FILE_SHARE_WRITE
FILE_SHARE_DRW = FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE

CREATE_NEW = 1
CREATE_ALWAYS = 2
OPEN_EXISTING = 3

FILE_ATTRIBUTE_NORMAL = 128
FILE_ATTRIBUTE_TEMPORARY = 256
FILE_ATTRIBUTE_ARCHIVE = 32

O_RDONLY = 0x0000  # open for reading only
O_WRONLY = 0x0001  # open for writing only
O_RDWR = 0x0002  # open for reading and writing
O_ACCMODE = 0x0003  # mask for above modes
O_APPEND = 0x0008  # set append mode

INVALID_HANDLE_VALUE = -1
LPOVERLAPPED = c_void_p
LPSECURITY_ATTRIBUTES = c_void_p

DUPLICATE_CLOSE_SOURCE = 0x00000001
DUPLICATE_SAME_ACCESS = 0x00000002

NULL = 0
FALSE = BOOL(0)
TRUE = BOOL(1)


def CreateFile(filename, access, sharemode, creation, flags):
    return HANDLE(
        windll.kernel32.CreateFileW(
            LPWSTR(filename),
            DWORD(access),
            DWORD(sharemode),
            LPSECURITY_ATTRIBUTES(NULL),
            DWORD(creation),
            DWORD(flags),
            HANDLE(NULL),
        )
    )

def translate_path(fpath):
    fpath = path.abspath(fpath)
    if fpath[len(fpath) - 1] == "\\" and fpath[len(fpath) - 2] == ":":
        fpath = fpath[: len(fpath) - 1]
    return "\\??\\%s" % fpath
```

handle_common.py で使っている CreateFileW() APIの説明は以下に記載されている。

https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew

```python:handle_write.py
from os import path
from ctypes import *
from ctypes.wintypes import *
import msvcrt
import time
import struct
from handle_common import *

link_name = "test.bin"
link_name = path.abspath(link_name)

hFile = CreateFile(
    link_name,
    GENERIC_WRITE,
    FILE_SHARE_READ_WRITE,# 0,
    CREATE_ALWAYS,
    FILE_ATTRIBUTE_NORMAL,
)

if hFile == HANDLE(INVALID_HANDLE_VALUE):
    raise Exception("Failed to open directory for junction creation.")
print("handle", hex(hFile.value))

cFile = msvcrt.open_osfhandle(hFile.value, O_WRONLY)
pyFile = open(cFile, "wb")
wdata = struct.pack("256B", *list(range(256)))
for i in range(600):
    print(i)
    pyFile.write(wdata)
    pyFile.flush()
    time.sleep(1)
pyFile.close()
"""
ctypes.cdll.msvcrt._close(cFile)

windll.kernel32.CloseHandle(hFile)
"""
```

handle_writer.py で test.bin にwriteするが、これでもまだReadプロセスがファイルを読める。



```python
hFile = CreateFile(
    link_name,
    GENERIC_WRITE,
    0,
    CREATE_ALWAYS,
    FILE_ATTRIBUTE_NORMAL,
)
```

にするとPermissionErrorがでてReadプロセスでファイルを開けない。

```
Traceback (most recent call last):
  File "simple_rw\simple_read.py", line 4, in <module>
    f = open("test.bin", "rb", buffering=0)
PermissionError: [Errno 13] Permission denied: 'test.bin'
```

CreateFileW()の第3引数をゼロにすると、以下の説明のとおりほかのプロセスがdelete, read, writeできなくなるようです。

https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew

> Prevents other processes from opening a file or device if they request delete, read, or write access.

この状況が直面している問題と同じ状況なのかはわからないが、とりあえず問題再現できたので、次はこれを何とか解決できないかやってみた。


# なんとかしてReadできないか？

2つ試してみた。

## 最初にReadプロセスがファイルをopenする

うまくいかなかった。
Readプロセスがファイルをopenするときに、
Read/Write可、ファイル共有もdelete,read,writeいずれも可としてopenしてみたが、
だめだった。

```python:handle_reader.py
from os import link, path
from ctypes import *
from ctypes.wintypes import *
import msvcrt
import time
from handle_common import *


link_name = "test.bin"
link_name = path.abspath(link_name)

attr = windll.kernel32.GetFileAttributesW(link_name)
print(attr)

hFile = CreateFile(
    link_name, GENERIC_RW, FILE_SHARE_DRW, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
)

if hFile == HANDLE(INVALID_HANDLE_VALUE):
    raise Exception("Failed to open directory for junction creation.")

cFile = msvcrt.open_osfhandle(hFile.value, O_RDWR)
pyFile = open(cFile, "w+b")
for i in range(600):
    rdata = pyFile.read(1024)
    if rdata is not None:
        print(i, len(rdata), rdata[:10])
    time.sleep(1)
pyFile.close()
# ctypes.cdll.msvcrt._close(cFile)

windll.kernel32.CloseHandle(hFile)

```

Writeプロセスが handle を取得できなくて失敗する。

```
Traceback (most recent call last):
  File "handle_writer.py", line 30, in <module>
    cFile = msvcrt.open_osfhandle(hFile.value, O_WRONLY)
OSError: [Errno 9] Bad file descriptor
```


## handle を何とか取得してみる

Microsoft 純正ツールでファイル名を指定するとそれを open しているプロセスと handle を教えてくえるツール(名前もそのまま handle )があるので、これでWriteプロセスが open したファイルの handle を取得してみる。

https://docs.microsoft.com/en-us/sysinternals/downloads/handle

```
PS C:\Users\xxxx\app\Handle> .\handle64.exe "C:\Users\xxxx\Documents\Python3\async_fileio\test.bin"

Nthandle v4.22 - Handle viewer
Copyright (C) 1997-2019 Mark Russinovich
Sysinternals - www.sysinternals.com

python.exe         pid: 16740  type: File           208: C:\Users\xxx\Documents\Python3\async_fileio\test.bin
```

この "208" が handle の16進表記なのでReadプロセスで無理やり設定してみるが

```python:handle_reader.py
cFile = msvcrt.open_osfhandle(int("208", 16), O_RDWR)
```

やっぱり無理。

```
Traceback (most recent call last):
  File "handle_reader.py", line 35, in <module>
    cFile = msvcrt.open_osfhandle(int("208", 16), O_RDWR)
OSError: [Errno 9] Bad file descriptor
```


というわけで、
最後まで読んでいただいた方には申し訳ありませんが、今のところ策なしで止まっています。