Blame browser/shell.ml

Packit bd2e5d
(*************************************************************************)
Packit bd2e5d
(*                                                                       *)
Packit bd2e5d
(*                         OCaml LablTk library                          *)
Packit bd2e5d
(*                                                                       *)
Packit bd2e5d
(*            Jacques Garrigue, Kyoto University RIMS                    *)
Packit bd2e5d
(*                                                                       *)
Packit bd2e5d
(*   Copyright 1999 Institut National de Recherche en Informatique et    *)
Packit bd2e5d
(*   en Automatique and Kyoto University.  All rights reserved.          *)
Packit bd2e5d
(*   This file is distributed under the terms of the GNU Library         *)
Packit bd2e5d
(*   General Public License, with the special exception on linking       *)
Packit bd2e5d
(*   described in file ../../../LICENSE.                                 *)
Packit bd2e5d
(*                                                                       *)
Packit bd2e5d
(*************************************************************************)
Packit bd2e5d
Packit bd2e5d
(* $Id$ *)
Packit bd2e5d
Packit bd2e5d
open StdLabels
Packit bd2e5d
module Unix = UnixLabels
Packit bd2e5d
open Tk
Packit bd2e5d
open Jg_tk
Packit bd2e5d
open Dummy
Packit bd2e5d
Packit bd2e5d
(* Here again, memoize regexps *)
Packit bd2e5d
Packit bd2e5d
let (~!) = Jg_memo.fast ~f:Str.regexp
Packit bd2e5d
Packit bd2e5d
(* Nice history class. May reuse *)
Packit bd2e5d
Packit bd2e5d
class ['a] history () = object
Packit bd2e5d
  val mutable history = ([] : 'a list)
Packit bd2e5d
  val mutable count = 0
Packit bd2e5d
  method empty = history = []
Packit bd2e5d
  method add s = count <- 0; history <- s :: history
Packit bd2e5d
  method previous =
Packit bd2e5d
    let s = List.nth history count in
Packit bd2e5d
    count <- (count + 1) mod List.length history;
Packit bd2e5d
    s
Packit bd2e5d
  method next =
Packit bd2e5d
    let l = List.length history in
Packit bd2e5d
    count <- (l + count - 1) mod l;
Packit bd2e5d
    List.nth history ((l + count - 1) mod l)
Packit bd2e5d
end
Packit bd2e5d
Packit bd2e5d
let dump_handle (h : Unix.file_descr) =
Packit bd2e5d
  let obj = Obj.repr h in
Packit bd2e5d
  if Obj.is_int obj || Obj.tag obj <> Obj.custom_tag then
Packit bd2e5d
    invalid_arg "Shell.dump_handle";
Packit bd2e5d
  Nativeint.format "%x" (Obj.obj obj)
Packit bd2e5d
Packit bd2e5d
(* The shell class. Now encapsulated *)
Packit bd2e5d
Packit bd2e5d
let protect f x = try f x with _ -> ()
Packit bd2e5d
Packit bd2e5d
let is_win32 = Sys.os_type = "Win32"
Packit bd2e5d
let use_threads = is_win32
Packit bd2e5d
let use_sigpipe = is_win32
Packit bd2e5d
Packit bd2e5d
class shell ~textw ~prog ~args ~env ~history =
Packit bd2e5d
  let (in2,out1) = Unix.pipe ()
Packit bd2e5d
  and (in1,out2) = Unix.pipe ()
Packit bd2e5d
  and (err1,err2) = Unix.pipe ()
Packit bd2e5d
  and (sig2,sig1) = Unix.pipe () in
Packit bd2e5d
object (self)
Packit bd2e5d
  val pid =
Packit bd2e5d
    let env =
Packit bd2e5d
      if use_sigpipe then
Packit bd2e5d
        let sigdef = "CAMLSIGPIPE=" ^ dump_handle sig2 in
Packit bd2e5d
        Array.append env [|sigdef|]
Packit bd2e5d
      else env
Packit bd2e5d
    in
Packit bd2e5d
    Unix.create_process_env ~prog ~args ~env
Packit bd2e5d
      ~stdin:in2 ~stdout:out2 ~stderr:err2
Packit bd2e5d
  val out = Unix.out_channel_of_descr out1
Packit bd2e5d
  val h : _ history = history
Packit bd2e5d
  val mutable alive = true
Packit bd2e5d
  val mutable reading = false
Packit bd2e5d
  val ibuffer = Buffer.create 1024
Packit bd2e5d
  val imutex = Mutex.create ()
Packit bd2e5d
  val mutable ithreads = []
Packit bd2e5d
  method alive = alive
Packit bd2e5d
  method kill =
Packit bd2e5d
    if Winfo.exists textw then Text.configure textw ~state:`Disabled;
Packit bd2e5d
    if alive then begin
Packit bd2e5d
      alive <- false;
Packit bd2e5d
      protect close_out out;
Packit bd2e5d
      try
Packit bd2e5d
        if use_sigpipe then
Packit bd2e5d
          ignore (Unix.write sig1 ~buf:(Bytes.make 1 'T') ~pos:0 ~len:1);
Packit bd2e5d
        List.iter ~f:(protect Unix.close) [in1; err1; sig1; sig2];
Packit bd2e5d
        if not use_threads then begin
Packit bd2e5d
          Fileevent.remove_fileinput ~fd:in1;
Packit bd2e5d
          Fileevent.remove_fileinput ~fd:err1;
Packit bd2e5d
        end;
Packit bd2e5d
        if not use_sigpipe then begin
Packit bd2e5d
          Unix.kill ~pid ~signal:Sys.sigkill;
Packit bd2e5d
          ignore (Unix.waitpid ~mode:[] pid)
Packit bd2e5d
        end
Packit bd2e5d
      with _ -> ()
Packit bd2e5d
    end
Packit bd2e5d
  method interrupt =
Packit bd2e5d
    if alive then try
Packit bd2e5d
      reading <- false;
Packit bd2e5d
      if use_sigpipe then begin
Packit bd2e5d
        ignore (Unix.write sig1 ~buf:(Bytes.make 1 'C') ~pos:0 ~len:1);
Packit bd2e5d
        self#send " "
Packit bd2e5d
      end else
Packit bd2e5d
        Unix.kill ~pid ~signal:Sys.sigint
Packit bd2e5d
    with Unix.Unix_error _ -> ()
Packit bd2e5d
  method send s =
Packit bd2e5d
    if alive then try
Packit bd2e5d
      output_string out s;
Packit bd2e5d
      flush out
Packit bd2e5d
    with Sys_error _ -> ()
Packit bd2e5d
  method private read ~fd ~len =
Packit bd2e5d
    begin try
Packit bd2e5d
      let buf = Bytes.create len in
Packit bd2e5d
      let len = Unix.read fd ~buf ~pos:0 ~len in
Packit bd2e5d
      if len > 0 then begin
Packit bd2e5d
        self#insert (Bytes.sub_string buf ~pos:0 ~len);
Packit bd2e5d
        Text.mark_set textw ~mark:"input" ~index:(`Mark"insert",[`Char(-1)])
Packit bd2e5d
      end;
Packit bd2e5d
      len
Packit bd2e5d
    with Unix.Unix_error _ -> 0
Packit bd2e5d
    end;
Packit bd2e5d
  method history (dir : [`Next|`Previous]) =
Packit bd2e5d
    if not h#empty then begin
Packit bd2e5d
      if reading then begin
Packit bd2e5d
        Text.delete textw ~start:(`Mark"input",[`Char 1])
Packit bd2e5d
          ~stop:(`Mark"insert",[])
Packit bd2e5d
      end else begin
Packit bd2e5d
        reading <- true;
Packit bd2e5d
        Text.mark_set textw ~mark:"input"
Packit bd2e5d
          ~index:(`Mark"insert",[`Char(-1)])
Packit bd2e5d
      end;
Packit bd2e5d
      self#insert (if dir = `Previous then h#previous else h#next)
Packit bd2e5d
    end
Packit bd2e5d
  method private lex ?(start = `Mark"insert",[`Linestart])
Packit bd2e5d
      ?(stop = `Mark"insert",[`Lineend]) () =
