_psaix.py 18 KB

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