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