1# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""OSX platform implementation.""" 6 7import contextlib 8import errno 9import functools 10import os 11from socket import AF_INET 12from collections import namedtuple 13 14from . import _common 15from . import _psposix 16from . import _psutil_osx as cext 17from . import _psutil_posix as cext_posix 18from ._common import AF_INET6 19from ._common import conn_tmap 20from ._common import isfile_strict 21from ._common import memoize_when_activated 22from ._common import parse_environ_block 23from ._common import sockfam_to_enum 24from ._common import socktype_to_enum 25from ._common import usage_percent 26from ._exceptions import AccessDenied 27from ._exceptions import NoSuchProcess 28from ._exceptions import ZombieProcess 29 30 31__extra__all__ = [] 32 33 34# ===================================================================== 35# --- globals 36# ===================================================================== 37 38 39PAGESIZE = os.sysconf("SC_PAGE_SIZE") 40AF_LINK = cext_posix.AF_LINK 41 42TCP_STATUSES = { 43 cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, 44 cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT, 45 cext.TCPS_SYN_RECEIVED: _common.CONN_SYN_RECV, 46 cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1, 47 cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2, 48 cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT, 49 cext.TCPS_CLOSED: _common.CONN_CLOSE, 50 cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, 51 cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK, 52 cext.TCPS_LISTEN: _common.CONN_LISTEN, 53 cext.TCPS_CLOSING: _common.CONN_CLOSING, 54 cext.PSUTIL_CONN_NONE: _common.CONN_NONE, 55} 56 57PROC_STATUSES = { 58 cext.SIDL: _common.STATUS_IDLE, 59 cext.SRUN: _common.STATUS_RUNNING, 60 cext.SSLEEP: _common.STATUS_SLEEPING, 61 cext.SSTOP: _common.STATUS_STOPPED, 62 cext.SZOMB: _common.STATUS_ZOMBIE, 63} 64 65kinfo_proc_map = dict( 66 ppid=0, 67 ruid=1, 68 euid=2, 69 suid=3, 70 rgid=4, 71 egid=5, 72 sgid=6, 73 ttynr=7, 74 ctime=8, 75 status=9, 76 name=10, 77) 78 79pidtaskinfo_map = dict( 80 cpuutime=0, 81 cpustime=1, 82 rss=2, 83 vms=3, 84 pfaults=4, 85 pageins=5, 86 numthreads=6, 87 volctxsw=7, 88) 89 90 91# ===================================================================== 92# --- named tuples 93# ===================================================================== 94 95 96# psutil.cpu_times() 97scputimes = namedtuple('scputimes', ['user', 'nice', 'system', 'idle']) 98# psutil.virtual_memory() 99svmem = namedtuple( 100 'svmem', ['total', 'available', 'percent', 'used', 'free', 101 'active', 'inactive', 'wired']) 102# psutil.Process.memory_info() 103pmem = namedtuple('pmem', ['rss', 'vms', 'pfaults', 'pageins']) 104# psutil.Process.memory_full_info() 105pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', )) 106# psutil.Process.memory_maps(grouped=True) 107pmmap_grouped = namedtuple( 108 'pmmap_grouped', 109 'path rss private swapped dirtied ref_count shadow_depth') 110# psutil.Process.memory_maps(grouped=False) 111pmmap_ext = namedtuple( 112 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) 113 114 115# ===================================================================== 116# --- memory 117# ===================================================================== 118 119 120def virtual_memory(): 121 """System virtual memory as a namedtuple.""" 122 total, active, inactive, wired, free = cext.virtual_mem() 123 avail = inactive + free 124 used = active + inactive + wired 125 percent = usage_percent((total - avail), total, _round=1) 126 return svmem(total, avail, percent, used, free, 127 active, inactive, wired) 128 129 130def swap_memory(): 131 """Swap system memory as a (total, used, free, sin, sout) tuple.""" 132 total, used, free, sin, sout = cext.swap_mem() 133 percent = usage_percent(used, total, _round=1) 134 return _common.sswap(total, used, free, percent, sin, sout) 135 136 137# ===================================================================== 138# --- CPU 139# ===================================================================== 140 141 142def cpu_times(): 143 """Return system CPU times as a namedtuple.""" 144 user, nice, system, idle = cext.cpu_times() 145 return scputimes(user, nice, system, idle) 146 147 148def per_cpu_times(): 149 """Return system CPU times as a named tuple""" 150 ret = [] 151 for cpu_t in cext.per_cpu_times(): 152 user, nice, system, idle = cpu_t 153 item = scputimes(user, nice, system, idle) 154 ret.append(item) 155 return ret 156 157 158def cpu_count_logical(): 159 """Return the number of logical CPUs in the system.""" 160 return cext.cpu_count_logical() 161 162 163def cpu_count_physical(): 164 """Return the number of physical CPUs in the system.""" 165 return cext.cpu_count_phys() 166 167 168def cpu_stats(): 169 ctx_switches, interrupts, soft_interrupts, syscalls, traps = \ 170 cext.cpu_stats() 171 return _common.scpustats( 172 ctx_switches, interrupts, soft_interrupts, syscalls) 173 174 175def cpu_freq(): 176 """Return CPU frequency. 177 On OSX per-cpu frequency is not supported. 178 Also, the returned frequency never changes, see: 179 https://arstechnica.com/civis/viewtopic.php?f=19&t=465002 180 """ 181 curr, min_, max_ = cext.cpu_freq() 182 return [_common.scpufreq(curr, min_, max_)] 183 184 185# ===================================================================== 186# --- disks 187# ===================================================================== 188 189 190disk_usage = _psposix.disk_usage 191disk_io_counters = cext.disk_io_counters 192 193 194def disk_partitions(all=False): 195 """Return mounted disk partitions as a list of namedtuples.""" 196 retlist = [] 197 partitions = cext.disk_partitions() 198 for partition in partitions: 199 device, mountpoint, fstype, opts = partition 200 if device == 'none': 201 device = '' 202 if not all: 203 if not os.path.isabs(device) or not os.path.exists(device): 204 continue 205 ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) 206 retlist.append(ntuple) 207 return retlist 208 209 210# ===================================================================== 211# --- sensors 212# ===================================================================== 213 214 215def sensors_battery(): 216 """Return battery information. 217 """ 218 try: 219 percent, minsleft, power_plugged = cext.sensors_battery() 220 except NotImplementedError: 221 # no power source - return None according to interface 222 return None 223 power_plugged = power_plugged == 1 224 if power_plugged: 225 secsleft = _common.POWER_TIME_UNLIMITED 226 elif minsleft == -1: 227 secsleft = _common.POWER_TIME_UNKNOWN 228 else: 229 secsleft = minsleft * 60 230 return _common.sbattery(percent, secsleft, power_plugged) 231 232 233# ===================================================================== 234# --- network 235# ===================================================================== 236 237 238net_io_counters = cext.net_io_counters 239net_if_addrs = cext_posix.net_if_addrs 240 241 242def net_connections(kind='inet'): 243 """System-wide network connections.""" 244 # Note: on OSX this will fail with AccessDenied unless 245 # the process is owned by root. 246 ret = [] 247 for pid in pids(): 248 try: 249 cons = Process(pid).connections(kind) 250 except NoSuchProcess: 251 continue 252 else: 253 if cons: 254 for c in cons: 255 c = list(c) + [pid] 256 ret.append(_common.sconn(*c)) 257 return ret 258 259 260def net_if_stats(): 261 """Get NIC stats (isup, duplex, speed, mtu).""" 262 names = net_io_counters().keys() 263 ret = {} 264 for name in names: 265 mtu = cext_posix.net_if_mtu(name) 266 isup = cext_posix.net_if_flags(name) 267 duplex, speed = cext_posix.net_if_duplex_speed(name) 268 if hasattr(_common, 'NicDuplex'): 269 duplex = _common.NicDuplex(duplex) 270 ret[name] = _common.snicstats(isup, duplex, speed, mtu) 271 return ret 272 273 274# ===================================================================== 275# --- other system functions 276# ===================================================================== 277 278 279def boot_time(): 280 """The system boot time expressed in seconds since the epoch.""" 281 return cext.boot_time() 282 283 284def users(): 285 """Return currently connected users as a list of namedtuples.""" 286 retlist = [] 287 rawlist = cext.users() 288 for item in rawlist: 289 user, tty, hostname, tstamp, pid = item 290 if tty == '~': 291 continue # reboot or shutdown 292 if not tstamp: 293 continue 294 nt = _common.suser(user, tty or None, hostname or None, tstamp, pid) 295 retlist.append(nt) 296 return retlist 297 298 299# ===================================================================== 300# --- processes 301# ===================================================================== 302 303 304def pids(): 305 ls = cext.pids() 306 if 0 not in ls: 307 # On certain OSX versions pids() C doesn't return PID 0 but 308 # "ps" does and the process is querable via sysctl(): 309 # https://travis-ci.org/giampaolo/psutil/jobs/309619941 310 try: 311 Process(0).create_time() 312 ls.append(0) 313 except NoSuchProcess: 314 pass 315 except AccessDenied: 316 ls.append(0) 317 return ls 318 319 320pid_exists = _psposix.pid_exists 321 322 323def wrap_exceptions(fun): 324 """Decorator which translates bare OSError exceptions into 325 NoSuchProcess and AccessDenied. 326 """ 327 @functools.wraps(fun) 328 def wrapper(self, *args, **kwargs): 329 try: 330 return fun(self, *args, **kwargs) 331 except OSError as err: 332 if err.errno == errno.ESRCH: 333 raise NoSuchProcess(self.pid, self._name) 334 if err.errno in (errno.EPERM, errno.EACCES): 335 raise AccessDenied(self.pid, self._name) 336 raise 337 return wrapper 338 339 340@contextlib.contextmanager 341def catch_zombie(proc): 342 """There are some poor C APIs which incorrectly raise ESRCH when 343 the process is still alive or it's a zombie, or even RuntimeError 344 (those who don't set errno). This is here in order to solve: 345 https://github.com/giampaolo/psutil/issues/1044 346 """ 347 try: 348 yield 349 except (OSError, RuntimeError) as err: 350 if isinstance(err, RuntimeError) or err.errno == errno.ESRCH: 351 try: 352 # status() is not supposed to lie and correctly detect 353 # zombies so if it raises ESRCH it's true. 354 status = proc.status() 355 except NoSuchProcess: 356 raise err 357 else: 358 if status == _common.STATUS_ZOMBIE: 359 raise ZombieProcess(proc.pid, proc._name, proc._ppid) 360 else: 361 raise AccessDenied(proc.pid, proc._name) 362 else: 363 raise 364 365 366class Process(object): 367 """Wrapper class around underlying C implementation.""" 368 369 __slots__ = ["pid", "_name", "_ppid"] 370 371 def __init__(self, pid): 372 self.pid = pid 373 self._name = None 374 self._ppid = None 375 376 @memoize_when_activated 377 def _get_kinfo_proc(self): 378 # Note: should work with all PIDs without permission issues. 379 ret = cext.proc_kinfo_oneshot(self.pid) 380 assert len(ret) == len(kinfo_proc_map) 381 return ret 382 383 @memoize_when_activated 384 def _get_pidtaskinfo(self): 385 # Note: should work for PIDs owned by user only. 386 with catch_zombie(self): 387 ret = cext.proc_pidtaskinfo_oneshot(self.pid) 388 assert len(ret) == len(pidtaskinfo_map) 389 return ret 390 391 def oneshot_enter(self): 392 self._get_kinfo_proc.cache_activate() 393 self._get_pidtaskinfo.cache_activate() 394 395 def oneshot_exit(self): 396 self._get_kinfo_proc.cache_deactivate() 397 self._get_pidtaskinfo.cache_deactivate() 398 399 @wrap_exceptions 400 def name(self): 401 name = self._get_kinfo_proc()[kinfo_proc_map['name']] 402 return name if name is not None else cext.proc_name(self.pid) 403 404 @wrap_exceptions 405 def exe(self): 406 with catch_zombie(self): 407 return cext.proc_exe(self.pid) 408 409 @wrap_exceptions 410 def cmdline(self): 411 with catch_zombie(self): 412 return cext.proc_cmdline(self.pid) 413 414 @wrap_exceptions 415 def environ(self): 416 with catch_zombie(self): 417 return parse_environ_block(cext.proc_environ(self.pid)) 418 419 @wrap_exceptions 420 def ppid(self): 421 self._ppid = self._get_kinfo_proc()[kinfo_proc_map['ppid']] 422 return self._ppid 423 424 @wrap_exceptions 425 def cwd(self): 426 with catch_zombie(self): 427 return cext.proc_cwd(self.pid) 428 429 @wrap_exceptions 430 def uids(self): 431 rawtuple = self._get_kinfo_proc() 432 return _common.puids( 433 rawtuple[kinfo_proc_map['ruid']], 434 rawtuple[kinfo_proc_map['euid']], 435 rawtuple[kinfo_proc_map['suid']]) 436 437 @wrap_exceptions 438 def gids(self): 439 rawtuple = self._get_kinfo_proc() 440 return _common.puids( 441 rawtuple[kinfo_proc_map['rgid']], 442 rawtuple[kinfo_proc_map['egid']], 443 rawtuple[kinfo_proc_map['sgid']]) 444 445 @wrap_exceptions 446 def terminal(self): 447 tty_nr = self._get_kinfo_proc()[kinfo_proc_map['ttynr']] 448 tmap = _psposix.get_terminal_map() 449 try: 450 return tmap[tty_nr] 451 except KeyError: 452 return None 453 454 @wrap_exceptions 455 def memory_info(self): 456 rawtuple = self._get_pidtaskinfo() 457 return pmem( 458 rawtuple[pidtaskinfo_map['rss']], 459 rawtuple[pidtaskinfo_map['vms']], 460 rawtuple[pidtaskinfo_map['pfaults']], 461 rawtuple[pidtaskinfo_map['pageins']], 462 ) 463 464 @wrap_exceptions 465 def memory_full_info(self): 466 basic_mem = self.memory_info() 467 uss = cext.proc_memory_uss(self.pid) 468 return pfullmem(*basic_mem + (uss, )) 469 470 @wrap_exceptions 471 def cpu_times(self): 472 rawtuple = self._get_pidtaskinfo() 473 return _common.pcputimes( 474 rawtuple[pidtaskinfo_map['cpuutime']], 475 rawtuple[pidtaskinfo_map['cpustime']], 476 # children user / system times are not retrievable (set to 0) 477 0.0, 0.0) 478 479 @wrap_exceptions 480 def create_time(self): 481 return self._get_kinfo_proc()[kinfo_proc_map['ctime']] 482 483 @wrap_exceptions 484 def num_ctx_switches(self): 485 # Unvoluntary value seems not to be available; 486 # getrusage() numbers seems to confirm this theory. 487 # We set it to 0. 488 vol = self._get_pidtaskinfo()[pidtaskinfo_map['volctxsw']] 489 return _common.pctxsw(vol, 0) 490 491 @wrap_exceptions 492 def num_threads(self): 493 return self._get_pidtaskinfo()[pidtaskinfo_map['numthreads']] 494 495 @wrap_exceptions 496 def open_files(self): 497 if self.pid == 0: 498 return [] 499 files = [] 500 with catch_zombie(self): 501 rawlist = cext.proc_open_files(self.pid) 502 for path, fd in rawlist: 503 if isfile_strict(path): 504 ntuple = _common.popenfile(path, fd) 505 files.append(ntuple) 506 return files 507 508 @wrap_exceptions 509 def connections(self, kind='inet'): 510 if kind not in conn_tmap: 511 raise ValueError("invalid %r kind argument; choose between %s" 512 % (kind, ', '.join([repr(x) for x in conn_tmap]))) 513 families, types = conn_tmap[kind] 514 with catch_zombie(self): 515 rawlist = cext.proc_connections(self.pid, families, types) 516 ret = [] 517 for item in rawlist: 518 fd, fam, type, laddr, raddr, status = item 519 status = TCP_STATUSES[status] 520 fam = sockfam_to_enum(fam) 521 type = socktype_to_enum(type) 522 if fam in (AF_INET, AF_INET6): 523 if laddr: 524 laddr = _common.addr(*laddr) 525 if raddr: 526 raddr = _common.addr(*raddr) 527 nt = _common.pconn(fd, fam, type, laddr, raddr, status) 528 ret.append(nt) 529 return ret 530 531 @wrap_exceptions 532 def num_fds(self): 533 if self.pid == 0: 534 return 0 535 with catch_zombie(self): 536 return cext.proc_num_fds(self.pid) 537 538 @wrap_exceptions 539 def wait(self, timeout=None): 540 return _psposix.wait_pid(self.pid, timeout, self._name) 541 542 @wrap_exceptions 543 def nice_get(self): 544 with catch_zombie(self): 545 return cext_posix.getpriority(self.pid) 546 547 @wrap_exceptions 548 def nice_set(self, value): 549 with catch_zombie(self): 550 return cext_posix.setpriority(self.pid, value) 551 552 @wrap_exceptions 553 def status(self): 554 code = self._get_kinfo_proc()[kinfo_proc_map['status']] 555 # XXX is '?' legit? (we're not supposed to return it anyway) 556 return PROC_STATUSES.get(code, '?') 557 558 @wrap_exceptions 559 def threads(self): 560 with catch_zombie(self): 561 rawlist = cext.proc_threads(self.pid) 562 retlist = [] 563 for thread_id, utime, stime in rawlist: 564 ntuple = _common.pthread(thread_id, utime, stime) 565 retlist.append(ntuple) 566 return retlist 567 568 @wrap_exceptions 569 def memory_maps(self): 570 with catch_zombie(self): 571 return cext.proc_memory_maps(self.pid) 572