-- File: CedarSnapshotMain.mesa
-- last edited by Levin:   3-Dec-81 11:21:40

DIRECTORY
  BcdDefs USING [GFTIndex, MTIndex],
  BcdOps USING [BcdBase, MTHandle, ProcessModules],
  Boot USING [BootFileType, Location, LVBootFiles],
  BootFile USING [MemorySizeToFileSize],
  BootSwap USING [Mon, OutLoad],
  BootSwapCross USING [pMon],
  CachedSpace USING [Desc, Handle, Level],
  CedarSnapshot USING [Outcome],
  DeviceCleanup USING [Perform],
  -- DiskChannel USING [Idle, Restart],
  Directory USING [CreateFile],  -- used only to find GF of DirectoryFilesImpl
  DirectoryFiles USING [DCEntry, --directoryCache,-- maxDCache],
  DirectoryFilesImpl USING [directoryCache],
  DiskChannelImpl USING [ChannelHandle, ChannelObject],
  Environment USING [Base, first64K, wordsPerPage],
  File USING [
    Capability, Create, Delete, delete, GetAttributes, GetSize, ID,
    LimitPermissions, nullID, PageCount, PageNumber, Permissions,
    read, Unknown, write],
  FileTypes USING [tUntypedFile],
  Frame USING [MyGlobalFrame],
  -- Hierarchy USING [GetDescriptor],
  HierarchyImpl USING [GetDescriptor],
  Inline USING [BITOR, BITXOR],
  KernelFile USING [GetBootLocation],
  -- LogicalVolume USING [CloseLogicalVolume, OpenLogicalVolume],
  MapLogImpl USING [WriteLog1],  -- should be VMMapLog.WriteLog
  PilotFileTypes USING [tVMBackingFile],
  -- PilotLoaderOps USING [
  --   CloseLinkSpace, LinkSegmentLength, OpenLinkSpace, ReadLink, WriteLink],
  PilotLoaderSupport USING [
    CloseLinkSpace, LinkSegmentLength, OpenLinkSpace, ReadLink, WriteLink],
  PilotLoadStateOps USING [
    AcquireBcd, ConfigIndex, EnumerateBcds, GetMap, GetModule, InputLoadState,
    Map, ReleaseBcd, ReleaseLoadState, ReleaseMap],
  PilotMP USING [cClient],
  PrincOps USING [ControlLink, GFTIndex, GlobalFrameHandle],
  PrincOpsRuntime USING [GetFrame, GFT],
  Process USING [
    GetPriority, Pause, Priority, SecondsToTicks, SetPriority --, Yield--],
  ProcessInternal USING [DisableInterrupts, EnableInterrupts],
  ProcessOperations USING [EnableAndRequeue, ReadPSB, ReadPTC, ReadWDC, WritePSB, WritePTC, WriteWDC],
  ProcessorFace USING [SetMP],
  ProcessPriorities USING [priorityPageFaultLow],
  PSB USING [PDA, PsbHandle],
  Runtime USING [GlobalFrame, LoadConfig -- for PilotLoaderOps hack only --],
  Snapshot USING [InLoad],
  Space USING [
    CopyIn, CopyOut, Create, Deactivate, defaultWindow, Delete, GetAttributes,
    GetHandle, GetWindow, Handle, LongPointer, MakeReadOnly, MakeWritable,
    Map, nullHandle, PageCount, PageFromLongPointer, PageOffset, Unmap,
    virtualMemory, WindowOrigin],
  SpecialFile USING [Link, MakeBootable],
  SpecialSpace USING [
    MakeGlobalFrameResident, MakeGlobalFrameSwappable, MakeProcedureResident,
    MakeProcedureSwappable, MakeResident, MakeSwappable, realMemorySize],
  SpecialVolume USING [GetLogicalVolumeBootFiles, SetLogicalVolumeBootFiles],
  SubVolumeImpl USING [CacheEntry, ceFirst, ceLast, CePtr],
  TemporarySetGMT USING [SetGMT],
  UserTerminal USING [CursorArray, GetCursorPattern, SetCursorPattern],
  VM USING [PageNumber],
  -- VMMapLog USING [WriteLog1],
  Volume USING [ID, InsufficientSpace, nullID, systemID],
  VolumeExtras USING [OpenVolume],
  -- VolumeImplInterface USING [SubvolumeOffline, SubvolumeOnline],
  VolumeImpl USING [CloseLogicalVolume, OpenLogicalVolume, SubvolumeOffline, SubvolumeOnline];

CedarSnapshotMain: MONITOR
  IMPORTS
    BcdOps, BootFile, BootSwap, DeviceCleanup, Directory, -- DiskChannel, -- File, Frame,
    -- Hierarchy, -- Inline, KernelFile, -- LogicalVolume, PilotLoaderOps, --
    PilotLoadStateOps, PrincOpsRuntime, Process, ProcessInternal,
    ProcessOperations, ProcessorFace, Runtime, Snapshot, Space, SpecialFile, SpecialSpace,
    SpecialVolume, TemporarySetGMT, UserTerminal, -- VMMapLog, -- Volume, VolumeExtras
  EXPORTS CedarSnapshot, BootSwap -- hack!!
  SHARES BootSwap, BootSwapCross, DiskChannelImpl, File, SubVolumeImpl =

BEGIN


-- Miscellaneous Constants --

read: File.Permissions = File.read;
write: File.Permissions = File.write;
delete: File.Permissions = File.delete;

