MODULE; IMPORT BackoffTimer, CVProto, Date, Env, ErrMsg, Fmt, FS, FSClient, IO, IP, LockFile, Logger, OSError, Params, Pathname, Process, Stdio, SupFile, SupFileRec, SupGUI, SupMisc, Text, Thread, Time, TokScan, Version, WrLogger; CONST DefaultVerbosity = 1; ThreadStackSize = 16 * 1024; (* In units of Word.T *) EXCEPTION Error(TEXT); PROCEDURE Main CheckPort (port: INTEGER) = BEGIN IF port = IP.NullPort OR NOT (FIRST(IP.Port) <= port AND port <= LAST(IP.Port)) THEN ErrMsg.Fatal("Invalid port " & Fmt.Int(port)); END; IF port < 1024 THEN ErrMsg.Fatal("Reserved port " & Fmt.Int(port) & " not permitted"); END; END CheckPort; PROCEDUREFetchArg (VAR argNum, argPos: CARDINAL): TEXT = VAR arg := Params.Get(argNum); argLen := Text.Length(arg); value: TEXT; BEGIN IF argPos < argLen THEN (* There was no whitespace after the flag. *) value := Text.Sub(arg, argPos); argPos := argLen; ELSE (* The value is in a separate argument. *) INC(argNum); IF argNum >= Params.Count THEN Usage(); END; value := Params.Get(argNum); END; RETURN value; END FetchArg; PROCEDUREOverrideOff (config: FSClient.Configuration; option: SupFileRec.Option) = BEGIN WITH options = SupFileRec.Options{option} DO config.override.options := config.override.options - options; config.overrideMask := config.overrideMask + options; END; END OverrideOff; PROCEDUREOverrideOn (config: FSClient.Configuration; option: SupFileRec.Option) = BEGIN WITH options = SupFileRec.Options{option} DO config.override.options := config.override.options + options; config.overrideMask := config.overrideMask + options; END; END OverrideOn; PROCEDUREUsage (msg: TEXT := NIL) = VAR prog := Pathname.Last(Params.Get(0)); BEGIN IF msg # NIL THEN msg := msg & "\n"; ELSE msg := ""; END; ErrMsg.Fatal(msg & "Usage: " & prog & " [options] supfile [destDir]" & "\n Options:" & "\n -1 Don't retry automatically on failure " & "(same as \"-r 0\")" & "\n -a Require server to authenticate itself to us" & "\n -A addr Bind local socket to a specific address" & "\n -b base Override supfile's \"base\" directory" & "\n -c collDir Subdirectory of \"base\" for collections" & " (default \"" & SupMisc.DefaultClientCollDir & "\")" & "\n -d delLimit Allow at most \"delLimit\" file deletions" & " (default unlimited)" & "\n -D Do file deletions only; skip all other updates" & "\n (not implemented for checkout mode)" & "\n -e Enable shell command execution for all collections" & "\n -E Disable shell command execution for all collections" & "\n -g Don't use the GUI (implied if $DISPLAY is not set)" & "\n -h host Override supfile's \"host\" name" & "\n -i pattern Include only files/directories matching pattern." & "\n May be repeated for an OR operation. Default is" & "\n to include each entire collection." & "\n -k Keep bad temporary files when fixups are required" & "\n -l lockfile Lock file during update; fail if already locked" & "\n -L n Verbosity level for \"-g\" (" & Fmt.Int(FIRST(Verbosity)) & ".." & Fmt.Int(LAST(Verbosity)) & ", default " & Fmt.Int(DefaultVerbosity) & ")" & "\n -p port Alternate server port (default " & Fmt.Int(SupMisc.Port) & ")" & "\n -P range Range for client data ports (default unrestricted)" & "\n Range can be a single port number or a range" & "\n \"lo-hi\". A range of \"-\" selects passive mode." & "\n A range of \"a\" selects active mode. " & "A range of \"m\"" & "\n selects multiplexed mode (the default)." & "\n -r n Maximum retries on transient errors (default " & "unlimited)" & "\n -s Don't stat client files; trust the checkouts file" & "\n -v Print version and exit" & "\n -x Require detailed compare for all RCS files" & "\n -z Enable compression for all collections" & "\n -Z Disable compression for all collections" ); END Usage; TYPE Verbosity = [0..2]; VAR useGUI := SupGUI.Supported; justPrintVersion := FALSE; lockFile: Pathname.T := NIL; lock: LockFile.T := NIL; verbosity: Verbosity := DefaultVerbosity; config := NEW(FSClient.Configuration); supFile: TEXT := NIL; arg: TEXT; argVal: TEXT; pos: INTEGER; argNum: CARDINAL; argPos: CARDINAL; argLen: CARDINAL; option: CHAR; retryCount := 0; maxRetries := -1; (* Infinity. *) BEGIN config.override := NEW(SupFileRec.T).init(); WITH m3socks = Env.Get("M3SOCKS") DO IF m3socks # NIL AND NOT Text.Empty(m3socks) THEN config.connectMode := FSClient.ConnectMode.Socks; END; END; TRY argNum := 1; WHILE argNum < Params.Count DO arg := Params.Get(argNum); argLen := Text.Length(arg); IF argLen > 0 AND Text.GetChar(arg, 0) = '-' THEN argPos := 1; WHILE argPos < argLen DO option := Text.GetChar(arg, argPos); INC(argPos); CASE option OF | '1' => maxRetries := 0; | 'a' => config.authRequired := TRUE; | 'A' => WITH t = FetchArg(argNum, argPos) DO TRY IF NOT SupMisc.ParseHost(t, config.localEndpoint.addr) THEN ErrMsg.Fatal(t & ": host unknown"); END; EXCEPT IP.Error(list) => ErrMsg.Fatal(t & ": " & ErrMsg.StrError(list)); END; END; | 'b' => config.override.clientBase := FetchArg(argNum, argPos); | 'c' => config.override.clientCollDir := FetchArg(argNum, argPos); | 'd' => config.deleteLimit := TokScan.AtoI(FetchArg(argNum, argPos), "deletion limit"); | 'D' => OverrideOn(config, SupFileRec.Option.DoDeletesOnly); | 'e' => OverrideOn(config, SupFileRec.Option.Execute); | 'E' => OverrideOff(config, SupFileRec.Option.Execute); | 'g' => useGUI := FALSE; | 'h' => config.override.serverHost := FetchArg(argNum, argPos); | 'i' => config.override.accepts.addhi(FetchArg(argNum, argPos)); | 'k' => OverrideOn(config, SupFileRec.Option.KeepBadFiles); | 'l' => lockFile := FetchArg(argNum, argPos); | 'L' => WITH n = TokScan.AtoI(FetchArg(argNum, argPos), "verbosity") DO IF n > LAST(Verbosity) THEN verbosity := LAST(Verbosity); ELSE verbosity := n; END; END; | 'p' => config.port := TokScan.AtoI(FetchArg(argNum, argPos), "server port"); CheckPort(config.port); | 'P' => argVal := FetchArg(argNum, argPos); IF Text.Equal(argVal, "-") THEN (* Passive mode. *) IF config.connectMode = FSClient.ConnectMode.Socks THEN ErrMsg.Fatal("\"-P -\" is incompatible with SOCKS mode"); END; config.connectMode := FSClient.ConnectMode.Passive; ELSIF Text.Equal(argVal, "a") THEN (* Active mode. *) IF config.connectMode = FSClient.ConnectMode.Socks THEN ErrMsg.Fatal("\"-P a\" is incompatible with SOCKS mode"); END; config.connectMode := FSClient.ConnectMode.Active; ELSIF Text.Equal(argVal, "m") THEN (* Multiplexed mode. *) (* We purposely don't check for SOCKS mode here. Multiplexed mode should work fine without any special processing for SOCKS. *) config.connectMode := FSClient.ConnectMode.Mux; ELSE (* FIXME - Remove this once SOCKS can handle it. *) IF config.connectMode = FSClient.ConnectMode.Socks THEN ErrMsg.Fatal("\"-P\" is not yet supported in SOCKS mode"); END; (**) pos := Text.FindChar(argVal, '-'); IF pos >= 0 THEN config.loDataPort := TokScan.AtoI(Text.Sub(argVal, 0, pos), "low data port"); CheckPort(config.loDataPort); config.hiDataPort := TokScan.AtoI(Text.Sub(argVal, pos+1), "high data port"); CheckPort(config.hiDataPort); IF config.loDataPort > config.hiDataPort THEN ErrMsg.Fatal("Invalid data port range " & argVal); END; ELSE config.loDataPort := TokScan.AtoI(argVal, "data port"); CheckPort(config.loDataPort); config.hiDataPort := config.loDataPort; END; config.connectMode := FSClient.ConnectMode.Active; END; | 'r' => maxRetries := TokScan.AtoI(FetchArg(argNum, argPos), "retry limit"); | 's' => OverrideOn(config, SupFileRec.Option.TrustStatusFile); | 'v' => justPrintVersion := TRUE; | 'x' => OverrideOn(config, SupFileRec.Option.DetailAllRCSFiles); | 'z' => OverrideOn(config, SupFileRec.Option.Compress); | 'Z' => OverrideOff(config, SupFileRec.Option.Compress); ELSE Usage("Invalid option \"-" & Text.FromChar(option) & "\""); END; END; ELSE IF supFile = NIL THEN supFile := arg; ELSIF config.destDir = NIL THEN config.destDir := arg; ELSE Usage(); END; END; INC(argNum); END; EXCEPT TokScan.Error(msg) => Usage(msg); END; IF justPrintVersion THEN IF SupGUI.Supported THEN IO.Put("CVSup client, GUI version\n"); ELSE IO.Put("CVSup client, non-GUI version\n"); END; IO.Put("Copyright 1996-2003 John D. Polstra\n"); IO.Put("Software version: " & Version.Name & "\n"); IO.Put("Protocol version: " & Fmt.Int(CVProto.Current.major) & "." & Fmt.Int(CVProto.Current.minor) & "\n"); IO.Put("Operating system: " & Version.Target & "\n"); IO.Put("https://www.cvsup.org/\n"); IO.Put("Report problems to cvsup-bugs@polstra.com\n"); IO.Put("CVSup is a registered trademark of John D. Polstra\n"); Process.Exit(0); END; IF supFile = NIL THEN Usage(); END; IF config.destDir # NIL THEN TRY IF FS.Status(config.destDir).type # FS.DirectoryFileType THEN ErrMsg.Fatal("\"" & config.destDir & "\" is not a directory"); END; EXCEPT OSError.E => ErrMsg.Fatal("Directory \"" & config.destDir & "\" does not exist"); END; END; IF useGUI THEN WITH display = Env.Get("DISPLAY") DO IF display = NIL OR Text.Empty(display) THEN useGUI := FALSE; END; END; END; (* Increase the default stack size for all threads. *) Thread.MinDefaultStackSize(ThreadStackSize); TRY IF useGUI THEN TRY config.lockFile := lockFile; (* Tell client to do its own locking. *) SupGUI.Run(supFile := supFile, config := config); EXCEPT | SupGUI.Error(msg) => ErrMsg.Fatal(msg); END; ELSE VAR bt: BackoffTimer.T; clientStatus: SupMisc.ThreadStatus; BEGIN CASE verbosity OF | 0 => (* Errors only *) config.trace := NEW(WrLogger.T).init(Stdio.stdout, Logger.Priority.Warning); config.updaterTrace := config.trace; config.listerTrace := config.trace; config.detailerTrace := config.trace; | 1 => (* Files updated *) config.trace := NEW(WrLogger.T).init(Stdio.stdout, Logger.Priority.Notice); config.updaterTrace := config.trace; config.listerTrace := config.trace; config.detailerTrace := config.trace; | 2 => (* File update details *) config.trace := NEW(WrLogger.T).init(Stdio.stdout, Logger.Priority.Info); config.updaterTrace := config.trace; config.listerTrace := NEW(WrLogger.T).init(Stdio.stdout, Logger.Priority.Notice); config.detailerTrace := config.listerTrace; END; TRY IF lockFile # NIL THEN TRY lock := LockFile.Lock(lockFile); EXCEPT OSError.E(l) => RAISE Error("Error locking \"" & lockFile & "\": " & ErrMsg.StrError(l)); END; IF lock = NIL THEN (* Already locked. *) RAISE Error("\"" & lockFile & "\" is already locked by another process"); END; END; TRY TRY Logger.Info(config.trace, "Parsing supfile \"" & supFile & "\""); config.collections := SupFile.Parse(supFile, override := config.override, mask := config.overrideMask); EXCEPT SupFile.Error(msg) => RAISE Error(msg); END; bt := BackoffTimer.New( min := 300.0d0, (* 5 minutes *) max := 7200.0d0, (* 2 hours *) backoff := 2.0, jitter := 0.1); LOOP clientStatus := NEW(FSClient.T).init(config).apply(); IF clientStatus.status # SupMisc.ExitCode.TransientFailure OR maxRetries >= 0 AND retryCount >= maxRetries THEN EXIT; END; WITH nextTry = Date.FromTime(Time.Now()+BackoffTimer.Get(bt)) DO Logger.Warning(config.trace, "Will retry at" & " " & Fmt.Pad(Fmt.Int(nextTry.hour), 2, '0') & ":" & Fmt.Pad(Fmt.Int(nextTry.minute), 2, '0') & ":" & Fmt.Pad(Fmt.Int(nextTry.second), 2, '0')); END; BackoffTimer.AlertPause(bt); Logger.Notice(config.trace, "Retrying"); INC(retryCount); END; FINALLY IF lock # NIL THEN TRY LockFile.Unlock(lock); EXCEPT OSError.E(l) => RAISE Error("Error unlocking \"" & lockFile & "\": " & ErrMsg.StrError(l)); END; END; END; IF clientStatus.status # SupMisc.ExitCode.Success THEN Process.Exit(1); END; EXCEPT Error(msg) => Logger.Err(config.trace, msg); Process.Exit(1); END; END; END; EXCEPT | Thread.Alerted => ErrMsg.Fatal("Interrupted"); END; END Main.