Skip to content

Commit c0ffa93

Browse files
authored
feat(openshell-vm): allow to have tty with exec (#939)
add support on both side (rust and python) fixes #936 Signed-off-by: Florent Benoit <fbenoit@redhat.com>
1 parent 4510b0d commit c0ffa93

2 files changed

Lines changed: 568 additions & 8 deletions

File tree

crates/openshell-vm/scripts/openshell-vm-exec-agent.py

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
import base64
6+
import fcntl
67
import json
78
import os
9+
import pty
810
import socket
11+
import struct
912
import subprocess
1013
import sys
14+
import termios
1115
import threading
1216

1317

@@ -42,6 +46,11 @@ def validate_env(env_items):
4246
return env
4347

4448

49+
def set_winsize(fd, cols, rows):
50+
winsize = struct.pack("HHHH", rows, cols, 0, 0)
51+
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
52+
53+
4554
def stream_reader(pipe, frame_type, sock_file, lock):
4655
try:
4756
while True:
@@ -79,6 +88,8 @@ def stdin_writer(proc, sock_file, sock, lock):
7988
proc.stdin.flush()
8089
elif kind == "stdin_close":
8190
break
91+
elif kind == "resize":
92+
pass
8293
else:
8394
send_frame(
8495
sock_file,
@@ -96,14 +107,10 @@ def stdin_writer(proc, sock_file, sock, lock):
96107
pass
97108

98109

99-
def handle_client(conn):
100-
sock_file = conn.makefile("rwb", buffering=0)
110+
def handle_client_pipe(conn, request, sock_file):
111+
"""Handle a client connection using pipes (non-TTY mode)."""
101112
lock = threading.Lock()
102113
try:
103-
request = recv_line(sock_file)
104-
if request is None:
105-
return
106-
107114
argv = request.get("argv") or ["sh"]
108115
cwd = request.get("cwd")
109116
env = os.environ.copy()
@@ -153,6 +160,148 @@ def handle_client(conn):
153160
conn.close()
154161

155162

163+
def handle_client_tty(conn, request, sock_file):
164+
"""Handle a client connection with PTY allocation."""
165+
lock = threading.Lock()
166+
master_fd = -1
167+
try:
168+
argv = request.get("argv") or ["sh"]
169+
cwd = request.get("cwd")
170+
env = os.environ.copy()
171+
env.update(validate_env(request.get("env") or []))
172+
env.setdefault("TERM", "xterm-256color")
173+
174+
master_fd, slave_fd = pty.openpty()
175+
176+
# Consume any resize frame sent right after the ExecRequest.
177+
# The host sends it before starting the stdin pump, so it
178+
# should arrive quickly. Use a short socket timeout.
179+
conn.settimeout(0.5)
180+
try:
181+
pending = sock_file.readline()
182+
if pending:
183+
frame = json.loads(pending.decode("utf-8"))
184+
if frame.get("type") == "resize":
185+
set_winsize(
186+
slave_fd,
187+
frame.get("cols", 80),
188+
frame.get("rows", 24),
189+
)
190+
except (socket.timeout, ValueError, OSError):
191+
pass
192+
finally:
193+
conn.settimeout(None)
194+
195+
proc = subprocess.Popen(
196+
argv,
197+
cwd=cwd or "/",
198+
env=env,
199+
stdin=slave_fd,
200+
stdout=slave_fd,
201+
stderr=slave_fd,
202+
preexec_fn=os.setsid,
203+
)
204+
os.close(slave_fd)
205+
206+
def pty_reader():
207+
try:
208+
while True:
209+
try:
210+
chunk = os.read(master_fd, 8192)
211+
except OSError:
212+
break
213+
if not chunk:
214+
break
215+
send_frame(
216+
sock_file,
217+
lock,
218+
{
219+
"type": "stdout",
220+
"data": base64.b64encode(chunk).decode("ascii"),
221+
},
222+
)
223+
except Exception:
224+
pass
225+
226+
def pty_stdin_writer():
227+
try:
228+
while True:
229+
frame = recv_line(sock_file)
230+
if frame is None:
231+
break
232+
kind = frame.get("type")
233+
if kind == "stdin":
234+
payload = base64.b64decode(frame.get("data", ""))
235+
try:
236+
os.write(master_fd, payload)
237+
except OSError:
238+
break
239+
elif kind == "resize":
240+
try:
241+
set_winsize(
242+
master_fd,
243+
frame.get("cols", 80),
244+
frame.get("rows", 24),
245+
)
246+
except OSError:
247+
pass
248+
elif kind == "stdin_close":
249+
break
250+
else:
251+
send_frame(
252+
sock_file,
253+
lock,
254+
{"type": "error", "message": f"unknown frame type: {kind}"},
255+
)
256+
break
257+
except (BrokenPipeError, OSError):
258+
pass
259+
260+
reader_thread = threading.Thread(target=pty_reader, daemon=True)
261+
stdin_thread = threading.Thread(target=pty_stdin_writer, daemon=True)
262+
reader_thread.start()
263+
stdin_thread.start()
264+
265+
code = proc.wait()
266+
reader_thread.join(timeout=2)
267+
send_frame(sock_file, lock, {"type": "exit", "code": code})
268+
except Exception as exc:
269+
try:
270+
send_frame(sock_file, lock, {"type": "error", "message": str(exc)})
271+
except Exception:
272+
pass
273+
finally:
274+
if master_fd >= 0:
275+
try:
276+
os.close(master_fd)
277+
except OSError:
278+
pass
279+
try:
280+
sock_file.close()
281+
except Exception:
282+
pass
283+
conn.close()
284+
285+
286+
def handle_client(conn):
287+
sock_file = conn.makefile("rwb", buffering=0)
288+
try:
289+
request = recv_line(sock_file)
290+
if request is None:
291+
sock_file.close()
292+
conn.close()
293+
return
294+
except Exception:
295+
sock_file.close()
296+
conn.close()
297+
return
298+
299+
if request.get("tty"):
300+
handle_client_tty(conn, request, sock_file)
301+
else:
302+
handle_client_pipe(conn, request, sock_file)
303+
304+
156305
def main():
157306
if not hasattr(socket, "AF_VSOCK"):
158307
print("AF_VSOCK is not available", file=sys.stderr)

0 commit comments

Comments
 (0)