Skip to content

Commit 805df7e

Browse files
committed
Improve pty
1 parent fb91608 commit 805df7e

1 file changed

Lines changed: 23 additions & 118 deletions

File tree

cli/polyaxon/_vendor/shell_pty.py

Lines changed: 23 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -37,157 +37,62 @@
3737

3838

3939
class PseudoTerminal:
40-
"""Wraps the pseudo-TTY (PTY) allocated to a container.
40+
"""Bridges a K8s websocket exec session to the local terminal."""
4141

42-
The PTY is managed via the current process' TTY until it is closed.
43-
"""
44-
45-
START_ALTERNATE_MODE = set("\x1b[?{0}h".format(i) for i in ("1049", "47", "1047"))
46-
END_ALTERNATE_MODE = set("\x1b[?{0}l".format(i) for i in ("1049", "47", "1047"))
47-
ALTERNATE_MODE_FLAGS = tuple(START_ALTERNATE_MODE) + tuple(END_ALTERNATE_MODE)
48-
49-
def __init__(self, client_shell=None):
42+
def __init__(self, client_shell):
5043
self.client_shell = client_shell
51-
self.master_fd = None
52-
53-
def start(self, argv=None):
54-
"""
55-
Create a spawned process.
56-
Based on the code for pty.spawn().
57-
"""
58-
if not argv:
59-
argv = [os.environ["SHELL"]]
60-
61-
pid, master_fd = pty.fork()
62-
self.master_fd = master_fd
63-
if pid == pty.CHILD:
64-
os.execlp(argv[0], *argv)
6544

45+
def start(self):
6646
old_handler = signal.signal(signal.SIGWINCH, self._signal_winch)
6747
try:
6848
mode = tty.tcgetattr(pty.STDIN_FILENO)
6949
tty.setraw(pty.STDIN_FILENO)
70-
restore = 1
71-
except tty.error: # This is the same as termios.error
72-
restore = 0
73-
self._init_fd()
50+
restore = True
51+
except tty.error:
52+
restore = False
53+
self._set_pty_size()
7454
try:
7555
self._loop()
76-
except (IOError, OSError):
56+
finally:
7757
if restore:
7858
tty.tcsetattr(pty.STDIN_FILENO, tty.TCSAFLUSH, mode)
79-
80-
self.client_shell.close()
81-
self.client_shell = None
82-
if self.master_fd:
83-
os.close(self.master_fd)
84-
self.master_fd = None
85-
signal.signal(signal.SIGWINCH, old_handler)
86-
87-
def _init_fd(self):
88-
"""
89-
Called once when the pty is first set up.
90-
"""
91-
self._set_pty_size()
59+
signal.signal(signal.SIGWINCH, old_handler)
60+
if self.client_shell:
61+
self.client_shell.close()
62+
self.client_shell = None
9263

9364
def _signal_winch(self, signum, frame):
94-
"""
95-
Signal handler for SIGWINCH - window size has changed.
96-
"""
9765
self._set_pty_size()
9866

9967
def _set_pty_size(self):
100-
"""
101-
Sets the window size of the child pty based on the window size of
102-
our own controlling terminal.
103-
"""
10468
packed = fcntl.ioctl(
10569
pty.STDOUT_FILENO, termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0)
10670
)
107-
rows, cols, h_pixels, v_pixels = struct.unpack("HHHH", packed)
71+
rows, cols, _, _ = struct.unpack("HHHH", packed)
10872
self.client_shell.write_channel(
10973
ws_client.RESIZE_CHANNEL, orjson_dumps({"Height": rows, "Width": cols})
11074
)
11175

11276
def _loop(self):
113-
"""
114-
Main select loop. Passes all data to self.master_read() or self.stdin_read().
115-
"""
116-
assert self.client_shell is not None
11777
client_shell = self.client_shell
118-
while 1:
78+
while True:
11979
try:
120-
rfds, wfds, xfds = select.select(
80+
rfds, _, _ = select.select(
12181
[pty.STDIN_FILENO, client_shell.sock.sock], [], []
12282
)
123-
except select.error as e:
124-
no = e.errno
125-
if no == errno.EINTR:
83+
except OSError as e:
84+
if e.errno == errno.EINTR:
12685
continue
86+
raise
12787
if pty.STDIN_FILENO in rfds:
12888
data = os.read(pty.STDIN_FILENO, 1024)
129-
self.stdin_read(data)
89+
if not data:
90+
break
91+
client_shell.write_stdin(data)
13092
if client_shell.sock.sock in rfds:
131-
# read from client_shell
13293
if client_shell.peek_stdout():
133-
self.master_read(client_shell.read_stdout())
94+
os.write(pty.STDOUT_FILENO, client_shell.read_stdout().encode())
13495
if client_shell.peek_stderr():
135-
self.master_read(client_shell.read_stderr())
136-
# error occurs
96+
os.write(pty.STDOUT_FILENO, client_shell.read_stderr().encode())
13797
if client_shell.peek_channel(ws_client.ERROR_CHANNEL):
13898
break
139-
140-
def write_stdout(self, data):
141-
"""
142-
Writes to stdout as if the child process had written the data.
143-
"""
144-
os.write(pty.STDOUT_FILENO, data.encode())
145-
146-
def write_master(self, data):
147-
"""
148-
Writes to the child process from its controlling terminal.
149-
"""
150-
assert self.client_shell is not None
151-
self.client_shell.write_stdin(data)
152-
153-
def master_read(self, data):
154-
"""
155-
Called when there is data to be sent from the child process back to the user.
156-
"""
157-
flag = self.findlast(data, self.ALTERNATE_MODE_FLAGS)
158-
if flag is not None:
159-
if flag in self.START_ALTERNATE_MODE:
160-
# This code is executed when the child process switches the
161-
# terminal into alternate mode. The line below
162-
# assumes that the user has opened vim, and writes a
163-
# message.
164-
self.write_master("IEntering special mode.\x1b")
165-
elif flag in self.END_ALTERNATE_MODE:
166-
# This code is executed when the child process switches the
167-
# terminal back out of alternate mode. The line below
168-
# assumes that the user has returned to the command
169-
# prompt.
170-
self.write_master('echo "Leaving special mode."\r')
171-
self.write_stdout(data)
172-
173-
def stdin_read(self, data):
174-
"""
175-
Called when there is data to be sent from the user/controlling
176-
terminal down to the child process.
177-
"""
178-
self.write_master(data)
179-
180-
@staticmethod
181-
def findlast(s, substrs):
182-
"""
183-
Finds whichever of the given substrings occurs last in the given string
184-
and returns that substring, or returns None if no such strings occur.
185-
"""
186-
i = -1
187-
result = None
188-
for substr in substrs:
189-
pos = s.rfind(substr)
190-
if pos > i:
191-
i = pos
192-
result = substr
193-
return result

0 commit comments

Comments
 (0)