(* $Id: shell.ml 50 2004-10-03 17:06:28Z gerd $
 * ----------------------------------------------------------------------
 *
 *)

let is_win32 =
  match Sys.os_type with
    | "Win32" -> true
    | _ -> false;;


let dev_null_name =
  if is_win32 then
    "NUL"
  else
    "/dev/null"



let stdin  = Unix.stdin;;
let stdout = Unix.stdout;;
let stderr = Unix.stderr;;

type assignment = Unix.file_descr * Unix.file_descr;;

let assign ~src ~target = (target,src);;
let ( >& ) src target = (target,src);;
let ( <& ) src target = (target,src);;
let assigned_pair (target,src) = (target,src);;

let command
      ?cmdname 
      ?arguments
      ?chdir
      ?environment
      ?descriptors
      ?assignments
      name =
  let filename = Shell_sys.lookup_executable name in
  Shell_sys.command
    ?cmdname
    ?arguments
    ?chdir
    ?environment
    ?descriptors
    ?assignments
    ~filename
    ()
;;


let cmd 
      ?cmdname 
      ?chdir
      ?environment
      ?descriptors
      ?assignments
      name args =
  command 
    ?cmdname
    ?chdir
    ?environment
    ?descriptors
    ?assignments
    ~arguments:(Array.of_list args)
    name
;;


exception Subprocess_error of (string * Unix.process_status) list;;

let () =
  Netexn.register_printer
    (Subprocess_error [])
    (fun e ->
       match e with
	 | Subprocess_error l ->
	     "Shell.Subprocess_error( [" ^ 
	       (String.concat "; " 
		  (List.map
		     (fun (cmd, ps) ->
			"\"" ^ String.escaped cmd ^ "\": " ^ 
			  match ps with
			    | Unix.WEXITED n -> 
				"WEXITED " ^ string_of_int n
			    | Unix.WSIGNALED n -> 
				"WSIGNALED " ^ string_of_int n
			    | Unix.WSTOPPED n ->
				"WSTOPPED " ^ string_of_int n
		     )
		     l)) ^ "] )"
	 | _ ->
	     assert false
    )

type consumer = 
    C_fun of (Unix.file_descr -> bool)
  | C_file of (bool * string)
  | C_fd of Unix.file_descr
  | C_dev_null
  | C_none

type producer = 
    P_fun of (Unix.file_descr -> bool)
  | P_file of string
  | P_fd of Unix.file_descr
  | P_dev_null
  | P_none


let from_string ?pos ?len ?epipe s =
  P_fun (Shell_sys.from_string ?pos:pos ?len:len ?epipe:epipe s);;

let from_stream ?epipe s =
  P_fun (Shell_sys.from_stream ?epipe:epipe s);;

let from_function ~producer () =
  P_fun producer;;

let from_file name =
  P_file name;;

let from_fd fd =
  P_fd fd;;

let from_dev_null =
  P_dev_null;;

let to_buffer b =
  C_fun (Shell_sys.to_buffer b);;

let to_function ~consumer () =
  C_fun consumer;;

let to_file ?(append = false) name =
  C_file (append,name);;

let to_fd fd =
  C_fd fd;;

let to_dev_null =
  C_dev_null;;


