@@ -192,7 +192,7 @@ 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__ (self , vm , icon_cache , force = False , follow_shift = True ):
196196 if force :
197197 super ().__init__ (
198198 vm ,
@@ -205,8 +205,11 @@ def __init__(self, vm, icon_cache, force=False):
205205 vm , label = _ ("Shutdown" ), icon_cache = icon_cache , icon_name = "shutdown"
206206 )
207207 self .force = force
208+ self .follow_shift = follow_shift
208209
209210 def set_force (self , force ):
211+ if not self .follow_shift :
212+ return
210213 self .force = force
211214 if self .force :
212215 self .label .set_text (_ ("Force shutdown" ))
@@ -332,18 +335,47 @@ async def perform_action(self, *_args, **_kwargs):
332335 ).format (self .vm .name , str (ex )),
333336 )
334337 return
338+ if isinstance (ex , exc .QubesVMInUseError ):
339+ title = _ ("Qube {0} is in use" ).format (self .vm .name )
340+ markup = _ (
341+ "The qube <b>{0}</b> could not be shut down because it "
342+ "is in use by connected qubes.\n \n "
343+ "<b>Warning:</b> force shutdown may cause unexpected "
344+ "issues in connected qubes."
345+ ).format (self .vm .name )
346+ action = "force"
347+ elif isinstance (ex , exc .QubesVMShutdownTimeoutError ):
348+ title = _ ("Qube {0} shutdown timed out" ).format (self .vm .name )
349+ markup = _ (
350+ "The qube <b>{0}</b> did not shut down within the "
351+ "expected time.\n \n You can retry the shutdown or, "
352+ "if the problem persists, kill the qube."
353+ ).format (self .vm .name )
354+ action = "timeout"
355+ else :
356+ title = _ ("Error restarting qube {0}" ).format (self .vm .name )
357+ markup = _ (
358+ "The qube <b>{0}</b> could not be shut down. "
359+ "The following error occurred:\n "
360+ "<tt>{1}</tt>\n \n "
361+ "Would you like to kill the qube?"
362+ ).format (self .vm .name , str (ex ))
363+ action = "kill"
364+
335365 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."
366+ None , 0 , Gtk .MessageType .ERROR , Gtk .ButtonsType .NONE
345367 )
346- dialog .connect ("response" , self .react_to_question )
368+ dialog .set_title (title )
369+ dialog .set_markup (markup )
370+ dialog .add_button (_ ("Cancel" ), Gtk .ResponseType .CANCEL )
371+ if action == "force" :
372+ dialog .add_button (_ ("Force shutdown" ), Gtk .ResponseType .OK )
373+ elif action == "timeout" :
374+ dialog .add_button (_ ("Retry shutdown" ), Gtk .ResponseType .OK )
375+ dialog .add_button (_ ("Kill" ), Gtk .ResponseType .YES )
376+ elif action == "kill" :
377+ dialog .add_button (_ ("Kill" ), Gtk .ResponseType .OK )
378+ dialog .connect ("response" , self .react_to_question , action )
347379 GLib .idle_add (dialog .show )
348380
349381 try :
@@ -366,20 +398,29 @@ async def perform_action(self, *_args, **_kwargs):
366398 ).format (self .vm .name , str (ex )),
367399 )
368400
369- def react_to_question (self , widget , response ):
370- if response == Gtk .ResponseType .OK :
371- try :
401+ def react_to_question (self , widget , response , action ):
402+ if response not in (Gtk .ResponseType .OK , Gtk .ResponseType .YES ):
403+ self .give_up = True
404+ widget .destroy ()
405+ return
406+ try :
407+ if action == "force" :
372408 self .vm .shutdown (force = True )
373- except exc .QubesException as ex :
374- show_error (
375- _ ("Error shutting down qube" ),
376- _ (
377- "The following error occurred while attempting to "
378- "shut down qube {0}:\n {1}"
379- ).format (self .vm .name , str (ex )),
380- )
381- self .give_up = True
382- else :
409+ elif action == "timeout" :
410+ if response == Gtk .ResponseType .YES :
411+ self .vm .kill ()
412+ elif response == Gtk .ResponseType .OK :
413+ self .vm .shutdown (force = False )
414+ elif action == "kill" and response == Gtk .ResponseType .OK :
415+ self .vm .kill ()
416+ except exc .QubesException as ex :
417+ show_error (
418+ _ ("Error shutting down qube" ),
419+ _ (
420+ "The following error occurred while attempting to "
421+ "shut down qube {0}:\n {1}"
422+ ).format (self .vm .name , str (ex )),
423+ )
383424 self .give_up = True
384425 widget .destroy ()
385426
@@ -547,7 +588,7 @@ def __init__(self, is_preload=False):
547588class StartedMenu (Gtk .Menu ):
548589 """The sub-menu for a started domain"""
549590
550- def __init__ (self , vm , app , icon_cache ):
591+ def __init__ (self , vm , app , icon_cache , shutdown_failed = False ):
551592 super ().__init__ ()
552593 self .vm = vm
553594 self .app = app
@@ -561,7 +602,17 @@ def __init__(self, vm, app, icon_cache):
561602
562603 self .add (PreferencesItem (self .vm , icon_cache ))
563604 self .add (PauseItem (self .vm , icon_cache ))
564- self .add (ShutdownItem (self .vm , icon_cache , force = app .shift_pressed ))
605+ self .add (
606+ ShutdownItem (
607+ self .vm ,
608+ icon_cache ,
609+ force = app .shift_pressed and not shutdown_failed ,
610+ follow_shift = not shutdown_failed ,
611+ )
612+ )
613+ if shutdown_failed :
614+ self .add (ShutdownItem (self .vm , icon_cache , force = True , follow_shift = False ))
615+ self .add (KillItem (self .vm , icon_cache ))
565616 if self .vm .klass != "DispVM" or not self .vm .auto_cleanup :
566617 self .add (RestartItem (self .vm , icon_cache , force = app .shift_pressed ))
567618
@@ -709,6 +760,7 @@ def __init__(self, vm, app, icon_cache, state=None):
709760 self .vm = vm
710761 self .app = app
711762 self .icon_cache = icon_cache
763+ self .shutdown_failed = False
712764 self .decorator = qui .decorators .DomainDecorator (vm )
713765
714766 # Main horizontal box
@@ -766,7 +818,12 @@ def _set_submenu(self, state):
766818 is_preload = is_preload ,
767819 )
768820 elif state == "Running" :
769- submenu = StartedMenu (self .vm , self .app , self .icon_cache )
821+ submenu = StartedMenu (
822+ self .vm ,
823+ self .app ,
824+ self .icon_cache ,
825+ shutdown_failed = self .shutdown_failed ,
826+ )
770827 elif state == "Paused" :
771828 submenu = PausedMenu (self .vm , self .icon_cache )
772829 else :
@@ -1182,6 +1239,16 @@ def update_domain_item(self, vm, event, **kwargs):
11821239 # it's a fragile DispVM
11831240 state = "Transient"
11841241
1242+ if event == "domain-shutdown-failed" :
1243+ item .shutdown_failed = True
1244+ elif event in (
1245+ "domain-start" ,
1246+ "domain-pre-start" ,
1247+ "domain-shutdown" ,
1248+ "domain-pre-shutdown" ,
1249+ ):
1250+ item .shutdown_failed = False
1251+
11851252 item .update_state (state )
11861253
11871254 if event == "property-reset:is_preload" :
0 commit comments