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