DIRECTORY BootFile USING[ Location ], Disk USING[ Channel, DoIO, DriveAttributes, GetBootChainLink, invalid, Label, labelCheck, ok, PageNumber, PageCount, Request, SameDrive, Status ], DiskFace USING[ DontCare, wordsPerPage ], File USING[ Error, FP, GetVolumePages, GetVolumeID, IsDebugger, nullFP, PageCount, PageNumber, PagesForWords, PropertyStorage, propertyWords, RC, Reason, SystemVolume, Volume, VolumeFile, VolumeID ], FileInternal, FileStats USING[ Data, Type, Pulses ], PrincOpsUtils USING[ LongCOPY ], ProcessorFace USING[ GetClockPulses ], VolumeFormat USING[ AbsID, Attributes, lastLogicalRun, LogicalPage, LogicalRun, LogicalRunObject, RelID, RunPageCount ], VM USING[ AddressForPageNumber, Allocate, Free, Interval, PageCount, PageNumber, PageNumberForAddress, PagesForWords, SwapIn, Unpin, wordsPerPage], VMBacking USING[ AttachBackingStorage, Run, RunTableIndex, RunTableObject, RunTablePageNumber ]; FileImpl: CEDAR MONITOR IMPORTS Disk, File, FileInternal, PrincOpsUtils, ProcessorFace, VM, VMBacking EXPORTS DiskFace--RelID,AbsID,Attributes--, File, FileInternal, FileStats SHARES File = BEGIN -- ******** Data Types and minor subroutines ******** -- --DiskFace.--Attributes: PUBLIC TYPE = VolumeFormat.Attributes; --DiskFace.--AbsID: PUBLIC TYPE = VolumeFormat.AbsID; --DiskFace.--RelID: PUBLIC TYPE = VolumeFormat.RelID; --File.--DA: PUBLIC TYPE = VolumeFormat.LogicalPage; Handle: TYPE = REF Object; --File.--Object: PUBLIC TYPE = FileInternal.Object; RunTable: TYPE = FileInternal.RunTable; RunTableObject: TYPE = VMBacking.RunTableObject; RunTableIndex: TYPE = VMBacking.RunTableIndex; PhysicalRun: TYPE = FileInternal.PhysicalRun; lastRun: VMBacking.RunTablePageNumber = LAST[INT]; -- end marker in runTable -- initRuns: CARDINAL = (DiskFace.wordsPerPage-SIZE[VolumeFormat.LogicalRunObject[0]]) / SIZE[VolumeFormat.LogicalRun]; DoPinnedIO: PROC[channel: Disk.Channel, label: POINTER TO Disk.Label, req: POINTER TO Disk.Request] RETURNS[ status: Disk.Status, countDone: Disk.PageCount] = TRUSTED BEGIN interval: VM.Interval = [ page: VM.PageNumberForAddress[req.data], count: VM.PagesForWords[ (IF req.incrementDataPtr THEN req.count ELSE 1)*DiskFace.wordsPerPage] ]; VM.SwapIn[interval: interval, kill: req.command.data=read, pin: TRUE]; [status, countDone] _ Disk.DoIO[channel, label, req ! UNWIND => VM.Unpin[interval] ]; VM.Unpin[interval]; END; --File.--Error: PUBLIC ERROR[why: File.Reason] = CODE; badData: Disk.Status = [unchanged[dataCRCError]]; -- indicates data is bad but label is ok CheckStatus: PROC[status: Disk.Status] = BEGIN why: File.RC = FileInternal.TranslateStatus[status]; IF why # ok THEN ERROR File.Error[why]; END; -- ******** Header and Run-table management ******** -- propFilePages: File.PageCount = File.PagesForWords[File.propertyWords]; --FileInternal.--GetHeaderVM: PUBLIC PROC[file: Handle, runs: CARDINAL] = TRUSTED BEGIN oldVM: LONG POINTER = file.headerVM; oldHeaderVMPages: INT = file.headerVMPages; oldProperties: File.PropertyStorage = file.properties; oldLogical: LONG POINTER TO VolumeFormat.LogicalRunObject = file.logicalRunTable; runTableWords: INT = SIZE[VolumeFormat.LogicalRunObject[runs]]; vmPages: INT = VM.PagesForWords[runTableWords] + VM.PagesForWords[File.propertyWords]; runTableFilePages: File.PageCount = File.PagesForWords[runTableWords]; file.headerVM _ VM.AddressForPageNumber[VM.Allocate[vmPages].page]; file.headerVMPages _ vmPages; -- assign only after VM.Allocate succeeds file.properties _ LOOPHOLE[file.headerVM+runTableFilePages*DiskFace.wordsPerPage]; file.logicalRunTable _ LOOPHOLE[file.headerVM]; file.logicalRunTable.headerPages _ runTableFilePages + propFilePages; file.logicalRunTable.maxRuns _ runs; IF oldVM = NIL THEN BEGIN -- initialise only -- file.logicalRunTable[0].first _ VolumeFormat.lastLogicalRun; file.properties^ _ ALL[0]; END ELSE BEGIN -- initialise from old tables -- PrincOpsUtils.LongCOPY[from: oldLogical, to: file.logicalRunTable, -- may copy extra nwords: MIN[runTableWords, oldHeaderVMPages*VM.wordsPerPage] ]; file.properties^ _ oldProperties^; VM.Free[[VM.PageNumberForAddress[oldVM], oldHeaderVMPages]]; END; END; --FileInternal.--FreeHeaderVM: PUBLIC PROC[file: Handle] = TRUSTED BEGIN IF file.headerVMPages # 0 THEN VM.Free[[VM.PageNumberForAddress[file.headerVM], file.headerVMPages]]; file.headerVMPages _ 0; file.headerVM _ NIL; file.logicalRunTable _ NIL; file.properties _ NIL END; --FileInternal.--TranslateLogicalRunTable: PUBLIC PROC[file: Handle] RETURNS[ File.PageCount ] = TRUSTED BEGIN nRuns: CARDINAL; filePage: File.PageNumber _ [-file.logicalRunTable.headerPages]; FOR nRuns IN [0..file.logicalRunTable.maxRuns) DO IF file.logicalRunTable[nRuns].first = VolumeFormat.lastLogicalRun THEN EXIT; REPEAT FINISHED => ERROR File.Error[inconsistent] ENDLOOP; IF file.runTable = NIL OR file.runTable.length < nRuns+10--arbitrary-- THEN file.runTable _ NEW[RunTableObject[nRuns+10]]; file.runTable.nRuns _ nRuns; FOR i: CARDINAL IN [0..nRuns) DO IF file.logicalRunTable[i].first = VolumeFormat.lastLogicalRun THEN EXIT; [channel: file.runTable[i].channel, diskPage: file.runTable[i].diskPage] _ FileInternal.TranslateLogicalRun[file.logicalRunTable[i], file.volume]; file.runTable[i].filePage _ filePage; filePage _ [filePage + file.logicalRunTable[i].size]; ENDLOOP; file.runTable.nDataPages _ filePage; file.runTable[file.runTable.nRuns].filePage _ lastRun; RETURN[ file.runTable.nDataPages ] END; --FileInternal.--AddRun: PUBLIC PROC[file: Handle, run: POINTER TO PhysicalRun, logicalPage: VolumeFormat.LogicalPage, okPages: VolumeFormat.RunPageCount] = TRUSTED BEGIN logical: LONG POINTER TO VolumeFormat.LogicalRunObject = file.logicalRunTable; physical: RunTable = file.runTable; oldNRuns: CARDINAL = physical.nRuns; physical.nDataPages _ file.size + okPages; IF oldNRuns > 0 AND logical[oldNRuns-1].first + logical[oldNRuns-1].size = logicalPage AND logical[oldNRuns-1].size <= LAST[VolumeFormat.RunPageCount] - okPages AND run.channel = physical[oldNRuns-1].channel THEN logical[oldNRuns-1].size _ logical[oldNRuns-1].size + okPages ELSE BEGIN -- Add another run IF physical.nRuns+1 = logical.maxRuns THEN BEGIN -- extend logical run table to ensure there is space to record an extension -- ERROR File.Error[fragmented] END; physical[oldNRuns] _ run^; logical[oldNRuns].first _ logicalPage; logical[oldNRuns].size _ okPages; physical.nRuns _ oldNRuns + 1; IF physical.nRuns = physical.length THEN BEGIN -- extend physical run table object -- file.runTable _ NEW[RunTableObject[physical.length*2]]; file.runTable.nDataPages _ physical.nDataPages; file.runTable.nRuns _ physical.nRuns; FOR i: CARDINAL IN [0..physical.length) DO file.runTable[i] _ physical[i] ENDLOOP; END; file.runTable[oldNRuns + 1].filePage _ lastRun; logical[oldNRuns + 1].first _ VolumeFormat.lastLogicalRun; END; END; --FileInternal.--LastLogicalPage: PUBLIC PROC[file: Handle] RETURNS [VolumeFormat.LogicalPage] = TRUSTED BEGIN lastRun: VolumeFormat.LogicalRun = file.logicalRunTable[file.runTable.nRuns-1]; RETURN[ [lastRun.first + lastRun.size-1] ] END; --FileInternal.--RemoveFromRunTable: PUBLIC PROC[file: Handle, remove: INT] = TRUSTED BEGIN logical: LONG POINTER TO VolumeFormat.LogicalRunObject = file.logicalRunTable; physical: RunTable = file.runTable; physical.nDataPages _ file.size; WHILE remove > 0 DO IF physical.nRuns = 0 THEN ERROR File.Error[inconsistent]; BEGIN runSize: VolumeFormat.RunPageCount = logical[physical.nRuns-1].size; amount: VolumeFormat.RunPageCount = MIN[remove, runSize]; logical[physical.nRuns-1].size _ runSize - amount; IF runSize - amount = 0 THEN BEGIN -- Remove the new run table entry entirely! -- physical.nRuns _ physical.nRuns - 1; physical[physical.nRuns].filePage _ lastRun; logical[physical.nRuns].first _ VolumeFormat.lastLogicalRun; END; remove _ remove - amount; END; ENDLOOP; END; --FileInternal.--FindRun: PUBLIC PROC[start: File.PageNumber, nPages: File.PageCount, runTable: RunTable] RETURNS[diskPage: Disk.PageNumber, size: Disk.PageCount, channel: Disk.Channel] = BEGIN probe: RunTableIndex _ runTable.nRuns / 2; -- NB: round down -- increment: CARDINAL _ probe; IF runTable.nRuns = 0 THEN ERROR File.Error[inconsistent]; IF start + nPages > runTable.nDataPages THEN ERROR File.Error[unknownPage]; IF start < runTable[0].filePage THEN ERROR File.Error[unknownPage]; DO increment _ (increment+1)/2; SELECT TRUE FROM runTable[probe].filePage > start => probe _ IF probe < increment THEN 0 ELSE probe-increment; runTable[probe+1].filePage <= start => probe _ MIN[probe + increment, runTable.nRuns-1]; ENDCASE => RETURN[ diskPage: [runTable[probe].diskPage + (start-runTable[probe].filePage)], size: IF start + nPages <= runTable[probe+1].filePage THEN nPages ELSE runTable[probe+1].filePage - start, channel: runTable[probe].channel ] ENDLOOP; END; -- ******** Subroutines for access to file pages ******** -- HeaderLabel: PROC[fp: File.FP] RETURNS[Disk.Label] = BEGIN RETURN[ [ fileID: [rel[RelID[fp]]], filePage: 0, attributes: Attributes[header], dontCare: LOOPHOLE[LONG[0]] ] ] END; DataLabel: PROC[fp: File.FP] RETURNS[Disk.Label] = BEGIN RETURN[ [ fileID: [rel[RelID[fp]]], filePage: 0, attributes: Attributes[data], dontCare: LOOPHOLE[LONG[0]] ] ] END; FreeLabel: PROC[volume: File.Volume] RETURNS[Disk.Label] = BEGIN RETURN[ [ fileID: [abs[AbsID[File.GetVolumeID[volume]]]], filePage: 0, attributes: Attributes[freePage], dontCare: LOOPHOLE[LONG[0]] ] ] END; WriteLabels: PROC[channel: Disk.Channel, diskPage: Disk.PageNumber, count: Disk.PageCount, data: LONG POINTER, label: POINTER TO Disk.Label] RETURNS[ status: Disk.Status, countDone: Disk.PageCount] = TRUSTED BEGIN req: Disk.Request _ [ diskPage: diskPage, data: IF data = NIL THEN scratchWriter ELSE data, incrementDataPtr: data # NIL, command: [header: verify, label: write, data: write], count: count ]; [status, countDone] _ DoPinnedIO[channel, label, @req]; END; VerifyLabels: PROC[channel: Disk.Channel, diskPage: Disk.PageNumber, count: Disk.PageCount, label: POINTER TO Disk.Label] RETURNS[ status: Disk.Status, countDone: Disk.PageCount] = TRUSTED BEGIN req: Disk.Request _ [ diskPage: diskPage, data: scratchReader, incrementDataPtr: FALSE, command: [header: verify, label: verify, data: read], count: count ]; [status, countDone] _ DoPinnedIO[channel, label, @req]; IF status = badData THEN { status _ Disk.ok; countDone _ countDone+1; label.filePage _ label.filePage+1 }; END; GetScratchPage: PROC RETURNS[data: LONG POINTER] = TRUSTED BEGIN temp: LONG POINTER TO ARRAY [0..DiskFace.wordsPerPage) OF WORD; data _ VM.AddressForPageNumber[VM.Allocate[VM.PagesForWords[DiskFace.wordsPerPage]].page]; temp _ LOOPHOLE[data]; temp^ _ ALL[0]; END; FreeScratchPage: PROC[data: LONG POINTER] = TRUSTED BEGIN VM.Free[ [ page: VM.PageNumberForAddress[data], count: VM.PagesForWords[DiskFace.wordsPerPage] ] ]; END; scratchWriter: LONG POINTER = GetScratchPage[]; -- scratch buffer for writing (all 0) scratchReader: LONG POINTER = GetScratchPage[]; -- scratch buffer for reading --FileInternal.--FreeRun: PUBLIC PROC[logicalRun: VolumeFormat.LogicalRun, volume: File.Volume, verifyLabel: POINTER TO Disk.Label _ NIL] = BEGIN label: Disk.Label _ FreeLabel[volume]; WHILE logicalRun.size > 0 DO channel: Disk.Channel; diskPage: Disk.PageNumber; status: Disk.Status; verifyStatus: Disk.Status _ Disk.ok; thisTime: Disk.PageCount _ logicalRun.size; countDone: Disk.PageCount; Consume: PROC = BEGIN logicalRun.first _ [logicalRun.first + countDone]; logicalRun.size _ logicalRun.size - countDone; IF verifyLabel # NIL THEN TRUSTED{ verifyLabel.filePage _ verifyLabel.filePage + countDone }; countDone _ 0; END; [channel, diskPage] _ FileInternal.TranslateLogicalRun[logicalRun, volume]; IF verifyLabel # NIL THEN TRUSTED BEGIN -- verify which pages are part of our file temp: Disk.Label _ verifyLabel^; [verifyStatus, thisTime] _ VerifyLabels[channel, diskPage, thisTime, @temp]; END; label.filePage _ logicalRun.first; FileInternal.Free[volume, [first: logicalRun.first, size: thisTime]]; TRUSTED{[status, countDone] _ WriteLabels[channel, diskPage, thisTime, NIL, @label]}; SELECT status FROM Disk.ok => NULL; Disk.invalid => ERROR File.Error[wentOffline]; ENDCASE => countDone _ countDone + 1; -- Page is in our file, but not writeable -- Consume[]; IF verifyLabel # NIL AND status = Disk.ok AND verifyStatus # Disk.ok THEN TRUSTED BEGIN free: Disk.Label _ FreeLabel[volume]; free.filePage _ logicalRun.first; [channel, diskPage] _ FileInternal.TranslateLogicalRun[logicalRun, volume]; [verifyStatus, countDone] _ VerifyLabels[channel, diskPage, logicalRun.size, @free]; IF verifyStatus # Disk.ok AND countDone = 0 THEN -- label isn't in our file, but isn't free, so ignore it -- countDone _ 1 ELSE FileInternal.Free[volume, [first: logicalRun.first, size: countDone]]; Consume[]; END; ENDLOOP; END--FreeRun--; maxTransferRun: Disk.PageCount _ 200; Transfer: PROC[file: Handle, data: LONG POINTER, filePage: File.PageNumber, nPages: File.PageCount, action: {write, read}, where: {header, data} ] = BEGIN label: Disk.Label _ IF where = header THEN HeaderLabel[file.fp] ELSE DataLabel[file.fp]; label.filePage _ filePage; WHILE nPages > 0 DO status: Disk.Status; countDone: Disk.PageCount; channel: Disk.Channel; req: Disk.Request; req.data _ data; req.incrementDataPtr _ TRUE; req.command _ IF action = read THEN [header: verify, label: verify, data: read] ELSE [header: verify, label: verify, data: write]; TRUSTED{ [diskPage: req.diskPage, size: req.count, channel: channel] _ FileInternal.FindRun[ -- NB: first call of FindRun checks entire transfer is within file start: IF where = header THEN [-file.logicalRunTable.headerPages+filePage] ELSE filePage, nPages: nPages, runTable: file.runTable] }; IF req.count > maxTransferRun THEN req.count _ maxTransferRun; TRUSTED{[status, countDone] _ DoPinnedIO[channel, @label, @req]}; CheckStatus[status]; TRUSTED{data _ data + countDone * DiskFace.wordsPerPage}; nPages _ nPages - countDone; filePage _ [filePage + countDone]; ENDLOOP; END; WriteRunTable: PROC[file: Handle] = TRUSTED BEGIN Transfer[file: file, data: file.headerVM, filePage: [0], nPages: file.logicalRunTable.headerPages - propFilePages, action: write, where: header] END; UnstableRunTable: PROC[file: Handle, newSize: File.PageCount] = TRUSTED BEGIN file.logicalRunTable.intention _ [unstable: TRUE, size: newSize]; Transfer[file: file, data: file.headerVM, filePage: [0], nPages: 1, action: write, where: header]; END; StableRunTable: PROC[file: Handle] = TRUSTED BEGIN file.logicalRunTable.intention _ [unstable: FALSE]; Transfer[file: file, data: file.headerVM, filePage: [0], nPages: 1, action: write, where: header]; END; MakeBootable: PROC[file: Handle, firstPage: File.PageNumber] RETURNS[id: RelID, firstLink: DiskFace.DontCare, channel: Disk.Channel] = BEGIN eof: DiskFace.DontCare = LOOPHOLE[LONG[-1]]; -- bit-pattern known by microcode data: LONG POINTER; label: Disk.Label _ DataLabel[file.fp]; req: Disk.Request; filePage: File.PageNumber _ firstPage; thisDiskPage: Disk.PageNumber; WriteLink: PROC[link: DiskFace.DontCare] = BEGIN status: Disk.Status; countDone: Disk.PageCount; label.filePage _ filePage-1; req _ [ diskPage: thisDiskPage, data: data, incrementDataPtr: TRUE, command: [header: verify, label: verify, data: read], count: 1 ]; TRUSTED{[status, countDone] _ DoPinnedIO[channel, @label, @req]}; -- get data CheckStatus[status]; label.dontCare _ link; label.filePage _ filePage-1; -- previous transfer incremented it TRUSTED{[status, countDone] _ WriteLabels[channel, thisDiskPage, 1, data, @label]}; CheckStatus[status]; END; thisSize: Disk.PageCount; IF file.size <= firstPage THEN ERROR File.Error[unknownPage]; [diskPage: thisDiskPage, size: thisSize, channel: channel] _ FileInternal.FindRun[start: filePage, nPages: file.size-filePage, runTable: file.runTable]; firstLink _ Disk.GetBootChainLink[channel, thisDiskPage]; id _ RelID[file.fp]; data _ GetScratchPage[]; DO BEGIN ENABLE UNWIND => FreeScratchPage[data]; nextDiskPage: Disk.PageNumber; nextSize: Disk.PageCount; nextChannel: Disk.Channel; filePage _ [filePage+thisSize]; -- file page number of start of next run thisDiskPage _ [thisDiskPage + thisSize - 1]; -- disk page number of last page of this run IF filePage >= file.size THEN EXIT; [diskPage: nextDiskPage, size: nextSize, channel: nextChannel] _ FileInternal.FindRun[start: filePage, nPages: file.size-filePage, runTable: file.runTable]; IF NOT Disk.SameDrive[nextChannel, channel] THEN ERROR File.Error[mixedDevices]; WriteLink[ Disk.GetBootChainLink[channel, nextDiskPage] ]; thisDiskPage _ nextDiskPage; thisSize _ nextSize; END ENDLOOP; WriteLink[eof]; FreeScratchPage[data]; END; --FileStats.--notReallyFree: INT _ 0; Reporter: TYPE = PROC[File.FP, File.PropertyStorage]; Extend: PROC[file: Handle, delta, min: INT, report: Reporter] = BEGIN volume: File.Volume = file.volume; amount: INT _ delta; nearTo: VolumeFormat.LogicalPage _ -- hint for FileInternal.Alloc -- IF file.runTable.nRuns = 0 THEN [0] ELSE FileInternal.LastLogicalPage[file]; headerVMPos: LONG POINTER _ file.headerVM; -- headerVM to be written to disk (creating) freeLabel: Disk.Label _ FreeLabel[volume]; label: Disk.Label; IF file.size >= 0 THEN BEGIN label _ DataLabel[file.fp]; label.filePage _ file.size; END; -- Otherwise, wait until we know the FP! TRUSTED{ file.logicalRunTable.intention _ [unstable: TRUE, size: file.size] }; WHILE amount > 0 -- Loop for each allocated disk run -- DO logicalRun: VolumeFormat.LogicalRun _ FileInternal.Alloc[volume: volume, first: nearTo, size: amount, min: min ]; nearTo _ [logicalRun.first + logicalRun.size]; -- hint for next call of Alloc -- WHILE logicalRun.size > 0 -- Loop for each available fragment of disk run -- DO labelsOK: Disk.PageCount; -- count of labels that are genuinely free pages -- status: Disk.Status; run: PhysicalRun; [run.channel, run.diskPage] _ FileInternal.TranslateLogicalRun[logicalRun, volume]; run.filePage _ file.size; freeLabel.filePage _ logicalRun.first; TRUSTED{ [status, labelsOK] _ VerifyLabels[run.channel, run.diskPage, logicalRun.size, @freeLabel] }; IF status # Disk.ok THEN notReallyFree _ notReallyFree+1; -- statistics IF labelsOK > 0 THEN BEGIN labelsWritten: Disk.PageCount _ 0; -- total labels actually written -- labelsThisTime: Disk.PageCount _ 0; -- labels written in this transfer -- Consume: PROC = BEGIN file.size _ file.size + labelsThisTime; labelsWritten _ labelsWritten + labelsThisTime; amount _ amount - labelsThisTime; logicalRun.first _ [logicalRun.first+labelsThisTime]; logicalRun.size _ logicalRun.size - labelsThisTime; END; firstHeaderPage: BOOL = file.runTable.nRuns = 0; TRUSTED{FileInternal.AddRun[file, @run, logicalRun.first, labelsOK]}; IF firstHeaderPage THEN BEGIN IF file.size >= 0 THEN ERROR File.Error[inconsistent]; file.fp _ [id: FileInternal.NewID[volume], da: logicalRun.first]; label _ HeaderLabel[file.fp]; IF report # NIL THEN report[file.fp, file.properties]; END ELSE BEGIN WriteRunTable[file]; END; IF file.size < 0 THEN BEGIN -- write labels and data for header area -- TRUSTED{[status, labelsThisTime] _ WriteLabels[run.channel, run.diskPage, MIN[-file.size, labelsOK], headerVMPos, @label]}; Consume[]; IF file.size < 0 -- still writing header pages, even after the ones we just wrote THEN TRUSTED{ headerVMPos _ headerVMPos + labelsThisTime * DiskFace.wordsPerPage} ELSE BEGIN label _ DataLabel[file.fp]; label.filePage _ file.size; END; END; IF labelsOK > labelsWritten AND file.size >= 0 -- i.e. if there's still pages free and we've finished the header THEN BEGIN TRUSTED{[status, labelsThisTime] _ WriteLabels[run.channel, [run.diskPage+labelsThisTime], labelsOK-labelsWritten, NIL, @label]}; Consume[]; END; -- correct run-table to number of pages successfully written -- IF labelsOK > labelsWritten THEN FileInternal.RemoveFromRunTable[file, labelsOK-labelsWritten]; END; SELECT status FROM Disk.ok => NULL; Disk.invalid => ERROR File.Error[wentOffline]; ENDCASE => -- skip bad/in-use page -- { logicalRun.first _ [logicalRun.first+1]; logicalRun.size _ logicalRun.size-1 }; ENDLOOP-- Loop for each available fragment of disk run --; ENDLOOP-- Loop for each allocated disk run --; StableRunTable[file] END--Extend--; Contract: PROC[file: Handle, delete: BOOL, newSize: File.PageCount-- -1 if delete--, recovery: BOOL] = TRUSTED BEGIN logical: LONG POINTER TO VolumeFormat.LogicalRunObject = file.logicalRunTable; IF NOT recovery THEN UnstableRunTable[file, newSize]; WHILE delete OR file.size > newSize DO IF file.runTable.nRuns=0 THEN { IF delete THEN EXIT ELSE ERROR File.Error[inconsistent] }; BEGIN label: Disk.Label; labelPtr: POINTER TO Disk.Label _ NIL; lastRun: VolumeFormat.LogicalRun _ logical[file.runTable.nRuns-1]; thisTime: VolumeFormat.RunPageCount = IF delete THEN IF recovery AND file.size > 0 THEN --restrict to data pages for label-check--MIN[file.size, lastRun.size] ELSE --run is entirely data or entirely header--lastRun.size ELSE MIN[file.size-newSize, lastRun.size]; file.size _ file.size - thisTime; IF recovery THEN BEGIN IF file.size + thisTime > 0 THEN BEGIN label _ DataLabel[file.fp]; label.filePage _ file.size; END ELSE BEGIN label _ HeaderLabel[file.fp]; label.filePage _ logical.headerPages + file.size; END; labelPtr _ @label; END ELSE labelPtr _ NIL; FileInternal.RemoveFromRunTable[file, thisTime]; FreeRun[ [first: [lastRun.first + lastRun.size-thisTime], size: thisTime], file.volume, labelPtr]; END; ENDLOOP; IF NOT delete THEN { logical.intention _ [unstable: FALSE]; WriteRunTable[file] }; END--Contract--; --FileStats.--recoveries: INT _ 0; DoOpen: PROC[file: Handle] = TRUSTED BEGIN volume: File.Volume = file.volume; FileInternal.GetHeaderVM[file, initRuns]; BEGIN -- First try to transfer entire header in a single request (an optimisation!) initTryPages: INT = file.logicalRunTable.headerPages; logicalRun: VolumeFormat.LogicalRun = [first: file.fp.da, size: initTryPages]; channel: Disk.Channel; diskPage: Disk.PageNumber; label: Disk.Label; req: Disk.Request; status: Disk.Status; countDone: Disk.PageCount; [channel, diskPage] _ FileInternal.TranslateLogicalRun[logicalRun, volume]; label _ HeaderLabel[file.fp]; req _ [ diskPage: diskPage, data: file.headerVM, incrementDataPtr: TRUE, command: [header: verify, label: verify, data: read], count: initTryPages ]; TRUSTED{[status, countDone] _ DoPinnedIO[channel, @label, @req]}; IF countDone = 0 THEN BEGIN IF status = Disk.labelCheck THEN ERROR File.Error[unknownFile] ELSE CheckStatus[status]; END; IF file.logicalRunTable.headerPages # initTryPages -- multi-page run-tables! THEN ERROR File.Error[inconsistent]; file.size _ FileInternal.TranslateLogicalRunTable[file]; -- assumes single-page run-table IF countDone # file.logicalRunTable.headerPages THEN -- read in the remaining header pages (e.g. property page) Transfer[file: file, data: file.headerVM + countDone*DiskFace.wordsPerPage, filePage: [countDone], nPages: file.logicalRunTable.headerPages - countDone, action: read, where: header]; END; IF file.logicalRunTable.intention.unstable THEN BEGIN delete: BOOL = file.logicalRunTable.intention.size < 0; recoveries _ recoveries+1; Contract[file: file, delete: delete, newSize: file.logicalRunTable.intention.size, recovery: TRUE]; IF delete THEN { file.state _ deleted; ERROR File.Error[unknownFile] }; END; file.state _ opened; END; nowHaveBackingFile: BOOL _ FALSE; HaveBackingFile: ENTRY PROC RETURNS[did: BOOL] = BEGIN ENABLE UNWIND => NULL; did _ nowHaveBackingFile; nowHaveBackingFile _ TRUE; END; --FileInternal.--RegisterVMFile: PUBLIC PROC[file: Handle] = BEGIN Acquire[file, shared]; BEGIN ENABLE File.Error => Unlock[file]; label: Disk.Label _ DataLabel[file.fp]; IF NOT HaveBackingFile[] THEN TRUSTED{ VMBacking.AttachBackingStorage[label, 0, file.runTable] }; END; Unlock[file]; END; unlocked: CONDITION; Acquire: PROC[file: Handle, mode: FileInternal.LockMode] = BEGIN Lock[file, mode]; IF file.state = none THEN BEGIN ENABLE File.Error => Unlock[file]; startPulse: FileStats.Pulses = GetPulses[]; DoOpen[file]; Incr[open, file.size, startPulse]; END; END; --FileInternal.--Lock: PUBLIC ENTRY PROC[file: Handle, mode: FileInternal.LockMode] = BEGIN ENABLE UNWIND => NULL; IF file = NIL THEN RETURN WITH ERROR File.Error[unknownFile]; DO IF file.state = deleted THEN RETURN WITH ERROR File.Error[unknownFile]; IF mode = shared AND file.state # none THEN { IF file.users >= 0 --no writers-- THEN { file.users _ file.users + 1; EXIT } } ELSE { IF file.users = 0 --nobody-- THEN { file.users _ -1; EXIT } }; WAIT unlocked; ENDLOOP; END; --FileInternal.--Unlock: PUBLIC ENTRY PROC[file: Handle] = BEGIN SELECT file.users FROM < 0 => file.users _ file.users + 1; > 0 => file.users _ file.users - 1; ENDCASE => NULL; BROADCAST unlocked; END; --File.--NextFile: PUBLIC PROC[volume: File.Volume, prev: File.FP] RETURNS[next: File.FP _ File.nullFP] = TRUSTED BEGIN finishedStatus: Disk.Status _ Disk.ok; Work: FileInternal.EnumeratePagesProc = TRUSTED BEGIN attr: Attributes = label.attributes; rel: RelID = label.fileID.relID; SELECT TRUE FROM status # Disk.ok => { exit _ TRUE; finishedStatus _ status }; attr = header AND label.filePage = 0 => { exit _ TRUE; next _ rel }; ENDCASE => NULL; END; EnumeratePages[volume, prev.da, Work]; CheckStatus[finishedStatus]; END; --FileInternal.--EnumeratePages: PUBLIC PROC[ volume: File.Volume, start: VolumeFormat.LogicalPage, work: FileInternal.EnumeratePagesProc] = TRUSTED BEGIN size: INT = File.GetVolumePages[volume].size; current: VolumeFormat.LogicalPage _ start; done: VolumeFormat.LogicalPage _ current; finished: BOOL _ FALSE; changed: CONDITION; Next: ENTRY PROC RETURNS[VolumeFormat.LogicalPage] = CHECKED INLINE { RETURN[current _ [current+1]] }; Scan: PROC = TRUSTED BEGIN exit: BOOL _ FALSE; UNTIL exit DO this: VolumeFormat.LogicalPage = Next[]; WaitTurn: ENTRY PROC = CHECKED INLINE { UNTIL done = this-1 DO WAIT changed[! UNWIND => NULL] ENDLOOP }; CompletedTurn: ENTRY PROC = CHECKED INLINE { done _ this; BROADCAST changed }; channel: Disk.Channel; diskPage: Disk.PageNumber; label: Disk.Label; req: Disk.Request; status: Disk.Status; IF this >= size THEN EXIT; [channel, diskPage] _ FileInternal.TranslateLogicalRun[[first: this, size: 1], volume]; req _ [ diskPage: diskPage, data: scratchReader, command: [header: verify, label: read, data: read], count: 1 ]; status _ Disk.DoIO[channel, @label, @req].status; IF status = badData THEN status _ Disk.ok; -- don't care about the data, only the label! WaitTurn[]; IF NOT finished THEN finished _ work[status, this, @label ! UNWIND => CompletedTurn[]]; exit _ finished; CompletedTurn[]; ENDLOOP; END; scratchInterval: VM.Interval = [ page: VM.PageNumberForAddress[scratchReader], count: VM.PagesForWords[DiskFace.wordsPerPage] ]; VM.SwapIn[interval: scratchInterval, kill: TRUE, pin: TRUE]; BEGIN ENABLE UNWIND => VM.Unpin[scratchInterval]; otherGuy: PROCESS = FORK Scan[]; Scan[]; JOIN otherGuy; END; VM.Unpin[scratchInterval]; END; -- ******** Statistics ******** -- statistics: REF ARRAY FileStats.Type OF FileStats.Data _ NEW[ARRAY FileStats.Type OF FileStats.Data _ ALL[]]; hardExtends: FileStats.Type = spare0; GetPulses: PROC RETURNS[FileStats.Pulses] = TRUSTED INLINE { RETURN[ ProcessorFace.GetClockPulses[] ] }; Incr: PUBLIC ENTRY PROC[type: FileStats.Type, pages: INT, startPulse: FileStats.Pulses] = BEGIN old: FileStats.Data = statistics[type]; statistics[type] _ [calls: old.calls+1, pages: old.pages+pages, pulses: old.pulses + (GetPulses[]-startPulse)]; END; --FileStats.--GetData: PUBLIC ENTRY PROC[type: FileStats.Type] RETURNS[FileStats.Data] = { RETURN[statistics[type]] }; --FileStats.--ClearData: PUBLIC ENTRY PROC[type: FileStats.Type] = { statistics[type] _ [] }; -- ******** Top-level procedures ******** -- --File.--Create: PUBLIC PROC[volume: File.Volume, size: File.PageCount, report: Reporter _ NIL] RETURNS[file: Handle] = BEGIN startPulse: FileStats.Pulses = GetPulses[]; file _ FileInternal.AllocForCreate[]; -- gives us a handle not yet in FileTable BEGIN ENABLE UNWIND => FileInternal.DontInsert[]; file.volume _ volume; GetHeaderVM[file, initRuns]; file.size _ TranslateLogicalRunTable[file]; InnerSetSize[file: file, size: size, create: TRUE, report: report ! UNWIND => FileInternal.FreeHeaderVM[file]--Delete[File]???--]; file.state _ opened; END; FileInternal.Insert[file]; Incr[create, size, startPulse]; END; --File.--Open: PUBLIC PROC[volume: File.Volume, fp: File.FP] RETURNS[file: Handle] = BEGIN IF fp = File.nullFP THEN ERROR File.Error[unknownFile]; file _ FileInternal.Lookup[volume, fp]; Acquire[file, shared]; Unlock[file]; END; --File.--Delete: PUBLIC PROC[file: Handle] = BEGIN Acquire[file, exclusive]; BEGIN ENABLE File.Error => Unlock[file]; startPulse: FileStats.Pulses = GetPulses[]; delta: INT = file.size; Contract[file, TRUE, -1, FALSE]; file.state _ deleted; Incr[delete, delta, startPulse]; END; Unlock[file]; END; --File.--SetSize: PUBLIC PROC[file: Handle, size: File.PageCount] = { InnerSetSize[file: file, size: size, create: FALSE, report: NIL] }; minFactor: INT _ 10; -- => initial minimal runs are 1/10th of size change InnerSetSize: PROC[file: Handle, size: File.PageCount, create: BOOL, report: Reporter ] = BEGIN minRun: INT _ size / minFactor; -- smallest run we will accept; decreased if necessary DO IF create THEN Lock[file, exclusive] ELSE Acquire[file, exclusive]; BEGIN startPulse: FileStats.Pulses = GetPulses[]; delta: INT = size-file.size; SELECT TRUE FROM delta > 0 => BEGIN ENABLE File.Error => BEGIN lack: INT = size-file.size; -- calculate it while we still have the file locked IF why = volumeFull THEN BEGIN Unlock[file]; IF FileInternal.Flush[file.volume, lack] THEN LOOP; IF File.GetVolumePages[file.volume].free >= lack THEN { Incr[hardExtends, delta, startPulse]; minRun _ minRun / 2; LOOP }; END ELSE BEGIN IF NOT create -- core run-table # disk, so close the file; let DoOpen recover. THEN { file.state _ none; FileInternal.FreeHeaderVM[file] }; Unlock[file]; END; END; Extend[file, delta, minRun, report]; Incr[extend, delta, startPulse]; END; delta < 0 => BEGIN ENABLE File.Error => { file.state _ none; FileInternal.FreeHeaderVM[file]; Unlock[file] }; Contract[file, FALSE, size, FALSE]; Incr[contract, -delta, startPulse]; END; ENDCASE => NULL; END; Unlock[file]; EXIT ENDLOOP; END; --File.--Info: PUBLIC PROC[file: Handle] RETURNS[volume: File.Volume, fp: File.FP, size: File.PageCount] = BEGIN Acquire[file, shared]; BEGIN ENABLE File.Error => Unlock[file]; volume _ file.volume; fp _ file.fp; size _ file.size; END; Unlock[file]; END; --File.--SetRoot: PUBLIC PROC[root: File.VolumeFile, file: Handle, page: File.PageNumber _ [0]] = TRUSTED BEGIN Acquire[file, shared]; BEGIN ENABLE File.Error => Unlock[file]; id: RelID; firstLink: DiskFace.DontCare; channel: Disk.Channel; [id, firstLink, channel] _ MakeBootable[file, page]; FileInternal.RecordRootFile[file.volume, root, file.fp, page, id, firstLink, channel]; END; Unlock[file]; BEGIN ENABLE File.Error => CONTINUE; IF root = VM AND File.SystemVolume[] # NIL AND File.IsDebugger[File.SystemVolume[]] = File.IsDebugger[file.volume] THEN RegisterVMFile[file]; END; END; --FileInternal.--GetFileLocation: PUBLIC PROC[file: Handle, firstPage: File.PageNumber] RETURNS[location: BootFile.Location] = TRUSTED BEGIN Acquire[file, shared]; BEGIN ENABLE File.Error => Unlock[file]; id: RelID; firstLink: DiskFace.DontCare; channel: Disk.Channel; [id, firstLink, channel] _ MakeBootable[file, firstPage]; location.diskFileID _ [fID: [rel[id]], firstPage: firstPage, firstLink: firstLink]; [type: location.deviceType, ordinal: location.deviceOrdinal] _ Disk.DriveAttributes[channel]; END; Unlock[file]; END; --File.--Read: PUBLIC UNSAFE PROC[file: Handle, from: File.PageNumber, nPages: File.PageCount, to: LONG POINTER] = BEGIN IF from < 0 THEN ERROR File.Error[unknownPage]; Acquire[file, shared]; BEGIN ENABLE File.Error => Unlock[file]; startPulse: FileStats.Pulses = GetPulses[]; Transfer[file: file, data: to, filePage: from, nPages: nPages, action: read, where: data]; Incr[read, nPages, startPulse]; END; Unlock[file]; END; --File.--Write: PUBLIC PROC[file: Handle, to: File.PageNumber, nPages: File.PageCount, from: LONG POINTER] = BEGIN IF to < 0 THEN ERROR File.Error[unknownPage]; Acquire[file, shared]; BEGIN ENABLE File.Error => Unlock[file]; startPulse: FileStats.Pulses = GetPulses[]; Transfer[file: file, data: from, filePage: to, nPages: nPages, action: write, where: data]; Incr[write, nPages, startPulse]; END; Unlock[file]; END; --File.--GetProperties: PUBLIC PROC[file: Handle] RETURNS[p: File.PropertyStorage] = BEGIN Acquire[file, shared]; BEGIN ENABLE File.Error => Unlock[file]; p _ file.properties; END; Unlock[file]; END; --File.--WriteProperties: PUBLIC PROC[file: Handle] = TRUSTED BEGIN Acquire[file, shared]; BEGIN ENABLE File.Error => Unlock[file]; Transfer[file: file, data: file.properties, filePage: [file.logicalRunTable.headerPages-propFilePages], nPages: propFilePages, action: write, where: header] END; Unlock[file]; END; END. FCedar Nucleus (Files): per-file operations, locking file header data structures FileImpl.mesa Andrew Birrell December 8, 1983 9:51 am Last Edited by: Levin, August 8, 1983 5:57 pm Last Edited by: Schroeder, June 10, 1983 5:20 pm Either extend last run, or add new run file.runTable^ _ physical^ . . . . but the compiler can't copy sequences Assumes file.runTable.nRuns > 0 Write page labels as "free" and mark as unused in VAM. Iff verifyLabel # NIL, do so only to pages whose labels verify correctly. We've freed everything up to but excluding a label mis-match. Now, look for free pages which should be notified to the VAM in case the VAM thinks they're in use. Largest number of disk pages to transfer in single request. This much VM will be pinned during the transfer. Variable, not constant, to allow patching. Write boot-chain link at "thisDiskPage" for current "filePage" At top of loop, thisDiskPage, thisSize and filePage correspond to the start of a run Here, thisDiskPage, thisSize and filePage correspond to the last page of the file NOTE: on entry for creation, file.size = -(number of header pages) and file.fp = nullFP. Record the safe length of the file in the disk run table Ensuing transfer will write the run table to disk Ensure disk run-table is superset of allocated pages Write new length and "unstable" in disk run table in case of crash Called instead of Lock to re-open file if it was invalidated by a checkpoint File is in table but has not yet been opened. We try it, under the file's exclusive lock Wrinkle: if file.state=none, we need exclusive access, so we can call DoOpen -- ******** Scanning for files (File.NextFile) ******** -- We know that the disk run-table matches the in-core one (???). Otherwise, let the error propagate to our client core run-table # disk, so close the file. DoOpen will try to recover. Ê$s˜JšœP™PJšœ ™ Jšœ(™(J™-J™0J™šÏk ˜ Jšœ œ ˜Jšœœˆ˜’Jšœ œ˜)Jšœœ œyœ7˜ÇJ˜ Jšœ œ˜&Jšœ ˜ Jšœœ˜&Jšœ œf˜xJšœœ‹˜“Jšœ œQ˜`—J˜šœ œ˜Jšœ9œ ˜MJšœ Ïcœ˜IJšœ˜ —J˜š˜J˜—J˜Jšž8˜8˜Jšž œ œœ˜?J˜Jšž œœœ˜5J˜Jšž œœœ˜5J˜Jšž œœœ˜4J˜Jšœœœ˜J˜Jšž œœœ˜3J˜Jšœ œ˜'J˜Jšœœ˜0J˜Jšœœ˜.J˜Jšœ œ˜-J˜Jšœ(œœž˜OJ˜šœ œœ%˜UJšœ˜—J˜š Ïn œœœœœœ˜cJšœ4˜BJš˜šœ œ ˜Jšœœ ˜(šœœ˜Jšœœœ œ˜I——Jšœ>œ˜FJšœ6œœ˜UJšœ˜Jšœ˜—J˜Jšž œœœœ˜6J˜Jšœ2ž(˜ZJ˜šŸ œœ˜(Jš˜Jšœ œ(˜4Jšœ œœ˜'Jšœ˜J˜J˜—J˜J˜—Jšž7˜7˜JšœG˜GJ˜š žŸ œœœœ˜QJš˜Jšœœœ˜$Jšœœ˜+Jšœ6˜6Jšœ œ6˜QJšœœœ&˜?Jšœ œœ œ#˜VJšœF˜FJšœœœ˜CJšœž)˜GJšœœ8˜RJšœœ˜/JšœE˜EJ˜$Jšœ ˜šœœž˜ Jšœ<˜˜Bšœœž˜Jšœ#˜%šœœžN˜YJšœ˜Jšœ˜—Jšœ˜Jšœ&˜&Jšœ!˜!Jšœ˜Jšœ!˜#šœœž&˜1Jšœœ$˜7JšœH™HJšœ/˜/Jšœ%˜%Jš œœœœ œ˜RJšœ˜—Jšœ/˜/Jšœ:˜:Jšœ˜—Jšœ˜—J˜š žŸœœœœ˜hJš˜Jšœ™JšœO˜OJšœ$˜*Jšœ˜—J˜š žŸœœœœ˜UJš˜Jšœ œœœ6˜NJšœ#˜#Jšœ ˜ Jšœ ˜šœœœœ˜=Jš˜JšœD˜DJšœ$œ˜9Jšœ2˜2Jšœ˜šœ˜ Jšž.˜.Jšœ$˜$Jšœ,˜,Jšœ<˜Jšœ:˜AJ˜Jšœ2˜9J˜J˜"—Jšœ˜Jšœ˜—J˜šŸ œœ˜+Jš˜˜8JšœW˜W—Jšœ˜—J˜šŸœœ*˜GJš˜Jšœ,œ˜AJšœb˜bJšœ˜—J˜šŸœœ˜,Jš˜Jšœ,œ˜3Jšœb˜bJšœ˜—J˜šŸ œœ+œB˜†Jš˜Jšœœœž!˜NJšœœœ˜Jšœ'˜'J˜Jšœ&˜&J˜šŸ œœ˜*Jšœ>™>Jš˜Jšœ˜Jšœ˜J˜˜J˜J˜ Jšœœ˜J˜5J˜ —Jšœ;ž ˜MJšœ˜J˜Jšœž#˜@JšœL˜SJšœ˜Jšœ˜—J˜Jšœœœ˜=šœ<˜˜EJšœ˜šœ˜ Jšœœœ˜6JšœA˜AJ˜Jšœ œœ"˜6Jšœ1™1Jš˜—šœ˜ Jšœ4™4Jšœ˜Jšœ˜—Jšœ˜šœ˜ Jšž+˜+šœ4˜;Jšœœ.˜?—J˜ Jšœž@˜Qšœœ˜ JšœC˜C—šœ˜ J˜J˜Jšœ˜—Jšœ˜—Jšœ˜JšœžA˜Tšœ˜ šœ4˜;Jšœ7œ ˜E—J˜ Jšœ˜—Jšž?˜?Jšœ˜Jšœ?˜CJšœ˜—šœ˜Jšœ œ˜Jšœœ˜.—šœž˜%J˜Q——Jšž2œ˜:—Jšž&œ˜.J˜Jšž œ˜—J˜š Ÿœœœžœ œ˜nJš˜Jšœ œœœ6˜NJšœB™BJšœœ œ!˜5Jšœœ˜#šœœ˜Jš œœœœœœ˜AJš˜Jšœ˜Jšœ œœœ˜&JšœB˜Bšœ%˜%Jšœ˜ šœœ œ˜"Jšœž*œ˜KJšœž+œ ˜<—Jšœœ"˜*—Jšœ!˜!Jšœ ˜ šœ˜ Jšœ˜šœ˜ Jšœ˜Jšœ˜Jš˜—šœ˜ Jšœ˜Jšœ1˜1Jšœ˜—Jšœ˜Jš˜—Jšœ œ˜J˜0šœ˜JšœY˜Y—Jšœ˜—Jšœ˜Jšœœ˜ Jšœ"œ˜DJšž œ˜—J˜Jšžœ œ˜"J˜šŸœœ˜$Jš˜Jšœ"˜"J˜)šœžM˜SJšœœ$˜5JšœN˜NJ˜J˜Jšœ˜J˜J˜J˜J˜KJšœ˜˜J˜J˜Jšœœ˜J˜5Jšœ˜—Jšœ:˜AJšœ˜šœ˜ Jšœ˜Jšœœ˜"Jšœ˜Jšœ˜—Jšœ1ž˜LJšœœ˜$Jšœ9ž ˜YJšœ-˜/šœž:˜?šœK˜KJšœ˜Jšœ5˜5Jšœ˜———Jšœ˜Jšœ(˜*šœ˜ Jšœœ+˜7Jšœ˜šœ˜Jšœ˜Jšœ-˜-Jšœ œ˜—Jšœœœ˜GJšœ˜—Jšœ˜Jšœ˜—J˜Jšœœœ˜!J˜š Ÿœœœœœ˜0Jš˜Jšœœœ˜Jšœ/œ˜4Jšœ˜—J˜šžŸœœœ˜—J˜—Jšœ˜J˜J˜Jšœ˜—J˜š ž Ÿœœœœœ˜TJš˜Jšœœœ˜7Jšœ'˜'J˜J˜ Jšœ˜—J˜šž Ÿœœœ˜,Jš˜J˜š˜Jšœ˜"J˜+Jšœœ ˜Jšœœœ˜ J˜Jšœ ˜ —Jšœ˜J˜ Jšœ˜—J˜šž Ÿœœœ&˜CJšœ/œ œ˜E—J˜Jšœ œž4˜IJ˜šŸ œœ-œ˜YJš˜Jšœœž6˜Všœœœœ˜Fš˜J˜+Jšœœ˜šœœ˜šœ ˜ Jš˜šœ˜Jš˜Jšœœž3˜OJšœ˜šœ˜ Jšœ>™>Jšœ ˜ Jšœ'œœ˜3Jšœ.˜0Jšœ>œ˜IJš˜—šœ˜ Jšœœž@˜NJšœ8˜˜>Jšœ˜——Jšœ˜J˜ Jšœ˜—J˜š ž Ÿœœœœ˜/J˜J˜Jšœœœ˜Jš˜Jšœ œœ˜/J˜š˜Jšœ˜"J˜+J˜ZJ˜—Jšœ˜J˜ Jšœ˜—J˜šž Ÿœœœ˜)J˜J˜Jšœœœ˜Jš˜Jšœœœ˜-J˜š˜Jšœ˜"J˜+J˜[J˜ —Jšœ˜J˜ Jšœ˜—J˜š ž Ÿ œœœœ˜TJš˜J˜š˜Jšœ˜"Jšœ˜—Jšœ˜J˜ Jšœ˜—J˜šž Ÿœœœ˜=Jš˜J˜š˜Jšœ˜"šœ+˜+Jšœ;˜;J˜4——Jšœ˜J˜ Jšœ˜—J˜—Jšœ˜J˜J˜—…—Œ­E