@@ -10,6 +10,102 @@ use crate::run_ephemeral::{run_detached, RunEphemeralOpts};
1010use crate :: ssh;
1111use crate :: supervisor_status:: { SupervisorState , SupervisorStatus } ;
1212
13+ /// Fetch and display container logs to help diagnose startup failures
14+ fn show_container_logs ( container_name : & str ) {
15+ debug ! ( "Fetching container logs for {}" , container_name) ;
16+
17+ // Try to get container state/exit code and error
18+ let exit_code = if let Ok ( output) = Command :: new ( "podman" )
19+ . args ( [
20+ "inspect" ,
21+ container_name,
22+ "--format" ,
23+ "{{.State.Status}} (exit code: {{.State.ExitCode}}){{if .State.Error}} - Error: {{.State.Error}}{{end}}" ,
24+ ] )
25+ . output ( )
26+ {
27+ let state = String :: from_utf8_lossy ( & output. stdout ) ;
28+ if !state. trim ( ) . is_empty ( ) {
29+ eprintln ! ( "\n Container state: {}" , state. trim( ) ) ;
30+ }
31+
32+ // Extract exit code for interpretation
33+ Command :: new ( "podman" )
34+ . args ( [ "inspect" , container_name, "--format" , "{{.State.ExitCode}}" ] )
35+ . output ( )
36+ . ok ( )
37+ . and_then ( |o| String :: from_utf8_lossy ( & o. stdout ) . trim ( ) . parse :: < i32 > ( ) . ok ( ) )
38+ } else {
39+ None
40+ } ;
41+
42+ // Provide helpful hints for common exit codes
43+ if let Some ( code) = exit_code {
44+ match code {
45+ 127 => {
46+ eprintln ! ( "\n Note: Exit code 127 typically means 'command not found'." ) ;
47+ eprintln ! ( "This container image may not be a valid bootc image." ) ;
48+ eprintln ! ( "Bootc images must have systemd and kernel modules in /usr/lib/modules." ) ;
49+ }
50+ 126 => {
51+ eprintln ! ( "\n Note: Exit code 126 typically means 'permission denied' or file not executable." ) ;
52+ }
53+ _ => { }
54+ }
55+ }
56+
57+ let output = match Command :: new ( "podman" )
58+ . args ( [ "logs" , container_name] )
59+ . stderr ( Stdio :: inherit ( ) )
60+ . output ( )
61+ {
62+ Ok ( output) => output,
63+ Err ( e) => {
64+ eprintln ! ( "Failed to fetch container logs: {}" , e) ;
65+ return ;
66+ }
67+ } ;
68+
69+ let logs = String :: from_utf8_lossy ( & output. stdout ) ;
70+ if !logs. trim ( ) . is_empty ( ) {
71+ eprintln ! ( "\n Container logs:" ) ;
72+ eprintln ! ( "----------------------------------------" ) ;
73+ for line in logs. lines ( ) {
74+ eprintln ! ( "{}" , line) ;
75+ }
76+ eprintln ! ( "----------------------------------------\n " ) ;
77+ } else {
78+ eprintln ! ( "(Container produced no output)" ) ;
79+ }
80+ }
81+
82+ /// RAII guard for ephemeral container cleanup
83+ /// Ensures container is removed when dropped, even on error paths
84+ struct ContainerCleanup {
85+ container_id : String ,
86+ }
87+
88+ impl ContainerCleanup {
89+ fn new ( container_id : String ) -> Self {
90+ Self { container_id }
91+ }
92+ }
93+
94+ impl Drop for ContainerCleanup {
95+ fn drop ( & mut self ) {
96+ debug ! ( "Cleaning up ephemeral container {}" , self . container_id) ;
97+ let result = Command :: new ( "podman" )
98+ . args ( [ "rm" , "-f" , & self . container_id ] )
99+ . stdout ( Stdio :: null ( ) )
100+ . stderr ( Stdio :: null ( ) )
101+ . status ( ) ;
102+
103+ if let Err ( e) = result {
104+ tracing:: warn!( "Failed to remove container {}: {}" , self . container_id, e) ;
105+ }
106+ }
107+ }
108+
13109/// Timeout waiting for connection
14110pub ( crate ) const SSH_TIMEOUT : std:: time:: Duration = const { Duration :: from_secs ( 240 ) } ;
15111
@@ -23,6 +119,17 @@ pub struct RunEphemeralSshOpts {
23119 pub ssh_args : Vec < String > ,
24120}
25121
122+ /// Check if container is running
123+ fn is_container_running ( container_name : & str ) -> Result < bool > {
124+ let output = Command :: new ( "podman" )
125+ . args ( [ "inspect" , container_name, "--format" , "{{.State.Status}}" ] )
126+ . output ( )
127+ . context ( "Failed to inspect container state" ) ?;
128+
129+ let state = String :: from_utf8_lossy ( & output. stdout ) ;
130+ Ok ( state. trim ( ) == "running" )
131+ }
132+
26133/// Wait for VM SSH availability using the supervisor status file
27134///
28135/// Monitors /run/supervisor-status.json inside the container for SSH.
@@ -40,6 +147,13 @@ pub fn wait_for_vm_ssh(
40147 timeout. as_secs( )
41148 ) ;
42149
150+ // Check if container is still running before attempting exec
151+ if !is_container_running ( container_name) ? {
152+ progress. finish_and_clear ( ) ;
153+ show_container_logs ( container_name) ;
154+ return Err ( eyre ! ( "Container exited before SSH became available" ) ) ;
155+ }
156+
43157 // Use the new monitor-status subcommand for efficient inotify-based monitoring
44158 let mut cmd = Command :: new ( "podman" ) ;
45159 cmd. args ( [
@@ -56,10 +170,14 @@ pub fn wait_for_vm_ssh(
56170 . map_err ( Into :: into)
57171 } ) ;
58172 }
59- let mut child = cmd
60- . stdout ( Stdio :: piped ( ) )
61- . spawn ( )
62- . context ( "Failed to start status monitor" ) ?;
173+ let mut child = match cmd. stdout ( Stdio :: piped ( ) ) . stderr ( Stdio :: inherit ( ) ) . spawn ( ) {
174+ Ok ( child) => child,
175+ Err ( e) => {
176+ progress. finish_and_clear ( ) ;
177+ show_container_logs ( container_name) ;
178+ return Err ( e) . context ( "Failed to start status monitor" ) ;
179+ }
180+ } ;
63181
64182 let stdout = child. stdout . take ( ) . unwrap ( ) ;
65183 let reader = std:: io:: BufReader :: new ( stdout) ;
@@ -100,6 +218,9 @@ pub fn wait_for_vm_ssh(
100218 }
101219
102220 let status = child. wait ( ) ?;
221+
222+ progress. finish_and_clear ( ) ;
223+ show_container_logs ( container_name) ;
103224 Err ( eyre ! ( "Monitor process exited unexpectedly: {status:?}" ) )
104225}
105226
@@ -147,14 +268,16 @@ pub fn wait_for_ssh_ready(
147268pub fn run_ephemeral_ssh ( opts : RunEphemeralSshOpts ) -> Result < ( ) > {
148269 // Start the ephemeral pod in detached mode with SSH enabled
149270 let mut ephemeral_opts = opts. run_opts . clone ( ) ;
150- ephemeral_opts. podman . rm = true ;
151271 ephemeral_opts. podman . detach = true ;
152272 ephemeral_opts. common . ssh_keygen = true ; // Enable SSH key generation and access
153273
154274 debug ! ( "Starting ephemeral VM..." ) ;
155275 let container_id = run_detached ( ephemeral_opts) ?;
156276 debug ! ( "Ephemeral VM started with container ID: {}" , container_id) ;
157277
278+ // Create cleanup guard to ensure container removal on any exit path
279+ let _cleanup = ContainerCleanup :: new ( container_id. clone ( ) ) ;
280+
158281 // Use the container ID for SSH and cleanup
159282 let container_name = container_id;
160283 debug ! ( "Using container ID: {}" , container_name) ;
@@ -176,16 +299,9 @@ pub fn run_ephemeral_ssh(opts: RunEphemeralSshOpts) -> Result<()> {
176299 let exit_code = status. code ( ) . unwrap_or ( 1 ) ;
177300 debug ! ( "SSH exit code: {}" , exit_code) ;
178301
179- // SSH completed, proceed with cleanup
180-
181- // Cleanup: stop and remove the container immediately
182- debug ! ( "SSH session ended, cleaning up ephemeral pod..." ) ;
183-
184- let _ = Command :: new ( "podman" )
185- . args ( [ "rm" , "-f" , & container_name] )
186- . stdout ( Stdio :: null ( ) )
187- . stderr ( Stdio :: null ( ) )
188- . status ( ) ;
302+ // Explicitly drop the cleanup guard before exit to ensure container removal
303+ // (std::process::exit doesn't run drop handlers)
304+ drop ( _cleanup) ;
189305
190306 // Exit with SSH client's exit code
191307 std:: process:: exit ( exit_code) ;
0 commit comments