test_unicode.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
  4. # Use of this source code is governed by a BSD-style license that can be
  5. # found in the LICENSE file.
  6. """Notes about unicode handling in psutil
  7. ======================================.
  8. Starting from version 5.3.0 psutil adds unicode support, see:
  9. https://github.com/giampaolo/psutil/issues/1040
  10. The notes below apply to *any* API returning a string such as
  11. process exe(), cwd() or username():
  12. * all strings are encoded by using the OS filesystem encoding
  13. (sys.getfilesystemencoding()) which varies depending on the platform
  14. (e.g. "UTF-8" on macOS, "mbcs" on Win)
  15. * no API call is supposed to crash with UnicodeDecodeError
  16. * instead, in case of badly encoded data returned by the OS, the
  17. following error handlers are used to replace the corrupted characters in
  18. the string:
  19. * Python 3: sys.getfilesystemencodeerrors() (PY 3.6+) or
  20. "surrogatescape" on POSIX and "replace" on Windows
  21. * Python 2: "replace"
  22. * on Python 2 all APIs return bytes (str type), never unicode
  23. * on Python 2, you can go back to unicode by doing:
  24. >>> unicode(p.exe(), sys.getdefaultencoding(), errors="replace")
  25. For a detailed explanation of how psutil handles unicode see #1040.
  26. Tests
  27. =====
  28. List of APIs returning or dealing with a string:
  29. ('not tested' means they are not tested to deal with non-ASCII strings):
  30. * Process.cmdline()
  31. * Process.connections('unix')
  32. * Process.cwd()
  33. * Process.environ()
  34. * Process.exe()
  35. * Process.memory_maps()
  36. * Process.name()
  37. * Process.open_files()
  38. * Process.username() (not tested)
  39. * disk_io_counters() (not tested)
  40. * disk_partitions() (not tested)
  41. * disk_usage(str)
  42. * net_connections('unix')
  43. * net_if_addrs() (not tested)
  44. * net_if_stats() (not tested)
  45. * net_io_counters() (not tested)
  46. * sensors_fans() (not tested)
  47. * sensors_temperatures() (not tested)
  48. * users() (not tested)
  49. * WindowsService.binpath() (not tested)
  50. * WindowsService.description() (not tested)
  51. * WindowsService.display_name() (not tested)
  52. * WindowsService.name() (not tested)
  53. * WindowsService.status() (not tested)
  54. * WindowsService.username() (not tested)
  55. In here we create a unicode path with a funky non-ASCII name and (where
  56. possible) make psutil return it back (e.g. on name(), exe(), open_files(),
  57. etc.) and make sure that:
  58. * psutil never crashes with UnicodeDecodeError
  59. * the returned path matches
  60. """
  61. import os
  62. import shutil
  63. import traceback
  64. import unittest
  65. import warnings
  66. from contextlib import closing
  67. import psutil
  68. from psutil import BSD
  69. from psutil import POSIX
  70. from psutil import WINDOWS
  71. from psutil._compat import PY3
  72. from psutil._compat import u
  73. from psutil.tests import APPVEYOR
  74. from psutil.tests import ASCII_FS
  75. from psutil.tests import CI_TESTING
  76. from psutil.tests import HAS_CONNECTIONS_UNIX
  77. from psutil.tests import HAS_ENVIRON
  78. from psutil.tests import HAS_MEMORY_MAPS
  79. from psutil.tests import INVALID_UNICODE_SUFFIX
  80. from psutil.tests import PYPY
  81. from psutil.tests import TESTFN_PREFIX
  82. from psutil.tests import UNICODE_SUFFIX
  83. from psutil.tests import PsutilTestCase
  84. from psutil.tests import bind_unix_socket
  85. from psutil.tests import chdir
  86. from psutil.tests import copyload_shared_lib
  87. from psutil.tests import create_exe
  88. from psutil.tests import get_testfn
  89. from psutil.tests import safe_mkdir
  90. from psutil.tests import safe_rmpath
  91. from psutil.tests import serialrun
  92. from psutil.tests import skip_on_access_denied
  93. from psutil.tests import spawn_testproc
  94. from psutil.tests import terminate
  95. if APPVEYOR:
  96. def safe_rmpath(path): # NOQA
  97. # TODO - this is quite random and I'm not sure why it happens,
  98. # nor I can reproduce it locally:
  99. # https://ci.appveyor.com/project/giampaolo/psutil/build/job/
  100. # jiq2cgd6stsbtn60
  101. # safe_rmpath() happens after reap_children() so this is weird
  102. # Perhaps wait_procs() on Windows is broken? Maybe because
  103. # of STILL_ACTIVE?
  104. # https://github.com/giampaolo/psutil/blob/
  105. # 68c7a70728a31d8b8b58f4be6c4c0baa2f449eda/psutil/arch/
  106. # windows/process_info.c#L146
  107. from psutil.tests import safe_rmpath as rm
  108. try:
  109. return rm(path)
  110. except WindowsError:
  111. traceback.print_exc()
  112. def try_unicode(suffix):
  113. """Return True if both the fs and the subprocess module can
  114. deal with a unicode file name.
  115. """
  116. sproc = None
  117. testfn = get_testfn(suffix=suffix)
  118. try:
  119. safe_rmpath(testfn)
  120. create_exe(testfn)
  121. sproc = spawn_testproc(cmd=[testfn])
  122. shutil.copyfile(testfn, testfn + '-2')
  123. safe_rmpath(testfn + '-2')
  124. except (UnicodeEncodeError, IOError):
  125. return False
  126. else:
  127. return True
  128. finally:
  129. if sproc is not None:
  130. terminate(sproc)
  131. safe_rmpath(testfn)
  132. # ===================================================================
  133. # FS APIs
  134. # ===================================================================
  135. class BaseUnicodeTest(PsutilTestCase):
  136. funky_suffix = None
  137. def setUp(self):
  138. if self.funky_suffix is not None:
  139. if not try_unicode(self.funky_suffix):
  140. raise self.skipTest("can't handle unicode str")
  141. @serialrun
  142. @unittest.skipIf(ASCII_FS, "ASCII fs")
  143. @unittest.skipIf(PYPY and not PY3, "too much trouble on PYPY2")
  144. class TestFSAPIs(BaseUnicodeTest):
  145. """Test FS APIs with a funky, valid, UTF8 path name."""
  146. funky_suffix = UNICODE_SUFFIX
  147. @classmethod
  148. def setUpClass(cls):
  149. cls.funky_name = get_testfn(suffix=cls.funky_suffix)
  150. create_exe(cls.funky_name)
  151. @classmethod
  152. def tearDownClass(cls):
  153. safe_rmpath(cls.funky_name)
  154. def expect_exact_path_match(self):
  155. # Do not expect psutil to correctly handle unicode paths on
  156. # Python 2 if os.listdir() is not able either.
  157. here = '.' if isinstance(self.funky_name, str) else u('.')
  158. with warnings.catch_warnings():
  159. warnings.simplefilter("ignore")
  160. return self.funky_name in os.listdir(here)
  161. # ---
  162. def test_proc_exe(self):
  163. subp = self.spawn_testproc(cmd=[self.funky_name])
  164. p = psutil.Process(subp.pid)
  165. exe = p.exe()
  166. self.assertIsInstance(exe, str)
  167. if self.expect_exact_path_match():
  168. self.assertEqual(os.path.normcase(exe),
  169. os.path.normcase(self.funky_name))
  170. def test_proc_name(self):
  171. subp = self.spawn_testproc(cmd=[self.funky_name])
  172. name = psutil.Process(subp.pid).name()
  173. self.assertIsInstance(name, str)
  174. if self.expect_exact_path_match():
  175. self.assertEqual(name, os.path.basename(self.funky_name))
  176. def test_proc_cmdline(self):
  177. subp = self.spawn_testproc(cmd=[self.funky_name])
  178. p = psutil.Process(subp.pid)
  179. cmdline = p.cmdline()
  180. for part in cmdline:
  181. self.assertIsInstance(part, str)
  182. if self.expect_exact_path_match():
  183. self.assertEqual(cmdline, [self.funky_name])
  184. def test_proc_cwd(self):
  185. dname = self.funky_name + "2"
  186. self.addCleanup(safe_rmpath, dname)
  187. safe_mkdir(dname)
  188. with chdir(dname):
  189. p = psutil.Process()
  190. cwd = p.cwd()
  191. self.assertIsInstance(p.cwd(), str)
  192. if self.expect_exact_path_match():
  193. self.assertEqual(cwd, dname)
  194. @unittest.skipIf(PYPY and WINDOWS, "fails on PYPY + WINDOWS")
  195. def test_proc_open_files(self):
  196. p = psutil.Process()
  197. start = set(p.open_files())
  198. with open(self.funky_name, 'rb'):
  199. new = set(p.open_files())
  200. path = (new - start).pop().path
  201. self.assertIsInstance(path, str)
  202. if BSD and not path:
  203. # XXX - see https://github.com/giampaolo/psutil/issues/595
  204. return self.skipTest("open_files on BSD is broken")
  205. if self.expect_exact_path_match():
  206. self.assertEqual(os.path.normcase(path),
  207. os.path.normcase(self.funky_name))
  208. @unittest.skipIf(not POSIX, "POSIX only")
  209. def test_proc_connections(self):
  210. name = self.get_testfn(suffix=self.funky_suffix)
  211. try:
  212. sock = bind_unix_socket(name)
  213. except UnicodeEncodeError:
  214. if PY3:
  215. raise
  216. else:
  217. raise unittest.SkipTest("not supported")
  218. with closing(sock):
  219. conn = psutil.Process().connections('unix')[0]
  220. self.assertIsInstance(conn.laddr, str)
  221. self.assertEqual(conn.laddr, name)
  222. @unittest.skipIf(not POSIX, "POSIX only")
  223. @unittest.skipIf(not HAS_CONNECTIONS_UNIX, "can't list UNIX sockets")
  224. @skip_on_access_denied()
  225. def test_net_connections(self):
  226. def find_sock(cons):
  227. for conn in cons:
  228. if os.path.basename(conn.laddr).startswith(TESTFN_PREFIX):
  229. return conn
  230. raise ValueError("connection not found")
  231. name = self.get_testfn(suffix=self.funky_suffix)
  232. try:
  233. sock = bind_unix_socket(name)
  234. except UnicodeEncodeError:
  235. if PY3:
  236. raise
  237. else:
  238. raise unittest.SkipTest("not supported")
  239. with closing(sock):
  240. cons = psutil.net_connections(kind='unix')
  241. conn = find_sock(cons)
  242. self.assertIsInstance(conn.laddr, str)
  243. self.assertEqual(conn.laddr, name)
  244. def test_disk_usage(self):
  245. dname = self.funky_name + "2"
  246. self.addCleanup(safe_rmpath, dname)
  247. safe_mkdir(dname)
  248. psutil.disk_usage(dname)
  249. @unittest.skipIf(not HAS_MEMORY_MAPS, "not supported")
  250. @unittest.skipIf(not PY3, "ctypes does not support unicode on PY2")
  251. @unittest.skipIf(PYPY, "unstable on PYPY")
  252. def test_memory_maps(self):
  253. # XXX: on Python 2, using ctypes.CDLL with a unicode path
  254. # opens a message box which blocks the test run.
  255. with copyload_shared_lib(suffix=self.funky_suffix) as funky_path:
  256. def normpath(p):
  257. return os.path.realpath(os.path.normcase(p))
  258. libpaths = [normpath(x.path)
  259. for x in psutil.Process().memory_maps()]
  260. # ...just to have a clearer msg in case of failure
  261. libpaths = [x for x in libpaths if TESTFN_PREFIX in x]
  262. self.assertIn(normpath(funky_path), libpaths)
  263. for path in libpaths:
  264. self.assertIsInstance(path, str)
  265. @unittest.skipIf(CI_TESTING, "unreliable on CI")
  266. class TestFSAPIsWithInvalidPath(TestFSAPIs):
  267. """Test FS APIs with a funky, invalid path name."""
  268. funky_suffix = INVALID_UNICODE_SUFFIX
  269. def expect_exact_path_match(self):
  270. # Invalid unicode names are supposed to work on Python 2.
  271. return True
  272. # ===================================================================
  273. # Non fs APIs
  274. # ===================================================================
  275. class TestNonFSAPIS(BaseUnicodeTest):
  276. """Unicode tests for non fs-related APIs."""
  277. funky_suffix = UNICODE_SUFFIX if PY3 else 'è'
  278. @unittest.skipIf(not HAS_ENVIRON, "not supported")
  279. @unittest.skipIf(PYPY and WINDOWS, "segfaults on PYPY + WINDOWS")
  280. def test_proc_environ(self):
  281. # Note: differently from others, this test does not deal
  282. # with fs paths. On Python 2 subprocess module is broken as
  283. # it's not able to handle with non-ASCII env vars, so
  284. # we use "è", which is part of the extended ASCII table
  285. # (unicode point <= 255).
  286. env = os.environ.copy()
  287. env['FUNNY_ARG'] = self.funky_suffix
  288. sproc = self.spawn_testproc(env=env)
  289. p = psutil.Process(sproc.pid)
  290. env = p.environ()
  291. for k, v in env.items():
  292. self.assertIsInstance(k, str)
  293. self.assertIsInstance(v, str)
  294. self.assertEqual(env['FUNNY_ARG'], self.funky_suffix)
  295. if __name__ == '__main__':
  296. from psutil.tests.runner import run_from_name
  297. run_from_name(__file__)