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