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 |
| -------------------- | ------------ |
| `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_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` |
@@ -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_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_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_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) |

View File

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

View File

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

View File

@@ -141,11 +141,23 @@ except:
HAVE_FICLONE = False
try:
import ctypes
import termios
except:
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:
if os.environ.get("PRTY_NO_IFADDR"):
raise Exception()
@@ -2887,7 +2899,7 @@ def get_df(abspath: str, prune: bool) -> tuple[int, int, str]:
bfree = ctypes.c_ulonglong(0)
btotal = 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.pointer(bavail),
ctypes.pointer(btotal),
@@ -4281,8 +4293,8 @@ def termsize() -> tuple[int, int]:
def hidedir(dp) -> None:
if ANYWIN:
try:
assert ctypes # type: ignore # !rm
k32 = ctypes.WinDLL("kernel32")
assert wk32 # type: ignore # !rm
k32 = wk32
attrs = k32.GetFileAttributesW(dp)
if attrs >= 0:
k32.SetFileAttributesW(dp, attrs | 2)
@@ -4357,6 +4369,31 @@ else:
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:
if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"):
# py3.8 doesn't have .files