mime.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # The Qubes OS Project, http://www.qubes-os.org
  5. #
  6. # Copyright (C) 2016 Marek Marczykowski-Górecki
  7. # <marmarek@invisiblethingslab.com>
  8. #
  9. # This library is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU Lesser General Public
  11. # License as published by the Free Software Foundation; either
  12. # version 2.1 of the License, or (at your option) any later version.
  13. #
  14. # This library is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  17. # Lesser General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU Lesser General Public
  20. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  21. #
  22. #
  23. from distutils import spawn
  24. import re
  25. import subprocess
  26. import time
  27. import unittest
  28. import itertools
  29. import asyncio
  30. import sys
  31. import qubes.tests
  32. import qubes
  33. @unittest.skipUnless(
  34. spawn.find_executable('xprop') and
  35. spawn.find_executable('xdotool') and
  36. spawn.find_executable('wmctrl'),
  37. "xprop or xdotool or wmctrl not installed")
  38. class TC_50_MimeHandlers:
  39. def setUp(self):
  40. super(TC_50_MimeHandlers, self).setUp()
  41. if self.template.startswith('whonix-gw') or 'minimal' in self.template:
  42. raise unittest.SkipTest(
  43. 'Template {} not supported by this test'.format(self.template))
  44. self.source_vmname = self.make_vm_name('source')
  45. self.source_vm = self.app.add_new_vm("AppVM",
  46. template=self.template,
  47. name=self.source_vmname,
  48. label='red')
  49. self.loop.run_until_complete(self.source_vm.create_on_disk())
  50. self.target_vmname = self.make_vm_name('target')
  51. self.target_vm = self.app.add_new_vm("AppVM",
  52. template=self.template,
  53. name=self.target_vmname,
  54. label='red')
  55. self.loop.run_until_complete(self.target_vm.create_on_disk())
  56. self.target_vm.template_for_dispvms = True
  57. self.source_vm.default_dispvm = self.target_vm
  58. done, not_done = self.loop.run_until_complete(asyncio.wait([
  59. self.source_vm.start(),
  60. self.target_vm.start()]))
  61. for result in itertools.chain(done, not_done):
  62. # catch any exceptions
  63. result.result()
  64. def get_window_class(self, winid, dispvm=False):
  65. (vm_winid, _) = subprocess.Popen(
  66. ['xprop', '-id', winid, '_QUBES_VMWINDOWID'],
  67. stdout=subprocess.PIPE
  68. ).communicate()
  69. vm_winid = vm_winid.decode().split("#")[1].strip('\n" ')
  70. if dispvm:
  71. (vmname, _) = subprocess.Popen(
  72. ['xprop', '-id', winid, '_QUBES_VMNAME'],
  73. stdout=subprocess.PIPE
  74. ).communicate()
  75. vmname = vmname.decode().split("=")[1].strip('\n" ')
  76. vm = self.app.domains[vmname]
  77. else:
  78. vm = self.target_vm
  79. window_class = None
  80. while window_class is None:
  81. try:
  82. window_class, _ = self.loop.run_until_complete(
  83. vm.run_for_stdio('xprop -id {} WM_CLASS'.format(vm_winid)))
  84. except subprocess.CalledProcessError as e:
  85. if e.returncode == 127:
  86. self.skipTest('xprop not installed')
  87. self.fail(
  88. "xprop -id {} WM_CLASS failed: {}".format(
  89. vm_winid, e.stderr.decode()))
  90. if b'not found' in window_class:
  91. # WM_CLASS not set yet, wait a little
  92. time.sleep(0.1)
  93. window_class = None
  94. # output: WM_CLASS(STRING) = "gnome-terminal-server", "Gnome-terminal"
  95. try:
  96. window_class = window_class.decode()
  97. window_class = window_class.split("=")[1].split(",")[0].strip('\n" ')
  98. except IndexError:
  99. raise Exception(
  100. "Unexpected output from xprop: '{}'".format(window_class))
  101. return window_class
  102. def open_file_and_check_viewer(self, filename, expected_app_titles,
  103. expected_app_classes, dispvm=False):
  104. if dispvm:
  105. p = self.loop.run_until_complete(self.source_vm.run(
  106. "qvm-open-in-dvm {}".format(filename), stdout=subprocess.PIPE))
  107. vmpattern = "disp[0-9]*"
  108. else:
  109. p = self.loop.run_until_complete(self.source_vm.run(
  110. "qvm-open-in-vm {} {}".format(self.target_vmname, filename),
  111. stdout=subprocess.PIPE))
  112. vmpattern = self.target_vmname
  113. wait_count = 0
  114. winid = None
  115. with self.qrexec_policy('qubes.OpenInVM', self.source_vm.name,
  116. self.target_vmname):
  117. with self.qrexec_policy('qubes.OpenURL', self.source_vm.name,
  118. self.target_vmname):
  119. while True:
  120. search = subprocess.Popen(['xdotool', 'search',
  121. '--onlyvisible', '--class', vmpattern],
  122. stdout=subprocess.PIPE,
  123. stderr=subprocess.DEVNULL)
  124. retcode = search.wait()
  125. if retcode == 0:
  126. winid = search.stdout.read().strip()
  127. # get window title
  128. (window_title, _) = subprocess.Popen(
  129. ['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
  130. communicate()
  131. window_title = window_title.decode('utf8').strip()
  132. # ignore LibreOffice splash screen and window with no title
  133. # set yet
  134. if window_title and \
  135. not window_title.startswith("LibreOffice") and\
  136. not window_title.startswith("NetworkManager") and\
  137. not window_title == 'VMapp command':
  138. break
  139. wait_count += 1
  140. if wait_count > 100:
  141. self.fail("Timeout while waiting for editor window")
  142. self.loop.run_until_complete(asyncio.sleep(0.3))
  143. # get window class
  144. window_class = self.get_window_class(winid, dispvm)
  145. # close the window - we've got the window class, it is no longer needed
  146. subprocess.check_call(['wmctrl', '-i', '-c', winid])
  147. p.wait()
  148. self.wait_for_window(window_title, show=False)
  149. def check_matches(obj, patterns):
  150. return any((pat.search(obj) if isinstance(pat, type(re.compile('')))
  151. else pat in obj) for pat in patterns)
  152. if not check_matches(window_title, expected_app_titles) and \
  153. not check_matches(window_class, expected_app_classes):
  154. self.fail("Opening file {} resulted in window '{} ({})', which is "
  155. "none of {!r} ({!r})".format(
  156. filename, window_title, window_class,
  157. expected_app_titles, expected_app_classes))
  158. def prepare_txt(self, filename):
  159. self.loop.run_until_complete(
  160. self.source_vm.run_for_stdio("cat > {}".format(filename),
  161. input=b'This is test\n'))
  162. def prepare_pdf(self, filename):
  163. self.prepare_txt("/tmp/source.txt")
  164. cmd = "convert text:/tmp/source.txt {}".format(filename)
  165. try:
  166. self.loop.run_until_complete(
  167. self.source_vm.run_for_stdio(cmd))
  168. except subprocess.CalledProcessError as e:
  169. self.fail('{} failed: {}'.format(cmd, e.stderr.decode()))
  170. def prepare_doc(self, filename):
  171. self.prepare_txt("/tmp/source.txt")
  172. cmd = "unoconv -f doc -o {} /tmp/source.txt".format(filename)
  173. try:
  174. self.loop.run_until_complete(
  175. self.source_vm.run_for_stdio(cmd))
  176. except subprocess.CalledProcessError as e:
  177. if e.returncode == 127:
  178. self.skipTest("unoconv not installed".format(cmd))
  179. self.skipTest("Failed to run '{}': {}".format(cmd,
  180. e.stderr.decode()))
  181. def prepare_pptx(self, filename):
  182. self.prepare_txt("/tmp/source.txt")
  183. cmd = "unoconv -f pptx -o {} /tmp/source.txt".format(filename)
  184. try:
  185. self.loop.run_until_complete(
  186. self.source_vm.run_for_stdio(cmd))
  187. except subprocess.CalledProcessError as e:
  188. if e.returncode == 127:
  189. self.skipTest("unoconv not installed".format(cmd))
  190. self.skipTest("Failed to run '{}': {}".format(cmd,
  191. e.stderr.decode()))
  192. def prepare_png(self, filename):
  193. self.prepare_txt("/tmp/source.txt")
  194. cmd = "convert text:/tmp/source.txt {}".format(filename)
  195. try:
  196. self.loop.run_until_complete(
  197. self.source_vm.run_for_stdio(cmd))
  198. except subprocess.CalledProcessError as e:
  199. if e.returncode == 127:
  200. self.skipTest("convert not installed".format(cmd))
  201. self.skipTest("Failed to run '{}': {}".format(cmd,
  202. e.stderr.decode()))
  203. def prepare_jpg(self, filename):
  204. self.prepare_txt("/tmp/source.txt")
  205. cmd = "convert text:/tmp/source.txt {}".format(filename)
  206. try:
  207. self.loop.run_until_complete(
  208. self.source_vm.run_for_stdio(cmd))
  209. except subprocess.CalledProcessError as e:
  210. if e.returncode == 127:
  211. self.skipTest("convert not installed".format(cmd))
  212. self.skipTest("Failed to run '{}': {}".format(cmd,
  213. e.stderr.decode()))
  214. def test_000_txt(self):
  215. filename = "/home/user/test_file.txt"
  216. self.prepare_txt(filename)
  217. self.open_file_and_check_viewer(filename, ["vim", "user@"],
  218. ["gedit", "emacs", "libreoffice"])
  219. def test_001_pdf(self):
  220. filename = "/home/user/test_file.pdf"
  221. self.prepare_pdf(filename)
  222. self.open_file_and_check_viewer(filename, [],
  223. ["evince"])
  224. def test_002_doc(self):
  225. filename = "/home/user/test_file.doc"
  226. self.prepare_doc(filename)
  227. self.open_file_and_check_viewer(filename, [],
  228. ["libreoffice", "abiword"])
  229. def test_003_pptx(self):
  230. filename = "/home/user/test_file.pptx"
  231. self.prepare_pptx(filename)
  232. self.open_file_and_check_viewer(filename, [],
  233. ["libreoffice"])
  234. def test_004_png(self):
  235. filename = "/home/user/test_file.png"
  236. self.prepare_png(filename)
  237. self.open_file_and_check_viewer(filename, [],
  238. ["shotwell", "eog", "display"])
  239. def test_005_jpg(self):
  240. filename = "/home/user/test_file.jpg"
  241. self.prepare_jpg(filename)
  242. self.open_file_and_check_viewer(filename, [],
  243. ["shotwell", "eog", "display"])
  244. def test_006_jpeg(self):
  245. filename = "/home/user/test_file.jpeg"
  246. self.prepare_jpg(filename)
  247. self.open_file_and_check_viewer(filename, [],
  248. ["shotwell", "eog", "display"])
  249. def test_010_url(self):
  250. self.open_file_and_check_viewer("https://www.qubes-os.org/", [],
  251. ["Firefox", "Iceweasel", "Navigator"])
  252. def test_100_txt_dispvm(self):
  253. filename = "/home/user/test_file.txt"
  254. self.prepare_txt(filename)
  255. self.open_file_and_check_viewer(filename, ["vim", "user@"],
  256. ["gedit", "emacs", "libreoffice"],
  257. dispvm=True)
  258. def test_101_pdf_dispvm(self):
  259. filename = "/home/user/test_file.pdf"
  260. self.prepare_pdf(filename)
  261. self.open_file_and_check_viewer(filename, [],
  262. ["evince"],
  263. dispvm=True)
  264. def test_102_doc_dispvm(self):
  265. filename = "/home/user/test_file.doc"
  266. self.prepare_doc(filename)
  267. self.open_file_and_check_viewer(filename, [],
  268. ["libreoffice", "abiword"],
  269. dispvm=True)
  270. def test_103_pptx_dispvm(self):
  271. filename = "/home/user/test_file.pptx"
  272. self.prepare_pptx(filename)
  273. self.open_file_and_check_viewer(filename, [],
  274. ["libreoffice"],
  275. dispvm=True)
  276. def test_104_png_dispvm(self):
  277. filename = "/home/user/test_file.png"
  278. self.prepare_png(filename)
  279. self.open_file_and_check_viewer(filename, [],
  280. ["shotwell", "eog", "display"],
  281. dispvm=True)
  282. def test_105_jpg_dispvm(self):
  283. filename = "/home/user/test_file.jpg"
  284. self.prepare_jpg(filename)
  285. self.open_file_and_check_viewer(filename, [],
  286. ["shotwell", "eog", "display"],
  287. dispvm=True)
  288. def test_106_jpeg_dispvm(self):
  289. filename = "/home/user/test_file.jpeg"
  290. self.prepare_jpg(filename)
  291. self.open_file_and_check_viewer(filename, [],
  292. ["shotwell", "eog", "display"],
  293. dispvm=True)
  294. def test_110_url_dispvm(self):
  295. self.open_file_and_check_viewer("https://www.qubes-os.org/", [],
  296. ["Firefox", "Iceweasel", "Navigator"],
  297. dispvm=True)
  298. def load_tests(loader, tests, pattern):
  299. tests.addTests(loader.loadTestsFromNames(
  300. qubes.tests.create_testcases_for_templates('TC_50_MimeHandlers',
  301. TC_50_MimeHandlers, qubes.tests.SystemTestCase,
  302. module=sys.modules[__name__])))
  303. return tests