diff --git a/qui/tray/domains.py b/qui/tray/domains.py
index aaaef375..fd5c6817 100644
--- a/qui/tray/domains.py
+++ b/qui/tray/domains.py
@@ -10,7 +10,6 @@
import asyncio
import os
-import subprocess
import sys
import traceback
import abc
@@ -117,12 +116,8 @@ def __init__(self, label, img=None, icon_cache=None, icon_name=None):
box.pack_start(placeholder, False, False, 0)
# Add a label to the menu item
- label_widget = label
- if isinstance(label_widget, Gtk.Label):
- label_widget.set_xalign(0)
- else:
- label_widget = Gtk.Label(label=label, xalign=0)
- box.pack_start(label_widget, True, True, 0)
+ self.label = Gtk.Label(label=label, xalign=0)
+ box.pack_start(self.label, True, True, 0)
# Add the box to the menu item
self.add(box)
@@ -197,41 +192,127 @@ async def perform_action(self):
class ShutdownItem(VMActionMenuItem):
"""Shutdown menu Item. When activated shutdowns the domain."""
- def __init__(self, vm, icon_cache):
- super().__init__(
- vm, label=_("Shutdown"), icon_cache=icon_cache, icon_name="shutdown"
- )
+ def __init__(self, vm, icon_cache, force=False):
+ if force:
+ super().__init__(
+ vm,
+ label=_("Force shutdown"),
+ icon_cache=icon_cache,
+ icon_name="shutdown",
+ )
+ else:
+ super().__init__(
+ vm, label=_("Shutdown"), icon_cache=icon_cache, icon_name="shutdown"
+ )
+ self.force = force
+
+ def set_force(self, force):
+ self.force = force
+ if self.force:
+ self.label.set_text(_("Force shutdown"))
+ else:
+ self.label.set_text(_("Shutdown"))
async def perform_action(self):
try:
- self.vm.shutdown()
+ self.vm.shutdown(force=self.force)
except exc.QubesException as ex:
- show_error(
- _("Error shutting down qube"),
- _(
- "The following error occurred while attempting to "
- "shut down qube {0}:\n{1}"
- ).format(self.vm.name, str(ex)),
+ if self.force:
+ show_error(
+ _("Error shutting down qube"),
+ _(
+ "The following error occurred while attempting to "
+ "shut down qube {0}:\n{1}"
+ ).format(self.vm.name, str(ex)),
+ )
+ return
+ dialog = Gtk.MessageDialog(
+ None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK_CANCEL
+ )
+ dialog.set_title("Error shutting down qube")
+ dialog.set_markup(
+ f"The qube {self.vm.name} couldn't be shut down "
+ "normally. The following error occurred: \n"
+ f"{str(ex)}\n\n"
+ "Do you want to force shutdown? \n\nWarning: "
+ "this may cause unexpected issues in connected qubes."
)
+ dialog.connect("response", self.react_to_question)
+ GLib.idle_add(dialog.show)
+
+ def react_to_question(self, widget, response):
+ if response == Gtk.ResponseType.OK:
+ try:
+ self.vm.shutdown(force=True)
+ except exc.QubesException as ex:
+ show_error(
+ _("Error shutting down qube"),
+ _(
+ "The following error occurred while attempting to "
+ "shut down qube {0}:\n{1}"
+ ).format(self.vm.name, str(ex)),
+ )
+ widget.destroy()
class RestartItem(VMActionMenuItem):
"""Restart menu Item. When activated shutdowns the domain and
then starts it again."""
- def __init__(self, vm, icon_cache):
- super().__init__(
- vm, label=_("Restart"), icon_cache=icon_cache, icon_name="restart"
- )
- self.restart_thread = None
+ def __init__(self, vm, icon_cache, force=False):
+ if force:
+ super().__init__(
+ vm, label=_("Force restart"), icon_cache=icon_cache, icon_name="restart"
+ )
+ else:
+ super().__init__(
+ vm, label=_("Restart"), icon_cache=icon_cache, icon_name="restart"
+ )
+ self.force = force
+ self.give_up = False
+
+ def set_force(self, force):
+ self.force = force
+ if self.force:
+ self.label.set_text(_("Force restart"))
+ else:
+ self.label.set_text(_("Restart"))
async def perform_action(self, *_args, **_kwargs):
try:
- self.vm.shutdown()
+ self.vm.shutdown(force=self.force)
+ except exc.QubesException as ex:
+ if self.force:
+ # we already tried forcing it, let's just give up
+ show_error(
+ _("Error restarting qube"),
+ _(
+ "The following error occurred while attempting to restart"
+ "qube {0}:\n{1}"
+ ).format(self.vm.name, str(ex)),
+ )
+ return
+ dialog = Gtk.MessageDialog(
+ None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK_CANCEL
+ )
+ dialog.set_title("Error restarting qube")
+ dialog.set_markup(
+ f"The qube {self.vm.name} couldn't be shut down "
+ "normally. The following error occurred: \n"
+ f"{str(ex)}\n\n"
+ "Do you want to force shutdown? \n\nWarning: "
+ "this may cause unexpected issues in connected qubes."
+ )
+ dialog.connect("response", self.react_to_question)
+ GLib.idle_add(dialog.show)
+
+ try:
while self.vm.is_running():
+ if self.give_up:
+ return
await asyncio.sleep(1)
proc = await asyncio.create_subprocess_exec(
- "qvm-start", self.vm.name, stderr=subprocess.PIPE
+ "qvm-start", self.vm.name, stderr=asyncio.subprocess.PIPE
)
_stdout, stderr = await proc.communicate()
if proc.returncode != 0:
@@ -240,11 +321,28 @@ async def perform_action(self, *_args, **_kwargs):
show_error(
_("Error restarting qube"),
_(
- "The following error occurred while attempting to "
- "restart qube {0}:\n{1}"
+ "The following error occurred while attempting to restart"
+ "qube {0}:\n{1}"
).format(self.vm.name, str(ex)),
)
+ def react_to_question(self, widget, response):
+ if response == Gtk.ResponseType.OK:
+ try:
+ self.vm.shutdown(force=True)
+ except exc.QubesException as ex:
+ show_error(
+ _("Error shutting down qube"),
+ _(
+ "The following error occurred while attempting to "
+ "shut down qube {0}:\n{1}"
+ ).format(self.vm.name, str(ex)),
+ )
+ self.give_up = True
+ else:
+ self.give_up = True
+ widget.destroy()
+
class KillItem(VMActionMenuItem):
"""Kill domain menu Item. When activated kills the domain."""
@@ -278,9 +376,7 @@ def __init__(self, vm, icon_cache):
async def perform_action(self):
# pylint: disable=consider-using-with
- await asyncio.create_subprocess_exec(
- "qubes-vm-settings", self.vm.name, stderr=subprocess.PIPE
- )
+ await asyncio.create_subprocess_exec("qubes-vm-settings", self.vm.name)
class LogItem(ActionMenuItem):
@@ -293,24 +389,20 @@ def __init__(self, name, path, icon_cache):
self.path = path
async def perform_action(self):
- await asyncio.create_subprocess_exec(
- "qubes-log-viewer", self.path, stderr=subprocess.PIPE
- )
+ await asyncio.create_subprocess_exec("qubes-log-viewer", self.path)
class RunTerminalItem(VMActionMenuItem):
"""Run Terminal menu Item. When activated runs a terminal emulator."""
def __init__(self, vm, icon_cache, as_root=False):
- label = Gtk.Label(label=RunTerminalItem.dynamic_label(as_root))
super().__init__(
vm,
- label=label,
+ label=RunTerminalItem.dynamic_label(as_root),
icon_cache=icon_cache,
icon_name="terminal",
)
self.as_root = as_root
- self.label = label
@staticmethod
def dynamic_label(as_root):
@@ -359,9 +451,7 @@ def on_show_event(self, widget):
async def perform_action(self):
# pylint: disable=consider-using-with
- await asyncio.create_subprocess_exec(
- "qvm-console-dispvm", self.vm.name, stderr=subprocess.PIPE
- )
+ await asyncio.create_subprocess_exec("qvm-console-dispvm", self.vm.name)
class OpenFileManagerItem(VMActionMenuItem):
@@ -390,8 +480,7 @@ async def perform_action(self):
class InternalInfoItem(Gtk.MenuItem):
- """Restart menu Item. When activated shutdowns the domain and
- then starts it again."""
+ """Internal info label."""
def __init__(self):
super().__init__()
@@ -415,7 +504,7 @@ def __init__(self, vm, app, icon_cache):
self.app = app
self.add(OpenFileManagerItem(self.vm, icon_cache))
- self.add(RunTerminalItem(self.vm, icon_cache, as_root=app.terminal_as_root))
+ self.add(RunTerminalItem(self.vm, icon_cache, as_root=app.shift_pressed))
# Debug console for developers, troubleshooting, headless qubes
self.debug_console = RunDebugConsoleItem(self.vm, icon_cache)
@@ -423,9 +512,9 @@ def __init__(self, vm, app, icon_cache):
self.add(PreferencesItem(self.vm, icon_cache))
self.add(PauseItem(self.vm, icon_cache))
- self.add(ShutdownItem(self.vm, icon_cache))
+ self.add(ShutdownItem(self.vm, icon_cache, force=app.shift_pressed))
if self.vm.klass != "DispVM" or not self.vm.auto_cleanup:
- self.add(RestartItem(self.vm, icon_cache))
+ self.add(RestartItem(self.vm, icon_cache, force=app.shift_pressed))
self.set_reserve_toggle_size(False)
self.debug_console_update()
@@ -562,9 +651,7 @@ def on_activate(self, *_args, **_kwargs):
async def perform_action(self):
# pylint: disable=consider-using-with
- await asyncio.create_subprocess_exec(
- "qubes-qube-manager", stderr=subprocess.PIPE
- )
+ await asyncio.create_subprocess_exec("qubes-qube-manager")
class DomainMenuItem(Gtk.MenuItem):
@@ -815,7 +902,7 @@ def debug_change(self, vm, *_args, **_kwargs):
submenu.debug_console_update()
def show_menu(self, _unused, event):
- self.terminal_as_root = False
+ self.shift_pressed = False
self.tray_menu.popup_at_pointer(event) # None means current event
def emit_notification(self, vm, event, **kwargs):
@@ -1163,19 +1250,19 @@ def _disconnect_signals(self, _event):
self.stats_dispatcher.remove_handler("vm-stats", self.update_stats)
@property
- def terminal_as_root(self):
+ def shift_pressed(self):
try:
- return self._terminal_as_root
+ return self._shift_pressed
except AttributeError:
- self._terminal_as_root = False
- return self.terminal_as_root
+ self._shift_pressed = False
+ return self.shift_pressed
- @terminal_as_root.setter
- def terminal_as_root(self, as_root):
- if as_root == self.terminal_as_root:
+ @shift_pressed.setter
+ def shift_pressed(self, shift_pressed):
+ if shift_pressed == self.shift_pressed:
return
- self._terminal_as_root = as_root
+ self._shift_pressed = shift_pressed
for item in self.menu_items.values():
if item.vm:
submenu = item.get_submenu()
@@ -1184,16 +1271,18 @@ def terminal_as_root(self, as_root):
def do_emit(child):
if isinstance(child, RunTerminalItem):
- child.set_as_root(as_root)
+ child.set_as_root(shift_pressed)
+ if isinstance(child, (RestartItem, ShutdownItem)):
+ child.set_force(shift_pressed)
submenu.foreach(do_emit)
def key_event(self, _unused, event):
if event.keyval in [Gdk.KEY_Shift_L, Gdk.KEY_Shift_R]:
if event.type == Gdk.EventType.KEY_PRESS:
- self.terminal_as_root = True
+ self.shift_pressed = True
elif event.type == Gdk.EventType.KEY_RELEASE:
- self.terminal_as_root = False
+ self.shift_pressed = False
def main():