|
37 | 37 |
|
38 | 38 |
|
39 | 39 | class PseudoTerminal: |
40 | | - """Wraps the pseudo-TTY (PTY) allocated to a container. |
| 40 | + """Bridges a K8s websocket exec session to the local terminal.""" |
41 | 41 |
|
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): |
50 | 43 | 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) |
65 | 44 |
|
| 45 | + def start(self): |
66 | 46 | old_handler = signal.signal(signal.SIGWINCH, self._signal_winch) |
67 | 47 | try: |
68 | 48 | mode = tty.tcgetattr(pty.STDIN_FILENO) |
69 | 49 | 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() |
74 | 54 | try: |
75 | 55 | self._loop() |
76 | | - except (IOError, OSError): |
| 56 | + finally: |
77 | 57 | if restore: |
78 | 58 | 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 |
92 | 63 |
|
93 | 64 | def _signal_winch(self, signum, frame): |
94 | | - """ |
95 | | - Signal handler for SIGWINCH - window size has changed. |
96 | | - """ |
97 | 65 | self._set_pty_size() |
98 | 66 |
|
99 | 67 | 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 | | - """ |
104 | 68 | packed = fcntl.ioctl( |
105 | 69 | pty.STDOUT_FILENO, termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0) |
106 | 70 | ) |
107 | | - rows, cols, h_pixels, v_pixels = struct.unpack("HHHH", packed) |
| 71 | + rows, cols, _, _ = struct.unpack("HHHH", packed) |
108 | 72 | self.client_shell.write_channel( |
109 | 73 | ws_client.RESIZE_CHANNEL, orjson_dumps({"Height": rows, "Width": cols}) |
110 | 74 | ) |
111 | 75 |
|
112 | 76 | 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 |
117 | 77 | client_shell = self.client_shell |
118 | | - while 1: |
| 78 | + while True: |
119 | 79 | try: |
120 | | - rfds, wfds, xfds = select.select( |
| 80 | + rfds, _, _ = select.select( |
121 | 81 | [pty.STDIN_FILENO, client_shell.sock.sock], [], [] |
122 | 82 | ) |
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: |
126 | 85 | continue |
| 86 | + raise |
127 | 87 | if pty.STDIN_FILENO in rfds: |
128 | 88 | data = os.read(pty.STDIN_FILENO, 1024) |
129 | | - self.stdin_read(data) |
| 89 | + if not data: |
| 90 | + break |
| 91 | + client_shell.write_stdin(data) |
130 | 92 | if client_shell.sock.sock in rfds: |
131 | | - # read from client_shell |
132 | 93 | if client_shell.peek_stdout(): |
133 | | - self.master_read(client_shell.read_stdout()) |
| 94 | + os.write(pty.STDOUT_FILENO, client_shell.read_stdout().encode()) |
134 | 95 | 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()) |
137 | 97 | if client_shell.peek_channel(ws_client.ERROR_CHANNEL): |
138 | 98 | 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