<> DIRECTORY CedarSnapshot USING [After, CheckpointProc, RollbackProc, Register], FileIO USING [Open], IO USING [CreateProcsStream, CreateRefStreamProcs, CreateDribbleStream, CreateBufferedOutputStream, Close, Flush, GetBlock, GetIndex, GetLength, noWhereStream, PutBlock, PutChar, PutF, PutRope, rope, SetLength, SetIndex, STREAM, time], List USING [Nconc1], MessageWindow USING [Append, Clear], Process USING [ GetCurrent], Rope USING [Cat, Concat, Equal, ROPE], Time USING [Current], TiogaOps USING [FirstChild, ViewerDoc, GetRope], UserExec USING [GetExecHandle, ExecHandle], UserExecExtras USING [], UserExecPrivate USING [ExecPrivateRecord], UserProfile USING [Boolean, Token] ; UserExecLogImpl: CEDAR MONITOR LOCKS data USING data: REF MonitoredStreamRecord IMPORTS CedarSnapshot, FileIO, IO, List, MessageWindow, Process, Rope, Time, TiogaOps, UserExec, UserProfile EXPORTS UserExec, UserExecExtras, UserExecPrivate = BEGIN OPEN IO; ExecPrivateRecord: PUBLIC TYPE = UserExecPrivate.ExecPrivateRecord; <> <> sessionLog: IO.STREAM; sessionLogFileStream: IO.STREAM; <<>> <> GetSessionLog: PUBLIC PROCEDURE RETURNS [IO.STREAM] = { IF NOT UserProfile.Boolean["CreateSessionLog", TRUE] THEN RETURN[NIL]; IF sessionLog = NIL THEN CreateSessionLog[]; RETURN[IO.CreateBufferedOutputStream[stream: sessionLog, deliverWhen: NIL, bufferSize: 0]]; }; -- of GetSessionLog CreateSessionLog: PROCEDURE [] = { sessionLogFileStream _ CreateLog["Session.Log"]; sessionLogFileStream.PutF["New Session began at %t\n", time[]]; sessionLog _ CreateMonitoredStream[out: sessionLogFileStream, proc: SessionLogProc]; }; -- of CreateSessionLog lastLoggedExec: UserExec.ExecHandle; SessionLogProc: PROC[self: IO.STREAM] = TRUSTED { thisExec: UserExec.ExecHandle = UserExec.GetExecHandle[process: Process.GetCurrent[]]; IF self # sessionLogFileStream THEN ERROR; IF thisExec # lastLoggedExec THEN { IF thisExec # NIL THEN { private: REF UserExecPrivate.ExecPrivateRecord = thisExec.privateStuff; sessionLogFileStream.PutF["\n\n----------------------Work Area %g----------------------\n", rope[private.id]]; } ELSE sessionLogFileStream.PutRope["\n\n--------------------------------------------\n"]; -- e.g. somebody else, like Jam typescript, got a sessionlog stream. lastLoggedExec _ thisExec; }; }; -- of SessionLogPutBlock <> changesLog: IO.STREAM; changesLogFileStream: IO.STREAM; GetChangesLog: PUBLIC PROCEDURE RETURNS [IO.STREAM] = { IF NOT UserProfile.Boolean["CreateChangesLog", TRUE] THEN RETURN[NIL]; IF changesLog = NIL THEN CreateChangesLog[]; RETURN[IO.CreateBufferedOutputStream[stream: changesLog, deliverWhen: NIL, bufferSize: 0]]; }; -- of GetChangesLog CreateChangesLog: PROCEDURE = { MessageWindow.Append["Creating Changes Log...", TRUE]; changesLogFileStream _ CreateLog["Changes.Log"]; MessageWindow.Clear[]; changesLogFileStream.PutF["Changes log created at %t\n", time[]]; changesLog _ CreateMonitoredStream[out: changesLogFileStream]; }; <> <> MonitoredStreamRecord: TYPE = MONITORED RECORD[stream: IO.STREAM, proc: PROC[self: IO.STREAM] _ NIL]; <<>> CreateMonitoredStream: PUBLIC PROCEDURE [out: IO.STREAM, proc: PROC[self: IO.STREAM] _ NIL] RETURNS [IO.STREAM] = -- if supplied, proc is called each time characters are output -- { RETURN[IO.CreateProcsStream[ streamProcs: IO.CreateRefStreamProcs[putBlock: MonitoredPutBlock, putChar: MonitoredPutChar, name: "Monitored"], streamData: NEW[MonitoredStreamRecord _ [stream: out, proc: proc]] ]]; }; -- of CreateMonitoredStream MonitoredPutBlock: PROC[self: IO.STREAM, block: REF READONLY TEXT, startIndex: NAT _ 0, stopIndexPlusOne: NAT _ LAST[NAT]] = { data: REF MonitoredStreamRecord = NARROW[self.streamData]; DoIt: ENTRY PROC [data: REF MonitoredStreamRecord] = { ENABLE UNWIND => NULL; IF data.proc # NIL THEN data.proc[data.stream]; IO.PutBlock[data.stream, block, startIndex, stopIndexPlusOne]; IO.Flush[data.stream]; }; DoIt[data]; }; -- of MonitoredPutBlock MonitoredPutChar: PROC[self: IO.STREAM, char: CHARACTER] = { -- the main reason why Putchar has to be implemented is to support a default implementation of eraseChar, e.g. if user types ^W or ^A when nothing is in the buffer. This can occur if user types line{ESC}^W, since the {ESC} causes material to be flushed, i.e. sent from buffered stream through monitored stream to the backing file data: REF MonitoredStreamRecord = NARROW[self.streamData]; DoIt: ENTRY PROC [data: REF MonitoredStreamRecord] = { ENABLE UNWIND => NULL; IF data.proc # NIL THEN data.proc[data.stream]; IO.PutChar[data.stream, char]; IO.Flush[data.stream]; }; DoIt[data]; }; -- of MonitoredPutChar <> CreateExecLogFile: PUBLIC PROC [exec: UserExec.ExecHandle] RETURNS[stream: IO.STREAM] = TRUSTED { private: REF ExecPrivateRecord = exec.privateStuff; name: Rope.ROPE = Rope.Cat["WorkArea", private.id, ".log"]; sessionLog: IO.STREAM = GetSessionLog[]; IF NOT UserProfile.Boolean["CreateWorkAreaLogs", FALSE] THEN RETURN[sessionLog]; IF NOT Rope.Equal[private.id, "A"] THEN MessageWindow.Append[Rope.Cat["Creating log for Work Area ", private.id, "..."], TRUE]; stream _ CreateLog[name, exec]; IF sessionLog # NIL THEN stream _ IO.CreateDribbleStream[stream, sessionLog]; stream.PutF["Work Area %g created at %t\n", rope[private.id], time[]]; IF NOT Rope.Equal[private.id, "A"] THEN MessageWindow.Clear[]; }; CloseExecLogFile: PUBLIC PROC [exec: UserExec.ExecHandle] = { FOR l: LIST OF REF LogFileRecord _ activeLogs, l.rest UNTIL l = NIL DO IF l.first.exec = exec THEN { IF l.first.log # NIL THEN {IO.Close[l.first.log]; l.first.log _ NIL}; -- so won't try to reopen following a rollback. EXIT; }; ENDLOOP; }; <> activeLogs: PUBLIC LIST OF REF LogFileRecord _ NIL; LogFileRecord: TYPE = RECORD[log: IO.STREAM, name: Rope.ROPE, length: INT _ 0, exec: UserExec.ExecHandle]; CreateLog: PROC [name: Rope.ROPE, exec: UserExec.ExecHandle _ NIL, dontRegister: BOOL _ FALSE] RETURNS[stream: IO.STREAM] = { r: Rope.ROPE = UserProfile.Token["WhenLogFileExists", "Rename"]; IF Rope.Equal[r, "Append", FALSE] THEN { stream _ FileIO.Open[fileName: name, accessOptions: append]; stream.PutRope["\n\n#######################################"]; } ELSE IF Rope.Equal[r, "OverWrite", FALSE] THEN { stream _ FileIO.Open[fileName: name, accessOptions: write]; IO.SetLength[stream, 0]; } ELSE { stream _ FileIO.Open[fileName: name, accessOptions: write]; SaveOldLog[name, stream]; IO.SetLength[stream, 0]; }; IF NOT dontRegister THEN RegisterLog[log: stream, name: name, exec: exec]; }; SaveOldLog: PROC [name: Rope.ROPE, stream: IO.STREAM] = { len: INT = IO.GetLength[stream]; dollarName: Rope.ROPE = Rope.Concat[name, "$"]; dollarStream: IO.STREAM = FileIO.Open[fileName: dollarName, accessOptions: overwrite, createLength: len]; bufferSize: INT = 512; buffer: REF TEXT = NEW[TEXT[bufferSize]]; bytes: INT; IO.SetIndex[stream, 0]; WHILE (bytes _ stream.GetBlock[buffer]) > 0 DO dollarStream.PutBlock[buffer]; ENDLOOP; dollarStream.Close[]; }; <> RegisterLog: PROC [log: IO.STREAM, name: Rope.ROPE _ NIL, exec: UserExec.ExecHandle _ NIL] = TRUSTED { activeLogs _ LOOPHOLE[List.Nconc1[ LOOPHOLE[activeLogs], NEW[LogFileRecord _ [log: log, name: name, exec: exec]] ]]; }; CheckpointProc: CedarSnapshot.CheckpointProc = {AtCheckpoint[]}; RollbackProc: PROC [after: CedarSnapshot.After] -- CedarSnapshot.RollbackProc-- = {AtRollback[after]}; AtCheckpoint: PROC = { FOR l: LIST OF REF LogFileRecord _ activeLogs, l.rest UNTIL l = NIL DO IF l.first.exec # NIL THEN { private: REF ExecPrivateRecord = l.first.exec.privateStuff; IF private.execState = destroyed THEN l.first.log _ NIL; }; IF l.first.log # NIL THEN { IO.Flush[l.first.log]; l.first.length _ IO.GetIndex[l.first.log]; IO.Close[l.first.log]; l.first.log^ _ IO.noWhereStream^; }; ENDLOOP; }; AtRollback: PROC [after: CedarSnapshot.After] = { FOR l: LIST OF REF LogFileRecord _ activeLogs, l.rest UNTIL l = NIL DO IF l.first.log # NIL THEN { name: Rope.ROPE = l.first.name; log: IO.STREAM; log _ CreateLog[name: name, exec: l.first.exec, dontRegister: TRUE]; IF Rope.Equal[UserProfile.Token["WhenLogFileExists", "Rename"], "Append", FALSE] THEN NULL -- this sessions log will be appended. ELSE IF IO.GetLength[log] >= l.first.length THEN IO.SetLength[log, l.first.length] -- Truncate log back to the point of the checkpoint. If length < l.first.length, file was somehow clobbered. in case of exec, can recover from typescript ELSE IF l.first.exec # NIL THEN { private: REF ExecPrivateRecord = l.first.exec.privateStuff; name: Rope.ROPE = Rope.Cat["WorkArea", private.id, ".log"]; log _ FileIO.Open[fileName: name, accessOptions: overwrite]; <> log.PutRope[TiogaOps.GetRope[TiogaOps.FirstChild[TiogaOps.ViewerDoc[l.first.exec.viewer]]]]; -- assumes only one node in typescript } ELSE IF l.first.log = sessionLogFileStream THEN { IO.SetLength[log, 0]; sessionLogFileStream^ _ log^; sessionLogFileStream.PutRope["****Previous session log was lost!\n"]; } ELSE IF l.first.log = changesLogFileStream THEN { IO.SetLength[log, 0]; changesLogFileStream^ _ log^; changesLogFileStream.PutRope["****Previous changes log was lost!\n"]; } ELSE ERROR; -- shouldn't happen l.first.log^ _ log^; -- already done for last two cases, but safer to have it here anyway }; ENDLOOP; IF sessionLogFileStream # NIL THEN sessionLogFileStream.PutF["\n%g at %t\n", rope[IF after = checkpoint THEN "Returned from Checkpoint" ELSE "Rolled back"], time[] ]; }; TRUSTED {CedarSnapshot.Register[c: CheckpointProc, r: RollbackProc]}; END. -- of UserExecLogImpl <<>> <<>>