py2docfx 0.1.20.dev2245319__py3-none-any.whl → 0.1.20.dev2245492__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. py2docfx/convert_prepare/get_source.py +1 -1
  2. py2docfx/convert_prepare/tests/test_get_source.py +3 -1
  3. py2docfx/docfx_yaml/build_finished.py +1 -1
  4. py2docfx/venv/venv1/Lib/site-packages/psutil/__init__.py +39 -19
  5. py2docfx/venv/venv1/Lib/site-packages/psutil/_common.py +3 -5
  6. py2docfx/venv/venv1/Lib/site-packages/psutil/_psaix.py +1 -2
  7. py2docfx/venv/venv1/Lib/site-packages/psutil/_psbsd.py +53 -78
  8. py2docfx/venv/venv1/Lib/site-packages/psutil/_pslinux.py +55 -38
  9. py2docfx/venv/venv1/Lib/site-packages/psutil/_psosx.py +40 -12
  10. py2docfx/venv/venv1/Lib/site-packages/psutil/_psposix.py +0 -1
  11. py2docfx/venv/venv1/Lib/site-packages/psutil/_pssunos.py +1 -2
  12. py2docfx/venv/venv1/Lib/site-packages/psutil/_pswindows.py +33 -13
  13. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/__init__.py +185 -122
  14. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/__main__.py +2 -3
  15. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_bsd.py +5 -10
  16. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_connections.py +3 -4
  17. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_contracts.py +41 -45
  18. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_linux.py +35 -38
  19. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_memleaks.py +4 -8
  20. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_misc.py +6 -12
  21. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_osx.py +17 -8
  22. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_posix.py +29 -17
  23. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_process.py +74 -75
  24. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_process_all.py +11 -13
  25. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_scripts.py +2 -3
  26. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_sudo.py +117 -0
  27. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_system.py +21 -31
  28. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_testutils.py +23 -23
  29. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_unicode.py +15 -8
  30. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_windows.py +65 -33
  31. {py2docfx-0.1.20.dev2245319.dist-info → py2docfx-0.1.20.dev2245492.dist-info}/METADATA +1 -1
  32. {py2docfx-0.1.20.dev2245319.dist-info → py2docfx-0.1.20.dev2245492.dist-info}/RECORD +34 -33
  33. {py2docfx-0.1.20.dev2245319.dist-info → py2docfx-0.1.20.dev2245492.dist-info}/WHEEL +0 -0
  34. {py2docfx-0.1.20.dev2245319.dist-info → py2docfx-0.1.20.dev2245492.dist-info}/top_level.txt +0 -0
@@ -54,7 +54,7 @@ def update_package_info(executable: str, pkg: PackageInfo, source_folder: str):
54
54
  for meta_info in metadata:
55
55
  meta_info_array = meta_info.split(":")
56
56
  meta_field = meta_info_array[0].strip().lower()
