_psaix.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. # Copyright (c) 2009, Giampaolo Rodola'
  2. # Copyright (c) 2017, Arnon Yaari
  3. # 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. """AIX platform implementation."""
  7. import functools
  8. import glob
  9. import os
  10. import re
  11. import subprocess
  12. import sys
  13. from collections import namedtuple
  14. from . import _common
  15. from . import _psposix
  16. from . import _psutil_aix as cext
  17. from . import _psutil_posix as cext_posix
  18. from ._common import NIC_DUPLEX_FULL
  19. from ._common import NIC_DUPLEX_HALF
  20. from ._common import NIC_DUPLEX_UNKNOWN
  21. from ._common import AccessDenied
  22. from ._common import NoSuchProcess
  23. from ._common import ZombieProcess
  24. from ._common import conn_to_ntuple
  25. from ._common import get_procfs_path
  26. from ._common import memoize_when_activated
  27. from ._common import usage_percent
  28. from ._compat import PY3
  29. from ._compat import FileNotFoundError
  30. from ._compat import PermissionError
  31. from ._compat import ProcessLookupError
  32. __extra__all__ = ["PROCFS_PATH"]
  33. # =====================================================================
  34. # --- globals
  35. # =====================================================================
  36. HAS_THREADS = hasattr(cext, "proc_threads")
  37. HAS_NET_IO_COUNTERS = hasattr(cext, "net_io_counters")
  38. HAS_PROC_IO_COUNTERS = hasattr(cext, "proc_io_counters")
  39. PAGE_SIZE = cext_posix.getpagesize()
  40. AF_LINK = cext_posix.AF_LINK
  41. PROC_STATUSES = {
  42. cext.SIDL: _common.STATUS_IDLE,
  43. cext.SZOMB: _common.STATUS_ZOMBIE,
  44. cext.SACTIVE: _common.STATUS_RUNNING,
  45. cext.SSWAP: _common.STATUS_RUNNING, # TODO what status is this?
  46. cext.SSTOP: _common.STATUS_STOPPED,
  47. }
  48. TCP_STATUSES = {
  49. cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED,
  50. cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT,
  51. cext.TCPS_SYN_RCVD: _common.CONN_SYN_RECV,
  52. cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1,
  53. cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2,
  54. cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT,
  55. cext.TCPS_CLOSED: _common.CONN_CLOSE,
  56. cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT,
  57. cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK,
  58. cext.TCPS_LISTEN: _common.CONN_LISTEN,
  59. cext.TCPS_CLOSING: _common.CONN_CLOSING,
  60. cext.PSUTIL_CONN_NONE: _common.CONN_NONE,
  61. }
  62. proc_info_map = dict(
  63. ppid=0,
  64. rss=1,
  65. vms=2,
  66. create_time=3,
  67. nice=4,
  68. num_threads=5,
  69. status=6,
  70. ttynr=7,
  71. )
  72. # =====================================================================
  73. # --- named tuples
  74. # =====================================================================
  75. # psutil.Process.memory_info()
  76. pmem = namedtuple('pmem', ['rss', 'vms'])
  77. # psutil.Process.memory_full_info()
  78. pfullmem = pmem
  79. # psutil.Process.cpu_times()
  80. scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'iowait'])
  81. # psutil.virtual_memory()
  82. svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free'])
  83. # =====================================================================
  84. # --- memory
  85. # =====================================================================
  86. def virtual_memory():
  87. total, avail, free, _pinned, inuse = cext.virtual_mem()
  88. percent = usage_percent((total - avail), total, round_=1)
  89. return svmem(total, avail, percent, inuse, free)
  90. def swap_memory():
  91. """Swap system memory as a (total, used, free, sin, sout) tuple."""
  92. total, free, sin, sout = cext.swap_mem()
  93. used = total - free
  94. percent = usage_percent(used, total, round_=1)
  95. return _common.sswap(total, used, free, percent, sin, sout)
  96. # =====================================================================
  97. # --- CPU
  98. # =====================================================================
  99. def cpu_times():
  100. """Return system-wide CPU times as a named tuple."""
  101. ret = cext.per_cpu_times()
  102. return scputimes(*[sum(x) for x in zip(*ret)])
  103. def per_cpu_times():
  104. """Return system per-CPU times as a list of named tuples."""
  105. ret = cext.per_cpu_times()
  106. return [scputimes(*x) for x in ret]
  107. def cpu_count_logical():
  108. """Return the number of logical CPUs in the system."""
  109. try:
  110. return os.sysconf("SC_NPROCESSORS_ONLN")
  111. except ValueError:
  112. # mimic os.cpu_count() behavior
  113. return None
  114. def cpu_count_cores():
  115. cmd = ["lsdev", "-Cc", "processor"]
  116. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  117. stdout, stderr = p.communicate()
  118. if PY3:
  119. stdout, stderr = (
  120. x.decode(sys.stdout.encoding) for x in (stdout, stderr)
  121. )
  122. if p.returncode != 0:
  123. raise RuntimeError("%r command error\n%s" % (cmd, stderr))
  124. processors = stdout.strip().splitlines()
  125. return len(processors) or None
  126. def cpu_stats():
  127. """Return various CPU stats as a named tuple."""
  128. ctx_switches, interrupts, soft_interrupts, syscalls = cext.cpu_stats()
  129. return _common.scpustats(
  130. ctx_switches, interrupts, soft_interrupts, syscalls
  131. )
  132. # =====================================================================
  133. # --- disks
  134. # =====================================================================
  135. disk_io_counters = cext.disk_io_counters
  136. disk_usage = _psposix.disk_usage
  137. def disk_partitions(all=False):
  138. """Return system disk partitions."""
  139. # TODO - the filtering logic should be better checked so that
  140. # it tries to reflect 'df' as much as possible
  141. retlist = []
  142. partitions = cext.disk_partitions()
  143. for partition in partitions:
  144. device, mountpoint, fstype, opts = partition
  145. if device == 'none':
  146. device = ''
  147. if not all:
  148. # Differently from, say, Linux, we don't have a list of
  149. # common fs types so the best we can do, AFAIK, is to
  150. # filter by filesystem having a total size > 0.
  151. if not disk_usage(mountpoint).total:
  152. continue
  153. ntuple = _common.sdiskpart(device, mountpoint, fstype, opts)
  154. retlist.append(ntuple)
  155. return retlist
  156. # =====================================================================
  157. # --- network
  158. # =====================================================================
  159. net_if_addrs = cext_posix.net_if_addrs
  160. if HAS_NET_IO_COUNTERS:
  161. net_io_counters = cext.net_io_counters
  162. def net_connections(kind, _pid=-1):
  163. """Return socket connections. If pid == -1 return system-wide
  164. connections (as opposed to connections opened by one process only).
  165. """
  166. cmap = _common.conn_tmap
  167. if kind not in cmap:
  168. raise ValueError(
  169. "invalid %r kind argument; choose between %s"
  170. % (kind, ', '.join([repr(x) for x in cmap]))
  171. )
  172. families, types = _common.conn_tmap[kind]
  173. rawlist = cext.net_connections(_pid)
  174. ret = []
  175. for item in rawlist:
  176. fd, fam, type_, laddr, raddr, status, pid = item
  177. if fam not in families:
  178. continue
  179. if type_ not in types:
  180. continue
  181. nt = conn_to_ntuple(
  182. fd,
  183. fam,
  184. type_,
  185. laddr,
  186. raddr,
  187. status,
  188. TCP_STATUSES,
  189. pid=pid if _pid == -1 else None,
  190. )
  191. ret.append(nt)
  192. return ret
  193. def net_if_stats():
  194. """Get NIC stats (isup, duplex, speed, mtu)."""
  195. duplex_map = {"Full": NIC_DUPLEX_FULL, "Half": NIC_DUPLEX_HALF}
  196. names = set([x[0] for x in net_if_addrs()])
  197. ret = {}
  198. for name in names:
  199. mtu = cext_posix.net_if_mtu(name)
  200. flags = cext_posix.net_if_flags(name)
  201. # try to get speed and duplex
  202. # TODO: rewrite this in C (entstat forks, so use truss -f to follow.
  203. # looks like it is using an undocumented ioctl?)
  204. duplex = ""
  205. speed = 0
  206. p = subprocess.Popen(
  207. ["/usr/bin/entstat", "-d", name],
  208. stdout=subprocess.PIPE,
  209. stderr=subprocess.PIPE,
  210. )
  211. stdout, stderr = p.communicate()
  212. if PY3:
  213. stdout, stderr = (
  214. x.decode(sys.stdout.encoding) for x in (stdout, stderr)
  215. )
  216. if p.returncode == 0:
  217. re_result = re.search(
  218. r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout
  219. )
  220. if re_result is not None:
  221. speed = int(re_result.group(1))
  222. duplex = re_result.group(2)
  223. output_flags = ','.join(flags)
  224. isup = 'running' in flags
  225. duplex = duplex_map.get(duplex, NIC_DUPLEX_UNKNOWN)
  226. ret[name] = _common.snicstats(isup, duplex, speed, mtu, output_flags)
  227. return ret
  228. # =====================================================================
  229. # --- other system functions
  230. # =====================================================================
  231. def boot_time():
  232. """The system boot time expressed in seconds since the epoch."""
  233. return cext.boot_time()
  234. def users():
  235. """Return currently connected users as a list of namedtuples."""
  236. retlist = []
  237. rawlist = cext.users()
  238. localhost = (':0.0', ':0')
  239. for item in rawlist:
  240. user, tty, hostname, tstamp, user_process, pid = item
  241. # note: the underlying C function includes entries about
  242. # system boot, run level and others. We might want
  243. # to use them in the future.
  244. if not user_process:
  245. continue
  246. if hostname in localhost:
  247. hostname = 'localhost'
  248. nt = _common.suser(user, tty, hostname, tstamp, pid)
  249. retlist.append(nt)
  250. return retlist
  251. # =====================================================================
  252. # --- processes
  253. # =====================================================================
  254. def pids():
  255. """Returns a list of PIDs currently running on the system."""
  256. return [int(x) for x in os.listdir(get_procfs_path()) if x.isdigit()]
  257. def pid_exists(pid):
  258. """Check for the existence of a unix pid."""
  259. return os.path.exists(os.path.join(get_procfs_path(), str(pid), "psinfo"))
  260. def wrap_exceptions(fun):
  261. """Call callable into a try/except clause and translate ENOENT,
  262. EACCES and EPERM in NoSuchProcess or AccessDenied exceptions.
  263. """
  264. @functools.wraps(fun)
  265. def wrapper(self, *args, **kwargs):
  266. try:
  267. return fun(self, *args, **kwargs)
  268. except (FileNotFoundError, ProcessLookupError):
  269. # ENOENT (no such file or directory) gets raised on open().
  270. # ESRCH (no such process) can get raised on read() if
  271. # process is gone in meantime.
  272. if not pid_exists(self.pid):
  273. raise NoSuchProcess(self.pid, self._name)
  274. else:
  275. raise ZombieProcess(self.pid, self._name, self._ppid)
  276. except PermissionError:
  277. raise AccessDenied(self.pid, self._name)
  278. return wrapper
  279. class Process:
  280. """Wrapper class around underlying C implementation."""
  281. __slots__ = ["_cache", "_name", "_ppid", "_procfs_path", "pid"]
  282. def __init__(self, pid):
  283. self.pid = pid
  284. self._name = None
  285. self._ppid = None
  286. self._procfs_path = get_procfs_path()
  287. def oneshot_enter(self):
  288. self._proc_basic_info.cache_activate(self)
  289. self._proc_cred.cache_activate(self)
  290. def oneshot_exit(self):
  291. self._proc_basic_info.cache_deactivate(self)
  292. self._proc_cred.cache_deactivate(self)
  293. @wrap_exceptions
  294. @memoize_when_activated
  295. def _proc_basic_info(self):
  296. return cext.proc_basic_info(self.pid, self._procfs_path)
  297. @wrap_exceptions
  298. @memoize_when_activated
  299. def _proc_cred(self):
  300. return cext.proc_cred(self.pid, self._procfs_path)
  301. @wrap_exceptions
  302. def name(self):
  303. if self.pid == 0:
  304. return "swapper"
  305. # note: max 16 characters
  306. return cext.proc_name(self.pid, self._procfs_path).rstrip("\x00")
  307. @wrap_exceptions
  308. def exe(self):
  309. # there is no way to get executable path in AIX other than to guess,
  310. # and guessing is more complex than what's in the wrapping class
  311. cmdline = self.cmdline()
  312. if not cmdline:
  313. return ''
  314. exe = cmdline[0]
  315. if os.path.sep in exe:
  316. # relative or absolute path
  317. if not os.path.isabs(exe):
  318. # if cwd has changed, we're out of luck - this may be wrong!
  319. exe = os.path.abspath(os.path.join(self.cwd(), exe))
  320. if (
  321. os.path.isabs(exe)
  322. and os.path.isfile(exe)
  323. and os.access(exe, os.X_OK)
  324. ):
  325. return exe
  326. # not found, move to search in PATH using basename only
  327. exe = os.path.basename(exe)
  328. # search for exe name PATH
  329. for path in os.environ["PATH"].split(":"):
  330. possible_exe = os.path.abspath(os.path.join(path, exe))
  331. if os.path.isfile(possible_exe) and os.access(
  332. possible_exe, os.X_OK
  333. ):
  334. return possible_exe
  335. return ''
  336. @wrap_exceptions
  337. def cmdline(self):
  338. return cext.proc_args(self.pid)
  339. @wrap_exceptions
  340. def environ(self):
  341. return cext.proc_environ(self.pid)
  342. @wrap_exceptions
  343. def create_time(self):
  344. return self._proc_basic_info()[proc_info_map['create_time']]
  345. @wrap_exceptions
  346. def num_threads(self):
  347. return self._proc_basic_info()[proc_info_map['num_threads']]
  348. if HAS_THREADS:
  349. @wrap_exceptions
  350. def threads(self):
  351. rawlist = cext.proc_threads(self.pid)
  352. retlist = []
  353. for thread_id, utime, stime in rawlist:
  354. ntuple = _common.pthread(thread_id, utime, stime)
  355. retlist.append(ntuple)
  356. # The underlying C implementation retrieves all OS threads
  357. # and filters them by PID. At this point we can't tell whether
  358. # an empty list means there were no connections for process or
  359. # process is no longer active so we force NSP in case the PID
  360. # is no longer there.
  361. if not retlist:
  362. # will raise NSP if process is gone
  363. os.stat('%s/%s' % (self._procfs_path, self.pid))
  364. return retlist
  365. @wrap_exceptions
  366. def net_connections(self, kind='inet'):
  367. ret = net_connections(kind, _pid=self.pid)
  368. # The underlying C implementation retrieves all OS connections
  369. # and filters them by PID. At this point we can't tell whether
  370. # an empty list means there were no connections for process or
  371. # process is no longer active so we force NSP in case the PID
  372. # is no longer there.
  373. if not ret:
  374. # will raise NSP if process is gone
  375. os.stat('%s/%s' % (self._procfs_path, self.pid))
  376. return ret
  377. @wrap_exceptions
  378. def nice_get(self):
  379. return cext_posix.getpriority(self.pid)
  380. @wrap_exceptions
  381. def nice_set(self, value):
  382. return cext_posix.setpriority(self.pid, value)
  383. @wrap_exceptions
  384. def ppid(self):
  385. self._ppid = self._proc_basic_info()[proc_info_map['ppid']]
  386. return self._ppid
  387. @wrap_exceptions
  388. def uids(self):
  389. real, effective, saved, _, _, _ = self._proc_cred()
  390. return _common.puids(real, effective, saved)
  391. @wrap_exceptions
  392. def gids(self):
  393. _, _, _, real, effective, saved = self._proc_cred()
  394. return _common.puids(real, effective, saved)
  395. @wrap_exceptions
  396. def cpu_times(self):
  397. t = cext.proc_cpu_times(self.pid, self._procfs_path)
  398. return _common.pcputimes(*t)
  399. @wrap_exceptions
  400. def terminal(self):
  401. ttydev = self._proc_basic_info()[proc_info_map['ttynr']]
  402. # convert from 64-bit dev_t to 32-bit dev_t and then map the device
  403. ttydev = ((ttydev & 0x0000FFFF00000000) >> 16) | (ttydev & 0xFFFF)
  404. # try to match rdev of /dev/pts/* files ttydev
  405. for dev in glob.glob("/dev/**/*"):
  406. if os.stat(dev).st_rdev == ttydev:
  407. return dev
  408. return None
  409. @wrap_exceptions
  410. def cwd(self):
  411. procfs_path = self._procfs_path
  412. try:
  413. result = os.readlink("%s/%s/cwd" % (procfs_path, self.pid))
  414. return result.rstrip('/')
  415. except FileNotFoundError:
  416. os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD
  417. return ""
  418. @wrap_exceptions
  419. def memory_info(self):
  420. ret = self._proc_basic_info()
  421. rss = ret[proc_info_map['rss']] * 1024
  422. vms = ret[proc_info_map['vms']] * 1024
  423. return pmem(rss, vms)
  424. memory_full_info = memory_info
  425. @wrap_exceptions
  426. def status(self):
  427. code = self._proc_basic_info()[proc_info_map['status']]
  428. # XXX is '?' legit? (we're not supposed to return it anyway)
  429. return PROC_STATUSES.get(code, '?')
  430. def open_files(self):
  431. # TODO rewrite without using procfiles (stat /proc/pid/fd/* and then
  432. # find matching name of the inode)
  433. p = subprocess.Popen(
  434. ["/usr/bin/procfiles", "-n", str(self.pid)],
  435. stdout=subprocess.PIPE,
  436. stderr=subprocess.PIPE,
  437. )
  438. stdout, stderr = p.communicate()
  439. if PY3:
  440. stdout, stderr = (
  441. x.decode(sys.stdout.encoding) for x in (stdout, stderr)
  442. )
  443. if "no such process" in stderr.lower():
  444. raise NoSuchProcess(self.pid, self._name)
  445. procfiles = re.findall(r"(\d+): S_IFREG.*name:(.*)\n", stdout)
  446. retlist = []
  447. for fd, path in procfiles:
  448. path = path.strip()
  449. if path.startswith("//"):
  450. path = path[1:]
  451. if path.lower() == "cannot be retrieved":
  452. continue
  453. retlist.append(_common.popenfile(path, int(fd)))
  454. return retlist
  455. @wrap_exceptions
  456. def num_fds(self):
  457. if self.pid == 0: # no /proc/0/fd
  458. return 0
  459. return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid)))
  460. @wrap_exceptions
  461. def num_ctx_switches(self):
  462. return _common.pctxsw(*cext.proc_num_ctx_switches(self.pid))
  463. @wrap_exceptions
  464. def wait(self, timeout=None):
  465. return _psposix.wait_pid(self.pid, timeout, self._name)
  466. if HAS_PROC_IO_COUNTERS:
  467. @wrap_exceptions
  468. def io_counters(self):
  469. try:
  470. rc, wc, rb, wb = cext.proc_io_counters(self.pid)
  471. except OSError:
  472. # if process is terminated, proc_io_counters returns OSError
  473. # instead of NSP
  474. if not pid_exists(self.pid):
  475. raise NoSuchProcess(self.pid, self._name)
  476. raise
  477. return _common.pio(rc, wc, rb, wb)