tao-shen Claude Opus 4.6 commited on
Commit
a1676f7
·
1 Parent(s): aa42ffe

feat: Python PID 1 wrapper for HF runtime log API compatibility

Browse files

HF'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>

Files changed (2) hide show
  1. Dockerfile +4 -2
  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
- ENTRYPOINT ["/scripts/entrypoint.sh"]
 
 
 
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)