let rec iter_pairs f l =
  match l with
      [] -> ()
    | [ c ] -> ()
    | c1 :: c2 :: l' ->
	f c1 c2;
	iter_pairs f (c2 :: l')
;;

let rec last l =
  match l with
      [] -> failwith "last"
    | [ x ] -> x
    | x::l' -> last l'
;;

let rec change_last x' l =
  match l with
      [] -> failwith "change_last"
    | [ x ] -> [ x' ]
    | x::l' -> x :: change_last x' l'
;;


let try_to_install_job_handlers() =
  try
    Shell_sys.install_job_handlers()
  with
      Shell_sys.Already_installed -> ()
;;


let setup_job 
      ?(stdin = P_none)
      ?(stdout = C_none)
      ?(stderr = C_none)
      commands =

  let files_to_close = ref [] in

  let cleanup() =
    try
      List.iter Unix.close !files_to_close;
      files_to_close := []
    with
	ex ->
	  raise (Shell_sys.Fatal_error ex)
  in

  if commands = [] then invalid_arg "Shell.setup_job";

  try
    (* Is /dev/null needed? *) (* TODO *)

    let dev_null = ref Unix.stdin in
    if stdin = P_dev_null || stdout = C_dev_null || stderr = C_dev_null then 
    begin
      dev_null := Unix.openfile dev_null_name [ Unix.O_RDWR ] 0;
      files_to_close := !dev_null :: !files_to_close;
    end;

    (* If there is redirection from a file, change the first command: *)
    
    let new_stdin =
      match stdin with
	  P_fd fd ->
	    Some fd
	| P_file name ->
	    let fd = Unix.openfile name [ Unix.O_RDONLY ] 0 in
	    files_to_close := fd :: !files_to_close;
	    Some fd
	| P_dev_null ->
	    Some (!dev_null)
	| _ ->
	    None
    in
    
    let new_first_cmd =
      match new_stdin with
	  Some fd ->
	    let c = Shell_sys.copy_command (List.hd commands) in
	    Shell_sys.set_assignments 
	      c
	      ( (fd, Unix.stdin) :: (Shell_sys.get_assignments c) );
	    c
	| None ->
	    List.hd commands
    in
    
    let commands' = new_first_cmd :: (List.tl commands) in
    
    (* If there is stdout redirection to a file, change the last command: *)

    let mk_new_out out =
      match out with
	  C_fd fd ->
	    Some fd
	| C_file (append,name) ->
	    let app_flags = 
              if append then [ Unix.O_APPEND ] else [ Unix.O_TRUNC ] in
	    let std_flags = [ Unix.O_WRONLY; Unix.O_CREAT ] in
	    let fd = Unix.openfile name (app_flags @ std_flags) 0o777 in
	    files_to_close := fd :: !files_to_close;
	    Some fd
	| C_dev_null ->
	    Some (!dev_null)
	| _ ->
	    None
    in
    
    let new_last_cmd =
      match mk_new_out stdout with
	  Some fd ->
	    let c = Shell_sys.copy_command (last commands') in
	    Shell_sys.set_assignments 
	      c
	      ( (fd, Unix.stdout) :: (Shell_sys.get_assignments c) );
	    c
	| None ->
	    last commands'
    in
    
    let commands'' = change_last new_last_cmd commands' in
    
    (* If there is stderr redirection to a file, change all commands: *)

    let commands''' =
      match mk_new_out stderr with
	  Some fd ->
	    List.map
	      (fun c ->
		 let c' = Shell_sys.copy_command c in
		 Shell_sys.set_assignments 
		   c'
		   ( (fd, Unix.stderr) :: (Shell_sys.get_assignments c') );
		 c'
	      )
	      commands''
	| None ->
	    commands''
    in
    
    (* Set up the job: *)

    let j = Shell_sys.new_job () in
    
    List.iter
      (fun c -> Shell_sys.add_command c j)
      commands''';
    
    iter_pairs
      (fun c1 c2 -> Shell_sys.add_pipeline ~src:c1 ~dest:c2 j)
      commands''';

    begin match stdin with
	P_fun p ->
	  let first_cmd = List.hd commands''' in
	  Shell_sys.add_producer ~producer:p first_cmd j
      | _ ->
	  ()
    end;

    begin match stdout with
	C_fun c ->
	  let last_cmd = last commands''' in
	  Shell_sys.add_consumer ~consumer:c last_cmd j
      | _ -> 
	  ()
    end;

    begin match stderr with
	C_fun c ->
	  List.iter
	    (fun cmd ->
	       Shell_sys.add_consumer ~descr:Unix.stderr ~consumer:c cmd j)
	    commands'''
      | _ -> 
	  ()
    end;
    
    (j, !files_to_close)
  with
    | ex ->
	cleanup();
	raise ex
;;


let postprocess_job
      ?(ignore_error_code = false)
      ji =

  if Shell_sys.job_status ji <> Shell_sys.Job_ok then begin
    let can_ignore =
      List.for_all
	(fun p ->
	   match Shell_sys.status p with
	       Unix.WEXITED _ -> true
	     | _              -> false
	)
	(Shell_sys.processes ji)
    in
    
    if not (ignore_error_code && not can_ignore) then begin
      (* Error cannot be ignored, throw a Subprocess_error: *)
      let l =
	List.map
	  (fun p ->
	     let s = Shell_sys.status p in
	     let c = Shell_sys.command_of_process p in
	     let name = Shell_sys.get_filename c in
	     name, s
	  )
	  (Shell_sys.processes ji)
      in
      raise (Subprocess_error l)
    end
  end
;;


let call 
      ?ignore_error_code
      ?mode
      ?stdin
      ?stdout
      ?stderr
      commands =

  let files_to_close = ref [] in

  let cleanup() =
    try
      List.iter Unix.close !files_to_close;
      files_to_close := []
    with
	ex ->
	  raise (Shell_sys.Fatal_error ex)
  in

  if commands = [] then invalid_arg "Shell.call";

  try_to_install_job_handlers();

  let (j, files) = setup_job ?stdin ?stdout ?stderr commands in
  files_to_close := files;

  let ji =
    try
      (* Invoke the job: *)
      let ji = Shell_sys.call_job ?mode:mode j in
      cleanup();           (* Close files no longer needed *)
      ji
    with
	(Shell_sys.Fatal_error _) as ex ->
	  (* We do not try to cleanup *)
	  raise ex
      | ex ->
	  cleanup();
	  raise ex
  in

  (* Interpret the result: *)
  postprocess_job ?ignore_error_code ji  (* may raise Subprocess_error *)
;;
