main.py 80 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083
  1. #!/usr/bin/python2
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
  6. #
  7. # This program is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU General Public License
  9. # as published by the Free Software Foundation; either version 2
  10. # of the License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program; if not, write to the Free Software
  19. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. #
  21. #
  22. import sys
  23. import os
  24. import os.path
  25. import signal
  26. import subprocess
  27. import time
  28. from datetime import datetime, timedelta
  29. from PyQt4.QtGui import *
  30. from PyQt4.QtDBus import QDBusVariant, QDBusMessage
  31. from PyQt4.QtDBus import QDBusConnection
  32. from PyQt4.QtDBus import QDBusInterface, QDBusAbstractAdaptor
  33. from pyinotify import WatchManager, ThreadedNotifier, EventsCodes, \
  34. ProcessEvent
  35. from qubes.qubes import QubesVmCollection
  36. from qubes.qubes import QubesException
  37. from qubes.qubes import system_path
  38. from qubes.qubes import QubesDaemonPidfile
  39. from qubes.qubes import QubesHost
  40. import table_widgets
  41. from block import QubesBlockDevicesManager
  42. from table_widgets import VmTypeWidget, VmLabelWidget, VmNameItem, \
  43. VmInfoWidget, VmTemplateItem, VmNetvmItem, VmUsageBarWidget, ChartWidget, \
  44. VmSizeOnDiskItem, VmInternalItem, VmIPItem, VmIncludeInBackupsItem, \
  45. VmLastBackupItem
  46. from qubes.qubes import QubesHVm
  47. from qubes import qubesutils
  48. from ui_mainwindow import *
  49. from create_new_vm import NewVmDlg
  50. from settings import VMSettingsWindow
  51. from restore import RestoreVMsWindow
  52. from backup import BackupVMsWindow
  53. from global_settings import GlobalSettingsWindow
  54. from log_dialog import LogDialog
  55. from thread_monitor import *
  56. qubes_clipboard_info_file = "/var/run/qubes/qubes-clipboard.bin.source"
  57. update_suggestion_interval = 14 # 14 days
  58. dbus_object_path = '/org/qubesos/QubesManager'
  59. dbus_interface = 'org.qubesos.QubesManager'
  60. system_bus = None
  61. session_bus = None
  62. class QMVmState:
  63. ErrorMsg = 1
  64. AudioRecAvailable = 2
  65. AudioRecAllowed = 3
  66. class QubesManagerFileWatcher(ProcessEvent):
  67. def __init__(self, update_func, **kargs):
  68. super(QubesManagerFileWatcher, self).__init__(**kargs)
  69. self.update_func = update_func
  70. def process_IN_MODIFY(self, event):
  71. if event.path == system_path["qubes_store_filename"]:
  72. self.update_func()
  73. def process_IN_MOVED_TO(self, event):
  74. if event.pathname == system_path["qubes_store_filename"]:
  75. self.update_func()
  76. # noinspection PyMethodMayBeStatic
  77. def process_IN_CLOSE_WRITE(self, event):
  78. if event.path == qubes_clipboard_info_file:
  79. src_info_file = open(qubes_clipboard_info_file, 'r')
  80. src_vmname = src_info_file.readline().strip('\n')
  81. if src_vmname == "":
  82. trayIcon.showMessage(
  83. "Qubes Clipboard has been copied to the VM and wiped.<i/>\n"
  84. "<small>Trigger a paste operation (e.g. Ctrl-v) to insert "
  85. "it into an application.</small>",
  86. msecs=3000)
  87. else:
  88. trayIcon.showMessage(
  89. "Qubes Clipboard fetched from VM: <b>'{0}'</b>\n"
  90. "<small>Press Ctrl-Shift-v to copy this clipboard into dest"
  91. " VM's clipboard.</small>".format(
  92. src_vmname), msecs=3000)
  93. src_info_file.close()
  94. def process_IN_CREATE(self, event):
  95. if event.name == os.path.basename(qubes_clipboard_info_file):
  96. event.path = qubes_clipboard_info_file
  97. self.process_IN_CLOSE_WRITE(event)
  98. wm.add_watch(qubes_clipboard_info_file,
  99. EventsCodes.OP_FLAGS.get('IN_CLOSE_WRITE'))
  100. elif event.name == os.path.basename(table_widgets
  101. .qubes_dom0_updates_stat_file):
  102. trayIcon.showMessage("Qubes dom0 updates available.", msecs=0)
  103. class VmRowInTable(object):
  104. cpu_graph_hue = 210
  105. mem_graph_hue = 120
  106. def __init__(self, vm, row_no, table, block_manager):
  107. self.vm = vm
  108. self.row_no = row_no
  109. table_widgets.row_height = VmManagerWindow.row_height
  110. table.setRowHeight(row_no, VmManagerWindow.row_height)
  111. self.type_widget = VmTypeWidget(vm)
  112. table.setCellWidget(row_no, VmManagerWindow.columns_indices['Type'],
  113. self.type_widget)
  114. table.setItem(row_no, VmManagerWindow.columns_indices['Type'],
  115. self.type_widget.tableItem)
  116. self.label_widget = VmLabelWidget(vm)
  117. table.setCellWidget(row_no, VmManagerWindow.columns_indices['Label'],
  118. self.label_widget)
  119. table.setItem(row_no, VmManagerWindow.columns_indices['Label'],
  120. self.label_widget.tableItem)
  121. self.name_widget = VmNameItem(vm)
  122. table.setItem(row_no, VmManagerWindow.columns_indices['Name'],
  123. self.name_widget)
  124. self.info_widget = VmInfoWidget(vm)
  125. table.setCellWidget(row_no, VmManagerWindow.columns_indices['State'],
  126. self.info_widget)
  127. table.setItem(row_no, VmManagerWindow.columns_indices['State'],
  128. self.info_widget.tableItem)
  129. self.template_widget = VmTemplateItem(vm)
  130. table.setItem(row_no, VmManagerWindow.columns_indices['Template'],
  131. self.template_widget)
  132. self.netvm_widget = VmNetvmItem(vm)
  133. table.setItem(row_no, VmManagerWindow.columns_indices['NetVM'],
  134. self.netvm_widget)
  135. self.cpu_usage_widget = VmUsageBarWidget(
  136. 0, 100, "%v %",
  137. lambda v, val: val if v.last_running else 0,
  138. vm, 0, self.cpu_graph_hue)
  139. table.setCellWidget(row_no, VmManagerWindow.columns_indices['CPU'],
  140. self.cpu_usage_widget)
  141. table.setItem(row_no, VmManagerWindow.columns_indices['CPU'],
  142. self.cpu_usage_widget.tableItem)
  143. self.load_widget = ChartWidget(
  144. vm,
  145. lambda v, val: val if v.last_running else 0,
  146. self.cpu_graph_hue, 0)
  147. table.setCellWidget(row_no,
  148. VmManagerWindow.columns_indices['CPU Graph'],
  149. self.load_widget)
  150. table.setItem(row_no, VmManagerWindow.columns_indices['CPU Graph'],
  151. self.load_widget.tableItem)
  152. self.mem_usage_widget = VmUsageBarWidget(
  153. 0, qubes_host.memory_total / 1024, "%v MB",
  154. lambda v, val: v.get_mem() / 1024,
  155. vm, 0, self.mem_graph_hue)
  156. table.setCellWidget(row_no, VmManagerWindow.columns_indices['MEM'],
  157. self.mem_usage_widget)
  158. table.setItem(row_no, VmManagerWindow.columns_indices['MEM'],
  159. self.mem_usage_widget.tableItem)
  160. self.mem_widget = ChartWidget(
  161. vm, lambda v, val: v.get_mem() * 100 / qubes_host.memory_total,
  162. self.mem_graph_hue, 0)
  163. table.setCellWidget(row_no,
  164. VmManagerWindow.columns_indices['MEM Graph'],
  165. self.mem_widget)
  166. table.setItem(row_no, VmManagerWindow.columns_indices['MEM Graph'],
  167. self.mem_widget.tableItem)
  168. self.size_widget = VmSizeOnDiskItem(vm)
  169. table.setItem(row_no, VmManagerWindow.columns_indices['Size'],
  170. self.size_widget)
  171. self.internal_widget = VmInternalItem(vm)
  172. table.setItem(row_no, VmManagerWindow.columns_indices['Internal'],
  173. self.internal_widget)
  174. self.ip_widget = VmIPItem(vm)
  175. table.setItem(row_no, VmManagerWindow.columns_indices['IP'],
  176. self.ip_widget)
  177. self.include_in_backups_widget = VmIncludeInBackupsItem(vm)
  178. table.setItem(row_no, VmManagerWindow.columns_indices[
  179. 'Backups'], self.include_in_backups_widget)
  180. self.last_backup_widget = VmLastBackupItem(vm)
  181. table.setItem(row_no, VmManagerWindow.columns_indices[
  182. 'Last backup'], self.last_backup_widget)
  183. def update(self, blk_visible=None, cpu_load=None, update_size_on_disk=False,
  184. rec_visible=None):
  185. self.info_widget.update_vm_state(self.vm, blk_visible, rec_visible)
  186. if cpu_load is not None:
  187. self.cpu_usage_widget.update_load(self.vm, cpu_load)
  188. self.mem_usage_widget.update_load(self.vm, None)
  189. self.load_widget.update_load(self.vm, cpu_load)
  190. self.mem_widget.update_load(self.vm, None)
  191. if update_size_on_disk:
  192. self.size_widget.update()
  193. vm_shutdown_timeout = 20000 # in msec
  194. class VmShutdownMonitor(QObject):
  195. def __init__(self, vm, shutdown_time=vm_shutdown_timeout):
  196. QObject.__init__(self)
  197. self.vm = vm
  198. self.shutdown_time = shutdown_time
  199. def check_if_vm_has_shutdown(self):
  200. vm = self.vm
  201. vm_start_time = vm.get_start_time()
  202. if not vm.is_running() or (
  203. vm_start_time and vm_start_time >= datetime.now() -
  204. timedelta(0, self.shutdown_time / 1000)):
  205. return
  206. reply = QMessageBox.question(
  207. None, "VM Shutdown",
  208. "The VM <b>'{0}'</b> hasn't shutdown within the last {1} seconds, "
  209. "do you want to kill it?<br>".format(
  210. vm.name, self.shutdown_time / 1000),
  211. "Kill it!",
  212. "Wait another {0} seconds...".format(
  213. self.shutdown_time / 1000))
  214. if reply == 0:
  215. vm.force_shutdown()
  216. else:
  217. # noinspection PyTypeChecker,PyCallByClass
  218. QTimer.singleShot(self.shutdown_time, self.check_if_vm_has_shutdown)
  219. class VmManagerWindow(Ui_VmManagerWindow, QMainWindow):
  220. row_height = 30
  221. column_width = 200
  222. min_visible_rows = 10
  223. update_interval = 1000 # in msec
  224. show_inactive_vms = True
  225. show_internal_vms = False
  226. # suppress saving settings while initializing widgets
  227. settings_loaded = False
  228. columns_indices = {"Type": 0,
  229. "Label": 1,
  230. "Name": 2,
  231. "State": 3,
  232. "Template": 4,
  233. "NetVM": 5,
  234. "CPU": 6,
  235. "CPU Graph": 7,
  236. "MEM": 8,
  237. "MEM Graph": 9,
  238. "Size": 10,
  239. "Internal": 11,
  240. "IP": 12,
  241. "Backups": 13,
  242. "Last backup": 14,
  243. }
  244. def __init__(self, qvm_collection, blk_manager, parent=None):
  245. super(VmManagerWindow, self).__init__()
  246. self.setupUi(self)
  247. self.toolbar = self.toolBar
  248. self.manager_settings = QSettings()
  249. self.qubes_watch = qubesutils.QubesWatch()
  250. self.qvm_collection = qvm_collection
  251. self.blk_manager = blk_manager
  252. self.blk_manager.tray_message_func = trayIcon.showMessage
  253. self.qubes_watch.setup_domain_watch(self.domain_state_changed_callback)
  254. self.qubes_watch.setup_block_watch(self.blk_manager.block_devs_event)
  255. self.blk_watch_thread = threading.Thread(
  256. target=self.qubes_watch.watch_loop)
  257. self.blk_watch_thread.daemon = True
  258. self.blk_watch_thread.start()
  259. self.connect(self.table, SIGNAL("itemSelectionChanged()"),
  260. self.table_selection_changed)
  261. self.table.setColumnWidth(0, self.column_width)
  262. self.sort_by_column = "Type"
  263. self.sort_order = Qt.AscendingOrder
  264. self.screen_number = -1
  265. self.screen_changed = False
  266. self.vms_list = []
  267. self.vms_in_table = {}
  268. self.reload_table = False
  269. self.running_vms_count = 0
  270. self.internal_vms_count = 0
  271. self.vm_errors = {}
  272. self.vm_rec = {}
  273. self.frame_width = 0
  274. self.frame_height = 0
  275. self.move(self.x(), 0)
  276. self.columns_actions = {
  277. self.columns_indices["Type"]: self.action_vm_type,
  278. self.columns_indices["Label"]: self.action_label,
  279. self.columns_indices["Name"]: self.action_name,
  280. self.columns_indices["State"]: self.action_state,
  281. self.columns_indices["Template"]: self.action_template,
  282. self.columns_indices["NetVM"]: self.action_netvm,
  283. self.columns_indices["CPU"]: self.action_cpu,
  284. self.columns_indices["CPU Graph"]: self.action_cpu_graph,
  285. self.columns_indices["MEM"]: self.action_mem,
  286. self.columns_indices["MEM Graph"]: self.action_mem_graph,
  287. self.columns_indices["Size"]: self.action_size_on_disk,
  288. self.columns_indices["Internal"]: self.action_internal,
  289. self.columns_indices["IP"]: self
  290. .action_ip, self.columns_indices["Backups"]: self
  291. .action_backups, self.columns_indices["Last backup"]: self
  292. .action_last_backup
  293. }
  294. self.visible_columns_count = len(self.columns_indices)
  295. self.table.setColumnHidden(self.columns_indices["NetVM"], True)
  296. self.action_netvm.setChecked(False)
  297. self.table.setColumnHidden(self.columns_indices["CPU Graph"], True)
  298. self.action_cpu_graph.setChecked(False)
  299. self.table.setColumnHidden(self.columns_indices["MEM Graph"], True)
  300. self.action_mem_graph.setChecked(False)
  301. self.table.setColumnHidden(self.columns_indices["Size"], True)
  302. self.action_size_on_disk.setChecked(False)
  303. self.table.setColumnHidden(self.columns_indices["Internal"], True)
  304. self.action_internal.setChecked(False)
  305. self.table.setColumnHidden(self.columns_indices["IP"], True)
  306. self.action_ip.setChecked(False)
  307. self.table.setColumnHidden(self.columns_indices["Backups"], True)
  308. self.action_backups.setChecked(False)
  309. self.table.setColumnHidden(self.columns_indices["Last backup"], True)
  310. self.action_last_backup.setChecked(False)
  311. self.table.setColumnWidth(self.columns_indices["State"], 80)
  312. self.table.setColumnWidth(self.columns_indices["Name"], 150)
  313. self.table.setColumnWidth(self.columns_indices["Label"], 40)
  314. self.table.setColumnWidth(self.columns_indices["Type"], 40)
  315. self.table.setColumnWidth(self.columns_indices["Size"], 100)
  316. self.table.setColumnWidth(self.columns_indices["Internal"], 60)
  317. self.table.setColumnWidth(self.columns_indices["IP"], 100)
  318. self.table.setColumnWidth(self.columns_indices["Backups"], 60)
  319. self.table.setColumnWidth(self.columns_indices["Last backup"], 90)
  320. self.table.horizontalHeader().setResizeMode(QHeaderView.Fixed)
  321. self.table.sortItems(self.columns_indices[self.sort_by_column],
  322. self.sort_order)
  323. self.context_menu = QMenu(self)
  324. self.context_menu.addAction(self.action_settings)
  325. self.context_menu.addAction(self.action_editfwrules)
  326. self.context_menu.addAction(self.action_appmenus)
  327. self.context_menu.addAction(self.action_set_keyboard_layout)
  328. self.context_menu.addMenu(self.blk_menu)
  329. self.context_menu.addAction(self.action_toggle_audio_input)
  330. self.context_menu.addSeparator()
  331. self.context_menu.addAction(self.action_updatevm)
  332. self.context_menu.addAction(self.action_run_command_in_vm)
  333. self.context_menu.addAction(self.action_resumevm)
  334. self.context_menu.addAction(self.action_startvm_tools_install)
  335. self.context_menu.addAction(self.action_pausevm)
  336. self.context_menu.addAction(self.action_shutdownvm)
  337. self.context_menu.addAction(self.action_killvm)
  338. self.context_menu.addSeparator()
  339. self.context_menu.addAction(self.action_clonevm)
  340. self.context_menu.addAction(self.action_removevm)
  341. self.context_menu.addSeparator()
  342. self.context_menu.addMenu(self.logs_menu)
  343. self.context_menu.addSeparator()
  344. self.tools_context_menu = QMenu(self)
  345. self.tools_context_menu.addAction(self.action_toolbar)
  346. self.tools_context_menu.addAction(self.action_menubar)
  347. self.table_selection_changed()
  348. self.connect(
  349. self.table.horizontalHeader(),
  350. SIGNAL("sortIndicatorChanged(int, Qt::SortOrder)"),
  351. self.sortIndicatorChanged)
  352. self.connect(self.table,
  353. SIGNAL("customContextMenuRequested(const QPoint&)"),
  354. self.open_context_menu)
  355. self.connect(self.menubar,
  356. SIGNAL("customContextMenuRequested(const QPoint&)"),
  357. lambda pos: self.open_tools_context_menu(self.menubar,
  358. pos))
  359. self.connect(self.toolBar,
  360. SIGNAL("customContextMenuRequested(const QPoint&)"),
  361. lambda pos: self.open_tools_context_menu(self.toolBar,
  362. pos))
  363. self.connect(self.blk_menu, SIGNAL("triggered(QAction *)"),
  364. self.attach_dettach_device_triggered)
  365. self.connect(self.logs_menu, SIGNAL("triggered(QAction *)"),
  366. self.show_log)
  367. self.table.setContentsMargins(0, 0, 0, 0)
  368. self.centralwidget.layout().setContentsMargins(0, 0, 0, 0)
  369. self.layout().setContentsMargins(0, 0, 0, 0)
  370. self.connect(self.action_menubar, SIGNAL("toggled(bool)"),
  371. self.showhide_menubar)
  372. self.connect(self.action_toolbar, SIGNAL("toggled(bool)"),
  373. self.showhide_toolbar)
  374. self.register_dbus_watches()
  375. self.load_manager_settings()
  376. self.action_showallvms.setChecked(self.show_inactive_vms)
  377. self.action_showinternalvms.setChecked(self.show_internal_vms)
  378. self.fill_table()
  379. self.counter = 0
  380. self.update_size_on_disk = False
  381. self.shutdown_monitor = {}
  382. self.last_measure_results = {}
  383. self.last_measure_time = time.time()
  384. # noinspection PyCallByClass,PyTypeChecker
  385. QTimer.singleShot(self.update_interval, self.update_table)
  386. QubesDbusNotifyServerAdaptor(self)
  387. def load_manager_settings(self):
  388. # visible columns
  389. self.manager_settings.beginGroup("columns")
  390. for col in self.columns_indices.keys():
  391. col_no = self.columns_indices[col]
  392. visible = self.manager_settings.value(
  393. col,
  394. defaultValue=not self.table.isColumnHidden(col_no)).toBool()
  395. self.columns_actions[col_no].setChecked(visible)
  396. self.manager_settings.endGroup()
  397. self.show_inactive_vms = self.manager_settings.value(
  398. "view/show_inactive_vms", defaultValue=False).toBool()
  399. self.show_internal_vms = self.manager_settings.value(
  400. "view/show_internal_vms", defaultValue=False).toBool()
  401. self.sort_by_column = str(
  402. self.manager_settings.value("view/sort_column",
  403. defaultValue=self.sort_by_column).toString())
  404. self.sort_order = Qt.SortOrder(
  405. self.manager_settings.value("view/sort_order",
  406. defaultValue=self.sort_order).toInt()[
  407. 0])
  408. self.table.sortItems(self.columns_indices[self.sort_by_column],
  409. self.sort_order)
  410. if not self.manager_settings.value("view/menubar_visible",
  411. defaultValue=True).toBool():
  412. self.action_menubar.setChecked(False)
  413. if not self.manager_settings.value("view/toolbar_visible",
  414. defaultValue=True).toBool():
  415. self.action_toolbar.setChecked(False)
  416. x = self.manager_settings.value('position/x', defaultValue=-1).toInt()[
  417. 0]
  418. y = self.manager_settings.value('position/y', defaultValue=-1).toInt()[
  419. 0]
  420. if x != -1 or y != -1:
  421. self.move(x, y)
  422. self.settings_loaded = True
  423. def show(self):
  424. super(VmManagerWindow, self).show()
  425. self.screen_number = app.desktop().screenNumber(self)
  426. def set_table_geom_size(self):
  427. desktop_width = app.desktop().availableGeometry(
  428. self).width() - self.frame_width # might be wrong...
  429. desktop_height = app.desktop().availableGeometry(
  430. self).height() - self.frame_height # might be wrong...
  431. desktop_height -= self.row_height # UGLY! to somehow ommit taskbar...
  432. w = self.table.horizontalHeader().length() + \
  433. self.table.verticalScrollBar().width() + \
  434. 2 * self.table.frameWidth() + 1
  435. h = self.table.horizontalHeader().height() + \
  436. 2 * self.table.frameWidth()
  437. mainwindow_to_add = 0
  438. available_space = desktop_height
  439. if self.menubar.isVisible():
  440. menubar_height = (self.menubar.sizeHint().height() +
  441. self.menubar.contentsMargins().top() +
  442. self.menubar.contentsMargins().bottom())
  443. available_space -= menubar_height
  444. mainwindow_to_add += menubar_height
  445. if self.toolbar.isVisible():
  446. toolbar_height = (self.toolbar.sizeHint().height() +
  447. self.toolbar.contentsMargins().top() +
  448. self.toolbar.contentsMargins().bottom())
  449. available_space -= toolbar_height
  450. mainwindow_to_add += toolbar_height
  451. if w >= desktop_width:
  452. available_space -= self.table.horizontalScrollBar().height()
  453. h += self.table.horizontalScrollBar().height()
  454. default_rows = int(available_space / self.row_height)
  455. if self.show_internal_vms:
  456. if self.show_inactive_vms:
  457. n = self.table.rowCount()
  458. else:
  459. n = self.running_vms_count
  460. elif self.show_inactive_vms:
  461. n = self.table.rowCount() - self.internal_vms_count
  462. else:
  463. n = self.running_vms_count
  464. if n > default_rows:
  465. h += default_rows * self.row_height
  466. self.table.verticalScrollBar().show()
  467. else:
  468. h += n * self.row_height
  469. self.table.verticalScrollBar().hide()
  470. w -= self.table.verticalScrollBar().width()
  471. w = min(desktop_width, w)
  472. self.centralwidget.setFixedHeight(h)
  473. h += mainwindow_to_add
  474. self.setMaximumHeight(h)
  475. self.setMinimumHeight(h)
  476. self.table.setFixedWidth(w)
  477. self.centralwidget.setFixedWidth(w)
  478. # don't change the following two lines to setFixedWidth!
  479. self.setMaximumWidth(w)
  480. self.setMinimumWidth(w)
  481. def moveEvent(self, event):
  482. super(VmManagerWindow, self).moveEvent(event)
  483. screen_number = app.desktop().screenNumber(self)
  484. if self.screen_number != screen_number:
  485. self.screen_changed = True
  486. self.screen_number = screen_number
  487. if self.settings_loaded:
  488. self.manager_settings.setValue('position/x', self.x())
  489. self.manager_settings.setValue('position/y', self.y())
  490. # do not sync for performance reasons
  491. def domain_state_changed_callback(self, name=None, uuid=None):
  492. if name is not None:
  493. vm = self.qvm_collection.get_vm_by_name(name)
  494. if vm:
  495. vm.refresh()
  496. def get_vms_list(self):
  497. self.qvm_collection.lock_db_for_reading()
  498. self.qvm_collection.load()
  499. self.qvm_collection.unlock_db()
  500. running_count = 0
  501. internal_count = 0
  502. vms_list = [vm for vm in self.qvm_collection.values()]
  503. for vm in vms_list:
  504. vm.last_power_state = vm.get_power_state()
  505. vm.last_running = vm.is_running()
  506. if vm.last_running:
  507. running_count += 1
  508. if vm.internal:
  509. internal_count += 1
  510. vm.qubes_manager_state = {}
  511. self.update_audio_rec_info(vm)
  512. vm.qubes_manager_state[QMVmState.ErrorMsg] = self.vm_errors[
  513. vm.qid] if vm.qid in self.vm_errors else None
  514. self.running_vms_count = running_count
  515. self.internal_vms_count = internal_count
  516. return vms_list
  517. def fill_table(self):
  518. # save current selection
  519. row_index = self.table.currentRow()
  520. selected_qid = -1
  521. if row_index != -1:
  522. vm_item = self.table.item(row_index, self.columns_indices["Name"])
  523. if vm_item:
  524. selected_qid = vm_item.qid
  525. self.table.setSortingEnabled(False)
  526. self.table.clearContents()
  527. vms_list = self.get_vms_list()
  528. self.table.setRowCount(len(vms_list))
  529. vms_in_table = {}
  530. row_no = 0
  531. for vm in vms_list:
  532. # if vm.internal:
  533. # continue
  534. vm_row = VmRowInTable(vm, row_no, self.table, self.blk_manager)
  535. vms_in_table[vm.qid] = vm_row
  536. row_no += 1
  537. self.table.setRowCount(row_no)
  538. self.vms_list = vms_list
  539. self.vms_in_table = vms_in_table
  540. self.reload_table = False
  541. if selected_qid in vms_in_table.keys():
  542. self.table.setCurrentItem(
  543. self.vms_in_table[selected_qid].name_widget)
  544. self.table.setSortingEnabled(True)
  545. self.showhide_vms(True, True)
  546. self.set_table_geom_size()
  547. if (not self.show_inactive_vms) or (not self.show_internal_vms):
  548. self.showhide_vms(self.show_inactive_vms, self.show_internal_vms)
  549. self.set_table_geom_size()
  550. def showhide_vms(self, show_inactive, show_internal):
  551. if show_inactive and show_internal:
  552. row_no = 0
  553. while row_no < self.table.rowCount():
  554. self.table.setRowHidden(row_no, False)
  555. row_no += 1
  556. else:
  557. row_no = 0
  558. while row_no < self.table.rowCount():
  559. widget = self.table.cellWidget(row_no,
  560. self.columns_indices["State"])
  561. running = widget.vm.last_running
  562. internal = widget.vm.internal
  563. if not (show_inactive or running) or not (
  564. show_internal or not internal):
  565. self.table.setRowHidden(row_no, True)
  566. else:
  567. self.table.setRowHidden(row_no, False)
  568. row_no += 1
  569. def mark_table_for_update(self):
  570. self.reload_table = True
  571. # When calling update_table() directly, always use out_of_schedule=True!
  572. def update_table(self, out_of_schedule=False):
  573. update_devs = self.update_block_devices() or out_of_schedule
  574. reload_table = self.reload_table
  575. if manager_window.isVisible():
  576. some_vms_have_changed_power_state = False
  577. for vm in self.vms_list:
  578. state = vm.get_power_state()
  579. if vm.last_power_state != state:
  580. if state == "Running" and \
  581. self.vm_errors.get(vm.qid, "") \
  582. .startswith("Error starting VM:"):
  583. self.clear_error(vm.qid)
  584. prev_running = vm.last_running
  585. vm.last_power_state = state
  586. vm.last_running = vm.is_running()
  587. self.update_audio_rec_info(vm)
  588. if not prev_running and vm.last_running:
  589. self.running_vms_count += 1
  590. some_vms_have_changed_power_state = True
  591. # Clear error state when VM just started
  592. self.clear_error(vm.qid)
  593. elif prev_running and not vm.last_running:
  594. # FIXME: remove when recAllowed state will be preserved
  595. if self.vm_rec.has_key(vm.name):
  596. self.vm_rec.pop(vm.name)
  597. self.running_vms_count -= 1
  598. some_vms_have_changed_power_state = True
  599. else:
  600. # pulseaudio agent register itself some time after VM
  601. # startup
  602. if state == "Running" and not vm.qubes_manager_state[
  603. QMVmState.AudioRecAvailable]:
  604. self.update_audio_rec_info(vm)
  605. if self.vm_errors.get(vm.qid, "") == \
  606. "Error starting VM: Cannot execute qrexec-daemon!" \
  607. and vm.is_qrexec_running():
  608. self.clear_error(vm.qid)
  609. if self.screen_changed:
  610. reload_table = True
  611. self.screen_changed = False
  612. if reload_table:
  613. self.fill_table()
  614. update_devs = True
  615. if not self.show_inactive_vms and \
  616. some_vms_have_changed_power_state:
  617. self.showhide_vms(True, True)
  618. self.showhide_vms(False, self.show_internal_vms)
  619. self.set_table_geom_size()
  620. if self.sort_by_column == \
  621. "State" and some_vms_have_changed_power_state:
  622. self.table.sortItems(self.columns_indices[self.sort_by_column],
  623. self.sort_order)
  624. blk_visible = None
  625. rows_with_blk = None
  626. if update_devs:
  627. rows_with_blk = []
  628. self.blk_manager.blk_lock.acquire()
  629. for d in self.blk_manager.attached_devs:
  630. rows_with_blk.append(
  631. self.blk_manager.attached_devs[d]['attached_to'][
  632. 'vm'].qid)
  633. self.blk_manager.blk_lock.release()
  634. if (not self.table.isColumnHidden(self.columns_indices['Size'])) and \
  635. self.counter % 60 == 0 or out_of_schedule:
  636. self.update_size_on_disk = True
  637. if self.counter % 3 == 0 or out_of_schedule:
  638. (self.last_measure_time, self.last_measure_results) = \
  639. qubes_host.measure_cpu_usage(self.qvm_collection,
  640. self.last_measure_results,
  641. self.last_measure_time)
  642. for vm_row in self.vms_in_table.values():
  643. cur_cpu_load = None
  644. if vm_row.vm.get_xid() in self.last_measure_results:
  645. cur_cpu_load = self.last_measure_results[vm_row.vm.xid][
  646. 'cpu_usage']
  647. else:
  648. cur_cpu_load = 0
  649. if rows_with_blk is not None:
  650. if vm_row.vm.qid in rows_with_blk:
  651. blk_visible = True
  652. else:
  653. blk_visible = False
  654. vm_row.update(blk_visible=blk_visible,
  655. cpu_load=cur_cpu_load,
  656. update_size_on_disk=self.update_size_on_disk,
  657. rec_visible=self.vm_rec.get(vm_row.vm.name,
  658. False))
  659. else:
  660. for vm_row in self.vms_in_table.values():
  661. if rows_with_blk is not None:
  662. if vm_row.vm.name in rows_with_blk:
  663. blk_visible = True
  664. else:
  665. blk_visible = False
  666. vm_row.update(blk_visible=blk_visible,
  667. update_size_on_disk=self.update_size_on_disk,
  668. rec_visible=self.vm_rec.get(vm_row.vm.name,
  669. False))
  670. if self.sort_by_column in ["CPU", "CPU Graph", "MEM", "MEM Graph",
  671. "State", "Size", "Internal"]:
  672. # "State": needed to sort after reload (fill_table sorts items
  673. # with setSortingEnabled, but by that time the widgets values
  674. # are not correct yet).
  675. self.table.sortItems(self.columns_indices[self.sort_by_column],
  676. self.sort_order)
  677. self.table_selection_changed()
  678. self.update_size_on_disk = False
  679. if not out_of_schedule:
  680. self.counter += 1
  681. # noinspection PyCallByClass,PyTypeChecker
  682. QTimer.singleShot(self.update_interval, self.update_table)
  683. def update_block_devices(self):
  684. res, msg = self.blk_manager.check_for_updates()
  685. if msg is not None and len(msg) > 0:
  686. trayIcon.showMessage('\n'.join(msg), msecs=5000)
  687. return res
  688. # noinspection PyPep8Naming
  689. @pyqtSlot(bool, str)
  690. def recAllowedChanged(self, state, vmname):
  691. self.vm_rec[str(vmname)] = bool(state)
  692. def register_dbus_watches(self):
  693. global session_bus
  694. if not session_bus:
  695. session_bus = QDBusConnection.sessionBus()
  696. if not session_bus.connect(QString(), # service
  697. QString(), # path
  698. QString("org.QubesOS.Audio"), # interface
  699. QString("RecAllowedChanged"), # name
  700. self.recAllowedChanged): # slot
  701. print session_bus.lastError().message()
  702. # noinspection PyPep8Naming
  703. def sortIndicatorChanged(self, column, order):
  704. self.sort_by_column = [name for name in self.columns_indices.keys() if
  705. self.columns_indices[name] == column][0]
  706. self.sort_order = order
  707. if self.settings_loaded:
  708. self.manager_settings.setValue('view/sort_column',
  709. self.sort_by_column)
  710. self.manager_settings.setValue('view/sort_order', self.sort_order)
  711. self.manager_settings.sync()
  712. def table_selection_changed(self):
  713. vm = self.get_selected_vm()
  714. if vm is not None:
  715. # Update available actions:
  716. self.action_settings.setEnabled(vm.qid != 0)
  717. self.action_removevm.setEnabled(
  718. not vm.installed_by_rpm and not vm.last_running)
  719. self.action_clonevm.setEnabled(
  720. not vm.last_running and not vm.is_netvm())
  721. self.action_resumevm.setEnabled(not vm.last_running or
  722. vm.last_power_state == "Paused")
  723. try:
  724. self.action_startvm_tools_install.setVisible(
  725. isinstance(vm, QubesHVm))
  726. except NameError:
  727. # ignore non existing QubesHVm
  728. pass
  729. self.action_startvm_tools_install.setEnabled(not vm.last_running)
  730. self.action_pausevm.setEnabled(
  731. vm.last_running and
  732. vm.last_power_state != "Paused" and
  733. vm.qid != 0)
  734. self.action_shutdownvm.setEnabled(
  735. vm.last_running and
  736. vm.last_power_state != "Paused" and
  737. vm.qid != 0)
  738. self.action_killvm.setEnabled((vm.last_running or
  739. vm.last_power_state == "Paused") and
  740. vm.qid != 0)
  741. self.action_appmenus.setEnabled(not vm.internal and
  742. not vm.is_disposablevm())
  743. self.action_editfwrules.setEnabled(vm.is_networked() and not (
  744. vm.is_netvm() and not vm.is_proxyvm()))
  745. self.action_updatevm.setEnabled(vm.is_updateable() or vm.qid == 0)
  746. self.action_toggle_audio_input.setEnabled(
  747. vm.qubes_manager_state[QMVmState.AudioRecAvailable])
  748. self.action_run_command_in_vm.setEnabled(
  749. not vm.last_power_state == "Paused" and vm.qid != 0)
  750. self.action_set_keyboard_layout.setEnabled(
  751. vm.qid != 0 and
  752. vm.last_running and
  753. vm.last_power_state != "Paused")
  754. else:
  755. self.action_settings.setEnabled(False)
  756. self.action_removevm.setEnabled(False)
  757. self.action_startvm_tools_install.setVisible(False)
  758. self.action_startvm_tools_install.setEnabled(False)
  759. self.action_clonevm.setEnabled(False)
  760. self.action_resumevm.setEnabled(False)
  761. self.action_pausevm.setEnabled(False)
  762. self.action_shutdownvm.setEnabled(False)
  763. self.action_killvm.setEnabled(False)
  764. self.action_appmenus.setEnabled(False)
  765. self.action_editfwrules.setEnabled(False)
  766. self.action_updatevm.setEnabled(False)
  767. self.action_toggle_audio_input.setEnabled(False)
  768. self.action_run_command_in_vm.setEnabled(False)
  769. self.action_set_keyboard_layout.setEnabled(False)
  770. def closeEvent(self, event):
  771. # There is something borked in Qt,
  772. # as the logic here is inverted on X11
  773. if event.spontaneous():
  774. self.hide()
  775. event.ignore()
  776. def set_error(self, qid, message):
  777. for vm in self.vms_list:
  778. if vm.qid == qid:
  779. vm.qubes_manager_state[QMVmState.ErrorMsg] = message
  780. # Store error in separate dict to make it immune to VM list reload
  781. self.vm_errors[qid] = str(message)
  782. def clear_error(self, qid):
  783. self.vm_errors.pop(qid, None)
  784. for vm in self.vms_list:
  785. if vm.qid == qid:
  786. vm.qubes_manager_state[QMVmState.ErrorMsg] = None
  787. def clear_error_exact(self, qid, message):
  788. for vm in self.vms_list:
  789. if vm.qid == qid:
  790. if vm.qubes_manager_state[QMVmState.ErrorMsg] == message:
  791. vm.qubes_manager_state[QMVmState.ErrorMsg] = None
  792. self.vm_errors.pop(qid, None)
  793. @pyqtSlot(name='on_action_createvm_triggered')
  794. def action_createvm_triggered(self):
  795. dialog = NewVmDlg(app, self.qvm_collection, trayIcon)
  796. dialog.exec_()
  797. def get_selected_vm(self):
  798. # vm selection relies on the VmInfo widget's value used
  799. # for sorting by VM name
  800. row_index = self.table.currentRow()
  801. if row_index != -1:
  802. vm_item = self.table.item(row_index, self.columns_indices["Name"])
  803. # here is possible race with update_table timer so check
  804. # if really got the item
  805. if vm_item is None:
  806. return None
  807. qid = vm_item.qid
  808. assert self.vms_in_table[qid] is not None
  809. vm = self.vms_in_table[qid].vm
  810. return vm
  811. else:
  812. return None
  813. @pyqtSlot(name='on_action_removevm_triggered')
  814. def action_removevm_triggered(self):
  815. vm = self.get_selected_vm()
  816. assert not vm.is_running()
  817. assert not vm.installed_by_rpm
  818. self.qvm_collection.lock_db_for_reading()
  819. self.qvm_collection.load()
  820. self.qvm_collection.unlock_db()
  821. vm = self.qvm_collection[vm.qid]
  822. if vm.is_template():
  823. dependent_vms = self.qvm_collection.get_vms_based_on(vm.qid)
  824. if len(dependent_vms) > 0:
  825. QMessageBox.warning(
  826. None, "Warning!",
  827. "This Template VM cannot be removed, because there is at "
  828. "least one AppVM that is based on it.<br>"
  829. "<small>If you want to remove this Template VM and all "
  830. "the AppVMs based on it,"
  831. "you should first remove each individual AppVM that uses "
  832. "this template.</small>")
  833. return
  834. reply = QMessageBox.question(
  835. None, "VM Removal Confirmation",
  836. "Are you sure you want to remove the VM <b>'{0}'</b>?<br>"
  837. "<small>All data on this VM's private storage will be lost!</small>"
  838. .format(vm.name),
  839. QMessageBox.Yes | QMessageBox.Cancel)
  840. if reply == QMessageBox.Yes:
  841. thread_monitor = ThreadMonitor()
  842. thread = threading.Thread(target=self.do_remove_vm,
  843. args=(vm, thread_monitor))
  844. thread.daemon = True
  845. thread.start()
  846. progress = QProgressDialog(
  847. "Removing VM: <b>{0}</b>...".format(vm.name), "", 0, 0)
  848. progress.setCancelButton(None)
  849. progress.setModal(True)
  850. progress.show()
  851. while not thread_monitor.is_finished():
  852. app.processEvents()
  853. time.sleep(0.1)
  854. progress.hide()
  855. if thread_monitor.success:
  856. trayIcon.showMessage(
  857. "VM '{0}' has been removed.".format(vm.name), msecs=3000)
  858. else:
  859. QMessageBox.warning(None, "Error removing VM!",
  860. "ERROR: {0}".format(
  861. thread_monitor.error_msg))
  862. @staticmethod
  863. def do_remove_vm(vm, thread_monitor):
  864. qc = QubesVmCollection()
  865. qc.lock_db_for_writing()
  866. try:
  867. qc.load()
  868. vm = qc[vm.qid]
  869. # TODO: the following two conditions should really be checked
  870. # by qvm_collection.pop() overload...
  871. if vm.is_template() and \
  872. qc.default_template_qid == vm.qid:
  873. qc.default_template_qid = None
  874. if vm.is_netvm() and \
  875. qc.default_netvm_qid == vm.qid:
  876. qc.default_netvm_qid = None
  877. qc.pop(vm.qid)
  878. qc.save()
  879. vm.remove_from_disk()
  880. except Exception as ex:
  881. thread_monitor.set_error_msg(str(ex))
  882. finally:
  883. qc.unlock_db()
  884. thread_monitor.set_finished()
  885. @pyqtSlot(name='on_action_clonevm_triggered')
  886. def action_clonevm_triggered(self):
  887. vm = self.get_selected_vm()
  888. name_number = 1
  889. name_format = vm.name + '-clone-%d'
  890. while self.qvm_collection.get_vm_by_name(name_format % name_number):
  891. name_number += 1
  892. (clone_name, ok) = QInputDialog.getText(
  893. self, 'Qubes clone VM',
  894. 'Enter name for VM <b>' + vm.name + '</b> clone:',
  895. name_format % name_number)
  896. if not ok or clone_name == "":
  897. return
  898. thread_monitor = ThreadMonitor()
  899. thread = threading.Thread(target=self.do_clone_vm,
  900. args=(vm, str(clone_name), thread_monitor))
  901. thread.daemon = True
  902. thread.start()
  903. progress = QProgressDialog(
  904. "Cloning VM <b>{0}</b> to <b>{1}</b>...".format(vm.name,
  905. clone_name), "", 0,
  906. 0)
  907. progress.setCancelButton(None)
  908. progress.setModal(True)
  909. progress.show()
  910. while not thread_monitor.is_finished():
  911. app.processEvents()
  912. time.sleep(0.2)
  913. progress.hide()
  914. if not thread_monitor.success:
  915. QMessageBox.warning(None, "Error while cloning VM",
  916. "Exception while cloning:<br>{0}".format(
  917. thread_monitor.error_msg))
  918. @staticmethod
  919. def do_clone_vm(vm, dst_name, thread_monitor):
  920. dst_vm = None
  921. qc = QubesVmCollection()
  922. qc.lock_db_for_writing()
  923. qc.load()
  924. try:
  925. src_vm = qc[vm.qid]
  926. dst_vm = qc.add_new_vm(src_vm.__class__.__name__,
  927. name=dst_name,
  928. template=src_vm.template,
  929. installed_by_rpm=False)
  930. dst_vm.clone_attrs(src_vm)
  931. dst_vm.clone_disk_files(src_vm=src_vm, verbose=False)
  932. qc.save()
  933. except Exception as ex:
  934. if dst_vm:
  935. qc.pop(dst_vm.qid)
  936. dst_vm.remove_from_disk()
  937. thread_monitor.set_error_msg(str(ex))
  938. finally:
  939. qc.unlock_db()
  940. thread_monitor.set_finished()
  941. @pyqtSlot(name='on_action_resumevm_triggered')
  942. def action_resumevm_triggered(self):
  943. vm = self.get_selected_vm()
  944. if vm.get_power_state() in ["Paused", "Suspended"]:
  945. try:
  946. vm.resume()
  947. except Exception as ex:
  948. QMessageBox.warning(None, "Error unpausing VM!",
  949. "ERROR: {0}".format(ex))
  950. return
  951. assert not vm.is_running()
  952. thread_monitor = ThreadMonitor()
  953. thread = threading.Thread(target=self.do_start_vm,
  954. args=(vm, thread_monitor))
  955. thread.daemon = True
  956. thread.start()
  957. trayIcon.showMessage("Starting '{0}'...".format(vm.name), msecs=3000)
  958. while not thread_monitor.is_finished():
  959. app.processEvents()
  960. time.sleep(0.1)
  961. if thread_monitor.success:
  962. trayIcon.showMessage("VM '{0}' has been started.".format(vm.name),
  963. msecs=3000)
  964. else:
  965. trayIcon.showMessage(
  966. "Error starting VM <b>'{0}'</b>: {1}".format(
  967. vm.name, thread_monitor.error_msg),
  968. msecs=3000)
  969. self.set_error(vm.qid,
  970. "Error starting VM: %s" % thread_monitor.error_msg)
  971. @staticmethod
  972. def do_start_vm(vm, thread_monitor):
  973. try:
  974. vm.verify_files()
  975. vm.start()
  976. except Exception as ex:
  977. thread_monitor.set_error_msg(str(ex))
  978. thread_monitor.set_finished()
  979. return
  980. thread_monitor.set_finished()
  981. @pyqtSlot(name='on_action_startvm_tools_install_triggered')
  982. def action_startvm_tools_install_triggered(self):
  983. vm = self.get_selected_vm()
  984. assert not vm.is_running()
  985. thread_monitor = ThreadMonitor()
  986. thread = threading.Thread(target=self.do_start_vm_tools_install,
  987. args=(vm, thread_monitor))
  988. thread.daemon = True
  989. thread.start()
  990. trayIcon.showMessage("Starting '{0}'...".format(vm.name), msecs=3000)
  991. while not thread_monitor.is_finished():
  992. app.processEvents()
  993. time.sleep(0.1)
  994. if thread_monitor.success:
  995. trayIcon.showMessage("VM '{0}' has been started. Start Qubes "
  996. "Tools installation from attached CD"
  997. .format(vm.name), msecs=3000)
  998. else:
  999. trayIcon.showMessage(
  1000. "Error starting VM <b>'{0}'</b>: {1}"
  1001. .format(vm.name, thread_monitor.error_msg),
  1002. msecs=3000)
  1003. self.set_error(vm.qid,
  1004. "Error starting VM: %s" % thread_monitor.error_msg)
  1005. # noinspection PyMethodMayBeStatic
  1006. def do_start_vm_tools_install(self, vm, thread_monitor):
  1007. prev_drive = vm.drive
  1008. try:
  1009. vm.verify_files()
  1010. vm.drive = 'cdrom:dom0:/usr/lib/qubes/qubes-windows-tools.iso'
  1011. vm.start()
  1012. except Exception as ex:
  1013. thread_monitor.set_error_msg(str(ex))
  1014. thread_monitor.set_finished()
  1015. return
  1016. finally:
  1017. vm.drive = prev_drive
  1018. thread_monitor.set_finished()
  1019. @pyqtSlot(name='on_action_pausevm_triggered')
  1020. def action_pausevm_triggered(self):
  1021. vm = self.get_selected_vm()
  1022. assert vm.is_running()
  1023. try:
  1024. vm.pause()
  1025. except Exception as ex:
  1026. QMessageBox.warning(None, "Error pausing VM!",
  1027. "ERROR: {0}".format(ex))
  1028. return
  1029. @pyqtSlot(name='on_action_shutdownvm_triggered')
  1030. def action_shutdownvm_triggered(self):
  1031. vm = self.get_selected_vm()
  1032. assert vm.is_running()
  1033. self.blk_manager.check_if_serves_as_backend(vm)
  1034. reply = QMessageBox.question(
  1035. None, "VM Shutdown Confirmation",
  1036. "Are you sure you want to power down the VM <b>'{0}'</b>?<br>"
  1037. "<small>This will shutdown all the running applications "
  1038. "within this VM.</small>".format(vm.name),
  1039. QMessageBox.Yes | QMessageBox.Cancel)
  1040. app.processEvents()
  1041. if reply == QMessageBox.Yes:
  1042. self.shutdown_vm(vm)
  1043. def shutdown_vm(self, vm, shutdown_time=vm_shutdown_timeout):
  1044. try:
  1045. vm.shutdown()
  1046. except Exception as ex:
  1047. QMessageBox.warning(None, "Error shutting down VM!",
  1048. "ERROR: {0}".format(ex))
  1049. return
  1050. trayIcon.showMessage("VM '{0}' is shutting down...".format(vm.name),
  1051. msecs=3000)
  1052. self.shutdown_monitor[vm.qid] = VmShutdownMonitor(vm, shutdown_time)
  1053. # noinspection PyCallByClass,PyTypeChecker
  1054. QTimer.singleShot(shutdown_time, self.shutdown_monitor[
  1055. vm.qid].check_if_vm_has_shutdown)
  1056. @pyqtSlot(name='on_action_killvm_triggered')
  1057. def action_killvm_triggered(self):
  1058. vm = self.get_selected_vm()
  1059. assert vm.is_running() or vm.is_paused()
  1060. reply = QMessageBox.question(
  1061. None, "VM Kill Confirmation",
  1062. "Are you sure you want to kill the VM <b>'{0}'</b>?<br>"
  1063. "<small>This will end <b>(not shutdown!)</b> all the running "
  1064. "applications within this VM.</small>".format(vm.name),
  1065. QMessageBox.Yes | QMessageBox.Cancel,
  1066. QMessageBox.Cancel)
  1067. app.processEvents()
  1068. if reply == QMessageBox.Yes:
  1069. try:
  1070. vm.force_shutdown()
  1071. except Exception as ex:
  1072. QMessageBox.critical(
  1073. None, "Error while killing VM!",
  1074. "<b>An exception ocurred while killing {0}.</b><br>"
  1075. "ERROR: {1}".format(vm.name, ex))
  1076. return
  1077. trayIcon.showMessage("VM '{0}' killed!".format(vm.name), msecs=3000)
  1078. @pyqtSlot(name='on_action_settings_triggered')
  1079. def action_settings_triggered(self):
  1080. vm = self.get_selected_vm()
  1081. settings_window = VMSettingsWindow(vm, app, self.qvm_collection,
  1082. "basic")
  1083. settings_window.exec_()
  1084. @pyqtSlot(name='on_action_appmenus_triggered')
  1085. def action_appmenus_triggered(self):
  1086. vm = self.get_selected_vm()
  1087. settings_window = VMSettingsWindow(vm, app, self.qvm_collection,
  1088. "applications")
  1089. settings_window.exec_()
  1090. def update_audio_rec_info(self, vm):
  1091. vm.qubes_manager_state[QMVmState.AudioRecAvailable] = (
  1092. session_bus.interface().isServiceRegistered(
  1093. 'org.QubesOS.Audio.%s' % vm.name).value())
  1094. if vm.qubes_manager_state[QMVmState.AudioRecAvailable]:
  1095. vm.qubes_manager_state[
  1096. QMVmState.AudioRecAllowed] = self.get_audio_rec_allowed(vm.name)
  1097. else:
  1098. vm.qubes_manager_state[QMVmState.AudioRecAllowed] = False
  1099. # noinspection PyMethodMayBeStatic
  1100. def get_audio_rec_allowed(self, vmname):
  1101. properties = QDBusInterface('org.QubesOS.Audio.%s' % vmname,
  1102. '/org/qubesos/audio',
  1103. 'org.freedesktop.DBus.Properties',
  1104. session_bus)
  1105. current_audio = properties.call('Get', 'org.QubesOS.Audio',
  1106. 'RecAllowed')
  1107. if current_audio.type() == current_audio.ReplyMessage:
  1108. value = current_audio.arguments()[0].toPyObject().toBool()
  1109. return bool(value)
  1110. return False
  1111. @pyqtSlot(name='on_action_toggle_audio_input_triggered')
  1112. def action_toggle_audio_input_triggered(self):
  1113. vm = self.get_selected_vm()
  1114. properties = QDBusInterface('org.QubesOS.Audio.%s' % vm.name,
  1115. '/org/qubesos/audio',
  1116. 'org.freedesktop.DBus.Properties',
  1117. session_bus)
  1118. properties.call('Set', 'org.QubesOS.Audio', 'RecAllowed',
  1119. QDBusVariant(not self.get_audio_rec_allowed(vm.name)))
  1120. # icon will be updated based on dbus signal
  1121. @pyqtSlot(name='on_action_updatevm_triggered')
  1122. def action_updatevm_triggered(self):
  1123. vm = self.get_selected_vm()
  1124. if not vm.is_running():
  1125. reply = QMessageBox.question(
  1126. None, "VM Update Confirmation",
  1127. "<b>{0}</b><br>The VM has to be running to be updated.<br>"
  1128. "Do you want to start it?<br>".format(vm.name),
  1129. QMessageBox.Yes | QMessageBox.Cancel)
  1130. if reply != QMessageBox.Yes:
  1131. return
  1132. trayIcon.showMessage("Starting '{0}'...".format(vm.name),
  1133. msecs=3000)
  1134. app.processEvents()
  1135. thread_monitor = ThreadMonitor()
  1136. thread = threading.Thread(target=self.do_update_vm,
  1137. args=(vm, thread_monitor))
  1138. thread.daemon = True
  1139. thread.start()
  1140. progress = QProgressDialog(
  1141. "<b>{0}</b><br>Please wait for the updater to launch...".format(
  1142. vm.name), "", 0, 0)
  1143. progress.setCancelButton(None)
  1144. progress.setModal(True)
  1145. progress.show()
  1146. while not thread_monitor.is_finished():
  1147. app.processEvents()
  1148. time.sleep(0.2)
  1149. progress.hide()
  1150. if vm.qid != 0:
  1151. if not thread_monitor.success:
  1152. QMessageBox.warning(None, "Error VM update!",
  1153. "ERROR: {0}".format(
  1154. thread_monitor.error_msg))
  1155. @staticmethod
  1156. def do_update_vm(vm, thread_monitor):
  1157. try:
  1158. if vm.qid == 0:
  1159. subprocess.check_call(
  1160. ["/usr/bin/qubes-dom0-update", "--clean", "--gui"])
  1161. else:
  1162. vm_run_common_args = {
  1163. 'verbose': False,
  1164. 'autostart': True,
  1165. 'notify_function': lambda lvl, msg: trayIcon
  1166. .showMessage(msg, msecs=3000)}
  1167. vm.run("yum clean expire-cache", user='root', wait=True,
  1168. **vm_run_common_args)
  1169. vm.run("gpk-update-viewer;service qubes-update-check start -P",
  1170. **vm_run_common_args)
  1171. except Exception as ex:
  1172. thread_monitor.set_error_msg(str(ex))
  1173. thread_monitor.set_finished()
  1174. return
  1175. thread_monitor.set_finished()
  1176. @pyqtSlot(name='on_action_run_command_in_vm_triggered')
  1177. def action_run_command_in_vm_triggered(self):
  1178. vm = self.get_selected_vm()
  1179. (command_to_run, ok) = QInputDialog.getText(
  1180. self, 'Qubes command entry',
  1181. 'Run command in <b>' + vm.name + '</b>:')
  1182. if not ok or command_to_run == "":
  1183. return
  1184. thread_monitor = ThreadMonitor()
  1185. thread = threading.Thread(target=self.do_run_command_in_vm, args=(
  1186. vm, str(command_to_run), thread_monitor))
  1187. thread.daemon = True
  1188. thread.start()
  1189. while not thread_monitor.is_finished():
  1190. app.processEvents()
  1191. time.sleep(0.2)
  1192. if not thread_monitor.success:
  1193. QMessageBox.warning(None, "Error while running command",
  1194. "Exception while running command:<br>{0}".format(
  1195. thread_monitor.error_msg))
  1196. @staticmethod
  1197. def do_run_command_in_vm(vm, command_to_run, thread_monitor):
  1198. try:
  1199. vm.run(command_to_run, verbose=False, autostart=True,
  1200. notify_function=lambda lvl, msg: trayIcon.showMessage(
  1201. msg, msecs=3000))
  1202. except Exception as ex:
  1203. thread_monitor.set_error_msg(str(ex))
  1204. thread_monitor.set_finished()
  1205. @pyqtSlot(name='on_action_set_keyboard_layout_triggered')
  1206. def action_set_keyboard_layout_triggered(self):
  1207. vm = self.get_selected_vm()
  1208. vm.run('qubes-change-keyboard-layout', verbose=False)
  1209. @pyqtSlot(name='on_action_showallvms_triggered')
  1210. def action_showallvms_triggered(self):
  1211. self.show_inactive_vms = self.action_showallvms.isChecked()
  1212. self.showhide_vms(self.show_inactive_vms, self.show_internal_vms)
  1213. self.set_table_geom_size()
  1214. if self.settings_loaded:
  1215. self.manager_settings.setValue('view/show_inactive_vms',
  1216. self.show_inactive_vms)
  1217. self.manager_settings.sync()
  1218. @pyqtSlot(name='on_action_showinternalvms_triggered')
  1219. def action_showinternalvms_triggered(self):
  1220. self.show_internal_vms = self.action_showinternalvms.isChecked()
  1221. self.showhide_vms(self.show_inactive_vms, self.show_internal_vms)
  1222. self.set_table_geom_size()
  1223. if self.settings_loaded:
  1224. self.manager_settings.setValue('view/show_internal_vms',
  1225. self.show_internal_vms)
  1226. self.manager_settings.sync()
  1227. @pyqtSlot(name='on_action_editfwrules_triggered')
  1228. def action_editfwrules_triggered(self):
  1229. vm = self.get_selected_vm()
  1230. settings_window = VMSettingsWindow(vm, app, self.qvm_collection,
  1231. "firewall")
  1232. settings_window.exec_()
  1233. @pyqtSlot(name='on_action_global_settings_triggered')
  1234. def action_global_settings_triggered(self):
  1235. global_settings_window = GlobalSettingsWindow(app, self.qvm_collection)
  1236. global_settings_window.exec_()
  1237. @pyqtSlot(name='on_action_restore_triggered')
  1238. def action_restore_triggered(self):
  1239. restore_window = RestoreVMsWindow(app, self.qvm_collection,
  1240. self.blk_manager)
  1241. restore_window.exec_()
  1242. @pyqtSlot(name='on_action_backup_triggered')
  1243. def action_backup_triggered(self):
  1244. backup_window = BackupVMsWindow(app, self.qvm_collection,
  1245. self.blk_manager, self.shutdown_vm)
  1246. backup_window.exec_()
  1247. def showhide_menubar(self, checked):
  1248. self.menubar.setVisible(checked)
  1249. self.set_table_geom_size()
  1250. if not checked:
  1251. self.context_menu.addAction(self.action_menubar)
  1252. else:
  1253. self.context_menu.removeAction(self.action_menubar)
  1254. if self.settings_loaded:
  1255. self.manager_settings.setValue('view/menubar_visible', checked)
  1256. self.manager_settings.sync()
  1257. def showhide_toolbar(self, checked):
  1258. self.toolbar.setVisible(checked)
  1259. self.set_table_geom_size()
  1260. if not checked:
  1261. self.context_menu.addAction(self.action_toolbar)
  1262. else:
  1263. self.context_menu.removeAction(self.action_toolbar)
  1264. if self.settings_loaded:
  1265. self.manager_settings.setValue('view/toolbar_visible', checked)
  1266. self.manager_settings.sync()
  1267. def showhide_column(self, col_num, show):
  1268. self.table.setColumnHidden(col_num, not show)
  1269. self.set_table_geom_size()
  1270. val = 1 if show else -1
  1271. self.visible_columns_count += val
  1272. if self.visible_columns_count == 1:
  1273. # disable hiding the last one
  1274. for c in self.columns_actions:
  1275. if self.columns_actions[c].isChecked():
  1276. self.columns_actions[c].setEnabled(False)
  1277. break
  1278. elif self.visible_columns_count == 2 and val == 1:
  1279. # enable hiding previously disabled column
  1280. for c in self.columns_actions:
  1281. if not self.columns_actions[c].isEnabled():
  1282. self.columns_actions[c].setEnabled(True)
  1283. break
  1284. if self.settings_loaded:
  1285. col_name = [name for name in self.columns_indices.keys() if
  1286. self.columns_indices[name] == col_num][0]
  1287. self.manager_settings.setValue('columns/%s' % col_name, show)
  1288. self.manager_settings.sync()
  1289. def on_action_vm_type_toggled(self, checked):
  1290. self.showhide_column(self.columns_indices['Type'], checked)
  1291. def on_action_label_toggled(self, checked):
  1292. self.showhide_column(self.columns_indices['Label'], checked)
  1293. def on_action_name_toggled(self, checked):
  1294. self.showhide_column(self.columns_indices['Name'], checked)
  1295. def on_action_state_toggled(self, checked):
  1296. self.showhide_column(self.columns_indices['State'], checked)
  1297. def on_action_internal_toggled(self, checked):
  1298. self.showhide_column(self.columns_indices['Internal'], checked)
  1299. def on_action_ip_toggled(self, checked):
  1300. self.showhide_column(self.columns_indices['IP'], checked)
  1301. def on_action_backups_toggled(self, checked):
  1302. self.showhide_column(self.columns_indices['Backups'], checked)
  1303. def on_action_last_backup_toggled(self, checked):
  1304. self.showhide_column(self.columns_indices['Last backup'], checked)
  1305. def on_action_template_toggled(self, checked):
  1306. self.showhide_column(self.columns_indices['Template'], checked)
  1307. def on_action_netvm_toggled(self, checked):
  1308. self.showhide_column(self.columns_indices['NetVM'], checked)
  1309. def on_action_cpu_toggled(self, checked):
  1310. self.showhide_column(self.columns_indices['CPU'], checked)
  1311. def on_action_cpu_graph_toggled(self, checked):
  1312. self.showhide_column(self.columns_indices['CPU Graph'], checked)
  1313. def on_action_mem_toggled(self, checked):
  1314. self.showhide_column(self.columns_indices['MEM'], checked)
  1315. def on_action_mem_graph_toggled(self, checked):
  1316. self.showhide_column(self.columns_indices['MEM Graph'], checked)
  1317. def on_action_size_on_disk_toggled(self, checked):
  1318. self.showhide_column(self.columns_indices['Size'], checked)
  1319. @pyqtSlot(name='on_action_about_qubes_triggered')
  1320. def action_about_qubes_triggered(self):
  1321. release_file = open('/etc/qubes-release', 'r')
  1322. release = release_file.read()
  1323. release_file.close()
  1324. QMessageBox.about(self, "About...",
  1325. "<b>Qubes OS</b><br><br>%s" % release)
  1326. def createPopupMenu(self):
  1327. menu = QMenu()
  1328. menu.addAction(self.action_toolbar)
  1329. menu.addAction(self.action_menubar)
  1330. return menu
  1331. def open_tools_context_menu(self, widget, point):
  1332. self.tools_context_menu.exec_(widget.mapToGlobal(point))
  1333. @pyqtSlot('const QPoint&')
  1334. def open_context_menu(self, point):
  1335. vm = self.get_selected_vm()
  1336. running = vm.is_running()
  1337. # logs menu
  1338. self.logs_menu.clear()
  1339. if vm.qid == 0:
  1340. text = "/var/log/xen/console/hypervisor.log"
  1341. action = self.logs_menu.addAction(QIcon(":/log.png"), text)
  1342. action.setData(QVariant(text))
  1343. self.logs_menu.setEnabled(True)
  1344. else:
  1345. menu_empty = True
  1346. text = "/var/log/xen/console/guest-" + vm.name + ".log"
  1347. if os.path.exists(text):
  1348. action = self.logs_menu.addAction(QIcon(":/log.png"), text)
  1349. action.setData(QVariant(text))
  1350. menu_empty = False
  1351. text = "/var/log/xen/console/guest-" + vm.name + "-dm.log"
  1352. if os.path.exists(text):
  1353. action = self.logs_menu.addAction(QIcon(":/log.png"), text)
  1354. action.setData(QVariant(text))
  1355. menu_empty = False
  1356. text = "/var/log/qubes/guid." + vm.name + ".log"
  1357. if os.path.exists(text):
  1358. action = self.logs_menu.addAction(QIcon(":/log.png"), text)
  1359. action.setData(QVariant(text))
  1360. menu_empty = False
  1361. text = "/var/log/qubes/qrexec." + vm.name + ".log"
  1362. if os.path.exists(text):
  1363. action = self.logs_menu.addAction(QIcon(":/log.png"), text)
  1364. action.setData(QVariant(text))
  1365. menu_empty = False
  1366. self.logs_menu.setEnabled(not menu_empty)
  1367. # blk menu
  1368. if not running:
  1369. self.blk_menu.setEnabled(False)
  1370. else:
  1371. self.blk_menu.clear()
  1372. self.blk_menu.setEnabled(True)
  1373. self.blk_manager.blk_lock.acquire()
  1374. if len(self.blk_manager.attached_devs) > 0:
  1375. for d in self.blk_manager.attached_devs:
  1376. if (self.blk_manager.attached_devs[d]
  1377. ['attached_to']['vm'].qid == vm.qid):
  1378. text = "Detach " + d + " " + unicode(
  1379. self.blk_manager.attached_devs[d]['size']) + " " + \
  1380. self.blk_manager.attached_devs[d]['desc']
  1381. action = self.blk_menu.addAction(QIcon(":/remove.png"),
  1382. text)
  1383. action.setData(QVariant(d))
  1384. if len(self.blk_manager.free_devs) > 0:
  1385. for d in self.blk_manager.free_devs:
  1386. if d.startswith(vm.name):
  1387. continue
  1388. # skip partitions heuristic
  1389. if d[-1].isdigit() and \
  1390. d[0:-1] in self.blk_manager.current_blk:
  1391. continue
  1392. text = "Attach " + d + " " + unicode(
  1393. self.blk_manager.free_devs[d]['size']) + " " + \
  1394. self.blk_manager.free_devs[d]['desc']
  1395. action = self.blk_menu.addAction(QIcon(":/add.png"), text)
  1396. action.setData(QVariant(d))
  1397. self.blk_manager.blk_lock.release()
  1398. if self.blk_menu.isEmpty():
  1399. self.blk_menu.setEnabled(False)
  1400. self.context_menu.exec_(self.table.mapToGlobal(point))
  1401. @pyqtSlot('QAction *')
  1402. def show_log(self, action):
  1403. log = str(action.data().toString())
  1404. log_dialog = LogDialog(app, log)
  1405. log_dialog.exec_()
  1406. @pyqtSlot('QAction *')
  1407. def attach_dettach_device_triggered(self, action):
  1408. dev = str(action.data().toString())
  1409. vm = self.get_selected_vm()
  1410. self.blk_manager.blk_lock.acquire()
  1411. try:
  1412. if dev in self.blk_manager.attached_devs:
  1413. self.blk_manager.detach_device(vm, dev)
  1414. else:
  1415. self.blk_manager.attach_device(vm, dev)
  1416. self.blk_manager.blk_lock.release()
  1417. except QubesException as e:
  1418. self.blk_manager.blk_lock.release()
  1419. QMessageBox.critical(None, "Block attach/detach error!", str(e))
  1420. class QubesTrayIcon(QSystemTrayIcon):
  1421. def __init__(self, icon, blk_manager):
  1422. QSystemTrayIcon.__init__(self, icon)
  1423. self.menu = QMenu()
  1424. action_showmanager = self.create_action("Open VM Manager",
  1425. slot=show_manager, icon="qubes")
  1426. action_backup = self.create_action("Make backup")
  1427. action_preferences = self.create_action("Preferences")
  1428. action_set_netvm = self.create_action("Set default NetVM",
  1429. icon="networking")
  1430. action_sys_info = self.create_action("System Info", icon="dom0")
  1431. action_exit = self.create_action("Exit", slot=exit_app)
  1432. action_backup.setDisabled(True)
  1433. action_preferences.setDisabled(True)
  1434. action_set_netvm.setDisabled(True)
  1435. action_sys_info.setDisabled(True)
  1436. self.blk_manager = blk_manager
  1437. self.blk_menu = QMenu(self.menu)
  1438. self.blk_menu.setTitle("Block devices")
  1439. action_blk_menu = self.create_action("Block devices")
  1440. action_blk_menu.setMenu(self.blk_menu)
  1441. self.add_actions(self.menu, (action_showmanager,
  1442. action_blk_menu,
  1443. action_backup,
  1444. action_sys_info,
  1445. None,
  1446. action_preferences,
  1447. action_set_netvm,
  1448. None,
  1449. action_exit))
  1450. self.setContextMenu(self.menu)
  1451. self.connect(self,
  1452. SIGNAL("activated (QSystemTrayIcon::ActivationReason)"),
  1453. self.icon_clicked)
  1454. self.tray_notifier_type = None
  1455. self.tray_notifier = QDBusInterface("org.freedesktop.Notifications",
  1456. "/org/freedesktop/Notifications",
  1457. "org.freedesktop.Notifications",
  1458. session_bus)
  1459. srv_info = self.tray_notifier.call("GetServerInformation")
  1460. if srv_info.type() == QDBusMessage.ReplyMessage and \
  1461. len(srv_info.arguments()) > 1:
  1462. self.tray_notifier_type = srv_info.arguments()[1]
  1463. if os.path.exists(table_widgets.qubes_dom0_updates_stat_file):
  1464. self.showMessage("Qubes dom0 updates available.", msecs=0)
  1465. def update_blk_menu(self):
  1466. global manager_window
  1467. def create_vm_submenu(dev):
  1468. blk_vm_menu = QMenu(self.blk_menu)
  1469. blk_vm_menu.triggered.connect(
  1470. lambda a, trig_dev=dev: self.attach_device_triggered(a,
  1471. trig_dev))
  1472. for this_vm in sorted(manager_window.qvm_collection.values(),
  1473. key=lambda x: x.name):
  1474. if not vm.is_running():
  1475. continue
  1476. if this_vm.qid == 0:
  1477. # skip dom0 to prevent (fatal) mistakes
  1478. continue
  1479. this_action = blk_vm_menu.addAction(QIcon(":/add.png"),
  1480. this_vm.name)
  1481. this_action.setData(QVariant(this_vm))
  1482. return blk_vm_menu
  1483. self.blk_menu.clear()
  1484. self.blk_menu.setEnabled(True)
  1485. self.blk_manager.blk_lock.acquire()
  1486. if len(self.blk_manager.attached_devs) > 0:
  1487. for d in self.blk_manager.attached_devs:
  1488. vm = self.blk_manager.attached_devs[d]['attached_to']['vm']
  1489. text = "Detach %s %s (%s) from %s" % (
  1490. d,
  1491. self.blk_manager.attached_devs[d]['desc'],
  1492. unicode(self.blk_manager.attached_devs[d]['size']),
  1493. vm.name)
  1494. action = self.blk_menu.addAction(QIcon(":/remove.png"), text)
  1495. action.setData(QVariant(d))
  1496. action.triggered.connect(
  1497. lambda b, a=action: self.dettach_device_triggered(a))
  1498. if len(self.blk_manager.free_devs) > 0:
  1499. for d in self.blk_manager.free_devs:
  1500. # skip partitions heuristic
  1501. if d[-1].isdigit() and d[0:-1] in self.blk_manager.current_blk:
  1502. continue
  1503. text = "Attach %s %s %s" % (
  1504. d,
  1505. unicode(self.blk_manager.free_devs[d]['size']),
  1506. self.blk_manager.free_devs[d]['desc']
  1507. )
  1508. action = self.blk_menu.addAction(QIcon(":/add.png"), text)
  1509. action.setMenu(create_vm_submenu(d))
  1510. self.blk_manager.blk_lock.release()
  1511. if self.blk_menu.isEmpty():
  1512. self.blk_menu.setEnabled(False)
  1513. @pyqtSlot('QAction *')
  1514. def attach_device_triggered(self, action, dev):
  1515. vm = action.data().toPyObject()
  1516. self.blk_manager.blk_lock.acquire()
  1517. try:
  1518. self.blk_manager.attach_device(vm, dev)
  1519. self.blk_manager.blk_lock.release()
  1520. except QubesException as e:
  1521. self.blk_manager.blk_lock.release()
  1522. QMessageBox.critical(None, "Block attach/detach error!", str(e))
  1523. @pyqtSlot('QAction *')
  1524. def dettach_device_triggered(self, action):
  1525. dev = str(action.data().toString())
  1526. vm = self.blk_manager.attached_devs[dev]['attached_to']['vm']
  1527. self.blk_manager.blk_lock.acquire()
  1528. try:
  1529. self.blk_manager.detach_device(vm, dev)
  1530. self.blk_manager.blk_lock.release()
  1531. except QubesException as e:
  1532. self.blk_manager.blk_lock.release()
  1533. QMessageBox.critical(None, "Block attach/detach error!", str(e))
  1534. def icon_clicked(self, reason):
  1535. if reason == QSystemTrayIcon.Context:
  1536. self.update_blk_menu()
  1537. # Handle the right click normally, i.e. display the context menu
  1538. return
  1539. else:
  1540. bring_manager_to_front()
  1541. # noinspection PyMethodMayBeStatic
  1542. def add_actions(self, target, actions):
  1543. for action in actions:
  1544. if action is None:
  1545. target.addSeparator()
  1546. else:
  1547. target.addAction(action)
  1548. def showMessage(self, message, msecs, **kwargs):
  1549. # QtDBus bindings doesn't use introspection to get proper method
  1550. # parameters types, so must cast explicitly
  1551. v_replace_id = QVariant(0)
  1552. v_replace_id.convert(QVariant.UInt)
  1553. v_actions = QVariant([])
  1554. v_actions.convert(QVariant.StringList)
  1555. if self.tray_notifier_type == "KDE":
  1556. message = message.replace('\n', '<br/>\n')
  1557. self.tray_notifier.call("Notify", "Qubes", v_replace_id,
  1558. "qubes-manager", "Qubes VM Manager",
  1559. message, v_actions, QVariant.fromMap({}), msecs)
  1560. def create_action(self, text, slot=None, shortcut=None, icon=None,
  1561. tip=None, checkable=False, signal="triggered()"):
  1562. action = QAction(text, self)
  1563. if icon is not None:
  1564. action.setIcon(QIcon(":/%s.png" % icon))
  1565. if shortcut is not None:
  1566. action.setShortcut(shortcut)
  1567. if tip is not None:
  1568. action.setToolTip(tip)
  1569. action.setStatusTip(tip)
  1570. if slot is not None:
  1571. self.connect(action, SIGNAL(signal), slot)
  1572. if checkable:
  1573. action.setCheckable(True)
  1574. return action
  1575. class QubesDbusNotifyServerAdaptor(QDBusAbstractAdaptor):
  1576. """ This provides the DBus adaptor to the outside world"""
  1577. Q_CLASSINFO("D-Bus Interface", dbus_interface)
  1578. @pyqtSlot(str, str)
  1579. def notify_error(self, vmname, message):
  1580. vm = self.parent().qvm_collection.get_vm_by_name(vmname)
  1581. if vm:
  1582. self.parent().set_error(vm.qid, message)
  1583. else:
  1584. # ignore VM-not-found error
  1585. pass
  1586. @pyqtSlot(str, str)
  1587. def clear_error_exact(self, vmname, message):
  1588. vm = self.parent().qvm_collection.get_vm_by_name(vmname)
  1589. if vm:
  1590. self.parent().clear_error_exact(vm.qid, message)
  1591. else:
  1592. # ignore VM-not-found error
  1593. pass
  1594. @pyqtSlot(str)
  1595. def clear_error(self, vmname):
  1596. vm = self.parent().qvm_collection.get_vm_by_name(vmname)
  1597. if vm:
  1598. self.parent().clear_error(vm.qid)
  1599. else:
  1600. # ignore VM-not-found error
  1601. pass
  1602. @pyqtSlot()
  1603. def show_manager(self):
  1604. bring_manager_to_front()
  1605. def get_frame_size():
  1606. w = 0
  1607. h = 0
  1608. cmd = ['/usr/bin/xprop', '-name', 'Qubes VM Manager', '|', 'grep',
  1609. '_NET_FRAME_EXTENTS']
  1610. xprop = subprocess.Popen(cmd, stdout=subprocess.PIPE)
  1611. for l in xprop.stdout:
  1612. line = l.split('=')
  1613. if len(line) == 2:
  1614. line = line[1].strip().split(',')
  1615. if len(line) == 4:
  1616. w = int(line[0].strip()) + int(line[1].strip())
  1617. h = int(line[2].strip()) + int(line[3].strip())
  1618. break
  1619. # in case of some weird window managers we have to assume sth...
  1620. if w <= 0:
  1621. w = 10
  1622. if h <= 0:
  1623. h = 30
  1624. manager_window.frame_width = w
  1625. manager_window.frame_height = h
  1626. return
  1627. def show_manager():
  1628. manager_window.show()
  1629. manager_window.set_table_geom_size()
  1630. manager_window.repaint()
  1631. manager_window.update_table(out_of_schedule=True)
  1632. app.processEvents()
  1633. get_frame_size()
  1634. # print manager_window.frame_width, " x ", manager_window.frame_height
  1635. manager_window.set_table_geom_size()
  1636. def bring_manager_to_front():
  1637. if manager_window.isVisible():
  1638. subprocess.check_call(
  1639. ['/usr/bin/wmctrl', '-R', str(manager_window.windowTitle())])
  1640. else:
  1641. show_manager()
  1642. def show_running_manager_via_dbus():
  1643. global system_bus
  1644. if system_bus is None:
  1645. system_bus = QDBusConnection.systemBus()
  1646. qubes_manager = QDBusInterface('org.qubesos.QubesManager',
  1647. '/org/qubesos/QubesManager',
  1648. 'org.qubesos.QubesManager', system_bus)
  1649. qubes_manager.call('show_manager')
  1650. def exit_app():
  1651. notifier.stop()
  1652. app.exit()
  1653. # Bases on the original code by:
  1654. # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
  1655. def handle_exception(exc_type, exc_value, exc_traceback):
  1656. import os.path
  1657. import traceback
  1658. filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
  1659. filename = os.path.basename(filename)
  1660. error = "%s: %s" % ( exc_type.__name__, exc_value )
  1661. strace = ""
  1662. stacktrace = traceback.extract_tb(exc_traceback)
  1663. while len(stacktrace) > 0:
  1664. (filename, line, func, txt) = stacktrace.pop()
  1665. strace += "----\n"
  1666. strace += "line: %s\n" % txt
  1667. strace += "func: %s\n" % func
  1668. strace += "line no.: %d\n" % line
  1669. strace += "file: %s\n" % filename
  1670. msg_box = QMessageBox()
  1671. msg_box.setDetailedText(strace)
  1672. msg_box.setIcon(QMessageBox.Critical)
  1673. msg_box.setWindowTitle("Houston, we have a problem...")
  1674. msg_box.setText(
  1675. "Whoops. A critical error has occured. This is most likely a bug "
  1676. "in Qubes Manager.<br><br>"
  1677. "<b><i>%s</i></b>" % error +
  1678. "<br/>at line <b>%d</b><br/>of file %s.<br/><br/>"
  1679. % (line, filename))
  1680. msg_box.exec_()
  1681. def sighup_handler(signum, frame):
  1682. os.execl("/usr/bin/qubes-manager", "qubes-manager")
  1683. def main():
  1684. # Avoid starting more than one instance of the app
  1685. lock = QubesDaemonPidfile("qubes-manager")
  1686. if lock.pidfile_exists():
  1687. if lock.read_pid() == os.getpid():
  1688. pass
  1689. elif lock.pidfile_is_stale():
  1690. lock.remove_pidfile()
  1691. print "Removed stale pidfile " \
  1692. "(has the previous daemon instance crashed?)."
  1693. else:
  1694. show_running_manager_via_dbus()
  1695. exit(0)
  1696. lock.create_pidfile()
  1697. signal.signal(signal.SIGHUP, sighup_handler)
  1698. global qubes_host
  1699. qubes_host = QubesHost()
  1700. global app
  1701. app = QApplication(sys.argv)
  1702. app.setOrganizationName("The Qubes Project")
  1703. app.setOrganizationDomain("http://qubes-os.org")
  1704. app.setApplicationName("Qubes VM Manager")
  1705. app.setWindowIcon(QIcon.fromTheme("qubes-manager"))
  1706. app.setAttribute(Qt.AA_DontShowIconsInMenus, False)
  1707. sys.excepthook = handle_exception
  1708. global session_bus
  1709. session_bus = QDBusConnection.sessionBus()
  1710. qvm_collection = QubesVmCollection()
  1711. qvm_collection.lock_db_for_reading()
  1712. qvm_collection.load()
  1713. qvm_collection.unlock_db()
  1714. blk_manager = QubesBlockDevicesManager(qvm_collection)
  1715. global trayIcon
  1716. trayIcon = QubesTrayIcon(QIcon.fromTheme("qubes-manager"), blk_manager)
  1717. global manager_window
  1718. manager_window = VmManagerWindow(qvm_collection, blk_manager)
  1719. global wm
  1720. wm = WatchManager()
  1721. global notifier
  1722. notifier = ThreadedNotifier(wm, QubesManagerFileWatcher(
  1723. manager_window.mark_table_for_update))
  1724. notifier.start()
  1725. wm.add_watch(system_path["qubes_store_filename"],
  1726. EventsCodes.OP_FLAGS.get('IN_MODIFY'))
  1727. wm.add_watch(os.path.dirname(system_path["qubes_store_filename"]),
  1728. EventsCodes.OP_FLAGS.get('IN_MOVED_TO'))
  1729. wm.add_watch(qubes_clipboard_info_file,
  1730. EventsCodes.OP_FLAGS.get('IN_CLOSE_WRITE'))
  1731. wm.add_watch(os.path.dirname(qubes_clipboard_info_file),
  1732. EventsCodes.OP_FLAGS.get('IN_CREATE'))
  1733. wm.add_watch(os.path.dirname(table_widgets.qubes_dom0_updates_stat_file),
  1734. EventsCodes.OP_FLAGS.get('IN_CREATE'))
  1735. global system_bus
  1736. system_bus = QDBusConnection.systemBus()
  1737. system_bus.registerService('org.qubesos.QubesManager')
  1738. system_bus.registerObject(dbus_object_path, manager_window)
  1739. trayIcon.show()
  1740. show_manager()
  1741. app.exec_()
  1742. lock.remove_pidfile()
  1743. trayIcon = None
  1744. if __name__ == "__main__":
  1745. main()