_psaix.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  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 = os.sysconf('SC_PAGE_SIZE')
  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. ntuple = _common.sdiskpart(device, mountpoint, fstype, opts)
  152. retlist.append(ntuple)
  153. return retlist
  154. # =====================================================================
  155. # --- network
  156. # =====================================================================
  157. net_if_addrs = cext_posix.net_if_addrs
  158. if HAS_NET_IO_COUNTERS:
  159. net_io_counters = cext.net_io_counters
  160. def net_connections(kind, _pid=-1):
  161. """Return socket connections. If pid == -1 return system-wide
  162. connections (as opposed to connections opened by one process only).
  163. """
  164. cmap = _common.conn_tmap
  165. if kind not in cmap:
  166. raise ValueError("invalid %r kind argument; choose between %s"
  167. % (kind, ', '.join([repr(x) for x in cmap])))
  168. families, types = _common.conn_tmap[kind]
  169. rawlist = cext.net_connections(_pid)
  170. ret = []
  171. for item in rawlist:
  172. fd, fam, type_, laddr, raddr, status, pid = item
  173. if fam not in families:
  174. continue
  175. if type_ not in types:
  176. continue
  177. nt = conn_to_ntuple(fd, fam, type_, laddr, raddr, status,
  178. TCP_STATUSES, pid=pid if _pid == -1 else None)
  179. ret.append(nt)
  180. return ret
  181. def net_if_stats():
  182. """Get NIC stats (isup, duplex, speed, mtu)."""
  183. duplex_map = {"Full": NIC_DUPLEX_FULL,
  184. "Half": NIC_DUPLEX_HALF}
  185. names = set([x[0] for x in net_if_addrs()])
  186. ret = {}
  187. for name in names:
  188. isup, mtu = cext.net_if_stats(name)
  189. # try to get speed and duplex
  190. # TODO: rewrite this in C (entstat forks, so use truss -f to follow.
  191. # looks like it is using an undocumented ioctl?)
  192. duplex = ""
  193. speed = 0
  194. p = subprocess.Popen(["/usr/bin/entstat", "-d", name],
  195. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  196. stdout, stderr = p.communicate()
  197. if PY3:
  198. stdout, stderr = [x.decode(sys.stdout.encoding)
  199. for x in (stdout, stderr)]
  200. if p.returncode == 0:
  201. re_result = re.search(
  202. r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout)
  203. if re_result is not None:
  204. speed = int(re_result.group(1))
  205. duplex = re_result.group(2)
  206. duplex = duplex_map.get(duplex, NIC_DUPLEX_UNKNOWN)
  207. ret[name] = _common.snicstats(isup, duplex, speed, mtu)
  208. return ret
  209. # =====================================================================
  210. # --- other system functions
  211. # =====================================================================
  212. def boot_time():
  213. """The system boot time expressed in seconds since the epoch."""
  214. return cext.boot_time()
  215. def users():
  216. """Return currently connected users as a list of namedtuples."""
  217. retlist = []
  218. rawlist = cext.users()
  219. localhost = (':0.0', ':0')
  220. for item in rawlist:
  221. user, tty, hostname, tstamp, user_process, pid = item
  222. # note: the underlying C function includes entries about
  223. # system boot, run level and others. We might want
  224. # to use them in the future.
  225. if not user_process:
  226. continue
  227. if hostname in localhost:
  228. hostname = 'localhost'
  229. nt = _common.suser(user, tty, hostname, tstamp, pid)
  230. retlist.append(nt)
  231. return retlist
  232. # =====================================================================
  233. # --- processes
  234. # =====================================================================
  235. def pids():
  236. """Returns a list of PIDs currently running on the system."""
  237. return [int(x) for x in os.listdir(get_procfs_path()) if x.isdigit()]
  238. def pid_exists(pid):
  239. """Check for the existence of a unix pid."""
  240. return os.path.exists(os.path.join(get_procfs_path(), str(pid), "psinfo"))
  241. def wrap_exceptions(fun):
  242. """Call callable into a try/except clause and translate ENOENT,
  243. EACCES and EPERM in NoSuchProcess or AccessDenied exceptions.
  244. """
  245. @functools.wraps(fun)
  246. def wrapper(self, *args, **kwargs):
  247. try:
  248. return fun(self, *args, **kwargs)
  249. except (FileNotFoundError, ProcessLookupError):
  250. # ENOENT (no such file or directory) gets raised on open().
  251. # ESRCH (no such process) can get raised on read() if
  252. # process is gone in meantime.
  253. if not pid_exists(self.pid):
  254. raise NoSuchProcess(self.pid, self._name)
  255. else:
  256. raise ZombieProcess(self.pid, self._name, self._ppid)
  257. except PermissionError:
  258. raise AccessDenied(self.pid, self._name)
  259. return wrapper
  260. class Process(object):
  261. """Wrapper class around underlying C implementation."""
  262. __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"]
  263. def __init__(self, pid):
  264. self.pid = pid
  265. self._name = None
  266. self._ppid = None
  267. self._procfs_path = get_procfs_path()
  268. def oneshot_enter(self):
  269. self._proc_basic_info.cache_activate(self)
  270. self._proc_cred.cache_activate(self)
  271. def oneshot_exit(self):
  272. self._proc_basic_info.cache_deactivate(self)
  273. self._proc_cred.cache_deactivate(self)
  274. @wrap_exceptions
  275. @memoize_when_activated
  276. def _proc_basic_info(self):
  277. return cext.proc_basic_info(self.pid, self._procfs_path)
  278. @wrap_exceptions
  279. @memoize_when_activated
  280. def _proc_cred(self):
  281. return cext.proc_cred(self.pid, self._procfs_path)
  282. @wrap_exceptions
  283. def name(self):
  284. if self.pid == 0:
  285. return "swapper"
  286. # note: max 16 characters
  287. return cext.proc_name(self.pid, self._procfs_path).rstrip("\x00")
  288. @wrap_exceptions
  289. def exe(self):
  290. # there is no way to get executable path in AIX other than to guess,
  291. # and guessing is more complex than what's in the wrapping class
  292. cmdline = self.cmdline()
  293. if not cmdline:
  294. return ''
  295. exe = cmdline[0]
  296. if os.path.sep in exe:
  297. # relative or absolute path
  298. if not os.path.isabs(exe):
  299. # if cwd has changed, we're out of luck - this may be wrong!
  300. exe = os.path.abspath(os.path.join(self.cwd(), exe))
  301. if (os.path.isabs(exe) and
  302. os.path.isfile(exe) and
  303. os.access(exe, os.X_OK)):
  304. return exe
  305. # not found, move to search in PATH using basename only
  306. exe = os.path.basename(exe)
  307. # search for exe name PATH
  308. for path in os.environ["PATH"].split(":"):
  309. possible_exe = os.path.abspath(os.path.join(path, exe))
  310. if (os.path.isfile(possible_exe) and
  311. os.access(possible_exe, os.X_OK)):
  312. return possible_exe
  313. return ''
  314. @wrap_exceptions
  315. def cmdline(self):
  316. return cext.proc_args(self.pid)
  317. @wrap_exceptions
  318. def environ(self):
  319. return cext.proc_environ(self.pid)
  320. @wrap_exceptions
  321. def create_time(self):
  322. return self._proc_basic_info()[proc_info_map['create_time']]
  323. @wrap_exceptions
  324. def num_threads(self):
  325. return self._proc_basic_info()[proc_info_map['num_threads']]
  326. if HAS_THREADS:
  327. @wrap_exceptions
  328. def threads(self):
  329. rawlist = cext.proc_threads(self.pid)
  330. retlist = []
  331. for thread_id, utime, stime in rawlist:
  332. ntuple = _common.pthread(thread_id, utime, stime)
  333. retlist.append(ntuple)
  334. # The underlying C implementation retrieves all OS threads
  335. # and filters them by PID. At this point we can't tell whether
  336. # an empty list means there were no connections for process or
  337. # process is no longer active so we force NSP in case the PID
  338. # is no longer there.
  339. if not retlist:
  340. # will raise NSP if process is gone
  341. os.stat('%s/%s' % (self._procfs_path, self.pid))
  342. return retlist
  343. @wrap_exceptions
  344. def connections(self, kind='inet'):
  345. ret = net_connections(kind, _pid=self.pid)
  346. # The underlying C implementation retrieves all OS connections
  347. # and filters them by PID. At this point we can't tell whether
  348. # an empty list means there were no connections for process or
  349. # process is no longer active so we force NSP in case the PID
  350. # is no longer there.
  351. if not ret:
  352. # will raise NSP if process is gone
  353. os.stat('%s/%s' % (self._procfs_path, self.pid))
  354. return ret
  355. @wrap_exceptions
  356. def nice_get(self):
  357. return cext_posix.getpriority(self.pid)
  358. @wrap_exceptions
  359. def nice_set(self, value):
  360. return cext_posix.setpriority(self.pid, value)
  361. @wrap_exceptions
  362. def ppid(self):
  363. self._ppid = self._proc_basic_info()[proc_info_map['ppid']]
  364. return self._ppid
  365. @wrap_exceptions
  366. def uids(self):
  367. real, effective, saved, _, _, _ = self._proc_cred()
  368. return _common.puids(real, effective, saved)
  369. @wrap_exceptions
  370. def gids(self):
  371. _, _, _, real, effective, saved = self._proc_cred()
  372. return _common.puids(real, effective, saved)
  373. @wrap_exceptions
  374. def cpu_times(self):
  375. cpu_times = cext.proc_cpu_times(self.pid, self._procfs_path)
  376. return _common.pcputimes(*cpu_times)
  377. @wrap_exceptions
  378. def terminal(self):
  379. ttydev = self._proc_basic_info()[proc_info_map['ttynr']]
  380. # convert from 64-bit dev_t to 32-bit dev_t and then map the device
  381. ttydev = (((ttydev & 0x0000FFFF00000000) >> 16) | (ttydev & 0xFFFF))
  382. # try to match rdev of /dev/pts/* files ttydev
  383. for dev in glob.glob("/dev/**/*"):
  384. if os.stat(dev).st_rdev == ttydev:
  385. return dev
  386. return None
  387. @wrap_exceptions
  388. def cwd(self):
  389. procfs_path = self._procfs_path
  390. try:
  391. result = os.readlink("%s/%s/cwd" % (procfs_path, self.pid))
  392. return result.rstrip('/')
  393. except FileNotFoundError:
  394. os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD
  395. return None
  396. @wrap_exceptions
  397. def memory_info(self):
  398. ret = self._proc_basic_info()
  399. rss = ret[proc_info_map['rss']] * 1024
  400. vms = ret[proc_info_map['vms']] * 1024
  401. return pmem(rss, vms)
  402. memory_full_info = memory_info
  403. @wrap_exceptions
  404. def status(self):
  405. code = self._proc_basic_info()[proc_info_map['status']]
  406. # XXX is '?' legit? (we're not supposed to return it anyway)
  407. return PROC_STATUSES.get(code, '?')
  408. def open_files(self):
  409. # TODO rewrite without using procfiles (stat /proc/pid/fd/* and then
  410. # find matching name of the inode)
  411. p = subprocess.Popen(["/usr/bin/procfiles", "-n", str(self.pid)],
  412. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  413. stdout, stderr = p.communicate()
  414. if PY3:
  415. stdout, stderr = [x.decode(sys.stdout.encoding)
  416. for x in (stdout, stderr)]
  417. if "no such process" in stderr.lower():
  418. raise NoSuchProcess(self.pid, self._name)
  419. procfiles = re.findall(r"(\d+): S_IFREG.*\s*.*name:(.*)\n", stdout)
  420. retlist = []
  421. for fd, path in procfiles:
  422. path = path.strip()
  423. if path.startswith("//"):
  424. path = path[1:]
  425. if path.lower() == "cannot be retrieved":
  426. continue
  427. retlist.append(_common.popenfile(path, int(fd)))
  428. return retlist
  429. @wrap_exceptions
  430. def num_fds(self):
  431. if self.pid == 0: # no /proc/0/fd
  432. return 0
  433. return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid)))
  434. @wrap_exceptions
  435. def num_ctx_switches(self):
  436. return _common.pctxsw(
  437. *cext.proc_num_ctx_switches(self.pid))
  438. @wrap_exceptions
  439. def wait(self, timeout=None):
  440. return _psposix.wait_pid(self.pid, timeout, self._name)
  441. if HAS_PROC_IO_COUNTERS:
  442. @wrap_exceptions
  443. def io_counters(self):
  444. try:
  445. rc, wc, rb, wb = cext.proc_io_counters(self.pid)
  446. except OSError:
  447. # if process is terminated, proc_io_counters returns OSError
  448. # instead of NSP
  449. if not pid_exists(self.pid):
  450. raise NoSuchProcess(self.pid, self._name)
  451. raise
  452. return _common.pio(rc, wc, rb, wb)