You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			321 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			321 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
| #!/usr/bin/env python3
 | |
| 
 | |
| # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
 | |
| # Use of this source code is governed by a BSD-style license that can be
 | |
| # found in the LICENSE file.
 | |
| 
 | |
| """Notes about unicode handling in psutil
 | |
| ======================================.
 | |
| 
 | |
| Starting from version 5.3.0 psutil adds unicode support, see:
 | |
| https://github.com/giampaolo/psutil/issues/1040
 | |
| The notes below apply to *any* API returning a string such as
 | |
| process exe(), cwd() or username():
 | |
| 
 | |
| * all strings are encoded by using the OS filesystem encoding
 | |
|   (sys.getfilesystemencoding()) which varies depending on the platform
 | |
|   (e.g. "UTF-8" on macOS, "mbcs" on Win)
 | |
| * no API call is supposed to crash with UnicodeDecodeError
 | |
| * instead, in case of badly encoded data returned by the OS, the
 | |
|   following error handlers are used to replace the corrupted characters in
 | |
|   the string:
 | |
|     * sys.getfilesystemencodeerrors() or "surrogatescape" on POSIX and
 | |
|       "replace" on Windows.
 | |
| 
 | |
| For a detailed explanation of how psutil handles unicode see #1040.
 | |
| 
 | |
| Tests
 | |
| =====
 | |
| 
 | |
| List of APIs returning or dealing with a string:
 | |
| ('not tested' means they are not tested to deal with non-ASCII strings):
 | |
| 
 | |
| * Process.cmdline()
 | |
| * Process.cwd()
 | |
| * Process.environ()
 | |
| * Process.exe()
 | |
| * Process.memory_maps()
 | |
| * Process.name()
 | |
| * Process.net_connections('unix')
 | |
| * Process.open_files()
 | |
| * Process.username()             (not tested)
 | |
| 
 | |
| * disk_io_counters()             (not tested)
 | |
| * disk_partitions()              (not tested)
 | |
| * disk_usage(str)
 | |
| * net_connections('unix')
 | |
| * net_if_addrs()                 (not tested)
 | |
| * net_if_stats()                 (not tested)
 | |
| * net_io_counters()              (not tested)
 | |
| * sensors_fans()                 (not tested)
 | |
| * sensors_temperatures()         (not tested)
 | |
| * users()                        (not tested)
 | |
| 
 | |
| * WindowsService.binpath()       (not tested)
 | |
| * WindowsService.description()   (not tested)
 | |
| * WindowsService.display_name()  (not tested)
 | |
| * WindowsService.name()          (not tested)
 | |
| * WindowsService.status()        (not tested)
 | |
| * WindowsService.username()      (not tested)
 | |
| 
 | |
| In here we create a unicode path with a funky non-ASCII name and (where
 | |
| possible) make psutil return it back (e.g. on name(), exe(), open_files(),
 | |
| etc.) and make sure that:
 | |
| 
 | |
| * psutil never crashes with UnicodeDecodeError
 | |
| * the returned path matches
 | |
| """
 | |
| 
 | |
| import os
 | |
| import shutil
 | |
| import warnings
 | |
| from contextlib import closing
 | |
| 
 | |
| import psutil
 | |
| from psutil import BSD
 | |
| from psutil import MACOS
 | |
| from psutil import NETBSD
 | |
| from psutil import OPENBSD
 | |
| from psutil import POSIX
 | |
| from psutil import WINDOWS
 | |
| from psutil.tests import ASCII_FS
 | |
| from psutil.tests import CI_TESTING
 | |
| from psutil.tests import HAS_ENVIRON
 | |
| from psutil.tests import HAS_MEMORY_MAPS
 | |
| from psutil.tests import HAS_NET_CONNECTIONS_UNIX
 | |
| from psutil.tests import INVALID_UNICODE_SUFFIX
 | |
| from psutil.tests import PYPY
 | |
| from psutil.tests import TESTFN_PREFIX
 | |
| from psutil.tests import UNICODE_SUFFIX
 | |
| from psutil.tests import PsutilTestCase
 | |
| from psutil.tests import bind_unix_socket
 | |
| from psutil.tests import chdir
 | |
| from psutil.tests import copyload_shared_lib
 | |
| from psutil.tests import create_py_exe
 | |
| from psutil.tests import get_testfn
 | |
| from psutil.tests import pytest
 | |
| from psutil.tests import safe_mkdir
 | |
| from psutil.tests import safe_rmpath
 | |
| from psutil.tests import skip_on_access_denied
 | |
| from psutil.tests import spawn_subproc
 | |
| from psutil.tests import terminate
 | |
| 
 | |
| 
 | |
| def try_unicode(suffix):
 | |
|     """Return True if both the fs and the subprocess module can
 | |
|     deal with a unicode file name.
 | |
|     """
 | |
|     sproc = None
 | |
|     testfn = get_testfn(suffix=suffix)
 | |
