_psaix.py 18 KB

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