@@ -105,33 +105,34 @@ func (s *Starter) GetVersion(p *paths.Paths) (string, error) {
105105 return "" , fmt .Errorf ("could not parse QEMU version from: %s" , string (output ))
106106}
107107
108- // StartVM launches QEMU with the VM configuration and returns a Hypervisor client.
109- // QEMU receives all configuration via command-line arguments at process start.
110- func (s * Starter ) StartVM (ctx context.Context , p * paths.Paths , version string , socketPath string , config hypervisor.VMConfig ) (int , hypervisor.Hypervisor , error ) {
108+ // buildQMPArgs returns the base QMP socket arguments for QEMU.
109+ func buildQMPArgs (socketPath string ) []string {
110+ return []string {
111+ "-chardev" , fmt .Sprintf ("socket,id=qmp,path=%s,server=on,wait=off" , socketPath ),
112+ "-mon" , "chardev=qmp,mode=control" ,
113+ }
114+ }
115+
116+ // startQEMUProcess handles the common QEMU process startup logic.
117+ // Returns the PID, hypervisor client, and a cleanup function.
118+ // The cleanup function must be called on error; call cleanup.Release() on success.
119+ func (s * Starter ) startQEMUProcess (ctx context.Context , p * paths.Paths , version string , socketPath string , args []string ) (int , * QEMU , * cleanup.Cleanup , error ) {
111120 log := logger .FromContext (ctx )
112121
113122 // Get binary path
114123 binaryPath , err := s .GetBinaryPath (p , version )
115124 if err != nil {
116- return 0 , nil , fmt .Errorf ("get binary: %w" , err )
125+ return 0 , nil , nil , fmt .Errorf ("get binary: %w" , err )
117126 }
118127
119128 // Check if socket is already in use
120129 if isSocketInUse (socketPath ) {
121- return 0 , nil , fmt .Errorf ("socket already in use, QEMU may be running at %s" , socketPath )
130+ return 0 , nil , nil , fmt .Errorf ("socket already in use, QEMU may be running at %s" , socketPath )
122131 }
123132
124133 // Remove stale socket if exists
125134 os .Remove (socketPath )
126135
127- // Build command arguments: QMP socket + VM configuration
128- args := []string {
129- "-chardev" , fmt .Sprintf ("socket,id=qmp,path=%s,server=on,wait=off" , socketPath ),
130- "-mon" , "chardev=qmp,mode=control" ,
131- }
132- // Append VM configuration as command-line arguments
133- args = append (args , BuildArgs (config )... )
134-
135136 // Create command
136137 cmd := exec .Command (binaryPath , args ... )
137138
@@ -144,7 +145,7 @@ func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, s
144145 instanceDir := filepath .Dir (socketPath )
145146 logsDir := filepath .Join (instanceDir , "logs" )
146147 if err := os .MkdirAll (logsDir , 0755 ); err != nil {
147- return 0 , nil , fmt .Errorf ("create logs directory: %w" , err )
148+ return 0 , nil , nil , fmt .Errorf ("create logs directory: %w" , err )
148149 }
149150
150151 vmmLogFile , err := os .OpenFile (
@@ -153,48 +154,71 @@ func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, s
153154 0644 ,
154155 )
155156 if err != nil {
156- return 0 , nil , fmt .Errorf ("create vmm log: %w" , err )
157+ return 0 , nil , nil , fmt .Errorf ("create vmm log: %w" , err )
157158 }
158159 defer vmmLogFile .Close ()
159160
160161 cmd .Stdout = vmmLogFile
161162 cmd .Stderr = vmmLogFile
162163
164+ processStartTime := time .Now ()
163165 if err := cmd .Start (); err != nil {
164- return 0 , nil , fmt .Errorf ("start qemu: %w" , err )
166+ return 0 , nil , nil , fmt .Errorf ("start qemu: %w" , err )
165167 }
166168
167169 pid := cmd .Process .Pid
170+ log .DebugContext (ctx , "QEMU process started" , "pid" , pid , "duration_ms" , time .Since (processStartTime ).Milliseconds ())
168171
169172 // Setup cleanup to kill the process if subsequent steps fail
170173 cu := cleanup .Make (func () {
171174 syscall .Kill (pid , syscall .SIGKILL )
172175 })
173- defer cu .Clean ()
174176
175177 // Wait for socket to be ready
178+ socketWaitStart := time .Now ()
176179 if err := waitForSocket (socketPath , socketWaitTimeout ); err != nil {
180+ cu .Clean ()
177181 vmmLogPath := filepath .Join (logsDir , "vmm.log" )
178182 if logData , readErr := os .ReadFile (vmmLogPath ); readErr == nil && len (logData ) > 0 {
179- return 0 , nil , fmt .Errorf ("%w; vmm.log: %s" , err , string (logData ))
183+ return 0 , nil , nil , fmt .Errorf ("%w; vmm.log: %s" , err , string (logData ))
180184 }
181- return 0 , nil , err
185+ return 0 , nil , nil , err
182186 }
187+ log .DebugContext (ctx , "QMP socket ready" , "duration_ms" , time .Since (socketWaitStart ).Milliseconds ())
183188
184189 // Create QMP client
185190 hv , err := New (socketPath )
186191 if err != nil {
187- return 0 , nil , fmt .Errorf ("create client: %w" , err )
192+ cu .Clean ()
193+ return 0 , nil , nil , fmt .Errorf ("create client: %w" , err )
188194 }
189195
196+ return pid , hv , & cu , nil
197+ }
198+
199+ // StartVM launches QEMU with the VM configuration and returns a Hypervisor client.
200+ // QEMU receives all configuration via command-line arguments at process start.
201+ func (s * Starter ) StartVM (ctx context.Context , p * paths.Paths , version string , socketPath string , config hypervisor.VMConfig ) (int , hypervisor.Hypervisor , error ) {
202+ log := logger .FromContext (ctx )
203+
204+ // Build command arguments: QMP socket + VM configuration
205+ args := buildQMPArgs (socketPath )
206+ args = append (args , BuildArgs (config )... )
207+
208+ pid , hv , cu , err := s .startQEMUProcess (ctx , p , version , socketPath , args )
209+ if err != nil {
210+ return 0 , nil , err
211+ }
212+ defer cu .Clean ()
213+
190214 // Save config for potential restore later
191215 // QEMU migration files only contain memory state, not device config
216+ instanceDir := filepath .Dir (socketPath )
192217 if err := saveVMConfig (instanceDir , config ); err != nil {
193218 // Non-fatal - restore just won't work
194219 log .WarnContext (ctx , "failed to save VM config for restore" , "error" , err )
195220 }
196221
197- // Success - release cleanup to prevent killing the process
198222 cu .Release ()
199223 return pid , hv , nil
200224}
@@ -205,20 +229,6 @@ func (s *Starter) RestoreVM(ctx context.Context, p *paths.Paths, version string,
205229 log := logger .FromContext (ctx )
206230 startTime := time .Now ()
207231
208- // Get binary path
209- binaryPath , err := s .GetBinaryPath (p , version )
210- if err != nil {
211- return 0 , nil , fmt .Errorf ("get binary: %w" , err )
212- }
213-
214- // Check if socket is already in use
215- if isSocketInUse (socketPath ) {
216- return 0 , nil , fmt .Errorf ("socket already in use, QEMU may be running at %s" , socketPath )
217- }
218-
219- // Remove stale socket if exists
220- os .Remove (socketPath )
221-
222232 // Load saved VM config from snapshot directory
223233 // QEMU requires exact same command-line args as when snapshot was taken
224234 configLoadStart := time .Now ()
@@ -228,14 +238,8 @@ func (s *Starter) RestoreVM(ctx context.Context, p *paths.Paths, version string,
228238 }
229239 log .DebugContext (ctx , "loaded VM config from snapshot" , "duration_ms" , time .Since (configLoadStart ).Milliseconds ())
230240
231- instanceDir := filepath .Dir (socketPath )
232-
233241 // Build command arguments: QMP socket + VM configuration + incoming migration
234- args := []string {
235- "-chardev" , fmt .Sprintf ("socket,id=qmp,path=%s,server=on,wait=off" , socketPath ),
236- "-mon" , "chardev=qmp,mode=control" ,
237- }
238- // Append VM configuration as command-line arguments
242+ args := buildQMPArgs (socketPath )
239243 args = append (args , BuildArgs (config )... )
240244
241245 // Add incoming migration flag to restore from snapshot
@@ -244,63 +248,11 @@ func (s *Starter) RestoreVM(ctx context.Context, p *paths.Paths, version string,
244248 incomingURI := "exec:cat < " + memoryFile
245249 args = append (args , "-incoming" , incomingURI )
246250
247- // Create command
248- cmd := exec .Command (binaryPath , args ... )
249-
250- // Daemonize: detach from parent process group
251- cmd .SysProcAttr = & syscall.SysProcAttr {
252- Setpgid : true ,
253- }
254-
255- // Redirect stdout/stderr to VMM log file
256- logsDir := filepath .Join (instanceDir , "logs" )
257- if err := os .MkdirAll (logsDir , 0755 ); err != nil {
258- return 0 , nil , fmt .Errorf ("create logs directory: %w" , err )
259- }
260-
261- vmmLogFile , err := os .OpenFile (
262- filepath .Join (logsDir , "vmm.log" ),
263- os .O_CREATE | os .O_WRONLY | os .O_APPEND ,
264- 0644 ,
265- )
251+ pid , hv , cu , err := s .startQEMUProcess (ctx , p , version , socketPath , args )
266252 if err != nil {
267- return 0 , nil , fmt .Errorf ("create vmm log: %w" , err )
268- }
269- defer vmmLogFile .Close ()
270-
271- cmd .Stdout = vmmLogFile
272- cmd .Stderr = vmmLogFile
273-
274- processStartTime := time .Now ()
275- if err := cmd .Start (); err != nil {
276- return 0 , nil , fmt .Errorf ("start qemu: %w" , err )
277- }
278-
279- pid := cmd .Process .Pid
280- log .DebugContext (ctx , "QEMU process started" , "pid" , pid , "duration_ms" , time .Since (processStartTime ).Milliseconds ())
281-
282- // Setup cleanup to kill the process if subsequent steps fail
283- cu := cleanup .Make (func () {
284- syscall .Kill (pid , syscall .SIGKILL )
285- })
286- defer cu .Clean ()
287-
288- // Wait for socket to be ready
289- socketWaitStart := time .Now ()
290- if err := waitForSocket (socketPath , 10 * time .Second ); err != nil {
291- vmmLogPath := filepath .Join (logsDir , "vmm.log" )
292- if logData , readErr := os .ReadFile (vmmLogPath ); readErr == nil && len (logData ) > 0 {
293- return 0 , nil , fmt .Errorf ("%w; vmm.log: %s" , err , string (logData ))
294- }
295253 return 0 , nil , err
296254 }
297- log .DebugContext (ctx , "QMP socket ready" , "duration_ms" , time .Since (socketWaitStart ).Milliseconds ())
298-
299- // Create QMP client
300- hv , err := New (socketPath )
301- if err != nil {
302- return 0 , nil , fmt .Errorf ("create client: %w" , err )
303- }
255+ defer cu .Clean ()
304256
305257 // Wait for incoming migration to complete
306258 // QEMU loads the migration data from the exec subprocess
@@ -311,7 +263,6 @@ func (s *Starter) RestoreVM(ctx context.Context, p *paths.Paths, version string,
311263 }
312264 log .DebugContext (ctx , "migration complete" , "duration_ms" , time .Since (migrationWaitStart ).Milliseconds ())
313265
314- // Success - release cleanup to prevent killing the process
315266 cu .Release ()
316267 log .DebugContext (ctx , "QEMU restore complete" , "pid" , pid , "total_duration_ms" , time .Since (startTime ).Milliseconds ())
317268 return pid , hv , nil
0 commit comments