From 9701249ad244cbdfd1c1df88af852be1484ae8e7 Mon Sep 17 00:00:00 2001 From: Norayr Chilingarian Date: Fri, 25 Sep 2026 19:05:52 +0400 Subject: [PATCH] adding ulm unix file and terminal streams. --- make.cmd | 2 + src/library/ulm/ulmSysIO.Mod | 42 +++- src/library/ulm/ulmTerminals.Mod | 309 ++++++++++++++++++++++++ src/library/ulm/ulmUnixFiles.Mod | 235 ++++++++++++++++++ src/library/ulm/ulmUnixTerminals.Mod | 327 ++++++++++++++++++++++++++ src/runtime/Platformunix.Mod | 123 +++++++++- src/runtime/Platformwindows.Mod | 26 +- src/test/ulm/readme.md | 27 +++ src/test/ulm/testUnixFiles.Mod | 45 ++++ src/test/ulm/testUnixTerminalExit.Mod | 9 + src/test/ulm/testUnixTerminals.Mod | 26 ++ src/tools/make/oberon.mk | 3 + 12 files changed, 1163 insertions(+), 11 deletions(-) create mode 100644 src/library/ulm/ulmTerminals.Mod create mode 100644 src/library/ulm/ulmUnixFiles.Mod create mode 100644 src/library/ulm/ulmUnixTerminals.Mod create mode 100644 src/test/ulm/readme.md create mode 100644 src/test/ulm/testUnixFiles.Mod create mode 100644 src/test/ulm/testUnixTerminalExit.Mod create mode 100644 src/test/ulm/testUnixTerminals.Mod diff --git a/make.cmd b/make.cmd index ba2c072f..81902051 100644 --- a/make.cmd +++ b/make.cmd @@ -391,7 +391,9 @@ cd %BUILDDIR%\%MODEL% %ROOTDIR%\%OBECOMP% -Ffs -O%MODEL% ../../../src/library/ulm/ulmResources.Mod || exit /b %ROOTDIR%\%OBECOMP% -Ffs -O%MODEL% ../../../src/library/ulm/ulmForwarders.Mod || exit /b %ROOTDIR%\%OBECOMP% -Ffs -O%MODEL% ../../../src/library/ulm/ulmRelatedEvents.Mod || exit /b +%ROOTDIR%\%OBECOMP% -Ffs -O%MODEL% ../../../src/library/ulm/ulmStreamsHost.Mod || exit /b %ROOTDIR%\%OBECOMP% -Ffs -O%MODEL% ../../../src/library/ulm/ulmStreams.Mod || exit /b +%ROOTDIR%\%OBECOMP% -Ffs -O%MODEL% ../../../src/library/ulm/ulmTerminals.Mod || exit /b %ROOTDIR%\%OBECOMP% -Ffs -O%MODEL% ../../../src/library/ulm/ulmStrings.Mod || exit /b %ROOTDIR%\%OBECOMP% -Ffs -O%MODEL% ../../../src/library/ulm/ulmSysTypes.Mod || exit /b %ROOTDIR%\%OBECOMP% -Ffs -O%MODEL% ../../../src/library/ulm/ulmTexts.Mod || exit /b diff --git a/src/library/ulm/ulmSysIO.Mod b/src/library/ulm/ulmSysIO.Mod index 3274efda..f83f797d 100644 --- a/src/library/ulm/ulmSysIO.Mod +++ b/src/library/ulm/ulmSysIO.Mod @@ -82,6 +82,21 @@ MODULE ulmSysIO; Protection* = Types.Int32; Whence* = Types.Int32; + PROCEDURE StdIn*() : File; + BEGIN + RETURN Platform.StdIn + END StdIn; + + PROCEDURE StdOut*() : File; + BEGIN + RETURN Platform.StdOut + END StdOut; + + PROCEDURE StdErr*() : File; + BEGIN + RETURN Platform.StdErr + END StdErr; + PROCEDURE OpenCreat*(VAR fd: File; filename: ARRAY OF CHAR; options: Types.Set; protection: Protection; @@ -179,8 +194,8 @@ MODULE ulmSysIO; BEGIN interrupted := FALSE; LOOP - error := Platform.Write(fd, buf, cnt); - IF error = 0 THEN RETURN cnt (* todo: Upfate Platform.Write to return actual length written. *) + error := Platform.WriteCount(fd, buf, cnt, byteswritten); + IF error = 0 THEN RETURN byteswritten ELSE IF Platform.Interrupted(error) THEN interrupted := TRUE; @@ -211,6 +226,29 @@ MODULE ulmSysIO; END; END Seek; + PROCEDURE Tell*(fd: File; VAR offset: Count; + errors: RelatedEvents.Object) : BOOLEAN; + VAR error: Platform.ErrorCode; + BEGIN + error := Platform.Tell(fd, offset); + IF error = 0 THEN RETURN TRUE + ELSE + SysErrors.Raise(errors, error, Sys.lseek, ""); + RETURN FALSE + END + END Tell; + + PROCEDURE Isatty*(fd: File) : BOOLEAN; + BEGIN + RETURN Platform.IsConsole(fd) + END Isatty; + + PROCEDURE Seekable*(fd: File) : BOOLEAN; + VAR offset: Count; + BEGIN + RETURN Platform.Tell(fd, offset) = 0 + END Seekable; + (* PROCEDURE Tell*(fd: File; VAR offset: Count; diff --git a/src/library/ulm/ulmTerminals.Mod b/src/library/ulm/ulmTerminals.Mod new file mode 100644 index 00000000..7977d8b3 --- /dev/null +++ b/src/library/ulm/ulmTerminals.Mod @@ -0,0 +1,309 @@ +(* Basic terminal abstraction from Ulm's Oberon Library. *) +MODULE ulmTerminals; + +IMPORT Events := ulmEvents, Objects := ulmObjects, Priorities := ulmPriorities, + RelatedEvents := ulmRelatedEvents, Services := ulmServices, + Streams := ulmStreams; + +CONST + autoleftmargin* = 0; + autorightmargin* = 1; + overstrikes* = 2; + safelastcolumn* = 3; + + cannotSetEcho* = 0; + cannotSetTermMode* = 1; + cannotSetCursor* = 2; + cannotMoveCursor* = 3; + cannotSetAppearance* = 4; + cannotScroll* = 5; + cannotSetScrollRegion* = 6; + cannotClearScreen* = 7; + invalidDirection* = 8; + invalidRegion* = 9; + invalidPosition* = 10; + notSupported* = 11; + errorcodes* = 12; + + on* = 0; + off* = 1; + raw* = 0; + cooked* = 1; + forward* = 0; + reverse* = 1; + visible* = 0; + invisible* = 1; + + setEcho* = 0; + setTermMode* = 1; + setCursor* = 2; + moveCursor* = 3; + setAppearance* = 4; + scroll* = 5; + setScrollRegion* = 6; + clearScreen* = 7; + +TYPE + CapabilitySet* = SET; + EchoMode* = SHORTINT; + TermMode* = SHORTINT; + Direction* = SHORTINT; + Shape* = SHORTINT; + Stream* = POINTER TO StreamRec; + + WindowChangeEvent* = POINTER TO WindowChangeEventRec; + WindowChangeEventRec* = RECORD (Events.EventRec) + stream*: Streams.Stream; + newlines*, newcolumns*: INTEGER + END; + + InterruptEvent* = POINTER TO InterruptEventRec; + InterruptEventRec* = RECORD (Events.EventRec) + stream*: Streams.Stream + END; + + QuitEvent* = POINTER TO QuitEventRec; + QuitEventRec* = RECORD (Events.EventRec) + stream*: Streams.Stream + END; + + HangupEvent* = POINTER TO HangupEventRec; + HangupEventRec* = RECORD (Events.EventRec) + stream*: Streams.Stream + END; + + ErrorEvent* = POINTER TO ErrorEventRec; + ErrorEventRec* = RECORD (Events.EventRec) + errorcode*: SHORTINT + END; + + SetTermModeProc* = PROCEDURE(s: Streams.Stream; mode: TermMode); + SetEchoProc* = PROCEDURE(s: Streams.Stream; mode: EchoMode); + SetCursorProc* = PROCEDURE(s: Streams.Stream; line, column: INTEGER); + MoveCursorProc* = PROCEDURE(s: Streams.Stream; fromline, fromcolumn, + toline, tocolumn: INTEGER); + SetAppearanceProc* = PROCEDURE(s: Streams.Stream; shape: Shape); + ScrollProc* = PROCEDURE(s: Streams.Stream; dir: Direction); + SetScrollRegionProc* = PROCEDURE(s: Streams.Stream; top, bottom: INTEGER); + ClearProc* = PROCEDURE(s: Streams.Stream); + + Interface* = POINTER TO InterfaceRec; + InterfaceRec* = RECORD (Objects.ObjectRec) + setEcho*: SetEchoProc; + setTermMode*: SetTermModeProc; + setCursor*: SetCursorProc; + moveCursor*: MoveCursorProc; + setAppearance*: SetAppearanceProc; + scroll*: ScrollProc; + setScrollRegion*: SetScrollRegionProc; + clearScreen*: ClearProc + END; + + Status* = RECORD (Objects.ObjectRec) + lines*, columns*: INTEGER; + scrtop*, scrbottom*: INTEGER; + echo*: EchoMode; + mode*: TermMode; + characteristics*: SET; + scrollDirections*: SET; + cursorShape*: Shape + END; + + StreamRec* = RECORD (Streams.StreamRec) + status: Status; + caps: CapabilitySet; + interface: Interface + END; + +VAR + console*: Streams.Stream; + windowchanged*, interrupt*, quit*, hangup*: Events.EventType; + error*: Events.EventType; + errormsg*: ARRAY errorcodes OF Events.Message; + terminaltype: Services.Type; + +PROCEDURE Error(object: RelatedEvents.Object; errorcode: SHORTINT); + VAR event: ErrorEvent; +BEGIN + NEW(event); + event.type := error; + event.message := errormsg[errorcode]; + event.errorcode := errorcode; + RelatedEvents.Raise(object, event) +END Error; + +PROCEDURE Init*(s: Stream; status: Status; caps: CapabilitySet; + interface: Interface); +BEGIN + ASSERT(interface # NIL); + s.status := status; + s.caps := caps; + s.interface := interface +END Init; + +PROCEDURE SetScreenSize(event: Events.Event); +BEGIN + WITH event: WindowChangeEvent DO + event.stream(Stream).status.lines := event.newlines; + event.stream(Stream).status.columns := event.newcolumns + END +END SetScreenSize; + +PROCEDURE ClearScreen*(s: Streams.Stream); +BEGIN + WITH s: Stream DO + IF clearScreen IN s.caps THEN s.interface.clearScreen(s) + ELSE Error(s, cannotClearScreen) + END + END +END ClearScreen; + +PROCEDURE Echo*(s: Streams.Stream; mode: EchoMode); +BEGIN + WITH s: Stream DO + IF s.status.echo # mode THEN + IF setEcho IN s.caps THEN + s.interface.setEcho(s, mode); + s.status.echo := mode + ELSE + Error(s, cannotSetEcho) + END + END + END +END Echo; + +PROCEDURE SetTermMode*(s: Streams.Stream; mode: TermMode); +BEGIN + WITH s: Stream DO + IF s.status.mode # mode THEN + IF setTermMode IN s.caps THEN + s.interface.setTermMode(s, mode); + s.status.mode := mode + ELSE + Error(s, cannotSetTermMode) + END + END + END +END SetTermMode; + +PROCEDURE SetCursor*(s: Streams.Stream; line, column: INTEGER); +BEGIN + WITH s: Stream DO + IF (line >= 0) & (line < s.status.lines) & + (column >= 0) & (column < s.status.columns) THEN + IF setCursor IN s.caps THEN s.interface.setCursor(s, line, column) + ELSE Error(s, cannotSetCursor) + END + ELSE + Error(s, invalidPosition) + END + END +END SetCursor; + +PROCEDURE MoveCursor*(s: Streams.Stream; fromline, fromcolumn, + toline, tocolumn: INTEGER); +BEGIN + WITH s: Stream DO + IF (fromline >= 0) & (fromline < s.status.lines) & + (toline >= 0) & (toline < s.status.lines) & + (fromcolumn >= 0) & (fromcolumn <= s.status.columns) & + (tocolumn >= 0) & (tocolumn < s.status.columns) THEN + IF moveCursor IN s.caps THEN + s.interface.moveCursor(s, fromline, fromcolumn, toline, tocolumn) + ELSIF setCursor IN s.caps THEN + s.interface.setCursor(s, toline, tocolumn) + ELSE + Error(s, cannotMoveCursor) + END + ELSE + Error(s, invalidPosition) + END + END +END MoveCursor; + +PROCEDURE CursorAppearance*(s: Streams.Stream; shape: Shape); +BEGIN + WITH s: Stream DO + IF shape # s.status.cursorShape THEN + IF setAppearance IN s.caps THEN + s.interface.setAppearance(s, shape); + s.status.cursorShape := shape + ELSE + Error(s, cannotSetAppearance) + END + END + END +END CursorAppearance; + +PROCEDURE Scroll*(s: Streams.Stream; dir: Direction); +BEGIN + WITH s: Stream DO + IF dir IN s.status.scrollDirections THEN + IF scroll IN s.caps THEN s.interface.scroll(s, dir) + ELSE Error(s, cannotScroll) + END + ELSE + Error(s, invalidDirection) + END + END +END Scroll; + +PROCEDURE SetScrollRegion*(s: Streams.Stream; top, bottom: INTEGER); +BEGIN + WITH s: Stream DO + IF (top >= 0) & (top < s.status.lines) & + (bottom >= 0) & (bottom < s.status.lines) & (top < bottom) THEN + IF setScrollRegion IN s.caps THEN + s.interface.setScrollRegion(s, top, bottom); + s.status.scrtop := top; + s.status.scrbottom := bottom; + IF setCursor IN s.caps THEN SetCursor(s, bottom, 0) END + ELSE + Error(s, cannotSetScrollRegion) + END + ELSE + Error(s, invalidRegion) + END + END +END SetScrollRegion; + +PROCEDURE Capabilities*(s: Streams.Stream): CapabilitySet; +BEGIN + WITH s: Stream DO RETURN s.caps END +END Capabilities; + +PROCEDURE GetStatus*(s: Streams.Stream; VAR status: Status); +BEGIN + WITH s: Stream DO status := s.status END +END GetStatus; + +BEGIN + Services.CreateType(terminaltype, "Terminals.Stream", "Streams.Stream"); + + Events.Define(error); + Events.SetPriority(error, Priorities.liberrors); + errormsg[cannotSetEcho] := "cannot change echo mode"; + errormsg[cannotSetTermMode] := "cannot change terminal mode"; + errormsg[cannotSetCursor] := "cannot set cursor"; + errormsg[cannotMoveCursor] := "cannot move cursor"; + errormsg[cannotSetAppearance] := "cannot change appearance of cursor"; + errormsg[cannotScroll] := "terminal cannot scroll"; + errormsg[cannotSetScrollRegion] := "scroll regions not supported by terminal"; + errormsg[cannotClearScreen] := "cannot clear screen"; + errormsg[invalidDirection] := "direction not valid"; + errormsg[invalidPosition] := "cursor coordinates not valid"; + errormsg[invalidRegion] := "parameters for scroll region not valid"; + errormsg[notSupported] := + "module Terminals not supported by underlying stream implementation"; + + Events.Define(windowchanged); + Events.Define(interrupt); + Events.Define(quit); + Events.Define(hangup); + Events.SetPriority(windowchanged, Priorities.interrupts); + Events.SetPriority(interrupt, Priorities.interrupts); + Events.SetPriority(quit, Priorities.interrupts); + Events.SetPriority(hangup, Priorities.interrupts); + Events.Handler(windowchanged, SetScreenSize); + console := NIL +END ulmTerminals. diff --git a/src/library/ulm/ulmUnixFiles.Mod b/src/library/ulm/ulmUnixFiles.Mod new file mode 100644 index 00000000..bfc4f9df --- /dev/null +++ b/src/library/ulm/ulmUnixFiles.Mod @@ -0,0 +1,235 @@ +(* Platform-backed port of Ulm's UnixFiles stream module. *) +MODULE ulmUnixFiles; + +IMPORT Events := ulmEvents, Priorities := ulmPriorities, + RelatedEvents := ulmRelatedEvents, Services := ulmServices, + Streams := ulmStreams, Sys := ulmSys, SysErrors := ulmSysErrors, + SysIO := ulmSysIO, SysStat := ulmSysStat, SysTypes := ulmSysTypes, + Platform, SYSTEM; + +CONST + illegalMode* = 0; + invalidFd* = 1; + errorcodes* = 2; + + read* = 0; + write* = 1; + rdwr* = 2; + create* = 4; + condcreate* = 8; + +TYPE + ErrorCode* = SHORTINT; + ErrorEvent* = POINTER TO ErrorEventRec; + ErrorEventRec* = RECORD (Events.EventRec) + errorcode*: ErrorCode + END; + + Mode* = SHORTINT; + Stream* = POINTER TO StreamRec; + StreamRec* = RECORD (Streams.StreamRec) + file*: SysTypes.File; + interrupted*: BOOLEAN; + retry*: BOOLEAN + END; + +VAR + error*: Events.EventType; + errormsg*: ARRAY errorcodes OF Events.Message; + interface: Streams.Interface; + type: Services.Type; + +PROCEDURE Error(errors: RelatedEvents.Object; errorcode: ErrorCode); + VAR event: ErrorEvent; +BEGIN + NEW(event); + event.type := error; + event.message := errormsg[errorcode]; + event.errorcode := errorcode; + RelatedEvents.Raise(errors, event) +END Error; + +PROCEDURE ReadBuf(s: Streams.Stream; address: SysTypes.Address; + count: SysTypes.Count): SysTypes.Count; +BEGIN + WITH s: Stream DO + RETURN SysIO.Read(s.file, address, count, s, s.retry, s.interrupted) + END +END ReadBuf; + +PROCEDURE WriteBuf(s: Streams.Stream; address: SysTypes.Address; + count: SysTypes.Count): SysTypes.Count; +BEGIN + WITH s: Stream DO + RETURN SysIO.Write(s.file, address, count, s, s.retry, s.interrupted) + END +END WriteBuf; + +PROCEDURE ReadByte(s: Streams.Stream; VAR byte: Streams.Byte): BOOLEAN; + VAR count: SysTypes.Count; +BEGIN + WITH s: Stream DO + count := SysIO.Read(s.file, SYSTEM.ADR(byte), 1, s, + s.retry, s.interrupted); + RETURN count = 1 + END +END ReadByte; + +PROCEDURE WriteByte(s: Streams.Stream; byte: Streams.Byte): BOOLEAN; + VAR count: SysTypes.Count; +BEGIN + WITH s: Stream DO + count := SysIO.Write(s.file, SYSTEM.ADR(byte), 1, s, + s.retry, s.interrupted); + RETURN count = 1 + END +END WriteByte; + +PROCEDURE Seek(s: Streams.Stream; offset: SysTypes.Count; + whence: Streams.Whence): BOOLEAN; +BEGIN + WITH s: Stream DO + RETURN SysIO.Seek(s.file, offset, whence, s) + END +END Seek; + +PROCEDURE Tell(s: Streams.Stream; VAR offset: SysTypes.Count): BOOLEAN; +BEGIN + WITH s: Stream DO + RETURN SysIO.Tell(s.file, offset, s) + END +END Tell; + +PROCEDURE Close(s: Streams.Stream): BOOLEAN; +BEGIN + WITH s: Stream DO + RETURN SysIO.Close(s.file, s, FALSE, s.interrupted) + END +END Close; + +PROCEDURE OpenFd*(VAR s: Streams.Stream; fd: SysTypes.File; + mode: Mode; bufmode: Streams.BufMode; + errors: RelatedEvents.Object): BOOLEAN; + VAR caps: Streams.CapabilitySet; newfile: Stream; stat: SysStat.StatRec; +BEGIN + IF ~SysStat.Fstat(fd, stat, errors) THEN + Error(errors, invalidFd); + RETURN FALSE + END; + + caps := {Streams.addrio, Streams.close}; + CASE mode OF + | read: INCL(caps, Streams.read) + | write: INCL(caps, Streams.write) + | rdwr: caps := caps + {Streams.read, Streams.write} + ELSE + Error(errors, illegalMode); + RETURN FALSE + END; + + IF SysIO.Seekable(fd) THEN + caps := caps + {Streams.seek, Streams.tell, Streams.holes} + END; + + NEW(newfile); + Services.Init(newfile, type); + Streams.Init(newfile, interface, caps, bufmode); + newfile.file := fd; + newfile.interrupted := FALSE; + newfile.retry := TRUE; + RelatedEvents.QueueEvents(newfile); + s := newfile; + RETURN TRUE +END OpenFd; + +PROCEDURE Open*(VAR s: Streams.Stream; filename: ARRAY OF CHAR; + mode: Mode; bufmode: Streams.BufMode; + errors: RelatedEvents.Object): BOOLEAN; + CONST accessMask = 4; + VAR accessMode, openMode: Mode; access, creation: INTEGER; + fd: SysTypes.File; interrupted: BOOLEAN; hostError: Platform.ErrorCode; +BEGIN + accessMode := mode MOD accessMask; + openMode := mode - accessMode; + + CASE accessMode OF + | read: access := Platform.ReadOnly + | write: access := Platform.WriteOnly + | rdwr: access := Platform.ReadWrite + ELSE + Error(errors, illegalMode); + RETURN FALSE + END; + + CASE openMode OF + | 0: creation := Platform.OpenExisting + | create: creation := Platform.CreateAlways + | condcreate: creation := Platform.OpenAlways + ELSE + Error(errors, illegalMode); + RETURN FALSE + END; + + filename[LEN(filename)-1] := 0X; + REPEAT + hostError := Platform.OpenFile(filename, access, creation, fd) + UNTIL (hostError = 0) OR ~Platform.Interrupted(hostError); + IF hostError # 0 THEN + SysErrors.Raise(errors, hostError, Sys.open, filename); + RETURN FALSE + END; + + IF OpenFd(s, fd, accessMode, bufmode, errors) THEN + RETURN TRUE + END; + IF ~SysIO.Close(fd, errors, FALSE, interrupted) THEN END; + RETURN FALSE +END Open; + +PROCEDURE InitInterface; +BEGIN + NEW(interface); + interface.addrread := ReadBuf; + interface.addrwrite := WriteBuf; + interface.read := ReadByte; + interface.write := WriteByte; + interface.seek := Seek; + interface.tell := Tell; + interface.close := Close +END InitInterface; + +PROCEDURE InitStandardStreams; + PROCEDURE Connect(VAR stream: Streams.Stream; fd: SysTypes.File; mode: Mode); + VAR bufmode: Streams.BufMode; + BEGIN + IF fd = SysIO.StdErr() THEN + bufmode := Streams.nobuf + ELSIF SysIO.Isatty(fd) THEN + bufmode := Streams.linebuf + ELSIF fd = SysIO.StdOut() THEN + (* ULM process termination is not wired into VOC program shutdown. *) + bufmode := Streams.nobuf + ELSE + bufmode := Streams.onebuf + END; + IF ~OpenFd(stream, fd, mode, bufmode, NIL) THEN END + END Connect; +BEGIN + Connect(Streams.stdin, SysIO.StdIn(), rdwr); + Connect(Streams.stdout, SysIO.StdOut(), rdwr); + Connect(Streams.stderr, SysIO.StdErr(), write); + IF (Streams.GetBufMode(Streams.stdin) = Streams.linebuf) & + (Streams.GetBufMode(Streams.stdout) = Streams.linebuf) THEN + Streams.Tie(Streams.stdin, Streams.stdout) + END +END InitStandardStreams; + +BEGIN + errormsg[illegalMode] := "illegal opening mode"; + errormsg[invalidFd] := "invalid file descriptor"; + Events.Define(error); + Events.SetPriority(error, Priorities.liberrors); + InitInterface; + Services.CreateType(type, "UnixFiles.Stream", "Streams.Stream"); + InitStandardStreams +END ulmUnixFiles. diff --git a/src/library/ulm/ulmUnixTerminals.Mod b/src/library/ulm/ulmUnixTerminals.Mod new file mode 100644 index 00000000..03361e6d --- /dev/null +++ b/src/library/ulm/ulmUnixTerminals.Mod @@ -0,0 +1,327 @@ +(* Synchronous Unix terminal streams backed by Platform termios operations. *) +MODULE ulmUnixTerminals; + +IMPORT Heap, SYSTEM, Forwarders := ulmForwarders, + RelatedEvents := ulmRelatedEvents, Services := ulmServices, + Streams := ulmStreams, Sys := ulmSys, SysErrors := ulmSysErrors, + Terminals := ulmTerminals, UnixFiles := ulmUnixFiles, Platform; + +TYPE + Stream = POINTER TO StreamRec; + StreamRec = RECORD (Terminals.StreamRec) + instream, outstream: Streams.Stream; + inputState, outputState: Platform.TerminalState; + inputHandle, outputHandle: Platform.FileHandle; + owned: Streams.Stream + END; + +VAR + streamType: Services.Type; + streamInterface: Streams.Interface; + terminalInterface: Terminals.Interface; + +PROCEDURE RaiseHostError(s: Streams.Stream; error: Platform.ErrorCode); +BEGIN + IF error # 0 THEN SysErrors.Raise(s, error, Sys.ioctl, "") END +END RaiseHostError; + +PROCEDURE ReadByte(s: Streams.Stream; VAR byte: Streams.Byte): BOOLEAN; +BEGIN + WITH s: Stream DO RETURN Streams.ReadByte(s.instream, byte) END +END ReadByte; + +PROCEDURE WriteByte(s: Streams.Stream; byte: Streams.Byte): BOOLEAN; +BEGIN + WITH s: Stream DO RETURN Streams.WriteByte(s.outstream, byte) END +END WriteByte; + +PROCEDURE Flush(s: Streams.Stream): BOOLEAN; +BEGIN + WITH s: Stream DO RETURN Streams.Flush(s.outstream) END +END Flush; + +PROCEDURE RestoreState(handle: Platform.FileHandle; + VAR state: Platform.TerminalState): Platform.ErrorCode; + VAR error: Platform.ErrorCode; +BEGIN + IF state = 0 THEN RETURN 0 END; + REPEAT + error := Platform.RestoreTerminalState(handle, state) + UNTIL (error = 0) OR ~Platform.Interrupted(error); + IF error = 0 THEN Platform.ReleaseTerminalState(state) END; + RETURN error +END RestoreState; + +PROCEDURE Restore(s: Stream; report: BOOLEAN): BOOLEAN; + VAR ok: BOOLEAN; error: Platform.ErrorCode; +BEGIN + ok := TRUE; + error := RestoreState(s.outputHandle, s.outputState); + IF error # 0 THEN + IF report THEN RaiseHostError(s, error) END; + ok := FALSE + END; + error := RestoreState(s.inputHandle, s.inputState); + IF error # 0 THEN + IF report THEN RaiseHostError(s, error) END; + ok := FALSE + END; + RETURN ok +END Restore; + +PROCEDURE Close(s: Streams.Stream): BOOLEAN; + VAR ok: BOOLEAN; +BEGIN + WITH s: Stream DO + ok := Restore(s, TRUE); + IF ok & (s.owned # NIL) THEN + ok := Streams.Close(s.owned); + IF ok THEN s.owned := NIL END + END; + RETURN ok + END +END Close; + +PROCEDURE Finalize(object: SYSTEM.PTR); + VAR s: Stream; +BEGIN + s := SYSTEM.VAL(Stream, object); + IF ~Restore(s, FALSE) THEN + Platform.ReleaseTerminalState(s.outputState); + Platform.ReleaseTerminalState(s.inputState) + END; + IF s.owned # NIL THEN + IF Streams.Close(s.owned) THEN END; + s.owned := NIL + END +END Finalize; + +PROCEDURE SetEcho(s: Streams.Stream; mode: Terminals.EchoMode); + VAR error: Platform.ErrorCode; +BEGIN + WITH s: Stream DO + IF s.inputState # 0 THEN + REPEAT + error := Platform.SetTerminalEcho(s.inputHandle, mode = Terminals.on) + UNTIL (error = 0) OR ~Platform.Interrupted(error); + RaiseHostError(s, error) + END + END +END SetEcho; + +PROCEDURE SetTermMode(s: Streams.Stream; mode: Terminals.TermMode); + VAR error: Platform.ErrorCode; raw: BOOLEAN; +BEGIN + WITH s: Stream DO + raw := mode = Terminals.raw; + IF s.inputState # 0 THEN + REPEAT + error := Platform.SetTerminalInputMode(s.inputHandle, raw) + UNTIL (error = 0) OR ~Platform.Interrupted(error); + RaiseHostError(s, error) + END; + IF s.outputState # 0 THEN + IF ~Streams.Flush(s.outstream) THEN END; + REPEAT + error := Platform.SetTerminalOutputMode(s.outputHandle, raw) + UNTIL (error = 0) OR ~Platform.Interrupted(error); + RaiseHostError(s, error) + END + END +END SetTermMode; + +PROCEDURE Open*(VAR s: Streams.Stream; instream, outstream: Streams.Stream; + tiname: ARRAY OF CHAR; + errors: RelatedEvents.Object): BOOLEAN; + VAR newterm: Stream; status: Terminals.Status; + caps: Terminals.CapabilitySet; streamCaps: Streams.CapabilitySet; + hostError: Platform.ErrorCode; +BEGIN + IF (instream = NIL) OR (outstream = NIL) THEN RETURN FALSE END; + streamCaps := Streams.Capabilities(instream); + IF ~(Streams.read IN streamCaps) THEN RETURN FALSE END; + streamCaps := Streams.Capabilities(outstream); + IF ~(Streams.write IN streamCaps) THEN RETURN FALSE END; + + NEW(newterm); + newterm.instream := instream; + newterm.outstream := outstream; + newterm.inputState := 0; + newterm.outputState := 0; + newterm.owned := NIL; + caps := {}; + + IF instream IS UnixFiles.Stream THEN + newterm.inputHandle := instream(UnixFiles.Stream).file; + IF Platform.IsConsole(newterm.inputHandle) THEN + hostError := Platform.CaptureTerminalState(newterm.inputHandle, + newterm.inputState); + IF hostError # 0 THEN + SysErrors.Raise(errors, hostError, Sys.ioctl, ""); + RETURN FALSE + END; + INCL(caps, Terminals.setEcho); + INCL(caps, Terminals.setTermMode) + END + END; + + IF outstream IS UnixFiles.Stream THEN + newterm.outputHandle := outstream(UnixFiles.Stream).file; + IF Platform.IsConsole(newterm.outputHandle) THEN + hostError := Platform.CaptureTerminalState(newterm.outputHandle, + newterm.outputState); + IF hostError # 0 THEN + IF newterm.inputState # 0 THEN + Platform.ReleaseTerminalState(newterm.inputState) + END; + SysErrors.Raise(errors, hostError, Sys.ioctl, ""); + RETURN FALSE + END; + INCL(caps, Terminals.setTermMode) + END + END; + + IF caps = {} THEN RETURN FALSE END; + + IF newterm.inputState # 0 THEN + hostError := Platform.SetTerminalEcho(newterm.inputHandle, TRUE); + IF hostError = 0 THEN + hostError := Platform.SetTerminalInputMode(newterm.inputHandle, FALSE) + END + ELSE + hostError := 0 + END; + IF (hostError = 0) & (newterm.outputState # 0) THEN + IF Streams.Flush(outstream) THEN + hostError := Platform.SetTerminalOutputMode(newterm.outputHandle, FALSE) + ELSE + IF ~Restore(newterm, FALSE) THEN END; + RETURN FALSE + END + END; + IF hostError # 0 THEN + IF ~Restore(newterm, FALSE) THEN END; + SysErrors.Raise(errors, hostError, Sys.ioctl, ""); + RETURN FALSE + END; + + status.lines := 24; + status.columns := 80; + IF newterm.outputState # 0 THEN + hostError := Platform.GetTerminalSize(newterm.outputHandle, + status.lines, status.columns) + ELSE + hostError := Platform.GetTerminalSize(newterm.inputHandle, + status.lines, status.columns) + END; + IF (hostError # 0) OR (status.lines <= 0) OR (status.columns <= 0) THEN + status.lines := 24; + status.columns := 80 + END; + status.scrtop := 0; + status.scrbottom := status.lines - 1; + status.echo := Terminals.on; + status.mode := Terminals.cooked; + status.characteristics := {}; + status.scrollDirections := {}; + status.cursorShape := Terminals.visible; + + Services.Init(newterm, streamType); + Streams.Init(newterm, streamInterface, + {Streams.read, Streams.write, Streams.flush, Streams.close}, + Streams.nobuf); + Terminals.Init(newterm, status, caps, terminalInterface); + RelatedEvents.QueueEvents(newterm); + RelatedEvents.Forward(instream, newterm); + Forwarders.Forward(newterm, outstream); + Heap.RegisterFinalizer(newterm, Finalize); + s := newterm; + RETURN TRUE +END Open; + +PROCEDURE OpenByName*(VAR s: Streams.Stream; devicename, tiname: ARRAY OF CHAR; + errors: RelatedEvents.Object): BOOLEAN; + VAR name: ARRAY 1024 OF CHAR; file: Streams.Stream; +BEGIN + IF devicename[0] = 0X THEN COPY("/dev/tty", name) + ELSE COPY(devicename, name) + END; + IF ~UnixFiles.Open(file, name, UnixFiles.rdwr, Streams.nobuf, errors) THEN + RETURN FALSE + END; + IF Open(s, file, file, tiname, errors) THEN + s(Stream).owned := file; + RETURN TRUE + END; + IF ~Streams.Close(file) THEN END; + RETURN FALSE +END OpenByName; + +PROCEDURE InitInterfaces; +BEGIN + NEW(streamInterface); + streamInterface.read := ReadByte; + streamInterface.write := WriteByte; + streamInterface.flush := Flush; + streamInterface.close := Close; + + NEW(terminalInterface); + terminalInterface.setEcho := SetEcho; + terminalInterface.setTermMode := SetTermMode +END InitInterfaces; + +PROCEDURE OpenConsole; + VAR console, oldout, file: Streams.Stream; + name, tiname: ARRAY 16 OF CHAR; handle: Platform.FileHandle; + hostError: Platform.ErrorCode; opened: BOOLEAN; +BEGIN + oldout := Streams.stdout; + opened := FALSE; + IF (Streams.stdin IS UnixFiles.Stream) & + (Streams.stdout IS UnixFiles.Stream) THEN + IF Platform.IsConsole(Streams.stdin(UnixFiles.Stream).file) & + Platform.IsConsole(Streams.stdout(UnixFiles.Stream).file) & + Platform.SameTerminal(Streams.stdin(UnixFiles.Stream).file, + Streams.stdout(UnixFiles.Stream).file) THEN + opened := Open(console, Streams.stdin, Streams.stdout, "", + RelatedEvents.null) + END + END; + + IF ~opened THEN + COPY("/dev/tty", name); + REPEAT + hostError := Platform.OpenFile(name, Platform.ReadWrite, + Platform.OpenExisting, handle) + UNTIL (hostError = 0) OR ~Platform.Interrupted(hostError); + IF hostError = 0 THEN + IF UnixFiles.OpenFd(file, handle, UnixFiles.rdwr, Streams.nobuf, + RelatedEvents.null) THEN + IF Open(console, file, file, "", RelatedEvents.null) THEN + console(Stream).owned := file; + opened := TRUE + ELSE + IF ~Streams.Close(file) THEN END + END + ELSE + IF Platform.Close(handle) # 0 THEN END + END + END + END; + + IF opened THEN + Terminals.console := console; + IF oldout IS UnixFiles.Stream THEN + IF Platform.SameTerminal(oldout(UnixFiles.Stream).file, + console(Stream).outputHandle) THEN + Streams.stdout := console + END + END + END +END OpenConsole; + +BEGIN + Services.CreateType(streamType, "UnixTerminals.Stream", "Terminals.Stream"); + InitInterfaces; + OpenConsole +END ulmUnixTerminals. diff --git a/src/runtime/Platformunix.Mod b/src/runtime/Platformunix.Mod index 0fc15bff..4a88fe5d 100644 --- a/src/runtime/Platformunix.Mod +++ b/src/runtime/Platformunix.Mod @@ -6,11 +6,19 @@ CONST StdOut- = 1; StdErr- = 2; + ReadOnly* = 0; + WriteOnly* = 1; + ReadWrite* = 2; + OpenExisting* = 0; + CreateAlways* = 1; + OpenAlways* = 2; + TYPE SignalHandler = PROCEDURE(signal: SYSTEM.INT32); ErrorCode* = INTEGER; FileHandle* = LONGINT; + TerminalState* = SYSTEM.ADDRESS; FileIdentity* = RECORD volume: LONGINT; (* dev on Unix filesystems, volume serial number on NTFS *) @@ -42,6 +50,8 @@ PROCEDURE -Aincludesysstat '#include '; PROCEDURE -Aincludefcntl '#include '; PROCEDURE -Aincludeerrno '#include '; PROCEDURE -Aincludeutime '#include '; +PROCEDURE -Aincludetermios '#include '; +PROCEDURE -Aincludesysioctl '#include '; PROCEDURE -Astdlib '#include '; PROCEDURE -Astdio '#include '; PROCEDURE -Aerrno '#include '; @@ -229,6 +239,8 @@ PROCEDURE Error*(): ErrorCode; BEGIN RETURN err() END Error; PROCEDURE -openrw (n: ARRAY OF CHAR): INTEGER "open((char*)n, O_RDWR)"; PROCEDURE -openro (n: ARRAY OF CHAR): INTEGER "open((char*)n, O_RDONLY)"; PROCEDURE -opennew(n: ARRAY OF CHAR): INTEGER "open((char*)n, O_CREAT | O_TRUNC | O_RDWR, 0664)"; +PROCEDURE -openfile(n: ARRAY OF CHAR; access, creation: INTEGER): LONGINT +"open((char*)n, (access == 0 ? O_RDONLY : access == 1 ? O_WRONLY : O_RDWR) | (creation == 1 ? O_CREAT | O_TRUNC : creation == 2 ? O_CREAT : 0), 0666)"; (* File APIs *) @@ -253,6 +265,14 @@ BEGIN IF (fd < 0) THEN RETURN err() ELSE h := fd; RETURN 0 END; END New; +PROCEDURE OpenFile*(VAR n: ARRAY OF CHAR; access, creation: INTEGER; + VAR h: FileHandle): ErrorCode; +VAR fd: LONGINT; +BEGIN + fd := openfile(n, access, creation); + IF fd < 0 THEN RETURN err() ELSE h := fd; RETURN 0 END +END OpenFile; + PROCEDURE -closefile(fd: LONGINT): INTEGER "close(fd)"; @@ -295,6 +315,86 @@ PROCEDURE -isatty(fd: LONGINT): INTEGER "isatty(fd)"; PROCEDURE IsConsole*(h: FileHandle): BOOLEAN; BEGIN RETURN isatty(h) # 0 END IsConsole; +PROCEDURE -captureTerminalState(h: FileHandle; + VAR state: TerminalState): INTEGER +"({ struct termios *p = malloc(sizeof(*p)); int rc = 0; if (p == 0) { errno = ENOMEM; rc = -1; } else if (tcgetattr(h, p) < 0) { free(p); rc = -1; } else *state = (ADDRESS)p; rc; })"; + +PROCEDURE CaptureTerminalState*(h: FileHandle; + VAR state: TerminalState): ErrorCode; +BEGIN + state := 0; + IF captureTerminalState(h, state) < 0 THEN RETURN err() ELSE RETURN 0 END +END CaptureTerminalState; + +PROCEDURE -restoreTerminalState(h: FileHandle; state: TerminalState): INTEGER +"tcsetattr(h, TCSANOW, (struct termios*)(ADDRESS)state)"; + +PROCEDURE RestoreTerminalState*(h: FileHandle; + state: TerminalState): ErrorCode; +BEGIN + IF restoreTerminalState(h, state) < 0 THEN RETURN err() ELSE RETURN 0 END +END RestoreTerminalState; + +PROCEDURE -releaseTerminalState(state: TerminalState) +"free((void*)(ADDRESS)state)"; + +PROCEDURE ReleaseTerminalState*(VAR state: TerminalState); +BEGIN + IF state # 0 THEN releaseTerminalState(state); state := 0 END +END ReleaseTerminalState; + +PROCEDURE -setTerminalEcho(h: FileHandle; enabled: INTEGER): INTEGER +"({ struct termios t; int rc = tcgetattr(h, &t); if (rc == 0) { if (enabled) t.c_lflag |= ECHO; else t.c_lflag &= ~ECHO; rc = tcsetattr(h, TCSANOW, &t); } rc; })"; + +PROCEDURE SetTerminalEcho*(h: FileHandle; enabled: BOOLEAN): ErrorCode; + VAR value: INTEGER; +BEGIN + IF enabled THEN value := 1 ELSE value := 0 END; + IF setTerminalEcho(h, value) < 0 THEN RETURN err() ELSE RETURN 0 END +END SetTerminalEcho; + +PROCEDURE -setTerminalInputRaw(h: FileHandle): INTEGER +"({ struct termios t; int rc = tcgetattr(h, &t); if (rc == 0) { t.c_iflag &= ~ICRNL; t.c_lflag &= ~ICANON; t.c_cc[VMIN] = 1; t.c_cc[VTIME] = 0; rc = tcsetattr(h, TCSANOW, &t); } rc; })"; + +PROCEDURE -setTerminalInputCooked(h: FileHandle): INTEGER +"({ struct termios t; int rc = tcgetattr(h, &t); if (rc == 0) { t.c_iflag |= ICRNL; t.c_lflag |= ICANON; t.c_cc[VEOF] = 4; t.c_cc[VEOL] = '\n'; rc = tcsetattr(h, TCSANOW, &t); } rc; })"; + +PROCEDURE SetTerminalInputMode*(h: FileHandle; raw: BOOLEAN): ErrorCode; + VAR result: INTEGER; +BEGIN + IF raw THEN result := setTerminalInputRaw(h) + ELSE result := setTerminalInputCooked(h) + END; + IF result < 0 THEN RETURN err() ELSE RETURN 0 END +END SetTerminalInputMode; + +PROCEDURE -setTerminalOutputMode(h: FileHandle; raw: INTEGER): INTEGER +"({ struct termios t; int rc = tcgetattr(h, &t); if (rc == 0) { if (raw) t.c_oflag &= ~(OPOST | OCRNL | ONLCR); else t.c_oflag |= OPOST | OCRNL | ONLCR; rc = tcsetattr(h, TCSANOW, &t); } rc; })"; + +PROCEDURE SetTerminalOutputMode*(h: FileHandle; raw: BOOLEAN): ErrorCode; + VAR value: INTEGER; +BEGIN + IF raw THEN value := 1 ELSE value := 0 END; + IF setTerminalOutputMode(h, value) < 0 THEN RETURN err() ELSE RETURN 0 END +END SetTerminalOutputMode; + +PROCEDURE -getTerminalSize(h: FileHandle; VAR lines, columns: INTEGER): INTEGER +"({ struct winsize ws; int rc = ioctl(h, TIOCGWINSZ, &ws); if (rc == 0) { *lines = ws.ws_row; *columns = ws.ws_col; } rc; })"; + +PROCEDURE GetTerminalSize*(h: FileHandle; + VAR lines, columns: INTEGER): ErrorCode; +BEGIN + IF getTerminalSize(h, lines, columns) < 0 THEN RETURN err() ELSE RETURN 0 END +END GetTerminalSize; + +PROCEDURE -sameTerminal(first, second: FileHandle): INTEGER +"({ struct stat a, b; int same = 0; if (fstat(first, &a) == 0 && fstat(second, &b) == 0) same = S_ISCHR(a.st_mode) && S_ISCHR(b.st_mode) && a.st_rdev == b.st_rdev; same; })"; + +PROCEDURE SameTerminal*(first, second: FileHandle): BOOLEAN; +BEGIN + RETURN sameTerminal(first, second) # 0 +END SameTerminal; + PROCEDURE -fstat(fd: LONGINT): INTEGER "fstat(fd, &s)"; @@ -375,11 +475,22 @@ END ReadBuf; PROCEDURE -writefile(fd: LONGINT; p: SYSTEM.ADDRESS; l: LONGINT): SYSTEM.ADDRESS "write(fd, (void*)(ADDRESS)(p), l)"; -PROCEDURE Write*(h: FileHandle; p: SYSTEM.ADDRESS; l: LONGINT): ErrorCode; +PROCEDURE -writecount(value: SYSTEM.ADDRESS): LONGINT "(LONGINT)value"; + +PROCEDURE WriteCount*(h: FileHandle; p: SYSTEM.ADDRESS; l: LONGINT; + VAR n: LONGINT): ErrorCode; VAR written: SYSTEM.ADDRESS; BEGIN written := writefile(h, p, l); - IF written < 0 THEN RETURN err() ELSE RETURN 0 END + IF written < 0 THEN n := 0; RETURN err() END; + n := writecount(written); + RETURN 0 +END WriteCount; + +PROCEDURE Write*(h: FileHandle; p: SYSTEM.ADDRESS; l: LONGINT): ErrorCode; + VAR written: LONGINT; +BEGIN + RETURN WriteCount(h, p, l, written) END Write; @@ -393,7 +504,7 @@ END Sync; -PROCEDURE -lseek(fd: LONGINT; o: LONGINT; w: INTEGER): INTEGER "lseek(fd, o, w)"; +PROCEDURE -lseek(fd: LONGINT; o: LONGINT; w: INTEGER): LONGINT "lseek(fd, o, w)"; PROCEDURE -seekset(): INTEGER "SEEK_SET"; PROCEDURE -seekcur(): INTEGER "SEEK_CUR"; PROCEDURE -seekend(): INTEGER "SEEK_END"; @@ -403,6 +514,12 @@ BEGIN IF lseek(h, offset, whence) < 0 THEN RETURN err() ELSE RETURN 0 END END Seek; +PROCEDURE Tell*(h: FileHandle; VAR offset: LONGINT): ErrorCode; +BEGIN + offset := lseek(h, 0, seekcur()); + IF offset < 0 THEN offset := 0; RETURN err() ELSE RETURN 0 END +END Tell; + PROCEDURE -ftruncate(fd: LONGINT; l: LONGINT): INTEGER "ftruncate(fd, l)"; diff --git a/src/runtime/Platformwindows.Mod b/src/runtime/Platformwindows.Mod index 3d2f7729..5ae1097d 100644 --- a/src/runtime/Platformwindows.Mod +++ b/src/runtime/Platformwindows.Mod @@ -412,10 +412,18 @@ END ReadBuf; PROCEDURE -writefile(fd: FileHandle; p: SYSTEM.ADDRESS; l: LONGINT; VAR n: SYSTEM.INT32): INTEGER "(INTEGER)WriteFile((HANDLE)fd, (void*)(p), (DWORD)l, (DWORD*)n, 0)"; -PROCEDURE Write*(h: FileHandle; p: SYSTEM.ADDRESS; l: LONGINT): ErrorCode; -VAR n: SYSTEM.INT32; +PROCEDURE WriteCount*(h: FileHandle; p: SYSTEM.ADDRESS; l: LONGINT; + VAR n: LONGINT): ErrorCode; +VAR result: INTEGER; written: SYSTEM.INT32; BEGIN - IF writefile(h, p, l, n) = 0 THEN RETURN err() ELSE RETURN 0 END + result := writefile(h, p, l, written); + IF result = 0 THEN n := 0; RETURN err() ELSE n := written; RETURN 0 END +END WriteCount; + +PROCEDURE Write*(h: FileHandle; p: SYSTEM.ADDRESS; l: LONGINT): ErrorCode; +VAR n: LONGINT; +BEGIN + RETURN WriteCount(h, p, l, n) END Write; @@ -444,12 +452,18 @@ BEGIN IF rc = 0 THEN RETURN err() ELSE RETURN 0 END END Seek; - - PROCEDURE -setEndOfFile(h: FileHandle): INTEGER "(INTEGER)SetEndOfFile((HANDLE)h)"; PROCEDURE -getFilePos(h: FileHandle; VAR r: LONGINT; VAR rc: INTEGER) "LARGE_INTEGER liz = {0}; *rc = (INTEGER)SetFilePointerEx((HANDLE)h, liz, &li, FILE_CURRENT); *r = (LONGINT)li.QuadPart"; +PROCEDURE Tell*(h: FileHandle; VAR offset: LONGINT): ErrorCode; +VAR rc: INTEGER; +BEGIN + largeInteger; + getFilePos(h, offset, rc); + IF rc = 0 THEN offset := 0; RETURN err() ELSE RETURN 0 END +END Tell; + PROCEDURE Truncate*(h: FileHandle; limit: LONGINT): ErrorCode; VAR rc: INTEGER; oldpos: LONGINT; BEGIN @@ -516,7 +530,7 @@ END EnableVT100; PROCEDURE IsConsole*(h: FileHandle): BOOLEAN; VAR mode: SYSTEM.INT32; -BEGIN RETURN GetConsoleMode(StdOut, mode) +BEGIN RETURN GetConsoleMode(h, mode) END IsConsole; diff --git a/src/test/ulm/readme.md b/src/test/ulm/readme.md new file mode 100644 index 00000000..42002128 --- /dev/null +++ b/src/test/ulm/readme.md @@ -0,0 +1,27 @@ +# ULM Unix smoke tests + +Build the tests with an installed VOC compiler: + +```sh +voc -M testUnixFiles.Mod +voc -M testUnixTerminals.Mod +voc -M testUnixTerminalExit.Mod +``` + +Run the file and explicit terminal-close tests normally from this directory: + +```sh +./testUnixFiles +./testUnixTerminals +``` + +The terminal tests require standard input and output to refer to the same +terminal. To verify restoration during program shutdown: + +```sh +before=$(stty -g) +./testUnixTerminalExit +test "$before" = "$(stty -g)" +``` + +`testUnixFiles` leaves `ulmUnixFiles.test` in the current directory. diff --git a/src/test/ulm/testUnixFiles.Mod b/src/test/ulm/testUnixFiles.Mod new file mode 100644 index 00000000..23d1ebe3 --- /dev/null +++ b/src/test/ulm/testUnixFiles.Mod @@ -0,0 +1,45 @@ +MODULE testUnixFiles; + +IMPORT Streams := ulmStreams, UnixFiles := ulmUnixFiles, Write := ulmWrite, + SYSTEM; + +VAR + stream: Streams.Stream; + filename: ARRAY 64 OF CHAR; + written, received: ARRAY 5 OF CHAR; + byte: Streams.Byte; + position: Streams.Count; + +BEGIN + ASSERT(Streams.stdin IS UnixFiles.Stream); + ASSERT(Streams.stdout IS UnixFiles.Stream); + ASSERT(Streams.stderr IS UnixFiles.Stream); + + COPY("ulmUnixFiles.test", filename); + COPY("test", written); + ASSERT(UnixFiles.Open(stream, filename, UnixFiles.rdwr + UnixFiles.create, + Streams.onebuf, NIL)); + ASSERT(Streams.WritePart(stream, written, 0, 4)); + Streams.GetPos(stream, position); + ASSERT(position = 4); + ASSERT(Streams.Close(stream)); + + COPY("ulmUnixFiles.test", filename); + ASSERT(UnixFiles.Open(stream, filename, + UnixFiles.read + UnixFiles.condcreate, + Streams.onebuf, NIL)); + ASSERT(Streams.ReadPart(stream, received, 0, 4)); + ASSERT((received[0] = "t") & (received[1] = "e") & + (received[2] = "s") & (received[3] = "t")); + ASSERT(Streams.Close(stream)); + + COPY("ulmUnixFiles.test", filename); + ASSERT(UnixFiles.Open(stream, filename, UnixFiles.read, + Streams.nobuf, NIL)); + ASSERT(Streams.ReadByte(stream, byte)); + ASSERT(SYSTEM.VAL(CHAR, byte) = "t"); + ASSERT(Streams.Close(stream)); + + Write.String("ulmUnixFiles ok"); + Write.Ln +END testUnixFiles. diff --git a/src/test/ulm/testUnixTerminalExit.Mod b/src/test/ulm/testUnixTerminalExit.Mod new file mode 100644 index 00000000..0cd105b9 --- /dev/null +++ b/src/test/ulm/testUnixTerminalExit.Mod @@ -0,0 +1,9 @@ +MODULE testUnixTerminalExit; + +IMPORT Terminals := ulmTerminals, UnixTerminals := ulmUnixTerminals; + +BEGIN + ASSERT(Terminals.console # NIL); + Terminals.Echo(Terminals.console, Terminals.off); + Terminals.SetTermMode(Terminals.console, Terminals.raw) +END testUnixTerminalExit. diff --git a/src/test/ulm/testUnixTerminals.Mod b/src/test/ulm/testUnixTerminals.Mod new file mode 100644 index 00000000..e9691605 --- /dev/null +++ b/src/test/ulm/testUnixTerminals.Mod @@ -0,0 +1,26 @@ +MODULE testUnixTerminals; + +IMPORT Streams := ulmStreams, Terminals := ulmTerminals, + UnixTerminals := ulmUnixTerminals, Write := ulmWrite; + +VAR status: Terminals.Status; caps: Terminals.CapabilitySet; + +BEGIN + ASSERT(Terminals.console # NIL); + ASSERT(Streams.stdout = Terminals.console); + caps := Terminals.Capabilities(Terminals.console); + ASSERT(Terminals.setEcho IN caps); + ASSERT(Terminals.setTermMode IN caps); + + Terminals.GetStatus(Terminals.console, status); + ASSERT((status.lines > 0) & (status.columns > 0)); + + Terminals.Echo(Terminals.console, Terminals.off); + Terminals.Echo(Terminals.console, Terminals.on); + Terminals.SetTermMode(Terminals.console, Terminals.raw); + Terminals.SetTermMode(Terminals.console, Terminals.cooked); + + Write.String("ulmUnixTerminals ok"); + Write.Ln; + ASSERT(Streams.Close(Terminals.console)) +END testUnixTerminals. diff --git a/src/tools/make/oberon.mk b/src/tools/make/oberon.mk index e56bb9f9..182c7861 100644 --- a/src/tools/make/oberon.mk +++ b/src/tools/make/oberon.mk @@ -316,6 +316,7 @@ ulm: cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmRelatedEvents.Mod cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmStreamsHost.Mod cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmStreams.Mod + cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmTerminals.Mod cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmStrings.Mod cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmSysTypes.Mod cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmTexts.Mod @@ -337,6 +338,8 @@ ulm: cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmConstStrings.Mod cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmPlotters.Mod cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmSysIO.Mod + if [ "$(PLATFORM)" = "unix" ]; then cd $(BUILDDIR)/$(MODEL) && "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmUnixFiles.Mod; fi + if [ "$(PLATFORM)" = "unix" ]; then cd $(BUILDDIR)/$(MODEL) && "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmUnixTerminals.Mod; fi cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmLoader.Mod cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmNetIO.Mod cd $(BUILDDIR)/$(MODEL); "$(ROOTDIR)/$(OBECOMP)" -Fs -O$(MODEL) ../../../src/library/ulm/ulmPersistentObjects.Mod