Add Guest -> Operating system information menu item.
[guestfs-browser.git] / window.ml
1 (* Guestfs Browser.
2  * Copyright (C) 2010 Red Hat Inc.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  *)
18
19 open Printf
20
21 open Utils
22 open Slave_types
23
24 module G = Guestfs
25
26 type connect_menu = {
27   connect_menu : GMenu.menu;
28   connect_kvm_item : GMenu.menu_item;
29   connect_xen_item : GMenu.menu_item;
30   connect_none_item : GMenu.menu_item;
31   connect_uri_item : GMenu.menu_item;
32   open_disk_item : GMenu.menu_item;
33   quit_item : GMenu.menu_item;
34 }
35
36 type guest_menu = {
37   guest_menu : GMenu.menu;
38   guest_inspection_item : GMenu.menu_item;
39 }
40
41 class window =
42   (* Window. *)
43   let title = "Guest Filesystem Browser" in
44   let window = GWindow.window ~width:700 ~height:700 ~title () in
45   let vbox = GPack.vbox ~packing:window#add () in
46
47   (* Menus. *)
48   let menubar = GMenu.menu_bar ~packing:vbox#pack () in
49   let factory = new GMenu.factory menubar in
50   let accel_group = factory#accel_group in
51
52   let connect_menu =
53     let menu = factory#add_submenu "_Connect" in
54     let factory = new GMenu.factory menu ~accel_group in
55     let kvm = factory#add_item "Connect to local _KVM hypervisor" in
56     let xen = factory#add_item "Connect to local _Xen hypervisor" in
57     let none = factory#add_item "_Connect to default hypervisor" in
58     let uri = factory#add_item "Connect to a _libvirt URI ..." in
59     ignore (factory#add_separator ());
60     let opend =
61       factory#add_item "_Open disk image ..." ~key:GdkKeysyms._O in
62     ignore (factory#add_separator ());
63     let quit = factory#add_item "E_xit" ~key:GdkKeysyms._Q in
64     { connect_menu = menu; connect_kvm_item = kvm;
65       connect_xen_item = xen; connect_none_item = none;
66       connect_uri_item = uri; open_disk_item = opend; quit_item = quit } in
67
68   let guest_menu =
69     let menu = factory#add_submenu "_Guest" in
70     let factory = new GMenu.factory menu ~accel_group in
71     let inspection = factory#add_item "Operating system information ..." in
72     { guest_menu = menu; guest_inspection_item = inspection } in
73
74   (* Top toolbar. *)
75   let hbox =
76     let hbox = GPack.hbox ~border_width:4 ~packing:vbox#pack () in
77     hbox#pack (mklabel "Guest: ");
78     hbox in
79
80   (* Combo box for displaying virtual machine names. *)
81   let vmcombo = GEdit.combo_box_text ~packing:hbox#pack () in
82
83   (* Refresh button.
84    * http://stackoverflow.com/questions/2188659/stock-icons-not-shown-on-buttons
85    *)
86   let refresh_button =
87     let image = GMisc.image ~stock:`REFRESH () in
88     let b = GButton.button ~packing:hbox#pack () in
89     b#set_image (image :> GObj.widget);
90     b in
91
92   (* Throbber. *)
93   let throbber_static = Throbber.static () in
94   let throbber_animation = Throbber.animation () in
95   let throbber =
96     (* Workaround for http://caml.inria.fr/mantis/view.php?id=4732 *)
97     let from = Obj.magic 3448763 (* `END *) in
98     GMisc.image ~pixbuf:throbber_static ~packing:(hbox#pack ~from) () in
99
100   (* Main part of display is the file tree. *)
101   (* Create the filetree inside a scrolled window. *)
102   let sw = GBin.scrolled_window
103     ~hpolicy:`AUTOMATIC ~vpolicy:`ALWAYS
104     ~packing:(vbox#pack ~expand:true ~fill:true) () in
105   let tree = new Filetree.tree ~packing:sw#add () in
106
107   (* Status bar and progress bar at the bottom. *)
108   let hbox = GPack.hbox ~spacing:4 ~packing:vbox#pack () in
109   let progress_bar = GRange.progress_bar ~packing:hbox#pack () in
110   let statusbar = GMisc.statusbar ~packing:(hbox#pack ~expand:true) () in
111   let statusbar_context = statusbar#new_context ~name:"Standard" in
112
113   (* Signals. *)
114   let connect_kvm_signal = new GUtil.signal () in
115   let connect_xen_signal = new GUtil.signal () in
116   let connect_none_signal = new GUtil.signal () in
117   let connect_uri_signal = new GUtil.signal () in
118   let inspection_signal = new GUtil.signal () in
119
120 object (self)
121   inherit GUtil.ml_signals [connect_kvm_signal#disconnect;
122                             connect_xen_signal#disconnect;
123                             connect_none_signal#disconnect;
124                             connect_uri_signal#disconnect;
125                             inspection_signal#disconnect]
126
127   method connect_kvm_signal = connect_kvm_signal#connect ~after
128   method connect_xen_signal = connect_xen_signal#connect ~after
129   method connect_none_signal = connect_none_signal#connect ~after
130   method connect_uri_signal = connect_uri_signal#connect ~after
131   method inspection_signal = inspection_signal#connect ~after
132
133   initializer
134     ignore (statusbar_context#push title);
135     window#show ();
136
137     (* Quit. *)
138     let quit _ = GMain.quit (); false in
139     ignore (window#connect#destroy ~callback:GMain.quit);
140     ignore (window#event#connect#delete ~callback:quit);
141     ignore (connect_menu.quit_item#connect#activate
142               ~callback:(fun () -> ignore (quit ()); ()));
143
144     (* Accel_group. *)
145     window#add_accel_group accel_group;
146
147     (* Menu entries emit signals. *)
148     ignore (connect_menu.connect_kvm_item#connect#activate
149               ~callback:connect_kvm_signal#call);
150     ignore (connect_menu.connect_xen_item#connect#activate
151               ~callback:connect_xen_signal#call);
152     ignore (connect_menu.connect_none_item#connect#activate
153               ~callback:connect_none_signal#call);
154     ignore (connect_menu.connect_uri_item#connect#activate
155               ~callback:connect_uri_signal#call);
156     ignore (guest_menu.guest_inspection_item#connect#activate
157               ~callback:inspection_signal#call);
158
159     (* VM combo box when changed by the user.
160      * The refresh button acts like changing the VM combo too.
161      *)
162     let combo, (model, column) = vmcombo in
163     ignore (
164       combo#connect#changed
165         ~callback:(
166           fun () ->
167             match combo#active_iter with
168             | None -> () (* nothing selected *)
169             | Some row -> self#open_domain (model#get ~row ~column)
170         )
171     );
172     ignore (
173       refresh_button#connect#clicked
174         ~callback:(
175           fun () ->
176             match combo#active_iter with
177             | None -> () (* nothing selected *)
178             | Some row -> self#open_domain (model#get ~row ~column)
179         )
180     )
181
182   (* Set the statusbar text. *)
183   method set_statusbar msg =
184     statusbar_context#pop ();
185     ignore (statusbar_context#push msg)
186
187   (* Return the filetree. *)
188   method tree = tree
189
190   (* Connect to the given URI. *)
191   method connect_to uri () =
192     tree#clear ();
193     Slave.discard_command_queue ();
194     Slave.connect uri (self#when_connected uri)
195
196   (* Called back when connected to a new hypervisor. *)
197   method private when_connected uri doms =
198     self#populate_vmcombo doms
199
200   (* Populate the VM combo box. *)
201   method private populate_vmcombo doms =
202     let combo, (model, column) = vmcombo in
203     model#clear ();
204     List.iter (
205       fun { dom_name = name } ->
206         let row = model#append () in
207         model#set ~row ~column name
208     ) doms
209
210   (* When a new domain is selected by the user, eg through vmcombo. *)
211   method private open_domain name =
212     tree#clear ();
213     Slave.discard_command_queue ();
214     Slave.open_domain name (self#when_opened_domain name)
215
216   (* Called back when domain was opened successfully. *)
217   method private when_opened_domain name data =
218     debug "when_opened_domain callback";
219     self#when_opened_common name data
220
221   (* When a set of disk images is selected by the user. *)
222   method private open_disk_images images =
223     match images with
224     | [] -> ()
225     | images ->
226         tree#clear ();
227         Slave.discard_command_queue ();
228         Slave.open_images images (self#when_opened_disk_images images)
229
230   (* Called back when disk image(s) were opened successfully. *)
231   method private when_opened_disk_images images data =
232     match images with
233     | [] -> ()
234     | (image, _) :: _ ->
235         debug "when_opened_disk_images callback";
236         self#when_opened_common image data
237
238   (* Common code for when_opened_domain/when_opened_disk_images. *)
239   method private when_opened_common name data =
240     (* Dump some of the inspection data in debug messages. *)
241     List.iter (fun (dev, t) -> debug "filesystem: %s: %s" dev t)
242       data.insp_all_filesystems;
243     List.iter (
244       fun { insp_root = root; insp_type = typ; insp_distro = distro;
245             insp_major_version = major; insp_minor_version = minor } ->
246         debug "root device %s contains %s %s %d.%d" root typ distro major minor;
247     ) data.insp_oses;
248
249     tree#add_os name data
250
251   (* Public callbacks. *)
252   method throbber_busy () =
253     throbber#set_pixbuf throbber_animation
254
255   method throbber_idle () =
256     throbber#set_pixbuf throbber_static
257
258   method progress (position, total) =
259     if position = 0L && total = 1L then
260       progress_bar#pulse ()
261     else (
262       let frac = Int64.to_float position /. Int64.to_float total in
263       if frac < 0. || frac > 1. then
264         eprintf "warning: progress bar out of range: %Ld / %Ld (%g)\n"
265           position total frac;
266       let frac = if frac < 0. then 0. else if frac > 1. then 1. else frac in
267       progress_bar#set_fraction frac
268     )
269
270   (* This is called in the main thread whenever a command fails in the
271    * slave thread.  The command queue has been cleared before this is
272    * called, so our job here is to reset the main window, and if
273    * necessary to turn the exception into an error message.
274    *)
275   method failure exn =
276     let raw_msg = Printexc.to_string exn in
277     debug "failure hook: %s" raw_msg;
278
279     let title, msg = pretty_string_of_exn exn in
280     let icon = GMisc.image () in
281     icon#set_stock `DIALOG_ERROR;
282     icon#set_icon_size `DIALOG;
283     GToolbox.message_box ~title ~icon msg
284
285   (* Do what the user asked on the command line. *)
286   method run_cli_request = function
287   | Cmdline.Empty_window -> ()
288   | Cmdline.Open_images images ->
289       self#open_disk_images images
290   | Cmdline.Open_guest guest ->
291       (* Open libvirt connection, and in the callback open the guest. *)
292       let uri = connect_uri () in
293       Slave.connect uri (self#when_connected_cli_request guest)
294
295   method private when_connected_cli_request guest doms =
296     self#populate_vmcombo doms;
297
298     (* "guest" should match a domain in "doms".  Check this and
299      * get the index of it.
300      *)
301     let rec loop i = function
302       | [] ->
303           failwith "guest %s not found (do you need to use --connect?)" guest
304       | d::ds when d = guest -> i
305       | _::ds -> loop (i+1) ds
306     in
307     let i = loop 0 (List.map (fun { dom_name = name } -> name) doms) in
308
309     let combo, _ = vmcombo in
310     combo#set_active i
311
312 end