74ce2175280c829587057dc71f33506a4ce8ec07
[guestfs-browser.git] / slave.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 ExtList
20 open Printf
21 open Utils
22
23 module C = Libvirt.Connect
24 module Cond = Condition
25 module D = Libvirt.Domain
26 module M = Mutex
27 module Q = Queue
28
29 type 'a callback = 'a -> unit
30
31 (* The commands. *)
32 type command =
33   | Exit_thread
34   | Connect of string option * domain list callback
35   | Open_domain of string * inspection_data callback
36   | Open_images of string list * inspection_data callback
37   | Read_directory of source * string * direntry list callback
38
39 and domain = {
40   dom_id : int;
41   dom_name : string;
42   dom_state : D.state;
43 }
44
45 and inspection_data = {
46   insp_all_filesystems : (string * string) list;
47   insp_oses : inspection_os list;
48 }
49
50 and inspection_os = {
51   insp_root : string;
52   insp_arch : string;
53   insp_distro : string;
54   insp_filesystems : string array;
55   insp_hostname : string;
56   insp_major_version : int;
57   insp_minor_version : int;
58   insp_mountpoints : (string * string) list;
59   insp_package_format : string;
60   insp_package_management : string;
61   insp_product_name : string;
62   insp_type : string;
63   insp_windows_systemroot : string option;
64 }
65
66 and source = OS of inspection_os | Volume of string
67
68 and direntry = {
69   dent_name : string;
70   dent_stat : Guestfs.stat;
71   dent_link : string;
72 }
73
74 let rec string_of_command = function
75   | Exit_thread -> "Exit_thread"
76   | Connect (Some name, _) -> sprintf "Connect %s" name
77   | Connect (None, _) -> "Connect NULL"
78   | Open_domain (name, _) -> sprintf "Open_domain %s" name
79   | Open_images (images, _) ->
80       sprintf "Open_images [%s]" (String.concat "; " images)
81   | Read_directory (OS { insp_root = root }, dir, _) ->
82       sprintf "Read_directory (OS %s, %s)" root dir
83   | Read_directory (Volume dev, dir, _) ->
84       sprintf "Read_directory (Volume %s, %s)" dev dir
85
86 let no_callback _ = ()
87
88 let failure_hook = ref (fun _ -> ())
89 let busy_hook = ref (fun _ -> ())
90 let idle_hook = ref (fun _ -> ())
91
92 let set_failure_hook cb = failure_hook := cb
93 let set_busy_hook cb = busy_hook := cb
94 let set_idle_hook cb = idle_hook := cb
95
96 (* Execute a function, while holding a mutex.  If the function
97  * fails, ensure we release the mutex before rethrowing the
98  * exception.
99  *)
100 let with_lock m f =
101   M.lock m;
102   let r = try Left (f ()) with exn -> Right exn in
103   M.unlock m;
104   match r with
105   | Left r -> r
106   | Right exn -> raise exn
107
108 (* The queue of commands, and a lock and condition to protect it. *)
109 let q = Q.create ()
110 let q_discard = ref false
111 let q_lock = M.create ()
112 let q_cond = Cond.create ()
113
114 (* Send a command message to the slave thread. *)
115 let send_to_slave ?fail cmd =
116   debug "sending message %s to slave thread ..." (string_of_command cmd);
117   with_lock q_lock (
118     fun () ->
119       Q.push (fail, cmd) q;
120       Cond.signal q_cond
121   )
122
123 let discard_command_queue () =
124   with_lock q_lock (
125     fun () ->
126       Q.clear q;
127       (* Discard the currently running command. *)
128       q_discard := true
129   )
130
131 let connect ?fail uri cb = send_to_slave ?fail (Connect (uri, cb))
132 let open_domain ?fail name cb = send_to_slave ?fail (Open_domain (name, cb))
133 let open_images ?fail images cb = send_to_slave ?fail (Open_images (images, cb))
134 let read_directory ?fail src path cb =
135   send_to_slave ?fail (Read_directory (src, path, cb))
136
137 (*----- Slave thread starts here -----*)
138
139 (* Set this to true to exit the thread. *)
140 let quit = ref false
141
142 (* Handles.  These are not protected by locks because only the slave
143  * thread has access to them.
144  *)
145 let conn = ref None
146 let g = ref None
147
148 (* Run the callback unless someone set the q_discard flag while
149  * we were running the command.
150  *)
151 let callback_if_not_discarded (cb : 'a callback) (arg : 'a) =
152   let discard = with_lock q_lock (fun () -> !q_discard) in
153   if not discard then
154     GtkThread.async cb arg
155
156 (* Call 'f ()' with source mounted read-only.  Ensure that everything
157  * is unmounted even if an exception is thrown.
158  *)
159 let with_mount_ro g src (f : unit -> 'a) : 'a =
160   Std.finally (fun () -> g#umount_all ()) (
161     fun () ->
162       (* Do the mount - could be OS or single volume. *)
163       (match src with
164       | Volume dev -> g#mount_ro dev "/";
165       | OS { insp_mountpoints = mps } ->
166           (* Sort the mountpoint keys by length, shortest first. *)
167           let cmp (a,_) (b,_) = compare (String.length a) (String.length b) in
168           let mps = List.sort ~cmp mps in
169           (* Mount the filesystems. *)
170           List.iter (
171             fun (mp, dev) -> g#mount_ro dev mp
172           ) mps
173       );
174       f ()
175   ) ()
176
177 let rec loop () =
178   debug "top of slave loop";
179
180   (* Get the next command. *)
181   let fail, cmd =
182     with_lock q_lock (
183       fun () ->
184         while Q.is_empty q do Cond.wait q_cond q_lock done;
185         q_discard := false;
186         Q.pop q
187     ) in
188
189   debug "slave processing command %s ..." (string_of_command cmd);
190
191   (try
192      GtkThread.async !busy_hook ();
193      execute_command cmd
194    with exn ->
195      (* If the user provided an override ?fail parameter to the
196       * original call, call that, else call the global hook.
197       *)
198      match fail with
199      | Some cb -> GtkThread.async cb exn
200      | None -> GtkThread.async !failure_hook exn
201   );
202
203   (* If there are no more commands in the queue, run the idle hook. *)
204   let empty = with_lock q_lock (fun () -> Q.is_empty q) in
205   if empty then GtkThread.async !idle_hook ();
206
207   if !quit then Thread.exit ();
208   loop ()
209
210 and execute_command = function
211   | Exit_thread ->
212       quit := true;
213       close_all ()
214
215   | Connect (name, cb) ->
216       close_all ();
217       conn := Some (C.connect_readonly ?name ());
218
219       let conn = get_conn () in
220       let doms = D.get_domains conn [D.ListAll] in
221       let doms = List.map (
222         fun d ->
223           { dom_id = D.get_id d;
224             dom_name = D.get_name d;
225             dom_state = (D.get_info d).D.state }
226       ) doms in
227       let cmp { dom_name = n1 } { dom_name = n2 } = compare n1 n2 in
228       let doms = List.sort ~cmp doms in
229       callback_if_not_discarded cb doms
230
231   | Open_domain (name, cb) ->
232       let conn = get_conn () in
233       let dom = D.lookup_by_name conn name in
234       let xml = D.get_xml_desc dom in
235       let images = get_disk_images_from_xml xml in
236       open_disk_images images cb
237
238   | Open_images (images, cb) ->
239       open_disk_images images cb
240
241   | Read_directory (src, dir, cb) ->
242       let g = get_g () in
243       let names, stats, links =
244         with_mount_ro g src (
245           fun () ->
246             let names = g#ls dir in (* sorted and without . and .. *)
247             let names = Array.to_list names in
248             let stats = lstatlist_wrapper g dir names in
249             let links = readlinklist_wrapper g dir names in
250             names, stats, links
251         ) in
252       assert (
253         let n = List.length names in
254         n = List.length stats && n = List.length links
255       );
256       let entries = List.combine (List.combine names stats) links in
257       let entries = List.map (
258         fun ((name, stat), link) ->
259           { dent_name = name; dent_stat = stat; dent_link = link }
260       ) entries in
261       callback_if_not_discarded cb entries
262
263 (* Expect to be connected, and return the current libvirt connection. *)
264 and get_conn () =
265   match !conn with
266   | Some conn -> conn
267   | None -> failwith "not connected to libvirt"
268
269 and get_g () =
270   match !g with
271   | Some g -> g
272   | None -> failwith "no domain or disk image is open"
273
274 (* Close all libvirt and libguestfs handles. *)
275 and close_all () =
276   (match !conn with Some conn -> C.close conn | None -> ());
277   conn := None;
278   close_g ()
279
280 and close_g () =
281   (match !g with Some g -> g#close () | None -> ());
282   g := None
283
284 and get_disk_images_from_xml xml =
285   let xml = Xml.parse_string xml in
286
287   (* Return the device nodes. *)
288   let devices =
289     match xml with
290     | Xml.Element ("domain", _, children) ->
291         let devices =
292           List.filter_map (
293             function
294             | Xml.Element ("devices", _, devices) -> Some devices
295             | _ -> None
296           ) children in
297         List.concat devices
298     | _ ->
299         failwith "get_xml_desc didn't return <domain/>" in
300
301   (* Look for <source attr_name=attr_val/> and return attr_val. *)
302   let rec source_of attr_name = function
303     | [] -> None
304     | Xml.Element ("source", attrs, _) :: rest ->
305         (try Some (List.assoc attr_name attrs)
306          with Not_found -> source_of attr_name rest)
307     | _ :: rest -> source_of attr_name rest
308   in
309
310   (* Look for <disk> nodes and return the sources (block devices) of those. *)
311   let blkdevs =
312     List.filter_map (
313       function
314       | Xml.Element ("disk", attrs, children) ->
315           (try
316              let typ = List.assoc "type" attrs in
317              if typ = "file" then source_of "file" children
318              else if typ = "block" then source_of "dev" children
319              else None
320            with
321              Not_found -> None)
322       | _ -> None
323     ) devices in
324   blkdevs
325
326 (* The common code for Open_domain and Open_images which opens the
327  * libguestfs handle, adds the disks, and launches the appliance.
328  *)
329 and open_disk_images images cb =
330   debug "opening disk image [%s]" (String.concat "; " images);
331
332   close_g ();
333   let g' = new Guestfs.guestfs () in
334   g := Some g';
335   let g = g' in
336
337   (* Uncomment the next line to pass the verbose flag from the command
338    * line through to libguestfs.  This is not generally necessary since
339    * we are not so interested in debugging libguestfs problems at this
340    * level, and the user can always set LIBGUESTFS_DEBUG=1 if they need
341    * to.
342    *)
343   (* g#set_verbose (verbose ());*)
344
345   List.iter g#add_drive_ro images;
346
347   g#launch ();
348
349   (* Get list of filesystems. *)
350   let fses = g#list_filesystems () in
351
352   (* Perform inspection.  This can fail, ignore errors. *)
353   let roots =
354     try Array.to_list (g#inspect_os ())
355     with
356       Guestfs.Error msg ->
357         debug "inspection failed (error ignored): %s" msg;
358         [] in
359
360   let oses = List.map (
361     fun root -> {
362       insp_root = root;
363       insp_arch = g#inspect_get_arch root;
364       insp_distro = g#inspect_get_distro root;
365       insp_filesystems = g#inspect_get_filesystems root;
366       insp_hostname = g#inspect_get_hostname root;
367       insp_major_version = g#inspect_get_major_version root;
368       insp_minor_version = g#inspect_get_minor_version root;
369       insp_mountpoints = g#inspect_get_mountpoints root;
370       insp_package_format = g#inspect_get_package_format root;
371       insp_package_management = g#inspect_get_package_management root;
372       insp_product_name = g#inspect_get_product_name root;
373       insp_type = g#inspect_get_type root;
374       insp_windows_systemroot =
375         try Some (g#inspect_get_windows_systemroot root)
376         with Guestfs.Error _ -> None
377     }
378   ) roots in
379   let data = {
380     insp_all_filesystems = fses;
381     insp_oses = oses;
382   } in
383   callback_if_not_discarded cb data
384
385 (* guestfs_lstatlist has a "hidden" limit of the protocol message size.
386  * Call this function, but split the list of names into chunks.
387  *)
388 and lstatlist_wrapper g dir = function
389   | [] -> []
390   | names ->
391       let names', names = List.take 1000 names, List.drop 1000 names in
392       let xs = g#lstatlist dir (Array.of_list names') in
393       let xs = Array.to_list xs in
394       xs @ lstatlist_wrapper g dir names
395
396 (* Same as above for guestfs_readlinklist. *)
397 and readlinklist_wrapper g dir = function
398   | [] -> []
399   | names ->
400       let names', names = List.take 1000 names, List.drop 1000 names in
401       let xs = g#readlinklist dir (Array.of_list names') in
402       let xs = Array.to_list xs in
403       xs @ readlinklist_wrapper g dir names
404
405 (* Start up one slave thread. *)
406 let slave_thread = Thread.create loop ()
407
408 (* Note the following function is called from the main thread. *)
409 let exit_thread () =
410   discard_command_queue ();
411   ignore (send_to_slave Exit_thread);
412   Thread.join slave_thread