|
7 | 7 | With ``--sentinel <name>``, also touches ``<sentinel-dir>/<name>-<ts>``. |
8 | 8 | Sentinel name is sanitized to ``[a-zA-Z0-9_-]+`` to prevent path traversal. |
9 | 9 |
|
10 | | -Sentinel dir mirrors JS getSentinelDir(): ``/tmp`` on POSIX, |
11 | | -``tempfile.gettempdir()`` on Windows. |
| 10 | +Sentinel dir honors ``$TMPDIR`` when set, else ``tempfile.gettempdir()`` — the Python |
| 11 | +equivalent of the shell ``${TMPDIR:-/tmp}`` idiom callers use to poll the sentinel. |
12 | 12 |
|
13 | 13 | ``--sentinel`` and its name are parsed by a direct ``argv`` check rather than argparse's |
14 | 14 | matcher so that the legacy always-exit-0 contract is preserved — a missing name, an unknown |
|
29 | 29 | from __future__ import annotations |
30 | 30 |
|
31 | 31 | import argparse |
| 32 | +import os |
32 | 33 | import re |
33 | 34 | import sys |
34 | 35 | import tempfile |
|
39 | 40 |
|
40 | 41 |
|
41 | 42 | def _sentinel_dir() -> Path: |
42 | | - """Return ``/tmp`` on POSIX or ``tempfile.gettempdir()`` on Windows. |
| 43 | + """Return ``$TMPDIR`` when set, else ``tempfile.gettempdir()``. |
43 | 44 |
|
44 | | - Mirrors JS ``getSentinelDir()`` so sentinel paths match hook expectations |
45 | | - on all platforms while preserving the existing ``/tmp`` path on POSIX. |
| 45 | + Matches the shell ``${TMPDIR:-/tmp}`` idiom used by callers that poll this sentinel, |
| 46 | + so both sides resolve to the same directory on every platform. ``os.environ`` is read |
| 47 | + first because ``tempfile.gettempdir()`` caches its result on first call and would not |
| 48 | + observe a later ``TMPDIR`` change. |
46 | 49 | """ |
47 | | - return Path(tempfile.gettempdir()) if sys.platform == "win32" else Path("/tmp") |
| 50 | + return Path(os.environ.get("TMPDIR") or tempfile.gettempdir()) |
48 | 51 |
|
49 | 52 |
|
50 | 53 | def main(argv: list[str] | None = None) -> int: |
@@ -80,7 +83,16 @@ def main(argv: list[str] | None = None) -> int: |
80 | 83 | if len(args) >= 2 and args[0] == "--sentinel" and args[1]: |
81 | 84 | sentinel_name = _SAFE_NAME_RE.sub("", args[1]) |
82 | 85 | if sentinel_name: |
83 | | - (_sentinel_dir() / f"{sentinel_name}-{ts}").touch() |
| 86 | + sentinel_path = _sentinel_dir() / f"{sentinel_name}-{ts}" |
| 87 | + # O_NOFOLLOW refuses a pre-planted symlink in the world-writable temp dir; |
| 88 | + # getattr keeps this working on native Windows, where the flag is absent. |
| 89 | + flags = os.O_CREAT | os.O_WRONLY | getattr(os, "O_NOFOLLOW", 0) |
| 90 | + try: |
| 91 | + fd = os.open(sentinel_path, flags, 0o600) |
| 92 | + except OSError: |
| 93 | + pass # sentinel skipped — never abort the caller's always-exit-0 contract |
| 94 | + else: |
| 95 | + os.close(fd) |
84 | 96 |
|
85 | 97 | sys.stdout.write(f"{ts}\n{run_dir.as_posix()}\n") |
86 | 98 | return 0 |
|
0 commit comments