@@ -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
@@ -538,7 +579,7 @@ def __init__(self):
538579class StartedMenu (Gtk .Menu ):
539580 """The sub-menu for a started domain"""
540581
541- def __init__ (self , vm , app , icon_cache ):
582+ def __init__ (self , vm , app , icon_cache , shutdown_failed = False ):
542583 super ().__init__ ()
543584 self .vm = vm
544585 self .app = app
@@ -552,7 +593,17 @@ def __init__(self, vm, app, icon_cache):
552593
553594 self .add (PreferencesItem (self .vm , icon_cache ))
554595 self .add (PauseItem (self .vm , icon_cache ))
555- self .add (ShutdownItem (self .vm , icon_cache , force = app .shift_pressed ))
596+ self .add (
597+ ShutdownItem (
598+ self .vm ,
599+ icon_cache ,
600+ force = app .shift_pressed and not shutdown_failed ,
601+ follow_shift = not shutdown_failed ,
602+ )
603+ )
604+ if shutdown_failed :
605+ self .add (ShutdownItem (self .vm , icon_cache , force = True , follow_shift = False ))
606+ self .add (KillItem (self .vm , icon_cache ))
556607 if self .vm .klass != "DispVM" or not self .vm .auto_cleanup :
557608 self .add (RestartItem (self .vm , icon_cache , force = app .shift_pressed ))
558609
@@ -700,6 +751,7 @@ def __init__(self, vm, app, icon_cache, state=None):
700751 self .vm = vm
701752 self .app = app
702753 self .icon_cache = icon_cache
754+ self .shutdown_failed = False
703755 self .decorator = qui .decorators .DomainDecorator (vm )
704756
705757 # Main horizontal box
@@ -753,7 +805,12 @@ def _set_submenu(self, state):
753805 self .vm , self .icon_cache , working_correctly = (state == "Running" )
754806 )
755807 elif state == "Running" :
756- submenu = StartedMenu (self .vm , self .app , self .icon_cache )
808+ submenu = StartedMenu (
809+ self .vm ,
810+ self .app ,
811+ self .icon_cache ,
812+ shutdown_failed = self .shutdown_failed ,
813+ )
757814 elif state == "Paused" :
758815 submenu = PausedMenu (self .vm , self .icon_cache )
759816 else :
@@ -1166,6 +1223,16 @@ def update_domain_item(self, vm, event, **kwargs):
11661223 # it's a fragile DispVM
11671224 state = "Transient"
11681225
1226+ if event == "domain-shutdown-failed" :
1227+ item .shutdown_failed = True
1228+ elif event in (
1229+ "domain-start" ,
1230+ "domain-pre-start" ,
1231+ "domain-shutdown" ,
1232+ "domain-pre-shutdown" ,
1233+ ):
1234+ item .shutdown_failed = False
1235+
11691236 item .update_state (state )
11701237
11711238 if event == "domain-shutdown" :
0 commit comments