やりたいこと
あるプロセスが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したいが、
できないという状態を再現してみる。
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)
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のどちらを先に実行してもうまくいく。
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() を使ってみる。
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されないと意味がないようです。
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.
まだ再現できないのでもう少し低レベルに降りてみる。
を参考にWin32 APIをつかってみる。
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の説明は以下に記載されている。
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プロセスがファイルを読める。
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できなくなるようです。
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してみたが、
だめだった。
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 を取得してみる。
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プロセスで無理やり設定してみるが
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
というわけで、
最後まで読んでいただいた方には申し訳ありませんが、今のところ策なしで止まっています。