tail/follow: add windows support; closes #1262

This commit is contained in:
ed 2026-02-06 18:57:00 +00:00
parent 4cb4e820f6
commit a368fc66b3
4 changed files with 62 additions and 11 deletions

View file

@ -2424,6 +2424,7 @@ buggy feature? rip it out by setting any of the following environment variables
| env-var | what it does | | env-var | what it does |
| -------------------- | ------------ | | -------------------- | ------------ |
| `PRTY_NO_CTYPES` | do not use features from external libraries such as kernel32 |
| `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access | | `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access |
| `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes | | `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes |
| `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` | | `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` |
@ -3088,6 +3089,7 @@ set any of the following environment variables to disable its associated optiona
| `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails | | `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails |
| `PRTY_NO_PIL_AVIF` | disable Pillow avif support (internal and/or [plugin](https://pypi.org/project/pillow-avif-plugin/)) | | `PRTY_NO_PIL_AVIF` | disable Pillow avif support (internal and/or [plugin](https://pypi.org/project/pillow-avif-plugin/)) |
| `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pillow-heif/) | | `PRTY_NO_PIL_HEIF` | disable 3rd-party Pillow plugin for [HEIF support](https://pypi.org/project/pillow-heif/) |
| `PRTY_NO_PIL_JXL` | disable 3rd-party Pillow plugin for [JXL support](https://pypi.org/project/pillow-jxl-plugin/) |
| `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow | | `PRTY_NO_PIL_WEBP` | disable use of native webp support in Pillow |
| `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows | | `PRTY_NO_PSUTIL` | do not use [psutil](https://pypi.org/project/psutil/) for reaping stuck hooks and plugins on Windows |
| `PRTY_NO_PYFTPD` | disable ftp(s) server ([pyftpdlib](https://pypi.org/project/pyftpdlib/)-based) | | `PRTY_NO_PYFTPD` | disable ftp(s) server ([pyftpdlib](https://pypi.org/project/pyftpdlib/)-based) |

View file

@ -63,6 +63,7 @@ from .util import (
Daemon, Daemon,
align_tab, align_tab,
b64enc, b64enc,
ctypes,
dedent, dedent,
has_resource, has_resource,
load_resource, load_resource,
@ -70,6 +71,7 @@ from .util import (
pybin, pybin,
read_utf8, read_utf8,
termsize, termsize,
wk32,
wrap, wrap,
) )
@ -504,8 +506,10 @@ def sighandler(sig: Optional[int] = None, frame: Optional[FrameType] = None) ->
def disable_quickedit() -> None: def disable_quickedit() -> None:
if not ctypes:
raise Exception("no ctypes")
import atexit import atexit
import ctypes
from ctypes import wintypes from ctypes import wintypes
def ecb(ok: bool, fun: Any, args: list[Any]) -> list[Any]: def ecb(ok: bool, fun: Any, args: list[Any]) -> list[Any]:
@ -515,10 +519,10 @@ def disable_quickedit() -> None:
raise ctypes.WinError(err) # type: ignore raise ctypes.WinError(err) # type: ignore
return args return args
k32 = ctypes.WinDLL(str("kernel32"), use_last_error=True) # type: ignore
if PY2: if PY2:
wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD) wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
k32 = wk32
k32.GetStdHandle.errcheck = ecb # type: ignore k32.GetStdHandle.errcheck = ecb # type: ignore
k32.GetConsoleMode.errcheck = ecb # type: ignore k32.GetConsoleMode.errcheck = ecb # type: ignore
k32.SetConsoleMode.errcheck = ecb # type: ignore k32.SetConsoleMode.errcheck = ecb # type: ignore

View file

@ -90,6 +90,7 @@ from .util import (
loadpy, loadpy,
log_reloc, log_reloc,
min_ex, min_ex,
open_nolock,
pathmod, pathmod,
quotep, quotep,
rand_name, rand_name,
@ -4833,7 +4834,7 @@ class HttpCli(object):
f = None f = None
try: try:
st = os.stat(abspath) st = os.stat(abspath)
f = open(*open_args) f = open_nolock(*open_args)
f.seek(0, os.SEEK_END) f.seek(0, os.SEEK_END)
eof = f.tell() eof = f.tell()
f.seek(0) f.seek(0)
@ -4867,6 +4868,7 @@ class HttpCli(object):
pass pass
gone = 0 gone = 0
unsent = False
t_fd = t_ka = time.time() t_fd = t_ka = time.time()
while True: while True:
assert f # !rm assert f # !rm
@ -4883,6 +4885,7 @@ class HttpCli(object):
t_fd = t_ka = now t_fd = t_ka = now
self.s.sendall(buf) self.s.sendall(buf)
sent += len(buf) sent += len(buf)
unsent = False
dls[dl_id] = (time.time(), sent) dls[dl_id] = (time.time(), sent)
continue continue
@ -4893,14 +4896,16 @@ class HttpCli(object):
if t_fd < now - sec_fd: if t_fd < now - sec_fd:
try: try:
st2 = os.stat(open_args[0]) st2 = os.stat(open_args[0])
szd = st2.st_size - st.st_size
if ( if (
st2.st_ino != st.st_ino st2.st_ino != st.st_ino
or st2.st_size < sent or st2.st_size < sent
or st2.st_size < st.st_size or szd < 0
or unsent
): ):
assert f # !rm assert f # !rm
# open new file before closing previous to avoid toctous (open may fail; cannot null f before) # open new file before closing previous to avoid toctous (open may fail; cannot null f before)
f2 = open(*open_args) f2 = open_nolock(*open_args)
f.close() f.close()
f = f2 f = f2
f.seek(0, os.SEEK_END) f.seek(0, os.SEEK_END)
@ -4915,7 +4920,10 @@ class HttpCli(object):
ofs = sent # just new fd? resume from same ofs ofs = sent # just new fd? resume from same ofs
f.seek(ofs) f.seek(ofs)
self.log("reopened at byte %d: %r" % (ofs, abspath), 6) self.log("reopened at byte %d: %r" % (ofs, abspath), 6)
unsent = False
gone = 0 gone = 0
elif szd:
unsent = True
st = st2 st = st2
except: except:
gone += 1 gone += 1
@ -5029,7 +5037,7 @@ class HttpCli(object):
self.log("moved to tier %d (%s)" % (tier, tiers[tier])) self.log("moved to tier %d (%s)" % (tier, tiers[tier]))
try: try:
with open(ap_data, "rb", self.args.iobuf) as f: with open_nolock(ap_data, "rb", self.args.iobuf) as f:
f.seek(lower) f.seek(lower)
page = f.read(min(winsz, data_end - lower, upper - lower)) page = f.read(min(winsz, data_end - lower, upper - lower))
if not page: if not page:
@ -5062,7 +5070,7 @@ class HttpCli(object):
break break
if lower < upper and not broken: if lower < upper and not broken:
with open(req_path, "rb") as f: with open_nolock(req_path, "rb") as f:
remains = sendfile_py( remains = sendfile_py(
self.log, self.log,
lower, lower,

View file

@ -141,11 +141,23 @@ except:
HAVE_FICLONE = False HAVE_FICLONE = False
try: try:
import ctypes
import termios import termios
except: except:
pass pass
try:
if os.environ.get("PRTY_NO_CTYPES"):
raise Exception()
import ctypes
except:
ctypes = None
try:
wk32 = ctypes.WinDLL(str("kernel32"), use_last_error=True) # type: ignore
except:
wk32 = None
try: try:
if os.environ.get("PRTY_NO_IFADDR"): if os.environ.get("PRTY_NO_IFADDR"):
raise Exception() raise Exception()
@ -2887,7 +2899,7 @@ def get_df(abspath: str, prune: bool) -> tuple[int, int, str]:
bfree = ctypes.c_ulonglong(0) bfree = ctypes.c_ulonglong(0)
btotal = ctypes.c_ulonglong(0) btotal = ctypes.c_ulonglong(0)
bavail = ctypes.c_ulonglong(0) bavail = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW( # type: ignore wk32.GetDiskFreeSpaceExW( # type: ignore
ctypes.c_wchar_p(abspath), ctypes.c_wchar_p(abspath),
ctypes.pointer(bavail), ctypes.pointer(bavail),
ctypes.pointer(btotal), ctypes.pointer(btotal),
@ -4281,8 +4293,8 @@ def termsize() -> tuple[int, int]:
def hidedir(dp) -> None: def hidedir(dp) -> None:
if ANYWIN: if ANYWIN:
try: try:
assert ctypes # type: ignore # !rm assert wk32 # type: ignore # !rm
k32 = ctypes.WinDLL("kernel32") k32 = wk32
attrs = k32.GetFileAttributesW(dp) attrs = k32.GetFileAttributesW(dp)
if attrs >= 0: if attrs >= 0:
k32.SetFileAttributesW(dp, attrs | 2) k32.SetFileAttributesW(dp, attrs | 2)
@ -4357,6 +4369,31 @@ else:
lock_file = _lock_file_noop lock_file = _lock_file_noop
def _open_nolock_windows(bap: Union[str, bytes], *a, **ka) -> typing.BinaryIO:
assert ctypes # !rm
assert wk32 # !rm
import msvcrt
try:
ap = bap.decode("utf-8", "replace") # type: ignore
except:
ap = bap
fh = wk32.CreateFileW(ap, 0x80000000, 7, None, 3, 0x80, None)
# `-ap, GENERIC_READ, FILE_SHARE_READ|WRITE|DELETE, None, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, None
if fh == -1:
ec = ctypes.get_last_error() # type: ignore
raise ctypes.WinError(ec) # type: ignore
fd = msvcrt.open_osfhandle(fh, os.O_RDONLY) # type: ignore
return os.fdopen(fd, "rb")
if ANYWIN:
open_nolock = _open_nolock_windows
else:
open_nolock = open
try: try:
if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"): if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"):
# py3.8 doesn't have .files # py3.8 doesn't have .files