unison

Fork of Unison, a bi-directional file synchronization tool
git clone git://git.laack.co/unison.git
Log | Files | Refs | README | LICENSE

uitext.ml (67814B)


      1 (* Unison file synchronizer: src/uitext.ml *)
      2 (* Copyright 1999-2020, Benjamin C. Pierce
      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 3 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
     15     along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16 *)
     17 
     18 open Common
     19 open Lwt
     20 
     21 module Body : Uicommon.UI = struct
     22 
     23 let debug = Trace.debug "ui"
     24 
     25 let dumbtty =
     26   Prefs.createBool "dumbtty"
     27     (try System.getenv "EMACS" <> "" with Not_found -> false)
     28     ~category:(`Advanced `CLI)
     29     "do not change terminal settings in text UI"
     30     ("When set to \\verb|true|, this flag makes the text mode user "
     31      ^ "interface avoid trying to change any of the terminal settings.  "
     32      ^ "(Normally, Unison puts the terminal in `raw mode', so that it can "
     33      ^ "do things like overwriting the current line.) This is useful, for "
     34      ^ "example, when Unison runs in a shell inside of Emacs.  "
     35      ^ "\n\n"
     36      ^ "When \\verb|dumbtty| is set, commands to the user interface need to "
     37      ^ "be followed by a carriage return before Unison will execute them.  "
     38      ^ "(When it is off, Unison "
     39      ^ "recognizes keystrokes as soon as they are typed.)\n\n"
     40      ^ "This preference has no effect on the graphical user "
     41      ^ "interface.")
     42 
     43 let silent =
     44   Prefs.createBool "silent" false
     45     ~category:(`Basic `Syncprocess_CLI)
     46     "print nothing except error messages"
     47     ("When this preference is set to {\\tt true}, the textual user "
     48      ^ "interface will print nothing at all, except in the case of errors.  "
     49      ^ "Setting \\texttt{silent} to true automatically sets the "
     50      ^ "\\texttt{batch} preference to {\\tt true}.")
     51 
     52 let cbreakMode = ref None
     53 
     54 let supportSignals = Sys.unix || Sys.cygwin
     55 
     56 let rawTerminal () =
     57   match !cbreakMode with
     58     None      -> ()
     59   | Some funs -> funs.System.rawTerminal ()
     60 
     61 let defaultTerminal () =
     62   match !cbreakMode with
     63     None      -> ()
     64   | Some funs -> funs.System.defaultTerminal ()
     65 
     66 let restoreTerminal() =
     67   if supportSignals && not (Prefs.read dumbtty) then
     68     Sys.set_signal Sys.sigcont Sys.Signal_default;
     69   defaultTerminal ();
     70   cbreakMode := None
     71 
     72 let setupTerminal() =
     73   if not (Prefs.read dumbtty) then
     74     try
     75       cbreakMode := Some (System.terminalStateFunctions ());
     76       let suspend _ =
     77         defaultTerminal ();
     78         Sys.set_signal Sys.sigtstp Sys.Signal_default;
     79         Unix.kill (Unix.getpid ()) Sys.sigtstp
     80       in
     81       let resume _ =
     82         if supportSignals then
     83           Sys.set_signal Sys.sigtstp (Sys.Signal_handle suspend);
     84         rawTerminal ()
     85       in
     86       if supportSignals then
     87         Sys.set_signal Sys.sigcont (Sys.Signal_handle resume);
     88       resume ()
     89     with Unix.Unix_error _ ->
     90       restoreTerminal ()
     91 
     92 let colorMode =
     93   Prefs.createBoolWithDefault "color"
     94     ~category:(`Advanced `CLI) ~local:true
     95     "use color output for text UI (true/false/default)"
     96     ("When set to {\\tt true}, this flag enables color output in "
     97      ^ "text mode user interface. When set to {\\tt false}, all "
     98      ^ "color output is disabled. Default is to enable color if "
     99      ^ "the {\\tt NO\\_COLOR} environment variable is not set.")
    100 
    101 let colorEnabled = ref false
    102 
    103 let setColorPreference () =
    104   let envOk = try let _ = System.getenv "NO_COLOR" in false
    105     with Not_found -> true
    106   and termOk = try System.getenv "TERM" <> "dumb" with Not_found -> true
    107   and ttyOk = (Unix.isatty Unix.stdout) && (Unix.isatty Unix.stderr) in
    108   let colorOk = envOk && termOk && ttyOk && not (Prefs.read dumbtty) in
    109   colorEnabled :=
    110     match Prefs.read colorMode with
    111     | `True    -> true
    112     | `False   -> false
    113     | `Default -> colorOk && (not Sys.win32
    114                     || (System.termVtCapable Unix.stdout
    115                         && System.termVtCapable Unix.stderr))
    116 
    117 let color t =
    118   if not !colorEnabled then "" else
    119   match t with
    120     `Reset       -> "\027[0m"
    121   | `Focus       -> "\027[1m"
    122   | `Success     -> "\027[1;32m"
    123   | `Information -> "\027[1;34m"
    124   | `Warning     -> "\027[1;33m"
    125   | `Failure     -> "\027[1;31m"
    126   | `AError      -> "\027[31m"
    127   | `ASkip       -> "\027[1;35m"
    128   | `ALtoRf      -> "\027[1;32m"
    129   | `ALtoRt      -> "\027[1;33m"
    130   | `ARtoLf      -> "\027[1;34m"
    131   | `ARtoLt      -> "\027[1;33m"
    132   | `AMerge      -> "\027[1;36m"
    133   | `DiffHead    -> "\027[1m"
    134   | `DiffAdd     -> "\027[32m"
    135   | `DiffDel     -> "\027[31m"
    136   | `DiffLoc     -> "\027[36m"
    137   | _            -> ""
    138 
    139 let lineRegexp = Str.regexp "^"
    140 
    141 let colorDiff text =
    142   let result = Buffer.create (String.length text) in
    143   let a s = Buffer.add_string result s in
    144   let p = Str.full_split lineRegexp text in
    145   Safelist.iter (fun t ->
    146               match t with
    147                 Str.Delim s -> a s
    148               | Str.Text s -> (let lineSt = s.[0] in
    149                                match lineSt with
    150                                | '+' -> a (color `DiffAdd); a s; a (color `Reset)
    151                                | '-' -> a (color `DiffDel); a s; a (color `Reset)
    152                                | '@' -> a (color `DiffLoc); a s; a (color `Reset)
    153                                | _   -> a s)
    154             ) p;
    155   Buffer.contents result
    156 
    157 let alwaysDisplay message =
    158   print_string message;
    159   flush stdout
    160 
    161 let alwaysDisplayAndLog message =
    162 (*  alwaysDisplay message;*)
    163   Trace.log (message ^ "\n")
    164 
    165 let display message =
    166   if not (Prefs.read silent) then alwaysDisplay message
    167 
    168 let displayWhenInteractive message =
    169   if not (Prefs.read Globals.batch) then alwaysDisplay message
    170 
    171 let readInput () =
    172   match !cbreakMode with
    173     None -> input_line stdin
    174   | Some funs ->
    175       (* Raw terminal mode, we want to read the input directly, without the line
    176          buffering. We can't use [Stdlib.input_char] because OCaml 'char' equals
    177          one byte and this is not what we want to read. Not all characters are
    178          one byte (mainly thinking of UTF-8). We also want to make sure that we
    179          properly read in any input ANSI escape sequences. *)
    180       let input_char () =
    181         (* We cannot used buffered I/Os under Windows, as character
    182            '\r' is not passed through (probably due to the code that
    183            turns \r\n into \n) *)
    184         let l = 9 in (* This should suffice to fit a complete escape sequence *)
    185         let s = Bytes.create l in
    186         let n = Unix.read Unix.stdin s 0 l in
    187         if n = 0 then raise End_of_file;
    188         if Bytes.get s 0 = '\003' then raise Sys.Break;
    189         Bytes.sub_string s 0 n
    190       in
    191       funs.System.startReading ();
    192       let c = input_char () in
    193       funs.System.stopReading ();
    194       c
    195 
    196 (* This is a really basic and dumb parser to extract input tokens from
    197    non-delimited input read in raw terminal mode. Input tokens are:
    198    US-ASCII byte, Latin1 byte, Unicode "character" in UTF-8 encoding,
    199    ANSI escape sequence.
    200    The parser does not support partial reads from the input; that is,
    201    tokens split between reads from input are not supported.
    202    Normally with interactive input we'd read one keypress at a time
    203    but this won't always work (extremely fast key repeat, pressing
    204    multiple keys at once, buffering by ssh, non-interactive input and
    205    other similar situations). *)
    206 let getInput =
    207   let inputBuffer = ref "" in
    208   let subInput s len =
    209     if String.length s > len then
    210       inputBuffer := String.sub s len (String.length s - len);
    211     String.sub s 0 len
    212   in
    213   let nextInputToken () =
    214     let s = if !inputBuffer <> "" then !inputBuffer else readInput () in
    215     inputBuffer := "";
    216     if s = "" then
    217       s
    218     else if s.[0] = '\027' then
    219       (* ANSI escape sequence *)
    220       (* If a beginning of an escape sequence is detected then the
    221          entire input string is considered as the escape sequence,
    222          or until another escape character is found. *)
    223       match String.index_from s 1 '\027' with
    224       | i -> subInput s i
    225       | exception  (Not_found | Invalid_argument _) -> s
    226     else if s.[0] < '\128' then
    227       (* US-ASCII byte *)
    228       subInput s 1
    229     else if s.[0] < '\224' && String.length s >= 2 &&
    230         (Unicode.check_utf_8 (String.sub s 0 2)) then
    231       (* UTF-8 2-byte sequence *)
    232       subInput s 2
    233     else if s.[0] < '\240' && String.length s >= 3 &&
    234         (Unicode.check_utf_8 (String.sub s 0 3)) then
    235       (* UTF-8 3-byte sequence *)
    236       subInput s 3
    237     else if String.length s >= 4 &&
    238         (Unicode.check_utf_8 (String.sub s 0 4)) then
    239       (* UTF-8 4-byte sequence *)
    240       subInput s 4
    241     else
    242       (* Latin1 byte *)
    243       subInput s 1
    244   in
    245   fun () ->
    246     let c = match nextInputToken () with
    247       | "\000" -> "(invalid input)" (* Windows*)
    248       | "\n" | "\r" -> ""
    249       | c when not (Unicode.check_utf_8 c) -> Unicode.protect c
    250                (* This is not correct because [Unicode.protect] assumes
    251                   Latin1 encoding. But it does not matter here as currently
    252                   non-ASCII input is not expected to be processed anyway. *)
    253       | c -> c in
    254     if c <> "" && c.[0] <> '\027' then
    255       display c;
    256     c
    257 
    258 let newLine () =
    259   (* If in dumb mode (i.e. not in cbreak mode) the newline is entered by the
    260      user to validate the input *)
    261   if !cbreakMode <> None then display "\n"
    262 
    263 let overwrite () =
    264   if !cbreakMode <> None then display "\r"
    265 
    266 
    267 let keyEsc = "\027"
    268 let keyF1 = "\027OP"
    269 let keyF2 = "\027OQ"
    270 let keyF3 = "\027OR"
    271 let keyF4 = "\027OS"
    272 let keyF5 = "\027[15~"
    273 let keyF6 = "\027[17~"
    274 let keyF7 = "\027[18~"
    275 let keyF8 = "\027[19~"
    276 let keyF9 = "\027[20~"
    277 let keyF10 = "\027[21~"
    278 let keyF11 = "\027[23~"
    279 let keyF12 = "\027[24~"
    280 let keyInsert = "\027[2~"
    281 let keyDelete = "\027[3~"
    282 let keyHome = "\027[H"
    283 let keyEnd = "\027[F"
    284 let keyPgUp = "\027[5~"
    285 let keyPgDn = "\027[6~"
    286 let keyUp = "\027[A"
    287 let keyDn = "\027[B"
    288 let keyLeft = "\027[D"
    289 let keyRight = "\027[C"
    290 let keyShiftUp = "\027[1;2A"
    291 let keyShiftDn = "\027[1;2B"
    292 let keyTab = "\t"
    293 let keyRvTab = "\027[Z"
    294 
    295 
    296 let rec selectAction batch actions tryagain =
    297   let formatname = function
    298       "" -> "<ret>"
    299     | " " -> "<spc>"
    300     | "\x7f" | "\027[3~" -> "<del>"
    301     | "\b" -> "<bsp>"
    302     | "\t" -> "<tab>"
    303     | "\027[Z" -> "<shift+tab>"
    304     | "\027" -> "<esc>"
    305     | "\027[A" -> "<up>"
    306     | "\027[B" -> "<down>"
    307     | "\027[D" -> "<left>"
    308     | "\027[C" -> "<right>"
    309     | "\027[5~" -> "<pg up>"
    310     | "\027[6~" -> "<pg down>"
    311     | "\027[H" -> "<home>"
    312     | "\027[F" -> "<end>"
    313     | n when n.[0] = '\027' -> "^" ^ String.map (function | '\027' -> '[' | c -> c) n
    314     | n -> n in
    315   let summarizeChoices() =
    316     display "[";
    317     Safelist.iter
    318       (fun (names,doc,action) ->
    319          if (Safelist.nth names 0) = "" then
    320            display (formatname (Safelist.nth names 1)))
    321       actions;
    322     display "] " in
    323   let tryagainOrLoop() =
    324     tryagain ();
    325     selectAction batch actions tryagain in
    326   let rec find n = function
    327       [] -> raise Not_found
    328     | (names,doc,action)::rest ->
    329         if Safelist.mem n names then action else find n rest
    330   in
    331   let doAction a =
    332     if a="?" || a = "\027OP" then
    333       (newLine ();
    334        display "Commands:\n";
    335        Safelist.iter (fun (names,doc,action) ->
    336          let n = Util.concatmap " or " formatname names in
    337          let space = String.make (max 2 (22 - String.length n)) ' ' in
    338          display ("  " ^ n ^ space ^ doc ^ "\n"))
    339          actions;
    340        tryagainOrLoop())
    341     else
    342       let action = try Some (find a actions) with Not_found -> None in
    343       match action with
    344         Some action ->
    345           action ()
    346       | None ->
    347           newLine ();
    348           if a="" then
    349             display ("No default command [type '?' or F1 for help]\n")
    350           else
    351             display ("Unrecognized command '" ^ String.escaped a
    352                      ^ "': try again  [type '?' or F1 for help]\n");
    353           tryagainOrLoop()
    354   in
    355   let handleExn s =
    356     (* Make sure that the error messages start on their own lines and not
    357      * after the prompt. *)
    358     alwaysDisplay "\n";
    359     raise (Util.Fatal ("Failure reading from the standard input ("^s^")\n"))
    360   in
    361   let userInput () =
    362     try
    363       Some (getInput ())
    364     with
    365       (* Restart an interrupted system call (which can happen notably when
    366        * the process is put in the background by SIGTSTP). *)
    367     | Unix.Unix_error (Unix.EINTR, _, _) -> None
    368       (* Simply print a slightly more informative message than the exception
    369        * itself (e.g. "Uncaught unix error: read failed: Resource temporarily
    370        * unavailable" or "Uncaught exception End_of_file"). *)
    371     | End_of_file -> handleExn "End of file"
    372     | Unix.Unix_error (err, _, _) -> handleExn (Unix.error_message err)
    373   in
    374   let a =
    375     match batch with
    376     | None ->
    377       summarizeChoices();
    378       userInput ()
    379     | _ -> batch
    380   in
    381   match a with
    382   | Some a -> doAction a
    383   | None -> tryagainOrLoop()
    384 
    385 let alwaysDisplayErrors prefix l =
    386   List.iter
    387     (fun err -> alwaysDisplay (Format.sprintf "%s%s\n" prefix err)) l
    388 
    389 let alwaysDisplayDetails ri =
    390   alwaysDisplay ((Uicommon.details2string ri "  ") ^ "\n");
    391   match ri.replicas with
    392     Problem _ ->
    393       ()
    394   | Different diff ->
    395       alwaysDisplayErrors "[root 1]: " diff.errors1;
    396       alwaysDisplayErrors "[root 2]: " diff.errors2
    397 
    398 let displayDetails ri =
    399   if not (Prefs.read silent) then alwaysDisplayDetails ri
    400 
    401 let displayri ri =
    402   let (r1, action, r2, path) = Uicommon.reconItem2stringList Path.empty ri in
    403   let forced =
    404     match ri.replicas with
    405       Different diff -> diff.direction <> diff.default_direction
    406     | Problem _      -> false
    407   in
    408   let (defaultAction, forcedAction) =
    409     match action with
    410       Uicommon.AError      -> ((color `AError) ^ "error" ^ (color `Reset), (color `AError) ^ "error" ^ (color `Reset))
    411     | Uicommon.ASkip _     -> ((color `ASkip)  ^ "<-?->" ^ (color `Reset), (color `ASkip)  ^ "<=?=>" ^ (color `Reset))
    412     | Uicommon.ALtoR false -> ((color `ALtoRf) ^ "---->" ^ (color `Reset), (color `ALtoRf) ^ "====>" ^ (color `Reset))
    413     | Uicommon.ALtoR true  -> ((color `ALtoRt) ^ "--?->" ^ (color `Reset), (color `ALtoRt) ^ "==?=>" ^ (color `Reset))
    414     | Uicommon.ARtoL false -> ((color `ARtoLf) ^ "<----" ^ (color `Reset), (color `ARtoLf) ^ "<====" ^ (color `Reset))
    415     | Uicommon.ARtoL true  -> ((color `ARtoLt) ^ "<-?--" ^ (color `Reset), (color `ARtoLt) ^ "<=?==" ^ (color `Reset))
    416     | Uicommon.AMerge      -> ((color `AMerge) ^ "<-M->" ^ (color `Reset), (color `AMerge) ^ "<=M=>" ^ (color `Reset))
    417   in
    418   let action = if forced then forcedAction else defaultAction in
    419   let s = Format.sprintf "%s %s %s   %s  " r1 action r2 path in
    420   match ri.replicas with
    421     Problem _ ->
    422       alwaysDisplay s
    423   | Different {direction = d} when isConflict d ->
    424       alwaysDisplay s
    425   | _ ->
    426       display s
    427 
    428 type proceed = ConfirmBeforeProceeding | ProceedImmediately
    429 
    430 (* "interact [] rilist" interactively reconciles each list item *)
    431 let interact prilist rilist =
    432   if not (Prefs.read Globals.batch) then display ("\n" ^ Uicommon.roots2string() ^ "\n");
    433   let (r1,r2) = Globals.roots() in
    434   let (host1, host2) = root2hostname r1, root2hostname r2 in
    435   let showdiffs ri =
    436     Uicommon.showDiffs ri
    437       (fun title text ->
    438          let colorText = colorDiff text in
    439          try
    440            let pager = System.getenv "PAGER" in
    441            restoreTerminal ();
    442            let out = System.open_process_out pager in
    443            Printf.fprintf out "\n%s\n\n%s\n\n" title colorText;
    444            let _ = System.close_process_out out in
    445            setupTerminal ()
    446          with Not_found ->
    447            Printf.printf "\n%s\n\n%s\n\n" title colorText)
    448       (fun s -> Printf.printf "%s\n" s)
    449       Uutil.File.dummy;
    450       true
    451   and ispropschanged = function
    452       {replicas = Different {rc1 = rc1; rc2 = rc2}}
    453       when rc1.status = `PropsChanged &&
    454            (rc2.status = `PropsChanged || rc2.status = `Unchanged) -> true
    455     | {replicas = Different {rc1 = rc1; rc2 = rc2}}
    456       when rc1.status = `Unchanged && rc2.status = `PropsChanged -> true
    457     | _ -> false
    458   and setdirchanged = function
    459       {replicas = Different ({rc1 = rc1; rc2 = rc2} as diff)}
    460       when rc1.status = `Modified && rc2.status = `PropsChanged ->
    461         diff.direction <- Replica1ToReplica2; true
    462     | {replicas = Different ({rc1 = rc1; rc2 = rc2} as diff)}
    463       when rc1.status = `PropsChanged && rc2.status = `Modified ->
    464         diff.direction <- Replica2ToReplica1; true
    465     | {replicas = Different _} -> false
    466     | _ -> true
    467   and setskip = function
    468       {replicas = Different ({direction = Conflict _})} -> true
    469     | {replicas = Different diff} ->
    470         begin diff.direction <- Conflict "skip requested"; true end
    471     | _ -> true
    472   and setdir dir = function
    473       {replicas = Different diff} -> begin diff.direction <- dir; true end
    474     | _ -> true
    475   and invertdir = function
    476       {replicas = Different ({direction = Replica1ToReplica2} as diff)}
    477         -> diff.direction <- Replica2ToReplica1; true
    478     | {replicas = Different ({direction = Replica2ToReplica1} as diff)}
    479         -> diff.direction <- Replica1ToReplica2; true
    480     | {replicas = Different _} -> false
    481     | _ -> true
    482   and setDirectionIfConflict dir = function
    483       {replicas = Different ({direction = Conflict _})} as ri ->
    484         begin Recon.setDirection ri dir `Force; true end
    485     | ri -> begin Recon.setDirection ri dir `Prefer; true end
    486   in
    487   let ripred = ref [] in
    488   let ritest ri = match !ripred with
    489       [] -> true
    490     | test::_ -> test ri in
    491   let rec loop prev =
    492     let rec previous prev ril =
    493       match prev with
    494         ({ replicas = Problem s } as pri)::pril ->
    495           displayri pri; display "\n"; display s; display "\n";
    496           previous pril (pri::ril)
    497       | pri::pril -> loop pril (pri::ril)
    498       | [] -> display ("\n" ^ Uicommon.roots2string() ^ "\n"); loop prev ril in
    499     let rec forward n prev ril =
    500       match n, prev, ril with
    501         0, prev, ril -> loop prev ril
    502       | n, [], ril when n < 0 -> loop [] ril
    503       | n, pri::pril, ril when n < 0 -> forward (n+1) pril (pri::ril)
    504       | _, [], [] -> loop [] []
    505       | n, pri::pril, [] when n > 0 -> loop pril [pri]
    506       | n, prev, ri::rest when n > 0 -> forward (n-1) (ri::prev) rest
    507       | _ -> assert false (* to silence the compiler *) in
    508     function
    509       [] -> (ConfirmBeforeProceeding, Safelist.rev prev)
    510     | ri::rest as ril ->
    511         let next() = loop (ri::prev) rest in
    512         let repeat() = loop prev ril in
    513         let ignore_pref pat rest what =
    514           display "  ";
    515           Uicommon.addIgnorePattern pat;
    516           display ("  Permanently ignoring " ^ what ^ "\n");
    517           begin match !Prefs.profileName with None -> assert false |
    518             Some(n) ->
    519               display ("  To un-ignore, edit "
    520                        ^ Prefs.profilePathname n
    521                        ^ " and restart " ^ Uutil.myName ^ "\n") end;
    522           let nukeIgnoredRis =
    523             Safelist.filter (fun ri -> not (Globals.shouldIgnore ri.path1)) in
    524           loop (nukeIgnoredRis (ri::prev)) (nukeIgnoredRis ril) in
    525         (* This should work on most terminals: *)
    526         let redisplayri() = overwrite (); displayri ri; display "\n" in
    527         let setripred cmd =
    528           ripred := match cmd, !ripred with
    529               `Unset, [] -> display "Matching condition already disabled\n"; []
    530             | `Unset, _ | `Pop, [_] -> display "  Disabling matching condition\n"; []
    531             | `Pop, p::pp::t -> pp::t
    532             | `Push rp, [] -> display "  Enabling matching condition\n"; [rp]
    533             | `Push rp, p -> rp::p
    534             | _, [] -> display "Matching condition not enabled\n"; []
    535             | `Op1 op, p::t -> (fun ri -> op (p ri))::t
    536             | `Op2 op, [p] -> display "Missing previous matching condition\n"; [p]
    537             | `Op2 op, p::pp::t -> (fun ri -> op (p ri) (pp ri))::t
    538             | _ -> assert false in
    539         let actOnMatching ?(change=true) ?(fail=Some(fun()->())) f =
    540           (* [f] can have effects on the ri and return false to run [fail] (if
    541              the matching condition is disabled) *)
    542           (* When [fail] is [None] if [f] returns false then instead of
    543              executing [fail] and repeating we discard the item (even when the
    544              matching condition is disabled) and go to the next *)
    545           (* Disabling [change] avoids to redisplay the item, allows [f] to
    546              print a message (info or error) on a separate line and repeats
    547              instead of going to the next item *)
    548           let discard, err =
    549             match fail with Some e -> false, e | None -> true, fun()->() in
    550           match !ripred with
    551           | [] -> if not change then newLine();
    552               let t = f ri in
    553               if t || not discard
    554               then begin
    555                 if change then redisplayri();
    556                 if not t then err();
    557                 if t && change then next() else repeat()
    558               end else begin
    559                 if change then newLine();
    560                 loop prev rest
    561               end
    562           | test::_ -> newLine();
    563               let filt = fun ri -> if test ri then f ri || not discard else true in
    564               loop prev (ri::Safelist.filter filt rest)
    565         in
    566         displayri ri;
    567         match ri.replicas with
    568           Problem s -> alwaysDisplay "\n"; alwaysDisplay s; alwaysDisplay "\n"; next()
    569         | Different {rc1 = _; rc2 = _; direction = dir} ->
    570             if Prefs.read Uicommon.auto && not (isConflict dir) then begin
    571               display "\n"; next()
    572             end else
    573               let (descr, descl) =
    574                 if host1 = host2 then
    575                   "left to right", "right to left"
    576                 else
    577                   "from "^host1^" to "^host2,
    578                   "from "^host2^" to "^host1
    579               in
    580               if Prefs.read Globals.batch then begin
    581                 if Prefs.read silent && isConflict dir then alwaysDisplay "\n";
    582                 display "\n";
    583                 if not (Prefs.read Trace.terse) then
    584                   displayDetails ri
    585               end;
    586               if Prefs.read Globals.batch then next () else
    587               selectAction
    588                 (if Prefs.read Globals.batch then Some " " else None)
    589                 [((if (isConflict dir) && not (Prefs.read Globals.batch)
    590                    then ["f"]  (* Offer no default behavior if we've got a
    591                                   conflict and we're in interactive mode *)
    592                    else ["";"f";" "]),
    593                   ("follow " ^ Uutil.myName ^ "'s recommendation (if any)"),
    594                   (fun () -> newLine();
    595                      if (isConflict dir) && not (Prefs.read Globals.batch)
    596                      then begin
    597                        display "No default action [type '?' for help]\n";
    598                        repeat()
    599                      end else
    600                        next()));
    601                  (["n";"j"; keyDn; keyTab],
    602                   ("go to the next item"),
    603                   (fun () -> newLine();
    604                      next()));
    605                  (["p";"b";"k"; keyUp; keyRvTab],
    606                   ("go back to previous item"),
    607                   (fun () -> newLine();
    608                      previous prev ril));
    609                  (["\x7f";"\b"; keyDelete],
    610                   ("revert then go back to previous item"),
    611                   (fun () ->
    612                      Recon.revertToDefaultDirection ri; redisplayri();
    613                      previous prev ril));
    614                  (["0"; keyHome],
    615                   ("go to the start of the list"),
    616                   (fun () -> newLine();
    617                      loop [] (Safelist.rev_append prev ril)));
    618                  (["9"; keyEnd],
    619                   ("go to the end of the list"),
    620                   (fun () -> newLine();
    621                      match Safelist.rev_append ril prev with
    622                        [] -> loop [] []
    623                      | lri::prev -> loop prev [lri]));
    624                  (["5"; keyPgDn],
    625                   ("go forward to the middle of the following items"),
    626                   (fun () -> newLine();
    627                      let l = (Safelist.length ril)/2 in
    628                      display ("  Moving "^(string_of_int l)^" items forward\n");
    629                      forward l prev ril));
    630                  (["6"; keyPgUp],
    631                   ("go backward to the middle of the preceding items"),
    632                   (fun () -> newLine();
    633                      let l = -((Safelist.length prev)+1)/2 in
    634                      display ("  Moving "^(string_of_int l)^" items backward\n");
    635                      forward l prev ril));
    636                  (["R"],
    637                   ("reverse the list of paths"),
    638                   (fun () -> newLine();
    639                      loop rest (ri::prev)));
    640                  (["d"],
    641                   ("show differences (curr or match)"),
    642                   (fun () ->
    643                      actOnMatching ~change:false showdiffs));
    644                  (["x"],
    645                   ("show details (curr or match)"),
    646                   (fun () ->
    647                       actOnMatching ~change:false
    648                         (fun ri -> displayDetails ri; true)));
    649                  (["L"],
    650                   ("list all (or matching) following changes tersely"),
    651                   (fun () -> newLine();
    652                      Safelist.iter
    653                        (fun ri -> display "  "; displayri ri; display "\n")
    654                        (Safelist.filter ritest ril);
    655                      repeat()));
    656                  (["l"],
    657                   ("list all (or matching) following changes with details"),
    658                   (fun () -> newLine();
    659                      Safelist.iter
    660                        (fun ri -> display "  "; displayri ri; display "\n";
    661                                   alwaysDisplayDetails ri)
    662                        (Safelist.filter ritest ril);
    663                      repeat()));
    664                  (["A";"*"],
    665                   ("match all the following"),
    666                   (fun () -> newLine();
    667                      setripred (`Push (fun _ -> true));
    668                      repeat()));
    669                  (["1"],
    670                   ("match all the following that propagate " ^ descr),
    671                   (fun () -> newLine();
    672                      setripred (`Push (function
    673                          {replicas = Different ({direction = Replica1ToReplica2})} -> true
    674                        | _ -> false));
    675                      repeat()));
    676                  (["2"],
    677                   ("match all the following that propagate " ^ descl),
    678                   (fun () -> newLine();
    679                      setripred (`Push (function
    680                          {replicas = Different ({direction = Replica2ToReplica1})} -> true
    681                        | _ -> false));
    682                      repeat()));
    683                  (["C"],
    684                   ("match all the following conflicts"),
    685                   (fun () -> newLine();
    686                      setripred (`Push (function
    687                          {replicas = Different ({direction = Conflict _})} -> true
    688                        | _ -> false));
    689                      repeat()));
    690                  (["P";"="],
    691                   ("match all the following with only props changes"),
    692                   (fun () -> newLine();
    693                      setripred (`Push ispropschanged);
    694                      repeat()));
    695                  (["M"],
    696                   ("match all the following merges"),
    697                   (fun () -> newLine();
    698                      setripred (`Push (function
    699                          {replicas = Different ({direction = Merge})} -> true
    700                        | _ -> false));
    701                      repeat()));
    702                  (["X";"!"],
    703                   ("invert the matching condition"),
    704                   (fun () -> newLine();
    705                      setripred (`Op1 not);
    706                      repeat()));
    707                  (["&"],
    708                   ("and the last two matching conditions"),
    709                   (fun () -> newLine();
    710                      setripred (`Op2 (&&));
    711                      repeat()));
    712                  (["|"],
    713                   ("or the last two matching conditions"),
    714                   (fun () -> newLine();
    715                      setripred (`Op2 (||));
    716                      repeat()));
    717                  (["D";"_"],
    718                   ("delete/pop the active matching condition"),
    719                   (fun () -> newLine();
    720                      setripred `Pop;
    721                      repeat()));
    722                  (["U";"$"],
    723                   ("unmatch (select current)"),
    724                   (fun () -> newLine();
    725                      setripred `Unset;
    726                      repeat()));
    727                  (["r";"u"],
    728                   ("revert to " ^ Uutil.myName ^ "'s default recommendation (curr or match)"),
    729                   (fun () ->
    730                      actOnMatching
    731                        (fun ri->Recon.revertToDefaultDirection ri; true)));
    732                  (["m"],
    733                   ("merge the versions (curr or match)"),
    734                   (fun () ->
    735                      actOnMatching (setdir Merge)));
    736                  ([">";"."; keyRight],
    737                   ("propagate from " ^ descr ^ " (curr or match)"),
    738                   (fun () ->
    739                      actOnMatching (setdir Replica1ToReplica2)));
    740                  (["<";","; keyLeft],
    741                   ("propagate from " ^ descl ^ " (curr or match)"),
    742                   (fun () ->
    743                      actOnMatching (setdir Replica2ToReplica1)));
    744                  (["]";"\""],
    745                   ("resolve conflicts in favor of the newer (curr or match)"),
    746                   (fun () ->
    747                      actOnMatching (setDirectionIfConflict `Newer)));
    748                  (["[";"'"],
    749                   ("resolve conflicts in favor of the older (curr or match)"),
    750                   (fun () ->
    751                      actOnMatching (setDirectionIfConflict `Older)));
    752                  (["c"],
    753                   ("resolve conflicts in favor of changed (curr or match)"),
    754                   (fun () ->
    755                      actOnMatching
    756                        ~fail:(Some (fun()->display "Cannot set direction\n"))
    757                        setdirchanged));
    758                  (["i"],
    759                   ("invert direction of propagation (curr or match)"),
    760                   (fun () ->
    761                      actOnMatching
    762                        ~fail:(Some (fun()->display "Cannot invert direction\n"))
    763                        invertdir));
    764                  (["/";":"],
    765                   ("skip (curr or match)"),
    766                   (fun () ->
    767                      actOnMatching setskip));
    768                  (["%"],
    769                   ("skip all the following"),
    770                   (fun () -> newLine();
    771                      Safelist.iter (fun ri -> ignore (setskip ri); ()) rest;
    772                      repeat()));
    773                  (["-"],
    774                   ("skip and discard for this session (curr or match)"),
    775                   (fun () ->
    776                      actOnMatching ~fail:None (fun _->false)));
    777                  (["+"],
    778                   ("skip and discard all the following"),
    779                   (fun () -> newLine();
    780                      loop prev [ri]));
    781                  (["I"],
    782                   ("ignore this path permanently"),
    783                   (fun () -> newLine();
    784                      ignore_pref (Uicommon.ignorePath ri.path1) rest
    785                        "this path"));
    786                  (["E"],
    787                   ("permanently ignore files with this extension"),
    788                   (fun () -> newLine();
    789                      ignore_pref (Uicommon.ignoreExt ri.path1) rest
    790                        "files with this extension"));
    791                  (["N"],
    792                   ("permanently ignore paths ending with this name"),
    793                   (fun () -> newLine();
    794                      ignore_pref (Uicommon.ignoreName ri.path1) rest
    795                        "files with this name"));
    796                  (["s"],
    797                   ("stop reconciling and go to the proceed menu"),
    798                   (fun () -> newLine();
    799                      (ConfirmBeforeProceeding, Safelist.rev_append prev ril)));
    800                  (["g"],
    801                   ("proceed immediately to propagating changes"),
    802                   (fun () -> newLine();
    803                      (ProceedImmediately, Safelist.rev_append prev ril)));
    804                  (["q"; keyEsc],
    805                   ("exit " ^ Uutil.myName ^ " without propagating any changes"),
    806                   (fun () -> newLine();
    807                      raise Sys.Break))
    808                 ]
    809                 (fun () -> displayri ri)
    810   in loop prilist rilist
    811 
    812 let verifyMerge title text =
    813   Util.set_infos "";
    814   Printf.printf "%s\n" text;
    815   if Prefs.read Globals.batch then
    816     true
    817   else begin
    818     if Prefs.read Uicommon.confirmmerge then begin
    819       display "Commit results of merge? ";
    820       selectAction
    821         None   (* Maybe better: (Some "n") *)
    822         [(["y";"g"],
    823           "Yes: commit",
    824           (fun() -> newLine();
    825              true));
    826          (["n"],
    827           "No: leave this file unchanged",
    828           (fun () -> newLine();
    829              false));
    830         ]
    831         (fun () -> display "Commit results of merge? ")
    832     end else
    833       true
    834   end
    835 
    836 let intrcount = ref 0
    837 let intrRequested () = !intrcount <> 0
    838 
    839 type stateItem =
    840   { mutable ri : reconItem;
    841     mutable bytesTransferred : Uutil.Filesize.t;
    842     mutable bytesToTransfer : Uutil.Filesize.t }
    843 
    844 let doTransport reconItemList numskip isSkip =
    845   let items =
    846     Array.map
    847       (fun ri ->
    848          {ri = ri;
    849           bytesTransferred = Uutil.Filesize.zero;
    850           bytesToTransfer = Common.riLength ri})
    851       (Array.of_list reconItemList)
    852   in
    853   let totalItemsTransferred = ref 0 in
    854   let totalItemsToTransfer = Array.length items - numskip in
    855   let totalItemsToTransferStr = string_of_int totalItemsToTransfer in
    856   let totalBytesTransferred = ref Uutil.Filesize.zero in
    857   let totalBytesToTransfer =
    858       (Array.fold_left
    859          (fun s item -> Uutil.Filesize.add item.bytesToTransfer s)
    860          Uutil.Filesize.zero items)
    861   in
    862   let totalBytesToTransferStr = Util.bytes2string
    863     (Uutil.Filesize.toInt64 totalBytesToTransfer) in
    864   let totalToTransfer =
    865     Uutil.Filesize.(add totalBytesToTransfer (ofInt totalItemsToTransfer)) in
    866   let sta = Uicommon.Stats.init totalBytesToTransfer in
    867   let calcProgress i bytes dbg =
    868     let i = Uutil.File.toLine i in
    869     let item = items.(i) in
    870     item.bytesTransferred <- Uutil.Filesize.add item.bytesTransferred bytes;
    871     totalBytesTransferred := Uutil.Filesize.add !totalBytesTransferred bytes;
    872     let totalTransferred =
    873       Uutil.Filesize.(add !totalBytesTransferred (ofInt !totalItemsTransferred)) in
    874     Uutil.Filesize.percentageOfTotalSize totalTransferred totalToTransfer
    875   in
    876   let tlog = ref (Unix.gettimeofday ()) in
    877   let t = ref 0. in
    878   let prevItems = ref 0 in
    879   let displayProgress v =
    880     let t1 = Unix.gettimeofday () in
    881     let () = Uicommon.Stats.update sta t1 !totalBytesTransferred in
    882     if t1 -. !t >= 0.1 || !prevItems <> !totalItemsTransferred then begin
    883       t := t1;
    884       prevItems := !totalItemsTransferred;
    885       let remTime =
    886         if v <= 0. then "--:--"
    887         else if v >= 100. then "00:00:00"
    888         else
    889           let rate = Uicommon.Stats.avgRate1 sta in
    890           if Float.is_nan rate then "--:--"
    891           else
    892             Format.sprintf "%8s/s    %s"
    893               (Util.bytes2string (Int64.of_float rate))
    894               (Uicommon.Stats.eta sta "--:--")
    895       in
    896       let totalBytesTransferredStr = Util.bytes2string
    897         (Uutil.Filesize.toInt64 !totalBytesTransferred) in
    898       let s = Format.sprintf "%s  %d/%s  (%s of %s)  %s ETA"
    899         (Util.percent2string v)
    900         !totalItemsTransferred totalItemsToTransferStr
    901         totalBytesTransferredStr totalBytesToTransferStr remTime in
    902 
    903       if not (Prefs.read Trace.terse) && (Prefs.read Trace.debugmods = []) then
    904         Util.set_infos s;
    905       if (Prefs.read Trace.terse) || (Prefs.read Globals.batch) then
    906         if (t1 -. !tlog) >= 60. then
    907         begin
    908           Trace.logonly (s ^ "\n");
    909           tlog := t1
    910         end
    911     end
    912   in
    913   let showProgress i bytes dbg =
    914     let v = calcProgress i bytes dbg in
    915     displayProgress v
    916   in
    917   Uutil.setProgressPrinter showProgress;
    918 
    919   let sigtermHandler _ =
    920     if !intrcount >= 3 then raise Sys.Break;
    921     Abort.all ();
    922     incr intrcount
    923   in
    924   let ctrlCHandler n =
    925     sigtermHandler n;
    926     if !intrcount = 1 then
    927       let s = "\n\nUpdate propagation interrupted. It may take a while \
    928         to stop.\nIf the process doesn't stop soon then wait or press \
    929         Ctrl-C\n3 more times to force immediate termination.\n\n\n" in
    930       (* Don't use [Printf.*printf] or [Format.*printf] (or other functions
    931          which use [Stdlib.out_channel]) because this can cause a deadlock
    932          with other outputting functions (in this case most likely at
    933          [Util.set_infos] called in [showProgress]) before OCaml 4.12. *)
    934       try Unix.write_substring Unix.stdout s 0 (String.length s) |> ignore
    935       with Unix.Unix_error _ -> ()
    936   in
    937   let stopAtIntr f =
    938     let signal_noerr signa behv =
    939       try Some (Sys.signal signa behv)
    940       with Sys_error _ | Invalid_argument _ -> None
    941     in
    942     let restore_noerr signa = function
    943     | Some prevSig -> ignore (signal_noerr signa prevSig)
    944     | None -> ()
    945     in
    946     let prevSigInt = signal_noerr Sys.sigint (Signal_handle ctrlCHandler) in
    947     let prevSigTerm = signal_noerr Sys.sigterm (Signal_handle sigtermHandler) in
    948     let restoreSig () =
    949       (* Set handlers will still raise [Sys.Break]; can ignore errors here. *)
    950       restore_noerr Sys.sigint prevSigInt;
    951       restore_noerr Sys.sigterm prevSigTerm
    952     in
    953 
    954     try f (); restoreSig ()
    955     with e ->
    956       let origbt = Printexc.get_raw_backtrace () in
    957       restoreSig ();
    958       Printexc.raise_with_backtrace e origbt
    959   in
    960 
    961   if not (Prefs.read Trace.terse) && (Prefs.read Trace.debugmods = []) then
    962     Util.set_infos "Starting...";
    963   Uicommon.transportStart ();
    964   let fFailedPaths = ref [] in
    965   let fPartialPaths = ref [] in
    966   let notstarted = ref (Array.length items) in
    967   let progressItem i =
    968     incr totalItemsTransferred;
    969     showProgress (Uutil.File.ofLine i) Uutil.Filesize.zero "itm"
    970   in
    971   let uiWrapper i item =
    972     Lwt.try_bind
    973       (fun () -> decr notstarted;
    974                  Transport.transportItem item.ri
    975                    (Uutil.File.ofLine i) verifyMerge)
    976       (fun () ->
    977          let notSkip = not (isSkip item.ri) in
    978          if partiallyProblematic item.ri && notSkip then
    979            fPartialPaths := item.ri.path1 :: !fPartialPaths;
    980          if notSkip then progressItem i;
    981          Lwt.return ())
    982       (fun e ->
    983         if not (isSkip item.ri) then progressItem i;
    984         match e with
    985           Util.Transient s ->
    986             let rem =
    987               Uutil.Filesize.sub
    988                 item.bytesToTransfer item.bytesTransferred
    989             in
    990             if rem <> Uutil.Filesize.zero then
    991               showProgress (Uutil.File.ofLine i) rem "done";
    992             let m = "[" ^ (Path.toString item.ri.path1)  ^ "]: " ^ s in
    993             Util.set_infos "";
    994             alwaysDisplay ("Failed " ^ m ^ "\n");
    995             fFailedPaths := item.ri.path1 :: !fFailedPaths;
    996             return ()
    997         | _ ->
    998             fail e) in
    999   stopAtIntr begin fun () ->
   1000     Uicommon.transportItems items (fun {ri; _} -> not (Common.isDeletion ri)) uiWrapper;
   1001     Uicommon.transportItems items (fun {ri; _} -> Common.isDeletion ri) uiWrapper
   1002   end;
   1003   Uicommon.transportFinish ();
   1004 
   1005   Uutil.setProgressPrinter (fun _ _ _ -> ());
   1006   Util.set_infos "";
   1007 
   1008   (Safelist.rev !fFailedPaths, Safelist.rev !fPartialPaths, !notstarted, !intrcount > 0)
   1009 
   1010 let setWarnPrinterForInitialization()=
   1011   Util.warnPrinter :=
   1012     Some (fun s -> alwaysDisplay ("Warning: " ^ s ^ "\n\n"))
   1013 
   1014 let setWarnPrinter() =
   1015   Util.warnPrinter :=
   1016     Some(fun s ->
   1017            Util.set_infos "";
   1018            alwaysDisplay "Warning: ";
   1019            alwaysDisplay (s^"\n");
   1020            if not (Prefs.read Globals.batch) then begin
   1021              display "Press return to continue.";
   1022              selectAction None
   1023                [(["";"";" ";"y"],
   1024                  ("Continue"),
   1025                  (fun () -> newLine()));
   1026                 (["n";"q";"x"],
   1027                  ("Exit"),
   1028                  (fun () -> newLine();
   1029                      restoreTerminal ();
   1030                      Lwt_unix.run (Update.unlockArchives ());
   1031                      exit Uicommon.fatalExit))]
   1032                (fun () -> display  "Press return to continue.")
   1033            end)
   1034 
   1035 let lastMajor = ref ""
   1036 
   1037 let formatStatus major minor =
   1038   let s =
   1039     if major = !lastMajor then "  " ^ minor
   1040     else major ^ (if minor="" then "" else "\n  " ^ minor)
   1041   in
   1042     lastMajor := major;
   1043     s
   1044 
   1045 let rec interactAndPropagateChanges prevItemList reconItemList
   1046       : bool * bool * bool * bool * (Path.t list)
   1047         (* anySkipped?, anyPartial?, anyFailures?, anyCancels?, failingPaths *) =
   1048   let (proceed,newReconItemList) = interact prevItemList reconItemList in
   1049   let isSkip = problematic in
   1050   let (updatesToDo, skipped, (totalBytesToRoot1, totalBytesToRoot2)) =
   1051     Safelist.fold_left
   1052       (fun (howmany, skipped, (bytes1, bytes2)) ri ->
   1053         if isSkip ri then (howmany, skipped + 1, (bytes1, bytes2))
   1054         else (howmany + 1, skipped,
   1055           match ri.replicas with
   1056           | Problem _ -> (bytes1, bytes2)
   1057           | Different {direction; _} ->
   1058               match direction with
   1059               | Conflict _ | Merge -> (bytes1, bytes2)
   1060               | Replica1ToReplica2 -> (bytes1, Uutil.Filesize.add (Common.riLength ri) bytes2)
   1061               | Replica2ToReplica1 -> (Uutil.Filesize.add (Common.riLength ri) bytes1, bytes2)))
   1062       (0, 0, (Uutil.Filesize.zero, Uutil.Filesize.zero)) newReconItemList in
   1063   if not (Prefs.read Trace.terse) && (updatesToDo > 0 || skipped > 0) then begin
   1064     let root1, root2 =
   1065       match Globals.roots () with
   1066       | (Local, path1), (Local, path2) -> Fspath.differentSuffix path1 path2
   1067       | (Local, _), (Remote host, _) -> "local", host
   1068       | (Remote host, _), (Local, _) -> host, "local"
   1069       | (Remote host1, _), (Remote host2, _) -> host1, host2
   1070     in
   1071     Trace.log_color (Printf.sprintf
   1072       "\n%s%d%s items will be synced, %s%d%s skipped\n\
   1073        %s to be synced from %s to %s\n\
   1074        %s to be synced from %s to %s\n"
   1075        (color `Focus) updatesToDo (color `Reset)
   1076        (color `Information) skipped (color `Reset)
   1077        (Util.bytes2string (Uutil.Filesize.toInt64 totalBytesToRoot2)) root1 root2
   1078        (Util.bytes2string (Uutil.Filesize.toInt64 totalBytesToRoot1)) root2 root1)
   1079   end;
   1080   let doTransp () =
   1081     try
   1082       doTransport newReconItemList skipped isSkip
   1083     with e ->
   1084       let origbt = Printexc.get_raw_backtrace () in
   1085       let summary =
   1086         "\nSynchronization "
   1087           ^ (color `Failure)
   1088           ^ (match e with Sys.Break -> "interrupted" | _ -> "failed")
   1089           ^ (color `Reset)
   1090           ^ (try let tm = Util.localtime (Util.time ()) in
   1091              Printf.sprintf " at %02d:%02d:%02d"
   1092                tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec with _ -> "")
   1093           ^ (match e with Sys.Break -> " by user request" | _ -> " due to a fatal error")
   1094           ^ "\n\n"
   1095       in
   1096       Util.set_infos "";
   1097       Trace.log_color summary;
   1098       Printexc.raise_with_backtrace e origbt
   1099   in
   1100   let doit() =
   1101     if not (Prefs.read Globals.batch || Prefs.read Trace.terse) then newLine();
   1102     if not (Prefs.read Trace.terse) then Trace.status "Propagating updates";
   1103     let timer = Trace.startTimer "Transmitting all files" in
   1104     let (failedPaths, partialPaths, notstarted, intr) = doTransp () in
   1105     let failures = Safelist.length failedPaths in
   1106     let partials = Safelist.length partialPaths in
   1107     Trace.showTimer timer;
   1108     if not (Prefs.read Trace.terse) then Trace.status "Saving synchronizer state";
   1109     Update.commitUpdates ();
   1110     let trans = updatesToDo - notstarted - failures in
   1111     let summary =
   1112       Printf.sprintf
   1113        "Synchronization %s at %s  (%d item%s transferred, %s%s, %s%s)"
   1114        (if failures = 0 && notstarted = 0 then (color `Success) ^ "complete" ^ (color `Reset)
   1115         else (color `Failure) ^ "incomplete" ^ (color `Reset))
   1116        (let tm = Util.localtime (Util.time()) in
   1117         Printf.sprintf "%02d:%02d:%02d"
   1118           tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec)
   1119        trans (if trans=1 then "" else "s")
   1120        (if partials <> 0 then
   1121           Format.sprintf "%d partially transferred, " partials
   1122         else
   1123           "")
   1124        (if skipped = 0 then "0 skipped" else (color `Information) ^ (Printf.sprintf "%d skipped" skipped) ^ (color `Reset))
   1125        (if failures = 0 then "0 failed" else (color `Failure) ^ (Printf.sprintf "%d failed" failures) ^ (color `Reset))
   1126        (if notstarted = 0 then "" else ", " ^ (color `Information) ^ (Printf.sprintf "%d not started" notstarted) ^ (color `Reset)) in
   1127     Trace.log_color (summary ^ "\n");
   1128     if skipped>0 then
   1129       Safelist.iter
   1130         (fun ri ->
   1131          match ri.replicas with
   1132            Problem r
   1133          | Different {rc1 = _; rc2 = _; direction = Conflict r; default_direction = _} ->
   1134             alwaysDisplayAndLog (Printf.sprintf "  skipped: %s (%s)"
   1135                                                 (Path.toString ri.path1) r)
   1136          | _ -> ())
   1137         newReconItemList;
   1138     if partials>0 then
   1139       Safelist.iter
   1140         (fun p ->
   1141            alwaysDisplayAndLog ("  partially transferred: " ^ Path.toString p))
   1142         partialPaths;
   1143     if failures>0 then
   1144       Safelist.iter
   1145         (fun p -> alwaysDisplayAndLog ("  failed: " ^ (Path.toString p)))
   1146         failedPaths;
   1147     if intr then raise Sys.Break; (* Make sure repeat mode is stopped *)
   1148     (skipped > 0, partials > 0, failures > 0, notstarted > 0, failedPaths) in
   1149   if updatesToDo = 0 then begin
   1150     (* BCP (3/09): We need to commit the archives even if there are
   1151        no updates to propagate because some files (in fact, if we've
   1152        just switched to DST on windows, a LOT of files) might have new
   1153        modtimes in the archive. *)
   1154     (* JV (5/09): Don't save the archive in repeat mode as it has some
   1155        costs and its unlikely there is much change to the archives in
   1156        this mode. *)
   1157     if !Update.foundArchives && Prefs.read Uicommon.repeat = `NoRepeat then
   1158       Update.commitUpdates ();
   1159     display "No updates to propagate\n";
   1160     if skipped > 0 then begin
   1161       let summary =
   1162         Printf.sprintf
   1163           "Synchronization %scomplete%s at %s  (0 items transferred, %s%d skipped%s, 0 failed)"
   1164           (color `Success)
   1165           (color `Reset)
   1166           (let tm = Util.localtime (Util.time()) in
   1167            Printf.sprintf "%02d:%02d:%02d"
   1168                           tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec)
   1169           (color `Information)
   1170           skipped
   1171           (color `Reset) in
   1172       Trace.log_color (summary ^ "\n");
   1173       Safelist.iter
   1174         (fun ri ->
   1175          match ri.replicas with
   1176            Problem r
   1177          | Different {rc1 = _; rc2 = _; direction = Conflict r; default_direction = _} ->
   1178             alwaysDisplayAndLog (Printf.sprintf "  skipped: %s (%s)"
   1179                                                 (Path.toString ri.path1) r)
   1180          | _ -> ())
   1181         newReconItemList
   1182       end;
   1183     (skipped > 0, false, false, false, [])
   1184   end else if proceed=ProceedImmediately then begin
   1185     doit()
   1186   end else
   1187     let rec askagain newReconItemList =
   1188       displayWhenInteractive "\nProceed with propagating updates? ";
   1189       selectAction
   1190         (* BCP: I find it counterintuitive that every other prompt except this one
   1191            would expect <CR> as a default.  But I got talked out of offering a
   1192            default here, because of safety considerations (too easy to press
   1193            <CR> one time too many). *)
   1194         (if Prefs.read Globals.batch then Some "y" else None)
   1195         [(["y";"g"],
   1196           "Yes: proceed with updates as selected above",
   1197           doit);
   1198          (["n"],
   1199           "No: go through reconciliation process again",
   1200           (fun () -> newLine();
   1201              Prefs.set Uicommon.auto false;
   1202              interactAndPropagateChanges [] newReconItemList));
   1203          (["p";"b"],
   1204           "go back to the last item of the reconciliation",
   1205           (fun () -> newLine();
   1206              Prefs.set Uicommon.auto false;
   1207              match Safelist.rev newReconItemList with
   1208                [] -> interactAndPropagateChanges [] []
   1209              | lastri::prev -> interactAndPropagateChanges prev [lastri]));
   1210          (["N"],
   1211           "sort by Name",
   1212           (fun () ->
   1213              Sortri.sortByName();
   1214              askagain (Sortri.sortReconItems newReconItemList)));
   1215          (["S"],
   1216           "sort by Size",
   1217           (fun () ->
   1218              Sortri.sortBySize();
   1219              askagain (Sortri.sortReconItems newReconItemList)));
   1220          (["W"],
   1221           "sort neW first (toggle)",
   1222           (fun () ->
   1223              Sortri.sortNewFirst();
   1224              askagain (Sortri.sortReconItems newReconItemList)));
   1225          (["D"],
   1226           "Default ordering",
   1227           (fun () ->
   1228              Sortri.restoreDefaultSettings();
   1229              askagain (Sortri.sortReconItems newReconItemList)));
   1230          (["R"],
   1231           "Reverse the sort order",
   1232           (fun () -> askagain (Safelist.rev newReconItemList)));
   1233          (["q"; keyEsc],
   1234           ("exit " ^ Uutil.myName ^ " without propagating any changes"),
   1235           (fun () -> newLine();
   1236              raise Sys.Break))
   1237         ]
   1238         (fun () -> display "Proceed with propagating updates? ")
   1239     in askagain newReconItemList
   1240 
   1241 let checkForDangerousPath dangerousPaths =
   1242   if Prefs.read Globals.confirmBigDeletes then begin
   1243     if dangerousPaths <> [] then begin
   1244       alwaysDisplayAndLog (Uicommon.dangerousPathMsg dangerousPaths);
   1245       if Prefs.read Globals.batch then begin
   1246           alwaysDisplay "Aborting...\n"; restoreTerminal ();
   1247           exit Uicommon.fatalExit
   1248       end else begin
   1249         displayWhenInteractive "Do you really want to proceed? ";
   1250         selectAction
   1251           None
   1252           [(["y"],
   1253             "Continue",
   1254             (fun () -> ()));
   1255            (["n";"q";"x";""],
   1256             "Exit",
   1257             (fun () -> alwaysDisplay "\n";
   1258                restoreTerminal ();
   1259                exit Uicommon.fatalExit))]
   1260           (fun () -> display "Do you really want to proceed? ")
   1261       end
   1262     end
   1263   end
   1264 
   1265 let displayWaitMessage () =
   1266   if not (Prefs.read silent) then
   1267     Util.msg "%s\n" (Uicommon.contactingServerMsg ())
   1268 
   1269 (* Most modern VT100 terminal emulators (and some ANSI) are able to switch
   1270    automatic line-wrapping off and on by control sequences ESC[?7l and ESC[?7h.
   1271    This here is a very blunt heuristic to filter out some that can't do it or
   1272    use a different control sequence. It does not need to be exact, as long as
   1273    it covers the vast majority of supported systems. *)
   1274 let termNowrapOk =
   1275   System.termVtCapable Unix.stdout &&
   1276   let s = try System.getenv "TERM" with Not_found -> "" in
   1277   not (
   1278     s = "dumb"
   1279     || s = "emacs"
   1280     || Util.startswith s "sun"
   1281     || Util.startswith s "cons"
   1282     || Util.startswith s "eterm"
   1283     || Util.startswith s "cygwin"
   1284     || Util.startswith s "dvtm"
   1285   )
   1286 
   1287 let synchronizeOnce ?wantWatcher pathsOpt =
   1288   let showStatus path =
   1289     if path = "" then Util.set_infos "" else
   1290     let shorten path =
   1291       let max_len = 70 in
   1292       let mid = (max_len - 3) / 2 in
   1293       let l = String.length path in
   1294       if l <= max_len then path else
   1295       String.sub path 0 (max_len - mid - 3) ^ "..." ^
   1296       String.sub path (l - mid) mid
   1297     in
   1298     let c = "-\\|/".[truncate (mod_float (4. *. Unix.gettimeofday ()) 4.)] in
   1299     if termNowrapOk && not (Prefs.read dumbtty) then
   1300       Util.set_infos (Format.sprintf "%c \027[?7l%s\027[?7h" c path) ~clr:"\r\027[K\r"
   1301     else
   1302       Util.set_infos (Format.sprintf "%c %s" c (shorten path))
   1303   in
   1304   Uicommon.connectRoots ~displayWaitMessage ();
   1305   Trace.status "Looking for changes";
   1306   if not (Prefs.read Trace.terse) && (Prefs.read Trace.debugmods = []) then
   1307     Uutil.setUpdateStatusPrinter (Some showStatus);
   1308 
   1309   debug (fun() -> Util.msg "temp: Globals.paths = %s\n"
   1310            (String.concat " "
   1311               (Safelist.map Path.toString (Prefs.read Globals.paths))));
   1312   let updates = Update.findUpdates ?wantWatcher pathsOpt in
   1313 
   1314   Uutil.setUpdateStatusPrinter None;
   1315   Util.set_infos "";
   1316 
   1317   let (reconItemList, anyEqualUpdates, dangerousPaths) =
   1318     Recon.reconcileAll ~allowPartial:true updates in
   1319 
   1320   if not !Update.foundArchives then Update.commitUpdates ();
   1321   if reconItemList = [] then begin
   1322     if !Update.foundArchives && Prefs.read Uicommon.repeat = `NoRepeat then
   1323       Update.commitUpdates ();
   1324     (if anyEqualUpdates then
   1325       Trace.status ("Nothing to do: replicas have been changed only "
   1326                     ^ "in identical ways since last sync.")
   1327      else
   1328        Trace.status "Nothing to do: replicas have not changed since last sync.");
   1329     (Uicommon.perfectExit, [])
   1330   end else begin
   1331     checkForDangerousPath dangerousPaths;
   1332     let (anySkipped, anyPartial, anyFailures, anyCancel, failedPaths) =
   1333       interactAndPropagateChanges [] reconItemList in
   1334     let exitStatus = Uicommon.exitCode (anySkipped || anyPartial || anyCancel, anyFailures) in
   1335     (exitStatus, failedPaths)
   1336   end
   1337 
   1338 (* ------------ Safe termination between synchronizations ------------ *)
   1339 
   1340 let safeStopReqd, requestSafeStop =
   1341   let safeStopReqd = ref false in
   1342   (* [safeStopReqd] can only go from false to true;
   1343      it must never be changed from true to false. *)
   1344   let isRequested () = !safeStopReqd
   1345   and request () = safeStopReqd := true in
   1346   isRequested, request
   1347 
   1348 (*** Requesting safe termination by signals ***)
   1349 
   1350 let set_signal_noerr signa nm behv =
   1351   try Sys.set_signal signa behv; true
   1352   with Invalid_argument _ | Sys_error _ as e ->
   1353     Trace.logonly
   1354       ("Warning: " ^ nm ^ " handler not set: " ^ (Printexc.to_string e) ^ "\n");
   1355     false
   1356 
   1357 let stopPipe = ref None
   1358 
   1359 let setupSafeStop () =
   1360   if supportSignals then begin
   1361     let safeStop _ =
   1362       if not (safeStopReqd ()) then begin
   1363         requestSafeStop ();
   1364         (* Interrupt the interruptible sleep *)
   1365         match !stopPipe with
   1366         | Some (i, o) -> Unix.close o; Lwt_unix.close i
   1367         | None -> ()
   1368       end
   1369     in
   1370     Util.blockSignals [Sys.sigusr2] (fun () ->
   1371     let ok = set_signal_noerr Sys.sigusr2 "SIGUSR2" (Signal_handle safeStop) in
   1372     if ok then stopPipe := Some (Lwt_unix.pipe_in ~cloexec:true ()))
   1373   end
   1374 
   1375 let safeStopRequested () =
   1376   safeStopReqd ()
   1377 
   1378 (*** Sleep interruptible by a termination request ***)
   1379 
   1380 let safeStopWait =
   1381   let safeStopWait_aux () =
   1382     let readStop =
   1383       match !stopPipe with
   1384       | None -> Lwt.wait ()
   1385       | Some (i, _) -> Lwt_unix.wait_read i
   1386     in
   1387     let readFail = function
   1388       | Unix.Unix_error (EBADF, _, _) -> Lwt.return (requestSafeStop ())
   1389       | e -> Lwt.fail e
   1390     in
   1391     let rec loop () =
   1392       Lwt.catch
   1393         (fun () -> readStop) readFail >>= fun () ->
   1394       if not (safeStopRequested ()) then
   1395         Lwt_unix.sleep 0.15 >>= loop
   1396       else
   1397         Lwt.return ()
   1398     in
   1399     loop ()
   1400   in
   1401   let wt = ref None in
   1402   fun () ->
   1403     match !wt with
   1404     | Some t -> t
   1405     | None -> let t = safeStopWait_aux () in wt := Some t; t
   1406 
   1407 let interruptibleSleepf dt =
   1408   Lwt_unix.run (Lwt.choose [Lwt_unix.sleep dt; safeStopWait ()])
   1409 let interruptibleSleep dt = interruptibleSleepf (float dt)
   1410 
   1411 (* ----------------- Filesystem watching mode ---------------- *)
   1412 
   1413 let watchinterval = 1.    (* Minimal interval between two synchronizations *)
   1414 let retrydelay = 5.       (* Minimal delay to retry failed paths *)
   1415 let maxdelay = 30. *. 60. (* Maximal delay to retry failed paths *)
   1416 
   1417 module PathMap = Map.Make (Path)
   1418 
   1419 let waitForChangesRoot: Common.root -> unit -> unit Lwt.t =
   1420   Remote.registerRootCmd
   1421     "waitForChanges" Umarshal.unit Umarshal.unit
   1422     (fun (fspath, _) -> Fswatchold.wait (Update.archiveHash fspath))
   1423 
   1424 let waitForChanges t =
   1425   let dt = t -. Unix.gettimeofday () in
   1426   if dt > 0. then begin
   1427     let timeout = if dt <= maxdelay then [Lwt_unix.sleep dt] else [] in
   1428     Lwt_unix.run
   1429       (Globals.allRootsMap (fun r -> Lwt.return (waitForChangesRoot r ()))
   1430          >>= fun l ->
   1431        Lwt.choose (timeout @ l @ [safeStopWait ()]))
   1432   end
   1433 
   1434 let synchronizePathsFromFilesystemWatcher fullintv =
   1435   let fullinterval = match fullintv with None -> 1e20 | Some i -> float i in
   1436   let rec loop lastFull delayInfo =
   1437     let t = Unix.gettimeofday () in
   1438     let sinceFull = t -. lastFull in
   1439     let isFull = sinceFull > fullinterval in
   1440     let lastFull = if isFull then t else lastFull in
   1441     let nextFull = lastFull +. fullinterval in
   1442     let (delayedPaths, readyPaths) =
   1443       PathMap.fold
   1444         (fun p (t', _) (delayed, ready) ->
   1445            if t' <= t then (delayed, p :: ready) else (p :: delayed, ready))
   1446         delayInfo ([], [])
   1447     in
   1448     let (exitStatus, failedPaths) =
   1449       synchronizeOnce ~wantWatcher:true
   1450         (if isFull then None else Some (readyPaths, delayedPaths))
   1451     in
   1452     (* After a failure, we retry at once, then use an exponential backoff *)
   1453     let delayInfo =
   1454       Safelist.fold_left
   1455         (fun newDelayInfo p ->
   1456            PathMap.add p
   1457              (try
   1458                 let (t', d) = PathMap.find p delayInfo in
   1459                 if t' > t then (t', d) else
   1460                 let d = max retrydelay (min maxdelay (2. *. d)) in
   1461                 (t +. d, d)
   1462               with Not_found ->
   1463                 (t, 0.))
   1464              newDelayInfo)
   1465         PathMap.empty
   1466         (Safelist.append delayedPaths failedPaths)
   1467     in
   1468     interruptibleSleepf watchinterval;
   1469     let nextTime =
   1470       PathMap.fold (fun _ (t, d) t' -> min t t') delayInfo nextFull in
   1471     if not (safeStopRequested ()) then waitForChanges nextTime;
   1472     if safeStopRequested () then exitStatus else loop lastFull delayInfo
   1473   in
   1474   loop 0. PathMap.empty
   1475 
   1476 (* ----------------- Repetition ---------------- *)
   1477 
   1478 let synchronizeUntilNoFailures repeatMode =
   1479   let wantWatcher = repeatMode in
   1480   let rec loop triesLeft pathsOpt =
   1481     let (exitStatus, failedPaths) =
   1482       synchronizeOnce ~wantWatcher pathsOpt in
   1483     if failedPaths <> [] && triesLeft <> 0
   1484          && not (repeatMode && safeStopRequested ()) then begin
   1485       loop (triesLeft - 1) (Some (failedPaths, []))
   1486     end else begin
   1487       exitStatus
   1488     end in
   1489   loop (Prefs.read Uicommon.retry) None
   1490 
   1491 let rec synchronizeUntilDone repeatinterval =
   1492   let exitStatus = synchronizeUntilNoFailures(repeatinterval >= 0) in
   1493   if repeatinterval < 0 || safeStopRequested () then
   1494     exitStatus
   1495   else begin
   1496     (* Do it again *)
   1497     Trace.status (Printf.sprintf
   1498        "\nSleeping for %d seconds...\n" repeatinterval);
   1499     interruptibleSleep repeatinterval;
   1500     if safeStopRequested () then exitStatus else synchronizeUntilDone repeatinterval
   1501   end
   1502 
   1503 let synchronizeUntilDone () =
   1504   match Prefs.read Uicommon.repeat with
   1505   | `Watch -> synchronizePathsFromFilesystemWatcher None
   1506   | `WatchAndInterval i -> synchronizePathsFromFilesystemWatcher (Some i)
   1507   | `Interval i -> synchronizeUntilDone i
   1508   | `NoRepeat -> synchronizeUntilDone (-1)
   1509   | `Invalid (_, e) -> raise e
   1510 
   1511 (* ----------------- Startup ---------------- *)
   1512 
   1513 let profmgrPrefName = "i"
   1514 let profmgrPref =
   1515   Prefs.createBool profmgrPrefName false
   1516     ~category:(`Basic `CLI)
   1517     ~cli_only:true
   1518     "interactive profile mode (text UI); command-line only"
   1519     ("Provide this preference in the command line arguments to enable "
   1520      ^ "interactive profile manager in the text user interface. Currently "
   1521      ^ "only profile listing and interactive selection are available. "
   1522      ^ "Preferences like \\texttt{batch} and \\texttt{silent} remain "
   1523      ^ "applicable to synchronization functionality.")
   1524 let profmgrUsageMsg = "To start interactive profile selection, type \""
   1525   ^ Uutil.myName ^ " -" ^ profmgrPrefName ^ "\"."
   1526 
   1527 let addProfileKeys list default =
   1528   let rec nextAvailKey i =
   1529     let n = i + 1 in
   1530     if n >= (Array.length Uicommon.profileKeymap) then
   1531       n
   1532     else
   1533       match Uicommon.profileKeymap.(n) with
   1534           None   -> n
   1535         | Some _ -> nextAvailKey n
   1536   in
   1537   let keyAndNext (p, info) i =
   1538     match info.Uicommon.key with
   1539       Some k -> (k, i)
   1540     | None   -> if p = default then ("d", i)
   1541                 else ((string_of_int i), (nextAvailKey i))
   1542   in
   1543   let rec addKey i acc = function
   1544   | []           -> []
   1545   | [prof]       -> let (key, _) = keyAndNext prof i in
   1546                       (key, prof) :: acc
   1547   | prof :: rest -> let (key, next) = keyAndNext prof i in
   1548                       addKey next ((key, prof) :: acc) rest
   1549   in
   1550   addKey 0 [] list
   1551 
   1552 let scanProfiles () =
   1553   let wp = !Util.warnPrinter in
   1554   (* Replace warn printer with something that doesn't quit
   1555      the UI just for errors in random scanned profiles. *)
   1556   Util.warnPrinter := Some (fun s -> alwaysDisplay ("Warning: " ^ s ^ "\n\n"));
   1557   let () = Uicommon.scanProfiles () in
   1558   Util.warnPrinter := wp
   1559 
   1560 let getProfile default =
   1561   let cmdArgs = Prefs.scanCmdLine Uicommon.shortUsageMsg in
   1562   if Util.StringMap.mem Uicommon.runTestsPrefName cmdArgs ||
   1563     not (Util.StringMap.mem profmgrPrefName cmdArgs) then
   1564     Some default
   1565   else
   1566   let () = scanProfiles () in
   1567   if (List.length !Uicommon.profilesAndRoots) > 10 then begin
   1568     Trace.log (Format.sprintf "You have too many profiles in %s \
   1569                 for interactive selection. Please specify profile \
   1570                 or roots on command line.\n"
   1571                 Util.unisonDir);
   1572     Trace.log "The profile names are:\n";
   1573     Safelist.iter (fun (p, _) -> Trace.log (Format.sprintf "  %s\n" p))
   1574       !Uicommon.profilesAndRoots;
   1575     Trace.log "\n";
   1576     Some default
   1577   end else if (List.length !Uicommon.profilesAndRoots) = 0 then
   1578     Some default
   1579   else
   1580 
   1581   let keyedProfileList = addProfileKeys
   1582     (Safelist.sort (fun (p, _) (p', _) -> compare p p')
   1583       !Uicommon.profilesAndRoots)
   1584     default in
   1585   let profileList = (Safelist.sort (fun (k, _) (k', _) -> compare k k')
   1586                       keyedProfileList)
   1587   in
   1588 
   1589   (* Must parse command line to get dumbtty and color preferences *)
   1590   Prefs.parseCmdLine Uicommon.shortUsageMsg;
   1591   setupTerminal(); setColorPreference ();
   1592   Prefs.resetToDefaults();
   1593 
   1594   display "Available profiles:\n key:  profilename         label\n";
   1595   Safelist.iteri
   1596     (fun n (key, (profile, info)) ->
   1597       let labeltext =
   1598           match info.Uicommon.label with None -> "" | Some l -> l in
   1599       display (Format.sprintf "  %s%s%s :"
   1600                 (color `Focus) key (color `Reset));
   1601       display (Format.sprintf "  %s%-18s%s  %s%s%s\n"
   1602                 (color `Focus) profile (color `Reset)
   1603                 (color `Information) labeltext (color `Reset));
   1604       Safelist.iteri
   1605           (fun i root -> display (Format.sprintf "         root %i = %s\n"
   1606                                    (i + 1) root))
   1607           info.Uicommon.roots
   1608     )
   1609     profileList;
   1610   display "\n";
   1611 
   1612   let selection = ref (Some default) in
   1613   let actions = Safelist.append
   1614     [(["";"n";"/"],
   1615       "Don't select any profile",
   1616       (fun () -> selection := None; newLine();
   1617                    display "\nNo profile selected\n\n"));
   1618      (["q"],
   1619       ("exit " ^ Uutil.myName),
   1620       (fun () -> newLine(); raise Sys.Break))]
   1621     (Safelist.map (fun (key, (profile, info)) ->
   1622         ([key],
   1623         "Profile: " ^ profile,
   1624         (fun () -> selection := Some profile; newLine();
   1625                      display ("\nProfile " ^ profile ^ " selected\n\n")))
   1626       )
   1627       profileList);
   1628   in
   1629   let rec askProfile () =
   1630     display "Select a profile ";
   1631     selectAction None actions (fun () -> display "Select a profile ")
   1632   in
   1633   askProfile ();
   1634   !selection
   1635 
   1636 let handleException e =
   1637   (* Keep the current status line (if any) and don't repeat it any more *)
   1638   alwaysDisplay "\n";
   1639   Util.set_infos "";
   1640   restoreTerminal();
   1641   let lbl =
   1642     if e = Sys.Break then ""
   1643     else "Error: " in
   1644   let msg = lbl ^ Uicommon.exn2string e in
   1645   let () =
   1646     try Trace.log (msg ^ "\n")
   1647     with Util.Fatal _ -> () in (* Can't allow fatal errors in fatal error handler *)
   1648   if not !Trace.sendLogMsgsToStderr then alwaysDisplay ("\n" ^ msg ^ "\n")
   1649 
   1650 let rec start interface =
   1651   if interface <> Uicommon.Text then
   1652     Util.msg "This Unison binary only provides the text GUI...\n";
   1653   begin try
   1654     Sys.catch_break true;
   1655     (* Just to make sure something is there... *)
   1656     setWarnPrinterForInitialization();
   1657     setupSafeStop ();
   1658     let errorOut s =
   1659       Util.msg "%s%s%s\n" Uicommon.shortUsageMsg profmgrUsageMsg s;
   1660       exit 1
   1661     in
   1662     let profileName = match Uicommon.uiInitClRootsAndProfile () with
   1663       | Error s -> errorOut ("\n\n" ^ s)
   1664       | Ok None ->
   1665           let profile = getProfile "default" in
   1666           let () = restoreTerminal () in
   1667           begin
   1668             match profile with
   1669             | None -> exit 0
   1670             | Some x -> x
   1671           end
   1672       | Ok (Some s) -> s
   1673     in
   1674     Uicommon.initPrefs ~profileName ~promptForRoots:(fun () -> errorOut "") ()
   1675   with e ->
   1676     handleException e;
   1677     exit Uicommon.fatalExit
   1678   end;
   1679 
   1680   (* Some preference settings imply others... *)
   1681   if Prefs.read silent then begin
   1682     Prefs.set Globals.batch true;
   1683     Prefs.set Trace.terse true;
   1684     Prefs.set dumbtty true;
   1685     Trace.sendLogMsgsToStderr := false;
   1686   end;
   1687   if Prefs.read Uicommon.repeat <> `NoRepeat then begin
   1688     Prefs.set Globals.batch true;
   1689   end;
   1690   setColorPreference ();
   1691   Trace.statusFormatter := formatStatus;
   1692 
   1693   start2 ()
   1694 
   1695 (* Uncaught exceptions up to this point are non-recoverable, treated
   1696    as permanent and will inevitably exit the process. Uncaught exceptions
   1697    from here onwards are treated as potentially temporary or recoverable.
   1698    The process does not have to exit if in repeat mode and can try again. *)
   1699 and start2 () =
   1700   let noRepeat =
   1701     true || (* Disabled by default until a better retry strategy is devised *)
   1702     Prefs.read Uicommon.repeat = `NoRepeat
   1703       || Prefs.read Uicommon.runtests
   1704       || Prefs.read Uicommon.testServer
   1705   in
   1706   let terminate () =
   1707     handleException Sys.Break;
   1708     exit Uicommon.fatalExit
   1709   in
   1710   begin try
   1711     Uicommon.connectRoots ~displayWaitMessage ();
   1712 
   1713     if Prefs.read Uicommon.testServer then exit 0;
   1714 
   1715     (* Run unit tests if requested *)
   1716     if Prefs.read Uicommon.runtests then begin
   1717       !Uicommon.testFunction ();
   1718       exit 0
   1719     end;
   1720 
   1721     (* Tell OCaml that we want to catch Control-C ourselves, so that
   1722        we get a chance to reset the terminal before exiting *)
   1723     Sys.catch_break true;
   1724     (* Put the terminal in cbreak mode if possible *)
   1725     if not (Prefs.read Globals.batch) then setupTerminal();
   1726     setWarnPrinter();
   1727 
   1728     let exitStatus = synchronizeUntilDone() in
   1729 
   1730     (* Put the terminal back in "sane" mode, if necessary, and quit. *)
   1731     restoreTerminal();
   1732     exit exitStatus
   1733   with
   1734   | Sys.Break -> terminate ()
   1735   | e when noRepeat || breakRepeat e || intrRequested () -> begin
   1736       handleException e;
   1737       exit Uicommon.fatalExit
   1738     end
   1739   | e -> begin
   1740       (* If any other bad thing happened and the -repeat preference is
   1741          set, then restart *)
   1742       handleException e;
   1743 
   1744       Util.msg "\nRestarting in 10 seconds...\n\n";
   1745       begin try interruptibleSleep 10 with Sys.Break -> terminate () end;
   1746       if safeStopRequested () then terminate () else start2 ()
   1747     end
   1748   end
   1749 
   1750 (* Though in some cases we could, there's no point in recovering
   1751    and continuing at any of these exceptions. *)
   1752 and breakRepeat = function
   1753   (* Programming errors *)
   1754   | Assert_failure _
   1755   | Match_failure _
   1756   | Invalid_argument _
   1757   | Fun.Finally_raised _
   1758   (* Async exceptions *)
   1759   | Out_of_memory
   1760   | Stack_overflow
   1761   | Sys.Break -> true
   1762   | _ -> false
   1763 
   1764 let defaultUi = Uicommon.Text
   1765 
   1766 end