-- File: VMFile.mesa
-- Last edited by Levin:  24-Aug-82 14:26:39

DIRECTORY
  AltoFile USING [
    CloseDirectory, CloseFile, Delete, DirHandle, Enter, FileHandle,
    fileNameChars, FP, IllegalFileName, LDPtr, Lookup, OpenDirectory,
    OpenFromFP, ReadLeaderPage, RewriteLeaderPage, sysDirFP,
    WaitForSpaceCrunch],
  DiskIODefs USING [DiskRequest],
  FileDefs USING [
    ComparePositions, FileHandle, FinalizeAlto, FinalizeIFS, FinalizeJuniper,
    FSInstance, InitializeAlto, InitializeIFS, InitializeJuniper, Operations],
  StringDefs USING [AppendChar, AppendString, MesaToBcplString],
  VMDefs USING [
    AccessFailure, CacheIndex, FileHandle, FileObject, FileSystemType, FileTime, FSAlto,
    OpenOptions, Page, PageNumber, Percentage, Position, Problem, ReadPage, Release,
    Start, TransactionProblem, Wait],
  VMPrivate USING [
    closedSeal, EnumerateCachePagesInFile, FileHandle, FileObject,
    FileSystem, FSInstance, MDSPageToAddress, Object, openSeal, PageHandle,
    underwaySeal, ValidateFile, ValidatePageNumber, Writable],
  VMSpecial USING [],
  VMStorage USING [longTerm, shortTerm];