clientImage: Boot.BootFileType = hardMicrocode;      -- as good as any...
firstOutloadPage: CARDINAL = 1;      -- can't use 0 with temporary file => label check
initialSpaceScriptPages: Space.PageCount = 3;
initialLinkScriptPages: Space.PageCount = 4;
maxReasonableSwapUnit: Space.PageCount = 32;

-- The following hacks allow us to get to the interfaces that PilotKernel doesn't
-- export.  BEWARE IF THE GFIs CHANGE!

BootSwapCrossGfi: PrincOps.GFTIndex = 3B;
HierarchyImplGfi: PrincOps.GFTIndex = 55B;
MapLogImplGfi: PrincOps.GFTIndex = 61B;
SubVolumeImplGfi: PrincOps.GFTIndex = 26B;
VolumeImplGfi: PrincOps.GFTIndex = 53B;

-- Hack export to BootSwap --

pMon: PUBLIC LONG POINTER TO BootSwap.Mon ←
  LOOPHOLE[PrincOpsRuntime.GetFrame[PrincOpsRuntime.GFT[BootSwapCrossGfi]],
  	    POINTER TO FRAME[BootSwapCross]].pMon;


-- Checkpoint --

Checkpoint: PUBLIC PROCEDURE [volume: Volume.ID ← Volume.nullID]
  RETURNS [outcome: CedarSnapshot.Outcome] =
  BEGIN
  
  outloadLocation: disk Boot.Location;
  ScriptFull: ERROR = CODE;

  -- Cork --

  oldCursor: UserTerminal.CursorArray;
  oldPriority: Process.Priority = Process.GetPriority[];
  cork: PROCESS;
  state: {initial, corked, uncork} ← initial;
  stateChange: CONDITION ← [timeout: 0];

  CorkRestOfWorld: PROCEDURE =
    BEGIN
    EnsureCorked: ENTRY PROCEDURE = INLINE
      {cork ← FORK Cork[]; UNTIL state = corked DO WAIT stateChange; ENDLOOP};
    Process.SetPriority[ProcessPriorities.priorityPageFaultLow];
    oldCursor ← UserTerminal.GetCursorPattern[];
    EnsureCorked[];
    -- The cork is now at the head of the ready list for priorityPageFaultLow,
    -- and since it never faults, it yields only to its own priority level,
    -- so nothing below priorityPageFaultLow will run.  This is assumed to
    -- include the Ethernet driver(s), Cedar runtime processes, and anything
    -- else that uses the Space machinery.
    END;

  UncorkRestOfWorld: PROCEDURE =
    BEGIN
    state ← uncork; JOIN cork;
    UserTerminal.SetCursorPattern[oldCursor];
    Process.SetPriority[oldPriority];
    END;

  Cork: PROCEDURE =
    BEGIN
    myGF: PrincOps.GlobalFrameHandle = Frame.MyGlobalFrame[];
    AnnounceCorked: ENTRY PROCEDURE = INLINE
      {state ← corked; NOTIFY stateChange};
    OutermostProcOf: PROCEDURE [p: UNSPECIFIED] RETURNS [link: PrincOps.ControlLink] =
      BEGIN
      -- This gets around a bug in SpecialSpace.MakeProcedure{Resident|Swappable}
      link ← LOOPHOLE[p];
      WHILE link.indirect AND ~link.proc DO link ← link.link↑; ENDLOOP;
      IF ~link.proc THEN ERROR;
      RETURN[link]
      END;
    Yield: PROCEDURE =
      -- stolen from Processes to ensure residency
      BEGIN OPEN ProcessOperations;
      ProcessInternal.DisableInterrupts[];
      EnableAndRequeue[@PSB.PDA.ready, @PSB.PDA.ready, ReadPSB[]];
      END;
    Process.SetPriority[ProcessPriorities.priorityPageFaultLow];
    SpecialSpace.MakeProcedureResident[OutermostProcOf[Cork]];
    IF ~myGF.alloced THEN
      SpecialSpace.MakeGlobalFrameResident[CedarSnapshotMain];
    AnnounceCorked[];
    UNTIL state = uncork DO --Process.--Yield[]; ENDLOOP;
    IF ~myGF.alloced THEN
      SpecialSpace.MakeGlobalFrameSwappable[CedarSnapshotMain];
    SpecialSpace.MakeProcedureSwappable[OutermostProcOf[Cork]];
    END;

  -- Space script management

  outloadFile: File.Capability;
  firstVMPage: File.PageNumber;
  vmPages: File.PageCount;

  RollbackAction: TYPE = {keep, delete};
  SpaceScriptEntry: TYPE = RECORD [
    rollbackAction: RollbackAction, fill: [0..17777B] ← 0,
    level: CachedSpace.Level,
    page: VM.PageNumber];
  spaceScript: LONG DESCRIPTOR FOR ARRAY OF SpaceScriptEntry ← NIL;
  spaceScriptSpace: Space.Handle ← Space.nullHandle;
  nSpaceScriptPages: Space.PageCount ← initialSpaceScriptPages;
  nSpaceEntries: CARDINAL;

  Hierarchy: POINTER TO FRAME [HierarchyImpl] ←
    LOOPHOLE[PrincOpsRuntime.GetFrame[PrincOpsRuntime.GFT[HierarchyImplGfi]]];
  VMMapLog: POINTER TO FRAME [MapLogImpl] ←
    LOOPHOLE[PrincOpsRuntime.GetFrame[PrincOpsRuntime.GFT[MapLogImplGfi]]];

  InitializeSpaceScript: PROCEDURE =
    BEGIN
    IF spaceScriptSpace ~= Space.nullHandle THEN FlushSpaceScript[];
    spaceScriptSpace ← Space.Create[nSpaceScriptPages, Space.virtualMemory];
    Space.Map[spaceScriptSpace];
    SpecialSpace.MakeResident[spaceScriptSpace];
    spaceScript ← DESCRIPTOR[Space.LongPointer[spaceScriptSpace],
                        nSpaceScriptPages*Environment.wordsPerPage/SIZE[SpaceScriptEntry]];
    nSpaceEntries ← 2;  -- leave slots for space/region data base and maplog
    vmPages ← 0;
    END;
  FlushSpaceScript: PROCEDURE =
    BEGIN
    SpecialSpace.MakeSwappable[spaceScriptSpace];
    Space.Unmap[spaceScriptSpace];
    Space.Delete[spaceScriptSpace];
    END;
  EnumerateChildren: PROCEDURE [space: Space.Handle, proc: PROCEDURE [Space.Handle]] =
    BEGIN
    FOR child: Space.Handle ← Space.GetAttributes[space].lowestChild, Space.GetAttributes[child].nextSibling
    UNTIL child = Space.nullHandle DO proc[child]; ENDLOOP;
    END;
  AddSpace: PROCEDURE [
    space: Space.Handle, size: Space.PageCount, rollbackAction: RollbackAction] =
    BEGIN
    -- Possible future optimization:  don't enter the space in the list if it is
    -- dead (i.e., the backing storage contents are worthless).  To do this, we must
    -- iterate through the region cache, looking at every descriptor that overlaps
    -- the interval associated with 'space' and ask about its state.  If all regions
    -- happen to be dead, we don't enter the space in the spaceScript (if the
    -- rollback action is 'delete', we should delete the space now).
    sH: CachedSpace.Handle = LOOPHOLE[space];
    IF nSpaceEntries = LENGTH[spaceScript] THEN ERROR ScriptFull;
    spaceScript[nSpaceEntries] ←
      [rollbackAction: rollbackAction, level: sH.level, page: sH.page];
    nSpaceEntries ← nSpaceEntries + 1;
    vmPages ← vmPages + size;
    END;
  EntryToHandle: PROCEDURE [i: CARDINAL] RETURNS [Space.Handle] =
    BEGIN
    sH: CachedSpace.Handle =
      [level: spaceScript[i].level, page: spaceScript[i].page];
    RETURN[LOOPHOLE[sH]]
    END;
  BuildSpaceScript: PROCEDURE [space: Space.Handle] =
    BEGIN
    size: Space.PageCount;
    mapped: BOOLEAN;
    [size: size, mapped: mapped] ← Space.GetAttributes[space];
    IF mapped THEN
      BEGIN
      window: Space.WindowOrigin = Space.GetWindow[space];
      SELECT TRUE FROM
        space = spaceScriptSpace,
	space = linkScriptSpace => NULL;
	window = Space.defaultWindow =>
          BEGIN
	  -- The following ugly code is used to determine whether the space under
	  -- consideration is pinned.  If so, there is no need to save it in the
	  -- VM checkpoint, since it will be saved by OutLoad.  (Also, as it turns
	  -- out, the implementation of CopyOut/CopyIn can't handle such spaces,
	  -- even though the operations make sense.)
	  desc: CachedSpace.Desc;
	  validSpace, validSwapUnit: BOOLEAN;
          [validSpace, validSwapUnit] ← Hierarchy.GetDescriptor[@desc, LOOPHOLE[space]];
	  IF ~(validSpace AND ~validSwapUnit) THEN ERROR;
	  IF ~desc.pinned THEN  -- pinned spaces will be saved by OutLoad.
	    BEGIN
	    -- Because of limitations in the implementation of CopyIn/CopyOut, we
	    -- dare not present a space beyond a certain size, even though it has
	    -- swap units.  If a space is too big and has swap units, we perform
	    -- the CopyIn/CopyOut on the swap units instead.  If, however, a big
	    -- space has no swap units, we have two options:  we can do the copy in
	    -- a single lump and hope enough main memory can be found, or we can
	    -- temporarily add our own swap units and copy them out.  Although the
	    -- latter course is feasible, it doesn't seem worth the effort, since
	    -- the guy who created the space will cause it to swap as a unit when
	    -- he touches it, and there will have to be sufficient main memory then
	    -- to accommodate it.
	    child: Space.Handle;
	    parentSize: Space.PageCount;
	    [size: parentSize, lowestChild: child] ← Space.GetAttributes[space];
	    IF size <= maxReasonableSwapUnit OR child = Space.nullHandle THEN
	      AddSpace[space, size, keep]
	    ELSE
	      BEGIN
	      -- We do the CopyOut on the swap units here.  We must be careful,
	      -- however, since the parent space may not be completely tiled with
	      -- swap units.  This can't happen with uniform swap units, only with
	      -- explicit subspaces; for example, the bitmap allocated by UserTerminalImpl
	      -- works this way.
	      expectedBase: Space.PageOffset ← 0;
	      FillTo: PROCEDURE [nextBase: Space.PageOffset] =
	        BEGIN
		fillerSize: Space.PageCount = nextBase - expectedBase;
		filler: Space.Handle =
		  Space.Create[size: fillerSize, parent: space, base: expectedBase];
		AddSpace[filler, fillerSize, delete];
		END;
	      DO
	        base: Space.PageOffset;
	        size: Space.PageCount;
	        nextSibling: Space.Handle;
	        [base: base, size: size, nextSibling: nextSibling] ← Space.GetAttributes[child];
		IF base ~= expectedBase THEN FillTo[expectedBase];
		AddSpace[child, size, keep];
		expectedBase ← base + size;
		IF nextSibling = Space.nullHandle THEN EXIT;
	        child ← nextSibling;
	        ENDLOOP;
	      IF expectedBase ~= parentSize THEN FillTo[parentSize];
	      END;
	    END;
	  END;
        window.file.fID = File.nullID => NULL;  -- initial, pinned space
        File.GetAttributes[window.file].type = PilotFileTypes.tVMBackingFile =>
	  BEGIN
	  -- spaceScript[0]: space/region data base, spaceScript[1]: maplog
	  -- Note:  it is assumed that these two spaces are not pinned!  (If they
	  -- were, we wouldn't have to copy them.)
	  sH: CachedSpace.Handle = LOOPHOLE[space];
	  spaceScript[IF window.base = 0 THEN 1 ELSE 0] ←
	    [rollbackAction: keep, level: sH.level, page: sH.page];
	  vmPages ← vmPages + size;
	  END;
        ENDCASE =>
	  -- This deactivation isn't really necessary, but we want to be friendly.
	  -- After the rollback, non-pinned spaces mapped to persistent files (as opposed
	  -- to data spaces) will reflect the current contents of the disk.  Also,
	  -- by doing a Deactivate here, we increase the amount of memory available for
	  -- the CopyOuts of data spaces.  Note:  the spaces that are mapped to the files
	  -- that implement the Common Software directory are fixed up explicitly during
	  -- rollback by RationalizeDirectory, below.  Deactivation here is not sufficient.
	  Space.Deactivate[space];
      END
    ELSE EnumerateChildren[space, BuildSpaceScript];
    END;
  CheckpointVM: PROCEDURE =
    BEGIN
    window: Space.WindowOrigin;
    BEGIN
    ENABLE ScriptFull => {nSpaceScriptPages ← nSpaceScriptPages + 1; RETRY};
    InitializeSpaceScript[];
    EnumerateChildren[Space.virtualMemory, BuildSpaceScript];
    END;
    [outloadFile, firstVMPage, outloadLocation] ← MakeSnapshotFile[volume, firstOutloadPage, vmPages];
    SetPhase[2, checkpoint];
    -- Initialize the window to leave space for the space/region data base.
    window ← [outloadFile, firstVMPage + Space.GetAttributes[EntryToHandle[0]].size];
    -- Save all spaces in the script except the space/region data base.
    FOR i: CARDINAL IN [1..nSpaceEntries) DO
      space: Space.Handle = EntryToHandle[i];
      size:  Space.PageCount;
      mapped: BOOLEAN;
      [mapped: mapped, size: size] ← Space.GetAttributes[space];
      Space.CopyOut[space: space, window: window];
      -- The following is an optimization.  If the entry in the space script
      -- corresponds to a swap unit of a mapped space (rather than the mapped
      -- space itself, it means the mapped space is "unreasonably large" (see
      -- comments in BuildSpaceScript).  We would prefer not to have this
      -- unreasonably large space hanging around in memory, so we Deactivate it.
      IF ~mapped THEN Space.Deactivate[space];
      window.base ← window.base + size;
      Twiddle[];
      ENDLOOP;
    -- Write the space/region data base.  This requires a bit of care, since we want to be
    -- certain that the data base saved in the outloadFile is consistent with the region and
    -- space caches saved by the Snapshot.Outload.  Once the data base is written, no changes
    -- to the caches are permitted.  Since the act of doing a CopyOut on the data base may cause
    -- the caches to be loaded with descriptors for the space and regions that describe the data
    -- base itself, we CopyOut the data base a second time, assuming that the caches will not
    -- change again.  Note that we require that there be enough main memory to hold the entire
    -- space/region data base at one time.  If, in the future, this becomes unworkable, a more
    -- elaborate scheme for copying out the data base in pieces without sacrificing its
    -- consistency will have to be worked out.
    window ← [outloadFile, firstVMPage];
    Space.CopyOut[space: EntryToHandle[0], window: window];
    Space.CopyOut[space: EntryToHandle[0], window: window];  -- no, you aren't seeing double...
    END;
  RollbackVM: PROCEDURE =
    BEGIN
    window: Space.WindowOrigin ← [outloadFile, firstVMPage];
    prevMappingSpace: Space.Handle ← Space.nullHandle;
    readOnly: BOOLEAN;
    SetPhase[0, rollback];
    FOR i: CARDINAL IN [0..nSpaceEntries) DO
      special: BOOLEAN = i < 2;  -- space/region data base and maplog
      space: Space.Handle = EntryToHandle[i];
      size:  Space.PageCount;
      mapped: BOOLEAN;
      parent: Space.Handle;
      mappingSpace: Space.Handle ← space;
      [mapped: mapped, size: size, parent: parent] ← Space.GetAttributes[space];
      IF ~mapped THEN
	DO
	  isMapped: BOOLEAN;
	  mappingSpace ← parent;
	  [parent: parent, mapped: isMapped] ← Space.GetAttributes[mappingSpace];
	  IF isMapped THEN EXIT;
	  IF parent = Space.virtualMemory THEN ERROR;
	  ENDLOOP;
      IF special THEN readOnly ← FALSE
      ELSE
        IF prevMappingSpace ~= mappingSpace THEN
	  BEGIN
          -- The following dicey bit of code ensures that anonymous memory exists
          -- to back up the data space being restored.  It obtains the file ID of the
          -- backing file behind the data space and touches it (File.GetSize) to see if
          -- it still exists.  If not, we create a new backing file by unmapping and
          -- remapping the space.  Note that this will never remap a data space whose
          -- backing storage is in the VM backing file, since this is assumed not to
          -- go away.  (In principle, the space monitor should be locked while calling
          -- Hierarchy.GetDescriptor, but we're already playing fast and loose here...)
          desc: CachedSpace.Desc;
          needFile: BOOLEAN ← FALSE;
          validSpace, validSwapUnit: BOOLEAN;
          -- We must reapply write protect to the previous space if it originally had it.
	  IF readOnly THEN Space.MakeReadOnly[prevMappingSpace];
	  [validSpace, validSwapUnit] ← Hierarchy.GetDescriptor[@desc, LOOPHOLE[mappingSpace]];
          IF ~(validSpace AND ~validSwapUnit) THEN ERROR;
          [] ← File.GetSize[desc.window.file ! File.Unknown => {needFile ← TRUE; CONTINUE}];
          IF needFile THEN
            BEGIN
	    Space.Unmap[mappingSpace ! File.Unknown =>
	      VMMapLog.WriteLog1[interval: desc.interval, pSpaceD: NIL] -- the Unmap wasn't done --];
	    Space.Map[mappingSpace];
	    END;
          IF (readOnly ← desc.writeProtected) THEN
            BEGIN
	    -- We must make the space writable before rolling it back.
	    window: Space.WindowOrigin ← Space.GetWindow[mappingSpace];
	    window.file.permissions ← Inline.BITOR[window.file.permissions, write];
	    Space.MakeWritable[mappingSpace, window.file];
	    END;
	  prevMappingSpace ← mappingSpace;
          END;
      Space.CopyIn[space: space, window: window];
      -- The following is the same performance optimization as we did in
      -- CheckpointVM, with the additional provision that we don't deactivate
      -- "special" spaces, since they will come back in right away anyway.
      IF ~(mapped OR special) THEN Space.Deactivate[space];
      window.base ← window.base + size;
      Twiddle[];
      IF spaceScript[i].rollbackAction = delete THEN Space.Delete[space];
      REPEAT
        FINISHED => IF readOnly THEN Space.MakeReadOnly[prevMappingSpace];
      ENDLOOP;
    FlushSpaceScript[];
    END;

  -- Link script management

  PLinkScriptEntry: TYPE = LONG ORDERED POINTER TO LinkScriptEntry;
  LinkScriptEntry: TYPE = MACHINE DEPENDENT RECORD [
    frame: PrincOps.GlobalFrameHandle,
    mth: BcdOps.MTHandle,
    bcd: BcdOps.BcdBase,
    links: ARRAY [0..0) OF PrincOps.ControlLink];
  
  linkScriptBase, linkScriptLimit: PLinkScriptEntry ← NIL;
  linkScriptSpace: Space.Handle ← Space.nullHandle;
  nLinkScriptPages: Space.PageCount ← initialLinkScriptPages;
  nLinkScriptEntries: CARDINAL;

  -- Until PilotLoaderOps is exported, we have to do the following ugliness:
  PilotLoaderOps: POINTER TO FRAME [PilotLoaderSupport] ←
    LOOPHOLE[Runtime.GlobalFrame[Runtime.LoadConfig]];

  InitializeLinkScript: PROCEDURE =
    BEGIN
    IF linkScriptSpace ~= Space.nullHandle THEN FlushLinkScript[];
    linkScriptSpace ← Space.Create[nLinkScriptPages, Space.virtualMemory];
    Space.Map[linkScriptSpace];
    linkScriptBase ← LOOPHOLE[Space.LongPointer[linkScriptSpace]];
    linkScriptLimit ← linkScriptBase + nLinkScriptPages*Environment.wordsPerPage;
    nLinkScriptEntries ← 0;
    END;
  FlushLinkScript: PROCEDURE =
    BEGIN
    Space.Unmap[linkScriptSpace];
    Space.Delete[linkScriptSpace];
    END;
  CheckpointLinks: PROCEDURE =
    BEGIN OPEN PilotLoadStateOps;
    bootfID: File.ID = GetBootFileID[];
    lSE: PLinkScriptEntry;
    GetBootFileID: PROCEDURE RETURNS [File.ID] =
      BEGIN
      bootFiles: Boot.LVBootFiles;
      SpecialVolume.GetLogicalVolumeBootFiles[systemVolume, @bootFiles];
      RETURN[bootFiles[pilot].fID]
      END;
    InBootFile: PROCEDURE [p: LONG POINTER] RETURNS [BOOLEAN] = INLINE
      BEGIN
      OPEN Space;
      RETURN[GetWindow[GetHandle[PageFromLongPointer[p]]].file.fID = bootfID]
      END;
    ProcessBcd: PROCEDURE [config: ConfigIndex] RETURNS [BOOLEAN] =
      BEGIN
      bcd: BcdOps.BcdBase ← AcquireBcd[config];
      IF ~InBootFile[bcd] THEN
        BEGIN
	map: Map ← GetMap[config];
	ProcessLinks: PROCEDURE [mth: BcdOps.MTHandle, mti: BcdDefs.MTIndex]
	  RETURNS [BOOLEAN] =
	  BEGIN OPEN PrincOps, PrincOpsRuntime;
	  rgfi: GFTIndex = map[mth.gfi];
	  gf: GlobalFrameHandle = GetFrame[GFT[rgfi]];
	  IF ~GetModule[rgfi].resolved AND gf.codelinks THEN
	    BEGIN OPEN PilotLoaderOps;
	    nLinks: CARDINAL ← PilotLoaderOps.LinkSegmentLength[mth, bcd];
	    nextEntry: PLinkScriptEntry ←
	      lSE + SIZE[LinkScriptEntry] + nLinks*SIZE[ControlLink];
	    IF nextEntry > linkScriptLimit THEN
	      {ReleaseMap[map]; ReleaseBcd[bcd]; ERROR ScriptFull};
	    lSE↑ ← [frame: gf, mth: mth, bcd: bcd, links: ];
	    OpenLinkSpace[gf, mth, bcd];
	    FOR i: CARDINAL IN [0..nLinks) DO
	      lSE.links[i] ← ReadLink[i];
	      ENDLOOP;
	    CloseLinkSpace[gf];
	    lSE ← nextEntry;
	    nLinkScriptEntries ← nLinkScriptEntries + 1;
	    Twiddle[];
	    END;
	  RETURN [FALSE]
	  END;
	[] ← BcdOps.ProcessModules[bcd, ProcessLinks];
	ReleaseMap[map];
	END;
      ReleaseBcd[bcd];
      RETURN[FALSE]
      END;
    SetPhase[0, checkpoint];
    [] ← InputLoadState[];
    BEGIN
    ENABLE ScriptFull => {nLinkScriptPages ← nLinkScriptPages + 1; RETRY};
    InitializeLinkScript[];
    lSE ← linkScriptBase;
    [] ← EnumerateBcds[recentlast, ProcessBcd];
    END;
    ReleaseLoadState[];
    END;
  RollbackLinks: PROCEDURE =
    BEGIN
    lSE: PLinkScriptEntry ← linkScriptBase;
    SetPhase[1, rollback];
    THROUGH [0..nLinkScriptEntries) DO
      OPEN PilotLoaderOps, PrincOps;
      nLinks: CARDINAL = LinkSegmentLength[lSE.mth, lSE.bcd];
      OpenLinkSpace[lSE.frame, lSE.mth, lSE.bcd];
      FOR i: CARDINAL IN [0..nLinks) DO
        WriteLink[i, lSE.links[i]];
	ENDLOOP;
      CloseLinkSpace[lSE.frame];
      lSE ← lSE + SIZE[LinkScriptEntry] + nLinks*SIZE[ControlLink];
      Twiddle[];
      ENDLOOP;
    FlushLinkScript[];
    END;

  -- System volume cleanup

  LogicalVolume: POINTER TO FRAME [VolumeImpl] ←
    LOOPHOLE[PrincOpsRuntime.GetFrame[PrincOpsRuntime.GFT[VolumeImplGfi]]];
  VolumeImplInterface: POINTER TO FRAME [VolumeImpl] ← LogicalVolume;

  CheckpointSystemVolume: PROCEDURE =
    BEGIN
    -- We now flush the file cache to make sure that files deleted after the
    -- checkpoint will not be "remembered" when the rollback occurs.  It is
    -- assumed that uses of the space machinery by the volume open/close do not
    -- cause any changes in the space/region caches (for reasons described in
    -- CheckpointVM, above).  This works because the volume stuff uses
    -- SimpleSpace exclusively, which pins all descriptors in the caches.  Since
    -- this is a logical requirement of the FileMgr, we aren't likely to get
    -- burned in the future.
    WaitForDiskToIdle: PROCEDURE =
      BEGIN
      -- All this is because the disk must be quiet before we can close the volume.
      subVolumeFrame: POINTER TO FRAME [SubVolumeImpl] ←
        LOOPHOLE[PrincOpsRuntime.GetFrame[PrincOpsRuntime.GFT[SubVolumeImplGfi]]];
      diskChannelFrame: POINTER TO FRAME [DiskChannelImpl];
      BEGIN OPEN subVolumeFrame;
      FOR cePtr: CePtr ← ceFirst, cePtr + SIZE[CacheEntry] WHILE cePtr <= ceLast DO
        IF cePtr.occupied THEN
          BEGIN
          -- Some turkey put the following procedures in a DISCARD CODE PACK, so we
          -- REALLY get our hands dirty!
          -- DiskChannel.Idle[cePtr.svDesc.channel];
          -- DiskChannel.Restart[cePtr.svDesc.channel];
          base: Environment.Base = Environment.first64K;
          channelObj: LONG POINTER TO diskChannelFrame.ChannelObject ←
            @base[LOOPHOLE[cePtr.svDesc.channel, diskChannelFrame.ChannelHandle]];
          UNTIL channelObj.ioCount = 0 DO Process.Pause[Process.SecondsToTicks[1]]; ENDLOOP;
          END;
        ENDLOOP;
      END;
      END;
    SetPhase[3, checkpoint];
    WaitForDiskToIdle[];
    -- We now want to close the system volume, causing Pilot (specifically, the FileMgr) to
    -- forget everything it has cached about the contents of the system volume.  This includes
    -- the file cache, the VFM, and the volume root page.  LogicalVolume.CloseLogicalVolume
    -- should do all of these, but because of a bug, it retains a cached copy of the
    -- volume root page.  To get around this, witness the crazy stuff below with
    -- bringing subvolumes on- and off-line, and observe the 'root' parameter is TRUE in
    -- the offline case.  This has the effect of leaving the logical volume table unchanged,
    -- but causing the volume root page to be flushed.  Look at the code in VolumeImpl if
    -- you are skeptical.
    VolumeImplInterface.SubvolumeOnline[lvID: systemVolume, root: TRUE];
    LogicalVolume.CloseLogicalVolume[@systemVolume];
    VolumeImplInterface.SubvolumeOffline[lvID: systemVolume, root: TRUE];
    END;
  RollbackSystemVolume: PROCEDURE =
    BEGIN
    [] ← LogicalVolume.OpenLogicalVolume[@systemVolume];
    END;

  -- OutLoad implementation

  OutLoad: PROCEDURE RETURNS [inLoaded: BOOLEAN] =
    BEGIN
    -- This code is stolen directly from SnapshotImpl.OutLoad.
    psb: PSB.PsbHandle; ptc: CARDINAL; wdc: CARDINAL;
    -- Save process state not captured in PDA:
    ProcessInternal.DisableInterrupts[]; -- make it hold still first
    psb ← ProcessOperations.ReadPSB[];
    ptc ← ProcessOperations.ReadPTC[];
    wdc ← ProcessOperations.ReadWDC[];
    DeviceCleanup.Perform[turnOff]; -- turn all devices off.
    -- Save our state on a boot file:  (If the boot file is inloaded
    -- later, we will reappear here with inLoaded=TRUE.)
    inLoaded ← BootSwap.OutLoad[@outloadLocation, restore] ~= outLoaded;
    IF inLoaded THEN
      BEGIN
      -- Restore process state not captured in PDA.
      ProcessOperations.WriteWDC[wdc];
      ProcessOperations.WritePTC[ptc];
      ProcessOperations.WritePSB[psb];
      -- The following is a temporary substitute for a clock chip.
      -- We must do it with interrupts off or Communication will be using the Ethernet--
      -- if we get an allocation trap, all is lost.
      [] ← TemporarySetGMT.SetGMT[];
      END;
    DeviceCleanup.Perform[turnOn]; -- turn devices back on
    ProcessorFace.SetMP[PilotMP.cClient]; -- announce our return
    ProcessInternal.EnableInterrupts[];
    END;

  -- Directory cleanup

  RationalizeDirectory: PROCEDURE =
    BEGIN
    directoryFilesFrame: POINTER TO FRAME [DirectoryFilesImpl] ←
      LOOPHOLE[Runtime.GlobalFrame[Directory.CreateFile]];
    SetPhase[2, rollback];
    FOR i: CARDINAL IN [1 .. DirectoryFiles.maxDCache] DO
      dCE: LONG POINTER TO DirectoryFiles.DCEntry = @directoryFilesFrame.directoryCache[i];
      IF dCE.refCount > 0 THEN
        BEGIN
	-- This fixes up the space descriptor (in particular, the countMapped
	-- information) to account for any change in file size.
	window: Space.WindowOrigin = Space.GetWindow[dCE.dir.space];
	Space.Unmap[dCE.dir.space];
	Space.Map[dCE.dir.space, window];
	Twiddle[];
	END;
      ENDLOOP;
    END;

  -- *** Here's the real work ***
  
  systemVolume: Volume.ID ← Volume.systemID;

  CorkRestOfWorld[];
  IF volume = Volume.nullID THEN volume ← systemVolume;
  -- Save all necessary code links
  CheckpointLinks[];
  -- Save space/region data base, maplog, and all data spaces.
  BEGIN
  CheckpointVM[ ! Volume.InsufficientSpace => GO TO cantCheckpoint];
  EXITS
    cantCheckpoint =>
      {UncorkRestOfWorld[]; RETURN[insufficientDiskSpace]};
  END;
  -- Flush file cache and shut down the system volume.
  CheckpointSystemVolume[];
  -- Save real memory.  Note:  we can't use Snapshot.Outload because it requires
  -- that the volume containing the boot file be open, and we can't tolerate that.
  IF ~OutLoad[].inLoaded THEN
    BEGIN
    RollbackSystemVolume[];  -- system volume must be open before we proceed!
    UncorkRestOfWorld[];
    RETURN[checkpointed]
    END;
  -- Inload has restored real memory.  Reopen the system volume before restoring VM.
  RollbackSystemVolume[];
  -- Restore space/region data base, maplog, and data spaces.
  RollbackVM[];
  -- Restore code links that may have been invalidated.
  RollbackLinks[];
  -- Now that memory is back together, make the directory system believe the current
  -- state of the directory files on the disk.
  RationalizeDirectory[];
  UncorkRestOfWorld[];
  RETURN[rolledBack]
  END;

-- Rollback --

RollBack: PUBLIC PROCEDURE [volume: Volume.ID ← Volume.nullID] =
  BEGIN
  bootFiles: Boot.LVBootFiles;
  IF volume = Volume.nullID THEN volume ← Volume.systemID;
  SpecialVolume.GetLogicalVolumeBootFiles[volume, @bootFiles];
  IF volume ~= Volume.systemID THEN
    VolumeExtras.OpenVolume[volume: volume, readOnly: TRUE];
  Snapshot.InLoad[
    pMicrocode: NIL, pGerm: NIL, countGerm: 0,
    file: [fID: bootFiles[clientImage].fID, permissions: read],
    firstPage: bootFiles[clientImage].firstPage ! ANY => CONTINUE];
  -- Note: execution normally continues inside Checkpoint.  If the
  -- above InLoad failed, we make sure that there is no trace of a
  -- possibly bad checkpoint file before returning to the caller of
  -- Rollback, who can decide what to do instead of the rollback.
  bootFiles[clientImage].fID ← File.nullID;
  SpecialVolume.SetLogicalVolumeBootFiles[volume, @bootFiles];
  END;


-- Utilities --

MakeSnapshotFile: PROCEDURE [
  volume: Volume.ID, firstRMPage: File.PageNumber, pagesForVM: File.PageCount]
  RETURNS [file: File.Capability, firstVMPage: File.PageNumber, location: disk Boot.Location] =
  BEGIN
  pagesForRM: File.PageCount =
    BootFile.MemorySizeToFileSize[SpecialSpace.realMemorySize];
  filePages: File.PageCount = firstRMPage + pagesForRM + pagesForVM;
  bootFiles: Boot.LVBootFiles;
  found: BOOLEAN;
  CheckForExistingFile: PROCEDURE RETURNS [found: BOOLEAN, file: File.Capability] =
    BEGIN
    found ← FALSE;
    IF bootFiles[clientImage].fID = File.nullID THEN RETURN;
    file ← [fID: bootFiles[clientImage].fID, permissions: read+write+delete];
    IF File.GetSize[file ! File.Unknown => GO TO noGood] < filePages THEN
      {File.Delete[file]; GO TO noGood};
    found ← TRUE;
    EXITS
      noGood =>
        BEGIN
	bootFiles[clientImage].fID ← File.nullID;
	SpecialVolume.SetLogicalVolumeBootFiles[volume, @bootFiles];
	END;
    END;
  SetPhase[1, checkpoint];
  SpecialVolume.GetLogicalVolumeBootFiles[volume, @bootFiles];
  [found, file] ← CheckForExistingFile[];
  IF ~found THEN
    file ← File.Create[volume, filePages, FileTypes.tUntypedFile];
  Twiddle[];
  [] ← SpecialFile.MakeBootable[
      file: file, firstPage: firstRMPage, count: pagesForRM,
      lastLink: SpecialFile.Link[0]];
  Twiddle[];
  location.diskFileID ← [fID: file.fID, firstPage: firstRMPage, da:];
  [deviceType: location.deviceType, deviceOrdinal: location.deviceOrdinal,
    link: LOOPHOLE[location.diskFileID.da, SpecialFile.Link]] ←
    KernelFile.GetBootLocation[file, firstRMPage];
  bootFiles[clientImage] ← [file.fID, firstRMPage, LOOPHOLE[location.diskFileID.da]];
  SpecialVolume.SetLogicalVolumeBootFiles[volume, @bootFiles];
  RETURN[File.LimitPermissions[file, read+write], firstRMPage + pagesForRM, location]
  END;


-- Feedback to User  --

cStart: CARDINAL = 0;
cEnd: CARDINAL = SIZE[UserTerminal.CursorArray];
cMiddle: CARDINAL = (cEnd + cStart) / 2;

Direction: TYPE = {checkpoint, rollback};

cursors: ARRAY Direction OF UserTerminal.CursorArray =
  [[075122B, 041122B, 041124B, 041734B, 041124B, 041122B, 075122B, 000000B,
    017370B, 011040B, 011040B, 016040B, 010040B, 010040B, 010040B, 000000B],
   [167210B, 125210B, 125210B, 145210B, 125210B, 125210B, 127356B, 000000B,
    147352B, 125212B, 125212B, 147214B, 125212B, 125212B, 145352B, 000000B]];

SetPhase: PROCEDURE [phase: [0..7], direction: Direction] =
  BEGIN
  cursor: UserTerminal.CursorArray ← cursors[direction];
  topMasks: ARRAY [0..4) OF WORD = [0, 177400B, 377B, 177777B];
  bottomMask: WORD = 177400B;
  topMask: WORD = topMasks[phase MOD 4];
  IF topMask ~= 0 THEN
    FOR i: CARDINAL IN [0..cMiddle) DO
      cursor[i] ← Inline.BITXOR[cursor[i], topMask];
      ENDLOOP;
  IF phase > 4 THEN
    FOR i: CARDINAL IN [cMiddle..cEnd) DO
      cursor[i] ← Inline.BITXOR[cursor[i], bottomMask];
      ENDLOOP;
  UserTerminal.SetCursorPattern[cursor];
  END;

Twiddle: PROCEDURE =
  BEGIN
  cursor: UserTerminal.CursorArray ← UserTerminal.GetCursorPattern[];
  FOR i: CARDINAL IN [cMiddle..cEnd) DO
    cursor[i] ← Inline.BITXOR[cursor[i], 377B];
    ENDLOOP;
  UserTerminal.SetCursorPattern[cursor];
  END;

END.