-- File: IFSFileOpsA.mesa
-- Last edited by Levin:   6-Mar-81 16:39:17

DIRECTORY
  FileDefs USING [
    bytesPerPage, ComparePositions, Completer, defaultTime, FileTime, Position],
  IFSFilePrivate USING [
    DoRead, DoWrite, FileAddressToPosition, FileHandle, fileTimeAddress,
    FileWatcher, FSInstance, GetIORequestBlock, ifsFiles, IFSTIME, IFSTimes,
    InsertFile, IORequest, PositionToFileAddress, PurgeFile, ReleaseFile,
    ValidateFile],
  Inline USING [COPY],
  Leaf USING [
    Answer, closeOp, deleteOp, FileAddress, IfsError, openOp, paramsOp, ptLeaf,
    Request, RequestObject],
  Mopcodes USING [zEXCH],
  Sequin USING [
    Broken, Buffer, Create, Destroy, Get, GetEmptyBuffer, Put, ReleaseBuffer],
  StringDefs USING [WordsForString],
  Time USING [Current],
  VMDefs USING [AccessFailure, Error, OpenOptions, Problem],
  VMStorage USING [shortTerm];

IFSFileOpsA: MONITOR LOCKS file.LOCK USING file: IFSFilePrivate.FileHandle
  IMPORTS
    FileDefs, IFSFilePrivate, Inline, Sequin, StringDefs, Time, VMDefs, VMStorage
  EXPORTS IFSFilePrivate =

  BEGIN OPEN IFSFilePrivate;
  

  -- Types --

  IOSynchRecord: TYPE = RECORD [
    done: BOOLEAN,
    outcome: VMDefs.Problem,
    file: FileHandle,
    finished: CONDITION ← [timeout: 0]];

  IOSynch: TYPE = POINTER TO IOSynchRecord;


  -- Miscellaneous --

  IllegalExtend: ERROR = CODE;
  IllegalTruncate: ERROR = CODE;
  PacketTooSmall: ERROR = CODE;
  

  -- Operations (exported to IFSFilePrivate on behalf of FileDefs) --

  Open: PUBLIC PROCEDURE [
    instance: FSInstance, name: STRING, options: VMDefs.OpenOptions ← oldReadOnly]
    RETURNS [file: FileHandle] =
    BEGIN

    DoOpen: PROCEDURE [file: FileHandle] RETURNS [problem: VMDefs.AccessFailure] =
      BEGIN
      buffer: Sequin.Buffer ← Sequin.GetEmptyBuffer[];
      request: Leaf.Request ← LOOPHOLE[buffer.data];
      openOpOffset: CARDINAL;

      Flush: PROCEDURE =
	BEGIN
	Sequin.Put[file.sequin, buffer];
	buffer ← Sequin.GetEmptyBuffer[];
	request ← LOOPHOLE[buffer.data];
	openOpOffset ← 0;
	END;

      MapError: PROCEDURE [leafError: Leaf.IfsError] RETURNS [VMDefs.AccessFailure] =
	{RETURN[
	  SELECT leafError FROM
	    accessDenied, fileBusy, illegalDIFAccess,
	    IN [userName .. connectPassword] => accessDenied,
	    fileNotFound, dirNotFound => notFound,
	    fileAlreadyExists => alreadyExists,
	    IN [nameMalformed .. nameTooLong] => illegalFileName,
	    ENDCASE => other]};

      file.sequin ← Sequin.Create[dest: instance.serverAddr, pupType: Leaf.ptLeaf];
      request↑ ←
	[Leaf.paramsOp, params[packetDataBytes: buffer.maxBytes,
			  fileLockTimeout: 2*60/5, connectionTimeout: 10*60/5]];
      buffer.nBytes ← openOpOffset ← Leaf.paramsOp.length;
      IF buffer.maxBytes - buffer.nBytes <= Leaf.openOp.length THEN
        Flush[! Sequin.Broken => GO TO serverDead]
      ELSE request ← LOOPHOLE[@buffer.data.words[SIZE[params Leaf.RequestObject]]];
      request↑ ← [Leaf.openOp, open[]];
      BEGIN
      ENABLE PacketTooSmall =>
	{IF openOpOffset ~= 0 THEN {Flush[! Sequin.Broken => GO TO serverDead]; RETRY}};
      fs: FSInstance = file.fs;
      WITH openReq: request SELECT open FROM
	open =>
	  BEGIN
	  IF options ~= oldReadOnly THEN openReq.write ← openReq.extend ← TRUE;
	  SELECT options FROM
	    oldOrNew => openReq.create ← TRUE;
	    new => {openReq.vDefault ← next; openReq.create ← TRUE};
	    ENDCASE;
	  END;
	ENDCASE;
      buffer.nBytes ← buffer.nBytes + Leaf.openOp.length;
      AddStringToBuffer[@buffer, fs.primaryName];
      AddStringToBuffer[@buffer, fs.primaryPassword];
      AddStringToBuffer[@buffer, fs.secondaryName];
      AddStringToBuffer[@buffer, fs.secondaryPassword];
      AddStringToBuffer[@buffer, name];
      END;
      request.op.length ← buffer.nBytes - openOpOffset;
      Sequin.Put[file.sequin, buffer ! Sequin.Broken => GO TO serverDead];
      buffer ← Sequin.Get[file.sequin ! Sequin.Broken => GO TO serverDead];
      BEGIN
      answer: Leaf.Answer ← LOOPHOLE[buffer.data];
      IF answer.op.sense ~= reply THEN {problem ← other; GO TO cantOpen};
      WITH ans: answer SELECT answer.op.type FROM
	error => {problem ← MapError[ans.error]; GO TO cantOpen};
	params => NULL;
	ENDCASE => {problem ← other; GO TO cantOpen};
      IF answer.op.length ~= buffer.nBytes THEN answer ← answer + answer.op.length/2
      ELSE
	BEGIN
	Sequin.ReleaseBuffer[buffer];
	buffer ← Sequin.Get[file.sequin ! Sequin.Broken => GO TO serverDead];
	answer ← LOOPHOLE[buffer.data];
	END;
      IF answer.op.sense ~= reply THEN {problem ← other; GO TO cantOpen};
      WITH ans: answer SELECT answer.op.type FROM
	error => {problem ← MapError[ans.error]; GO TO cantOpen};
	open =>
	  BEGIN
	  problem ← ok;
	  file.leafHandle ← ans.handle;
	  file.length ← FileAddressToPosition[ans.eofAddress];
	  ifsFiles ← ifsFiles + 1;
	  END;
	ENDCASE => {problem ← other; GO TO cantOpen};
      EXITS
	cantOpen => Sequin.Destroy[file.sequin];
      END;
      Sequin.ReleaseBuffer[buffer];
      EXITS
	serverDead => {problem ← io; Sequin.Destroy[file.sequin]};
      END;

    file ← InsertFile[instance, name, DoOpen];
    file.watcher ← FORK FileWatcher[file];
    END;

  Close: PUBLIC PROCEDURE [file: FileHandle] =
    BEGIN
    DoClose: PROCEDURE [file: FileHandle] = {CleanupFile[file, close]};

    ValidateFile[file];
    ReleaseFile[file, DoClose];
    END;

  Abandon: PUBLIC PROCEDURE [file: FileHandle] =
    BEGIN
    DoAbandon: PROCEDURE [file: FileHandle] = {CleanupFile[file, abandon]};

    ValidateFile[file];
    ReleaseFile[file, DoAbandon];
    END;

  Destroy: PUBLIC PROCEDURE [file: FileHandle] =
    BEGIN
    DoDestroy: PROCEDURE [file: FileHandle] = {CleanupFile[file, destroy]};

    ValidateFile[file];
    PurgeFile[file, DoDestroy];
    END;

  GetLength: PUBLIC ENTRY PROCEDURE [file: FileHandle] RETURNS [FileDefs.Position] =
    BEGIN
    RETURN[file.length]
    END;

  SetLength: PUBLIC PROCEDURE [file: FileHandle, length: FileDefs.Position] =
    BEGIN
    oldLength: FileDefs.Position;
    ValidateFile[file];
    oldLength ← GetLength[file];
    SELECT FileDefs.ComparePositions[oldLength, length] FROM
      less => DoIO[file, PrepareSetLength[file, length]];
      equal => NULL;
      greater => Truncate[file, length];
      ENDCASE;
    END;

  Extend: PUBLIC PROCEDURE [file: FileHandle, length: FileDefs.Position, buffer: POINTER] =
    BEGIN
    PrepareExtend: ENTRY PROCEDURE [file: FileHandle]
      RETURNS [request: IORequest] = INLINE
      BEGIN
      request ← GetIORequestBlock[];
      ValidateFile[file];
      request↑ ←
	[buffer: buffer,
    	  address: PositionToFileAddress[[file.length.page, 0], [write[newEOF: TRUE]]],
	  op: write, bytesToGo: ];
      IF FileDefs.ComparePositions[length, file.length] ~= greater THEN GO TO bogus;
      IF length.page = file.length.page THEN {request.bytesToGo ← length.byte}
      ELSE
	BEGIN
	IF ~(length.page = file.length.page + 1 AND length.byte = 0) THEN GO TO bogus;
	request.bytesToGo ← FileDefs.bytesPerPage;
	END;
      file.length ← length;
      EXITS
	bogus => ERROR IllegalExtend;
      END;
    DoIO[file, PrepareExtend[file]];
    END;

  Truncate: PUBLIC PROCEDURE [file: FileHandle, length: FileDefs.Position] =
    BEGIN
    ValidateFile[file];
    SELECT FileDefs.ComparePositions[file.length, length] FROM
      less => ERROR IllegalTruncate;
      equal => NULL;
      greater => DoIO[file, PrepareSetLength[file, length]];
      ENDCASE;
    END;

  GetTimes: PUBLIC PROCEDURE [file: FileHandle]
    RETURNS [read, write, create: FileDefs.FileTime] =
    BEGIN
    times: IFSTimes;
    request: IORequest ← GetIORequestBlock[];
    request↑ ← [buffer: @times, address: fileTimeAddress,
		op: read, bytesToGo: 2*SIZE[IFSTimes]];
    DoIO[file, request];
    RETURN[
      IFSToMesaTime[times.read], IFSToMesaTime[times.write],
      IFSToMesaTime[times.create]]
    END;

  SetCreationTime: PUBLIC PROCEDURE [
    file: FileHandle, create: FileDefs.FileTime ← FileDefs.defaultTime] =
    BEGIN
    creation: IFSTIME;
    request: IORequest ← GetIORequestBlock[];
    IF create = FileDefs.defaultTime THEN create ← Time.Current[];
    creation ← MesaToIFSTime[create];
    request↑ ← [buffer: @creation, address: fileTimeAddress,
		op: write, bytesToGo: 2*SIZE[IFSTIME]];
    DoIO[file, request];
    END;


  -- Other Procedures exported to IFSFilePrivate --

  CopyString: PUBLIC PROCEDURE [s: STRING] RETURNS [ns: STRING] =
    BEGIN
    IF s = NIL THEN RETURN[NIL];
    ns ← VMStorage.shortTerm.NEW[StringBody[s.length]];
    Inline.COPY[from: s, to: ns, nwords: StringDefs.WordsForString[s.length]];
    END;

  AddStringToBuffer: PUBLIC PROCEDURE [buffer: POINTER TO Sequin.Buffer, s: STRING] =
    BEGIN
    startWord: CARDINAL = (buffer.nBytes + 1)/2;
    nWords: CARDINAL;
    IF s = NIL THEN s ← ""L;
    nWords ← (s.length + 1)/2;
    IF buffer.maxBytes < 2*(startWord + nWords) THEN ERROR PacketTooSmall;
    buffer.data.words[startWord] ← s.length;
    Inline.COPY[from: @s.text, to: @buffer.data.words[startWord + 1], nwords: nWords];
    buffer.nBytes ← buffer.nBytes + 2*(nWords + 1);
    END;


  -- Internal Procedures --

  PrepareSetLength: ENTRY PROCEDURE [file: FileHandle, length: FileDefs.Position]
    RETURNS [request: IORequest] =
    BEGIN
    request ← GetIORequestBlock[];
    request↑ ← [buffer: NIL,
	  	address: PositionToFileAddress[length, [write[newEOF: TRUE]]],
		op: write, bytesToGo: 0];
    file.length ← length;
    END;

  CleanupFile: ENTRY PROCEDURE [file: FileHandle, op: {close, destroy, abandon}] =
    BEGIN
    file.state ← closing; NOTIFY file.ioSynch;
    UNTIL file.state = closed DO WAIT file.ioSynch ENDLOOP;
    IF (JOIN file.watcher) AND op ~= abandon THEN
      BEGIN OPEN Sequin;
      buffer: Buffer ← GetEmptyBuffer[];
      request: Leaf.Request ← LOOPHOLE[buffer.data];
      IF op = close THEN
	BEGIN
	request↑ ← [Leaf.closeOp, close[handle: file.leafHandle]];
	buffer.nBytes ← Leaf.closeOp.length;
	END
      ELSE -- op = destroy
	BEGIN
	request↑ ← [Leaf.deleteOp, delete[handle: file.leafHandle]];
	buffer.nBytes ← Leaf.deleteOp.length;
	END;
      Put[file.sequin, buffer ! Broken => GO TO ignore];
      buffer ← Get[file.sequin ! Broken => GO TO ignore];
      ReleaseBuffer[buffer];
      EXITS
	ignore => NULL;
      END;
    Sequin.Destroy[file.sequin];
    VMStorage.shortTerm.FREE[@file.name];
    ifsFiles ← ifsFiles - 1;
    END;

  DoIO: PROCEDURE [file: FileHandle, request: IORequest] =
    BEGIN
    ioSynch: IOSynchRecord ← [done: FALSE, outcome: ok, file: file];
    WaitForCompletion: ENTRY PROCEDURE [file: FileHandle] = INLINE
      {UNTIL ioSynch.done DO WAIT ioSynch.finished; ENDLOOP};
    request.proc ← NoteCompletion; request.arg ← @ioSynch; 
    SELECT request.op FROM
      read => DoRead[file, request];
      write => DoWrite[file, request];
      ENDCASE;
    WaitForCompletion[file];
    IF ioSynch.outcome ~= ok THEN ERROR VMDefs.Error[ioSynch.outcome];
    END;

  NoteCompletion: FileDefs.Completer =
    BEGIN
    ioSynch: IOSynch = LOOPHOLE[arg];
    DoNotify: ENTRY PROCEDURE [file: FileHandle] = INLINE
      BEGIN
      ioSynch.done ← TRUE; ioSynch.outcome ← outcome;
      NOTIFY ioSynch.finished;
      END;
    DoNotify[ioSynch.file];
    END;

  IFSToMesaTime: PROCEDURE [IFSTIME] RETURNS [FileDefs.FileTime] = MACHINE CODE
    BEGIN Mopcodes.zEXCH; END;

  MesaToIFSTime: PROCEDURE [FileDefs.FileTime] RETURNS [IFSTIME] = MACHINE CODE
    BEGIN Mopcodes.zEXCH; END;


  END.