_psaix.py 17 KB

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