@@ -13,6 +13,8 @@ use state::{
1313 delete as delete_state, load_exec_state, load_pid, load_state, save_exec_state, save_pid,
1414 save_state, ContainerState , OciUser ,
1515} ;
16+ #[ cfg( target_os = "linux" ) ]
17+ use state:: { exec_resize_path, resize_path} ;
1618
1719#[ cfg( target_os = "linux" ) ]
1820mod overlay;
@@ -243,6 +245,58 @@ fn exit_code_from_status(status: std::process::ExitStatus) -> i32 {
243245 }
244246}
245247
248+ /// Spawn a thread that polls a resize file and applies `TIOCSWINSZ` to the PTY master.
249+ ///
250+ /// The shim writes `"width height\n"` to `resize_file`. This thread polls every 100ms,
251+ /// reads the dimensions, applies them via ioctl, then deletes the file. The thread exits
252+ /// when `stop` is set to `true` (signaled by the caller after the child process exits).
253+ #[ cfg( target_os = "linux" ) ]
254+ fn spawn_resize_watcher (
255+ master_raw_fd : i32 ,
256+ resize_file : PathBuf ,
257+ stop : std:: sync:: Arc < std:: sync:: atomic:: AtomicBool > ,
258+ ) {
259+ std:: thread:: spawn ( move || {
260+ use std:: sync:: atomic:: Ordering ;
261+ while !stop. load ( Ordering :: Relaxed ) {
262+ if resize_file. exists ( ) {
263+ if let Ok ( content) = fs:: read_to_string ( & resize_file) {
264+ let _ = fs:: remove_file ( & resize_file) ;
265+ let parts: Vec < & str > = content. split_whitespace ( ) . collect ( ) ;
266+ if parts. len ( ) == 2 {
267+ if let ( Ok ( width) , Ok ( height) ) =
268+ ( parts[ 0 ] . parse :: < u16 > ( ) , parts[ 1 ] . parse :: < u16 > ( ) )
269+ {
270+ let ws = nix:: libc:: winsize {
271+ ws_row : height,
272+ ws_col : width,
273+ ws_xpixel : 0 ,
274+ ws_ypixel : 0 ,
275+ } ;
276+ let ret = unsafe {
277+ nix:: libc:: ioctl (
278+ master_raw_fd,
279+ nix:: libc:: TIOCSWINSZ as _ ,
280+ & ws as * const nix:: libc:: winsize ,
281+ )
282+ } ;
283+ if ret < 0 {
284+ tracing:: warn!(
285+ "TIOCSWINSZ failed: {}" ,
286+ std:: io:: Error :: last_os_error( )
287+ ) ;
288+ } else {
289+ tracing:: debug!( "PTY resized to {}x{}" , width, height) ;
290+ }
291+ }
292+ }
293+ }
294+ }
295+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
296+ }
297+ } ) ;
298+ }
299+
246300#[ allow( clippy:: too_many_arguments) ]
247301fn do_create (
248302 id : & str ,
@@ -658,13 +712,31 @@ fn do_start(id: &str, bundle: &Path) -> Result<()> {
658712 // Close slave in parent - child has it via dup2
659713 drop ( pty. slave ) ;
660714
715+ // Capture master raw fd for resize ioctl before converting to File
716+ #[ cfg( target_os = "linux" ) ]
717+ let master_raw_fd = {
718+ use std:: os:: unix:: io:: AsRawFd ;
719+ pty. master . as_raw_fd ( )
720+ } ;
721+
661722 // Convert PTY master OwnedFd to File for I/O
662723 let master_file: std:: fs:: File = pty. master . into ( ) ;
663724 let master_clone = master_file. try_clone ( ) . unwrap_or_else ( |e| {
664725 tracing:: error!( "failed to clone master fd: {}" , e) ;
665726 std:: process:: exit ( 1 ) ;
666727 } ) ;
667728
729+ // Start PTY resize watcher thread
730+ #[ cfg( target_os = "linux" ) ]
731+ let resize_stop =
732+ std:: sync:: Arc :: new ( std:: sync:: atomic:: AtomicBool :: new ( false ) ) ;
733+ #[ cfg( target_os = "linux" ) ]
734+ spawn_resize_watcher (
735+ master_raw_fd,
736+ resize_path ( & container_id) ,
737+ resize_stop. clone ( ) ,
738+ ) ;
739+
668740 // Relay: stdin FIFO → PTY master (user input to process)
669741 if let Some ( ref state) = io_state {
670742 if let Some ( ref stdin_path) = state. stdin {
@@ -746,6 +818,8 @@ fn do_start(id: &str, bundle: &Path) -> Result<()> {
746818
747819 match child. wait ( ) {
748820 Ok ( exit_status) => {
821+ #[ cfg( target_os = "linux" ) ]
822+ resize_stop. store ( true , std:: sync:: atomic:: Ordering :: Relaxed ) ;
749823 let exit_code = exit_code_from_status ( exit_status) ;
750824 if let Ok ( mut state) = load_state ( & container_id) {
751825 state. status = "stopped" . into ( ) ;
@@ -754,6 +828,8 @@ fn do_start(id: &str, bundle: &Path) -> Result<()> {
754828 }
755829 }
756830 Err ( _e) => {
831+ #[ cfg( target_os = "linux" ) ]
832+ resize_stop. store ( true , std:: sync:: atomic:: Ordering :: Relaxed ) ;
757833 if let Ok ( mut state) = load_state ( & container_id) {
758834 state. status = "stopped" . into ( ) ;
759835 state. exit_code = Some ( 1 ) ;
@@ -1167,13 +1243,30 @@ fn exec_with_pty(
11671243 // Close slave in parent - child has it via dup2
11681244 drop ( pty. slave ) ;
11691245
1246+ // Capture master raw fd for resize ioctl before converting to File
1247+ #[ cfg( target_os = "linux" ) ]
1248+ let master_raw_fd = {
1249+ use std:: os:: unix:: io:: AsRawFd ;
1250+ pty. master . as_raw_fd ( )
1251+ } ;
1252+
11701253 // Convert PTY master OwnedFd to File for I/O
11711254 let master_file: std:: fs:: File = pty. master . into ( ) ;
11721255 let master_clone = master_file. try_clone ( ) . unwrap_or_else ( |e| {
11731256 tracing:: error!( "failed to clone master fd: {}" , e) ;
11741257 std:: process:: exit ( 1 ) ;
11751258 } ) ;
11761259
1260+ // Start PTY resize watcher thread
1261+ #[ cfg( target_os = "linux" ) ]
1262+ let resize_stop = std:: sync:: Arc :: new ( std:: sync:: atomic:: AtomicBool :: new ( false ) ) ;
1263+ #[ cfg( target_os = "linux" ) ]
1264+ spawn_resize_watcher (
1265+ master_raw_fd,
1266+ exec_resize_path ( container_id, exec_id) ,
1267+ resize_stop. clone ( ) ,
1268+ ) ;
1269+
11771270 // Start relay threads
11781271 // stdin FIFO → PTY master (user input to process)
11791272 if let Some ( ref stdin_p) = stdin_path {
@@ -1226,10 +1319,13 @@ fn exec_with_pty(
12261319 }
12271320
12281321 // Wait for child
1229- match child. wait ( ) {
1322+ let exit = match child. wait ( ) {
12301323 Ok ( status) => exit_code_from_status ( status) ,
12311324 Err ( _) => 1 ,
1232- }
1325+ } ;
1326+ #[ cfg( target_os = "linux" ) ]
1327+ resize_stop. store ( true , std:: sync:: atomic:: Ordering :: Relaxed ) ;
1328+ exit
12331329}
12341330
12351331#[ allow( clippy:: too_many_arguments) ]
0 commit comments