@@ -115,6 +115,76 @@ mod tests {
115115 #[ cfg( windows) ]
116116 const GDB_COMMAND : & str = "gdb" ;
117117
118+ /// Construct the (out_file_path, cmd_file_path, manifest_dir)
119+ /// triple every gdb test needs.
120+ fn gdb_test_paths ( name : & str ) -> ( String , String , String ) {
121+ let out_dir = std:: env:: var ( "OUT_DIR" ) . expect ( "Failed to get out dir" ) ;
122+ let manifest_dir = std:: env:: var ( "CARGO_MANIFEST_DIR" )
123+ . expect ( "Failed to get manifest dir" )
124+ . replace ( '\\' , "/" ) ;
125+ let out_file_path = format ! ( "{out_dir}/{name}.output" ) ;
126+ let cmd_file_path = format ! ( "{out_dir}/{name}-commands.txt" ) ;
127+ ( out_file_path, cmd_file_path, manifest_dir)
128+ }
129+
130+ /// Build a gdb script that connects to `port`, sets a single
131+ /// breakpoint at `breakpoint`, prints `echo_msg` when hit, and
132+ /// detaches before quitting.
133+ ///
134+ /// The breakpoint commands end with `detach` + `quit` instead of
135+ /// `continue`. The previous "inner continue, outer continue, quit"
136+ /// shape races with the inferior exit. After the breakpoint hits
137+ /// and the inner `continue` resumes the guest, the guest may run
138+ /// to completion and the gdb stub may close the remote before gdb
139+ /// has dispatched the outer `continue`, producing a non-zero exit
140+ /// with `Remote connection closed`. Detaching from the breakpoint
141+ /// commands removes that window. The host process keeps running
142+ /// the guest call to completion on its own after detach.
143+ fn single_breakpoint_script (
144+ manifest_dir : & str ,
145+ port : u16 ,
146+ out_file_path : & str ,
147+ breakpoint : & str ,
148+ echo_msg : & str ,
149+ ) -> String {
150+ let cmd = format ! (
151+ "file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
152+ target remote :{port}
153+
154+ set pagination off
155+ set logging file {out_file_path}
156+ set logging enabled on
157+
158+ break {breakpoint}
159+ commands
160+ echo \" {echo_msg}\\ n\"
161+ backtrace
162+
163+ set logging enabled off
164+ detach
165+ quit
166+ end
167+
168+ continue
169+ "
170+ ) ;
171+ #[ cfg( windows) ]
172+ let cmd = format ! ( "set osabi none\n {cmd}" ) ;
173+ cmd
174+ }
175+
176+ /// Spawn the gdb client to execute the script in `cmd_file_path`.
177+ fn spawn_gdb_client ( cmd_file_path : & str ) -> std:: process:: Child {
178+ Command :: new ( GDB_COMMAND )
179+ . arg ( "-nx" )
180+ . arg ( "--nw" )
181+ . arg ( "--batch" )
182+ . arg ( "-x" )
183+ . arg ( cmd_file_path)
184+ . spawn ( )
185+ . expect ( "Failed to start gdb" )
186+ }
187+
118188 fn write_cmds_file ( cmd_file_path : & str , cmd : & str ) -> io:: Result < ( ) > {
119189 let file = File :: create ( cmd_file_path) ?;
120190 let mut writer = BufWriter :: new ( file) ;
@@ -163,14 +233,7 @@ mod tests {
163233 // wait 3 seconds for the gdb to connect
164234 thread:: sleep ( Duration :: from_secs ( 3 ) ) ;
165235
166- let mut gdb = Command :: new ( GDB_COMMAND )
167- . arg ( "-nx" ) // Don't load any .gdbinit files
168- . arg ( "--nw" )
169- . arg ( "--batch" )
170- . arg ( "-x" )
171- . arg ( cmd_file_path)
172- . spawn ( )
173- . map_err ( |e| new_error ! ( "Failed to start gdb process: {}" , e) ) ?;
236+ let mut gdb = spawn_gdb_client ( cmd_file_path) ;
174237
175238 // wait 3 seconds for the gdb to connect
176239 thread:: sleep ( Duration :: from_secs ( 10 ) ) ;
@@ -245,38 +308,16 @@ mod tests {
245308 #[ test]
246309 #[ serial]
247310 fn test_gdb_end_to_end ( ) {
248- let out_dir = std:: env:: var ( "OUT_DIR" ) . expect ( "Failed to get out dir" ) ;
249- let manifest_dir = std:: env:: var ( "CARGO_MANIFEST_DIR" )
250- . expect ( "Failed to get manifest dir" )
251- . replace ( '\\' , "/" ) ;
252- let out_file_path = format ! ( "{out_dir}/gdb.output" ) ;
253- let cmd_file_path = format ! ( "{out_dir}/gdb-commands.txt" ) ;
254-
255- let cmd = format ! (
256- "file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
257- target remote :8080
258-
259- set pagination off
260- set logging file {out_file_path}
261- set logging enabled on
262-
263- break hyperlight_main
264- commands
265- echo \" Stopped at hyperlight_main breakpoint\\ n\"
266- backtrace
267-
268- set logging enabled off
269- detach
270- quit
271- end
272-
273- continue
274- "
311+ let ( out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths ( "gdb" ) ;
312+
313+ let cmd = single_breakpoint_script (
314+ & manifest_dir,
315+ 8080 ,
316+ & out_file_path,
317+ "hyperlight_main" ,
318+ "Stopped at hyperlight_main breakpoint" ,
275319 ) ;
276320
277- #[ cfg( windows) ]
278- let cmd = format ! ( "set osabi none\n {}" , cmd) ;
279-
280321 let checker = |contents : String | contents. contains ( "Stopped at hyperlight_main breakpoint" ) ;
281322
282323 let result = run_guest_and_gdb ( & cmd_file_path, & out_file_path, & cmd, checker) ;
@@ -288,13 +329,8 @@ mod tests {
288329 #[ test]
289330 #[ serial]
290331 fn test_gdb_sse_check ( ) {
291- let out_dir = std:: env:: var ( "OUT_DIR" ) . expect ( "Failed to get out dir" ) ;
292- let manifest_dir = std:: env:: var ( "CARGO_MANIFEST_DIR" )
293- . expect ( "Failed to get manifest dir" )
294- . replace ( '\\' , "/" ) ;
332+ let ( out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths ( "gdb-sse" ) ;
295333 println ! ( "manifest dir {manifest_dir}" ) ;
296- let out_file_path = format ! ( "{out_dir}/gdb-sse.output" ) ;
297- let cmd_file_path = format ! ( "{out_dir}/gdb-sse--commands.txt" ) ;
298334
299335 let cmd = format ! (
300336 "file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
@@ -330,4 +366,81 @@ mod tests {
330366 cleanup ( & out_file_path, & cmd_file_path) ;
331367 assert ! ( result. is_ok( ) , "{}" , result. unwrap_err( ) ) ;
332368 }
369+
370+ #[ test]
371+ #[ serial]
372+ fn test_gdb_from_snapshot ( ) {
373+ use std:: sync:: Arc ;
374+
375+ use hyperlight_host:: HostFunctions ;
376+ use hyperlight_host:: sandbox:: snapshot:: Snapshot ;
377+
378+ const PORT : u16 = 8081 ;
379+
380+ let ( out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths ( "gdb-from-snapshot" ) ;
381+ let out_dir = std:: env:: var ( "OUT_DIR" ) . unwrap ( ) ;
382+ let snap_path = format ! ( "{out_dir}/from-snapshot-debug.hls" ) ;
383+
384+ // Build a sandbox the normal way and persist its snapshot.
385+ let mut producer: MultiUseSandbox = UninitializedSandbox :: new (
386+ hyperlight_host:: GuestBinary :: FilePath (
387+ hyperlight_testing:: simple_guest_as_string ( ) . unwrap ( ) ,
388+ ) ,
389+ None ,
390+ )
391+ . unwrap ( )
392+ . evolve ( )
393+ . unwrap ( ) ;
394+ producer. snapshot ( ) . unwrap ( ) . to_file ( & snap_path) . unwrap ( ) ;
395+
396+ // Order matters. The gdb stub event loop must enter (i.e.
397+ // `VcpuStopped` must be sent on the channel) before the gdb
398+ // client connects, otherwise the wire protocol desyncs. The
399+ // evolve case gets this for free because `evolve()` runs
400+ // `vm.initialise()` which trips the entry breakpoint
401+ // immediately. For a `Call` snapshot `vm.initialise` is a
402+ // no-op, so we trigger the breakpoint by running `sbox.call`
403+ // here before the client is launched below.
404+ let snap_path_thread = snap_path. clone ( ) ;
405+ let sandbox_thread = thread:: spawn ( move || -> Result < ( ) > {
406+ let mut cfg = SandboxConfiguration :: default ( ) ;
407+ cfg. set_guest_debug_info ( DebugInfo { port : PORT } ) ;
408+
409+ let loaded = Arc :: new ( Snapshot :: from_file ( & snap_path_thread) ?) ;
410+ let mut sbox =
411+ MultiUseSandbox :: from_snapshot ( loaded, HostFunctions :: default ( ) , Some ( cfg) ) ?;
412+ sbox. call :: < i32 > (
413+ "PrintOutput" ,
414+ "Hello from a from_snapshot sandbox\n " . to_string ( ) ,
415+ ) ?;
416+ Ok ( ( ) )
417+ } ) ;
418+
419+ // Wait for the sandbox thread to bind the listener, install
420+ // the one-shot breakpoint, and trip it.
421+ thread:: sleep ( Duration :: from_secs ( 3 ) ) ;
422+
423+ let cmd = single_breakpoint_script (
424+ & manifest_dir,
425+ PORT ,
426+ & out_file_path,
427+ "main.rs:simpleguest::print_output" ,
428+ "Stopped at print_output breakpoint" ,
429+ ) ;
430+ write_cmds_file ( & cmd_file_path, & cmd) . expect ( "Failed to write gdb commands" ) ;
431+
432+ let mut gdb = spawn_gdb_client ( & cmd_file_path) ;
433+ let _ = gdb. wait ( ) ;
434+ let sandbox_result = sandbox_thread
435+ . join ( )
436+ . expect ( "from_snapshot sandbox thread panicked" ) ;
437+ let _ = std:: fs:: remove_file ( & snap_path) ;
438+
439+ let checker = |contents : String | contents. contains ( "Stopped at print_output breakpoint" ) ;
440+ let result = check_output ( & out_file_path, checker) ;
441+
442+ cleanup ( & out_file_path, & cmd_file_path) ;
443+ sandbox_result. expect ( "from_snapshot sandbox returned error" ) ;
444+ result. expect ( "gdb output missing expected breakpoint hit" ) ;
445+ }
333446}
0 commit comments