Port to Python 3
[iotop.git] / iotop / ui.py
blob187ba189d0b82bfe4c36e68c1bd0fae2c23b90f1
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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 import iotop.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 = self.process_list.refresh_processes()
154 total_read, total_write = total
155 self.refresh_display(iterations == 0, total_read, total_write,
156 self.process_list.duration)
157 if self.options.iterations is not None:
158 iterations += 1
159 if iterations >= self.options.iterations:
160 break
161 elif iterations == 0:
162 iterations = 1
164 try:
165 events = poll.poll(self.options.delay_seconds * 1000.0)
166 except select.error as e:
167 if e.args and e.args[0] == errno.EINTR:
168 events = 0
169 else:
170 raise
171 if not self.options.batch:
172 self.resize()
173 if events:
174 key = self.win.getch()
175 self.handle_key(key)
177 def reverse_sorting(self):
178 self.sorting_reverse = not self.sorting_reverse
180 def adjust_sorting_key(self, delta):
181 orig_sorting_key = self.sorting_key
182 self.sorting_key += delta
183 self.sorting_key = max(0, self.sorting_key)
184 self.sorting_key = min(len(IOTopUI.sorting_keys) - 1, self.sorting_key)
185 if orig_sorting_key != self.sorting_key:
186 self.sorting_reverse = IOTopUI.sorting_keys[self.sorting_key][1]
188 # I wonder if switching to urwid for the display would be better here
190 def prompt_str(self, prompt, default=None, empty_is_cancel=True):
191 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
192 self.win.addstr(1, 0, prompt, curses.A_BOLD)
193 self.win.refresh()
194 curses.echo()
195 curses.curs_set(1)
196 inp = self.win.getstr(1, len(prompt))
197 curses.curs_set(0)
198 curses.noecho()
199 if inp not in (None, ''):
200 return inp
201 if empty_is_cancel:
202 raise CancelInput()
203 return default
205 def prompt_int(self, prompt, default = None, empty_is_cancel = True):
206 inp = self.prompt_str(prompt, default, empty_is_cancel)
207 try:
208 return int(inp)
209 except ValueError:
210 raise InvalidInt()
212 def prompt_pid(self):
213 try:
214 return self.prompt_int('PID to ionice: ')
215 except InvalidInt:
216 raise InvalidPid()
217 except CancelInput:
218 raise
220 def prompt_tid(self):
221 try:
222 return self.prompt_int('TID to ionice: ')
223 except InvalidInt:
224 raise InvalidTid()
225 except CancelInput:
226 raise
228 def prompt_data(self, ioprio_data):
229 try:
230 if ioprio_data is not None:
231 inp = self.prompt_int('I/O priority data (0-7, currently %s): '
232 % ioprio_data, ioprio_data, False)
233 else:
234 inp = self.prompt_int('I/O priority data (0-7): ', None, False)
235 except InvalidInt:
236 raise InvalidIoprioData()
237 if inp < 0 or inp > 7:
238 raise InvalidIoprioData()
239 return inp
241 def prompt_set(self, prompt, display_list, ret_list, selected):
242 try:
243 selected = ret_list.index(selected)
244 except ValueError:
245 selected = -1
246 set_len = len(display_list) - 1
247 while True:
248 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
249 self.win.insstr(1, 0, prompt, curses.A_BOLD)
250 offset = len(prompt)
251 for i, item in enumerate(display_list):
252 display = ' %s ' % item
253 if i is selected:
254 attr = curses.A_REVERSE
255 else:
256 attr = curses.A_NORMAL
257 self.win.insstr(1, offset, display, attr)
258 offset += len(display)
259 while True:
260 key = self.win.getch()
261 if key in (curses.KEY_LEFT, ord('l')) and selected > 0:
262 selected -= 1
263 break
264 elif key in (curses.KEY_RIGHT, ord('r')) and selected < set_len:
265 selected += 1
266 break
267 elif key in (curses.KEY_ENTER, ord('\n'), ord('\r')):
268 return ret_list[selected]
269 elif key in (27, curses.KEY_CANCEL, curses.KEY_CLOSE,
270 curses.KEY_EXIT, ord('q'), ord('Q')):
271 raise CancelInput()
273 def prompt_class(self, ioprio_class=None):
274 prompt = 'I/O priority class: '
275 classes_prompt = ['Real-time', 'Best-effort', 'Idle']
276 classes_ret = ['rt', 'be', 'idle']
277 if ioprio_class is None:
278 ioprio_class = 2
279 inp = self.prompt_set(prompt, classes_prompt, classes_ret, ioprio_class)
280 return inp
282 def prompt_error(self, error = 'Error!'):
283 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
284 self.win.insstr(1, 0, ' %s ' % error, curses.A_REVERSE)
285 self.win.refresh()
286 time.sleep(1)
288 def prompt_clear(self):
289 self.win.hline(1, 0, ord(' ') | curses.A_NORMAL, self.width)
290 self.win.refresh()
292 def handle_key(self, key):
293 def toggle_accumulated():
294 self.options.accumulated ^= True
295 def toggle_only_io():
296 self.options.only ^= True
297 def toggle_processes():
298 self.options.processes ^= True
299 self.process_list.clear()
300 self.process_list.refresh_processes()
301 def ionice():
302 try:
303 if self.options.processes:
304 pid = self.prompt_pid()
305 exec_unit = self.process_list.get_process(pid)
306 else:
307 tid = self.prompt_tid()
308 exec_unit = ThreadInfo(tid,
309 self.process_list.taskstats_connection)
310 ioprio_value = exec_unit.get_ioprio()
311 (ioprio_class, ioprio_data) = \
312 ioprio.to_class_and_data(ioprio_value)
313 ioprio_class = self.prompt_class(ioprio_class)
314 if ioprio_class == 'idle':
315 ioprio_data = 0
316 else:
317 ioprio_data = self.prompt_data(ioprio_data)
318 exec_unit.set_ioprio(ioprio_class, ioprio_data)
319 self.process_list.clear()
320 self.process_list.refresh_processes()
321 except IoprioSetError as e:
322 self.prompt_error('Error setting I/O priority: %s' % e.err)
323 except InvalidPid:
324 self.prompt_error('Invalid process id!')
325 except InvalidTid:
326 self.prompt_error('Invalid thread id!')
327 except InvalidIoprioData:
328 self.prompt_error('Invalid I/O priority data!')
329 except InvalidInt:
330 self.prompt_error('Invalid integer!')
331 except CancelInput:
332 self.prompt_clear()
333 else:
334 self.prompt_clear()
336 key_bindings = {
337 ord('q'):
338 lambda: sys.exit(0),
339 ord('Q'):
340 lambda: sys.exit(0),
341 ord('r'):
342 lambda: self.reverse_sorting(),
343 ord('R'):
344 lambda: self.reverse_sorting(),
345 ord('a'):
346 toggle_accumulated,
347 ord('A'):
348 toggle_accumulated,
349 ord('o'):
350 toggle_only_io,
351 ord('O'):
352 toggle_only_io,
353 ord('p'):
354 toggle_processes,
355 ord('P'):
356 toggle_processes,
357 ord('i'):
358 ionice,
359 ord('I'):
360 ionice,
361 curses.KEY_LEFT:
362 lambda: self.adjust_sorting_key(-1),
363 curses.KEY_RIGHT:
364 lambda: self.adjust_sorting_key(1),
365 curses.KEY_HOME:
366 lambda: self.adjust_sorting_key(-len(IOTopUI.sorting_keys)),
367 curses.KEY_END:
368 lambda: self.adjust_sorting_key(len(IOTopUI.sorting_keys))
371 action = key_bindings.get(key, lambda: None)
372 action()
374 def get_data(self):
375 def format(p):
376 stats = format_stats(self.options, p, self.process_list.duration)
377 io_delay, swapin_delay, read_bytes, write_bytes = stats
378 if Stats.has_blkio_delay_total:
379 delay_stats = '%7s %7s ' % (swapin_delay, io_delay)
380 else:
381 delay_stats = ' ?unavailable? '
382 pid_format = '%%%dd' % MAX_PID_WIDTH
383 line = (pid_format + ' %4s %-8s %11s %11s %s') % (
384 p.pid, p.get_ioprio(), p.get_user()[:8], read_bytes,
385 write_bytes, delay_stats)
386 cmdline = p.get_cmdline()
387 if not self.options.batch:
388 remaining_length = self.width - len(line)
389 if 2 < remaining_length < len(cmdline):
390 len1 = (remaining_length - 1) // 2
391 offset2 = -(remaining_length - len1 - 1)
392 cmdline = cmdline[:len1] + '~' + cmdline[offset2:]
393 line += cmdline
394 if not self.options.batch:
395 line = line[:self.width]
396 return line
398 def should_format(p):
399 return not self.options.only or \
400 p.did_some_io(self.options.accumulated)
402 processes = list(filter(should_format, self.process_list.processes.values()))
403 key = IOTopUI.sorting_keys[self.sorting_key][0]
404 if self.options.accumulated:
405 stats_lambda = lambda p: p.stats_accum
406 else:
407 stats_lambda = lambda p: p.stats_delta
408 processes.sort(key=lambda p: key(p, stats_lambda(p)),
409 reverse=self.sorting_reverse)
410 if not self.options.batch:
411 del processes[self.height - 2:]
412 return list(map(format, processes))
414 def refresh_display(self, first_time, total_read, total_write, duration):
415 summary = 'Total DISK READ: %s | Total DISK WRITE: %s' % (
416 format_bandwidth(self.options, total_read, duration).rjust(14),
417 format_bandwidth(self.options, total_write, duration).rjust(14))
419 pid = max(0, (MAX_PID_WIDTH - 3)) * ' '
420 if self.options.processes:
421 pid += 'PID'
422 else:
423 pid += 'TID'
424 titles = [pid, ' PRIO', ' USER', ' DISK READ', ' DISK WRITE',
425 ' SWAPIN', ' IO', ' COMMAND']
426 lines = self.get_data()
427 if self.options.time:
428 titles = [' TIME'] + titles
429 current_time = time.strftime('%H:%M:%S ')
430 lines = [current_time + l for l in lines]
431 summary = current_time + summary
432 if self.options.batch:
433 if self.options.quiet <= 2:
434 print(summary)
435 if self.options.quiet <= int(first_time):
436 print(''.join(titles))
437 for l in lines:
438 print(l.encode('utf-8'))
439 sys.stdout.flush()
440 else:
441 self.win.erase()
442 self.win.addstr(summary[:self.width])
443 self.win.hline(1, 0, ord(' ') | curses.A_REVERSE, self.width)
444 remaining_cols = self.width
445 for i in range(len(titles)):
446 attr = curses.A_REVERSE
447 title = titles[i]
448 if i == self.sorting_key:
449 title = title[1:]
450 if i == self.sorting_key:
451 attr |= curses.A_BOLD
452 title += self.sorting_reverse and '>' or '<'
453 title = title[:remaining_cols]
454 remaining_cols -= len(title)
455 self.win.addstr(title, attr)
456 if Stats.has_blkio_delay_total:
457 status_msg = None
458 else:
459 status_msg = ('CONFIG_TASK_DELAY_ACCT not enabled in kernel, '
460 'cannot determine SWAPIN and IO %')
461 num_lines = min(len(lines), self.height - 2 - int(bool(status_msg)))
462 for i in range(num_lines):
463 try:
464 self.win.addstr(i + 2, 0, lines[i])
465 except curses.error:
466 pass
467 if status_msg:
468 self.win.insstr(self.height - 1, 0, status_msg, curses.A_BOLD)
469 self.win.refresh()
471 def run_iotop_window(win, options):
472 if options.batch:
473 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
474 taskstats_connection = TaskStatsNetlink(options)
475 process_list = ProcessList(taskstats_connection, options)
476 ui = IOTopUI(win, process_list, options)
477 ui.run()
479 def run_iotop(options):
480 try:
481 if options.batch:
482 return run_iotop_window(None, options)
483 else:
484 return curses.wrapper(run_iotop_window, options)
485 except OSError as e:
486 if e.errno == errno.EPERM:
487 print(e, file=sys.stderr)
488 print('''
489 The Linux kernel interfaces that iotop relies on now require root priviliges
490 or the NET_ADMIN capability. This change occured because a security issue
491 (CVE-2011-2494) was found that allows leakage of sensitive data across user
492 boundaries. If you require the ability to run iotop as a non-root user, please
493 configure sudo to allow you to run iotop as root.
495 Please do not file bugs on iotop about this.''', file=sys.stderr)
496 sys.exit(1)
497 else:
498 raise
501 # Profiling
504 def _profile(continuation):
505 prof_file = 'iotop.prof'
506 try:
507 import cProfile
508 import pstats
509 print('Profiling using cProfile')
510 cProfile.runctx('continuation()', globals(), locals(), prof_file)
511 stats = pstats.Stats(prof_file)
512 except ImportError:
513 import hotshot
514 import hotshot.stats
515 prof = hotshot.Profile(prof_file, lineevents=1)
516 print('Profiling using hotshot')
517 prof.runcall(continuation)
518 prof.close()
519 stats = hotshot.stats.load(prof_file)
520 stats.strip_dirs()
521 stats.sort_stats('time', 'calls')
522 stats.print_stats(50)
523 stats.print_callees(50)
524 os.remove(prof_file)
527 # Main program
530 USAGE = '''%s [OPTIONS]
532 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
533 period. SWAPIN and IO are the percentages of time the thread spent respectively
534 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
535 which the thread is running (set using the ionice command).
537 Controls: left and right arrows to change the sorting column, r to invert the
538 sorting order, o to toggle the --only option, p to toggle the --processes
539 option, a to toggle the --accumulated option, q to quit, any other key to force
540 a refresh.''' % sys.argv[0]
542 def main():
543 try:
544 locale.setlocale(locale.LC_ALL, '')
545 except locale.Error:
546 print('unable to set locale, falling back to the default locale')
547 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
548 parser.add_option('-o', '--only', action='store_true',
549 dest='only', default=False,
550 help='only show processes or threads actually doing I/O')
551 parser.add_option('-b', '--batch', action='store_true', dest='batch',
552 help='non-interactive mode')
553 parser.add_option('-n', '--iter', type='int', dest='iterations',
554 metavar='NUM',
555 help='number of iterations before ending [infinite]')
556 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
557 help='delay between iterations [1 second]',
558 metavar='SEC', default=1)
559 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
560 help='processes/threads to monitor [all]', metavar='PID')
561 parser.add_option('-u', '--user', type='str', dest='users', action='append',
562 help='users to monitor [all]', metavar='USER')
563 parser.add_option('-P', '--processes', action='store_true',
564 dest='processes', default=False,
565 help='only show processes, not all threads')
566 parser.add_option('-a', '--accumulated', action='store_true',
567 dest='accumulated', default=False,
568 help='show accumulated I/O instead of bandwidth')
569 parser.add_option('-k', '--kilobytes', action='store_true',
570 dest='kilobytes', default=False,
571 help='use kilobytes instead of a human friendly unit')
572 parser.add_option('-t', '--time', action='store_true', dest='time',
573 help='add a timestamp on each line (implies --batch)')
574 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
575 help='suppress some lines of header (implies --batch)')
576 parser.add_option('--profile', action='store_true', dest='profile',
577 default=False, help=optparse.SUPPRESS_HELP)
579 options, args = parser.parse_args()
580 if args:
581 parser.error('Unexpected arguments: ' + ' '.join(args))
582 find_uids(options)
583 options.pids = options.pids or []
584 options.batch = options.batch or options.time or options.quiet
586 main_loop = lambda: run_iotop(options)
588 if options.profile:
589 def safe_main_loop():
590 try:
591 main_loop()
592 except:
593 pass
594 _profile(safe_main_loop)
595 else:
596 main_loop()