Packit bd2e5d
    Lexical.tag textw ~start ~stop
Packit bd2e5d
  method insert text =
Packit bd2e5d
    let idx = Text.index textw
Packit bd2e5d
        ~index:(`Mark"insert",[`Char(-1);`Linestart]) in
Packit bd2e5d
    Text.insert textw ~text ~index:(`Mark"insert",[]);
Packit bd2e5d
    self#lex ~start:(idx,[`Linestart]) ();
Packit bd2e5d
    Text.see textw ~index:(`Mark"insert",[])
Packit bd2e5d
  method private keypress c =
Packit bd2e5d
    if not reading && c > " " then begin
Packit bd2e5d
      reading <- true;
Packit bd2e5d
      Text.mark_set textw ~mark:"input" ~index:(`Mark"insert",[`Char(-1)])
Packit bd2e5d
    end
Packit bd2e5d
  method private keyrelease c = if c <> "" then self#lex ()
Packit bd2e5d
  method private return =
Packit bd2e5d
    if reading then reading <- false
Packit bd2e5d
    else Text.mark_set textw ~mark:"input"
Packit bd2e5d
        ~index:(`Mark"insert",[`Linestart;`Char 1]);
Packit bd2e5d
    Text.mark_set textw ~mark:"insert" ~index:(`Mark"insert",[`Lineend]);
