Example program with threads
Richard Jones <[email protected]> Tue, 29 Sep 2009 12:20:13 +0100
| Newsgroups | gmane.comp.lang.ocaml.lib.gtk |
|---|---|
| Message-ID | <[email protected]> |
I will just post this, a freely copyable example program using lablgtk, with threads for a responsive interface: http://git.et.redhat.com/?p=libguestfs.git;a=blob;f=ocaml/examples/viewer.ml;hb=HEAD Rich. -- Richard Jones Red Hat _______________________________________________ Lablgtk mailing list [email protected] http://yquem.inria.fr/cgi-bin/mailman/listinfo/lablgtk
viewer.ml
(text/plain, 22.6 KB)
(* This is a virtual machine graphical viewer tool.
* Written by Richard W.M. Jones, Sept. 2009.
*
* It demonstrates some complex programming techniques: OCaml, Gtk+,
* threads, and use of both libguestfs and libvirt from threads.
*
* You will need the following installed in order to compile it:
* - ocaml (http://caml.inria.fr/)
* - ocamlfind (http://projects.camlcity.org/projects/findlib.html/)
* - extlib (http://code.google.com/p/ocaml-extlib/)
* - lablgtk2 (http://wwwfun.kurims.kyoto-u.ac.jp/soft/lsl/lablgtk.html
* - xml-light (http://tech.motion-twin.com/xmllight.html)
* - ocaml-libvirt (http://libvirt.org/ocaml)
* - ocaml-libguestfs
*
* Note that most/all of these are available as packages via Fedora,
* Debian, Ubuntu or GODI. You won't need to compile them from source.
*
* You will also need to configure libguestfs:
* ./configure --enable-ocaml-viewer
*
* All programs in the ocaml/examples subdirectory, including this
* one, may be freely copied without any restrictions.
*)
(* Architecturally, there is one main thread which does all the Gtk
* calls, and one slave thread which executes all libguestfs and
* libvirt calls. The main thread sends commands to the slave thread,
* which are delivered in a queue and acted on in sequence. Responses
* are delivered back to the main thread as commands finish.
*
* The commands are just OCaml objects (type: Slave.command). The
* queue of commands is an OCaml Queue. The responses are sent by adding
* idle events to the glib main loop[1].
*
* If a command fails, it causes the input queue to be cleared. In
* this case, a failure response is sent to the main loop which
* causes the display to be reset and possibly an error message to
* be shown.
*
* The global variables [conn], [dom] and [g] are the libvirt
* connection, current domain, and libguestfs handle respectively.
* Because these can be accessed by both threads, they are
* protected from the main thread by access methods which
* (a) prevent the main thread from using them unlocked, and
* (b) prevent the main thread from doing arbitrary / long-running
* operations on them (the main thread must send a command instead).
*
* [1] http://library.gnome.org/devel/gtk-faq/stable/x499.html
*)
open Printf
open ExtList
let (//) = Filename.concat
(* Short names for commonly used modules. *)
module C = Libvirt.Connect
module Cd = Condition
module D = Libvirt.Domain
module G = Guestfs
module M = Mutex
module Q = Queue
let verbose = ref false (* Verbose mode. *)
let debug fs =
let f str = if !verbose then ( prerr_string str; prerr_newline () ) in
ksprintf f fs
(*----------------------------------------------------------------------*)
(* Slave thread. The signature describes what operations the main
* thread can perform, and protects the locked internals of the
* slave thread.
*)
module Slave : sig
type 'a callback = 'a -> unit
type partinfo = {
pt_name : string; (** device / LV name *)
pt_size : int64; (** in bytes *)
pt_content : string; (** the output of the 'file' command *)
pt_statvfs : G.statvfs option; (** None if not mountable *)
}
val no_callback : 'a callback
(** Use this as the callback if you don't want a callback. *)
val set_failure_callback : exn callback -> unit
(** Set the function that is called in the main thread whenever
there is a command failure in the slave. The command queue
is cleared before this is sent. [exn] is the exception
associated with the failure. *)
val set_busy_callback : [`Busy|`Idle] callback -> unit
(** Set the function that is called in the main thread whenever
the slave thread goes busy or idle. *)
val exit_thread : unit -> unit
(** [exit_thread ()] causes the slave thread to exit. *)
val connect : string option -> string option callback -> unit
(** [connect uri cb] connects to libvirt [uri], and calls [cb]
if it completes successfully. Any previous connection is
automatically cleaned up and disconnected. *)
val get_domains : string list callback -> unit
(** [get_domains cb] gets the list of active domains from libvirt,
and calls [cb domains] with the names of those domains. *)
val open_domain : string -> partinfo list callback -> unit
(** [open_domain dom cb] sets the domain [dom] as the current
domain, and launches a libguestfs handle for it. Any previously
current domain and libguestfs handle is closed. Once the
libguestfs handle is opened (which usually takes some time),
callback [cb] is called with the list of partitions found
in the guest. *)
val slave_loop : unit -> unit
(** The slave thread's main loop, running in the slave thread. *)
end = struct
type partinfo = {
pt_name : string;
pt_size : int64;
pt_content : string;
pt_statvfs : G.statvfs option;
}
(* Commands sent by the main thread to the slave thread. When
* [cmd] is successfully completed, [callback] will be delivered
* (in the main thread). If [cmd] fails, then the global error
* callback will be delivered in the main thread.
*)
type command =
| Exit_thread
| Connect of string option * string option callback
| Get_domains of string list callback
| Open_domain of string * partinfo list callback
and 'a callback = 'a -> unit
let string_of_command = function
| Exit_thread -> "Exit_thread"
| Connect (None, _) -> "Connect [no uri]"
| Connect (Some uri, _) -> "Connect " ^ uri
| Get_domains _ -> "Get_domains"
| Open_domain (name, _) -> "Open_domain " ^ name
let no_callback _ = ()
let failure_cb = ref (fun _ -> ())
let set_failure_callback cb = failure_cb := cb
let busy_cb = ref (fun _ -> ())
let set_busy_callback cb = busy_cb := cb
(* Execute a function, while holding a mutex. If the function
* fails, ensure we release the mutex before rethrowing the
* exception.
*)
type ('a, 'b) choice = Either of 'a | Or of 'b
let with_lock m f =
M.lock m;
let r = try Either (f ()) with exn -> Or exn in
M.unlock m;
match r with
| Either r -> r
| Or exn -> raise exn
let q = Q.create () (* queue of commands *)
let q_lock = M.create ()
let q_cond = Cd.create ()
(* Send a command message to the slave thread. *)
let send_to_slave c =
debug "sending to slave: %s" (string_of_command c);
with_lock q_lock (
fun () ->
Q.push c q;
Cd.signal q_cond
)
let exit_thread () =
with_lock q_lock (fun () -> Q.clear q);
send_to_slave Exit_thread
let connect uri cb =
send_to_slave (Connect (uri, cb))
let get_domains cb =
send_to_slave (Get_domains cb)
let open_domain dom cb =
send_to_slave (Open_domain (dom, cb))
(* These are not protected by a mutex because we don't allow
* any references to these objects to escape from the slave
* thread.
*)
let conn = ref None (* libvirt connection *)
let dom = ref None (* libvirt domain *)
let g = ref None (* libguestfs handle *)
let quit = ref false
let rec slave_loop () =
debug "Slave.slave_loop: waiting for a command";
let c =
with_lock q_lock (
fun () ->
while Q.is_empty q do
Cd.wait q_cond q_lock
done;
Q.pop q
) in
(try
debug "Slave.slave_loop: executing: %s" (string_of_command c);
!busy_cb `Busy;
exec_command c;
!busy_cb `Idle;
debug "Slave.slave_loop: command succeeded";
with exn ->
(* If an exception is thrown, it means the command failed. In
* this case we clear the command queue and deliver the failure
* callback in the main thread.
*)
debug "Slave.slave_loop: command failed";
!busy_cb `Idle;
with_lock q_lock (fun () -> Q.clear q);
GtkThread.async !failure_cb exn
);
if !quit then Thread.exit ();
slave_loop ()
and exec_command = function
| Exit_thread ->
quit := true; (* quit first in case disconnect_all throws an exn *)
disconnect_all ()
| Connect (name, cb) ->
disconnect_all ();
conn := Some (C.connect_readonly ?name ());
cb name
| Get_domains cb ->
let conn = Option.get !conn in
let doms = D.get_domains conn [D.ListAll] in
(* Only return the names, so that the libvirt objects
* aren't leaked outside the slave thread.
*)
let doms = List.map D.get_name doms in
cb doms
| Open_domain (domname, cb) ->
let conn = Option.get !conn in
disconnect_dom ();
dom := Some (D.lookup_by_name conn domname);
let dom = Option.get !dom in
(* Get the devices. *)
let xml = D.get_xml_desc dom in
let devs = get_devices_from_xml xml in
(* Create the libguestfs handle and launch it. *)
let g' = G.create () in
List.iter (G.add_drive_ro g') devs;
G.launch g';
g := Some g';
(* Get the list of partitions. *)
let parts = Array.to_list (G.list_partitions g') in
(* Remove any which are PVs. *)
let pvs = Array.to_list (G.pvs g') in
let parts = List.filter (fun part -> not (List.mem part pvs)) parts in
let lvs = Array.to_list (G.lvs g') in
let parts = parts @ lvs in
let parts = List.map (
fun part ->
(* Find out the size of each partition. *)
let size = G.blockdev_getsize64 g' part in
(* Find out what's on each partition. *)
let content = G.file g' part in
(* Try to mount it. *)
let statvfs =
try
G.mount_ro g' part "/";
Some (G.statvfs g' "/")
with _ -> None in
G.umount_all g';
{ pt_name = part; pt_size = size; pt_content = content;
pt_statvfs = statvfs }
) parts in
(* Call the callback. *)
cb parts
(* Close all libvirt/libguestfs handles. *)
and disconnect_all () =
disconnect_dom ();
(match !conn with Some conn -> C.close conn | None -> ());
conn := None
(* Close dom and libguestfs handles. *)
and disconnect_dom () =
(match !g with Some g -> G.close g | None -> ());
g := None;
(match !dom with Some dom -> D.free dom | None -> ());
dom := None
(* This would be much simpler if OCaml had either a decent XPath
* implementation, or if ocamlduce was stable enough that we
* could rely on it being available. So this is *not* an example
* of either good OCaml or good programming. XXX
*)
and get_devices_from_xml xml =
let xml = Xml.parse_string xml in
let devices =
match xml with
| Xml.Element ("domain", _, children) ->
let devices =
List.filter_map (
function
| Xml.Element ("devices", _, devices) -> Some devices
| _ -> None
) children in
List.concat devices
| _ ->
failwith "get_xml_desc didn't return <domain/>" in
let rec source_dev_of = function
| [] -> None
| Xml.Element ("source", attrs, _) :: rest ->
(try Some (List.assoc "dev" attrs)
with Not_found -> source_dev_of rest)
| _ :: rest -> source_dev_of rest
in
let rec source_file_of = function
| [] -> None
| Xml.Element ("source", attrs, _) :: rest ->
(try Some (List.assoc "file" attrs)
with Not_found -> source_file_of rest)
| _ :: rest -> source_file_of rest
in
let devs =
List.filter_map (
function
| Xml.Element ("disk", _, children) -> source_dev_of children
| _ -> None
) devices in
let files =
List.filter_map (
function
| Xml.Element ("disk", _, children) -> source_file_of children
| _ -> None
) devices in
devs @ files
end
(* End of slave thread code. *)
(*----------------------------------------------------------------------*)
(* Display state. *)
type display_state = {
window : GWindow.window;
vmlist_set : string list -> unit;
throbber_set : [`Busy|`Idle] -> unit;
da : GMisc.drawing_area;
draw : GDraw.drawable;
drawing_area_repaint : unit -> unit;
set_statusbar : string -> unit;
clear_statusbar : unit -> unit;
pango_large_context : GPango.context_rw;
pango_small_context : GPango.context_rw;
}
(* This is called in the main thread whenever a command fails in the
* slave thread. The command queue has been cleared before this is
* called, so our job here is to reset the main window, and if
* necessary to turn the exception into an error message.
*)
let failure ds exn =
let title = "Error" in
let msg = Printexc.to_string exn in
debug "failure callback: %s" msg;
let icon = GMisc.image () in
icon#set_stock `DIALOG_ERROR;
icon#set_icon_size `DIALOG;
GToolbox.message_box ~title ~icon msg
(* This is called in the main thread when the slave thread transitions
* to busy or idle.
*)
let busy ds state = ds.throbber_set state
(* Main window and callbacks from menu etc. *)
let main_window opened_domain repaint =
let window_title = "Virtual machine graphical viewer" in
let window = GWindow.window ~width:800 ~height:600 ~title:window_title () in
let vbox = GPack.vbox ~packing:window#add () in
(* Do the menus. *)
let menubar = GMenu.menu_bar ~packing:vbox#pack () in
let factory = new GMenu.factory menubar in
let accel_group = factory#accel_group in
let connect_menu = factory#add_submenu "_Connect" in
let factory = new GMenu.factory connect_menu ~accel_group in
let quit_item = factory#add_item "E_xit" ~key:GdkKeysyms._Q in
(* Quit. *)
let quit _ = GMain.quit (); false in
ignore (window#connect#destroy ~callback:GMain.quit);
ignore (window#event#connect#delete ~callback:quit);
ignore (quit_item#connect#activate
~callback:(fun () -> ignore (quit ()); ()));
(* Top status area. *)
let hbox = GPack.hbox ~border_width:4 ~packing:vbox#pack () in
ignore (GMisc.label ~text:"Guest: " ~packing:hbox#pack ());
(* List of VMs. *)
let vmcombo = GEdit.combo_box_text ~packing:hbox#pack () in
let vmlist_set names =
let combo, (model, column) = vmcombo in
model#clear ();
List.iter (
fun name ->
let row = model#append () in
model#set ~row ~column name
) names
in
(* Throbber, http://faq.pygtk.org/index.py?req=show&file=faq23.037.htp *)
let static = Throbber.static () in
(*let animation = Throbber.animation () in*)
let throbber =
GMisc.image ~pixbuf:static ~packing:(hbox#pack ~from:`END) () in
let throbber_set = function
| `Busy -> (*throbber#set_pixbuf animation*)
(* Workaround because no binding for GdkPixbufAnimation: *)
let file = Filename.dirname Sys.argv.(0) // "Throbber.gif" in
throbber#set_file file
| `Idle -> throbber#set_pixbuf static
in
(* Drawing area. *)
let da = GMisc.drawing_area ~packing:(vbox#pack ~expand:true ~fill:true) () in
da#misc#realize ();
let draw = new GDraw.drawable da#misc#window in
window#set_geometry_hints ~min_size:(80,80) (da :> GObj.widget);
(* Calling this can be used to force a redraw of the drawing area. *)
let drawing_area_repaint () = GtkBase.Widget.queue_draw da#as_widget in
(* Pango contexts used to draw large and small text. *)
let pango_large_context = da#misc#create_pango_context in
pango_large_context#set_font_description (Pango.Font.from_string "Sans 12");
let pango_small_context = da#misc#create_pango_context in
pango_small_context#set_font_description (Pango.Font.from_string "Sans 8");
(* Status bar at the bottom of the screen. *)
let set_statusbar =
let statusbar = GMisc.statusbar ~packing:vbox#pack () in
let context = statusbar#new_context ~name:"Standard" in
ignore (context#push window_title);
fun msg ->
context#pop ();
ignore (context#push msg)
in
let clear_statusbar () = set_statusbar "" in
(* Display the window and enter Gtk+ main loop. *)
window#show ();
window#add_accel_group accel_group;
(* display_state which is threaded through all the other callbacks,
* allowing callbacks to update the window.
*)
let ds =
{ window = window; vmlist_set = vmlist_set; throbber_set = throbber_set;
da = da; draw = draw; drawing_area_repaint = drawing_area_repaint;
set_statusbar = set_statusbar; clear_statusbar = clear_statusbar;
pango_large_context = pango_large_context;
pango_small_context = pango_small_context; } in
(* Set up some callbacks which require access to the display_state. *)
ignore (
let combo, (model, column) = vmcombo in
combo#connect#changed
~callback:(
fun () ->
match combo#active_iter with
| None -> ()
| Some row ->
let name = model#get ~row ~column in
ds.set_statusbar (sprintf "Opening %s ..." name);
Slave.open_domain name (opened_domain ds))
);
ignore (da#event#connect#expose ~callback:(repaint ds));
ds
(* Partition info for the current domain, if one is loaded. *)
let parts = ref None
(* This is called in the main thread when we've connected to libvirt. *)
let rec connected ds uri =
debug "connected callback";
let msg =
match uri with
| None -> "Connected to libvirt"
| Some uri -> sprintf "Connected to %s" uri in
ds.set_statusbar msg;
Slave.get_domains (got_domains ds)
(* This is called in the main thread when we've got the list of domains. *)
and got_domains ds doms =
debug "got_domains callback: (%s)" (String.concat " " doms);
ds.vmlist_set doms
(* This is called when we have opened a domain. *)
and opened_domain ds parts' =
debug "opened_domain callback";
ds.clear_statusbar ();
parts := Some parts';
ds.drawing_area_repaint ()
and repaint ds _ =
(match !parts with
| None -> ()
| Some parts ->
real_repaint ds parts
);
false
and real_repaint ds parts =
let width, height = ds.draw#size in
ds.draw#set_background `WHITE;
ds.draw#set_foreground `WHITE;
ds.draw#rectangle ~x:0 ~y:0 ~width ~height ~filled:true ();
let sum = List.fold_left Int64.add 0L in
let totsize = sum (List.map (fun { Slave.pt_size = size } -> size) parts) in
let scale = (float height -. 16.) /. Int64.to_float totsize in
(* Calculate the height in pixels of each partition, if we were to
* display it at a true relative size.
*)
let parts =
List.map (
fun ({ Slave.pt_size = size } as part) ->
let h = scale *. Int64.to_float size in
(h, part)
) parts in
(*
if !verbose then (
eprintf "real_repaint: before borrowing:\n";
List.iter (
fun (h, part) ->
eprintf "%s\t%g pix\n" part.Slave.pt_name h
) parts
);
*)
(* Now adjust the heights of small partitions so they "borrow" some
* height from the larger partitions.
*)
let min_h = 32. in
let rec borrow needed = function
| [] -> 0., []
| (h, part) :: parts ->
let spare = h -. min_h in
if spare >= needed then (
needed, (h -. needed, part) :: parts
) else if spare > 0. then (
let needed = needed -. spare in
let spare', parts = borrow needed parts in
spare +. spare', (h -. spare, part) :: parts
) else (
let spare', parts = borrow needed parts in
spare', (h, part) :: parts
)
in
let rec loop = function
| parts, [] -> List.rev parts
| prev, ((h, part) :: parts) ->
let needed = min_h -. h in
let h, prev, parts =
if needed > 0. then (
(* Find some spare height in a succeeding partition(s). *)
let spare, parts = borrow needed parts in
(* Or if not, in a preceeding partition(s). *)
let spare, prev =
if spare = 0. then borrow needed prev else spare, prev in
h +. spare, prev, parts
) else (
h, prev, parts
) in
loop (((h, part) :: prev), parts)
in
let parts = loop ([], parts) in
(*
if !verbose then (
eprintf "real_repaint: after borrowing:\n";
List.iter (
fun (h, part) ->
eprintf "%s\t%g pix\n" part.Slave.pt_name h
) parts
);
*)
(* Calculate the proportion space used in each partition. *)
let parts = List.map (
fun (h, part) ->
let used =
match part.Slave.pt_statvfs with
| None -> 0.
| Some { G.bavail = bavail; blocks = blocks } ->
let num = Int64.to_float (Int64.sub blocks bavail) in
let denom = Int64.to_float blocks in
num /. denom in
(h, used, part)
) parts in
(* Draw it. *)
ignore (
List.fold_left (
fun y (h, used, part) ->
(* This partition occupies pixels 8+y .. 8+y+h-1 *)
let yb = 8 + int_of_float y
and yt = 8 + int_of_float (y +. h) in
ds.draw#set_foreground `WHITE;
ds.draw#rectangle ~x:8 ~y:yb ~width:(width-16) ~height:(yt-yb)
~filled:true ();
let col =
if used < 0.6 then `NAME "grey"
else if used < 0.8 then `NAME "pink"
else if used < 0.9 then `NAME "hot pink"
else `NAME "red" in
ds.draw#set_foreground col;
let w = int_of_float (used *. (float width -. 16.)) in
ds.draw#rectangle ~x:8 ~y:yb ~width:w ~height:(yt-yb) ~filled:true ();
ds.draw#set_foreground `BLACK;
ds.draw#rectangle ~x:8 ~y:yb ~width:(width-16) ~height:(yt-yb) ();
(* Large text - the device name. *)
let txt = ds.pango_large_context#create_layout in
Pango.Layout.set_text txt part.Slave.pt_name;
let fore = `NAME "dark slate grey" in
ds.draw#put_layout ~x:12 ~y:(yb+4) ~fore txt;
let { Pango.height = txtheight; Pango.width = txtwidth } =
Pango.Layout.get_pixel_extent txt in
(* Small text below - the content. *)
let txt = ds.pango_small_context#create_layout in
Pango.Layout.set_text txt part.Slave.pt_content;
let fore = `BLACK in
ds.draw#put_layout ~x:12 ~y:(yb+4+txtheight) ~fore txt;
(* Small text right - size. *)
let size =
match part.Slave.pt_statvfs with
| None -> printable_size part.Slave.pt_size
| Some { G.blocks = blocks; bsize = bsize } ->
let bytes = Int64.mul blocks bsize in
let pc = 100. *. used in
sprintf "%s (%.1f%% used)" (printable_size bytes) pc in
let txt = ds.pango_small_context#create_layout in
Pango.Layout.set_text txt size;
ds.draw#put_layout ~x:(16+txtwidth) ~y:(yb+4) ~fore txt;
(y +. h)
) 0. parts
)
and printable_size bytes =
if bytes < 16_384L then sprintf "%Ld bytes" bytes
else if bytes < 16_777_216L then
sprintf "%Ld KiB" (Int64.div bytes 1024L)
else if bytes < 17_179_869_184L then
sprintf "%Ld MiB" (Int64.div bytes 1_048_576L)
else
sprintf "%Ld GiB" (Int64.div bytes 1_073_741_824L)
let default_uri = ref ""
let argspec = Arg.align [
"-verbose", Arg.Set verbose, "Verbose mode";
"-connect", Arg.Set_string default_uri, "Connect to libvirt URI";
]
let anon_fun _ =
failwith (sprintf "%s: unknown command line argument"
(Filename.basename Sys.executable_name))
let usage_msg =
sprintf "\
%s: graphical virtual machine disk usage viewer
Options:"
(Filename.basename Sys.executable_name)
let main () =
Arg.parse argspec anon_fun usage_msg;
(* Start up the slave thread. *)
let slave = Thread.create Slave.slave_loop () in
(* Set up the display. *)
let ds = main_window opened_domain repaint in
Slave.set_failure_callback (failure ds);
Slave.set_busy_callback (busy ds);
let uri = match !default_uri with "" -> None | s -> Some s in
Slave.connect uri (connected ds);
(* Run the main thread. When this returns, the application has been closed. *)
GtkThread.main ();
(* Tell the slave thread to exit and wait for it to do so. *)
Slave.exit_thread ();
Thread.join slave
let () =
main ()