File: forkhelpers.ml

package info (click to toggle)
xen-api-libs 0.5.2-3
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 1,940 kB
  • sloc: ml: 13,925; sh: 2,930; ansic: 1,699; makefile: 1,240; python: 83
file content (194 lines) | stat: -rw-r--r-- 7,459 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
(*
 * Copyright (C) 2006-2009 Citrix Systems Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation; version 2.1 only. with the special
 * exception on linking described in file LICENSE.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *)
(* Functions to safely fork potentially long-running sub-processes without
   leaking file descriptors or accidentally deadlocking the parent process. *)

(* Functions should:
   1. Arrange to close all fds except the ones they actually want to keep open
   2. Not access any ocaml library or runtime function which might touch a lock
   (since that would cause deadlock) *)

(* XXX: this is a work in progress *)

let default_path = [ "/sbin"; "/usr/sbin"; "/bin"; "/usr/bin" ]

open Pervasiveext

type pidty = (Unix.file_descr * int) (* The forking executioner has been used, therefore we need to tell *it* to waitpid *)

let string_of_pidty (fd, pid) = Printf.sprintf "(FEFork (%d,%d))" (Unixext.int_of_file_descr fd) pid

exception Subprocess_failed of int
exception Subprocess_killed of int

let waitpid (sock, pid) =
	let status = Fecomms.read_raw_rpc sock in
	Unix.close sock;
	begin match status with
	  | Fe.Finished (Fe.WEXITED n) -> (pid,Unix.WEXITED n)
	  | Fe.Finished (Fe.WSIGNALED n) -> (pid,Unix.WSIGNALED n)
	  | Fe.Finished (Fe.WSTOPPED n) -> (pid,Unix.WSTOPPED n)
	end

let waitpid_nohang ((sock, _) as x) =
	(match Unix.select [sock] [] [] 0.0 with
	  | ([s],_,_) -> waitpid x
	  | _ -> (0,Unix.WEXITED 0))
	  
let dontwaitpid (sock, pid) =
	Unix.close sock


let waitpid_fail_if_bad_exit ty =
  let (_,status) = waitpid ty in
  match status with
    | (Unix.WEXITED 0) -> ()
    | (Unix.WEXITED n) -> raise (Subprocess_failed n)
    | (Unix.WSIGNALED n) -> raise (Subprocess_killed n)
    | (Unix.WSTOPPED n) -> raise (Subprocess_killed n)

let getpid (sock, pid) = pid

type 'a result = Success of string * 'a | Failure of string * exn

(** Creates a temporary file and opens it for logging. The fd is passed to the function
    'f'. The logfile is guaranteed to be closed afterwards, and unlinked if either the delete flag is set or the call fails. If the
    function 'f' throws an error then the log file contents are read in *)
let with_logfile_fd ?(delete = true) prefix f = 
  let logfile = Filename.temp_file prefix ".log" in
  let read_logfile () = 
    let contents = Unixext.string_of_file logfile in
    Unix.unlink logfile;
    contents in

  let log_fd = Unix.openfile logfile [ Unix.O_WRONLY; Unix.O_CREAT ] 0o0 in
  try
    let result = f log_fd in
    Unix.close log_fd;
    Success((if delete then read_logfile() else logfile), result)
  with e ->
    Unix.close log_fd;
    Failure(read_logfile(), e)


exception Spawn_internal_error of string * string * Unix.process_status

let id = ref 0 

type syslog_stdout_t =
  | NoSyslogging
  | Syslog_DefaultKey
  | Syslog_WithKey of string

(** Safe function which forks a command, closing all fds except a whitelist and
    having performed some fd operations in the child *)