57
- if meta_field in attrs:
57
+ if meta_field in attrs and not hasattr(pkg, meta_field):
58
58
  setattr(
59
59
  pkg,
60
60
  meta_field,
@@ -50,8 +50,10 @@ def test_update_package_info(init_package_info):
50
50
  assert package.name == "dummy_package"
51
51
  assert package.version == "3.1.0"
52
52
 
53
- # case of metadata
53
+ # case of metadata, unly use metadata file as a fallback
54
54
  package = init_package_info
55
+ del package.name
56
+ del package.version
55
57
  get_source.update_package_info(sys.executable, package, os.path.join(base_path, "mock-2"))
56
58
  assert package.name == "mock_package"
57
59
  assert package.version == "2.2.0"
@@ -277,7 +277,7 @@ def build_finished(app, exception):
277
277
  obj['kind'] = 'import'
278
278
  package_obj = obj
279
279
 
280
- if (obj['type'] == 'class' and obj['inheritance']):
280
+ if (obj['type'] == 'class' and 'inheritance' in obj):
281
281
  convert_class_to_enum_if_needed(obj)
282
282
 
283
283
  is_root = insert_node_to_toc_tree_return_is_root_package(toc_yaml, uid, project_name, toc_node_map)
@@ -30,7 +30,6 @@ import sys
30
30
  import threading
31
31
  import time
32
32
 
33
-
34
33
  try:
35
34
  import pwd
36
35
  except ImportError:
@@ -86,7 +85,6 @@ from ._common import debug
86
85
  from ._common import memoize_when_activated
87
86
  from ._common import wrap_numbers as _wrap_numbers
88
87
 
89
-
90
88
  if LINUX:
91
89
  # This is public API and it will be retrieved from _pslinux.py
92
90
  # via sys.modules.
@@ -207,7 +205,7 @@ if hasattr(_psplatform.Process, "rlimit"):
207
205
  AF_LINK = _psplatform.AF_LINK
208
206
 
209
207
  __author__ = "Giampaolo Rodola'"
210
- __version__ = "7.0.0"
208
+ __version__ = "7.1.0"
211
209
  version_info = tuple(int(num) for num in __version__.split('.'))
212
210
 
213
211
  _timer = getattr(time, 'monotonic', time.time)
@@ -377,7 +375,11 @@ class Process:
377
375
  won't reuse the same PID after such a short period of time
378
376
  (0.01 secs). Technically this is inherently racy, but
379
377
  practically it should be good enough.
378
+
379
+ NOTE: unreliable on FreeBSD and OpenBSD as ctime is subject to
380
+ system clock updates.
380
381
  """
382
+
381
383
  if WINDOWS:
382
384
  # Use create_time() fast method in order to speedup
383
385
  # `process_iter()`. This means we'll get AccessDenied for
@@ -386,6 +388,11 @@ class Process:
386
388
  # https://github.com/giampaolo/psutil/issues/2366#issuecomment-2381646555
387
389
  self._create_time = self._proc.create_time(fast_only=True)
388
390
  return (self.pid, self._create_time)
391
+ elif LINUX or NETBSD or OSX:
392
+ # Use 'monotonic' process starttime since boot to form unique
393
+ # process identity, since it is stable over changes to system
394
+ # time.
395
+ return (self.pid, self._proc.create_time(monotonic=True))
389
396
  else:
390
397
  return (self.pid, self.create_time())
391
398
 
@@ -426,12 +433,12 @@ class Process:
426
433
  # on PID and creation time.
427
434
  if not isinstance(other, Process):
428
435
  return NotImplemented
429
- if OPENBSD or NETBSD: # pragma: no cover
430
- # Zombie processes on Open/NetBSD have a creation time of
431
- # 0.0. This covers the case when a process started normally
432
- # (so it has a ctime), then it turned into a zombie. It's
433
- # important to do this because is_running() depends on
434
- # __eq__.
436
+ if OPENBSD or NETBSD or SUNOS: # pragma: no cover
437
+ # Zombie processes on Open/NetBSD/illumos/Solaris have a
438
+ # creation time of 0.0. This covers the case when a process
439
+ # started normally (so it has a ctime), then it turned into a
440
+ # zombie. It's important to do this because is_running()
441
+ # depends on __eq__.
435
442
  pid1, ident1 = self._ident
436
443
  pid2, ident2 = other._ident
437
444
  if pid1 == pid2:
@@ -593,10 +600,13 @@ class Process:
593
600
  return None
594
601
  ppid = self.ppid()
595
602
  if ppid is not None:
596
- ctime = self.create_time()
603
+ # Get a fresh (non-cached) ctime in case the system clock
604
+ # was updated. TODO: use a monotonic ctime on platforms
605
+ # where it's supported.
606
+ proc_ctime = Process(self.pid).create_time()
597
607
  try:
598
608
  parent = Process(ppid)
599
- if parent.create_time() <= ctime:
609
+ if parent.create_time() <= proc_ctime:
600
610
  return parent
601
611
  # ...else ppid has been reused by another process
602
612
  except NoSuchProcess:
@@ -765,8 +775,11 @@ class Process:
765
775
 
766
776
  def create_time(self):
767
777
  """The process creation time as a floating point number
768
- expressed in seconds since the epoch.
769
- The return value is cached after first call.
778
+ expressed in seconds since the epoch (seconds since January 1,
779
+ 1970, at midnight UTC). The return value, which is cached after
780
+ first call, is based on the system clock, which means it may be
781
+ affected by changes such as manual adjustments or time
782
+ synchronization (e.g. NTP).
770
783
  """
771
784
  if self._create_time is None:
772
785
  self._create_time = self._proc.create_time()
@@ -964,6 +977,10 @@ class Process:
964
977
  """
965
978
  self._raise_if_pid_reused()
966
979
  ppid_map = _ppid_map()
980
+ # Get a fresh (non-cached) ctime in case the system clock was
981
+ # updated. TODO: use a monotonic ctime on platforms where it's
982
+ # supported.
983
+ proc_ctime = Process(self.pid).create_time()
967
984
  ret = []
968
985
  if not recursive:
969
986
  for pid, ppid in ppid_map.items():
@@ -972,7 +989,7 @@ class Process:
972
989
  child = Process(pid)
973
990
  # if child happens to be older than its parent
974
991
  # (self) it means child's PID has been reused
975
- if self.create_time() <= child.create_time():
992
+ if proc_ctime <= child.create_time():
976
993
  ret.append(child)
977
994
  except (NoSuchProcess, ZombieProcess):
978
995
  pass
@@ -998,7 +1015,7 @@ class Process:
998
1015
  child = Process(child_pid)
999
1016
  # if child happens to be older than its parent
1000
1017
  # (self) it means child's PID has been reused
1001
- intime = self.create_time() <= child.create_time()
1018
+ intime = proc_ctime <= child.create_time()
1002
1019
  if intime:
1003
1020
  ret.append(child)
1004
1021
  stack.append(child_pid)
@@ -1484,7 +1501,7 @@ def process_iter(attrs=None, ad_value=None):
1484
1501
 
1485
1502
  Every new Process instance is only created once and then cached
1486
1503
  into an internal table which is updated every time this is used.
1487
- Cache can optionally be cleared via `process_iter.clear_cache()`.
1504
+ Cache can optionally be cleared via `process_iter.cache_clear()`.
1488
1505
 
1489
1506
  The sorting order in which processes are yielded is based on
1490
1507
  their PIDs.
@@ -2352,9 +2369,12 @@ if hasattr(_psplatform, "sensors_battery"):
2352
2369
 
2353
2370
 
2354
2371
  def boot_time():
2355
- """Return the system boot time expressed in seconds since the epoch."""
2356
- # Note: we are not caching this because it is subject to
2357
- # system clock updates.
2372
+ """Return the system boot time expressed in seconds since the epoch
2373
+ (seconds since January 1, 1970, at midnight UTC). The returned
2374
+ value is based on the system clock, which means it may be affected
2375
+ by changes such as manual adjustments or time synchronization (e.g.
2376
+ NTP).
2377
+ """
2358
2378
  return _psplatform.boot_time()
2359
2379
 
2360
2380
 
@@ -22,7 +22,6 @@ from socket import AF_INET
22
22
  from socket import SOCK_DGRAM
23
23
  from socket import SOCK_STREAM
24
24
 
25
-
26
25
  try:
27
26
  from socket import AF_INET6
28
27
  except ImportError:
@@ -409,7 +408,7 @@ def memoize(fun):
409
408
  except KeyError:
410
409
  try:
411
410
  ret = cache[key] = fun(*args, **kwargs)
412
- except Exception as err: # noqa: BLE001
411
+ except Exception as err:
413
412
  raise err from None
414
413
  return ret
415
414
 
@@ -458,14 +457,14 @@ def memoize_when_activated(fun):
458
457
  # case 2: we never entered oneshot() ctx
459
458
  try:
460
459
  return fun(self)
461
- except Exception as err: # noqa: BLE001
460
+ except Exception as err:
462
461
  raise err from None
463
462
  except KeyError:
464
463
  # case 3: we entered oneshot() ctx but there's no cache
465
464
  # for this entry yet
466
465
  try:
467
466
  ret = fun(self)
468
- except Exception as err: # noqa: BLE001
467
+ except Exception as err:
469
468
  raise err from None
470
469
  try:
471
470
  self._cache[fun] = ret
@@ -523,7 +522,6 @@ def path_exists_strict(path):
523
522
  return True
524
523
 
525
524
 
526
- @memoize
527
525
  def supports_ipv6():
528
526
  """Return True if IPv6 is supported on this platform."""
529
527
  if not socket.has_ipv6 or AF_INET6 is None:
@@ -29,7 +29,6 @@ from ._common import get_procfs_path
29
29
  from ._common import memoize_when_activated
30
30
  from ._common import usage_percent
31
31
 
32
-
33
32
  __extra__all__ = ["PROCFS_PATH"]
34
33
 
35
34
 
@@ -279,7 +278,7 @@ def boot_time():
279
278
  def users():
280
279
  """Return currently connected users as a list of namedtuples."""
281
280
  retlist = []
282
- rawlist = cext.users()
281
+ rawlist = cext_posix.users()
283
282
  localhost = (':0.0', ':0')
284
283
  for item in rawlist:
285
284
  user, tty, hostname, tstamp, user_process, pid = item
@@ -29,7 +29,6 @@ from ._common import memoize
29
29
  from ._common import memoize_when_activated
30
30
  from ._common import usage_percent
31
31
 
32
-
33
32
  __extra__all__ = []
34
33
 
35
34
 
@@ -98,10 +97,7 @@ TCP_STATUSES = {
98
97
  PAGESIZE = cext_posix.getpagesize()
99
98
  AF_LINK = cext_posix.AF_LINK
100
99
 
101
- HAS_PER_CPU_TIMES = hasattr(cext, "per_cpu_times")
102
100
  HAS_PROC_NUM_THREADS = hasattr(cext, "proc_num_threads")
103
- HAS_PROC_OPEN_FILES = hasattr(cext, 'proc_open_files')
104
- HAS_PROC_NUM_FDS = hasattr(cext, 'proc_num_fds')
105
101
 
106
102
  kinfo_proc_map = dict(
107
103
  ppid=0,
@@ -240,36 +236,14 @@ def cpu_times():
240
236
  return scputimes(user, nice, system, idle, irq)
241
237
 
242
238
 
243
- if HAS_PER_CPU_TIMES:
244
-
245
- def per_cpu_times():
246
- """Return system CPU times as a namedtuple."""
247
- ret = []
248
- for cpu_t in cext.per_cpu_times():
249
- user, nice, system, idle, irq = cpu_t
250
- item = scputimes(user, nice, system, idle, irq)
251
- ret.append(item)
252
- return ret
253
-
254
- else:
255
- # XXX
256
- # Ok, this is very dirty.
257
- # On FreeBSD < 8 we cannot gather per-cpu information, see:
258
- # https://github.com/giampaolo/psutil/issues/226
259
- # If num cpus > 1, on first call we return single cpu times to avoid a
260
- # crash at psutil import time.
261
- # Next calls will fail with NotImplementedError
262
- def per_cpu_times():
263
- """Return system CPU times as a namedtuple."""
264
- if cpu_count_logical() == 1:
265
- return [cpu_times()]
266
- if per_cpu_times.__called__:
267
- msg = "supported only starting from FreeBSD 8"
268
- raise NotImplementedError(msg)
269
- per_cpu_times.__called__ = True
270
- return [cpu_times()]
271
-
272
- per_cpu_times.__called__ = False
239
+ def per_cpu_times():
240
+ """Return system CPU times as a namedtuple."""
241
+ ret = []
242
+ for cpu_t in cext.per_cpu_times():
243
+ user, nice, system, idle, irq = cpu_t
244
+ item = scputimes(user, nice, system, idle, irq)
245
+ ret.append(item)
246
+ return ret
273
247
 
274
248
 
275
249
  def cpu_count_logical():
@@ -505,15 +479,36 @@ def boot_time():
505
479
  return cext.boot_time()
506
480
 
507
481
 
482
+ if NETBSD:
483
+
484
+ try:
485
+ INIT_BOOT_TIME = boot_time()
486
+ except Exception as err: # noqa: BLE001
487
+ # Don't want to crash at import time.
488
+ debug(f"ignoring exception on import: {err!r}")
489
+ INIT_BOOT_TIME = 0
490
+
491
+ def adjust_proc_create_time(ctime):
492
+ """Account for system clock updates."""
493
+ if INIT_BOOT_TIME == 0:
494
+ return ctime
495
+
496
+ diff = INIT_BOOT_TIME - boot_time()
497
+ if diff == 0 or abs(diff) < 1:
498
+ return ctime
499
+
500
+ debug("system clock was updated; adjusting process create_time()")
501
+ if diff < 0:
502
+ return ctime - diff
503
+ return ctime + diff
504
+
505
+
508
506
  def users():
509
507
  """Return currently connected users as a list of namedtuples."""
510
508
  retlist = []
511
- rawlist = cext.users()
509
+ rawlist = cext.users() if OPENBSD else cext_posix.users()
512
510
  for item in rawlist:
513
511
  user, tty, hostname, tstamp, pid = item
514
- if pid == -1:
515
- assert OPENBSD
516
- pid = None
517
512
  if tty == '~':
518
513
  continue # reboot or shutdown
519
514
  nt = _common.suser(user, tty or None, hostname, tstamp, pid)
@@ -779,13 +774,17 @@ class Process:
779
774
  memory_full_info = memory_info
780
775
 
781
776
  @wrap_exceptions
782
- def create_time(self):
783
- return self.oneshot()[kinfo_proc_map['create_time']]
777
+ def create_time(self, monotonic=False):
778
+ ctime = self.oneshot()[kinfo_proc_map['create_time']]
779
+ if NETBSD and not monotonic:
780
+ # NetBSD: ctime subject to system clock updates.
781
+ ctime = adjust_proc_create_time(ctime)
782
+ return ctime
784
783
 
785
784
  @wrap_exceptions
786
785
  def num_threads(self):
787
786
  if HAS_PROC_NUM_THREADS:
788
- # FreeBSD
787
+ # FreeBSD / NetBSD
789
788
  return cext.proc_num_threads(self.pid)
790
789
  else:
791
790
  return len(self.threads())
@@ -870,14 +869,7 @@ class Process:
870
869
  # it into None
871
870
  if OPENBSD and self.pid == 0:
872
871
  return "" # ...else it would raise EINVAL
873
- elif NETBSD or HAS_PROC_OPEN_FILES:
874
- # FreeBSD < 8 does not support functions based on
875
- # kinfo_getfile() and kinfo_getvmmap()
876
- return cext.proc_cwd(self.pid)
877
- else:
878
- raise NotImplementedError(
879
- "supported only starting from FreeBSD 8" if FREEBSD else ""
880
- )
872
+ return cext.proc_cwd(self.pid)
881
873
 
882
874
  nt_mmap_grouped = namedtuple(
883
875
  'mmap', 'path rss, private, ref_count, shadow_count'
@@ -886,36 +878,19 @@ class Process:
886
878
  'mmap', 'addr, perms path rss, private, ref_count, shadow_count'
887
879
  )
888
880
 
889
- def _not_implemented(self):
890
- raise NotImplementedError
891
-
892
- # FreeBSD < 8 does not support functions based on kinfo_getfile()
893
- # and kinfo_getvmmap()
894
- if HAS_PROC_OPEN_FILES:
895
-
896
- @wrap_exceptions
897
- def open_files(self):
898
- """Return files opened by process as a list of namedtuples."""
899
- rawlist = cext.proc_open_files(self.pid)
900
- return [_common.popenfile(path, fd) for path, fd in rawlist]
901
-
902
- else:
903
- open_files = _not_implemented
904
-
905
- # FreeBSD < 8 does not support functions based on kinfo_getfile()
906
- # and kinfo_getvmmap()
907
- if HAS_PROC_NUM_FDS:
908
-
909
- @wrap_exceptions
910
- def num_fds(self):
911
- """Return the number of file descriptors opened by this process."""
912
- ret = cext.proc_num_fds(self.pid)
913
- if NETBSD:
914
- self._assert_alive()
915
- return ret
881
+ @wrap_exceptions
882
+ def open_files(self):
883
+ """Return files opened by process as a list of namedtuples."""
884
+ rawlist = cext.proc_open_files(self.pid)
885
+ return [_common.popenfile(path, fd) for path, fd in rawlist]
916
886
 
917
- else:
918
- num_fds = _not_implemented
887
+ @wrap_exceptions
888
+ def num_fds(self):
889
+ """Return the number of file descriptors opened by this process."""
890
+ ret = cext.proc_num_fds(self.pid)
891
+ if NETBSD:
892
+ self._assert_alive()
893
+ return ret
919
894
 
920
895
  # --- FreeBSD only APIs
921
896
 
@@ -47,7 +47,6 @@ from ._common import path_exists_strict
47
47
  from ._common import supports_ipv6
48
48
  from ._common import usage_percent
49
49
 
50
-
51
50
  # fmt: off
52
51
  __extra__all__ = [
53
52
  'PROCFS_PATH',
@@ -81,8 +80,8 @@ HAS_CPU_AFFINITY = hasattr(cext, "proc_cpu_affinity_get")
81
80
  # Number of clock ticks per second
82
81
  CLOCK_TICKS = os.sysconf("SC_CLK_TCK")
83
82
  PAGESIZE = cext_posix.getpagesize()
84
- BOOT_TIME = None # set later
85
83
  LITTLE_ENDIAN = sys.byteorder == 'little'
84
+ UNSET = object()
86
85
 
87
86
  # "man iostat" states that sectors are equivalent with blocks and have
88
87
  # a size of 512 bytes. Despite this value can be queried at runtime
@@ -421,12 +420,6 @@ def virtual_memory():
421
420
  except KeyError:
422
421
  slab = 0
423
422
 
424
- used = total - free - cached - buffers
425
- if used < 0:
426
- # May be symptomatic of running within a LCX container where such
427
- # values will be dramatically distorted over those of the host.
428
- used = total - free
429
-
430
423
  # - starting from 4.4.0 we match free's "available" column.
431
424
  # Before 4.4.0 we calculated it as (free + buffers + cached)
432
425
  # which matched htop.
@@ -457,6 +450,8 @@ def virtual_memory():
457
450
  # 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L764
458
451
  avail = free
459
452
 
453
+ used = total - avail
454
+
460
455
  percent = usage_percent((total - avail), total, round_=1)
461
456
 
462
457
  # Warn about missing metrics which are set to 0.
@@ -1525,7 +1520,7 @@ def sensors_battery():
1525
1520
  secsleft = _common.POWER_TIME_UNLIMITED
1526
1521
  elif energy_now is not None and power_now is not None:
1527
1522
  try:
1528
- secsleft = int(energy_now / power_now * 3600)
1523
+ secsleft = int(energy_now / abs(power_now) * 3600)
1529
1524
  except ZeroDivisionError:
1530
1525
  secsleft = _common.POWER_TIME_UNKNOWN
1531
1526
  elif time_to_empty is not None:
@@ -1546,7 +1541,7 @@ def sensors_battery():
1546
1541
  def users():
1547
1542
  """Return currently connected users as a list of namedtuples."""
1548
1543
  retlist = []
1549
- rawlist = cext.users()
1544
+ rawlist = cext_posix.users()
1550
1545
  for item in rawlist:
1551
1546
  user, tty, hostname, tstamp, pid = item
1552
1547
  nt = _common.suser(user, tty or None, hostname, tstamp, pid)
@@ -1556,14 +1551,11 @@ def users():
1556
1551
 
1557
1552
  def boot_time():
1558
1553
  """Return the system boot time expressed in seconds since the epoch."""
1559
- global BOOT_TIME
1560
1554
  path = f"{get_procfs_path()}/stat"
1561
1555
  with open_binary(path) as f:
1562
1556
  for line in f:
1563
1557
  if line.startswith(b'btime'):
1564
- ret = float(line.strip().split()[1])
1565
- BOOT_TIME = ret
1566
- return ret
1558
+ return float(line.strip().split()[1])
1567
1559
  msg = f"line 'btime' not found in {path}"
1568
1560
  raise RuntimeError(msg)
1569
1561
 
@@ -1623,9 +1615,9 @@ def ppid_map():
1623
1615
  with open_binary(f"{procfs_path}/{pid}/stat") as f:
1624
1616
  data = f.read()
1625
1617
  except (FileNotFoundError, ProcessLookupError):
1626
- # Note: we should be able to access /stat for all processes
1627
- # aka it's unlikely we'll bump into EPERM, which is good.
1628
1618
  pass
1619
+ except PermissionError as err:
1620
+ raise AccessDenied(pid) from err
1629
1621
  else:
1630
1622
  rpar = data.rfind(b')')
1631
1623
  dset = data[rpar + 2 :].split()
@@ -1664,12 +1656,20 @@ def wrap_exceptions(fun):
1664
1656
  class Process:
1665
1657
  """Linux process implementation."""
1666
1658
 
1667
- __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"]
1659
+ __slots__ = [
1660
+ "_cache",
1661
+ "_ctime",
1662
+ "_name",
1663
+ "_ppid",
1664
+ "_procfs_path",
1665
+ "pid",
1666
+ ]
1668
1667
 
1669
1668
  def __init__(self, pid):
1670
1669
  self.pid = pid
1671
1670
  self._name = None
1672
1671
  self._ppid = None
1672
+ self._ctime = None
1673
1673
  self._procfs_path = get_procfs_path()
1674
1674
 
1675
1675
  def _is_zombie(self):
@@ -1698,6 +1698,22 @@ class Process:
1698
1698
  # incorrect or incomplete result.
1699
1699
  os.stat(f"{self._procfs_path}/{self.pid}")
1700
1700
 
1701
+ def _readlink(self, path, fallback=UNSET):
1702
+ # * https://github.com/giampaolo/psutil/issues/503
1703
+ # os.readlink('/proc/pid/exe') may raise ESRCH (ProcessLookupError)
1704
+ # instead of ENOENT (FileNotFoundError) when it races.
1705
+ # * ENOENT may occur also if the path actually exists if PID is
1706
+ # a low PID (~0-20 range).
1707
+ # * https://github.com/giampaolo/psutil/issues/2514
1708
+ try:
1709
+ return readlink(path)
1710
+ except (FileNotFoundError, ProcessLookupError):
1711
+ if os.path.lexists(f"{self._procfs_path}/{self.pid}"):
1712
+ self._raise_if_zombie()
1713
+ if fallback is not UNSET:
1714
+ return fallback
1715
+ raise
1716
+
1701
1717
  @wrap_exceptions
1702
1718
  @memoize_when_activated
1703
1719
  def _parse_stat_file(self):
@@ -1770,16 +1786,9 @@ class Process:
1770
1786
 
1771
1787
  @wrap_exceptions
1772
1788
  def exe(self):
1773
- try:
1774
- return readlink(f"{self._procfs_path}/{self.pid}/exe")
1775
- except (FileNotFoundError, ProcessLookupError):
1776
- self._raise_if_zombie()
1777
- # no such file error; might be raised also if the
1778
- # path actually exists for system processes with
1779
- # low pids (about 0-20)
1780
- if os.path.lexists(f"{self._procfs_path}/{self.pid}"):
1781
- return ""
1782
- raise
1789
+ return self._readlink(
1790
+ f"{self._procfs_path}/{self.pid}/exe", fallback=""
1791
+ )
1783
1792
 
1784
1793
  @wrap_exceptions
1785
1794
  def cmdline(self):
@@ -1880,15 +1889,21 @@ class Process:
1880
1889
  return _psposix.wait_pid(self.pid, timeout, self._name)
1881
1890
 
1882
1891
  @wrap_exceptions
1883
- def create_time(self):
1884
- ctime = float(self._parse_stat_file()['create_time'])
1885
- # According to documentation, starttime is in field 21 and the
1886
- # unit is jiffies (clock ticks).
1887
- # We first divide it for clock ticks and then add uptime returning
1888
- # seconds since the epoch.
1889
- # Also use cached value if available.
1890
- bt = BOOT_TIME or boot_time()
1891
- return (ctime / CLOCK_TICKS) + bt
1892
+ def create_time(self, monotonic=False):
1893
+ # The 'starttime' field in /proc/[pid]/stat is expressed in
1894
+ # jiffies (clock ticks per second), a relative value which
1895
+ # represents the number of clock ticks that have passed since
1896
+ # the system booted until the process was created. It never
1897
+ # changes and is unaffected by system clock updates.
1898
+ if self._ctime is None:
1899
+ self._ctime = (
1900
+ float(self._parse_stat_file()['create_time']) / CLOCK_TICKS
1901
+ )
1902
+ if monotonic:
1903
+ return self._ctime
1904
+ # Add the boot time, returning time expressed in seconds since
1905
+ # the epoch. This is subject to system clock updates.
1906
+ return self._ctime + boot_time()
1892
1907
 
1893
1908
  @wrap_exceptions
1894
1909
  def memory_info(self):
@@ -2001,7 +2016,7 @@ class Process:
2001
2016
  else:
2002
2017
  try:
2003
2018
  data[fields[0]] = int(fields[1]) * 1024
2004
- except ValueError:
2019
+ except (ValueError, IndexError):
2005
2020
  if fields[0].startswith(b'VmFlags:'):
2006
2021
  # see issue #369
2007
2022
  continue
@@ -2054,7 +2069,9 @@ class Process:
2054
2069
 
2055
2070
  @wrap_exceptions
2056
2071
  def cwd(self):
2057
- return readlink(f"{self._procfs_path}/{self.pid}/cwd")
2072
+ return self._readlink(
2073
+ f"{self._procfs_path}/{self.pid}/cwd", fallback=""
2074
+ )
2058
2075
 
2059
2076
  @wrap_exceptions
2060
2077
  def num_ctx_switches(