@@ -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,74 @@ 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 hyperlight_host:: HostFunctions ;
374+
375+ const PORT : u16 = 8081 ;
376+
377+ let ( out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths ( "gdb-from-snapshot" ) ;
378+
379+ // Build a sandbox the normal way and snapshot it in-memory.
380+ let mut producer: MultiUseSandbox = UninitializedSandbox :: new (
381+ hyperlight_host:: GuestBinary :: FilePath (
382+ hyperlight_testing:: simple_guest_as_string ( ) . unwrap ( ) ,
383+ ) ,
384+ None ,
385+ )
386+ . unwrap ( )
387+ . evolve ( )
388+ . unwrap ( ) ;
389+ let snap = producer. snapshot ( ) . unwrap ( ) ;
390+
391+ // Order matters. The gdb stub event loop must enter (i.e.
392+ // `VcpuStopped` must be sent on the channel) before the gdb
393+ // client connects, otherwise the wire protocol desyncs. The
394+ // evolve case gets this for free because `evolve()` runs
395+ // `vm.initialise()` which trips the entry breakpoint
396+ // immediately. For a `Call` snapshot `vm.initialise` is a
397+ // no-op, so we trigger the breakpoint by running `sbox.call`
398+ // here before the client is launched below.
399+ let snap_thread = snap. clone ( ) ;
400+ let sandbox_thread = thread:: spawn ( move || -> Result < ( ) > {
401+ let mut cfg = SandboxConfiguration :: default ( ) ;
402+ cfg. set_guest_debug_info ( DebugInfo { port : PORT } ) ;
403+
404+ let mut sbox =
405+ MultiUseSandbox :: from_snapshot ( snap_thread, HostFunctions :: default ( ) , Some ( cfg) ) ?;
406+ sbox. call :: < i32 > (
407+ "PrintOutput" ,
408+ "Hello from a from_snapshot sandbox\n " . to_string ( ) ,
409+ ) ?;
410+ Ok ( ( ) )
411+ } ) ;
412+
413+ // Wait for the sandbox thread to bind the listener, install
414+ // the one-shot breakpoint, and trip it.
415+ thread:: sleep ( Duration :: from_secs ( 3 ) ) ;
416+
417+ let cmd = single_breakpoint_script (
418+ & manifest_dir,
419+ PORT ,
420+ & out_file_path,
421+ "main.rs:simpleguest::print_output" ,
422+ "Stopped at print_output breakpoint" ,
423+ ) ;
424+ write_cmds_file ( & cmd_file_path, & cmd) . expect ( "Failed to write gdb commands" ) ;
425+
426+ let mut gdb = spawn_gdb_client ( & cmd_file_path) ;
427+ let _ = gdb. wait ( ) ;
428+ let sandbox_result = sandbox_thread
429+ . join ( )
430+ . expect ( "from_snapshot sandbox thread panicked" ) ;
431+
432+ let checker = |contents : String | contents. contains ( "Stopped at print_output breakpoint" ) ;
433+ let result = check_output ( & out_file_path, checker) ;
434+
435+ cleanup ( & out_file_path, & cmd_file_path) ;
436+ sandbox_result. expect ( "from_snapshot sandbox returned error" ) ;
437+ result. expect ( "gdb output missing expected breakpoint hit" ) ;
438+ }
333439}
0 commit comments