Split up huge Top module into smaller modules.
[virt-top.git] / src / top.ml
1 (* 'top'-like tool for libvirt domains.
2    (C) Copyright 2007-2017 Richard W.M. Jones, Red Hat Inc.
3    http://libvirt.org/
4
5    This program is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 2 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14
15    You should have received a copy of the GNU General Public License
16    along with this program; if not, write to the Free Software
17    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 *)
19
20 open Printf
21 open ExtList
22 open Curses
23
24 open Opt_gettext.Gettext
25 open Utils
26 open Types
27 open Collect
28 open Screen
29
30 module C = Libvirt.Connect
31 module D = Libvirt.Domain
32 module N = Libvirt.Network
33
34 let rcfile = ".virt-toprc"
35
36 (* Hooks for CSV support (see [opt_csv.ml]). *)
37 let csv_start : (string -> unit) ref =
38   ref (
39     fun _ -> failwith (s_"virt-top was compiled without support for CSV files")
40   )
41
42 (* Hook for calendar support (see [opt_calendar.ml]). *)
43 let parse_date_time : (string -> float) ref =
44   ref (
45     fun _ ->
46       failwith (s_"virt-top was compiled without support for dates and times")
47   )
48
49 (* Init file. *)
50 type init_file = NoInitFile | DefaultInitFile | InitFile of string
51
52 (* Settings. *)
53 let quit = ref false
54 let delay = ref 3000 (* milliseconds *)
55 let historical_cpu_delay = ref 20 (* secs *)
56 let iterations = ref (-1)
57 let end_time = ref None
58 let batch_mode = ref false
59 let secure_mode = ref false
60 let sort_order = ref Processor
61 let display_mode = ref TaskDisplay
62 let uri = ref None
63 let debug_file = ref ""
64 let csv_enabled = ref false
65 let csv_cpu = ref true
66 let csv_mem = ref true
67 let csv_block = ref true
68 let csv_net = ref true
69 let init_file = ref DefaultInitFile
70 let script_mode = ref false
71 let stream_mode = ref false
72 let block_in_bytes = ref false
73
74 (* Function to read command line arguments and go into curses mode. *)
75 let start_up () =
76   (* Read command line arguments. *)
77   let rec set_delay newdelay =
78     if newdelay <= 0. then
79       failwith (s_"-d: cannot set a negative delay");
80     delay := int_of_float (newdelay *. 1000.)
81   and set_uri = function "" -> uri := None | u -> uri := Some u
82   and set_sort order = sort_order := sort_order_of_cli order
83   and set_pcpu_mode () = display_mode := PCPUDisplay
84   and set_net_mode () = display_mode := NetDisplay
85   and set_block_mode () = display_mode := BlockDisplay
86   and set_csv filename =
87     (!csv_start) filename;
88     csv_enabled := true
89   and no_init_file () = init_file := NoInitFile
90   and set_init_file filename = init_file := InitFile filename
91   and set_end_time time = end_time := Some ((!parse_date_time) time)
92   and display_version () =
93     printf "virt-top %s ocaml-libvirt %s\n"
94       Version.version Libvirt_version.version;
95     exit 0
96   in
97   let argspec = Arg.align [
98     "-1", Arg.Unit set_pcpu_mode,
99       " " ^ s_"Start by displaying pCPUs (default: tasks)";
100     "-2", Arg.Unit set_net_mode,
101       " " ^ s_"Start by displaying network interfaces";
102     "-3", Arg.Unit set_block_mode,
103       " " ^ s_"Start by displaying block devices";
104     "-b", Arg.Set batch_mode,
105       " " ^ s_"Batch mode";
106     "-c", Arg.String set_uri,
107       "uri " ^ s_"Connect to libvirt URI";
108     "--connect", Arg.String set_uri,
109       "uri " ^ s_"Connect to libvirt URI";
110     "--csv", Arg.String set_csv,
111       "file " ^ s_"Log statistics to CSV file";
112     "--no-csv-cpu", Arg.Clear csv_cpu,
113       " " ^ s_"Disable CPU stats in CSV";
114     "--no-csv-mem", Arg.Clear csv_mem,
115       " " ^ s_"Disable memory stats in CSV";
116     "--no-csv-block", Arg.Clear csv_block,
117       " " ^ s_"Disable block device stats in CSV";
118     "--no-csv-net", Arg.Clear csv_net,
119       " " ^ s_"Disable net stats in CSV";
120     "-d", Arg.Float set_delay,
121       "delay " ^ s_"Delay time interval (seconds)";
122     "--debug", Arg.Set_string debug_file,
123       "file " ^ s_"Send debug messages to file";
124     "--end-time", Arg.String set_end_time,
125       "time " ^ s_"Exit at given time";
126     "--hist-cpu", Arg.Set_int historical_cpu_delay,
127       "secs " ^ s_"Historical CPU delay";
128     "--init-file", Arg.String set_init_file,
129       "file " ^ s_"Set name of init file";
130     "--no-init-file", Arg.Unit no_init_file,
131       " " ^ s_"Do not read init file";
132     "-n", Arg.Set_int iterations,
133       "iterations " ^ s_"Number of iterations to run";
134     "-o", Arg.String set_sort,
135       "sort " ^ sprintf (f_"Set sort order (%s)")
136         "cpu|mem|time|id|name|netrx|nettx|blockrdrq|blockwrrq";
137     "-s", Arg.Set secure_mode,
138       " " ^ s_"Secure (\"kiosk\") mode";
139     "--script", Arg.Set script_mode,
140       " " ^ s_"Run from a script (no user interface)";
141     "--stream", Arg.Set stream_mode,
142       " " ^ s_"dump output to stdout (no userinterface)";
143     "--block-in-bytes", Arg.Set block_in_bytes,
144       " " ^ s_"show block device load in bytes rather than reqs";
145     "--version", Arg.Unit display_version,
146       " " ^ s_"Display version number and exit";
147   ] in
148   let anon_fun str =
149     raise (Arg.Bad (sprintf (f_"%s: unknown parameter") str)) in
150   let usage_msg = s_"virt-top : a 'top'-like utility for virtualization
151
152 SUMMARY
153   virt-top [-options]
154
155 OPTIONS" in
156   Arg.parse argspec anon_fun usage_msg;
157
158   (* Read the init file. *)
159   let try_to_read_init_file filename =
160     let config = read_config_file filename in
161     (* Replacement functions that raise better errors when
162      * parsing the init file.
163      *)
164     let int_of_string s =
165       try int_of_string s
166       with Invalid_argument _ ->
167         failwithf (f_"%s: could not parse '%s' in init file: expecting an integer")
168           filename s in
169     let float_of_string s =
170       try float_of_string s
171       with Invalid_argument _ ->
172         failwithf (f_"%s: could not parse '%s' in init file: expecting a number")
173           filename s in
174     let bool_of_string s =
175       try bool_of_string s
176       with Invalid_argument _ ->
177         failwithf (f_"%s: could not parse '%s' in init file: expecting %s")
178           filename s "true|false" in
179     List.iter (
180       function
181       | _, "display", mode -> display_mode := display_of_cli mode
182       | _, "delay", secs -> set_delay (float_of_string secs)
183       | _, "hist-cpu", secs -> historical_cpu_delay := int_of_string secs
184       | _, "iterations", n -> iterations := int_of_string n
185       | _, "sort", order -> set_sort order
186       | _, "connect", uri -> set_uri uri
187       | _, "debug", filename -> debug_file := filename
188       | _, "csv", filename -> set_csv filename
189       | _, "csv-cpu", b -> csv_cpu := bool_of_string b
190       | _, "csv-mem", b -> csv_mem := bool_of_string b
191       | _, "csv-block", b -> csv_block := bool_of_string b
192       | _, "csv-net", b -> csv_net := bool_of_string b
193       | _, "batch", b -> batch_mode := bool_of_string b
194       | _, "secure", b -> secure_mode := bool_of_string b
195       | _, "script", b -> script_mode := bool_of_string b
196       | _, "stream", b -> stream_mode := bool_of_string b
197       | _, "block-in-bytes", b -> block_in_bytes := bool_of_string b
198       | _, "end-time", t -> set_end_time t
199       | _, "overwrite-init-file", "false" -> no_init_file ()
200       | lineno, key, _ ->
201           eprintf (f_"%s:%d: configuration item ``%s'' ignored\n%!")
202             filename lineno key
203     ) config
204   in
205   (match !init_file with
206    | NoInitFile -> ()
207    | DefaultInitFile ->
208        let home = try Sys.getenv "HOME" with Not_found -> "/" in
209        let filename = home // rcfile in
210        try_to_read_init_file filename
211    | InitFile filename ->
212        try_to_read_init_file filename
213   );
214
215   (* Connect to the hypervisor before going into curses mode, since
216    * this is the most likely thing to fail.
217    *)
218   let conn =
219     let name = !uri in
220     try C.connect_readonly ?name ()
221     with
222       Libvirt.Virterror err ->
223         prerr_endline (Libvirt.Virterror.to_string err);
224         (* If non-root and no explicit connection URI, print a warning. *)
225         if Unix.geteuid () <> 0 && name = None then (
226           print_endline (s_"NB: If you want to monitor a local hypervisor, you usually need to be root");
227         );
228         exit 1 in
229
230   (* Get the node_info.  This never changes, right?  So we get it just once. *)
231   let node_info = C.get_node_info conn in
232
233   (* Hostname and libvirt library version also don't change. *)
234   let hostname =
235     try C.get_hostname conn
236     with
237     (* qemu:/// and other URIs didn't support virConnectGetHostname until
238      * libvirt 0.3.3.  Before that they'd throw a virterror. *)
239     | Libvirt.Virterror _
240     | Libvirt.Not_supported "virConnectGetHostname" -> "unknown" in
241
242   let libvirt_version =
243     let v, _ = Libvirt.get_version () in
244     v / 1_000_000, (v / 1_000) mod 1_000, v mod 1_000 in
245
246   (* Open debug file if specified.
247    * NB: Do this just before jumping into curses mode.
248    *)
249   (match !debug_file with
250    | "" -> (* No debug file specified, send stderr to /dev/null unless
251             * we're in script mode.
252             *)
253        if not !script_mode && not !stream_mode then (
254          let fd = Unix.openfile "/dev/null" [Unix.O_WRONLY] 0o644 in
255          Unix.dup2 fd Unix.stderr;
256          Unix.close fd
257        )
258    | filename -> (* Send stderr to the named file. *)
259        let fd =
260          Unix.openfile filename [Unix.O_WRONLY;Unix.O_CREAT;Unix.O_TRUNC]
261            0o644 in
262        Unix.dup2 fd Unix.stderr;
263        Unix.close fd
264   );
265
266   (* Curses voodoo (see ncurses(3)). *)
267   if not !script_mode && not !stream_mode then (
268     ignore (initscr ());
269     ignore (cbreak ());
270     ignore (noecho ());
271     nonl ();
272     let stdscr = stdscr () in
273     ignore (intrflush stdscr false);
274     ignore (keypad stdscr true);
275     ()
276   );
277
278   (* This tuple of static information is called 'setup' in other parts
279    * of this program, and is passed to other functions such as redraw and
280    * main_loop.  See [main.ml].
281    *)
282   (conn,
283    !batch_mode, !script_mode, !csv_enabled, !stream_mode, (* immutable modes *)
284    node_info, hostname, libvirt_version (* info that doesn't change *)
285   )
286
287 (* Sleep in seconds. *)
288 let sleep = Unix.sleep
289
290 (* Sleep in milliseconds. *)
291 let millisleep n =
292   ignore (Unix.select [] [] [] (float n /. 1000.))
293
294 (* The curses getstr/getnstr functions are just weird.
295  * This helper function also enables echo temporarily.
296  *)
297 let get_string maxlen =
298   ignore (echo ());
299   let str = String.create maxlen in
300   let ok = getstr str in (* Safe because binding calls getnstr. *)
301   ignore (noecho ());
302   if not ok then ""
303   else (
304     (* Chop at first '\0'. *)
305     try
306       let i = String.index str '\000' in
307       String.sub str 0 i
308     with
309       Not_found -> str (* it is full maxlen bytes *)
310   )
311
312 (* Main loop. *)
313 let rec main_loop ((_, batch_mode, script_mode, csv_enabled, stream_mode, _, _, _)
314                      as setup) =
315   let csv_flags = !csv_cpu, !csv_mem, !csv_block, !csv_net in
316
317   if csv_enabled then
318     Csv_output.write_csv_header csv_flags !block_in_bytes;
319
320   while not !quit do
321     (* Collect stats. *)
322     let state = collect setup !block_in_bytes in
323     let pcpu_display =
324       if !display_mode = PCPUDisplay then Some (collect_pcpu state)
325       else None in
326     (* Redraw display. *)
327     if not script_mode && not stream_mode then
328       Redraw.redraw !display_mode !sort_order
329                     setup !block_in_bytes !historical_cpu_delay
330                     state pcpu_display;
331
332     (* Update CSV file. *)
333     if csv_enabled then
334       Csv_output.append_csv setup csv_flags state;
335
336     (* Append to stream output file. *)
337     if stream_mode then
338       Stream_output.append_stream setup !block_in_bytes state;
339
340     (* Clear up unused virDomainPtr objects. *)
341     Gc.compact ();
342
343     (* Max iterations? *)
344     if !iterations >= 0 then (
345       decr iterations;
346       if !iterations = 0 then quit := true
347     );
348
349     (* End time?  We might need to adjust the precise delay down if
350      * the delay would be longer than the end time (RHBZ#637964).  Note
351      * 'delay' is in milliseconds.
352      *)
353     let delay =
354       match !end_time with
355       | None ->
356           (* No --end-time option, so use the current delay. *)
357           !delay
358       | Some end_time ->
359           let delay_secs = float !delay /. 1000. in
360           if end_time <= state.rd_time +. delay_secs then (
361             quit := true;
362             let delay = int_of_float (1000. *. (end_time -. state.rd_time)) in
363             if delay >= 0 then delay else 0
364           ) else
365             !delay in
366     (*eprintf "adjusted delay = %d\n%!" delay;*)
367
368     (* Get next key.  This does the sleep. *)
369     if not batch_mode && not script_mode && not stream_mode then
370       get_key_press setup delay
371     else (
372       (* Batch mode, script mode, stream mode.  We didn't call
373        * get_key_press, so we didn't sleep.  Sleep now, unless we are
374        * about to quit.
375        *)
376       if not !quit || !end_time <> None then
377         millisleep delay
378     )
379   done
380
381 and get_key_press setup delay =
382   (* Read the next key, waiting up to 'delay' milliseconds. *)
383   timeout delay;
384   let k = getch () in
385   timeout (-1); (* Reset to blocking mode. *)
386
387   if k >= 0 && k <> 32 (* ' ' *) && k <> 12 (* ^L *) && k <> Key.resize
388   then (
389     if k = Char.code 'q' then quit := true
390     else if k = Char.code 'h' then show_help setup
391     else if k = Char.code 's' || k = Char.code 'd' then change_delay ()
392     else if k = Char.code 'M' then sort_order := Memory
393     else if k = Char.code 'P' then sort_order := Processor
394     else if k = Char.code 'T' then sort_order := Time
395     else if k = Char.code 'N' then sort_order := DomainID
396     else if k = Char.code 'F' then change_sort_order ()
397     else if k = Char.code '0' then set_tasks_display ()
398     else if k = Char.code '1' then toggle_pcpu_display ()
399     else if k = Char.code '2' then toggle_net_display ()
400     else if k = Char.code '3' then toggle_block_display ()
401     else if k = Char.code 'W' then write_init_file ()
402     else if k = Char.code 'B' then toggle_block_in_bytes_mode ()
403     else unknown_command k
404   )
405
406 and change_delay () =
407   print_msg
408     (sprintf (f_"Change delay from %.1f to: ") (float !delay /. 1000.));
409   let str = get_string 16 in
410   (* Try to parse the number. *)
411   let error =
412     try
413       let newdelay = float_of_string str in
414       if newdelay <= 0. then (
415         print_msg (s_"Delay must be > 0"); true
416       ) else (
417         delay := int_of_float (newdelay *. 1000.); false
418       )
419     with
420       Failure "float_of_string" ->
421         print_msg (s_"Not a valid number"); true in
422   refresh ();
423   sleep (if error then 2 else 1)
424
425 and change_sort_order () =
426   clear ();
427   let lines, cols = get_size () in
428
429   mvaddstr top_lineno 0 (s_"Set sort order for main display");
430   mvaddstr summary_lineno 0 (s_"Type key or use up and down cursor keys.");
431
432   attron A.reverse;
433   mvaddstr header_lineno 0 (pad cols "KEY   Sort field");
434   attroff A.reverse;
435
436   let accelerator_key = function
437     | Memory -> "(key: M)"
438     | Processor -> "(key: P)"
439     | Time -> "(key: T)"
440     | DomainID -> "(key: N)"
441     | _ -> (* all others have to be changed from here *) ""
442   in
443
444   let rec key_of_int = function
445     | i when i < 10 -> Char.chr (i + Char.code '0')
446     | i when i < 20 -> Char.chr (i + Char.code 'a')
447     | _ -> assert false
448   and int_of_key = function
449     | k when k >= 0x30 && k <= 0x39 (* '0' - '9' *) -> k - 0x30
450     | k when k >= 0x61 && k <= 0x7a (* 'a' - 'j' *) -> k - 0x61 + 10
451     | k when k >= 0x41 && k <= 0x6a (* 'A' - 'J' *) -> k - 0x41 + 10
452     | _ -> -1
453   in
454
455   (* Display possible sort fields. *)
456   let selected_index = ref 0 in
457   List.iteri (
458     fun i ord ->
459       let selected = !sort_order = ord in
460       if selected then selected_index := i;
461       mvaddstr (domains_lineno+i) 0
462         (sprintf "  %c %s %s %s"
463            (key_of_int i) (if selected then "*" else " ")
464            (printable_sort_order ord)
465            (accelerator_key ord))
466   ) all_sort_fields;
467
468   move message_lineno 0;
469   refresh ();
470   let k = getch () in
471   if k >= 0 && k <> 32 && k <> Char.code 'q' && k <> 13 then (
472     let new_order, loop =
473       (* Redraw the display. *)
474       if k = 12 (* ^L *) then None, true
475       (* Make the UP and DOWN arrow keys do something useful. *)
476       else if k = Key.up then (
477         if !selected_index > 0 then
478           Some (List.nth all_sort_fields (!selected_index-1)), true
479         else
480           None, true
481       )
482       else if k = Key.down then (
483         if !selected_index < List.length all_sort_fields - 1 then
484           Some (List.nth all_sort_fields (!selected_index+1)), true
485         else
486           None, true
487       )
488       (* Also understand the regular accelerator keys. *)
489       else if k = Char.code 'M' then
490         Some Memory, false
491       else if k = Char.code 'P' then
492         Some Processor, false
493       else if k = Char.code 'T' then
494         Some Time, false
495       else if k = Char.code 'N' then
496         Some DomainID, false
497       else (
498         (* It's one of the KEYs. *)
499         let i = int_of_key k in
500         if i >= 0 && i < List.length all_sort_fields then
501           Some (List.nth all_sort_fields i), false
502         else
503           None, true
504       ) in
505
506     (match new_order with
507      | None -> ()
508      | Some new_order ->
509          sort_order := new_order;
510          print_msg (sprintf "Sort order changed to: %s"
511                       (printable_sort_order new_order));
512          if not loop then (
513            refresh ();
514            sleep 1
515          )
516     );
517
518     if loop then change_sort_order ()
519   )
520
521 (* Note: We need to clear_pcpu_display_data every time
522  * we _leave_ PCPUDisplay mode.
523  *)
524 and set_tasks_display () =              (* key 0 *)
525   if !display_mode = PCPUDisplay then clear_pcpu_display_data ();
526   display_mode := TaskDisplay
527
528 and toggle_pcpu_display () =            (* key 1 *)
529   display_mode :=
530     match !display_mode with
531     | TaskDisplay | NetDisplay | BlockDisplay -> PCPUDisplay
532     | PCPUDisplay -> clear_pcpu_display_data (); TaskDisplay
533
534 and toggle_net_display () =             (* key 2 *)
535   display_mode :=
536     match !display_mode with
537     | PCPUDisplay -> clear_pcpu_display_data (); NetDisplay
538     | TaskDisplay | BlockDisplay -> NetDisplay
539     | NetDisplay -> TaskDisplay
540
541 and toggle_block_display () =           (* key 3 *)
542   display_mode :=
543     match !display_mode with
544     | PCPUDisplay -> clear_pcpu_display_data (); BlockDisplay
545     | TaskDisplay | NetDisplay -> BlockDisplay
546     | BlockDisplay -> TaskDisplay
547
548 and toggle_block_in_bytes_mode () =      (* key B *)
549   block_in_bytes :=
550     match !block_in_bytes with
551     | false -> true
552     | true  -> false
553
554 (* Write an init file. *)
555 and write_init_file () =
556   match !init_file with
557   | NoInitFile -> ()                    (* Do nothing if --no-init-file *)
558   | DefaultInitFile ->
559       let home = try Sys.getenv "HOME" with Not_found -> "/" in
560       let filename = home // rcfile in
561       _write_init_file filename
562   | InitFile filename ->
563       _write_init_file filename
564
565 and _write_init_file filename =
566   try
567     (* Create the new file as filename.new. *)
568     let chan = open_out (filename ^ ".new") in
569
570     let time = Unix.gettimeofday () in
571     let tm = Unix.localtime time in
572     let printable_date_time =
573       sprintf "%04d-%02d-%02d %02d:%02d:%02d"
574         (tm.Unix.tm_year + 1900) (tm.Unix.tm_mon+1) tm.Unix.tm_mday
575         tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec in
576     let username =
577       try
578         let uid = Unix.geteuid () in
579         (Unix.getpwuid uid).Unix.pw_name
580       with
581         Not_found -> "unknown" in
582
583     let fp = fprintf in
584     let nl () = fp chan "\n" in
585     let () = fp chan (f_"# %s virt-top configuration file\n") rcfile in
586     let () = fp chan (f_"# generated on %s by %s\n") printable_date_time username in
587     nl ();
588     fp chan "display %s\n" (cli_of_display !display_mode);
589     fp chan "delay %g\n" (float !delay /. 1000.);
590     fp chan "hist-cpu %d\n" !historical_cpu_delay;
591     if !iterations <> -1 then fp chan "iterations %d\n" !iterations;
592     fp chan "sort %s\n" (cli_of_sort_order !sort_order);
593     (match !uri with
594      | None -> ()
595      | Some uri -> fp chan "connect %s\n" uri
596     );
597     if !batch_mode = true then fp chan "batch true\n";
598     if !secure_mode = true then fp chan "secure true\n";
599     nl ();
600     output_string chan (s_"# To send debug and error messages to a file, uncomment next line\n");
601     fp chan "#debug virt-top.out\n";
602     nl ();
603     output_string chan (s_"# Enable CSV output to the named file\n");
604     fp chan "#csv virt-top.csv\n";
605     nl ();
606     output_string chan (s_"# To protect this file from being overwritten, uncomment next line\n");
607     fp chan "#overwrite-init-file false\n";
608
609     close_out chan;
610
611     (* If the file exists, rename it as filename.old. *)
612     (try Unix.rename filename (filename ^ ".old")
613      with Unix.Unix_error _ -> ());
614
615     (* Rename filename.new to filename. *)
616     Unix.rename (filename ^ ".new") filename;
617
618     print_msg (sprintf (f_"Wrote settings to %s") filename);
619     refresh ();
620     sleep 2
621   with
622   | Sys_error err ->
623       print_msg (s_"Error" ^ ": " ^ err);
624       refresh (); sleep 2
625   | Unix.Unix_error (err, fn, str) ->
626       print_msg (s_"Error" ^ ": " ^
627                    (Unix.error_message err) ^ " " ^ fn ^ " " ^ str);
628       refresh ();
629       sleep 2
630
631 and show_help (_, _, _, _, _, _, hostname,
632                (libvirt_major, libvirt_minor, libvirt_release)) =
633   clear ();
634
635   (* Get the screen/window size. *)
636   let lines, cols = get_size () in
637
638   (* Banner at the top of the screen. *)
639   let banner =
640     sprintf (f_"virt-top %s ocaml-libvirt %s libvirt %d.%d.%d by Red Hat")
641       Version.version
642       Libvirt_version.version
643       libvirt_major libvirt_minor libvirt_release in
644   let banner = pad cols banner in
645   attron A.reverse;
646   mvaddstr 0 0 banner;
647   attroff A.reverse;
648
649   (* Status. *)
650   mvaddstr 1 0
651     (sprintf
652        (f_"Delay: %.1f secs; Batch: %s; Secure: %s; Sort: %s")
653        (float !delay /. 1000.)
654        (if !batch_mode then s_"On" else s_"Off")
655        (if !secure_mode then s_"On" else s_"Off")
656        (printable_sort_order !sort_order));
657   mvaddstr 2 0
658     (sprintf
659        (f_"Connect: %s; Hostname: %s")
660        (match !uri with None -> s_"default" | Some s -> s)
661        hostname);
662
663   (* Misc keys on left. *)
664   let banner = pad 38 (s_"MAIN KEYS") in
665   attron A.reverse;
666   mvaddstr header_lineno 1 banner;
667   attroff A.reverse;
668
669   let get_lineno =
670     let lineno = ref domains_lineno in
671     fun () -> let i = !lineno in incr lineno; i
672   in
673   let key keys description =
674     let lineno = get_lineno () in
675     move lineno 1; attron A.bold; addstr keys; attroff A.bold;
676     move lineno 10; addstr description
677   in
678   key "space ^L" (s_"Update display");
679   key "q"        (s_"Quit");
680   key "d s"      (s_"Set update interval");
681   key "h"        (s_"Help");
682   key "B"        (s_"toggle block info req/bytes");
683
684   (* Sort order. *)
685   ignore (get_lineno ());
686   let banner = pad 38 (s_"SORTING") in
687   attron A.reverse;
688   mvaddstr (get_lineno ()) 1 banner;
689   attroff A.reverse;
690
691   key "P" (s_"Sort by %CPU");
692   key "M" (s_"Sort by %MEM");
693   key "T" (s_"Sort by TIME");
694   key "N" (s_"Sort by ID");
695   key "F" (s_"Select sort field");
696
697   (* Display modes on right. *)
698   let banner = pad 39 (s_"DISPLAY MODES") in
699   attron A.reverse;
700   mvaddstr header_lineno 40 banner;
701   attroff A.reverse;
702
703   let get_lineno =
704     let lineno = ref domains_lineno in
705     fun () -> let i = !lineno in incr lineno; i
706   in
707   let key keys description =
708     let lineno = get_lineno () in
709     move lineno 40; attron A.bold; addstr keys; attroff A.bold;
710     move lineno 49; addstr description
711   in
712   key "0" (s_"Domains display");
713   key "1" (s_"Toggle physical CPUs");
714   key "2" (s_"Toggle network interfaces");
715   key "3" (s_"Toggle block devices");
716
717   (* Update screen and wait for key press. *)
718   mvaddstr (lines-1) 0
719     (s_"More help in virt-top(1) man page. Press any key to return.");
720   refresh ();
721   ignore (getch ())
722
723 and unknown_command k =
724   print_msg (s_"Unknown command - try 'h' for help");
725   refresh ();
726   sleep 1