@@ -192,21 +192,36 @@ async def perform_action(self):
192192class ShutdownItem (VMActionMenuItem ):
193193 """Shutdown menu Item. When activated shutdowns the domain."""
194194
195- def __init__ (self , vm , icon_cache , force = False ):
195+ def __init__ (
196+ self ,
197+ vm ,
198+ icon_cache ,
199+ force = False ,
200+ follow_shift = True ,
201+ label = None ,
202+ force_label = None ,
203+ icon_name = "shutdown" ,
204+ ):
196205 if force :
197206 super ().__init__ (
198207 vm ,
199- label = _ ("Force shutdown" ),
208+ label = force_label or _ ("Force shutdown" ),
200209 icon_cache = icon_cache ,
201- icon_name = "shutdown" ,
210+ icon_name = icon_name ,
202211 )
203212 else :
204213 super ().__init__ (
205- vm , label = _ ("Shutdown" ), icon_cache = icon_cache , icon_name = "shutdown"
214+ vm ,
215+ label = label or _ ("Shutdown" ),
216+ icon_cache = icon_cache ,
217+ icon_name = icon_name ,
206218 )
207219 self .force = force
220+ self .follow_shift = follow_shift
208221
209222 def set_force (self , force ):
223+ if not self .follow_shift :
224+ return
210225 self .force = force
211226 if self .force :
212227 self .label .set_text (_ ("Force shutdown" ))
@@ -226,89 +241,97 @@ async def perform_action(self):
226241 ).format (self .vm .name , str (ex )),
227242 )
228243 return
229- if isinstance (ex , exc .QubesVMInUseError ):
230- title = _ ("Qube {0} is in use" ).format (self .vm .name )
231- markup = _ (
232- "The qube <b>{0}</b> could not be shut down because it "
233- "is in use by connected qubes.\n \n "
234- "<b>Warning:</b> force shutdown may cause unexpected "
235- "issues in connected qubes."
236- ).format (self .vm .name )
237- button_label = _ ("Force shutdown" )
238- action = "force"
239- elif isinstance (ex , exc .QubesVMShutdownTimeoutError ):
240- title = _ ("Qube {0} shutdown timed out" ).format (self .vm .name )
241- markup = _ (
242- "The qube <b>{0}</b> did not shut down within the "
243- "expected time.\n \n You can retry the shutdown or, "
244- "if the problem persists, kill the qube."
245- ).format (self .vm .name )
246- button_label = None # timeout uses custom buttons below
247- action = "timeout"
248- else :
249- title = _ ("Error shutting down qube {0}" ).format (self .vm .name )
250- markup = _ (
251- "The qube <b>{0}</b> could not be shut down. "
252- "The following error occurred:\n "
253- "<tt>{1}</tt>\n \n "
254- "Would you like to kill the qube?"
255- ).format (self .vm .name , str (ex ))
256- button_label = _ ("Kill" )
257- action = "kill"
258-
259- dialog = Gtk .MessageDialog (
260- None , 0 , Gtk .MessageType .ERROR , Gtk .ButtonsType .NONE
261- )
262- dialog .set_title (title )
263- dialog .set_markup (markup )
264- dialog .add_button (_ ("Cancel" ), Gtk .ResponseType .CANCEL )
265- if action == "timeout" :
266- dialog .add_button (_ ("Retry shutdown" ), Gtk .ResponseType .OK )
267- dialog .add_button (_ ("Kill" ), Gtk .ResponseType .YES )
268- else :
269- dialog .add_button (button_label , Gtk .ResponseType .OK )
270- dialog .connect ("response" , self .react_to_question , action )
271- GLib .idle_add (dialog .show )
244+ self .show_shutdown_dialog (ex )
245+
246+ def get_shutdown_question (self , ex ):
247+ if isinstance (ex , exc .QubesVMInUseError ):
248+ title = _ ("Qube {0} is in use" ).format (self .vm .name )
249+ markup = _ (
250+ "The qube <b>{0}</b> could not be shut down because it "
251+ "is in use by connected qubes.\n \n "
252+ "<b>Warning:</b> force shutdown may cause unexpected "
253+ "issues in connected qubes."
254+ ).format (self .vm .name )
255+ action = "force"
256+ elif isinstance (ex , exc .QubesVMShutdownTimeoutError ):
257+ title = _ ("Qube {0} shutdown timed out" ).format (self .vm .name )
258+ markup = _ (
259+ "The qube <b>{0}</b> did not shut down within the "
260+ "expected time.\n \n You can retry the shutdown or, "
261+ "if the problem persists, kill the qube."
262+ ).format (self .vm .name )
263+ action = "timeout"
264+ else :
265+ title = _ ("Error shutting down qube {0}" ).format (self .vm .name )
266+ markup = _ (
267+ "The qube <b>{0}</b> could not be shut down. "
268+ "The following error occurred:\n "
269+ "<tt>{1}</tt>\n \n "
270+ "Would you like to kill the qube?"
271+ ).format (self .vm .name , str (ex ))
272+ action = "kill"
273+ return title , markup , action
274+
275+ def show_shutdown_dialog (self , ex ):
276+ title , markup , action = self .get_shutdown_question (ex )
277+ dialog = Gtk .MessageDialog (None , 0 , Gtk .MessageType .ERROR , Gtk .ButtonsType .NONE )
278+ dialog .set_title (title )
279+ dialog .set_markup (markup )
280+ dialog .add_button (_ ("Cancel" ), Gtk .ResponseType .CANCEL )
281+ if action == "force" :
282+ dialog .add_button (_ ("Force shutdown" ), Gtk .ResponseType .OK )
283+ elif action == "timeout" :
284+ dialog .add_button (_ ("Retry shutdown" ), Gtk .ResponseType .OK )
285+ dialog .add_button (_ ("Kill" ), Gtk .ResponseType .YES )
286+ elif action == "kill" :
287+ dialog .add_button (_ ("Kill" ), Gtk .ResponseType .OK )
288+ dialog .connect ("response" , self .react_to_question , action )
289+ GLib .idle_add (dialog .show )
272290
273291 def react_to_question (self , widget , response , action ):
274292 if response not in (Gtk .ResponseType .OK , Gtk .ResponseType .YES ):
275293 widget .destroy ()
276294 return
277295 try :
278- if action == "force" :
279- self .vm .shutdown (force = True )
280- elif action == "timeout" :
281- if response == Gtk .ResponseType .YES :
282- self .vm .kill ()
283- elif response == Gtk .ResponseType .OK :
284- self .vm .shutdown (force = False )
285- elif action == "kill" and response == Gtk .ResponseType .OK :
286- self .vm .kill ()
296+ self .do_shutdown_dialog_action (response , action )
287297 except exc .QubesException as ex :
288- show_error (
289- _ ("Error shutting down qube" ),
290- _ (
291- "The following error occurred while attempting to "
292- "shut down qube {0}:\n {1}"
293- ).format (self .vm .name , str (ex )),
294- )
298+ if action == "timeout" and response == Gtk .ResponseType .OK :
299+ self .show_shutdown_dialog (ex )
300+ else :
301+ show_error (
302+ _ ("Error shutting down qube" ),
303+ _ (
304+ "The following error occurred while attempting to "
305+ "shut down qube {0}:\n {1}"
306+ ).format (self .vm .name , str (ex )),
307+ )
295308 widget .destroy ()
296309
310+ def do_shutdown_dialog_action (self , response , action ):
311+ if action == "force" :
312+ self .vm .shutdown (force = True )
313+ elif action == "timeout" :
314+ if response == Gtk .ResponseType .YES :
315+ self .vm .kill ()
316+ elif response == Gtk .ResponseType .OK :
317+ self .vm .shutdown (force = False )
318+ elif action == "kill" and response == Gtk .ResponseType .OK :
319+ self .vm .kill ()
320+
297321
298- class RestartItem (VMActionMenuItem ):
322+ class RestartItem (ShutdownItem ):
299323 """Restart menu Item. When activated shutdowns the domain and
300324 then starts it again."""
301325
302326 def __init__ (self , vm , icon_cache , force = False ):
303- if force :
304- super ().__init__ (
305- vm , label = _ ("Force restart" ), icon_cache = icon_cache , icon_name = "restart"
306- )
307- else :
308- super ().__init__ (
309- vm , label = _ ("Restart" ), icon_cache = icon_cache , icon_name = "restart"
310- )
311- self .force = force
327+ super ().__init__ (
328+ vm ,
329+ icon_cache ,
330+ force = force ,
331+ label = _ ("Restart" ),
332+ force_label = _ ("Force restart" ),
333+ icon_name = "restart" ,
334+ )
312335 self .give_up = False
313336
314337 def set_force (self , force ):
@@ -332,19 +355,7 @@ async def perform_action(self, *_args, **_kwargs):
332355 ).format (self .vm .name , str (ex )),
333356 )
334357 return
335- dialog = Gtk .MessageDialog (
336- None , 0 , Gtk .MessageType .ERROR , Gtk .ButtonsType .OK_CANCEL
337- )
338- dialog .set_title ("Error restarting qube" )
339- dialog .set_markup (
340- f"The qube { self .vm .name } couldn't be shut down "
341- "normally. The following error occurred: \n "
342- f"<tt>{ str (ex )} </tt>\n \n "
343- "Do you want to force shutdown? \n \n <b>Warning:</b> "
344- "this may cause unexpected issues in connected qubes."
345- )
346- dialog .connect ("response" , self .react_to_question )
347- GLib .idle_add (dialog .show )
358+ self .show_shutdown_dialog (ex )
348359
349360 try :
350361 while self .vm .is_running ():
@@ -366,11 +377,17 @@ async def perform_action(self, *_args, **_kwargs):
366377 ).format (self .vm .name , str (ex )),
367378 )
368379
369- def react_to_question (self , widget , response ):
370- if response == Gtk .ResponseType .OK :
371- try :
372- self .vm .shutdown (force = True )
373- except exc .QubesException as ex :
380+ def react_to_question (self , widget , response , action ):
381+ if response not in (Gtk .ResponseType .OK , Gtk .ResponseType .YES ):
382+ self .give_up = True
383+ widget .destroy ()
384+ return
385+ try :
386+ self .do_shutdown_dialog_action (response , action )
387+ except exc .QubesException as ex :
388+ if action == "timeout" and response == Gtk .ResponseType .OK :
389+ self .show_shutdown_dialog (ex )
390+ else :
374391 show_error (
375392 _ ("Error shutting down qube" ),
376393 _ (
@@ -379,8 +396,6 @@ def react_to_question(self, widget, response):
379396 ).format (self .vm .name , str (ex )),
380397 )
381398 self .give_up = True
382- else :
383- self .give_up = True
384399 widget .destroy ()
385400
386401
@@ -547,7 +562,7 @@ def __init__(self, is_preload=False):
547562class StartedMenu (Gtk .Menu ):
548563 """The sub-menu for a started domain"""
549564
550- def __init__ (self , vm , app , icon_cache ):
565+ def __init__ (self , vm , app , icon_cache , shutdown_failed = False ):
551566 super ().__init__ ()
552567 self .vm = vm
553568 self .app = app
@@ -561,7 +576,17 @@ def __init__(self, vm, app, icon_cache):
561576
562577 self .add (PreferencesItem (self .vm , icon_cache ))
563578 self .add (PauseItem (self .vm , icon_cache ))
564- self .add (ShutdownItem (self .vm , icon_cache , force = app .shift_pressed ))
579+ self .add (
580+ ShutdownItem (
581+ self .vm ,
582+ icon_cache ,
583+ force = app .shift_pressed and not shutdown_failed ,
584+ follow_shift = not shutdown_failed ,
585+ )
586+ )
587+ if shutdown_failed :
588+ self .add (ShutdownItem (self .vm , icon_cache , force = True , follow_shift = False ))
589+ self .add (KillItem (self .vm , icon_cache ))
565590 if self .vm .klass != "DispVM" or not self .vm .auto_cleanup :
566591 self .add (RestartItem (self .vm , icon_cache , force = app .shift_pressed ))
567592
@@ -709,6 +734,7 @@ def __init__(self, vm, app, icon_cache, state=None):
709734 self .vm = vm
710735 self .app = app
711736 self .icon_cache = icon_cache
737+ self .shutdown_failed = False
712738 self .decorator = qui .decorators .DomainDecorator (vm )
713739
714740 # Main horizontal box
@@ -766,7 +792,12 @@ def _set_submenu(self, state):
766792 is_preload = is_preload ,
767793 )
768794 elif state == "Running" :
769- submenu = StartedMenu (self .vm , self .app , self .icon_cache )
795+ submenu = StartedMenu (
796+ self .vm ,
797+ self .app ,
798+ self .icon_cache ,
799+ shutdown_failed = self .shutdown_failed ,
800+ )
770801 elif state == "Paused" :
771802 submenu = PausedMenu (self .vm , self .icon_cache )
772803 else :
@@ -1182,6 +1213,11 @@ def update_domain_item(self, vm, event, **kwargs):
11821213 # it's a fragile DispVM
11831214 state = "Transient"
11841215
1216+ if event == "domain-shutdown-failed" :
1217+ item .shutdown_failed = True
1218+ elif event in ("domain-start" , "domain-pre-start" , "domain-shutdown" ):
1219+ item .shutdown_failed = False
1220+
11851221 item .update_state (state )
11861222
11871223 if event == "property-reset:is_preload" :
0 commit comments