@@ -73,26 +73,49 @@ def _resolve_pod(cclient, deployment_id, pod_name=None):
7373 return running_pods [0 ]
7474
7575
76- async def _forward_io (ws ):
76+ async def _forward_io (ws , fix_dimensions = False ):
7777 """Bidirectional forwarding between local stdin/stdout and WebSocket.
7878
79+ Args:
80+ ws: WebSocket connection.
81+ fix_dimensions: When True, send ``stty rows R cols C`` through stdin
82+ on the first output message to fix the remote PTY dimensions,
83+ then clear the screen so the prompt redraws at the correct width.
84+
7985 Returns the remote exit code.
8086 """
8187 loop = asyncio .get_running_loop ()
8288 exit_code = 0
8389 stdin_fd = sys .stdin .fileno ()
84-
8590 stdin_closed = asyncio .Event ()
91+ dimensions_fixed = not fix_dimensions
8692
8793 async def _read_ws ():
88- nonlocal exit_code
94+ nonlocal exit_code , dimensions_fixed
8995 async for raw_msg in ws :
9096 msg = json .loads (raw_msg )
9197 if msg .get ("data" ):
92- sys .stdout .buffer .write (msg ["data" ].encode ("utf-8" , errors = "replace" ))
98+ if not dimensions_fixed :
99+ dimensions_fixed = True
100+ r , c = shutil .get_terminal_size ()
101+ # Set PTY size from inside the shell and clear screen.
102+ # Leading space keeps the command out of bash history.
103+ await ws .send (
104+ json .dumps (
105+ {
106+ "operation" : "stdin" ,
107+ "data" : f" stty rows { r } cols { c } 2>/dev/null; clear\n " ,
108+ }
109+ )
110+ )
111+ # Convert bare \n to \r\n (equivalent to xterm.js convertEol).
112+ # The remote PTY may send lone \n; without this the cursor
113+ # moves down without returning to column 0.
114+ text = msg ["data" ].replace ("\n " , "\r \n " )
115+ sys .stdout .buffer .write (text .encode ("utf-8" , errors = "replace" ))
93116 sys .stdout .buffer .flush ()
94117 elif msg .get ("error" ):
95- sys .stderr .write (f"Error: { msg ['error' ]} \n " )
118+ sys .stderr .write (f"Error: { msg ['error' ]} \r \ n " )
96119 sys .stderr .flush ()
97120 if "Code" in msg :
98121 exit_code = msg ["Code" ]
@@ -154,14 +177,6 @@ async def _interactive_session(ws_url, token):
154177 old_settings = termios .tcgetattr (fd )
155178 try :
156179 tty .setraw (fd )
157- # Re-enable output post-processing so the local terminal converts
158- # bare \n to \r\n. The remote PTY may send lone \n (Kubernetes
159- # exec PTY behavior). xterm.js handles this with convertEol;
160- # for a raw CLI terminal we need OPOST. Extra \r before \n from
161- # a remote that already sends \r\n is harmless.
162- attrs = termios .tcgetattr (fd )
163- attrs [1 ] |= termios .OPOST
164- termios .tcsetattr (fd , termios .TCSANOW , attrs )
165180 rows , cols = shutil .get_terminal_size ()
166181
167182 headers = {"Authorization" : f"Bearer { token } " }
@@ -180,35 +195,10 @@ def _send_resize():
180195 ws .send (json .dumps ({"operation" : "resize" , "rows" : r , "cols" : c }))
181196 )
182197
183- async def _force_initial_redraw ():
184- """Toggle PTY width to force SIGWINCH, then Ctrl+L to redraw.
185-
186- The initial resize may arrive before the remote shell
187- starts, so the shell never sees a SIGWINCH. Toggling
188- cols by +1/-1 guarantees a size change and SIGWINCH.
189- Ctrl+L then clears the screen so the prompt redraws at
190- the correct width.
191- """
192- try :
193- await asyncio .sleep (0.5 )
194- r , c = shutil .get_terminal_size ()
195- await ws .send (
196- json .dumps ({"operation" : "resize" , "rows" : r , "cols" : c + 1 })
197- )
198- await asyncio .sleep (0.05 )
199- await ws .send (
200- json .dumps ({"operation" : "resize" , "rows" : r , "cols" : c })
201- )
202- await asyncio .sleep (0.1 )
203- await ws .send (json .dumps ({"operation" : "stdin" , "data" : "\x0c " }))
204- except Exception :
205- pass
206-
207- asyncio .create_task (_force_initial_redraw ())
208198 loop .add_signal_handler (signal .SIGWINCH , _send_resize )
209199
210200 try :
211- exit_code = await _forward_io (ws )
201+ exit_code = await _forward_io (ws , fix_dimensions = True )
212202 finally :
213203 loop .remove_signal_handler (signal .SIGWINCH )
214204
0 commit comments