Python3 can print UTF-8 to curses, python2 can't so let's handle both.
[iotop.git] / iotop / ui.py
blob18ef227e1df15cb845b20a20cd1594a93ceae7b9
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 signal.signal(signal.SIGINT, lambda *args, **kwargs: sys.exit(0))
495 taskstats_connection = TaskStatsNetlink(options)
496 process_list = ProcessList(taskstats_connection, options)
497 ui = IOTopUI(win, process_list, options)
498 ui.run()
500 def run_iotop(options):
501 try:
502 if options.batch:
503 return run_iotop_window(None, options)
504 else:
505 return curses.wrapper(run_iotop_window, options)
506 except OSError as e:
507 if e.errno == errno.EPERM:
508 print(e, file=sys.stderr)
509 print('''
510 The Linux kernel interfaces that iotop relies on now require root priviliges
511 or the NET_ADMIN capability. This change occured because a security issue
512 (CVE-2011-2494) was found that allows leakage of sensitive data across user
513 boundaries. If you require the ability to run iotop as a non-root user, please
514 configure sudo to allow you to run iotop as root.
516 Please do not file bugs on iotop about this.''', file=sys.stderr)
517 sys.exit(1)
518 else:
519 raise
522 # Profiling
525 def _profile(continuation):
526 prof_file = 'iotop.prof'
527 try:
528 import cProfile
529 import pstats
530 print('Profiling using cProfile')
531 cProfile.runctx('continuation()', globals(), locals(), prof_file)
532 stats = pstats.Stats(prof_file)
533 except ImportError:
534 import hotshot
535 import hotshot.stats
536 prof = hotshot.Profile(prof_file, lineevents=1)
537 print('Profiling using hotshot')
538 prof.runcall(continuation)
539 prof.close()
540 stats = hotshot.stats.load(prof_file)
541 stats.strip_dirs()
542 stats.sort_stats('time', 'calls')
543 stats.print_stats(50)
544 stats.print_callees(50)
545 os.remove(prof_file)
548 # Main program
551 USAGE = '''%s [OPTIONS]
553 DISK READ and DISK WRITE are the block I/O bandwidth used during the sampling
554 period. SWAPIN and IO are the percentages of time the thread spent respectively
555 while swapping in and waiting on I/O more generally. PRIO is the I/O priority at
556 which the thread is running (set using the ionice command).
558 Controls: left and right arrows to change the sorting column, r to invert the
559 sorting order, o to toggle the --only option, p to toggle the --processes
560 option, a to toggle the --accumulated option, i to change I/O priority, q to
561 quit, any other key to force a refresh.''' % sys.argv[0]
563 def main():
564 try:
565 locale.setlocale(locale.LC_ALL, '')
566 except locale.Error:
567 print('unable to set locale, falling back to the default locale')
568 parser = optparse.OptionParser(usage=USAGE, version='iotop ' + VERSION)
569 parser.add_option('-o', '--only', action='store_true',
570 dest='only', default=False,
571 help='only show processes or threads actually doing I/O')
572 parser.add_option('-b', '--batch', action='store_true', dest='batch',
573 help='non-interactive mode')
574 parser.add_option('-n', '--iter', type='int', dest='iterations',
575 metavar='NUM',
576 help='number of iterations before ending [infinite]')
577 parser.add_option('-d', '--delay', type='float', dest='delay_seconds',
578 help='delay between iterations [1 second]',
579 metavar='SEC', default=1)
580 parser.add_option('-p', '--pid', type='int', dest='pids', action='append',
581 help='processes/threads to monitor [all]', metavar='PID')
582 parser.add_option('-u', '--user', type='str', dest='users', action='append',
583 help='users to monitor [all]', metavar='USER')
584 parser.add_option('-P', '--processes', action='store_true',
585 dest='processes', default=False,
586 help='only show processes, not all threads')
587 parser.add_option('-a', '--accumulated', action='store_true',
588 dest='accumulated', default=False,
589 help='show accumulated I/O instead of bandwidth')
590 parser.add_option('-k', '--kilobytes', action='store_true',
591 dest='kilobytes', default=False,
592 help='use kilobytes instead of a human friendly unit')
593 parser.add_option('-t', '--time', action='store_true', dest='time',
594 help='add a timestamp on each line (implies --batch)')
595 parser.add_option('-q', '--quiet', action='count', dest='quiet', default=0,
596 help='suppress some lines of header (implies --batch)')
597 parser.add_option('--profile', action='store_true', dest='profile',
598 default=False, help=optparse.SUPPRESS_HELP)
600 options, args = parser.parse_args()
601 if args:
602 parser.error('Unexpected arguments: ' + ' '.join(args))
603 find_uids(options)
604 options.pids = options.pids or []
605 options.batch = options.batch or options.time or options.quiet
607 main_loop = lambda: run_iotop(options)
609 if options.profile:
610 def safe_main_loop():
611 try:
612 main_loop()
613 except:
614 pass
615 _profile(safe_main_loop)
616 else:
617 main_loop()