VMFile: MONITOR
  IMPORTS AltoFile, FileDefs, StringDefs, VMDefs, VMPrivate, VMStorage
  EXPORTS VMDefs, VMPrivate, VMSpecial =

  BEGIN OPEN VMDefs, VMPrivate;


  -- Global Variables --

  fileList: FileHandle;
  ChangeInOpenState: CONDITION;

  cacheLimit: CacheIndex;

  fsOps: PUBLIC ARRAY FileSystemType OF FileDefs.Operations;

  altoFileSystem: FileSystem;


  -- Miscellaneous Declarations --

  CantDestroyReadOnlyFile: ERROR = CODE;
  FileInUse: ERROR = CODE;
  FileNotInList: ERROR = CODE;
  FileListSmashed: ERROR = CODE;
  LogoutIllegalWithOpenFiles: ERROR = CODE;
  PageInUse: ERROR = CODE;
  UseCountWrong: ERROR = CODE;


  -- Procedures, Signals, and Types Exported to VMDefs --

  FileObject: PUBLIC TYPE = VMPrivate.FileObject;
  FSInstance: PUBLIC TYPE = VMPrivate.FSInstance;

  -- File system access --

  Error: PUBLIC ERROR [reason: Problem] = CODE;

  Login: PUBLIC PROCEDURE [system: FileSystemType,
    server, userName, password, secondaryName, secondaryPassword: STRING ← NIL]
    RETURNS [FileSystem] =
    BEGIN
    instance: FileDefs.FSInstance;
    IF fsOps[system] = NIL THEN ERROR UnableToLogin[other];
    instance ← fsOps[system].login[server, userName, password,
    				    secondaryName, secondaryPassword];
    RETURN[VMStorage.shortTerm.NEW[FSInstance ← [fsOps[system], instance]]]
    END;

  UnableToLogin: PUBLIC ERROR [reason: Problem] = CODE;

  Logout: PUBLIC PROCEDURE [fs: FileSystem] =
    BEGIN
    IF fs = FSAlto THEN RETURN;
    EnsureNoOpenFiles[fs];
    fs.ops.logout[fs.instance];
    VMStorage.shortTerm.FREE[@fs];
    END;

  CheckpointTransaction: PUBLIC PROCEDURE [fs: FileSystem] =
    {fs.ops.checkpoint[fs.instance]};

  TransactionError: PUBLIC ERROR [reason: TransactionProblem] = CODE;

  AbortTransaction: PUBLIC PROCEDURE [fs: FileSystem] =
    {fs.ops.abort[fs.instance]};

  -- File handling --

  OpenFile: PUBLIC PROCEDURE [
    system: FileSystem ← FSAlto, name: STRING, options: OpenOptions ← oldReadOnly,
    cacheFraction: Percentage ← 0]
    RETURNS [vmFile: FileHandle] =
    BEGIN
    new: BOOLEAN;
    IF system = FSAlto THEN system ← altoFileSystem;
    [new, vmFile] ← AddToList[system.ops.open[system.instance, name, options], options];
    IF new THEN
      BEGIN
      vmFile.fs ← system;
      vmFile.cachePages ← (cacheFraction*cacheLimit)/100;
      vmFile.altoFile ← (system.ops = fsOps[Alto]);
      END;
    END;

  CantOpen: PUBLIC ERROR [reason: AccessFailure] = CODE;

  CloseFile: PUBLIC PROCEDURE [file: FileHandle] =
    {StartFile[file]; DoCloseDestroyOrAbandon[file, close]};

  GetOpenFileParameters: PUBLIC PROCEDURE [file: FileHandle]
    RETURNS [system: FileSystem, options: OpenOptions, cacheFraction: Percentage] =
    {RETURN[
      IF file.fs = altoFileSystem THEN FSAlto ELSE file.fs,
      file.options, (file.cachePages*100)/cacheLimit]};

  AbandonFile: PUBLIC PROCEDURE [file: FileHandle] =
    {DoCloseDestroyOrAbandon[file, abandon]};

  DestroyFile: PUBLIC PROCEDURE [file: FileHandle] =
    -- At present, the StartFile is necessary to avoid a difficult back-out later.
    -- It may generate unnecessary I/O, but it is otherwise harmless.
    {StartFile[file]; DoCloseDestroyOrAbandon[file, destroy]};

  GetFileLength: PUBLIC PROCEDURE [file: FileHandle] RETURNS [length: Position] =
    BEGIN
    ValidateFile[file];
    RETURN[file.fs.ops.getLength[file.fh]]
    END;

  SetFileLength: PUBLIC PROCEDURE [file: FileHandle, position: Position] =
    BEGIN
    oldLength: Position = GetFileLength[file];
    atomicExtend: BOOLEAN ← FALSE;

    FlushTruncatedPage: PROCEDURE [page: PageHandle] RETURNS [found, unmap: BOOLEAN] =
      -- checks to see if 'page' should be flushed because of file truncation.
      BEGIN
      found ← unmap ← FALSE;
      IF page.page < position.page OR
	(page.page = position.page AND position.byte > 0) THEN RETURN;
      IF page.useCount ~= 1 THEN ERROR PageInUse;  -- useCount = 1 from enumeration
      unmap ← TRUE;
      END;

    ValidateFile[file];
    ValidatePageNumber[position.page];
    SELECT FileDefs.ComparePositions[oldLength, position] FROM
      greater => [] ← EnumerateCachePagesInFile[file, FlushTruncatedPage];
      equal => RETURN;
      less =>
	atomicExtend ← oldLength.byte ~= 0 AND 
			(oldLength.page = position.page OR
			position = Position[oldLength.page + 1, 0]);
      ENDCASE;
    IF atomicExtend THEN
      BEGIN
      data: Page = ReadPage[[file, oldLength.page]];
      file.fs.ops.extend[file.fh, position, data ! Error => Release[data]];
      Release[data];
      END
    ELSE file.fs.ops.setLength[file.fh, position];
    END;

  StartFile: PUBLIC PROCEDURE [file: FileHandle] =
    BEGIN

    StartPage: PROCEDURE [page: PageHandle] RETURNS [found, unmap: BOOLEAN] =
      -- if 'page' is dirty, initiates a transfer of 'page' to the file.
      {Start[MDSPageToAddress[page.buffer]]; RETURN[FALSE, FALSE]};

    ValidateFile[file];
    [] ← EnumerateCachePagesInFile[file, StartPage];
    END;

  WaitFile: PUBLIC PROCEDURE [file: FileHandle] =
    BEGIN

    WaitPage: PROCEDURE [page: PageHandle] RETURNS [found, unmap: BOOLEAN] =
      -- waits until 'page' is in a stable state.
      {Wait[MDSPageToAddress[page.buffer]]; RETURN[FALSE, FALSE]};

    ValidateFile[file];
    [] ← EnumerateCachePagesInFile[file, WaitPage];
    END;

  GetFileTimes: PUBLIC PROCEDURE [file: FileHandle]
    RETURNS [read, write, create: VMDefs.FileTime] =
    {[read, write, create] ← file.fs.ops.getTimes[file.fh]};

  SetCreationTime: PUBLIC PROCEDURE [file: FileHandle, create: FileTime ← 0] =
    {file.fs.ops.setCreation[file.fh, create]};


  -- Procedures and Signals Exported to VMSpecial --

  OpenAltoFileFromFP: PUBLIC PROCEDURE [
    fp: AltoFile.FP, writable: BOOLEAN, cacheFraction: Percentage ← 0]
    RETURNS [vmFile: FileHandle] =
    BEGIN
    new: BOOLEAN;
    options: OpenOptions = IF writable THEN old ELSE oldReadOnly;
    [new, vmFile] ← AddToList[AltoFile.OpenFromFP[fp, writable], options];
    IF new THEN
      BEGIN
      vmFile.fs ← altoFileSystem;
      vmFile.cachePages ← (cacheFraction*cacheLimit)/100;
      vmFile.altoFile ← TRUE;
      END;
    END;

  QuickAndDirtyAltoRename: PUBLIC PROCEDURE [old, new: STRING] RETURNS [worked: BOOLEAN] =
    BEGIN OPEN AltoFile;
    dir: DirHandle = OpenDirectory[sysDirFP];
    fp: FP;
    ValidRename: PROCEDURE RETURNS [BOOLEAN] = INLINE
      BEGIN
      ENABLE IllegalFileName => GO TO failed;
      RETURN[Lookup[dir, old, @fp].found AND Enter[dir, new, @fp]]
      EXITS
	failed => RETURN[FALSE];
      END;
    IF (worked ← ValidRename[]) THEN
      BEGIN
      leader: LDPtr;
      request: DiskIODefs.DiskRequest;
      fullName: STRING ← [fileNameChars];
      file: FileHandle = OpenFromFP[fp: fp, markWritten: FALSE];
      leader ← ReadLeaderPage[file, @request];
      StringDefs.AppendString[fullName, new];
      IF fullName[fullName.length-1] ~= '. THEN
        StringDefs.AppendChar[fullName, '.];
      StringDefs.MesaToBcplString[fullName, LOOPHOLE[@leader.name]];
      RewriteLeaderPage[file, @request, leader];
      CloseFile[file];
      [] ← Delete[dir, old];
      END;
    CloseDirectory[dir];
    END;

  WaitForAltoDiskSpaceCrunch: PUBLIC PROCEDURE [pages: CARDINAL] RETURNS [BOOLEAN] =
    {RETURN[AltoFile.WaitForSpaceCrunch[pages]]};


  -- Procedures and Signals Exported to VMPrivate --

  InitializeVMFile: PUBLIC PROCEDURE [maxCachePages: CacheIndex] =
    BEGIN
    fileList ← NIL;
    cacheLimit ← maxCachePages;
    fsOps[Alto] ← FileDefs.InitializeAlto[];
    fsOps[IFS] ← FileDefs.InitializeIFS[];
    fsOps[Juniper] ← FileDefs.InitializeJuniper[];
    altoFileSystem ← VMStorage.longTerm.NEW[FSInstance ← FSInstance[fsOps[Alto], NIL]];
    altoFileSystem.instance ← fsOps[Alto].login[];
    END;

  FinalizeVMFile: PUBLIC PROCEDURE =
    BEGIN
    fsOps[Alto].logout[altoFileSystem.instance];
    VMStorage.longTerm.FREE[@altoFileSystem];
    IF fileList ~= NIL THEN ERROR FileInUse;
    FileDefs.FinalizeJuniper[];
    FileDefs.FinalizeIFS[];
    FileDefs.FinalizeAlto[];
    END;

  InvalidFile: PUBLIC ERROR = CODE;
  InvalidPageNumber: PUBLIC ERROR = CODE;


  -- Internal Procedures --

  AddToList: ENTRY PROCEDURE [fFile: FileDefs.FileHandle, options: OpenOptions]
    RETURNS [newlyEntered: BOOLEAN, vmFile: FileHandle] =
    -- ensures that only a single entry for file 'fFile' appears in the fileList.
    BEGIN
    writable: BOOLEAN = options ~= oldReadOnly;
    newlyEntered ← FALSE;
    vmFile ← fileList;
    UNTIL vmFile = NIL DO
      IF vmFile.fh = fFile THEN
	SELECT vmFile.seal FROM
	  openSeal =>
	    IF writable THEN RETURN WITH ERROR CantOpen[accessDenied]
	    ELSE {vmFile.openCount ← vmFile.openCount + 1; RETURN};
	  underwaySeal => {WAIT ChangeInOpenState; vmFile ← fileList};
	  ENDCASE => GO TO bogusList
      ELSE
	SELECT vmFile.seal FROM
	  openSeal, underwaySeal => vmFile ← vmFile.link;
	  ENDCASE => GO TO bogusList;
      REPEAT
	bogusList => ERROR FileListSmashed;
      ENDLOOP;
    vmFile ← VMStorage.shortTerm.NEW[FileObject ← Object[file[
      seal: openSeal, useCount: 0, link: fileList, fh: fFile, fs: ,
      cachePages: 0, options: options, altoFile: , openCount: 1]]];
    fileList ← vmFile;
    newlyEntered ← TRUE;
    END;

  DoCloseDestroyOrAbandon: PROCEDURE [file: FileHandle, op: {close, destroy, abandon}] =
    -- eliminates 'file' from file list and cleans it up as indicated by 'op'.
    BEGIN
    last: BOOLEAN;

    LockIfLastReference: ENTRY PROCEDURE RETURNS [last: BOOLEAN] = INLINE
      BEGIN
      IF file.seal ~= openSeal OR file.openCount = 0 THEN ERROR InvalidFile;
      IF (last ← file.openCount = 1) THEN file.seal ← underwaySeal
      ELSE file.openCount ← file.openCount - 1;
      END;

    RemoveFile: ENTRY PROCEDURE = INLINE
      BEGIN
      IF file = fileList THEN fileList ← file.link
      ELSE
	BEGIN
	IF fileList = NIL THEN GO TO Trouble;
	FOR prev: FileHandle ← fileList, prev.link UNTIL prev.link = NIL DO
	  IF prev.link = file THEN {prev.link ← file.link; EXIT};
	  REPEAT FINISHED => GO TO Trouble;
	  ENDLOOP;
	EXITS Trouble => ERROR FileNotInList;
	END;
      file.seal ← closedSeal; -- try to catch dangling references
      BROADCAST ChangeInOpenState;
      END;

    RemovePage: PROCEDURE [page: PageHandle] RETURNS [found, unmap: BOOLEAN] =
      BEGIN
      -- useCount = 1 in the following because of the enumeration
      IF op = abandon THEN page.useCount ← 1  -- force unmap to take effect
      ELSE IF page.useCount ~= 1 OR page.dirty THEN ERROR PageInUse;
      RETURN[FALSE, TRUE]
      END;

    IF op ~= abandon THEN WaitFile[file];
    last ← LockIfLastReference[];
    SELECT op FROM
      close => file.fs.ops.close[file.fh];
      destroy =>
	IF Writable[file] THEN file.fs.ops.destroy[file.fh]
	ELSE ERROR CantDestroyReadOnlyFile;
      ENDCASE => file.fs.ops.abandon[file.fh];
    IF ~last THEN RETURN;
    [] ← EnumerateCachePagesInFile[file, RemovePage];
    IF file.useCount ~= 0 THEN ERROR UseCountWrong;
    RemoveFile[];
    VMStorage.shortTerm.FREE[@file];
    END;

  EnsureNoOpenFiles: ENTRY PROCEDURE [fs: FileSystem] =
    BEGIN
    vmFile: FileHandle ← fileList;
    UNTIL vmFile = NIL DO
      IF vmFile.fs = fs THEN ERROR LogoutIllegalWithOpenFiles
      ELSE
	SELECT vmFile.seal FROM
	  openSeal, underwaySeal => vmFile ← vmFile.link;
	  ENDCASE => ERROR FileListSmashed;
      ENDLOOP;
    END;

  END.