-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsession_logs.py
More file actions
120 lines (103 loc) · 4.44 KB
/
Copy pathsession_logs.py
File metadata and controls
120 lines (103 loc) · 4.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""Fetch or stream the devin-remote log from a serving sandbox.
The orchestrator launches devin-remote inside each session sandbox with its
output redirected to /tmp/devin-outposts/<session>.log. This helper connects
to a sandbox from this outpost and reads that file.
Usage (from the repo root, .env is picked up automatically):
./.venv/bin/python session_logs.py # list this outpost's sandboxes
./.venv/bin/python session_logs.py <name> # dump the session log
./.venv/bin/python session_logs.py <name> -f # stream it live (Ctrl-C to stop)
"""
import sys
import time
from tensorlake.sandbox.exceptions import RemoteAPIError, SandboxConnectionError
from devin_outposts.config import Config, load_dotenv_if_available
from devin_outposts.sandbox import (
connect_sandbox,
list_outpost_sandboxes,
sandbox_status,
)
LOG_GLOB = "/tmp/devin-outposts/*.log"
def main() -> int:
load_dotenv_if_available()
config = Config.from_env()
infos = list_outpost_sandboxes(config)
if len(sys.argv) < 2:
if not infos:
print("No sandboxes found for this outpost.")
return 0
for info in infos:
print(f"{info.name} [{sandbox_status(info)}] {info.sandbox_id}")
print("\nRun again with a sandbox name to read its session log.")
return 0
name = sys.argv[1]
follow = "-f" in sys.argv[2:]
match = next((i for i in infos if i.name == name or i.sandbox_id == name), None)
if match is None:
print(f"No sandbox named {name!r} on this outpost. Run without arguments to list.")
return 1
sandbox = connect_sandbox(config, match)
try:
listing = sandbox.run("sh", ["-lc", f"ls {LOG_GLOB} 2>/dev/null"], timeout=15)
log_files = listing.stdout.split()
if not log_files:
print("No session log file in the sandbox yet (no session served?).")
return 1
log_path = log_files[-1]
print(f"--- {match.name}:{log_path} ---", file=sys.stderr)
if not follow:
result = sandbox.run("tail", ["-n", "500", log_path], timeout=30)
print(result.stdout, end="")
if result.stderr:
print(result.stderr, file=sys.stderr, end="")
return 0
# Live mode: a PTY websocket is the SDK's real streaming channel.
# (follow_output blocks until the process exits, so it can't tail -F.)
# The websocket drops after idle periods, so reconnect until Ctrl-C.
def on_data(data) -> None:
if isinstance(data, bytes):
data = data.decode("utf-8", "replace")
print(data, end="", flush=True)
backlog = "100" # first attach shows recent lines; reconnects show new only
try:
while True:
pty = None
try:
pty = sandbox.create_pty(
"tail", ["-n", backlog, "-F", log_path], on_data=on_data
)
backlog = "0"
pty.wait()
except SandboxConnectionError as exc:
print(f"\n[reconnecting: {exc}]", file=sys.stderr)
time.sleep(2)
except RemoteAPIError as exc:
message = str(exc)
if "SANDBOX_NOT_RUNNING" in message:
# Devin put the session to sleep; the orchestrator
# suspended the sandbox. Wait for it to resume.
print(
"\n[session asleep, sandbox suspended — waiting for resume]",
file=sys.stderr,
)
time.sleep(15)
elif exc.status_code == 404 or "not found" in message.lower():
print(
"\n[sandbox gone — session terminated]", file=sys.stderr
)
return 0
else:
raise
finally:
if pty is not None:
for cleanup in (pty.kill, pty.disconnect):
try:
cleanup()
except Exception:
pass
except KeyboardInterrupt:
pass
return 0
finally:
sandbox.close()
if __name__ == "__main__":
raise SystemExit(main())