_psaix.py 18 KB

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