|     try:
 | |
|         safe_rmpath(testfn)
 | |
|         create_py_exe(testfn)
 | |
|         sproc = spawn_subproc(cmd=[testfn])
 | |
|         shutil.copyfile(testfn, testfn + '-2')
 | |
|         safe_rmpath(testfn + '-2')
 | |
|     except (UnicodeEncodeError, OSError):
 | |
|         return False
 | |
|     else:
 | |
|         return True
 | |
|     finally:
 | |
|         if sproc is not None:
 | |
|             terminate(sproc)
 | |
|         safe_rmpath(testfn)
 | |
| 
 | |
| 
 | |
| # ===================================================================
 | |
| # FS APIs
 | |
| # ===================================================================
 | |
| 
 | |
| 
 | |
| class BaseUnicodeTest(PsutilTestCase):
 | |
|     funky_suffix = None
 | |
| 
 | |
|     @classmethod
 | |
|     def setUpClass(cls):
 | |
|         super().setUpClass()
 | |
|         cls.skip_tests = False
 | |
|         cls.funky_name = None
 | |
|         if cls.funky_suffix is not None:
 | |
|             if not try_unicode(cls.funky_suffix):
 | |
|                 cls.skip_tests = True
 | |
|             else:
 | |
|                 cls.funky_name = get_testfn(suffix=cls.funky_suffix)
 | |
|                 create_py_exe(cls.funky_name)
 | |
| 
 | |
|     def setUp(self):
 | |
|         super().setUp()
 | |
|         if self.skip_tests:
 | |
|             raise pytest.skip("can't handle unicode str")
 | |
| 
 | |
| 
 | |
| @pytest.mark.xdist_group(name="serial")
 | |
| @pytest.mark.skipif(ASCII_FS, reason="ASCII fs")
 | |
| class TestFSAPIs(BaseUnicodeTest):
 | |
|     """Test FS APIs with a funky, valid, UTF8 path name."""
 | |
| 
 | |
|     funky_suffix = UNICODE_SUFFIX
 | |
| 
 | |
|     def expect_exact_path_match(self):
 | |
|         with warnings.catch_warnings():
 | |
|             warnings.simplefilter("ignore")
 | |
|             return self.funky_name in os.listdir(".")
 | |
| 
 | |
|     # ---
 | |
| 
 | |
|     def test_proc_exe(self):
 | |
|         cmd = [
 | |
|             self.funky_name,
 | |
|             "-c",
 | |
|             "import time; [time.sleep(0.1) for x in range(100)]",
 | |
|         ]
 | |
|         subp = self.spawn_subproc(cmd)
 | |
|         p = psutil.Process(subp.pid)
 | |
|         exe = p.exe()
 | |
|         assert isinstance(exe, str)
 | |
|         if self.expect_exact_path_match():
 | |
|             assert os.path.normcase(exe) == os.path.normcase(self.funky_name)
 | |
| 
 | |
|     def test_proc_name(self):
 | |
|         cmd = [
 | |
|             self.funky_name,
 | |
|             "-c",
 | |
|             "import time; [time.sleep(0.1) for x in range(100)]",
 | |
|         ]
 | |
|         subp = self.spawn_subproc(cmd)
 | |
|         name = psutil.Process(subp.pid).name()
 | |
|         assert isinstance(name, str)
 | |
|         if self.expect_exact_path_match():
 | |
|             assert name == os.path.basename(self.funky_name)
 | |
| 
 | |
|     def test_proc_cmdline(self):
 | |
|         cmd = [
 | |
|             self.funky_name,
 | |
|             "-c",
 | |
|             "import time; [time.sleep(0.1) for x in range(100)]",
 | |
|         ]
 | |
|         subp = self.spawn_subproc(cmd)
 | |
|         p = psutil.Process(subp.pid)
 | |
|         cmdline = p.cmdline()
 | |
|         for part in cmdline:
 | |
|             assert isinstance(part, str)
 | |
|         if self.expect_exact_path_match():
 | |
|             assert cmdline == cmd
 | |
| 
 | |
|     def test_proc_cwd(self):
 | |
|         dname = self.funky_name + "2"
 | |
|         self.addCleanup(safe_rmpath, dname)
 | |
|         safe_mkdir(dname)
 | |
|         with chdir(dname):
 | |
|             p = psutil.Process()
 | |
|             cwd = p.cwd()
 | |
|         assert isinstance(p.cwd(), str)
 | |
|         if self.expect_exact_path_match():
 | |
|             assert cwd == dname
 | |
| 
 | |
|     @pytest.mark.skipif(PYPY and WINDOWS, reason="fails on PYPY + WINDOWS")
 | |
|     @pytest.mark.skipif(
 | |
|         NETBSD or OPENBSD, reason="broken on NETBSD or OPENBSD"
 | |
|     )
 | |
|     def test_proc_open_files(self):
 | |
