forked from All-Hands-AI/OpenHands
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdocker.py
145 lines (129 loc) · 4.72 KB
/
docker.py
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import os
import pty
import sys
import uuid
import time
import shlex
import select
import subprocess
from typing import List
from collections import namedtuple
InputType = namedtuple("InputDtype", ["content"])
OutputType = namedtuple("OutputDtype", ["content"])
class DockerInteractive:
CONTAINER_IMAGE = "opendevin/sandbox:latest"
def __init__(
self,
workspace_dir: str = None,
container_image: str = None,
timeout: int = 5
):
self.instance_id: str = uuid.uuid4()
if workspace_dir is not None:
assert os.path.exists(workspace_dir), f"Directory {workspace_dir} does not exist."
# expand to absolute path
workspace_dir = os.path.abspath(workspace_dir)
else:
workspace_dir = os.getcwd()
print(f"workspace unspecified, using current directory: {workspace_dir}")
# TODO: this timeout is actually essential - need a better way to set it
# if it is too short, the container may still waiting for previous
# command to finish (e.g. apt-get update)
# if it is too long, the user may have to wait for a unnecessary long time
self.timeout: int = timeout
if container_image is None:
container_image = self.CONTAINER_IMAGE
uid = os.getuid()
cmd = (
f"docker run -it --rm --name sandbox-{self.instance_id} "
f"-v {workspace_dir}:/workspace "
f"-w /workspace "
f"--network=host "
f"{container_image} "
f"/bin/bash -c 'useradd --shell /bin/bash -u {uid} -o -c \"\" -m devin && su devin'"
)
# print(f"Starting Docker container with command: {cmd}")
self.master_fd, self.slave_fd = pty.openpty()
self.container = subprocess.Popen(
shlex.split(cmd),
stdin=self.slave_fd,
stdout=self.slave_fd,
stderr=self.slave_fd,
text=True,
close_fds=True,
)
time.sleep(1) # wait for the container to start
# TODO: use a more robust way to check if the container is ready
self.history: List[InputType | OutputType] = [
OutputType(self._wait_and_read_output())
]
def _wait_and_read_output(self, user_input: str = None) -> str:
output_str = ""
while True:
readable, _, _ = select.select([self.master_fd], [], [], self.timeout)
if readable:
output = os.read(self.master_fd, 1024).decode()
if not output:
break
output_str += output
else:
break
if user_input:
output_str = output_str.lstrip(user_input).lstrip()
return output_str
def execute(self, cmd: str) -> str:
os.write(self.master_fd, (cmd + "\n").encode())
self.history.append(InputType(cmd))
output = self._wait_and_read_output(cmd)
self.history.append(OutputType(output))
return output
def close(self):
if hasattr(self, "master_fd") and self.master_fd is not None:
os.close(self.master_fd)
self.master_fd = None
if hasattr(self, "container") and self.container is not None:
self.container.terminate()
try:
self.container.wait(timeout=5)
print("Container stopped.")
except subprocess.TimeoutExpired:
self.container.kill()
print("Container killed.")
self.container = None
def __del__(self):
self.close()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Interactive Docker container")
parser.add_argument(
"-d",
"--directory",
type=str,
default=None,
help="The directory to mount as the workspace in the Docker container.",
)
args = parser.parse_args()
docker_interactive = DockerInteractive(
workspace_dir=args.directory,
container_image="opendevin/sandbox:latest",
)
print("Interactive Docker container started. Type 'exit' or use Ctrl+C to exit.")
for item in docker_interactive.history:
print(item.content, end="")
sys.stdout.flush()
try:
while True:
try:
user_input = input()
except EOFError:
print("\nExiting...")
break
if user_input.lower() == "exit":
print(f"Exiting...")
break
output = docker_interactive.execute(user_input)
print(output, end="")
sys.stdout.flush()
except KeyboardInterrupt:
print("\nExiting...")
docker_interactive.close()