MODULE; IMPORT Auth, AuthMD5, ChannelMux, CVProto, Detailer, Env, ErrMsg, FileAttr, Fmt, Glob, GlobTree, IOWatchDog, IP, LockFile, Logger, OSError, Pathname, Process, RCSKeyword, Rd, Reaper, SigHandler, StreamRd, StreamWr, SupFileRec, SupMisc, SyncFixupQueue, TCPMisc, Text, Thread, TokScan, TreeList, UnixMisc, Updater, Usignal, Version, WatchDog, Wr; IMPORT ConnRW; IMPORT TCP; EXCEPTION Error(TEXT); TransientFailure(TEXT); CONST AuthFile = ".cvsup/auth"; IdleTimeout = 15.0d0 * 60.0d0; S1GUrl = "https://www.cvsup.org/s1g/"; REVEAL T = Public BRANDED OBJECT config: Configuration; server: TEXT; peer: IP.Endpoint; proto: CVProto.T := NIL; tcp0, tcp1, tcp2, tcp3: TCP.T := NIL; mux: ChannelMux.T := NIL; rdA, rdB, oldRdA: StreamRd.T := NIL; wrA, wrB, oldWrA: StreamWr.T := NIL; watchDog: IOWatchDog.T := NIL; authPath: Pathname.T; authDB: Auth.DB := NIL; OVERRIDES init := Init; apply := Apply; END; PROCEDURE FSClient Init (self: T; config: Configuration): T = BEGIN self.config := config; IF self.config.hiDataPort = IP.NullPort THEN self.config.hiDataPort := self.config.loDataPort; END; RETURN self; END Init; PROCEDUREApply (self: T): REFANY = VAR fixups: SyncFixupQueue.T; reaper: Reaper.T; lister: TreeList.T; detailer: Detailer.T; updater: Updater.T; listerThread: Thread.T; detailerThread: Thread.T; updaterThread: Thread.T; threadStatus: SupMisc.ThreadStatus; lock: LockFile.T := NIL; homeDir: Pathname.T; killer: Killer := NIL; retVal: REFANY; thread: Thread.T; BEGIN TRY IF self.config.lockFile # NIL THEN TRY lock := LockFile.Lock(self.config.lockFile); EXCEPT OSError.E(l) => RAISE Error("Error locking \"" & self.config.lockFile & "\": " & ErrMsg.StrError(l)); END; IF lock = NIL THEN (* Already locked. *) RAISE Error("\"" & self.config.lockFile & "\" is already locked by another process"); END; END; TRY homeDir := Env.Get("HOME"); IF homeDir = NIL THEN RAISE Error("Environment variable $\"HOME\" is not set"); END; self.authPath := SupMisc.ResolvePath(homeDir, AuthFile); TRY self.authDB := Auth.Open(self.authPath); EXCEPT Auth.Error(msg) => IF self.config.authRequired THEN RAISE Error(msg); END; END; TRY IF CheckCollections(self) = 0 THEN RAISE Error("No collections selected"); END; self.server := self.config.collections.get(0).serverHost; TRY EstablishControlConnection(self); self.watchDog := NEW(IOWatchDog.T).init(timeout := IdleTimeout); TRY IOWatchDog.AddRd(self.watchDog, self.rdA); IOWatchDog.AddWr(self.watchDog, self.wrA); ShakeHands(self); AdjustForProtocol(self); Authorize(self); ExchangeAttributeInfo(self); ExchangeCollectionInfo(self); EstablishDataConnection(self); reaper := NEW(Reaper.T).init(); fixups := NEW(SyncFixupQueue.T).init(); lister := NEW(TreeList.T).init( proto := self.proto, wr := self.wrA, collections := self.config.collections, reaper := reaper, stats := self.config.listerStats, trace := self.config.listerTrace); detailer := NEW(Detailer.T).init( proto := self.proto, rd := self.rdA, wr := self.wrB, collections := self.config.collections, fixups := fixups, reaper := reaper, stats := self.config.detailerStats, logger := self.config.detailerTrace); updater := NEW(Updater.T).init( proto := self.proto, rd := self.rdB, collections := self.config.collections, fixups := fixups, deleteLimit := self.config.deleteLimit, reaper := reaper, destDir := self.config.destDir, stats := self.config.updaterStats, trace := self.config.updaterTrace); TRY killer := NEW(Killer, thread := Thread.Self()); IF NOT UnixMisc.SigIsIgnored(Usignal.SIGINT) THEN SigHandler.Register(Usignal.SIGINT, killer); END; IF NOT UnixMisc.SigIsIgnored(Usignal.SIGHUP) THEN SigHandler.Register(Usignal.SIGHUP, killer); END; IF NOT UnixMisc.SigIsIgnored(Usignal.SIGTERM) THEN SigHandler.Register(Usignal.SIGTERM, killer); END; (* Start the worker threads. *) listerThread := Reaper.Fork(reaper, lister); detailerThread := Reaper.Fork(reaper, detailer); updaterThread := Reaper.Fork(reaper, updater); TRY Trace(self, "Running"); (* Wait until all the threads have finished, or until an error is returned from one of them. *) TRY threadStatus := NIL; WHILE Reaper.AlertJoinNext(reaper, thread, retVal) DO threadStatus := retVal; IF threadStatus.status # SupMisc.ExitCode.Success THEN Err(self, threadStatus.message); Reaper.AlertAll(reaper); (* Kill remaining threads. *) EXIT END; END; EXCEPT Thread.Alerted => (* Interrupted from GUI or signal. *) Reaper.AlertAll(reaper); Trace(self, "Cleaning up ..."); RAISE Thread.Alerted; END; FINALLY (* Reap all the remaining threads. *) WHILE Reaper.JoinNext(reaper, thread, retVal) DO (* Nothing *) END; END; FINALLY IF NOT UnixMisc.SigIsIgnored(Usignal.SIGINT) THEN SigHandler.Register(Usignal.SIGINT, NIL); END; IF NOT UnixMisc.SigIsIgnored(Usignal.SIGHUP) THEN SigHandler.Register(Usignal.SIGHUP, NIL); END; IF NOT UnixMisc.SigIsIgnored(Usignal.SIGTERM) THEN SigHandler.Register(Usignal.SIGTERM, NIL); END; END; IF threadStatus.status = SupMisc.ExitCode.Success THEN Trace(self, "Shutting down connection to server"); Wr.Close(self.wrA); self.wrA := NIL; Wr.Close(self.wrB); self.wrB := NIL; IF NOT Rd.EOF(self.rdA) THEN RAISE Error("Detailer protocol error: Expected EOF, " & "didn't get it"); END; IF NOT Rd.EOF(self.rdB) THEN RAISE Error("Updater protocol error: Expected EOF, " & "didn't get it"); END; IF self.mux # NIL THEN ChannelMux.Close(self.mux); self.mux := NIL; END; IF self.oldWrA # NIL THEN Wr.Close(self.oldWrA); self.oldWrA := NIL; END; Notice(self, "Finished successfully"); END; FINALLY IOWatchDog.Cancel(self.watchDog); END; FINALLY ShutdownConnections(self); END; threadStatus.message := NIL; (* Already printed. *) RETURN threadStatus; FINALLY IF self.authDB # NIL THEN TRY Auth.Close(self.authDB); EXCEPT ELSE END; self.authDB := NIL; END; END; FINALLY IF lock # NIL THEN TRY LockFile.Unlock(lock); EXCEPT OSError.E(l) => RAISE Error("Error unlocking \"" & self.config.lockFile & "\": " & ErrMsg.StrError(l)); END; END; END; EXCEPT | Error(msg) => Err(self, msg); RETURN NEW(SupMisc.ThreadStatus, status := SupMisc.ExitCode.Failure); | Rd.EndOfFile => Err(self, "Premature EOF from server"); RETURN NEW(SupMisc.ThreadStatus, status := SupMisc.ExitCode.TransientFailure); | Rd.Failure(list) => Err(self, "Network read failure: " & ErrMsg.StrError(list)); RETURN NEW(SupMisc.ThreadStatus, status := SupMisc.ExitCode.TransientFailure); | Thread.Alerted => IF self.watchDog # NIL AND IOWatchDog.Expired(self.watchDog) THEN Err(self, "Inactivity timeout"); RETURN NEW(SupMisc.ThreadStatus, status := SupMisc.ExitCode.TransientFailure); ELSE Err(self, "Interrupted"); IF killer # NIL AND killer.killedBySignal # -1 THEN EVAL Usignal.kill(Process.GetMyID(), killer.killedBySignal); END; RETURN NEW(SupMisc.ThreadStatus, status := SupMisc.ExitCode.Failure); END; | TransientFailure(msg) => Err(self, msg); RETURN NEW(SupMisc.ThreadStatus, status := SupMisc.ExitCode.TransientFailure); | Wr.Failure(list) => Err(self, "Network write failure: " & ErrMsg.StrError(list)); RETURN NEW(SupMisc.ThreadStatus, status := SupMisc.ExitCode.TransientFailure); END; END Apply; PROCEDUREAccept (self: T; addr: IP.Address; cmd: TEXT): TCP.T RAISES {Thread.Alerted, TransientFailure, Wr.Failure} =
Creates a connector, binds it to an acceptable port on the given
address, sends its endpoint with the command cmd
, accepts one
connection from the server, and closes the connector.
VAR conn: TCP.Connector; wd: WatchDog.T; epToks: ARRAY [0..4] OF TEXT; BEGIN conn := NewConnector(addr, self.config.loDataPort, self.config.hiDataPort); TRY TokScan.EncodeEndpoint(TCP.GetEndPoint(conn), epToks); self.proto.putCmd(self.wrA, cmd, epToks[0], epToks[1], epToks[2], epToks[3], epToks[4]); Wr.Flush(self.wrA); TRY wd := WatchDog.New(SupMisc.ListenTimeout); TRY RETURN TCP.Accept(conn); FINALLY WatchDog.Cancel(wd); END; EXCEPT | IP.Error(list) => RAISE TransientFailure("Accept failed: " & ErrMsg.StrError(list)); | Thread.Alerted => IF WatchDog.Expired(wd) THEN RAISE TransientFailure( "Timed out waiting for connection from server. Check your" & " firewall setup or try the \"-P m\" option"); ELSE RAISE Thread.Alerted; END; END; FINALLY TCP.CloseConnector(conn); END; END Accept; PROCEDUREShutdownConnections (self: T) RAISES {Thread.Alerted} =
Closes all the network connections, being careful to get it all done even if exceptions occur along the way.
BEGIN IF self.mux # NIL THEN ChannelMux.Close(self.mux) END; (* Close all of the TCP channels, being careful to avoid any delays. This will close the file descriptors right out from under the associated readers and writers. Below, when we close the readers and writers themselves, we'll get some Rd.Failure and Wr.Failure exceptions. But that is better than hanging up waiting for the buffer flushes to complete. *) IF self.tcp0 # NIL THEN TRY TurnOffLinger(self.tcp0) EXCEPT Error => (* Ignore *) END; TCP.Close(self.tcp0); END; IF self.tcp1 # NIL THEN TRY TurnOffLinger(self.tcp1) EXCEPT Error => (* Ignore *) END; TCP.Close(self.tcp1); END; IF self.tcp2 # NIL THEN TRY TurnOffLinger(self.tcp2) EXCEPT Error => (* Ignore *) END; TCP.Close(self.tcp2); END; IF self.tcp3 # NIL THEN TRY TurnOffLinger(self.tcp3) EXCEPT Error => (* Ignore *) END; TCP.Close(self.tcp3); END; (* Close the readers and writers. *) TRY IF self.oldRdA # NIL THEN TRY Rd.Close(self.oldRdA) EXCEPT Rd.Failure => (* Ignore *) END; END; FINALLY TRY IF self.rdA # NIL THEN TRY Rd.Close(self.rdA) EXCEPT Rd.Failure => (* Ignore *) END; END; FINALLY TRY IF self.rdB # NIL THEN TRY Rd.Close(self.rdB) EXCEPT Rd.Failure => (* Ignore *) END; END; FINALLY TRY IF self.oldWrA # NIL THEN TRY Wr.Close(self.oldWrA) EXCEPT Wr.Failure => (* Ignore *) END; END; FINALLY TRY IF self.wrA # NIL THEN TRY Wr.Close(self.wrA) EXCEPT Wr.Failure => (* Ignore *) END; END; FINALLY IF self.wrB # NIL THEN TRY Wr.Close(self.wrB) EXCEPT Wr.Failure => (* Ignore *) END; END; END; END; END; END; END; END ShutdownConnections;***************************************************************************
PROCEDURE***************************************************************************GetSockName (tcp: TCP.T): IP.Endpoint RAISES {Error} = BEGIN TRY RETURN TCPMisc.GetSockName(tcp); EXCEPT IP.Error(list) => RAISE Error("GetSockName failed: " & ErrMsg.StrError(list)); END; END GetSockName; PROCEDURENewConnector (addr: IP.Address; loPort, hiPort: IP.Port): TCP.Connector RAISES {TransientFailure} = BEGIN TRY RETURN SupMisc.NewConnector(addr, loPort, hiPort); EXCEPT IP.Error(list) => RAISE TransientFailure("Listen failed: " & ErrMsg.StrError(list)); END; END NewConnector; PROCEDURETurnOffLinger (tcp: TCP.T) RAISES {Error} = BEGIN TRY TCPMisc.LingerOnClose(tcp, FALSE); EXCEPT IP.Error(list) => RAISE Error("Cannot turn off SO_LINGER: " & ErrMsg.StrError(list)); END; END TurnOffLinger; PROCEDURETurnOffNoDelay (tcp: TCP.T) RAISES {Error} = BEGIN TRY TCPMisc.CoalesceWrites(tcp, TRUE); EXCEPT IP.Error(list) => RAISE Error("Cannot turn off TCP_NODELAY: " & ErrMsg.StrError(list)); END; END TurnOffNoDelay;
PROCEDURE***************************************************************************Err (self: T; msg: TEXT) = BEGIN IF self.config.trace # NIL THEN Logger.Put(self.config.trace, Logger.Priority.Err, msg); END; END Err; PROCEDURENotice (self: T; msg: TEXT) = BEGIN IF self.config.trace # NIL THEN Logger.Put(self.config.trace, Logger.Priority.Notice, msg); END; END Notice; PROCEDURETrace (self: T; msg: TEXT) = BEGIN IF self.config.trace # NIL THEN Logger.Put(self.config.trace, Logger.Priority.Info, msg); END; END Trace; PROCEDUREWarn (self: T; msg: TEXT) = BEGIN IF self.config.trace # NIL THEN Logger.Put(self.config.trace, Logger.Priority.Warning, msg); END; END Warn;
PROCEDUREShakeHands (self: T) RAISES {Error, Rd.EndOfFile, Rd.Failure, Thread.Alerted, TransientFailure, Wr.Failure} = VAR ts: TokScan.T; cmd: TEXT; serverMajor, serverMinor: CVProto.VersionNumber; actualMajor, actualMinor: CVProto.VersionNumber; serverSW: TEXT; BEGIN TRY ts := TokScan.New(SupMisc.GetCmdLine(self.rdA)); cmd := ts.getToken("greeting"); IF TokScan.EqualFolded(cmd, "OK") THEN serverMajor := ts.getInt("server protocol major version"); serverMinor := ts.getInt("server protocol minor version"); IF NOT ts.next(serverSW) THEN serverSW := "." END; ELSIF TokScan.EqualFolded(cmd, "!") THEN RAISE TransientFailure("Rejected by server: " & ts.getRest()); ELSE RAISE TokScan.Error("Invalid greeting from server"); END; SupMisc.PutCmd(self.wrA, "PROTO", Fmt.Int(CVProto.Current.major), Fmt.Int(CVProto.Current.minor), Version.Name); Wr.Flush(self.wrA); ts := TokScan.New(SupMisc.GetCmdLine(self.rdA)); cmd := ts.getToken("PROTO"); IF TokScan.EqualFolded(cmd, "PROTO") THEN actualMajor := ts.getInt("actual protocol major version"); actualMinor := ts.getInt("actual protocol minor version"); ELSIF TokScan.EqualFolded(cmd, "!") THEN RAISE Error("Protocol negotiation failed: " & ts.getRest()); ELSE RAISE TokScan.Error("Invalid PROTO command from server"); END; self.proto := CVProto.Lookup(actualMajor, actualMinor); IF self.proto.v.exchangesVersions THEN Trace(self, "Server software version: " & serverSW); END; IF CVProto.HasS1GBug(self.proto, serverSW) THEN Err(self, "Server " & self.server & " has the S1G bug"); Err(self, "See " & S1GUrl & " for details"); Err(self, "Please notify the maintainer of " & self.server); RAISE Error("Refusing update from server with S1G bug"); END; IF self.proto.major # CVProto.Current.major OR self.proto.minor # CVProto.Current.minor THEN Notice(self, "Falling back to protocol version " & Fmt.Int(self.proto.major) & "." & Fmt.Int(self.proto.minor)); END; EXCEPT | TokScan.Error(msg) => RAISE Error("Startup protocol error: " & msg); | CVProto.NotSupported => RAISE Error("Server protocol version" & " " & Fmt.Int(serverMajor) & "." & Fmt.Int(serverMinor) & " " & "not supported by client"); END; END ShakeHands; PROCEDUREAdjustForProtocol (self: T) RAISES {Error} =
Make sure the client and server can work together, and adjust various options according to what has been negotiated.
BEGIN CASE self.config.connectMode OF | ConnectMode.Default => IF self.proto.v.hasMuxMode THEN self.config.connectMode := ConnectMode.Mux; ELSE self.config.connectMode := ConnectMode.Active; END; | ConnectMode.Active, ConnectMode.Passive, ConnectMode.Socks => (* Nothing special *) | ConnectMode.Mux => IF NOT self.proto.v.hasMuxMode THEN RAISE Error("Server does not support multiplexed mode"); END; END; FOR i := 0 TO self.config.collections.size()-1 DO WITH sfr = self.config.collections.get(i) DO IF NOT SupFileRec.Option.Skip IN sfr.options THEN IF NOT self.proto.v.hasClientAccepts AND sfr.accepts.size() > 0 THEN RAISE Error("Server does not support the \"-i pattern\" option"); END; END; END; END; IF self.config.authRequired AND NOT self.proto.v.hasMD5Auth THEN RAISE Error("Server does not support authentication"); END; END AdjustForProtocol; PROCEDUREAuthorize (self: T) RAISES {Error, Rd.EndOfFile, Rd.Failure, Thread.Alerted, Wr.Failure} = VAR user: TEXT; host: TEXT; ts: TokScan.T; BEGIN TRY user := UnixMisc.GetLogin(); IF user = NIL THEN user := "?" END; TRY host := UnixMisc.GetHostName() EXCEPT OSError.E => host := "?" END; (* Early servers expected only the user name, and not the host name. But no servers ever checked for the end of the line, so even early ones can handle the host name. Therefore we don't need a new protocol version to distinguish between when it is sent and when it is not. *) self.proto.putCmd(self.wrA, "USER", user, host); Wr.Flush(self.wrA); IF self.proto.v.hasMD5Auth THEN DoMD5Auth(self); ELSE ts := self.proto.getCmd(self.rdA); ts.getFolded("OK"); END; EXCEPT | TokScan.Error(msg) => RAISE Error("Authorization protocol error: " & msg); END; END Authorize; PROCEDUREDoMD5Auth (self: T) RAISES {Error, Rd.EndOfFile, Rd.Failure, Thread.Alerted, TokScan.Error, Wr.Failure} = VAR ts: TokScan.T; realm: TEXT; serverChallenge: TEXT; serverResponse: TEXT; client := "."; clientResponse := "."; clientChallenge := "."; sharedSecret := "*"; auth: Auth.T; cmd: TEXT; BEGIN ts := self.proto.getCmd(self.rdA); ts.getFolded("AUTHMD5"); realm := ts.getToken("realm"); serverChallenge := ts.getToken("server challenge"); ts.getEnd("end of AUTHMD5 command"); IF self.config.authRequired OR NOT Text.Equal(serverChallenge, ".") THEN IF self.authDB = NIL THEN RAISE Error("Authentication required, but could not open \"" & self.authPath & "\""); END; IF Text.Equal(realm, ".") THEN RAISE Error("Authentication required, but not enabled on server"); END; TRY IF NOT Auth.Lookup(self.authDB, realm, auth) THEN RAISE Error("No record for server \"" & realm & "\" in \"" & self.authPath & "\""); END; EXCEPT Auth.Error(msg) => RAISE Error(msg); END; client := auth.client; sharedSecret := AuthMD5.MakeSecret(auth.server, auth.client, auth.password); END; IF NOT Text.Equal(serverChallenge, ".") THEN clientResponse := AuthMD5.GenResponse(serverChallenge, sharedSecret); END; IF self.config.authRequired THEN clientChallenge := AuthMD5.GenChallenge(self.peer.addr, "."); END; self.proto.putCmd(self.wrA, "AUTHMD5", client, clientResponse, clientChallenge); Wr.Flush(self.wrA); ts := self.proto.getCmd(self.rdA); cmd := ts.getToken("server reply to AUTHMD5"); IF TokScan.EqualFolded(cmd, "OK") THEN serverResponse := ts.getToken("server auth response"); IF self.config.authRequired AND NOT AuthMD5.CheckResponse(serverResponse, clientChallenge, sharedSecret) THEN RAISE Error("Server failed to authenticate itself to client"); END; ELSIF Text.Equal(cmd, "!") THEN RAISE Error("Server error: " & ts.getRest()); ELSE RAISE Error("Invalid server reply to AUTHMD5"); END; END DoMD5Auth; PROCEDUREExchangeAttributeInfo (self: T) RAISES {Error, Rd.EndOfFile, Rd.Failure, Thread.Alerted, Wr.Failure} = VAR ts: TokScan.T; count: INTEGER; tok: TEXT; BEGIN IF self.proto.v.hasFileAttrs THEN TRY Trace(self, "Negotiating file attribute support"); self.proto.v.attrSupport := FileAttr.Supported; self.proto.putCmd(self.wrA, "ATTR", Fmt.Int(NUMBER(self.proto.v.attrSupport))); FOR i := FIRST(self.proto.v.attrSupport) TO LAST(self.proto.v.attrSupport) DO Wr.PutText(self.wrA, FileAttr.EncodeAttrTypes(self.proto.v.attrSupport[i]) & "\n"); END; self.proto.putCmd(self.wrA, "."); Wr.Flush(self.wrA); ts := self.proto.getCmd(self.rdA); ts.getFolded("ATTR"); count := ts.getInt("number of file types"); ts.getEnd("end of \"ATTR\" command"); IF count > NUMBER(self.proto.v.attrSupport) THEN RAISE TokScan.Error("Invalid file type count"); END; FOR i := 0 TO count-1 DO ts := self.proto.getCmd(self.rdA); tok := ts.getToken("attrTypes"); ts.getEnd("end of attrTypes"); WITH as = self.proto.v.attrSupport[VAL(i, FileAttr.FileType)] DO as := as * FileAttr.DecodeAttrTypes(tok); END; END; ts := self.proto.getCmd(self.rdA); ts.getLiteral("."); ts.getEnd("end of \".\""); FOR i := count TO NUMBER(self.proto.v.attrSupport)-1 DO self.proto.v.attrSupport[VAL(i, FileAttr.FileType)] := FileAttr.AttrTypes{}; END; EXCEPT TokScan.Error(msg) => RAISE Error("Protocol error negotiating attribute support: " & msg); END; ELSE self.proto.v.attrSupport := FileAttr.Historical; END; END ExchangeAttributeInfo; PROCEDUREExchangeCollectionInfo (self: T) RAISES {Error, Rd.EndOfFile, Rd.Failure, Thread.Alerted, Wr.Failure} =
AllRd
andWr
exceptions relate to the network connection.
CONST ServerMaySet = SupFileRec.Options{ SupFileRec.Option.Skip, SupFileRec.Option.NoRsync, SupFileRec.Option.NoRCS}; ServerMayClear = SupFileRec.Options{ SupFileRec.Option.CheckRCS}; VAR (* CONST *) NoWS := GlobTree.Not(GlobTree.Match("*[ \t\r\n]*")); VAR sfr: SupFileRec.T; ts: TokScan.T; cmd: TEXT; dirAccept := GlobTree.True; fileAccept := GlobTree.True; dirRefuse := GlobTree.False; fileRefuse := GlobTree.False; pat: TEXT; aliasName: TEXT; keyName: TEXT; BEGIN Trace(self, "Exchanging collection information"); TRY (* Send all the supfile information to the server. *) FOR i := 0 TO self.config.collections.size()-1 DO sfr := self.config.collections.get(i); IF NOT SupFileRec.Option.Skip IN sfr.options THEN self.proto.putCmd(self.wrA, "COLL", sfr.collection, sfr.release, more := TRUE); IF self.proto.v.clientSendsUmask THEN self.proto.putCmd(self.wrA, NIL, Fmt.Unsigned(sfr.umask, base := 8), more := TRUE); END; self.proto.putCmd(self.wrA, NIL, SupFileRec.EncodeOptions(sfr.options)); IF self.proto.v.hasClientAccepts THEN FOR j := 0 TO sfr.accepts.size()-1 DO self.proto.putCmd(self.wrA, "ACC", sfr.accepts.get(j)); END; END; FOR j := 0 TO sfr.refusals.size()-1 DO self.proto.putCmd(self.wrA, "REF", sfr.refusals.get(j)); END; self.proto.putCmd(self.wrA, "."); END; END; self.proto.putCmd(self.wrA, "."); Wr.Flush(self.wrA); (* Read back the filtering details. *) FOR i := 0 TO self.config.collections.size()-1 DO sfr := self.config.collections.get(i); IF NOT SupFileRec.Option.Skip IN sfr.options THEN ts := self.proto.getCmd(self.rdA); ts.getFolded("COLL"); WITH collection = ts.getToken("collection name") DO IF NOT Text.Equal(collection, sfr.collection) THEN RAISE TokScan.Error("Expected collection \"" & sfr.collection & "\", got \"" & collection & "\""); END; END; WITH release = ts.getToken("release name") DO IF NOT Text.Equal(release, sfr.release) THEN RAISE TokScan.Error("Expected release \"" & sfr.release & "\", got \"" & release & "\""); END; END; WITH so = SupFileRec.DecodeOptions(ts.getToken("options")) DO sfr.options := sfr.options + ServerMaySet * so - ServerMayClear * (SupFileRec.AllOptions - so); END; LOOP ts := self.proto.getCmd(self.rdA); cmd := ts.getToken("command"); IF Text.Equal(cmd, ".") THEN EXIT END; IF TokScan.EqualFolded(cmd, "PRFX") THEN sfr.keywordPrefix := ts.getToken("keyword prefix"); ELSIF TokScan.EqualFolded(cmd, "KEYALIAS") THEN aliasName := ts.getToken("alias name"); keyName := ts.getToken("RCS keyword"); TRY sfr.expander.alias(aliasName, keyName); EXCEPT RCSKeyword.Unknown => Warn(self, "Attempt by server to alias unknown RCS keyword \"" & keyName & "\""); END; ELSIF TokScan.EqualFolded(cmd, "KEYON") THEN keyName := ts.getToken("RCS keyword"); IF Text.Equal(keyName, ".") THEN sfr.expander.enableAll(); ELSE TRY sfr.expander.enable(keyName); EXCEPT RCSKeyword.Unknown => Warn(self, "Attempt by server to enable unknown RCS" & " keyword \"" & keyName & "\""); END; END; ELSIF TokScan.EqualFolded(cmd, "KEYOFF") THEN keyName := ts.getToken("RCS keyword"); IF Text.Equal(keyName, ".") THEN sfr.expander.enableAll(enabled := FALSE); ELSE TRY sfr.expander.enable(keyName, enabled := FALSE); EXCEPT RCSKeyword.Unknown => Warn(self, "Attempt by server to disable unknown RCS" & " keyword \"" & keyName & "\""); END; END; ELSIF TokScan.EqualFolded(cmd, "NORS") THEN pat := ts.getToken("pattern"); sfr.noRsync := GlobTree.Or(sfr.noRsync, GlobTree.Match(pat, Glob.MatchOptions{ Glob.MatchOption.Pathname })); ELSIF TokScan.EqualFolded(cmd, "RNORS") THEN pat := ts.getToken("pattern"); sfr.noRsync := GlobTree.Or(sfr.noRsync, GlobTree.Match(pat, Glob.MatchOptions{ Glob.MatchOption.Pathname, Glob.MatchOption.LeadingDir })); ELSIF TokScan.EqualFolded(cmd, "!") THEN sfr.options := sfr.options + SupFileRec.Options{SupFileRec.Option.Skip}; Warn(self, "Server message: " & ts.getRest()); ELSIF TokScan.EqualFolded(cmd, "UPGR") OR TokScan.EqualFolded(cmd, "ALWS") OR TokScan.EqualFolded(cmd, "OANY") THEN EVAL ts.getToken("pattern"); (* Not used any more. *) ELSE RAISE TokScan.Error("Invalid command \"" & cmd & "\""); END; END; IF sfr.accepts.size() > 0 THEN dirAccept := GlobTree.False; fileAccept := GlobTree.False; FOR i := 0 TO sfr.accepts.size()-1 DO pat := sfr.accepts.get(i); dirAccept := GlobTree.Or(dirAccept, GlobTree.Match(pat, Glob.MatchOptions{ Glob.MatchOption.Pathname, Glob.MatchOption.LeadingDir, Glob.MatchOption.PrefixDirs })); IF SupFileRec.Option.CheckoutMode IN sfr.options THEN (* We must modify the pattern so that it refers to the RCS file, rather than the checked-out file. *) WITH len = Text.Length(pat) DO IF len = 0 OR Text.GetChar(pat, len-1) # '*' THEN pat := pat & SupMisc.RCSSuffix; END; END; END; fileAccept := GlobTree.Or(fileAccept, GlobTree.Match(pat, Glob.MatchOptions{ Glob.MatchOption.Pathname, Glob.MatchOption.LeadingDir })); END; END; FOR j := 0 TO sfr.refusals.size()-1 DO pat := sfr.refusals.get(j); dirRefuse := GlobTree.Or(dirRefuse, GlobTree.Match(pat)); IF SupFileRec.Option.CheckoutMode IN sfr.options THEN (* We must modify the pattern so that it refers to the RCS file, rather than the checked-out file. *) WITH len = Text.Length(pat) DO IF len = 0 OR Text.GetChar(pat, len-1) # '*' THEN pat := pat & SupMisc.RCSSuffix; END; END; END; fileRefuse := GlobTree.Or(fileRefuse, GlobTree.Match(pat)); END; sfr.dirFilter := GlobTree.And(dirAccept, GlobTree.Not(dirRefuse)); sfr.fileFilter := GlobTree.And(fileAccept, GlobTree.Not(fileRefuse)); (* If the server can't handle white space in file names, make sure we don't send him any. *) IF NOT self.proto.v.handlesWhiteSpace THEN sfr.dirFilter := GlobTree.And(sfr.dirFilter, NoWS); sfr.fileFilter := GlobTree.And(sfr.fileFilter, NoWS); END; (* Set up a mask of file attribes that we don't want to sync with the server. *) IF NOT SupFileRec.Option.SetOwner IN sfr.options THEN sfr.attrIgnore := sfr.attrIgnore + FileAttr.AttrTypes{ FileAttr.AttrType.Owner, FileAttr.AttrType.Group }; END; IF NOT SupFileRec.Option.SetMode IN sfr.options THEN sfr.attrIgnore := sfr.attrIgnore + FileAttr.AttrTypes{ FileAttr.AttrType.Mode }; END; IF NOT SupFileRec.Option.SetFlags IN sfr.options THEN sfr.attrIgnore := sfr.attrIgnore + FileAttr.AttrTypes{ FileAttr.AttrType.Flags }; END; END; END; ts := self.proto.getCmd(self.rdA); ts.getLiteral("."); EXCEPT TokScan.Error(msg) => RAISE Error("Protocol error exchanging collection info: " & msg); END; END ExchangeCollectionInfo; PROCEDURE***************************************************************************EstablishControlConnection (self: T) RAISES {Error, Thread.Alerted, TransientFailure} = BEGIN TRY IF NOT SupMisc.ParseHost(self.server, self.peer.addr) THEN RAISE Error("Unknown host \"" & self.server & "\""); END; EXCEPT IP.Error(list) => RAISE TransientFailure("Name lookup failure for \"" & self.server & "\": " & ErrMsg.StrError(list)); END; self.peer.port := self.config.port; Trace(self, "Connecting to " & self.server); TRY self.tcp0 := TCPMisc.ConnectFrom(self.peer, self.config.localEndpoint); EXCEPT IP.Error(list) => RAISE TransientFailure("Cannot connect to " & self.server & ": " & ErrMsg.StrError(list)); END; TurnOffNoDelay(self.tcp0); self.rdA := ConnRW.NewRd(self.tcp0); self.wrA := ConnRW.NewWr(self.tcp0); Notice(self, "Connected to " & self.server); END EstablishControlConnection; PROCEDUREEstablishDataConnection (self: T) RAISES {Error, Rd.EndOfFile, Rd.Failure, Thread.Alerted, TransientFailure, Wr.Failure} = VAR ts: TokScan.T; addr: IP.Address; ep: IP.Endpoint; id: ChannelMux.ChannelID; chan0, chan1: ChannelMux.Channel; BEGIN TRY CASE self.config.connectMode OF | ConnectMode.Active => Trace(self, "Establishing active-mode data connection"); addr := GetSockName(self.tcp0).addr; self.tcp1 := Accept(self, addr, "PORT"); TurnOffNoDelay(self.tcp1); self.rdB := ConnRW.NewRd(self.tcp1); IOWatchDog.AddRd(self.watchDog, self.rdB); self.wrB := ConnRW.NewWr(self.tcp1); IOWatchDog.AddWr(self.watchDog, self.wrB); | ConnectMode.Socks => Trace(self, "Establishing SOCKS-mode data connection"); addr := GetSockName(self.tcp0).addr; self.tcp1 := Accept(self, addr, "SOCKS1"); self.tcp2 := Accept(self, addr, "SOCKS2"); self.tcp3 := Accept(self, addr, "SOCKS3"); (* We would like to do a "Rd.Close(self.rdA)" at this point, since we aren't going to use that direction any more. But that causes SOCKS to drop the entire connection. *) self.oldRdA := self.rdA; TurnOffNoDelay(self.tcp1); self.rdA := ConnRW.NewRd(self.tcp1); IOWatchDog.AddRd(self.watchDog, self.rdA); TurnOffNoDelay(self.tcp2); self.wrB := ConnRW.NewWr(self.tcp2); IOWatchDog.AddWr(self.watchDog, self.wrB); TurnOffNoDelay(self.tcp3); self.rdB := ConnRW.NewRd(self.tcp3); IOWatchDog.AddRd(self.watchDog, self.rdB); | ConnectMode.Passive => Trace(self, "Establishing passive-mode data connection"); self.proto.putCmd(self.wrA, "PASV"); Wr.Flush(self.wrA); ts := self.proto.getCmd(self.rdA); ts.getFolded("PORT"); ep := ts.getEndpoint(); ts.getEnd(); TRY self.tcp1 := TCPMisc.ConnectFrom(ep, self.config.localEndpoint); EXCEPT IP.Error(list) => RAISE TransientFailure("Cannot connect to data port: " & ErrMsg.StrError(list)); END; TurnOffNoDelay(self.tcp1); self.rdB := ConnRW.NewRd(self.tcp1); IOWatchDog.AddRd(self.watchDog, self.rdB); self.wrB := ConnRW.NewWr(self.tcp1); IOWatchDog.AddWr(self.watchDog, self.wrB); | ConnectMode.Mux => Trace(self, "Establishing multiplexed-mode data connection"); self.proto.putCmd(self.wrA, "MUX"); Wr.Flush(self.wrA); TRY self.mux := ChannelMux.Open(self.rdA, self.wrA, chan0, active := TRUE); EXCEPT IP.Error(l) => RAISE Error("ChannelMux.Open failed: " & ErrMsg.StrError(l)); END; self.oldRdA := self.rdA; self.oldWrA := self.wrA; self.rdA := ConnRW.NewRd(chan0); IOWatchDog.AddRd(self.watchDog, self.rdA); self.wrA := ConnRW.NewWr(chan0); IOWatchDog.AddWr(self.watchDog, self.wrA); TRY id := ChannelMux.Listen(self.mux); EXCEPT IP.Error(l) => RAISE Error("ChannelMux.Listen failed: " & ErrMsg.StrError(l)); END; self.proto.putCmd(self.wrA, "CHAN", Fmt.Int(id)); Wr.Flush(self.wrA); TRY chan1 := ChannelMux.Accept(self.mux, id); EXCEPT IP.Error(l) => RAISE Error("ChannelMux.Accept failed: " & ErrMsg.StrError(l)); END; self.rdB := ConnRW.NewRd(chan1); IOWatchDog.AddRd(self.watchDog, self.rdB); self.wrB := ConnRW.NewWr(chan1); IOWatchDog.AddWr(self.watchDog, self.wrB); | ConnectMode.Default => (* It should have been set to a concrete value by now. *) <* ASSERT FALSE *> END; EXCEPT | TokScan.Error(msg) => RAISE Error("Protocol error establishing data connection: " & msg); END; END EstablishDataConnection; PROCEDURECheckCollections (self: T): CARDINAL = VAR numValid: CARDINAL := 0; sfr: SupFileRec.T; link: TEXT; BEGIN FOR i := 0 TO self.config.collections.size()-1 DO sfr := self.config.collections.get(i); IF NOT SupMisc.IsDirectory(sfr.clientPrefix) THEN (* Skip this collection, and warn about it unless its prefix is a symbolic link pointing to "SKIP". *) sfr.options := sfr.options + SupFileRec.Options{SupFileRec.Option.Skip}; TRY link := UnixMisc.ReadLink(sfr.clientPrefix) EXCEPT OSError.E => link := NIL; END; IF link = NIL OR NOT Text.Equal(link, "SKIP") THEN Warn(self, "Nonexistent prefix \"" & sfr.clientPrefix & "\" for " & sfr.collection & "/" & sfr.release); END; END; IF NOT SupFileRec.Option.Skip IN sfr.options THEN INC(numValid); END; END; RETURN numValid; END CheckCollections;
TYPE Killer = SigHandler.T OBJECT thread: Thread.T; killedBySignal := -1; OVERRIDES apply := KillerApply; END; PROCEDUREKillerApply (self: Killer; sig: INTEGER) = BEGIN self.killedBySignal := sig; Thread.Alert(self.thread); END KillerApply; BEGIN END FSClient.