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