|         p = psutil.Process()
 | |
|         start = set(p.open_files())
 | |
|         with open(self.funky_name, 'rb'):
 | |
|             new = set(p.open_files())
 | |
|         path = (new - start).pop().path
 | |
|         assert isinstance(path, str)
 | |
|         if BSD and not path:
 | |
|             # XXX - see https://github.com/giampaolo/psutil/issues/595
 | |
|             raise pytest.skip("open_files on BSD is broken")
 | |
|         if self.expect_exact_path_match():
 | |
|             assert os.path.normcase(path) == os.path.normcase(self.funky_name)
 | |
| 
 | |
|     @pytest.mark.skipif(not POSIX, reason="POSIX only")
 | |
|     def test_proc_net_connections(self):
 | |
|         name = self.get_testfn(suffix=self.funky_suffix)
 | |
|         sock = bind_unix_socket(name)
 | |
|         with closing(sock):
 | |
|             conn = psutil.Process().net_connections('unix')[0]
 | |
|             assert isinstance(conn.laddr, str)
 | |
|             if not conn.laddr and MACOS and CI_TESTING:
 | |
|                 raise pytest.skip("unreliable on OSX")
 | |
|             assert conn.laddr == name
 | |
| 
 | |
|     @pytest.mark.skipif(not POSIX, reason="POSIX only")
 | |
|     @pytest.mark.skipif(
 | |
|         not HAS_NET_CONNECTIONS_UNIX, reason="can't list UNIX sockets"
 | |
|     )
 | |
|     @skip_on_access_denied()
 | |
|     def test_net_connections(self):
 | |
|         def find_sock(cons):
 | |
|             for conn in cons:
 | |
|                 if os.path.basename(conn.laddr).startswith(TESTFN_PREFIX):
 | |
|                     return conn
 | |
|             raise ValueError("connection not found")
 | |
| 
 | |
|         name = self.get_testfn(suffix=self.funky_suffix)
 | |
|         sock = bind_unix_socket(name)
 | |
|         with closing(sock):
 | |
|             cons = psutil.net_connections(kind='unix')
 | |
|             conn = find_sock(cons)
 | |
|             assert isinstance(conn.laddr, str)
 | |
|             assert conn.laddr == name
 | |
| 
 | |
|     def test_disk_usage(self):
 | |
|         dname = self.funky_name + "2"
 | |
|         self.addCleanup(safe_rmpath, dname)
 | |
|         safe_mkdir(dname)
 | |
|         psutil.disk_usage(dname)
 | |
| 
 | |
|     @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported")
 | |
|     def test_memory_maps(self):
 | |
|         with copyload_shared_lib(suffix=self.funky_suffix) as funky_path:
 | |
| 
 | |
|             def normpath(p):
 | |
|                 return os.path.realpath(os.path.normcase(p))
 | |
| 
 | |
|             libpaths = [
 | |
|                 normpath(x.path) for x in psutil.Process().memory_maps()
 | |
|             ]
 | |
|             # ...just to have a clearer msg in case of failure
 | |
|             libpaths = [x for x in libpaths if TESTFN_PREFIX in x]
 | |
|             assert normpath(funky_path) in libpaths
 | |
|             for path in libpaths:
 | |
|                 assert isinstance(path, str)
 | |
| 
 | |
| 
 | |
| @pytest.mark.skipif(CI_TESTING, reason="unreliable on CI")
 | |
| class TestFSAPIsWithInvalidPath(TestFSAPIs):
 | |
|     """Test FS APIs with a funky, invalid path name."""
 | |
| 
 | |
|     funky_suffix = INVALID_UNICODE_SUFFIX
 | |
| 
 | |
|     def expect_exact_path_match(self):
 | |
|         return not MACOS
 | |
| 
 | |
| 
 | |
| # ===================================================================
 | |
| # Non fs APIs
 | |
| # ===================================================================
 | |
| 
 | |
| 
 | |
| class TestNonFSAPIS(BaseUnicodeTest):
 | |
|     """Unicode tests for non fs-related APIs."""
 | |
| 
 | |
|     funky_suffix = UNICODE_SUFFIX
 | |
| 
 | |
|     @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported")
 | |
|     @pytest.mark.skipif(PYPY and WINDOWS, reason="segfaults on PYPY + WINDOWS")
 | |
|     def test_proc_environ(self):
 | |
|         # Note: differently from others, this test does not deal
 | |
|         # with fs paths.
 | |
|         env = os.environ.copy()
 | |
|         env['FUNNY_ARG'] = self.funky_suffix
 | |
|         sproc = self.spawn_subproc(env=env)
 | |
|         p = psutil.Process(sproc.pid)
 | |
|         env = p.environ()
 | |
|         for k, v in env.items():
 | |
|             assert isinstance(k, str)
 | |
|             assert isinstance(v, str)
 | |
|         assert env['FUNNY_ARG'] == self.funky_suffix
 |