Make pep8 happy.
[iotop.git] / iotop / ui.py
blob5ea22bc588f69361938a05985492c66c4d63d157
1 # This program is free software; you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation; either version 2 of the License, or
4 # (at your option) any later version.
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU Library General Public License for more details.
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
15 # See the COPYING file for license information.
17 # Copyright (c) 2007 Guillaume Chazarain <guichaz@gmail.com>
19 # Allow printing with same syntax in Python 2/3
20 from __future__ import print_function
22 import curses
23 import errno
24 import locale
25 import math
26 import optparse
27 import os
28 import select
29 import signal
30 import sys
31 import time
33 from iotop.data import find_uids, TaskStatsNetlink, ProcessList, Stats
34 from iotop.data import ThreadInfo
35 from iotop.version import VERSION
36 from iotop import ioprio
37 from iotop.ioprio import IoprioSetError
40 # Utility functions for the UI
43 UNITS = ['B', 'K', 'M', 'G', 'T', 'P', 'E']
46 def human_size(size):
47 if size > 0:
48 sign = ''
49 elif size < 0:
50 sign = '-'
51 size = -size
52 else:
53 return '0.00 B'
55 expo = int(math.log(size / 2, 2) / 10)
56 return '%s%.2f %s' % (
57 sign, (float(size) / (1 << (10 * expo))), UNITS[expo])
60 def format_size(options, bytes):
61 if options.kilobytes:
62 return '%.2f K' % (bytes / 1024.0)
63 return human_size(bytes)
66 def format_bandwidth(options, size, duration):
67 return format_size(options, size and float(size) / duration) + '/s'
70 def format_stats(options, process, duration):
71 # Keep in sync with TaskStatsNetlink.members_offsets and
72 # IOTopUI.get_data(self)
73 def delay2percent(delay): # delay in ns, duration in s
74 return '%.2f %%' % min(99.99, delay / (duration * 10000000.0))
75 if options.accumulated:
76 stats = process.stats_accum
77 display_format = lambda size, duration: format_size(options, size)
78 duration = time.time() - process.stats_accum_timestamp
79 else:
80 stats = process.stats_delta
81 display_format = lambda size, duration: format_bandwidth(
82 options, size, duration)
83 io_delay = delay2percent(stats.blkio_delay_total)
84 swapin_delay = delay2percent(stats.swapin_delay_total)
85 read_bytes = display_format(stats.read_bytes, duration)
86 written_bytes = stats.write_bytes - stats.cancelled_write_bytes
87 written_bytes = max(0, written_bytes)
88 write_bytes = display_format(written_bytes, duration)
89 return io_delay, swapin_delay, read_bytes, write_bytes
92 def get_max_pid_width():
93 try:
94 return len(open('/proc/sys/kernel/pid_max').read().strip())
95 except Exception as e:
96 print(e)
97 # Reasonable default in case something fails
98 return 5
100 MAX_PID_WIDTH = get_max_pid_width()
103 # UI Exceptions
107 class CancelInput(Exception):
108 pass
111 class InvalidInt(Exception):
112 pass
115 class InvalidPid(Exception):
116 pass
119 class InvalidTid(Exception):
120 pass
123 class InvalidIoprioData(Exception):
124 pass
127 # The UI
131 class IOTopUI(object):
132 # key, reverse
133 sorting_keys = [
134 (lambda p, s: p.pid, False),
135 (lambda p, s: p.ioprio_sort_key(), False),
136 (lambda p, s: p.get_user(), False),
137 (lambda p, s: s.read_bytes, True),
138 (lambda p, s: s.write_bytes - s.cancelled_write_bytes, True),
139 (lambda p, s: s.swapin_delay_total, True),
140 # The default sorting (by I/O % time) should show processes doing
141 # only writes, without waiting on them
142 (lambda p, s: s.blkio_delay_total or
143 int(not(not(s.read_bytes or s.write_bytes))), True),
144 (lambda p, s: p.get_cmdline(), False),
147 def __init__(self, win, process_list, options):
148 self.process_list = process_list
149 self.options = options
150 self.sorting_key = 6
151 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
152 if not self.options.batch:
153 self.win = win
154 self.resize()
155 try:
156 curses.use_default_colors()
157 curses.start_color()
158 curses.curs_set(0)
159 except curses.error:
160 # This call can fail with misconfigured terminals, for example
161 # TERM=xterm-color. This is harmless
162 pass
164 def resize(self):
165 self.height, self.width = self.win.getmaxyx()
167 def run(self):
168 iterations = 0
169 poll = select.poll()
170 if not self.options.batch:
171 poll.register(sys.stdin.fileno(), select.POLLIN | select.POLLPRI)
172 while self.options.iterations is None or \
173 iterations < self.options.iterations:
174 total, actual = self.process_list.refresh_processes()
175 self.refresh_display(iterations == 0, total, actual,
176 self.process_list.duration)
177 if self.options.iterations is not None:
178 iterations += 1
179 if iterations >= self.options.iterations:
180 break
181 elif iterations == 0:
182 iterations = 1
184 try:
185 events = poll.poll(self.options.delay_seconds * 1000.0)
186 except select.error as e:
187 if e.args and e.args[0] == errno.EINTR:
188 events = []
189 else:
190 raise
191 for (fd, event) in events:
192 if event & (select.POLLERR | select.POLLHUP):
193 sys.exit(1)
194 if not self.options.batch:
195 self.resize()
196 if events:
197 key = self.win.getch()
198 self.handle_key(key)
200 def reverse_sorting(self):
201 self.sorting_reverse = not self.sorting_reverse
203 def adjust_sorting_key(self, delta):
204 orig_sorting_key = self.sorting_key
205 self.sorting_key += delta
206 self.sorting_key = max(0, self.sorting_key)
207 self.sorting_key = min(len(IOTopUI.sorting_keys) - 1, self.sorting_key)
208 if orig_sorting_key != self.sorting_key:
209 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
211 # I wonder if switching to urwid for the display would be better here
213 def prompt_str(self, prompt, default=None, empty_is_cancel=True):
214 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
215 self.win.addstr(1, 0, prompt, curses.A_BOLD)
216 self.win.refresh()
217 curses.echo()
218 curses.curs_set(1)
219 inp = self.win.getstr(1, len(prompt))
220 curses.curs_set(0)
221 curses.noecho()
222 if inp not in (None, ''):
223 return inp
224 if empty_is_cancel:
225 raise CancelInput()
226 return default
228 def prompt_int(self, prompt, default=None, empty_is_cancel=True):
229 inp = self.prompt_str(prompt, default, empty_is_cancel)
230 try:
231 return int(inp)
232 except ValueError:
233 raise InvalidInt()
235 def prompt_pid(self):
236 try:
237 return self.prompt_int('PID to ionice: ')
238 except InvalidInt:
239 raise InvalidPid()
240 except CancelInput:
241 raise
243 def prompt_tid(self):
244 try:
245 return self.prompt_int('TID to ionice: ')
246 except InvalidInt:
247 raise InvalidTid()
248 except CancelInput:
249 raise
251 def prompt_data(self, ioprio_data):
252 try:
253 if ioprio_data is not None:
254 inp = self.prompt_int('I/O priority data (0-7, currently %s): '
255 % ioprio_data, ioprio_data, False)
256 else:
257 inp = self.prompt_int('I/O priority data (0-7): ', None, False)
258 except InvalidInt:
259 raise InvalidIoprioData()
260 if inp < 0 or inp > 7:
261 raise InvalidIoprioData()
262 return inp
264 def prompt_set(self, prompt, display_list, ret_list, selected):
265 try:
266 selected = ret_list.index(selected)
267 except ValueError:
268 selected = -1
269 set_len = len(display_list) - 1
270 while True:
271 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
272 self.win.insstr(1, 0, prompt, curses.A_BOLD)
273 offset = len(prompt)
274 for i, item in enumerate(display_list):
275 display = ' %s ' % item
276 if i is selected:
277 attr = curses.A_REVERSE
278 else:
279 attr = curses.A_NORMAL
280 self.win.insstr(1, offset, display, attr)
281 offset += len(display)
282 while True:
283 key = self.win.getch()
284 if key in (curses.KEY_LEFT, ord('l')) and selected > 0:
285 selected -= 1
286 break
287 elif (key in (curses.KEY_RIGHT, ord('r')) and
288 selected < set_len):
289 selected += 1
290 break
291 elif key in (curses.KEY_ENTER, ord('\n'), ord('\r')):
292 return ret_list[selected]
293 elif key in (27, curses.KEY_CANCEL, curses.KEY_CLOSE,
294 curses.KEY_EXIT, ord('q'), ord('Q')):
295 raise CancelInput()
297 def prompt_class(self, ioprio_class=None):
298 prompt = 'I/O priority class: '
299 classes_prompt = ['Real-time', 'Best-effort', 'Idle']
300 classes_ret = ['rt', 'be', 'idle']
301 if ioprio_class is None:
302 ioprio_class = 2
303 inp = self.prompt_set(prompt, classes_prompt,
304 classes_ret, ioprio_class)
305 return inp
307 def prompt_error(self, error='Error!'):
308 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
309 self.win.insstr(1, 0, ' %s ' % error, curses.A_REVERSE)
310 self.win.refresh()
311 time.sleep(1)
313 def prompt_clear(self):
314 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
315 self.win.refresh()
317 def handle_key(self, key):
318 def toggle_accumulated():
319 self.options.accumulated ^= True
321 def toggle_only_io():
322 self.options.only ^= True
324 def toggle_processes():
325 self.options.processes ^= True
326 self.process_list.clear()
327 self.process_list.refresh_processes()
329 def ionice():
330 try:
331 if self.options.processes:
332 pid = self.prompt_pid()
333 exec_unit = self.process_list.get_process(pid)
334 else:
335 tid = self.prompt_tid()
336 exec_unit = ThreadInfo(tid,
337 self.process_list.taskstats_connection)
338 ioprio_value = exec_unit.get_ioprio()
339 (ioprio_class, ioprio_data) = \
340 ioprio.to_class_and_data(ioprio_value)
341 ioprio_class = self.prompt_class(ioprio_class)
342 if ioprio_class == 'idle':
343 ioprio_data = 0
344 else:
345 ioprio_data = self.prompt_data(ioprio_data)
346 exec_unit.set_ioprio(ioprio_class, ioprio_data)
347 self.process_list.clear()
348 self.process_list.refresh_processes()
349 except IoprioSetError as e:
350 self.prompt_error('Error setting I/O priority: %s' % e.err)
351 except InvalidPid:
352 self.prompt_error('Invalid process id!')
353 except InvalidTid:
354 self.prompt_error('Invalid thread id!')
355 except InvalidIoprioData:
356 self.prompt_error('Invalid I/O priority data!')
357 except InvalidInt:
358 self.prompt_error('Invalid integer!')
359 except CancelInput:
360 self.prompt_clear()
361 else:
362 self.prompt_clear()
364 key_bindings = {
365 ord('q'):
366 lambda: sys.exit(0),
367 ord('Q'):
368 lambda: sys.exit(0),
369 ord('r'):
370 lambda: self.reverse_sorting(),
371 ord('R'):
372 lambda: self.reverse_sorting(),
373 ord('a'):
374 toggle_accumulated,
375 ord('A'):
376 toggle_accumulated,
377 ord('o'):
378 toggle_only_io,
379 ord('O'):
380 toggle_only_io,
381 ord('p'):
382 toggle_processes,
383 ord('P'):
384 toggle_processes,
385 ord('i'):
386 ionice,
387 ord('I'):
388 ionice,
389 curses.KEY_LEFT:
390 lambda: self.adjust_sorting_key(-1),
391 curses.KEY_RIGHT:
392 lambda: self.adjust_sorting_key(1),
393 curses.KEY_HOME:
394 lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
395 curses.KEY_END:
396 lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
399 action = key_bindings.get(key, lambda: None)
400 action()
402 def get_data(self):
403 def format(p):
404 stats = format_stats(self.options, p, self.process_list.duration)
405 io_delay, swapin_delay, read_bytes, write_bytes = stats
406 if Stats.has_blkio_delay_total:
407 delay_stats = '%7s %7s ' % (swapin_delay, io_delay)
408 else:
409 delay_stats = ' ?unavailable? '
410 pid_format = '%%%dd' % MAX_PID_WIDTH
411 line = (pid_format + ' %4s %-8s %11s %11s %s') % (
412 p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
413 write_bytes, delay_stats)
414 cmdline = p.get_cmdline()
415 if not self.options.batch:
416 remaining_length = self.width - len(line)
417 if 2 < remaining_length < len(cmdline):
418 len1 = (remaining_length - 1) // 2
419 offset2 = -(remaining_length - len1 - 1)
420 cmdline = cmdline[:len1] + '~' + cmdline[offset2:]
421 line += cmdline
422 if not self.options.batch:
423 line = line[:self.width]
424 return line
426 def should_format(p):
427 return not self.options.only or \
428 p.did_some_io(self.options.accumulated)
430 processes = list(filter(should_format,
431 self.process_list.processes.values()))
432 key = IOTopUI.sorting_keys[self.sorting_key][0]
433 if self.options.accumulated:
434 stats_lambda = lambda p: p.stats_accum
435 else:
436 stats_lambda = lambda p: p.stats_delta
437 processes.sort(key=lambda p: key(p, stats_lambda(p)),
438 reverse=self.sorting_reverse)
439 if not self.options.batch:
440 del processes[self.height - 2:]
441 return list(map(format, processes))
443 def refresh_display(self, first_time, total, actual, duration):
444 summary = [
445 'Total DISK READ : %s | Total DISK WRITE : %s' % (
446 format_bandwidth(self.options, total[0], duration).rjust(14),
447 format_bandwidth(self.options, total[1], duration).rjust(14)),
448 'Actual DISK READ: %s | Actual DISK WRITE: %s' % (
449 format_bandwidth(self.options, actual[0], duration).rjust(14),
450 format_bandwidth(self.options, actual[1], duration).rjust(14))
453 pid = max(0, (MAX_PID_WIDTH - 3)) * ' '
454 if self.options.processes:
455 pid += 'PID'
456 else:
457 pid += 'TID'
458 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
459 ' SWAPIN', ' IO', ' COMMAND']
460 lines = self.get_data()
461 if self.options.time:
462 titles = [' TIME'] + titles
463 current_time = time.strftime('%H:%M:%S ')
464 lines = [current_time + l for l in lines]
465 summary = [current_time + s for s in summary]
466 if self.options.batch:
467 if self.options.quiet <= 2:
468 for s in summary:
469 print(s)
470 if self.options.quiet <= int(first_time):
471 print(''.join(titles))
472 for l in lines:
473 print(l)
474 sys.stdout.flush()
475 else:
476 self.win.erase()
477 for i, s in enumerate(summary):
478 self.win.addstr(i, 0, s[:self.width])
479 self.win.hline(len(summary), 0, ord(' ') | curses.A_REVERSE,
480 self.width)
481 remaining_cols = self.width
482 for i in range(len(titles)):
483 attr = curses.A_REVERSE
484 title = titles[i]
485 if i == self.sorting_key:
486 title = title[1:]
487 if i == self.sorting_key:
488 attr |= curses.A_BOLD
489 title += self.sorting_reverse and '>' or '<'
490 title = title[:remaining_cols]
491 remaining_cols -= len(title)
492 self.win.addstr(title, attr)
493 if Stats.has_blkio_delay_total:
494 status_msg = None
495 else:
496 status_msg = ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
497 'cannot determine SWAPIN and IO %')
498 num_lines = min(len(lines),
499 self.height - 2 - int(bool(status_msg)))
500 for i in range(num_lines):
501 try:
502 def print_line(line):
503 self.win.addstr(i + len(summary) + 1, 0, line)
504 try:
505 print_line(lines[i])
506 except UnicodeEncodeError:
507 # Python2: 'ascii' codec can't encode character ...
508 # http://bugs.debian.org/708252
509 print_line(lines[i].encode('utf-8'))
510 except curses.error:
511 pass
512 if status_msg:
513 self.win.insstr(self.height - len(summary), 0, status_msg,
514 curses.A_BOLD)
515 self.win.refresh()
518 def run_iotop_window(win, options):
519 if options.batch:
520 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
521 else:
522 def clean_exit(*args, **kwargs):
523 sys.exit(0)
524 signal.signal(signal.SIGINT, clean_exit)
525 signal.signal(signal.SIGTERM, clean_exit)
526 taskstats_connection = TaskStatsNetlink(options)
527 process_list = ProcessList(taskstats_connection, options)
528 ui = IOTopUI(win, process_list, options)
529 ui.run()
532 def run_iotop(options):
533 try:
534 if options.batch:
535 return run_iotop_window(None, options)
536 else:
537 return curses.wrapper(run_iotop_window, options)
538 except OSError as e:
539 if e.errno == errno.EPERM:
540 print(e, file=sys.stderr)
541 print('''
542 The Linux kernel interfaces that iotop relies on now require root priviliges
543 or the NET_ADMIN capability. This change occured because a security issue
544 (CVE-2011-2494) was found that allows leakage of sensitive data across user
545 boundaries. If you require the ability to run iotop as a non-root user, please
546 configure sudo to allow you to run iotop as root.
548 Please do not file bugs on iotop about this.''', file=sys.stderr)
549 sys.exit(1)
550 else:
551 raise
554 # Profiling
558 def _profile(continuation):
559 prof_file = 'iotop.prof'
560 try:
561 import cProfile
562 import pstats
563 print('Profiling using cProfile')
564 cProfile.runctx('continuation()', globals(), locals(), prof_file)
565 stats = pstats.Stats(prof_file)
566 except ImportError:
567 import hotshot
568 import hotshot.stats
569 prof = hotshot.Profile(prof_file, lineevents=1)
570 print('Profiling using hotshot')
571 prof.runcall(continuation)
572 prof.close()
573 stats = hotshot.stats.load(prof_file)
574 stats.strip_dirs()
575 stats.sort_stats('time', 'calls')
576 stats.print_stats(50)
577 stats.print_callees(50)
578 os.remove(prof_file)
581 # Main program
584 USAGE = '''%s [OPTIONS]
586 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
587 period. SWAPIN and IO are the percentages of time the thread spent respectively
588 while swapping in and waiting on I/O more generally. PRIO is the I/O priority
589 at which the thread is running (set using the ionice command).
591 Controls: left and right arrows to change the sorting column, r to invert the
592 sorting order, o to toggle the --only option, p to toggle the --processes
593 option, a to toggle the --accumulated option, i to change I/O priority, q to
594 quit, any other key to force a refresh.''' % sys.argv[0]
597 def main():
598 try:
599 locale.setlocale(locale.LC_ALL, '')
600 except locale.Error:
601 print('unable to set locale, falling back to the default locale')
602 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
603 parser.add_option('-o', '--only', action='store_true',
604 dest='only', default=False,
605 help='only show processes or threads actually doing I/O')
606 parser.add_option('-b', '--batch', action='store_true', dest='batch',
607 help='non-interactive mode')
608 parser.add_option('-n', '--iter', type='int', dest='iterations',
609 metavar='NUM',
610 help='number of iterations before ending [infinite]')
611 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
612 help='delay between iterations [1 second]',
613 metavar='SEC', default=1)
614 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
615 help='processes/threads to monitor [all]', metavar='PID')
616 parser.add_option('-u', '--user', type='str', dest='users',
617 action='append', help='users to monitor [all]',
618 metavar='USER')
619 parser.add_option('-P', '--processes', action='store_true',
620 dest='processes', default=False,
621 help='only show processes, not all threads')
622 parser.add_option('-a', '--accumulated', action='store_true',
623 dest='accumulated', default=False,
624 help='show accumulated I/O instead of bandwidth')
625 parser.add_option('-k', '--kilobytes', action='store_true',
626 dest='kilobytes', default=False,
627 help='use kilobytes instead of a human friendly unit')
628 parser.add_option('-t', '--time', action='store_true', dest='time',
629 help='add a timestamp on each line (implies --batch)')
630 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
631 help='suppress some lines of header (implies --batch)')
632 parser.add_option('--profile', action='store_true', dest='profile',
633 default=False, help=optparse.SUPPRESS_HELP)
635 options, args = parser.parse_args()
636 if args:
637 parser.error('Unexpected arguments: ' + ' '.join(args))
638 find_uids(options)
639 options.pids = options.pids or []
640 options.batch = options.batch or options.time or options.quiet
642 main_loop = lambda: run_iotop(options)
644 if options.profile:
645 def safe_main_loop():
646 try:
647 main_loop()
648 except:
649 pass
650 _profile(safe_main_loop)
651 else:
652 main_loop()