Spaces:
Paused
Paused
feat: Python PID 1 wrapper for HF runtime log API compatibility
Browse filesHF's log API doesn't capture bash stdout/stderr from Docker containers.
Use a Python process as PID 1 that tails /var/log/huggingrun.log and
prints to stdout/stderr. All scripts write to the logfile, Python
PID 1 echoes them for HF to capture.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Dockerfile +4 -2
- scripts/entrypoint_wrapper.py +84 -0
Dockerfile
CHANGED
|
@@ -49,7 +49,7 @@ COPY ubuntu-server/ws-ssh-bridge.py /opt/ws-ssh-bridge.py
|
|
| 49 |
COPY ubuntu-server/git_sync_daemon.py /opt/git_sync_daemon.py
|
| 50 |
COPY ubuntu-server/start-server.sh /opt/start-server.sh
|
| 51 |
COPY scripts /scripts
|
| 52 |
-
RUN chmod +x /scripts/entrypoint.sh /opt/start-server.sh
|
| 53 |
|
| 54 |
ENV PERSIST_PATH=/data
|
| 55 |
ENV PYTHONUNBUFFERED=1
|
|
@@ -58,4 +58,6 @@ ENV SSH_PORT=2222
|
|
| 58 |
|
| 59 |
# Run as root (needed for: apt install persistence, bind mounts, sshd)
|
| 60 |
EXPOSE 7860
|
| 61 |
-
|
|
|
|
|
|
|
|
|
| 49 |
COPY ubuntu-server/git_sync_daemon.py /opt/git_sync_daemon.py
|
| 50 |
COPY ubuntu-server/start-server.sh /opt/start-server.sh
|
| 51 |
COPY scripts /scripts
|
| 52 |
+
RUN chmod +x /scripts/entrypoint.sh /scripts/entrypoint_wrapper.py /opt/start-server.sh
|
| 53 |
|
| 54 |
ENV PERSIST_PATH=/data
|
| 55 |
ENV PYTHONUNBUFFERED=1
|
|
|
|
| 58 |
|
| 59 |
# Run as root (needed for: apt install persistence, bind mounts, sshd)
|
| 60 |
EXPOSE 7860
|
| 61 |
+
# Python PID 1: HF log API captures Python stdout/stderr reliably
|
| 62 |
+
# The wrapper tails /var/log/huggingrun.log → stdout/stderr for HF to capture
|
| 63 |
+
ENTRYPOINT ["python3", "/scripts/entrypoint_wrapper.py"]
|
scripts/entrypoint_wrapper.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Python PID 1 wrapper for HF Spaces log compatibility.
|
| 4 |
+
HF's runtime log API captures Python stdout/stderr better than bash.
|
| 5 |
+
This wrapper stays as PID 1, runs entrypoint.sh as a child,
|
| 6 |
+
and mirrors all log output to stdout/stderr.
|
| 7 |
+
"""
|
| 8 |
+
import subprocess
|
| 9 |
+
import sys
|
| 10 |
+
import os
|
| 11 |
+
import signal
|
| 12 |
+
import time
|
| 13 |
+
import threading
|
| 14 |
+
|
| 15 |
+
LOGFILE = "/var/log/huggingrun.log"
|
| 16 |
+
|
| 17 |
+
def log(msg):
|
| 18 |
+
"""Write to stdout, stderr, and logfile simultaneously."""
|
| 19 |
+
line = f"{msg}\n"
|
| 20 |
+
sys.stdout.write(line)
|
| 21 |
+
sys.stdout.flush()
|
| 22 |
+
sys.stderr.write(line)
|
| 23 |
+
sys.stderr.flush()
|
| 24 |
+
try:
|
| 25 |
+
with open(LOGFILE, "a") as f:
|
| 26 |
+
f.write(line)
|
| 27 |
+
except Exception:
|
| 28 |
+
pass
|
| 29 |
+
|
| 30 |
+
def tail_logfile():
|
| 31 |
+
"""Background thread: tail the logfile and print new lines to stdout/stderr."""
|
| 32 |
+
# Wait for logfile to exist
|
| 33 |
+
while not os.path.exists(LOGFILE):
|
| 34 |
+
time.sleep(0.5)
|
| 35 |
+
|
| 36 |
+
with open(LOGFILE, "r") as f:
|
| 37 |
+
# Skip to current end
|
| 38 |
+
f.seek(0, 2)
|
| 39 |
+
while True:
|
| 40 |
+
line = f.readline()
|
| 41 |
+
if line:
|
| 42 |
+
line = line.rstrip("\n")
|
| 43 |
+
sys.stdout.write(f"{line}\n")
|
| 44 |
+
sys.stdout.flush()
|
| 45 |
+
sys.stderr.write(f"{line}\n")
|
| 46 |
+
sys.stderr.flush()
|
| 47 |
+
else:
|
| 48 |
+
time.sleep(0.3)
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
log("========================================")
|
| 52 |
+
log("[wrapper] HuggingRun Python PID 1 wrapper")
|
| 53 |
+
log(f"[wrapper] PID={os.getpid()}, Date={time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}")
|
| 54 |
+
log(f"[wrapper] Python {sys.version.split()[0]}")
|
| 55 |
+
log("========================================")
|
| 56 |
+
|
| 57 |
+
# Start background tailer: reads logfile and echos to stdout/stderr
|
| 58 |
+
# This ensures all log output (from bash scripts, python daemons) gets
|
| 59 |
+
# printed through PID 1's stdout/stderr for HF to capture
|
| 60 |
+
tailer = threading.Thread(target=tail_logfile, daemon=True)
|
| 61 |
+
tailer.start()
|
| 62 |
+
|
| 63 |
+
# Run entrypoint.sh as child process
|
| 64 |
+
proc = subprocess.Popen(
|
| 65 |
+
["/bin/bash", "/scripts/entrypoint.sh"],
|
| 66 |
+
stdout=subprocess.DEVNULL, # entrypoint writes to logfile
|
| 67 |
+
stderr=subprocess.DEVNULL, # entrypoint writes to logfile
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Forward signals to child
|
| 71 |
+
def forward_signal(sig, frame):
|
| 72 |
+
log(f"[wrapper] Signal {sig} received, forwarding to child PID={proc.pid}")
|
| 73 |
+
try:
|
| 74 |
+
proc.send_signal(sig)
|
| 75 |
+
except ProcessLookupError:
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
signal.signal(signal.SIGTERM, forward_signal)
|
| 79 |
+
signal.signal(signal.SIGINT, forward_signal)
|
| 80 |
+
|
| 81 |
+
# Wait for child to exit
|
| 82 |
+
rc = proc.wait()
|
| 83 |
+
log(f"[wrapper] Child exited with code {rc}")
|
| 84 |
+
sys.exit(rc)
|