-- Copyright (C) 1981, 1982, 1984  by Xerox Corporation. All rights reserved. 
-- File: VMFile.mesa

-- HGM,  6-Jan-85 11:02:00
-- Last edited by Wobber:  2-Nov-82 12:27:34
-- Last edited by Gobbel: 11-Sep-81 16:41:44

-- NOTE: To maintain source compatibility with AltoMesa based Grapevine servers, Pilot
-- file system operations are exported through the Alto file system slot.

DIRECTORY
  FileDefs USING [
    ComparePositions, FileHandle, FinalizeAlto, FinalizeIFS, FSInstance,
    InitializeAlto, InitializeIFS, Operations],
  LogDefs USING [ShowTwoStrings],
  Time USING [Current],
  VMDefs USING [
    AccessFailure, CacheIndex, defaultTime, FileHandle, FileSystemType, FileTime,
    FSAlto, OpenOptions, Page, PageNumber, Percentage, Position, Problem, Release,
    Start, UsePage, Wait],
  VMPrivate USING [
    closedSeal, EnumerateCachePagesInFile, FileHandle, FileObject, FileSystem,
    FSInstance, MDSPageToAddress, Object, openSeal, PageHandle, underwaySeal,
    ValidateFile, ValidatePageNumber, Writable],
  VMStorage USING [longTerm, shortTerm];

VMFile: MONITOR
  IMPORTS FileDefs, LogDefs, Time, VMDefs, VMPrivate, VMStorage EXPORTS VMDefs, VMPrivate =

  BEGIN OPEN VMDefs, VMPrivate;


  -- Global Variables --

  fileList: FileHandle;
  ChangeInOpenState: CONDITION;

  cacheLimit: CacheIndex;

  fsOps: PUBLIC ARRAY FileSystemType OF FileDefs.Operations;

  pilotFileSystem: 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;
  UnableToLogin: PUBLIC ERROR [reason: Problem] = CODE;

  Login: PUBLIC ENTRY PROCEDURE [
    system: FileSystemType,
    server, userName, password, secondaryName, secondaryPassword: STRING ← NIL]
    RETURNS [FileSystem] =
    BEGIN
    ENABLE UNWIND => NULL;
    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;

  Logout: PUBLIC ENTRY PROCEDURE [fs: FileSystem] =
    BEGIN
    ENABLE UNWIND => NULL;
    EnsureNoOpenFiles[fs];
    fs.ops.logout[fs.instance];
    VMStorage.shortTerm.FREE[@fs];
    END;

  AbortTransaction, CheckpointTransaction: PUBLIC PROCEDURE [fs: FileSystem] = {};

  -- File handling --

  OpenFile: PUBLIC PROCEDURE [
    system: FileSystem ← FSAlto, name: STRING, options: OpenOptions ← oldReadOnly,
    cacheFraction: Percentage ← 0] RETURNS [vmFile: FileHandle] =
    BEGIN
    ENABLE UNWIND => LogDefs.ShowTwoStrings["Can't open "L, name];
    IF system = FSAlto THEN system ← pilotFileSystem;
    RETURN[OpenFileInner[system, name, options, cacheFraction]];
    END;

  OpenFileInner: ENTRY PROCEDURE [
    system: FileSystem ← FSAlto, name: STRING, options: OpenOptions ← oldReadOnly,
    cacheFraction: Percentage ← 0] RETURNS [vmFile: FileHandle] =
    BEGIN
    ENABLE UNWIND => NULL;
    new: BOOLEAN;
    [new, vmFile] ← AddToList[
      system.ops.open[system.instance, name, options], options];
    IF new THEN {
      vmFile.fs ← system; vmFile.cachePages ← (cacheFraction * cacheLimit) / 100};
    END;

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

  CloseFile: PUBLIC ENTRY PROCEDURE [file: FileHandle] = {
    ENABLE UNWIND => NULL;
    StartFileInternal[file];
    DoCloseDestroyOrAbandon[file, close]};

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

  AbandonFile: PUBLIC ENTRY PROCEDURE [file: FileHandle] = {
    ENABLE UNWIND => NULL; DoCloseDestroyOrAbandon[file, abandon]};

  DestroyFile: PUBLIC ENTRY PROCEDURE [file: FileHandle] = {
    ENABLE UNWIND => NULL;
    StartFileInternal[file];
    DoCloseDestroyOrAbandon[file, destroy]};

  GetFileLength: PUBLIC ENTRY PROCEDURE [file: FileHandle]
    RETURNS [length: Position] = {
    ENABLE UNWIND => NULL;
    ValidateFile[file];
    RETURN[file.fs.ops.getLength[file.fh]]};

  SetFileLength: PUBLIC ENTRY PROCEDURE [file: FileHandle, position: Position] =
    BEGIN
    ENABLE UNWIND => NULL;
    oldLength: Position;
    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];
    oldLength ← file.fs.ops.getLength[file.fh];
    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 = UsePage[[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 ENTRY PROCEDURE [file: FileHandle] = {
    ENABLE UNWIND => NULL; StartFileInternal[file]};
  StartFileInternal: INTERNAL 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 ENTRY PROCEDURE [file: FileHandle] = {
    ENABLE UNWIND => NULL; WaitFileInternal[file]};
  WaitFileInternal: INTERNAL 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 ENTRY PROCEDURE [file: FileHandle]
    RETURNS [read, write, create: VMDefs.FileTime] = {
    ENABLE UNWIND => NULL; [read, write, create] ← file.fs.ops.getTimes[file.fh]};

  SetCreationTime: PUBLIC ENTRY PROCEDURE [
    file: FileHandle, create: FileTime ← defaultTime] =
    BEGIN
    ENABLE UNWIND => NULL;
    IF create = defaultTime THEN create ← Time.Current[];
    file.fs.ops.setCreation[file.fh, create];
    END;


  -- Procedures and Signals Exported to VMPrivate --

  InitializeVMFile: PUBLIC PROCEDURE [maxCachePages: CacheIndex] =
    BEGIN
    fileList ← NIL;
    cacheLimit ← maxCachePages;
    fsOps[Alto] ← FileDefs.InitializeAlto[];  -- 'Alto' really means 'local'
    pilotFileSystem ← VMStorage.longTerm.NEW[
      FSInstance ← FSInstance[fsOps[Alto], NIL]];
    pilotFileSystem.instance ← fsOps[Alto].login["Gobbel"L, "Randy"L];
    fsOps[IFS] ← FileDefs.InitializeIFS[];
    END;

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

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


  -- Internal Procedures --

  AddToList: INTERNAL PROCEDURE [fFile: FileDefs.FileHandle, options: OpenOptions]
    RETURNS [newlyEntered: BOOLEAN, vmFile: FileHandle] =
    -- ensures that only a single entry for file 'fFile' appears in the fileList.
    INLINE --only called from OpenFile--
    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: TRUE, openCount: 1]]];
    fileList ← vmFile;
    newlyEntered ← TRUE;
    END;

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

    LockIfLastReference: INTERNAL 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: INTERNAL 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: INTERNAL 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 WaitFileInternal[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: INTERNAL 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.