let safe_close_and_exec ?env stdin stdout stderr (fds: (string * Unix.file_descr) list) ?(syslog_stdout=NoSyslogging)
    (cmd: string) (args: string list) =

  let sock = Fecomms.open_unix_domain_sock_client "/var/lib/xcp/forker/main" in
  let stdinuuid = Uuid.to_string (Uuid.make_uuid ()) in
  let stdoutuuid = Uuid.to_string (Uuid.make_uuid ()) in
  let stderruuid = Uuid.to_string (Uuid.make_uuid ()) in

  let fds_to_close = ref [] in

  let add_fd_to_close_list fd = fds_to_close := fd :: !fds_to_close in
  let remove_fd_from_close_list fd = fds_to_close := List.filter (fun fd' -> fd' <> fd) !fds_to_close in
  let close_fds () = List.iter (fun fd -> Unix.close fd) !fds_to_close in

  finally (fun () -> 

    let maybe_add_id_to_fd_map id_to_fd_map (uuid,fd,v) =
      match v with 
	| Some _ -> (uuid, fd)::id_to_fd_map
	| None -> id_to_fd_map
    in

    let predefined_fds = [
      (stdinuuid, Some 0, stdin);
      (stdoutuuid, Some 1, stdout);
      (stderruuid, Some 2, stderr)] 
    in

    (* We don't care what fd these end up as - they're named in the argument list for us, and the
       forking executioner will sort it out. *)
    let dest_named_fds = List.map (fun (uuid,_) -> (uuid,None)) fds in
    let id_to_fd_map = List.fold_left maybe_add_id_to_fd_map dest_named_fds predefined_fds in

    let env = match env with 
      |	Some e -> e
      | None -> [| "PATH=" ^ (String.concat ":" default_path) |]
    in
    let syslog_stdout = match syslog_stdout with
      | NoSyslogging -> {Fe.enabled=false; Fe.key=None}
      | Syslog_DefaultKey -> {Fe.enabled=true; Fe.key=None}
      | Syslog_WithKey k -> {Fe.enabled=true; Fe.key=Some k}
    in
    Fecomms.write_raw_rpc sock (Fe.Setup {Fe.cmdargs=(cmd::args); env=(Array.to_list env); id_to_fd_map = id_to_fd_map; syslog_stdout = syslog_stdout});

    let response = Fecomms.read_raw_rpc sock in

    let s = match response with
      | Fe.Setup_response s -> s 
      | _ -> failwith "Failed to communicate with forking executioner"
    in

    let fd_sock = Fecomms.open_unix_domain_sock_client s.Fe.fd_sock_path in
    add_fd_to_close_list fd_sock;
    
    let send_named_fd uuid fd =
      Fecomms.send_named_fd fd_sock uuid fd;
    in

    List.iter (fun (uuid,_,srcfdo) ->
      match srcfdo with Some srcfd -> send_named_fd uuid srcfd | None -> ()) predefined_fds;
    List.iter (fun (uuid,srcfd) ->
      send_named_fd uuid srcfd) fds;
    Fecomms.write_raw_rpc sock Fe.Exec;
    match Fecomms.read_raw_rpc sock with Fe.Execed pid -> (sock, pid))
   
    close_fds


let execute_command_get_output_inner ?env ?stdin ?(syslog_stdout=NoSyslogging) cmd args =
	let stdinandpipes = Opt.map (fun str -> 
		let (x,y) = Unix.pipe () in
		(str,x,y)) stdin in
	Pervasiveext.finally (fun () -> 
		match with_logfile_fd "execute_command_get_out" (fun out_fd ->
			with_logfile_fd "execute_command_get_err" (fun err_fd ->
				let (sock,pid) = safe_close_and_exec ?env (Opt.map (fun (_,fd,_) -> fd) stdinandpipes) (Some out_fd) (Some err_fd) [] ~syslog_stdout cmd args in
				Opt.map (fun (str,_,wr) -> Unixext.really_write_string wr str) stdinandpipes;
				match Fecomms.read_raw_rpc sock with
					| Fe.Finished x -> Unix.close sock; x
					| _ -> Unix.close sock; failwith "Communications error"	    
			)) with
			| Success(out,Success(err,(status))) -> 
				begin
					match status with
						| Fe.WEXITED 0 -> (out,err)
						| Fe.WEXITED n -> raise (Spawn_internal_error(err,out,Unix.WEXITED n))
						| Fe.WSTOPPED n -> raise (Spawn_internal_error(err,out,Unix.WSTOPPED n))
						| Fe.WSIGNALED n -> raise (Spawn_internal_error(err,out,Unix.WSIGNALED n))
				end
			| Success(_,Failure(_,exn))
			| Failure(_, exn) ->
				raise exn)
		(fun () -> Opt.iter (fun (_,x,y) -> Unix.close x; Unix.close y) stdinandpipes)

let execute_command_get_output ?env ?(syslog_stdout=NoSyslogging) cmd args =
	execute_command_get_output_inner ?env ?stdin:None ~syslog_stdout cmd args

let execute_command_get_output_send_stdin ?env ?(syslog_stdout=NoSyslogging) cmd args stdin =
	execute_command_get_output_inner ?env ~stdin ~syslog_stdout cmd args