Packit bd2e5d
    self#lex ~start:(`Mark"input",[`Linestart]) ();
Packit bd2e5d
    let s =
Packit bd2e5d
      (* input is one character before real input *)
Packit bd2e5d
      Text.get textw ~start:(`Mark"input",[`Char 1])
Packit bd2e5d
        ~stop:(`Mark"insert",[]) in
Packit bd2e5d
    h#add s;
Packit bd2e5d
    Text.insert textw ~index:(`Mark"insert",[]) ~text:"\n";
Packit bd2e5d
    Text.yview_index textw ~index:(`Mark"insert",[]);
Packit bd2e5d
    self#send s;
Packit bd2e5d
    self#send "\n"
Packit bd2e5d
  method private paste ev =
Packit bd2e5d
    if not reading then begin
Packit bd2e5d
      reading <- true;
Packit bd2e5d
      Text.mark_set textw ~mark:"input"
Packit bd2e5d
        ~index:(`Atxy(ev.ev_MouseX, ev.ev_MouseY),[`Char(-1)])
Packit bd2e5d
    end
Packit bd2e5d
  initializer
Packit bd2e5d
    Lexical.init_tags textw;
Packit bd2e5d
    let rec bindings =
Packit bd2e5d
      [ ([], `KeyPress, [`Char], fun ev -> self#keypress ev.ev_Char);
Packit bd2e5d
        ([], `KeyRelease, [`Char], fun ev -> self#keyrelease ev.ev_Char);
Packit bd2e5d
        (* [], `KeyPressDetail"Return", [], fun _ -> self#return; *)
Packit bd2e5d
        ([], `ButtonPressDetail 2, [`MouseX; `MouseY],  self#paste);
Packit bd2e5d
        ([`Alt], `KeyPressDetail"p", [], fun _ -> self#history `Previous);
Packit bd2e5d
        ([`Alt], `KeyPressDetail"n", [], fun _ -> self#history `Next);
Packit bd2e5d
        ([`Meta], `KeyPressDetail"p", [], fun _ -> self#history `Previous);
Packit bd2e5d
        ([`Meta], `KeyPressDetail"n", [], fun _ -> self#history `Next);
Packit bd2e5d
        ([`Control], `KeyPressDetail"c", [], fun _ -> self#interrupt);
Packit bd2e5d
        ([], `Destroy, [], fun _ -> self#kill) ]
Packit bd2e5d
    in
Packit bd2e5d
    List.iter bindings ~f:
Packit bd2e5d
      begin fun (modif,event,fields,action) ->
Packit bd2e5d
        bind textw ~events:[`Modified(modif,event)] ~fields ~action
Packit bd2e5d
      end;
Packit bd2e5d
    bind textw ~events:[`KeyPressDetail"Return"] ~breakable:true
Packit bd2e5d
      ~action:(fun _ -> self#return; break());
Packit bd2e5d
    List.iter ~f:Unix.close [in2;out2;err2];
Packit bd2e5d
    if use_threads then begin
Packit bd2e5d
      let fileinput_thread fd =
Packit bd2e5d
        let buf = Bytes.create 1024 in
Packit bd2e5d
        let len = ref 0 in
Packit bd2e5d
        try while len := Unix.read fd ~buf ~pos:0 ~len:1024; !len > 0 do
Packit bd2e5d
          Mutex.lock imutex;
Packit bd2e5d
          Buffer.add_subbytes ibuffer buf 0 !len;
Packit bd2e5d
          Mutex.unlock imutex
Packit bd2e5d
        done with Unix.Unix_error _ -> ()
Packit bd2e5d
      in
Packit bd2e5d
      ithreads <- List.map [in1; err1] ~f:(Thread.create fileinput_thread);
Packit bd2e5d
      let rec read_buffer () =
Packit bd2e5d
        Mutex.lock imutex;
Packit bd2e5d
        if Buffer.length ibuffer > 0 then begin
Packit bd2e5d
          self#insert (Str.global_replace ~!"\r\n" "\n"
Packit bd2e5d
                         (Buffer.contents ibuffer));
Packit bd2e5d
          Buffer.reset ibuffer;
Packit bd2e5d
          Text.mark_set textw ~mark:"input" ~index:(`Mark"insert",[`Char(-1)])
Packit bd2e5d
        end;
Packit bd2e5d
        Mutex.unlock imutex;
Packit bd2e5d
        Timer.set ~ms:100 ~callback:read_buffer
Packit bd2e5d
      in
Packit bd2e5d
      read_buffer ()
Packit bd2e5d
    end else begin
Packit bd2e5d
      try
Packit bd2e5d
        List.iter [in1;err1] ~f:
Packit bd2e5d
          begin fun fd ->
Packit bd2e5d
            Fileevent.add_fileinput ~fd
Packit bd2e5d
              ~callback:(fun () -> ignore (self#read ~fd ~len:1024))
Packit bd2e5d
          end
Packit bd2e5d
      with _ -> ()
Packit bd2e5d
    end
Packit bd2e5d
end
Packit bd2e5d
Packit bd2e5d
(* Specific use of shell, for OCamlBrowser *)
Packit bd2e5d
Packit bd2e5d
let shells : (string * shell) list ref = ref []
Packit bd2e5d
Packit bd2e5d
(* Called before exiting *)
Packit bd2e5d
let kill_all () =
Packit bd2e5d
  List.iter !shells ~f:(fun (_,sh) -> if sh#alive then sh#kill);
Packit bd2e5d
  shells := []
Packit bd2e5d
Packit bd2e5d
let get_all () =
Packit bd2e5d
  let all = List.filter !shells ~f:(fun (_,sh) -> sh#alive) in
Packit bd2e5d
  shells := all;
Packit bd2e5d
  all
Packit bd2e5d
Packit bd2e5d
let may_exec_unix prog =
Packit bd2e5d
  try Unix.access prog ~perm:[Unix.X_OK]; prog
Packit bd2e5d
  with Unix.Unix_error _ -> ""
Packit bd2e5d
Packit bd2e5d
let may_exec_win prog =
Packit bd2e5d
  let has_ext =
Packit bd2e5d
    List.exists ~f:(Filename.check_suffix prog) ["exe"; "com"; "bat"] in
Packit bd2e5d
  if has_ext then may_exec_unix prog else
Packit bd2e5d
  List.fold_left [prog^".bat"; prog^".exe"; prog^".com"] ~init:""
Packit bd2e5d
    ~f:(fun res prog -> if res = "" then may_exec_unix prog else res)
Packit bd2e5d
Packit bd2e5d
let may_exec =
Packit bd2e5d
  if is_win32 then may_exec_win else may_exec_unix
Packit bd2e5d
Packit bd2e5d
let path_sep = if is_win32 then ";" else ":"
Packit bd2e5d
Packit bd2e5d
let warnings = ref Warnings.defaults_w
Packit bd2e5d
Packit bd2e5d
let program_not_found prog =
Packit bd2e5d
  Jg_message.info ~title:"Error"
Packit bd2e5d
    ("Program \"" ^ prog ^ "\"\nwas not found in path")
Packit bd2e5d
Packit bd2e5d
let protect_arg s =
Packit bd2e5d
  if String.contains s ' ' then "\"" ^ s ^ "\"" else s
Packit bd2e5d
Packit bd2e5d
let f ~prog ~title =
Packit bd2e5d
  let progargs =
Packit bd2e5d
    List.filter ~f:((<>) "") (Str.split ~!" " prog) in
Packit bd2e5d
  if progargs = [] then () else
Packit bd2e5d
  let prog = List.hd progargs in
Packit bd2e5d
  let path =
Packit bd2e5d
    try Sys.getenv "PATH" with Not_found -> "/bin" ^ path_sep ^ "/usr/bin" in
Packit bd2e5d
  let exec_path = Str.split ~!path_sep path in
Packit bd2e5d
  let exec_path = if is_win32 then "."::exec_path else exec_path in
Packit bd2e5d
  let progpath =
Packit bd2e5d
    if not (Filename.is_implicit prog) then may_exec prog else
Packit bd2e5d
    List.fold_left exec_path ~init:"" ~f:
Packit bd2e5d
      (fun res dir ->
Packit bd2e5d
        if res = "" then may_exec (Filename.concat dir prog) else res) in
Packit bd2e5d
  if progpath = "" then program_not_found prog else
Packit bd2e5d
  let tl = Jg_toplevel.titled title in
Packit bd2e5d
  let menus = Menu.create tl ~name:"menubar" ~typ:`Menubar in
Packit bd2e5d
  Toplevel.configure tl ~menu:menus;
Packit bd2e5d
  let file_menu = new Jg_menu.c "File" ~parent:menus
Packit bd2e5d
  and history_menu = new Jg_menu.c "History" ~parent:menus
Packit bd2e5d
  and signal_menu = new Jg_menu.c "Signal" ~parent:menus in
Packit bd2e5d
  let frame, tw, sb = Jg_text.create_with_scrollbar tl in
Packit bd2e5d
  Text.configure tw ~background:`White;
Packit bd2e5d
  pack [sb] ~fill:`Y ~side:`Right;
Packit bd2e5d
  pack [tw] ~fill:`Both ~expand:true ~side:`Left;
Packit bd2e5d
  pack [frame] ~fill:`Both ~expand:true;
Packit bd2e5d
  let env = Array.map (Unix.environment ()) ~f:
Packit bd2e5d
      begin fun s ->
Packit bd2e5d
        if Str.string_match ~!"TERM=" s 0 then "TERM=dumb" else s
Packit bd2e5d
      end in
Packit bd2e5d
  let load_path =
Packit bd2e5d
    List2.flat_map !Config.load_path ~f:(fun dir -> ["-I"; dir]) in
Packit bd2e5d
  let load_path =
Packit bd2e5d
    if is_win32 then List.map ~f:protect_arg load_path else load_path in
Packit bd2e5d
  let labels = if !Clflags.classic then ["-nolabels"] else [] in
Packit bd2e5d
  let rectypes = if !Clflags.recursive_types then ["-rectypes"] else [] in
Packit bd2e5d
  let warnings =
Packit bd2e5d
    if List.mem "-w" progargs || !warnings = "Al" then []
Packit bd2e5d
    else ["-w"; !warnings]
Packit bd2e5d
  in
Packit bd2e5d
  let args =
Packit bd2e5d
    Array.of_list (progargs @ labels @ warnings @ rectypes @ load_path) in
Packit bd2e5d
  let history = new history () in
Packit bd2e5d
  let start_shell () =
Packit bd2e5d
    let sh = new shell ~textw:tw ~prog:progpath ~env ~args ~history in
Packit bd2e5d
    shells := (title, sh) :: !shells;
Packit bd2e5d
    sh
Packit bd2e5d
  in
Packit bd2e5d
  let sh = ref (start_shell ()) in
Packit bd2e5d
  let current_dir = ref (Unix.getcwd ()) in
Packit bd2e5d
  file_menu#add_command "Restart" ~command:
Packit bd2e5d
    begin fun () ->
Packit bd2e5d
      (!sh)#kill;
Packit bd2e5d
      Text.configure tw ~state:`Normal;
Packit bd2e5d
      Text.insert tw ~index:(`End,[]) ~text:"\n";
Packit bd2e5d
      Text.see tw ~index:(`End,[]);
Packit bd2e5d
      Text.mark_set tw ~mark:"insert" ~index:(`End,[]);
Packit bd2e5d
      sh := start_shell ();
Packit bd2e5d
    end;
Packit bd2e5d
  file_menu#add_command "Use..." ~command:
Packit bd2e5d
    begin fun () ->
Packit bd2e5d
      Fileselect.f ~title:"Use File" ~filter:"*.ml"
Packit bd2e5d
        ~sync:true ~dir:!current_dir ()
Packit bd2e5d
        ~action:(fun l ->
Packit bd2e5d
          if l = [] then () else
Packit bd2e5d
          let name = Fileselect.caml_dir (List.hd l) in
Packit bd2e5d
          current_dir := Filename.dirname name;
Packit bd2e5d
          if Filename.check_suffix name ".ml"
Packit bd2e5d
          then
Packit bd2e5d
            let cmd = "#use \"" ^ String.escaped name ^ "\";;\n" in
Packit bd2e5d
            (!sh)#insert cmd; (!sh)#send cmd)
Packit bd2e5d
    end;
Packit bd2e5d
  file_menu#add_command "Load..." ~command:
Packit bd2e5d
    begin fun () ->
Packit bd2e5d
      Fileselect.f ~title:"Load File" ~filter:"*.cm[oa]" ~sync:true ()
Packit bd2e5d
        ~dir:!current_dir
Packit bd2e5d
        ~action:(fun l ->
Packit bd2e5d
          if l = [] then () else
Packit bd2e5d
          let name = Fileselect.caml_dir (List.hd l) in
Packit bd2e5d
          current_dir := Filename.dirname name;
Packit bd2e5d
          if Filename.check_suffix name ".cmo" ||
Packit bd2e5d
            Filename.check_suffix name ".cma"
Packit bd2e5d
          then
Packit bd2e5d
            let cmd = "#load \"" ^ String.escaped name ^ "\";;\n" in
Packit bd2e5d
            (!sh)#insert cmd; (!sh)#send cmd)
Packit bd2e5d
    end;
Packit bd2e5d
  file_menu#add_command "Import path" ~command:
Packit bd2e5d
    begin fun () ->
Packit bd2e5d
      List.iter (List.rev !Config.load_path) ~f:
Packit bd2e5d
        (fun dir ->
Packit bd2e5d
          (!sh)#send ("#directory \"" ^ String.escaped dir ^ "\";;\n"))
Packit bd2e5d
    end;
Packit bd2e5d
  file_menu#add_command "Close" ~command:(fun () -> destroy tl);
Packit bd2e5d
  history_menu#add_command "Previous  " ~accelerator:"M-p"
Packit bd2e5d
    ~command:(fun () -> (!sh)#history `Previous);
Packit bd2e5d
  history_menu#add_command "Next" ~accelerator:"M-n"
Packit bd2e5d
    ~command:(fun () -> (!sh)#history `Next);
Packit bd2e5d
  signal_menu#add_command "Interrupt  " ~accelerator:"C-c"
Packit bd2e5d
    ~command:(fun () -> (!sh)#interrupt);
Packit bd2e5d
  signal_menu#add_command "Kill" ~command:(fun () -> (!sh)#kill)