DIRECTORY BcdDefs USING [BCD], Convert USING [MapValue, Parse, Value, ValueToRope], ConvertUnsafe USING [AppendRope, ToRope], DFUser USING [CleanupEnumerate, EnumerateEntries, EnumerateProcType, TooManyEntries], Environment USING [bytesPerWord], FileIO USING [Open], Heap USING [systemZone], IFSFile USING [Close, Completer, FileHandle, Finalize, FSInstance, Initialize, Login, Logout, Open, StartRead], IO USING [Close, GetChar, GetIndex, GetLength, GetSequence, PutChar, SetIndex, SetLength, STREAM], MessageWindow USING [Append], PriorityQueue USING [Insert, Predict, Ref, Remove, SortPred], Process USING [Detach, SecondsToTicks, SetTimeout], Rope USING [Cat, Concat, Equal, Fetch, FetchType, Flatten, FromProc, Index, MakeRope, Match, Replace, ROPE, Run, Size, SkipTo, Substr], SafeStorage USING [NewZone], SymTab USING [Create, Fetch, Insert, Ref], System USING [GetClockPulses, Microseconds, PulsesToMicroseconds], Time USING [Current, defaultTime], TimeStamp USING [Stamp], UnsafeSTP USING [Close, Create, DesiredProperties, Enumerate, FileInfo, GetFileInfo, Handle, Login, NoteFileProcType, Open, SetDesiredProperties], UserCredentials USING [GetUserCredentials], UserExec USING [AskUser, ExecHandle, GetExecHandle, GetStreams], VersionMap USING [Fetch, FetchName, GetPrefix, Length, Map, MapRep, MapEntry, NameMapIndex, MyStamp], VersionMapBuilder USING [EachProc, FilterProc]; VersionMapBuilderImpl: CEDAR MONITOR IMPORTS Convert, ConvertUnsafe, DFUser, FileIO, Heap, IFSFile, IO, MessageWindow, PriorityQueue, Process, Rope, SafeStorage, UnsafeSTP, SymTab, System, Time, UserCredentials, UserExec, VersionMap EXPORTS VersionMapBuilder SHARES VersionMap = BEGIN OPEN Rope, VersionMap, VersionMapBuilder; XMyStamp: TYPE = MACHINE DEPENDENT RECORD [lo,num,hi: CARDINAL]; NullStamp: MyStamp _ [0, 0, 0]; MyStampAsHex: TYPE = PACKED ARRAY [0..12) OF [0..16); XMap: TYPE = REF MapRep; XMapList: TYPE = LIST OF Map; XMapRep: PUBLIC TYPE = RECORD [ names: ROPE _ NIL, prefix: NameMapIndex _ 0, -- place to find factored prefix entries: SEQUENCE len: CARDINAL OF MapEntry]; XMapEntry: TYPE = RECORD [stamp: MyStamp, index: NameMapIndex]; XNameMapIndex: TYPE = INT; -- byte index of name in map NullNameIndex: NameMapIndex = LAST[NameMapIndex]; EntryIndex: TYPE = CARDINAL; NullEntryIndex: EntryIndex = LAST[CARDINAL]; EntryRef: TYPE = REF EntryRep; EntryRep: TYPE = RECORD [next: EntryRef, name: ROPE, version: MyStamp]; GenerateData: TYPE = REF GenerateDataRep; GenerateDataRep: TYPE = RECORD [ host,directory: ROPE _ NIL, pq: PriorityQueue.Ref _ NIL, list: EntryRef _ NIL, countGoing: NAT _ 0, errors: NAT _ 0, done: CONDITION]; pz: ZONE _ SafeStorage.NewZone[prefixed]; qz: ZONE _ SafeStorage.NewZone[quantized]; alarmClock: CONDITION; reportTime: CONDITION; reportInterval: NAT _ 0; reportProcess: PROCESS _ NIL; report: ROPE _ NIL; fileCount: INT _ 0; outstandingRequests: INT _ 2; -- # of processes to have looking at version pages lastMap: Map _ NIL; -- last map produced (useful for debugging) maxPiece: INT _ 8000; VersionNotAvailable: PUBLIC ERROR = CODE; NameError: PUBLIC ERROR = CODE; DuplicateVersionSignal: PUBLIC SIGNAL = CODE; WriteMapToFile: PUBLIC PROC [map: Map, name: ROPE, checkForDuplicateVersions: BOOL _ FALSE] = { lagStamp: MyStamp _ NullStamp; prefix: ROPE _ NIL; st: IO.STREAM _ FileIO.Open[name, write]; count: NAT _ map.len; names: ROPE _ map.names; firstCR: INT _ names.Index[0, "\n"]; prefix _ names.Flatten[0, firstCR]; MyPut[st, prefix]; MyPut[st, "\n"]; FOR i: NAT IN [0..map.len) DO entry: MapEntry _ map[i]; index: INT _ entry.index; stamp: MyStamp _ entry.stamp; IF stamp = lagStamp AND checkForDuplicateVersions THEN { r1: ROPE _ IndexToShortName[map, map[i-1].index]; r2: ROPE _ IndexToShortName[map, index]; IF r1.Equal[r2, FALSE] THEN LOOP ELSE SIGNAL DuplicateVersionSignal}; lagStamp _ stamp; PutStampAsHex[st, stamp]; MyPut[st, " "]; DO c: CHAR _ names.Fetch[index]; index _ index + 1; IO.PutChar[st, c]; IF c = '\n THEN EXIT; ENDLOOP; ENDLOOP; MyPut[st, "\n"]; IO.SetLength[st, IO.GetIndex[st]]; -- force goddamm truncation already!!! IO.Close[st]; }; ParseMapFromFile: PUBLIC PROC [name: ROPE] RETURNS [map: Map _ NIL] = { st: IO.STREAM _ FileIO.Open[name, read]; bytes: INT _ IO.GetLength[st]; getNext: PROC RETURNS [CHAR] = { RETURN [st.GetChar[]]; }; rope: ROPE _ Rope.FromProc[bytes, getNext, maxPiece]; pos: INT _ 0; head,list: EntryRef _ NIL; prefix: ROPE _ IO.GetSequence[st]; count: NAT _ 0; IO.Close[st]; prefix _ rope.Flatten[0, pos _ rope.Index[0, "\n"]]; pos _ pos + 1; WHILE pos < bytes DO c: CHAR _ rope.Fetch[pos]; start: INT _ 0; name: ROPE _ NIL; hex: MyStampAsHex _ ALL[0]; pos _ pos + 1; IF c <= 40C THEN LOOP; FOR i: NAT IN [0..12) WHILE pos <= bytes DO x: INTEGER _ 0; SELECT c FROM IN ['0..'9] => x _ c - '0; IN ['A..'F] => x _ (c - 'A) + 10; IN ['a..'f] => x _ (c - 'a) + 10; ENDCASE => EXIT; hex[i] _ x; c _ rope.Fetch[pos]; pos _ pos + 1; ENDLOOP; WHILE pos <= bytes DO IF c > 40C THEN {start _ pos - 1; EXIT}; c _ rope.Fetch[pos]; pos _ pos + 1; ENDLOOP; WHILE pos <= bytes DO IF c <= 40C THEN { name _ rope.Substr[start, pos-start-1]; EXIT}; c _ rope.Fetch[pos]; pos _ pos + 1; ENDLOOP; IF head = NIL THEN head _ list _ NewEntryRef[name] ELSE {list.next _ NewEntryRef[name]; list _ list.next}; list.version _ LOOPHOLE[hex]; count _ count + 1; ENDLOOP; lastMap _ map _ GenerateMapFromEntryList[head, prefix, count]; }; SetReportInterval: PUBLIC PROC [seconds: NAT _ 0] = TRUSTED { temp: NAT _ IF seconds = 0 THEN 10 ELSE seconds; forkIt: ENTRY PROC = TRUSTED { ENABLE UNWIND => NULL; reportInterval _ seconds; IF reportProcess = NIL AND seconds # 0 THEN Process.Detach[reportProcess _ FORK ClarkKent[]]; BROADCAST reportTime; }; Process.SetTimeout[@reportTime, Process.SecondsToTicks[temp]]; forkIt[]; }; GenerateMapFromRemote: PUBLIC PROC [host: ROPE _ NIL, directory: ROPE _ NIL, oldMap: Map _ NIL, filter: FilterProc _ NIL] RETURNS [map: Map] = TRUSTED { head: EntryRef _ NIL; prefix: ROPE _ NIL; count: NAT _ 0; IF host = NIL THEN host _ "Ivy"; IF directory = NIL THEN { user: ROPE _ UserCredentials.GetUserCredentials[].name; user _ user.Flatten[0, user.Index[0, "."]]; -- remove registry directory _ Rope.Cat["<", user, ">*.bcd"]}; IF directory.Index[0, "*"] = directory.Size[] THEN { directory _ directory.Concat["*.bcd"]}; [head, prefix, count] _ GenerateEntryListFromRemote[host, directory, oldMap, filter]; lastMap _ map _ GenerateMapFromEntryList[head, prefix, count]; }; GenerateMapFromDFFile: PUBLIC PROC [name: ROPE _ NIL, pattern: ROPE _ NIL, oldMap: Map _ NIL, filter: FilterProc _ NIL] RETURNS [map: Map] = TRUSTED { flat: ROPE _ name.Flatten[]; string: LONG STRING _ LOOPHOLE[flat]; head: EntryRef _ NIL; prefix: ROPE _ NIL; count: NAT _ 0; guess: NAT _ 8000; tab: SymTab.Ref _ NIL; verbose: BOOL _ FALSE; eachProc: DFUser.EnumerateProcType = TRUSTED { localName: STRING _ [120]; eachName: ROPE _ NIL; new: EntryRef _ NIL; Add1: PROC [c: CHAR] = TRUSTED { k: NAT _ localName.length; localName[k] _ c; localName.length _ k+1}; Add: PROC [s1,s2,s3,s4: LONG STRING _ NIL] = TRUSTED { as: ARRAY [0..4) OF LONG STRING _ [s1, s2, s3, s4]; k: NAT _ localName.length; FOR i: NAT IN [0..4) DO s: LONG STRING _ as[i]; IF s # NIL THEN FOR j: NAT IN [0..s.length) DO localName[k] _ s[j]; k _ k + 1; ENDLOOP; ENDLOOP; localName.length _ k; }; eachName _ ConvertUnsafe.ToRope[shortName]; IF NOT Rope.Match[pattern, eachName, FALSE] THEN RETURN; -- no match IF host # NIL THEN Add["[", host, "]"]; IF directory # NIL THEN Add["<", directory, ">", shortName]; IF version # 0 THEN {Add1['!]; Convert.MapValue[Add1, [signed[version]]]}; eachName _ ConvertUnsafe.ToRope[localName]; IF LookupName[tab, eachName] THEN RETURN; -- name already here IF filter # NIL AND NOT filter[eachName] THEN RETURN; -- user does not want this one new _ NewEntryRef[eachName]; IF eachName.Index[0, ".bcd", FALSE] = eachName.Size[] THEN { new.version _ LOOPHOLE[TimeStamp.Stamp[0, 0, createTime]]}; count _ count + 1; new.next _ head; head _ new; StoreName[tab, eachName]; }; IF pattern = NIL THEN pattern _ "*"; [head, prefix, count] _ GenerateEntryListFromMap[oldMap]; tab _ GenSymTabFromVersionMap[oldMap]; FOR try: NAT IN [0..4) DO err: BOOL _ FALSE; {ENABLE UNWIND => DFUser.CleanupEnumerate[]; exec: UserExec.ExecHandle _ UserExec.GetExecHandle[]; in,out: IO.STREAM _ NIL; [in: in, out: out] _ UserExec.GetStreams[exec]; DFUser.EnumerateEntries [ dfFileName: string, procToCall: eachProc, nEntriesInDFFiles: guess, confirmBeforeOverwriting: FALSE, printProgressMessages: verbose, in: in, out: out, confirmData: NIL, Confirm: Confirm ! DFUser.TooManyEntries => {err _ TRUE; CONTINUE}]; DFUser.CleanupEnumerate[]}; guess _ guess + guess; IF err OR count = 0 THEN LOOP; lastMap _ map _ GenerateMapFromEntryList[head, NIL, count]; RETURN ENDLOOP; ERROR DFUser.TooManyEntries; }; KeyList: LIST OF ATOM = LIST[$Y, $N, $A, $Q]; Confirm: PROC [in, out: IO.STREAM, data: REF, msg: ROPE, dch: CHAR] RETURNS [CHAR] = TRUSTED { exec: UserExec.ExecHandle _ UserExec.GetExecHandle[]; answer: ATOM _ UserExec.AskUser [msg: " OK? ", defaultKey: $Y, exec: exec, keyList: KeyList]; SELECT answer FROM $Y => RETURN ['Y]; $N => RETURN ['N]; $A => RETURN ['A]; $Q => RETURN ['Q]; ENDCASE => RETURN ['?]; }; GenerateMapFromProc: PUBLIC PROC [each: EachProc, data: REF _ NIL, prefix: ROPE _ NIL] RETURNS [map: Map] = { head: EntryRef _ NIL; stamp: TimeStamp.Stamp; name: ROPE _ NIL; new: EntryRef _ NIL; count: NAT _ 0; DO [stamp, name] _ each[data]; IF name.Size[] = 0 THEN EXIT; new _ NewEntryRef[name]; new.version _ LOOPHOLE[stamp]; count _ count + 1; new.next _ head; head _ new; ENDLOOP; lastMap _ map _ GenerateMapFromEntryList[head, prefix, count]; }; bcdList: LIST OF ROPE _ LIST["bcd", "symbols"]; NonBCDFilter: PUBLIC FilterProc = { RETURN[NOT TestExtension[name, bcdList]] }; OnlyBCDFilter: PUBLIC FilterProc = { RETURN[TestExtension[name, bcdList]] }; MergeMaps: PUBLIC PROC [map1,map2: VersionMap.Map] RETURNS [map: VersionMap.Map] = { implicit1: INT _ map1.len - CountExplicitPrefixes[map1]; prefix1: ROPE _ map1.GetPrefix[]; implicit2: INT _ map2.len - CountExplicitPrefixes[map2]; prefix2: ROPE _ map2.GetPrefix[]; prefix: ROPE _ prefix1; IF map1.Length[] = 0 THEN RETURN [map2]; IF map2.Length[] = 0 THEN RETURN [map1]; IF implicit1*prefix1.Size[] < implicit2*prefix2.Size[] THEN { tempMap: VersionMap.Map _ map1; map1 _ map2; map2 _ tempMap; }; IF NOT Rope.Equal[prefix1, prefix2, FALSE] THEN map2 _ DistributePrefix[map2]; prefix1 _ map1.GetPrefix[]; prefix2 _ map2.GetPrefix[]; map _ pz.NEW[VersionMap.MapRep[map1.len + map2.len]]; map.names _ Rope.Replace[map2.names, 0, prefix2.Size[]+1, map1.names]; map.prefix _ map1.prefix; {-- now merge the index set index1: NAT _ 0; index2: NAT _ 0; valid1, valid2: BOOL _ TRUE; offset: INT _ map1.names.Size[] - prefix2.Size[] - 1; -- offset of start of map2 names entry1: VersionMap.MapEntry _ map1[index1]; entry2: VersionMap.MapEntry _ map2[index2]; FOR i: NAT IN [0..map.len) DO SELECT TRUE FROM NOT valid2 OR (valid1 AND VersionPred[entry1.stamp, entry2.stamp]) => { map[i] _ entry1; index1 _ index1 + 1; valid1 _ index1 < map1.len; IF valid1 THEN entry1 _ map1[index1]; }; valid2 => { entry2.index _ entry2.index + offset; map[i] _ entry2; index2 _ index2 + 1; valid2 _ index2 < map2.len; IF valid2 THEN entry2 _ map2[index2]; }; ENDCASE => ERROR; -- we blew it somewhere ENDLOOP; }; }; DistributePrefix: PUBLIC PROC [map: VersionMap.Map] RETURNS [newMap: VersionMap.Map _ NIL] = { IF map # NIL THEN { names: ROPE _ map.names; explicit: INT _ CountExplicitPrefixes[map]; implicit: INT _ map.len - explicit; prefix: ROPE _ map.GetPrefix[]; prefixSize: INT _ prefix.Size[]; newNamesLen: INT _ names.Size[] + implicit * prefixSize; delta: INT _ 0; prefixPos: INT _ 0; inName: BOOL _ TRUE; namePos: INT _ prefixSize; nextIndex: NAT _ 0; getChar: PROC RETURNS [c: CHAR] = { DO IF prefixPos < prefixSize THEN { c _ prefix.Fetch[prefixPos]; prefixPos _ prefixPos + 1; RETURN}; IF inName THEN { c _ names.Fetch[namePos]; namePos _ namePos + 1; inName _ c # '\n; RETURN}; namePos _ map[nextIndex].index; nextIndex _ nextIndex + 1; inName _ TRUE; c _ names.Fetch[namePos]; SELECT c FROM '/, '[ => prefixPos _ prefixSize; ENDCASE => prefixPos _ 0; ENDLOOP; }; IF implicit <= 0 OR names.Fetch[map.prefix] = '\n THEN RETURN [map]; -- no prefix to distribute newMap _ pz.NEW[VersionMap.MapRep[map.len]]; FOR i: CARDINAL IN [0..map.len) DO entry: VersionMap.MapEntry _ map[i]; char: CHAR _ names.Fetch[entry.index]; entry.index _ entry.index + delta; newMap[i] _ entry; SELECT char FROM '/, '[ => {}; ENDCASE => delta _ delta + prefixSize; ENDLOOP; newMap.prefix _ 0; -- we retain the prefix, even though it is distributed newMap.names _ Rope.FromProc[newNamesLen, getChar, maxPiece]; }; }; SplitMap: PUBLIC PROC [map: VersionMap.Map] RETURNS [symbols,source: VersionMap.Map _ NIL] = { IF map # NIL THEN { count1,count2: INT _ 0; chars1,chars2: INT _ 0; index1,index2: INT _ 0; names: ROPE _ map.names; FOR i: NAT IN [0..map.len) DO entry: VersionMap.MapEntry _ map[i]; pos: INT _ Rope.SkipTo[names, entry.index, ".\n"]+1; len: INT _ Rope.SkipTo[names, pos, "!\n"] - pos; IF len > 0 THEN { chars: INT _ Rope.SkipTo[names, pos, "\n"] - entry.index; SELECT len FROM Rope.Run[names, pos, "bcd", 0, FALSE] => {count1 _ count1 + 1; chars1 _ chars1 + chars}; Rope.Run[names, pos, "symbols", 0, FALSE] => {count1 _ count1 + 1; chars1 _ chars1 + chars}; ENDCASE => {count2 _ count2 + 1; chars2 _ chars2 + chars}; }; ENDLOOP; IF count1 > 0 THEN { symbols _ pz.NEW[VersionMap.MapRep[count1]]; symbols.prefix _ map.prefix; symbols.names _ map.names}; IF count2 > 0 THEN { source _ pz.NEW[VersionMap.MapRep[count2]]; source.prefix _ map.prefix; source.names _ map.names}; FOR i: NAT IN [0..map.len) DO entry: VersionMap.MapEntry _ map[i]; pos: INT _ Rope.SkipTo[names, entry.index, ".\n"]+1; len: INT _ Rope.SkipTo[names, pos, "!\n"] - pos; IF len > 0 THEN { SELECT len FROM Rope.Run[names, pos, "bcd", 0, FALSE] => {symbols[index1] _ entry; index1 _ index1 + 1}; Rope.Run[names, pos, "symbols", 0, FALSE] => {symbols[index1] _ entry; index1 _ index1 + 1}; ENDCASE => {source[index2] _ entry; index2 _ index2 + 1}; }; ENDLOOP; }; }; CompressMap: PUBLIC PROC [map: VersionMap.Map] RETURNS [newMap: VersionMap.Map _ NIL] = { names: ROPE _ map.names; newLen: INT _ 0; inName: BOOL _ FALSE; inPrefix: BOOL _ TRUE; nextIndex: NAT _ 0; ropePos: INT _ 0; namePos: INT _ 0; prefix: ROPE _ names.Flatten[namePos, names.SkipTo[namePos, "\n"]]; prefixLen: INT _ prefix.Size[]; getChar: PROC RETURNS [c: CHAR] = { DO IF inPrefix THEN { IF namePos < prefixLen THEN {c _ prefix.Fetch[namePos]; namePos _ namePos + 1} ELSE {c _ '\n; inPrefix _ FALSE}; ropePos _ ropePos + 1; RETURN}; IF inName THEN { c _ names.Fetch[namePos]; namePos _ namePos + 1; inName _ c # '\n; ropePos _ ropePos + 1; RETURN}; namePos _ newMap[nextIndex].index; newMap[nextIndex].index _ ropePos; nextIndex _ nextIndex + 1; inName _ TRUE; ENDLOOP; }; newMap _ pz.NEW[MapRep[map.len]]; newMap.prefix _ 0; IF prefixLen = 0 THEN FOR i: CARDINAL IN [0..map.len) DO entry: VersionMap.MapEntry _ map[i]; char: CHAR _ names.Fetch[entry.index]; SELECT char FROM '[ => { end: INT _ Rope.SkipTo[names, entry.index, ">\n"]; IF names.Fetch[end] = '> THEN { prefixLen _ end - entry.index + 1; prefix _ names.Flatten[entry.index, prefixLen]; EXIT}}; ENDCASE; ENDLOOP; newLen _ prefixLen+1; FOR i: CARDINAL IN [0..map.len) DO entry: VersionMap.MapEntry _ map[i]; pos: INT _ entry.index; char: CHAR _ names.Fetch[pos]; len: INT _ Rope.SkipTo[names, pos, "\n"] - pos + 1; SELECT char FROM '[ => IF Rope.Run[names, pos, prefix, 0, FALSE] = prefixLen THEN { newLen _ newLen + len - prefixLen; entry.index _ pos + prefixLen} ELSE newLen _ newLen + len; ENDCASE => newLen _ newLen + len; newMap[i] _ entry; ENDLOOP; newMap.names _ Rope.FromProc[newLen, getChar, maxPiece]; }; TestExtension: PROC [name: ROPE, extList: LIST OF ROPE] RETURNS [BOOL] = { exclPos: INT _ name.Size[]; pos: INT _ exclPos; WHILE pos > 0 DO c: CHAR _ name.Fetch[pos _ pos - 1]; SELECT c FROM '. => { len: INT _ exclPos-(pos _ pos + 1); FOR xl: LIST OF ROPE _ extList, xl.rest WHILE xl # NIL DO ext: ROPE _ xl.first; IF Rope.Run[name, pos, ext, 0, FALSE] = len THEN RETURN [TRUE]; ENDLOOP; RETURN [FALSE]}; '! => exclPos _ pos; ENDCASE; ENDLOOP; RETURN [FALSE]; }; CurrentTime: PROC RETURNS [ROPE] = TRUSTED { RETURN [Convert.ValueToRope[[time[Time.Current[]]]]]; }; MyStampToHex: PROC [stamp: MyStamp] RETURNS [ROPE] = { index: NAT _ 0; each: PROC RETURNS [c: CHAR] = { x: CARDINAL [0..15] _ hex[index]; IF x IN [0..9] THEN c _ '0 + x ELSE c _ 'A + (x-10); index _ index + 1; }; hex: MyStampAsHex _ LOOPHOLE[stamp]; RETURN [Rope.FromProc[12, each]]; }; PutStampAsHex: PROC [st: IO.STREAM, stamp: MyStamp] = { hex: MyStampAsHex _ LOOPHOLE[stamp]; FOR index: CARDINAL IN [0..12) DO x: CARDINAL [0..15] _ hex[index]; c: CHAR _ IF x IN [0..9] THEN '0 + x ELSE 'A + (x-10); st.PutChar[c]; ENDLOOP; }; IndexToShortName: PROC [map: Map, index: NameMapIndex] RETURNS [ROPE] = { names: ROPE _ map.names; end: INT _ names.SkipTo[index, "!\n"]; pos: INT _ end; WHILE pos > index DO SELECT names.Fetch[pos] FROM '/, '], '> => EXIT; ENDCASE; pos _ pos - 1; ENDLOOP; RETURN [names.Substr[pos, end-pos]]; }; Snooze: ENTRY PROC [lp: LONG POINTER TO BOOL] = TRUSTED { ENABLE UNWIND => NULL; UNTIL lp^ DO WAIT alarmClock; ENDLOOP; }; Awake: ENTRY PROC [lp: LONG POINTER TO BOOL] = TRUSTED { ENABLE UNWIND => NULL; lp^ _ TRUE; BROADCAST alarmClock; }; ClarkKent: PROC = TRUSTED { Superman: ENTRY PROC RETURNS [ok: BOOL _ TRUE] = TRUSTED { ENABLE UNWIND => NULL; IF reportInterval = 0 THEN {ok _ FALSE; reportProcess _ NIL} ELSE WAIT reportTime; }; last: ROPE _ NIL; lastTime: System.Microseconds _ System.PulsesToMicroseconds[System.GetClockPulses[]]; lastCount: INT _ fileCount _ 0; WHILE Superman[] DO IF last # report THEN { thisTime: System.Microseconds _ System.PulsesToMicroseconds[System.GetClockPulses[]]; currentFiles: INT _ fileCount; deltaFiles: INT _ currentFiles - lastCount; deltaMicros: System.Microseconds _ thisTime - lastTime; MessageWindow.Append[last _ report, TRUE]; IF deltaMicros > 1000 THEN { invSecs: REAL _ 1E6/deltaMicros; filesPerSec: REAL _ deltaFiles * invSecs; MessageWindow.Append[" (files: ", FALSE]; MessageWindow.Append[Convert.ValueToRope[[signed[currentFiles]]], FALSE]; MessageWindow.Append[", rate: ", FALSE]; MessageWindow.Append[Convert.ValueToRope[[real[filesPerSec, 2]]], FALSE]; MessageWindow.Append[")", FALSE]; lastTime _ thisTime; lastCount _ currentFiles; }; }; ENDLOOP; MessageWindow.Append[report _ NIL, TRUE]; }; BytesForEntries: PROC [n: INT] RETURNS [INT] = { words: INT _ SIZE[MapEntry]*n + SIZE[MapRep[0]] - SIZE[ROPE]; RETURN [words+words]}; EntrySortPred: PriorityQueue.SortPred = { xx: EntryRef _ NARROW[x]; yy: EntryRef _ NARROW[y]; IF xx.version.num # yy.version.num THEN RETURN [xx.version.num < yy.version.num]; IF xx.version.hi # yy.version.hi THEN RETURN [xx.version.hi < yy.version.hi]; RETURN [xx.version.lo < yy.version.lo]; }; VersionPred: PROC [version1,version2: VersionMap.MyStamp] RETURNS [BOOL] = { IF version1.num # version2.num THEN RETURN [version1.num < version2.num]; IF version1.hi # version2.hi THEN RETURN [version1.hi < version2.hi]; RETURN [version1.lo < version2.lo]; }; NewEntryRef: PROC [name: ROPE] RETURNS [EntryRef] = { report _ name; fileCount _ fileCount + 1; RETURN [qz.NEW[EntryRep _ [next: NIL, name: name, version: NullStamp]]]}; GenerateEntryListFromMap: PROC [map: Map] RETURNS [head: EntryRef, prefix: ROPE, count: NAT] = { count _ VersionMap.Length[map]; prefix _ NIL; head _ NIL; IF count > 0 THEN { stamp: TimeStamp.Stamp; name: ROPE; ent, tail: EntryRef; [stamp, name] _ map.Fetch[0]; head _ tail _ NewEntryRef[name]; tail.version _ LOOPHOLE[stamp]; FOR i: NAT IN [1..count) DO [stamp, name] _ map.Fetch[i]; ent _ NewEntryRef[name]; ent.version _ LOOPHOLE[stamp]; tail.next _ ent; tail _ ent; ENDLOOP; }; }; EntryListSize: PROC [elist: EntryRef] RETURNS [count: INT _ 0] = { FOR list: EntryRef _ elist, list.next UNTIL list = NIL DO count _ count + 1; ENDLOOP; }; CopyEntryList: PROC [elist: EntryRef] RETURNS [copy: EntryRef _ NIL] = { tail: EntryRef _ NIL; FOR list: EntryRef _ elist, list.next UNTIL list = NIL DO new: EntryRef _ NewEntryRef[list.name]; new.version _ list.version; IF tail = NIL THEN copy _ new ELSE tail.next _ new; tail _ new; ENDLOOP; }; MergeEntryLists: PROC [elist1,elist2: EntryRef, prefix1,prefix2: ROPE _ NIL] RETURNS [head: EntryRef _ NIL, prefix: ROPE _ NIL, count: NAT] = { count1: INT _ EntryListSize[elist1]; count2: INT _ EntryListSize[elist2]; tail, rem: EntryRef _ NIL; count _ count1 + count2; WHILE elist1 # NIL AND elist2 # NIL DO better: EntryRef _ NIL; IF VersionPred[elist1.version, elist2.version] THEN { better _ elist1; elist1 _ elist1.next; } ELSE { better _ elist2; elist2 _ elist2.next; }; IF tail = NIL THEN head _ better ELSE tail.next _ better; tail _ better; ENDLOOP; IF prefix1 # NIL AND (count1 > count2 OR prefix2 = NIL) THEN prefix _ prefix1 ELSE prefix _ prefix2; IF elist1 = NIL THEN rem _ elist1 ELSE rem _ elist2; IF tail = NIL THEN head _ rem ELSE tail.next _ rem; }; CountExplicitPrefixes: PROC [map: VersionMap.Map] RETURNS [count: INT _ 0] = { IF map # NIL THEN { names: ROPE _ map.names; FOR i: NAT IN [0..map.len) DO entry: VersionMap.MapEntry _ map[i]; c: CHAR _ names.Fetch[entry.index]; SELECT c FROM '/, '[ => count _ count + 1; ENDCASE; ENDLOOP; }; }; GenerateEntryListFromRemote: PROC [host,match: ROPE _ NIL, oldMap: Map _ NIL, filter: FilterProc _ NIL] RETURNS [head: EntryRef _ NIL, prefix: ROPE _ NIL, count: NAT _ 0] = TRUSTED { stringMatch: STRING _ [80]; stp: UnsafeSTP.Handle _ NIL; tab: SymTab.Ref _ NIL; prefixLen: INT _ 0; -- length of common prefix, including server skipLen: INT _ 0; -- length of common prefix, excluding server bracketedHost: ROPE _ Rope.Cat["[", host, "]"]; tail: EntryRef _ NIL; EachFile: UnsafeSTP.NoteFileProcType = TRUSTED { rname: ROPE _ ConvertUnsafe.ToRope[file]; new: EntryRef _ NIL; usePrefix: BOOL _ Rope.Run[rname, 0, match, 0, FALSE] = skipLen; continue _ yes; IF filter # NIL AND NOT filter[rname] THEN RETURN; -- user does not want this one IF usePrefix AND prefix # NIL THEN rname _ rname.Substr[skipLen] ELSE rname _ bracketedHost.Concat[rname]; IF LookupName[tab, rname] THEN RETURN; -- already seen new _ NewEntryRef[rname]; IF tail = NIL THEN head _ new ELSE tail.next _ new; tail _ new; count _ count + 1; SELECT TRUE FROM Rope.Match["*.bcd!*", rname, FALSE], Rope.Match["*.symbols!*", rname, FALSE] => { -- we will get the date later by accessing the first page of the bcd }; ENDCASE => { info: UnsafeSTP.FileInfo _ UnsafeSTP.GetFileInfo[stp]; IF info # NIL AND info.create # NIL THEN { s: LONG STRING _ info.create; len: NAT _ s.length; get: PROC [i: INT] RETURNS [CHAR] = TRUSTED { IF i IN [0..len) THEN RETURN [s[i]]; RETURN [0C]}; value: Convert.Value _ Convert.Parse [[get[get]], [time[Time.defaultTime]], 0, len].value; WITH v: value SELECT FROM time => new.version _ LOOPHOLE[TimeStamp.Stamp[0, 0, v.time], MyStamp] ENDCASE; }; }; }; [head, prefix, count] _ GenerateEntryListFromMap[oldMap]; tab _ GenSymTabFromVersionMap[oldMap]; skipLen _ match.Index[0, "*"]; IF oldMap = NIL THEN { prefix _ Rope.Cat[bracketedHost, match.Substr[0, skipLen]]; prefixLen _ prefix.Size[]; }; ConvertUnsafe.AppendRope[stringMatch, match]; stp _ STPInit[host]; UnsafeSTP.Enumerate[stp, stringMatch, EachFile]; UnsafeSTP.Close[stp ! ANY => CONTINUE]; }; GenSymTabFromVersionMap: PROC [map: Map] RETURNS [tab: SymTab.Ref _ NIL] = { count: NAT _ VersionMap.Length[map]; tab _ SymTab.Create[MAX[53, (count+5)/2], FALSE]; IF count > 0 THEN { FOR i: NAT IN [0..count) DO StoreName[tab, VersionMap.FetchName[map, i]]; ENDLOOP; }; }; StoreName: PROC [tab: SymTab.Ref, name: ROPE] = { [] _ SymTab.Insert[tab, name, name]; }; LookupName: PROC [tab: SymTab.Ref, name: ROPE] RETURNS [BOOL] = { x: REF _ SymTab.Fetch[tab, name].val; RETURN [x # NIL]; }; GetNextFromList: ENTRY PROC [gen: GenerateData] RETURNS [ent: EntryRef] = { IF gen.list # NIL AND gen.errors = 0 THEN { ent _ gen.list; gen.list _ ent.next; RETURN}; gen.countGoing _ gen.countGoing - 1; BROADCAST gen.done; }; WaitForGenDone: ENTRY PROC [gen: GenerateData] = { UNTIL gen.countGoing = 0 DO WAIT gen.done; ENDLOOP; }; GenerateMapFromEntryList: PROC [head: EntryRef, prefix: ROPE, count: NAT] RETURNS [map: Map] = TRUSTED { prefixLen: INT _ prefix.Size[]; skipPos: INT _ prefix.Index[0, "]"] + 1; skipLen: INT _ prefixLen - skipPos; host: ROPE _ IF prefixLen = 0 THEN NIL ELSE prefix.Flatten[1, skipPos-2]; directory: ROPE _ IF skipPos >= prefixLen THEN NIL ELSE prefix.Substr[skipPos]; list: EntryRef _ head; offset: INT _ 0; pq: PriorityQueue.Ref _ PriorityQueue.Predict[count, EntrySortPred]; gen: GenerateData _ pz.NEW[GenerateDataRep]; gen.host _ host; gen.directory _ directory; gen.list _ head; gen.pq _ pq; gen.countGoing _ outstandingRequests; IFSFile.Initialize[]; FOR i: NAT IN [0..outstandingRequests) DO Process.Detach[FORK VersionFromName[prefix, gen]]; ENDLOOP; map _ pz.NEW[MapRep[count]]; WaitForGenDone[gen]; FlushServerCache[]; IFSFile.Finalize[! ANY => CONTINUE]; IF gen.errors # 0 THEN RETURN [NIL]; map.prefix _ offset _ 0; -- offset of prefix start offset _ prefixLen + 1; -- byte offset of first name head _ list _ NIL; FOR i: NAT IN [0..count) DO each: EntryRef _ NARROW[pq.Remove[]]; each.next _ NIL; map[i] _ [each.version, offset]; IF head = NIL THEN head _ each ELSE list.next _ each; list _ each; offset _ offset + each.name.Size[] + 1; ENDLOOP; {-- make up the names string EachChar: PROC RETURNS [c: CHAR] = TRUSTED { IF thisIndex = thisLen THEN { c _ '\n; IF list = NIL THEN RETURN; thisRope _ list.name; thisIndex _ 0; thisLen _ thisRope.Size[]; list _ list.next; RETURN }; c _ thisRope.Fetch[thisIndex]; thisIndex _ thisIndex + 1; }; thisRope: ROPE _ prefix; thisIndex: INT _ 0; thisLen: INT _ prefixLen; list _ head; map.names _ Rope.FromProc[offset, EachChar, maxPiece]}; }; STPInit: PROC [server: ROPE] RETURNS [stp: UnsafeSTP.Handle] = TRUSTED { nameRope,passRope: ROPE _ NIL; nameString: STRING _ [80]; passString: STRING _ [80]; hostString: STRING _ [40]; herald: LONG STRING _ NIL; props: UnsafeSTP.DesiredProperties _ ALL[FALSE]; stp _ UnsafeSTP.Create[]; [nameRope,passRope] _ UserCredentials.GetUserCredentials[]; ConvertUnsafe.AppendRope[nameString, nameRope]; ConvertUnsafe.AppendRope[passString, passRope]; ConvertUnsafe.AppendRope[hostString, server]; UnsafeSTP.Login[stp, nameString, passString]; herald _ UnsafeSTP.Open[stp, hostString]; Heap.systemZone.FREE[@herald]; props[directory] _ TRUE; props[nameBody] _ TRUE; props[version] _ TRUE; props[createDate] _ TRUE; UnsafeSTP.SetDesiredProperties[stp, props]; }; IFSInit: ENTRY PROC [server: ROPE] RETURNS [ifs: IFSFile.FSInstance] = TRUSTED { ENABLE UNWIND => NULL; nameRope,passRope: ROPE _ NIL; nameString: STRING _ [80]; passString: STRING _ [80]; serverString: STRING _ [80]; FOR list: OpenServerList _ serverCache, list.rest UNTIL list = NIL DO IF Rope.Equal[list.first.name, server, FALSE] THEN { list.first.count _ list.first.count + 1; RETURN [list.first.ifs]}; ENDLOOP; [nameRope,passRope] _ UserCredentials.GetUserCredentials[]; ConvertUnsafe.AppendRope[serverString, server]; ConvertUnsafe.AppendRope[nameString, nameRope]; ConvertUnsafe.AppendRope[passString, passRope]; ifs _ IFSFile.Login [serverString, nameString, passString, nameString, passString]; serverCache _ CONS[[server, ifs, 1], serverCache]; }; IFSFinis: ENTRY PROC [ifs: IFSFile.FSInstance] = TRUSTED { ENABLE UNWIND => NULL; FOR list: OpenServerList _ serverCache, list.rest UNTIL list = NIL DO IF ifs = list.first.ifs THEN list.first.count _ list.first.count - 1; ENDLOOP; }; FlushServerCache: PROC = TRUSTED { lag: OpenServerList _ NIL; FOR list: OpenServerList _ serverCache, list.rest UNTIL list = NIL DO IF list.first.count <= 0 THEN { IFSFile.Logout[list.first.ifs ! ANY => CONTINUE]; IF lag = NIL THEN serverCache _ list.rest ELSE lag.rest _ list.rest}; ENDLOOP; }; OpenServerList: TYPE = LIST OF OpenServerRecord; OpenServerRecord: TYPE = RECORD [name: ROPE, ifs: IFSFile.FSInstance, count: INTEGER]; serverCache: OpenServerList _ NIL; WhenCompleted: IFSFile.Completer = TRUSTED { IF outcome # ok THEN ERROR VersionNotAvailable; Awake[LOOPHOLE[arg, LONG POINTER TO BOOL]]; }; VersionFromName: PROC [prefix: ROPE, gen: GenerateData] = TRUSTED { ent: EntryRef _ NIL; WHILE (ent _ GetNextFromList[gen]) # NIL DO ENABLE ABORTED => {gen.errors _ gen.errors + 1; EXIT}; len: INT _ prefix.Size[]; IF len > 0 AND Rope.Run[prefix, 0, ent.name, 0, FALSE] = len THEN { -- now we can strip off the prefix! ent.name _ Rope.Substr[ent.name, len]; }; IF ent.version = NullStamp THEN { -- must fill in the version FillInBcdVersion[gen, ent]}; PQInsert[gen.pq, ent]; ENDLOOP; }; FillInBcdVersion: PROC [gen: GenerateData, ent: EntryRef] = TRUSTED { bcd: BcdDefs.BCD; ifs: IFSFile.FSInstance _ NIL; fh: IFSFile.FileHandle _ NIL; string: STRING _ [132]; ready: BOOL _ FALSE; lp: LONG POINTER TO BOOL _ @ready; fname: ROPE _ ent.name; fnSize: INT _ fname.Size[]; rbPos: INT _ fname.Index[0, "]"]; host: ROPE _ gen.host; {ENABLE UNWIND => { IF fh # NIL THEN IFSFile.Close[fh]; IF ifs # NIL THEN IFSFinis[ifs]}; IF rbPos < fnSize THEN { host _ fname.Substr[1, rbPos-1]; fname _ fname.Substr[rbPos+1]}; ifs _ IFSInit[host]; ConvertUnsafe.AppendRope[string, gen.directory]; ConvertUnsafe.AppendRope[string, fname]; fh _ IFSFile.Open[ifs, string ! ABORTED => GO TO abort; ANY => GO TO none]; IFSFile.StartRead [ fh, 0, Environment.bytesPerWord*SIZE[BcdDefs.BCD], @bcd, WhenCompleted, LOOPHOLE[lp, LONG UNSPECIFIED]]; report _ fname; fileCount _ fileCount + 1; Snooze[lp]; IFSFile.Close[fh ! ABORTED => GO TO abort; ANY => CONTINUE]; IFSFinis[ifs]}; ent.version _ LOOPHOLE[bcd.version, MyStamp]; EXITS none => {}; abort => ERROR ABORTED; }; RopeFromFileData: TYPE = RECORD [st: IO.STREAM, size: INT, offset: INT _ 0]; RopeFromFile: PROC [st: IO.STREAM, size: INT, offset: INT _ 0] RETURNS [ROPE] = { data: REF _ qz.NEW[RopeFromFileData _ [st, size, offset]]; RETURN [Rope.MakeRope[data, size, FetchFromFile]]; }; FetchFromFile: ENTRY Rope.FetchType = { ENABLE UNWIND => NULL; mine: REF RopeFromFileData _ NARROW[data]; IO.SetIndex[mine.st, index+mine.offset]; RETURN [IO.GetChar[mine.st]]; }; PQInsert: ENTRY PROC [pq: PriorityQueue.Ref, entry: EntryRef] = { pq.Insert[entry]; }; MyPut: PROC [st: IO.STREAM, r: ROPE] = { FOR i: INT IN [0..r.Size[]) DO IO.PutChar[st, r.Fetch[i]]; ENDLOOP; }; END. öVersionMapBuilderImpl.mesa Russ Atkinson, February 18, 1983 5:07 pm edited by Doug Wyatt, July 6, 1983 1:08 pm Types & stuff must be as long as TimeStamp.Stamp we extract num to use as uniformly distributed bits num should be the low bits of the time the full names follow the entries, sans prefix, follow the index each full name ends in a CR (15C) **** BEGIN EXPORTED PROCEDURES **** Display the entry. Examine short names for equality. the 1st line must have the prefix for the map the rest of the lines have version # followed by the mid name (full name sans prefix) grab the hex stamp skip over separators grab the name SetReportInterval sets the interval between reports of progress to the message window. These reports are made by GenerateMapFromRemote and other long-winded procedures. If the interval is 0, then no reporting is performed. generates a map from a DF file requires export of DFUser to run PROC [host, directory, shortName: LONG STRING, version: CARDINAL, createTime: LONG CARDINAL, includedDFFile, importedDFFile, readOnly: BOOL, immediateParentDF: LONG STRING] Accumulate the long file name we use the create date as the version stamp generates a map from user-supplied entries generate the next entry FilterProc: TYPE = PROC [name: ROPE] RETURNS [ok: BOOL]; type of user-supplied filter on file names return TRUE to use name, FALSE to skip it filters out BCDs filters in BCDs only We save more characters by using map2's prefix, so swap the maps The prefixes are not equal, so we must expand map2 Now we have to merge the maps. map1 has the better entry map2 has the better entry DistributePrefix[map] takes a version map and expands all of the prefixes in it. This is good preparation for merging version maps, of course. The resulting map will be the initial map if all of the entries have explicit prefixes. The resulting map will have a prefix stored in the names, incidentally. We are expanding the prefix We are expanding the file name We need another entry Oddly enough, first we build the index SplitMap[map] splits a version map into maps for symbols and source. The names rope is shared between both the input map and the two resulting maps, which is much more efficient, but may hold on to some rather large ropes. Determine the size of the symbols and source maps Conditionally create the symbols map Conditionally create the source map Now, actually copy the entries to the new map(s) CompressMap[map] compresses a version map, discarding unused portions from the names. We also make a half-hearted attempt to factor out the first prefix we see from the rest of the names (we should be able to do better). We are expanding the prefix We are expanding the file name We need another entry Find some suitable prefix Calculate the eventual size of the rope & adjust the new map indexes Factor out this prefix (including from the new map) Finally make the new names **** END EXPORTED PROCEDURES **** Return the name without the prefix or version [x: Item, y: Item, data: REF _ NIL] RETURNS [BOOL] GenerateEntryListFromMap[map] generates a SORTED entry list. MergeEntryLists[elist1,elist2] performs a destructive merge on elist1 and elist2, which must both be initially sorted. No elements are discarded and no checking for duplicate versions is performed. elist1 has the better entry elist2 has the better entry assert elist1 = NIL OR elist2 = NIL declarations of local variables PROC [file: STRING] RETURNS [continue: Continue] get the time from the create date only login using our default credentials build the name list clean up the STP connection its leaf time, folks! wait for the dust to settle (otherwise there is TROUBLE!) close up the leafy world calculate the entries in the map we need the next name [arg: CompleterArg, outcome: Problem] called on completion of Leaf transfer this proc is the base of a process that fills in version stamps for an entry names in the entries will have the prefix (if any) removed unfortunately this must be an entry proc to avoid races monitorized to prevent scrambling the priority queue Ê,˜Jšœ™Jšœ(™(Jšœ*™*J˜šÏk ˜ Jšœœœ˜Jšœœ'˜4Jšœœ˜)šœ˜ J˜H—Jšœ œ˜!Jšœœ˜Jšœœ˜šœ˜ J˜a—šœ˜JšœQœ˜Y—Jšœœ ˜Jšœœ*˜=Jšœœ&˜3šœ˜ JšœQœœ˜|—Jšœ œ ˜Jšœœ˜*Jšœœ6˜BJšœœ˜"Jšœ œ ˜šœ ˜J˜‚—Jšœœ˜+Jšœ œ2˜@šœ ˜J˜T—Jšœœ˜/J˜—šÐblœœ˜$Jšœ8œ‚˜ÃJšœ˜Jšœ ˜Jšœœœ%˜1J˜—šœ ™ š œ œœ œœ œ˜@Jšœ"™"Jšœ3™3Jšœ&™&—J˜Jš œœœœ œ ˜5J˜Jšœœœ˜Jšœ œœœ˜šœ œœœ˜Jšœœœ˜JšœÏc ˜;Jšœ œœœ ˜-Jšœ@™@Jšœ!™!—šœ œ˜J˜&—JšœœœŸ˜7Jšœœ˜1J˜Jšœ œœ˜Jšœœœ˜,J˜Jšœ œœ ˜Jšœ œœœ˜GJ˜Jšœœœ˜)šœœœ˜ Jšœœœ˜Jšœœ˜Jšœœ˜Jšœ œ˜Jšœœ˜šœ œ˜J˜——Jšœœ!˜)Jšœœ"˜*J˜Jšœ œ˜Jšœ œ˜Jšœœ˜Jšœœœ˜Jšœœœ˜Jšœ œ˜J˜JšœœŸ2˜QJšœœŸ+˜?Jšœ œ˜J˜—šœ#™#J˜Jšœœœœ˜)Jšœ œœœ˜Jšœœœœ˜-J˜—šÏnœœ˜Jšœœœœ˜CJ˜Jšœœœ˜Jšœœœ˜)Jšœœ ˜Jšœœ ˜Jšœ œ˜$J˜#J˜J˜šœœœ˜Jšœ™J˜Jšœœ˜J˜šœœœ˜8Jšœ!™!Jšœœ)˜1Jšœœ ˜(šœœ˜Jšœ˜ Jšœœ˜$——J˜J˜J˜š˜Jšœœ˜J˜Jšœ˜Jšœ œœ˜Jšœ˜—Jšœ˜—J˜JšœœŸ&˜IJšœ ˜ J˜J˜—š  œœœœœ œ˜GJšœ-™-Jšœ=™=Jšœ™Jšœœœ˜(Jšœœœ˜šœ œœœ˜ Jšœ˜J˜—Jšœœ+˜5Jšœœ˜ Jšœœ˜Jšœœœ˜"Jšœœ˜Jšœ ˜ J˜4J˜šœ ˜Jšœœ˜Jšœœ˜Jšœœœ˜Jšœœ˜J˜Jšœ œœ˜Jšœ™š œœœ œ˜+Jšœœ˜šœ˜ Jšœ˜Jšœ˜!Jšœ˜!Jšœœ˜—J˜ J˜J˜Jšœ˜—Jšœ™šœ˜Jšœ œœ˜(J˜J˜Jšœ˜—Jšœ ™ šœ˜šœ œ˜Jšœ(œ˜.—J˜J˜Jšœ˜—šœ˜ Jšœ ˜$Jšœ3˜7—Jšœœ˜J˜Jšœ˜—J˜>J˜J˜—š œ œ œœ˜=Jšœà™àJš œœœ œœ ˜0šœœœœ˜Jšœœœ˜Jšœ˜šœœœ ˜+Jšœœ˜1—Jš œ ˜J˜—Jšœ>˜>Jšœ ˜ J˜J˜—š œœ˜"Jš œœœ œœœœ˜VJšœœ˜Jšœœ˜Jšœœœ˜Jšœœ˜J˜Jšœœœ˜ šœ œœ˜Jšœœ-˜7Jšœ,Ÿ˜>J˜+—šœ,œ˜4Jšœ'˜'—J˜UJ˜>J˜J˜—š œœ˜"Jš œœœ œœœœ˜TJšœœ˜Jšœ™Jšœ ™ Jšœœ˜Jšœœœœ˜%Jšœœ˜Jšœœœ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœ œœ˜šœ%œ˜.Jšœ¬™¬Jšœ œ ˜Jšœ œœ˜Jšœœ˜š œœœœ˜ Jšœœ˜J˜J˜—š  œœœœœœ˜6Jš œœœœœ˜3Jšœœ˜šœœœ˜Jšœœœ ˜šœœ˜šœœœ˜J˜J˜ Jšœ˜——Jšœ˜—J˜J˜—J˜+Jš œœœœœŸ ˜DJ˜Jšœ™Jšœœœ˜'Jšœ œœ%˜Jš œ œœœœœŸ˜TJ˜šœœœ˜J˜J˜—šœ8™8Jšœ*™*Jšœ)™)J˜—Jš œ œœœœ˜/J˜šœœ˜#Jšœ™Jšœœ˜(J˜J˜—šœœ˜$Jšœ™Jšœ˜$J˜J˜—š  œ ˜Jšœœ˜=Jšœ œ*˜8Jšœ œ˜!Jšœ œ*˜8Jšœ œ˜!Jšœœ ˜Jšœœœ˜(Jšœœœ˜(šœ5œ˜=Jšœ@™@Jšœ˜Jšœ ˜ Jšœ˜J˜—šœœœ˜/Jšœ2™2Jšœ˜—Jšœ™Jšœ˜Jšœ˜Jšœ œ)˜5J˜FJ˜šœŸ˜Jšœœ˜Jšœœ˜Jšœœœ˜Jšœœ+Ÿ ˜VJšœ+˜+Jšœ+˜+J˜šœœœ˜šœœ˜šœœ œ.˜GJšœ™Jšœ˜Jšœ˜Jšœ˜Jšœœ˜%J˜—šœ ˜ Jšœ™Jšœ%˜%Jšœ˜Jšœ˜Jšœ˜Jšœœ˜%J˜—JšœœŸ˜)—Jšœ˜—J˜—J˜J˜—š œœ˜Jšœœœ˜@Jšœ±™±šœœœ˜Jšœœ ˜Jšœ œ˜+Jšœ œ˜#Jšœœ˜Jšœ œ˜ Jšœ œ(˜8Jšœœ˜Jšœ œ˜Jšœœœ˜Jšœ œ˜Jšœ œ˜šœ œœœ˜#š˜šœœ˜ Jšœ™Jšœ˜Jšœ˜Jšœ˜—šœœ˜Jšœ™J˜J˜J˜Jšœ˜—Jšœ™J˜J˜Jšœ œ˜Jšœ˜šœ˜ Jšœ!˜!Jšœ˜—Jšœ˜—J˜—šœœ˜6Jšœ Ÿ˜)—Jšœ œ˜,Jšœ&™&šœœœ˜"Jšœ$˜$Jšœœ˜&Jšœ"˜"Jšœ˜šœ˜Jšœ ˜ Jšœ˜&—Jšœ˜—JšœŸ6˜JJšœ=˜=J˜—J˜J˜—š œ ˜Jšœœ#œ˜HJšœß™ßšœœœ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœœ ˜J™1šœœœ˜Jšœ$˜$Jšœœ,˜4Jšœœ(˜0šœ œ˜Jšœœ/˜9šœ˜šœœ˜(Jšœ/˜/—šœ#œ˜,Jšœ/˜/—šœ˜ Jšœ/˜/——J˜—Jšœ˜—Jšœ$™$šœ œ˜Jšœ œ˜,J˜J˜—Jšœ#™#šœ œ˜Jšœ œ˜+Jšœ˜Jšœ˜—Jšœ0™0šœœœ˜Jšœ$˜$Jšœœ,˜4Jšœœ(˜0šœ œ˜šœ˜šœœ˜(Jšœ/˜/—šœ#œ˜,Jšœ/˜/—šœ˜ Jšœ.˜.——J˜—Jšœ˜—Jšœ˜—J˜J˜—š  œœ˜Jšœœœ˜@JšœÝ™ÝJšœœ ˜Jšœœ˜Jšœœœ˜Jšœ œœ˜Jšœ œ˜Jšœ œ˜Jšœ œ˜Jšœœ7˜CJšœ œ˜šœ œœœ˜#š˜šœ œ˜Jšœ™šœ˜Jšœ3˜7Jšœœ˜!—J˜Jšœ˜—šœœ˜Jšœ™J˜J˜J˜J˜Jšœ˜—Jšœ™Jšœ"˜"J˜"J˜Jšœ œ˜Jš˜—J˜—Jšœ œ˜!Jšœ˜šœ˜Jšœ™šœœœ˜"Jšœ$˜$Jšœœ˜&šœ˜šœ˜Jšœœ*˜2šœœ˜Jšœ"˜"Jšœ/˜/Jšœ˜——Jšœ˜—Jšœ˜——JšœD™DJšœ˜šœœœ˜"Jšœ$˜$Jšœœ˜Jšœœ˜Jšœœ+˜3šœ˜šœ˜šœ!œ ˜5šœ˜Jšœ4™4Jšœ"˜"Jšœ˜—Jšœ˜——Jšœ˜!—J˜Jšœ˜—J™Jšœ8˜8Jšœ˜—J˜Jšœ!™!J˜š  œœœ œœœœœ˜JJšœ œ˜Jšœœ ˜šœ ˜Jšœœ˜$šœ˜ šœ˜Jšœœ˜#š œœœœœœ˜9Jšœœ ˜šœœ˜+Jšœœœ˜—Jšœ˜Jšœœ˜——J˜Jšœ˜—Jšœ˜—Jšœœ˜J˜J˜—š   œœœœœ˜,Jšœ/˜5J˜J˜—š  œœœœ˜6Jšœœ˜šœœœœ˜ Jšœœ˜!Jšœœœ œ˜4J˜J˜—Jšœœ˜$Jšœ˜!J˜J˜—š  œœ œ˜7Jšœœ˜$šœœœ ˜!Jšœœ˜!Jš œœœœœœ ˜6Jšœ˜Jšœ˜—J˜J˜—š œœ!œœ˜IJšœ-™-Jšœœ ˜Jšœœ˜&Jšœœ˜šœ ˜šœ˜Jšœœ˜Jšœ˜—J˜Jšœ˜—Jšœ˜$J˜J˜—š œœœœœœœœ˜9Jšœœœ˜Jšœœœ œ˜&J˜J˜—š œœœœœœœœ˜8Jšœœœ˜Jšœœ˜ Jš œ ˜J˜J˜—š  œœœ˜š œœœœœœœ˜:Jšœœœ˜šœ˜Jšœœœ˜&Jšœœ ˜—Jšœ˜—Jšœœœ˜J˜UJšœ œ˜šœ ˜šœœ˜J˜UJšœœ ˜Jšœ œ˜+Jšœ7˜7Jšœ$œ˜*šœœ˜Jšœ œ˜ Jšœ œ˜)Jšœ"œ˜)JšœBœ˜IJšœ!œ˜(JšœBœ˜IJšœœ˜!Jšœ˜Jšœ˜J˜—J˜—Jšœ˜—Jšœœœ˜)Jšœ˜J˜—š  œœœœœ˜0Jš œœœœœœ˜=Jšœ˜J˜—˜)Jšœ2™2Jšœœ˜Jšœœ˜Jšœ!œœ#˜QJšœœœ!˜MJšœ!˜'J˜J˜—š  œœ)œœ˜LJšœœœ˜IJšœœœ˜EJšœ˜#J˜J˜—š  œœœœ˜5Jšœ˜J˜Jšœœœ%˜IJ˜—š œ˜Jšœ œœ œ˜AJšœ<™