1 Introduction
2 Ground Rules

Building a File System
3 File Systems
4 File Content Data Structure
5 Allocation Cluster Manager
6 Exceptions and Emancipation
7 Base Classes, Testing, and More
8 File Meta Data
9 Native File Class
10 Our File System
11 Allocation Table
12 File System Support Code
13 Initializing the File System
14 Contiguous Files
15 Rebuilding the File System
16 Native File System Support Methods
17 Lookups, Wildcards, and Unicode, Oh My
18 Finishing the File System Class

The Init Program
19 Hardware Abstraction and UOS Architecture
20 Init Command Mode
21 Using Our File System
22 Hardware and Device Lists
23 Fun with Stores: Partitions
24 Fun with Stores: RAID
25 Fun with Stores: RAM Disks
26 Init wrap-up

The Executive
27 Overview of The Executive
28 Starting the Kernel
29 The Kernel
30 Making a Store Bootable
31 The MMC
32 The HMC
33 Loading the components
34 Using the File Processor
35 Symbols and the SSC
36 The File Processor and Device Management
37 The File Processor and File System Management
38 Finishing Executive Startup

Users and Security
39 Introduction to Users and Security
40 More Fun With Stores: File Heaps
41 File Heaps, part 2
42 SysUAF
43 TUser
44 SysUAF API

Terminal I/O
45 Shells and UCL
46 UOS API, the Application Side
47 UOS API, the Executive Side
48 I/O Devices
49 Streams
50 Terminal Output Filters
51 The TTerminal Class
52 Handles
53 Putting it All Together
54 Getting Terminal Input
55 QIO
56 Cooking Terminal Input
57 Putting it all together, part 2
58 Quotas and I/O

UCL
59 UCL Basics
60 Symbol Substitution
61 Command execution
62 Command execution, part 2
63 Command Abbreviation
64 ASTs
65 Expressions, Part 1
66 Expressions, Part 2: Support code
67 Expressions, part 3: Parsing
68 SYS_GETJPIW and SYS_TRNLNM
69 Expressions, part 4: Evaluation

UCL Lexical Functions
70 PROCESS_SCAN
71 PROCESS_SCAN, Part 2
72 TProcess updates
73 Unicode revisted
74 Lexical functions: F$CONTEXT
75 Lexical functions: F$PID
76 Lexical Functions: F$CUNITS
77 Lexical Functions: F$CVSI and F$CVUI
78 UOS Date and Time Formatting
79 Lexical Functions: F$CVTIME
80 LIB_CVTIME
81 Date/Time Contexts
82 SYS_GETTIM, LIB_Get_Timestamp, SYS_ASCTIM, and LIB_SYS_ASCTIM
83 Lexical Functions: F$DELTA_TIME
84 Lexical functions: F$DEVICE
85 SYS_DEVICE_SCAN
86 Lexical functions: F$DIRECTORY
87 Lexical functions: F$EDIT and F$ELEMENT
88 Lexical functions: F$ENVIRONMENT
89 SYS_GETUAI
90 Lexical functions: F$EXTRACT and F$IDENTIFIER
91 LIB_FAO and LIB_FAOL
92 LIB_FAO and LIB_FAOL, part 2
93 Lexical functions: F$FAO
94 File Processing Structures
95 Lexical functions: F$FILE_ATTRIBUTES
96 SYS_DISPLAY
97 Lexical functions: F$GETDVI
98 Parse_GetDVI
99 GetDVI
100 GetDVI, part 2
101 GetDVI, part 3
102 Lexical functions: F$GETJPI
103 GETJPI
104 Lexical functions: F$GETSYI
105 GETSYI
106 Lexical functions: F$INTEGER, F$LENGTH, F$LOCATE, and F$MATCH_WILD
107 Lexical function: F$PARSE
108 FILESCAN
109 SYS_PARSE
110 Lexical Functions: F$MODE, F$PRIVILEGE, and F$PROCESS
111 File Lookup Service
112 Lexical Functions: F$SEARCH
113 SYS_SEARCH
114 F$SETPRV and SYS_SETPRV
115 Lexical Functions: F$STRING, F$TIME, and F$TYPE
116 More on symbols
117 Lexical Functions: F$TRNLNM
118 SYS_TRNLNM, Part 2
119 Lexical functions: F$UNIQUE, F$USER, and F$VERIFY
120 Lexical functions: F$MESSAGE
121 TUOS_File_Wrapper
122 OPEN, CLOSE, and READ system services

UCL Commands
123 WRITE
124 Symbol assignment
125 The @ command
126 @ and EXIT
127 CRELNT system service
128 DELLNT system service
129 IF...THEN...ELSE
130 Comments, labels, and GOTO
131 GOSUB and RETURN
132 CALL, SUBROUTINE, and ENDSUBROUTINE
133 ON, SET {NO}ON, and error handling
134 INQUIRE
135 SYS_WRITE Service
136 OPEN
137 CLOSE
138 DELLNM system service
139 READ
140 Command Recall
141 RECALL
142 RUN
143 LIB_RUN
144 The Data Stream Interface
145 Preparing for execution
146 EOJ and LOGOUT
147 SYS_DELPROC and LIB_GET_FOREIGN

CUSPs and utilities
148 The I/O Queue
149 Timers
150 Logging in, part one
151 Logging in, part 2
152 System configuration
153 SET NODE utility
154 UUI
155 SETTERM utility
156 SETTERM utility, part 2
157 SETTERM utility, part 3
158 AUTHORIZE utility
159 AUTHORIZE utility, UI
160 AUTHORIZE utility, Access Restrictions
161 AUTHORIZE utility, Part 4
162 AUTHORIZE utility, Reporting
163 AUTHORIZE utility, Part 6
164 Authentication
165 Hashlib
166 Authenticate, Part 7
167 Logging in, part 3
168 DAY_OF_WEEK, CVT_FROM_INTERNAL_TIME, and SPAWN
169 DAY_OF_WEEK and CVT_FROM_INTERNAL_TIME
170 LIB_SPAWN
171 CREPRC
172 CREPRC, Part 2
173 COPY
174 COPY, part 2
175 COPY, part 3
176 COPY, part 4
177 LIB_Get_Default_File_Protection and LIB_Substitute_Wildcards
178 CREATESTREAM, STREAMNAME, and Set_Contiguous
179 Help Files
180 LBR Services
181 LBR Services, Part 2
182 LIBRARY utility
183 LIBRARY utility, Part 2
184 FS Services
185 FS Services, Part 2
186 Implementing Help
187 HELP
188 HELP, Part 2
189 DMG_Get_Key and LIB_Put_Formatted_Output
190 LIBRARY utility, Part 3
191 Shutting Down UOS
192 SHUTDOWN
193 WAIT
194 SETIMR
195 WAITFR and Scheduling
196 REPLY, OPCOM, and Mailboxes
197 REPLY utility
198 Mailboxes
199 BRKTHRU
200 OPCOM
201 Mailbox Services
202 Mailboxes, Part 2
203 DEFINE
204 CRELNM
205 DISABLE
206 STOP
207 OPCCRASH and SHUTDOWN
208 APPEND

Glossary/Index


Downloads

RECALL

In the last article, we discussed the control-B command recall feature. In this article we introduce the UCL RECALL command which is a way to more effectively use a recall buffer containing a lot of commands. RECALL in UCL is backwards compatible to DCL, but adds some capabilities that are new. Note that control-B recall is an inherent feature of UOS that is available to all applications (so long as they use the starlet interface), whereas RECALL is a command specific to UCL that makes the use of command recall easier. Here is the user documentation:

RECALL

Displays previously entered commands for re-execution.

Format

RECALL {qualifiers} {specifier}

Parameters

specifier
Indicates the first character(s) of the command to recall. If more than one previous command begins with the characters specified here, the most recent matching command is shown.
If the specified value is a number, the number is used as an index into the recall buffer, and that command is the one shown.
Note that the command cannot be specified along with certain qualifiers.

Description

When you enter commands, they are stored in a recall buffer for later reuse with the RECALL command or control-B. The RECALL command, itself, is not stored in the recall buffer. By default, up to 254 commands are stored in the buffer. If this limit is exceeded, the oldest entered command is removed to make use for the new command.
When RECALL is used, the command is shown but not executed. To execute the command, you must press ENTER. The command line editing feature can be used to modify the recalled command before you press ENTER to execute it.

Qualifiers

/ALL
Displays all of the commands (and their numbers) in the recall buffer. If a specifier is provided, all commands whose prefix matches that specifier are shown.
/ERASE
Erases the contents of the recall buffer.
/INPUT=filespec
Reads the contents of the specified file into the recall buffer. Each line of the file is saved as a command in the buffer. If not specified, the file extension defaults to ".LIS".
/OUTPUT=filespec
Writes the contents of the recall buffer to the specified file. If not specified, the file extension defaults to ".LIS". This can be combined with a specifier or /SEARCH to direct a subset of commands to the file.
/{NO}PAGE
/NOPAGE is the default. When used with /ALL, /PAGE will pause after each screenful of commands. /PAGE is ignored in cases where multiple lines are not output to the terminal. The list can be terminated at this point by control-Z, control-C, or control-Y.
/SEARCH
Searches the recall buffer for any/all command(s) containing the specifier and displays them.

Examples

$ RECALL S

This command recalls the most recent command starting with "S".

$ RECALL/SEARCH/OUTPUT=commands 3

This command writes each command containing the character "3" to "commands.lis".


First, let's look at the recall buffer support routines that have been added to starlet. Note that there is no equivalent to these in VMS. But an application can use these to provide an equivalent to the UCL RECALL command.

procedure LIB_Clear_Recall ;

begin
    Commands.Clear ;
    Recall_Position := -1 ;
end ;
This function erases the contents of the recall buffer.

function LIB_Recall_Count : int64 ;

begin
    Result := Commands.Count ;
end ;
This function returns the count of commands in the recall buffer.

function LIB_Get_Recall( Index, OutLen, OutBuf : int64 ) : int64 ;

var S : string ;

begin
    if( Index < 0 ) then
    begin
        Index := Commands.Count + Index ;
    end ;
    if( ( Index < 0 ) or ( Index >= Commands.Count ) ) then
    begin
        S := '' ;
    end else
    begin
        S := Commands[ Index ] ;
    end ;
    if( PInt64( pointer( OutLen ) )^ < length( S ) ) then
    begin
        setlength( S, PInt64( pointer( OutLen ) )^ ) ;
    end ;
    PInt64( pointer( OutLen ) )^ := length( S ) ;
    move( PChar( S )[ 0 ], PChar( pointer( OutBuf ) )[ 0 ], length( S ) ) ;
    Result := 0 ;
end ;
This routine returns a command from the buffer. If a negative index is provided, the command returned is the total count plus the index. Thus, -1 gets the last command in the buffer, while 0 gets the first command. OutLen points to an int64 which contains the number of characters to return. Upon return, the value is updated with the actual number of characters written (which is limited to the original value).

function LIB_Get_Recall_Length( Index, OutLen : int64 ) : int64 ;

begin
    if( Index < 0 ) then
    begin
        Index := Commands.Count + Index ;
    end ;
    if( ( Index < 0 ) or ( Index >= Commands.Count ) ) then
    begin
        Result := 0 ;
    end else
    begin
        Result := length( Commands[ Index ] ) ;
    end ;
    PInt64( pointer( OutLen ) )^ := Result ;
    Result := 0 ;
end ;
This routine returns the length, in characters, of the command at the given offset in the recall buffer. If a negative index is provided, the command returned is the total count plus the index. Thus, -1 gets the last command in the buffer, while 0 gets the first command. If the index is out of range, 0 is returned.

procedure LIB_Add_Recall( SRB : int64 ) ;

var S : string ;

begin
    S := Get_String( PSRB( pointer( SRB ) )^ ) ;
    Add_Recall( S ) ;
end ;
This routine adds a string to the command recall buffer.

procedure LIB_Pop_Recall() ;

begin
    if( Commands.Count > 0 ) then
    begin
        Commands.Delete( Commands.Count - 1 ) ;
        if( Recall_Position >= Commands.Count ) then
        begin
            Recall_Position := Commands.Count - 1 ;
        end ;
    end ;
end ;
This command pops the most recent command from the recall buffer.


                    if( Sym = 'recall' ) then
                    begin
                        Process_Recall ;
                    end else
This code is added to the Process function in UCL.

procedure Process_Recall ;

var All, Erase, Page, Search : boolean ;
    Command : string ;
    Err : int64 ;
    Fi, Fo : TCOM_File64 ;
    I : integer ;
    I64 : int64 ;
    IOSB : TIOSB ;
    Input, Output, S, Temp : string ;
    P : PAnsiChar ;
    SRB : TSRB ;
    Switches : TStringList ;
    Line_Count : integer ;
    Max_Line : integer ;
    Index : integer ;

begin
    // Setup...
    Switches := Parse_Switches ;
    Fi := nil ;
    Fo := nil ;
    All := False ;
    Erase := False ;
    Page := False ;
    Search := False ;
    Input := '' ;
    Output := '' ;
    Line_Count := 1 ;
    LIB_Pop_Recall() ; // Remove command from recall buffer
This new routine handles the RECALL command. After setting up, we call the LIB_Pop_Recall to remove the most recent command from the recall buffer since RECALL is the most recent command.

    // Process switches...
    if( ( Switches.Count = 0 ) and ( Parser.Peek = '' ) ) then // Just plain old "RECALL"
    begin
        Output := Get_Prompted_Parameter( 'recall/out=', True ) ;
        if( Output = '' ) then
        begin
            exit ; // Aborted
        end ;
    end ;
If there is no specifier and there are no switches, this is simply a "RECALL". In this case, we prompt the user for an output file.

    for I := 0 to Switches.Count - 1 do
    begin
        S := lowercase( Switches[ I ] ) ;
        Temp := '' ;
        if( Switch_Match( S, 'all', 1, Temp ) ) then
        begin
            All := True ;
            if( Temp <> '' ) then
            begin
                Exception( UCL_NOVALU, Switches[ I ] ) ;
                Switches.Free ;
                exit ;
            end ;
        end else
        if( Switch_Match( S, 'erase', 1, Temp ) ) then
        begin
            Erase := True ;
            if( Temp <> '' ) then
            begin
                Exception( UCL_NOVALU, Switches[ I ] ) ;
                Switches.Free ;
                exit ;
            end ;
        end else
        if( Switch_Match( S, 'nopage', 3, Temp ) ) then
        begin
            Page := False ;
            if( Temp <> '' ) then
            begin
                Exception( UCL_NOVALU, Switches[ I ] ) ;
                Switches.Free ;
                exit ;
            end ;
        end else
        if( Switch_Match( S, 'page', 1, Temp ) ) then
        begin
            Page := True ;
            if( Temp <> '' ) then
            begin
                Exception( UCL_NOVALU, Switches[ I ] ) ;
                Switches.Free ;
                exit ;
            end ;
            Max_Line := Get_Integer_From_String( Get_DVI( 'sys$output:', '', DVI_TT_PAGE ) ) ;
        end else
        if( Switch_Match( S, 'search', 1, Temp ) ) then
        begin
            Search := True ;
            if( Temp <> '' ) then
            begin
                Exception( UCL_NOVALU, Switches[ I ] ) ;
                Switches.Free ;
                exit ;
            end ;
        end else
        if( Switch_Match( S, 'input', 1, Input ) ) then
        begin
            Input := copy( S, 7, length( S ) ) ;
            if( Input = '' ) then
            begin
                Exception( UCL_NULFIL ) ;
                Switches.Free ;
                exit ;
            end ;
        end else
        if( Switch_Match( S, 'output', 1, Output ) ) then
        begin
            if( Output = '' ) then
            begin
                Exception( UCL_NULFIL ) ;
                Switches.Free ;
                exit ;
            end ;
        end else
        begin
            Exception( UCL_IVQUAL, S ) ;
            Switches.Free ;
            exit ;
        end ;
    end ;
    Switches.Free ;
Next we process the switches. This is typical of what we've seen before. The only different thing here is generating an error if /INPUT or /OUTPUT is specified without a filename. Also, if /PAGE is specified, we obtain the terminal's page (screen) height. At the end of the article, we will revisit this.

    if( ( All and Search ) or ( All and ( Input <> '' ) ) or ( Page and ( Output <> '' ) ) ) then 
                                     // Invalid combinations
    begin
        Exception( UCL_CONFLICT ) ;
        exit ;
    end ;
The next step is to check for conflicting switch/specifier combinations. For instance RECALL/ALL/SEARCH makes no logical sense. In such as case we generate an error and exit.

    // Process command...
    Command := lowercase( Get_Token ) ; // Get command...
    if( Output <> '' ) then // If output file was specified
    begin
        Output := Default_Filespec( Output, 'lis' ) ;
        // Open the output file...
        Fo := Open_Binary_File( Output, FAB_V_CIF or FAB_V_SUP ) ;
        if( Fo = nil ) then // Open failure
        begin
            Err := LIB_Get_Exception( 0 ) ;
            if( Err <> 0 ) then
            begin
                Set_Exception( LIB_Get_Exception_Text( 0, Err ) ) ;
                Err := LIB_Get_Exception_Code( 0, Err ) ;
                exit ;
            end ;
        end ;
    end ;
Next we get the specifier (the Command variable). If /OUTPUT was specified, we default the extension to ".lis" and then attempt to open the file. If there was an error, we exit with that error.

    if( Search ) then // RECALL/SEARCH
    begin
        if( Fo <> nil ) then // /OUTPUT
        begin
            I := 0 ;
        end else
        begin
            I := -1 ;
        end ;
        S := ' ' ;
        while( S <> '' ) do
        begin
            Temp := Get_Recall( I ) ;
            if( Temp = '' ) then
            begin
                break ;
            end ;
            S := lowercase( Temp ) ;
            if( Fo <> nil ) then // /OUTPUT
            begin
                inc( I ) ;
                if( pos( Command, S ) > 0 ) then // Found match in recall buffer
                begin
                    Fo.Writeln( PChar( Temp ) ) ;
                    Err := LIB_Get_Exception( 0 ) ;
                    if( Err <> 0 ) then
                    begin
                        Err := LIB_Get_Exception_Code( 0, Err ) ;
                        Set_Exception( Err, Input ) ;
                    end ;
            end ;
            end else
            begin
                dec( I ) ;
                if( pos( Command, S ) > 0 ) then // Found match in recall buffer
                begin
                    Temp := inttostr( I ) ;
                    while( length( Temp ) < 3 ) do
                    begin
                        Temp := ' ' + Temp ;
                    end ;
                    if( _Output( Temp + ' ' + S ) ) then
                    begin
                        break ;
                    end ;
                end ;
            end ;
        end ; // while( S <> '' )
    end else
If /SEARCH was specified, we need to iterate through the recall buffer looking for any matches. Things are a little complicated by the fact that when listed on the terminal, recall commands are listed in reverse order (newest to oldest), such that the first one shown is prefixed with 1 and is the last item in the list. The next one shown is prefixed with 2 and is the next-to-last item in the list. But when writing out to a file with /OUTPUT, the commands need to be written out in the order they were entered (ie starting with the oldest). So, depending upon whether we're writing to the terminal or to a file, we process the recall buffer in different orders. Also, when writing to a file, we don't prefix the commands with numbers - but when writing to the terminal, we do. _Output handles /PAGE on terminal output and we will cover that later in this article.

    if( All ) then // RECALL/ALL
    begin
        if( Fo <> nil ) then // /OUTPUT
        begin
            I := 0 ;
        end else
        begin
            I := -1 ;
        end ;
        S := ' ' ;
        Index := 0 ;
        while( S <> '' ) do // Until we've looked at all lines in the recall buffer
        begin
            Temp := Get_Recall( I ) ;
            if( Temp = '' ) then
            begin
                break ;
            end ;
            S := lowercase( Temp ) ;
            if( Fo <> nil ) then // /OUTPUT
            begin
                inc( I ) ;
                if( ( Command = '' ) or ( copy( S, 1, length( Command ) ) = Command ) ) then
                begin
                    Fo.Writeln( PChar( Temp ) ) ;
                end ;
            end else
            begin
                dec( I ) ;
                if( ( Command = '' ) or ( copy( S, 1, length( Command ) ) = Command ) ) then
                begin
                    inc( Index ) ;
                    Temp := inttostr( Index ) ;
                    while( length( Temp ) < 3 ) do
                    begin
                        Temp := ' ' + Temp ;
                    end ;
                    if( _Output( Temp + ' ' + S ) ) then
                    begin
                        break ;
                    end ;
                end ;
            end ;
        end ; // while( S <> '' )
    end else
Next we check for /ALL. If specified, we do something almost identical to the /SEARCH processing, but instead of looking for a match somewhere within the command strings, we check for a match with the command prefix.

    if( ( Input = '' ) and ( not Erase ) ) then
    begin
        // Handle index...
        if( Valid_Int( Command, I64 ) ) then
        begin
            Temp := Get_Recall( I64 ) ;
            if( Temp = '' ) then // Value was out of range
            begin
                Temp := Get_Recall( -1 ) ;
            end ;
            if( Temp = '' ) then // Must not be any commands in buffer
            begin
                exit ;
            end ;
            if( Fo <> nil ) then // /OUTPUT
            begin
                Fo.Writeln( PChar( Temp ) ) ;
                Err := LIB_Get_Exception( 0 ) ;
                if( Err <> 0 ) then
                begin
                    Err := LIB_Get_Exception_Code( 0, Err ) ;
                    Set_Exception( Err, Input ) ;
                end ;
            end else
            begin
                Set_String( Temp, SRB ) ;
                SYS_QIOW( 0, RH_SysCommand, IO_SETMODE or IOM_FORCE_INPUT,
                   integer( @IOSB ), 0, 0, integer( @SRB ), 0, 0, 0, 0, 0 ) ;
            end ;
            exit ;
        end ;

        // Find most recent command matching the specified prefix...
        if( Fo <> nil ) then // /OUTPUT
        begin
            I := 0 ;
        end else
        begin
            I := -1 ;
        end ;
        S := ' ' ;
        while( S <> '' ) do // Until we've looked at all lines or find a match
        begin
            Temp := Get_Recall( I ) ;
            if( Temp = '' ) then
            begin
                break ;
            end ;
            S := lowercase( Temp ) ;
            if( Fo <> nil ) then // /OUTPUT
            begin
                inc( I ) ;
                if( copy( S, 1, length( Command ) ) = Command ) then
                begin
                    Fo.Writeln( PChar( Temp ) ) ;
                    Err := LIB_Get_Exception( 0 ) ;
                    if( Err <> 0 ) then
                    begin
                        Err := LIB_Get_Exception_Code( 0, Err ) ;
                        Set_Exception( Err, Input ) ;
                    end ;
                    break ;
                end ;
            end else
            begin
                dec( I ) ;
                if( copy( S, 1, length( Command ) ) = Command ) then
                begin
                    Set_String( Temp, SRB ) ;
                    SYS_QIOW( 0, RH_SysCommand, IO_SETMODE or IOM_FORCE_INPUT,
                       integer( @IOSB ), 0, 0, integer( @SRB ), 0, 0, 0, 0, 0 ) ;
                    break ;
                end ;
            end ;
        end ; // while( S <> '' )
    end ;
If we get here, we are processing a recall without /ALL, /SEARCH, or /ERASE. In this case, we simply look for the first command in the recall buffer that has a prefix matching the specifier. If using /OUTPUT, the match is written to the file, otherwise it is put into the terminal's input buffer.

    if( Erase ) then
    begin
        if( Command <> '' ) then
        begin
            Exception( UCL_MAXPARM, Command ) ;
            exit ;
        end ;
        LIB_Clear_Recall ;
        exit ;
    end ;
The order we process the switches is important. The first phase handles outputting commands, if appropriate. The second phase is to erase the buffer. This is important since if we erase the buffer first, there will be nothing to write out. The third step is to process /INPUT, which must be done after we erase the buffer - doing it the other way around would be pointless. In this code, we process the erase operation, if /ERASE was specified. /ERASE is not valid with a specifier, so if there is one, we exit with an error. Otherwise, we call LIB_Clear_Recall.

    if( Input <> '' ) then
    begin
        if( Command <> '' ) then
        begin
            Exception( UCL_MAXPARM, Command ) ;
            exit ;
        end ;
        Input := Default_Filespec( Input, 'lis' ) ;
        Fi := Open_Binary_File( Input, 0 ) ;
        if( Fi = nil ) then // Open failure
        begin
            Err := LIB_Get_Exception( 0 ) ;
            if( Err <> 0 ) then
            begin
                Err := LIB_Get_Exception_Code( 0, Err ) ;
                Set_Exception( Err, Input ) ;
                if( Fo <> nil ) then
                begin
                    Fo.Free ;
                end ;
                exit ;
            end ;
        end else
        while( not Fi.EOF ) do
        begin
            Fi.Readln( P ) ;
            Err := LIB_Get_Exception( 0 ) ;
            if( Err <> 0 ) then
            begin
                Err := LIB_Get_Exception_Code( 0, Err ) ;
                Set_Exception( Err, Input ) ;
            end ;
            S := string( P ) ;
            Set_String( S, SRB ) ;
            LIB_Add_Recall( int64( @SRB ) ) ;
        end ;
    end ; // if( Input <> '' )
If /INPUT is specified, we default the filename extension to ".lis" and then open the file. If there was an error, we exit with the exception. Otherwise, we read the entire file, one line at a time and insert it into the recall buffer via LIB_Add_Recall.

    if( Fo <> nil ) then
    begin
        Fo.Free ;
    end ;
    if( Fi <> nil ) then
    begin
        Fi.Free ;
    end ;
end ; // Process_Recall
Finally, we close any open files.

    function _Output( S : string ) : boolean ;

    begin
        Result := False ;
        Output_Line( RH_SysOutput, S ) ;
        inc( Line_Count ) ;
        if( Page and ( Line_Count >= Max_Line - 2 ) ) then
        begin
            Line_Count := 1 ;
            S := Get_Line( 'Press RETURN to continue ...' ) ;
            if( pos( Control_Z, S ) > 0 ) then
            begin
                Result := True ;
            end ;
        end ;
    end ;
This local function is used to output a line to the terminal. We keep track of the number of lines output with Line_Count. If this exceeds the height of the terminal, we output a prompt so the user has a chance to read the text. Once the user presses ENTER, we reset the line count and continue with the list. However, if the user types control-Z, we immediately exit.

//NEW--->
    end else
    begin
        E := USC.Get_Process_Exception( Kernel.PID ) ;
        if( E <> nil ) then
        begin
            IOSB.r_io_64.w_status := E.Get_Error ;
        end ;
        Call_To_User( Err ) ;
//<--NEW
    end ; // if( FAB.FAB_Q_Handle <> 0 )
    Result := IOSB.r_io_64.w_status ;
end ; // TUOS_FiP.File_Open_Service
Another thing we need to update is the handling of file open operations in the FIP. During testing, I discovered that a file open failure wasn't returning the exception in the IOSB. This was an oversight (ie bug). Without this change, a file open could fail but no indication of the failure was returned.

            begin
                RAB.RAB_W_RSZ := Resource.Position - Position ; // Number of bytes written
                Status := Write_User( Kernel, PID, _RAB, RAB.RAB_Size, RAB ) ; // Update RAB
                if( Status <> 0 ) then
                begin
                    Status := IOSB.r_io_64.w_status ;
                    Result := Status ;
                    exit ;
                end ;
            end ;
When we were writing to stream devices, such as terminals, keeping track of the number of bytes written was unimportant except in terms of quotas. Now that we're writing to stores, we need to track how many bytes were written so that we can update the file position. So, the above code was added to TUOS_FiP.File_Write. This code updates the RAB's RAB_W_RSZ field. First we update our local copy of the RAB, then copy that RAB back to the user's memory. Thus, when the system service returns, the caller can check this field in the RAB and use the value to keep track of the current file position. Note: we could assume that all write operations succeed and just update the file position, but if any write errors occur, the file pointer would be wrong. So, it is always best to advance the pointer by the number of bytes actually written.

procedure TUOS_File_Wrapper.Writeln( Buffer : PAnsiChar ) ;

var Res : int64 ;
    S : string ;

begin
    Blockwrite( Buffer[ 0 ], strlen( Buffer ), Res ) ;
    _FilePos := _FilePos + Res ;

    S := CRLF ;
    Blockwrite( PChar( S )[ 0 ], length( S ), Res ) ;
    _FilePos := _FilePos + Res ;
end ;
We've now implemented the Writeln method of the TUOS_File_Wrapper class. This simply calls the Blockwrite for the passed string, and updates the file position. Then we write a CR/LF combination to end the text line.

function TTerminal.Get_Char_Columns : cardinal ;

begin
    if( Max_Char_Columns = 0 ) then
    begin
        Max_Char_Columns := 80 ;
    end ;
    if( _Char_Columns = 0 ) then
    begin
        _Char_Columns := Max_Char_Columns ;
    end else
    begin
        Result := _Char_Columns ;
    end ;
end ;


function TTerminal.Get_Char_Rows : cardinal ;

begin
    if( Max_Char_Rows = 0 ) then
    begin
        Max_Char_Rows := 66 ;
    end ;
    if( _Char_Rows = 0 ) then
    begin
        _Char_Rows := Max_Char_Rows ;
    end else
    begin
        Result := _Char_Rows ;
    end ;
end ;
We've added these two new methods (accessed via properties) to the TTerminal class. Before this, we defaulted the terminal height and width to 66 and 80, respectively. Here, we replace the previous behavior with something similar, but with a subtle difference.
Terminals have a physical size that depends upon the terminal model. In the case of a VT100, for instance, this is 24 line of 80 columns. But UOS allows a terminal to have a logical size, which differs from the physical size. In most cases, setting the logical size to a different physical size would probably cause odd behavior on the terminal. But in a serial connection through a cable, we may not have an idea as to the physical size So, we handle these situations by having a Max_Char_Columns and Max_Char_Rows properties that represent the physical size of the display screen or printer page. If the size is unknowable, these values will be 0. In that case, we set them to the appropriate defaults. If the terminal has a different configuration, the Max_Chars and Max_Columns properties (indicating the logical terminal size) can be manually set by the user. Unless set, these two properties are 0, in which case we return the Max_* value rather than the logical value. If these defaults don't match the actual device, the user can set the logical size to the appropriate values.
Remember that a UOS terminal is any serial stream device. This may be a physical terminal like a VT100, or it could be a "ticker tape" display with a physical size of 1 row, or a printing terminal with 66-line long pages. Logical terminals work similarly except that most of them have size metrics which can change. For instance, if you turn your hand-held phone 90 degrees, the numbers of rows and columns switch. Changing the font size could result in greater or fewer characters being able to be displayed. This essentially changes the physical size of the display. Some terminals, like remote virtual terminals, may be a window opened on a desktop computer. If the user resizes this window, that may change the "physical" size of the terminal. The UOS simulator works in much this way, with a resizable virtual console terminal.
Note: the next release of the simulator will allow resizing.
To support these dynamic changes, UOS needs a means of being notified of each change. Otherwise, the GETDVI call with the DVI_TT_PAGE item would return the wrong value. This would have the effect of breaking the /PAGE switch on the RECALL command. We will handle this in a similar manner to the I/O notifications from the HAL.

procedure TUOS_FiP.Interrupt( Value : int64 ) ;

var Controller, Index : word ;
    Device : Devices.TDevice ;
    C, I : integer ;
    Got_Input : boolean ;
    _Unit : word ;

begin
    Value := Value shr 16 ; // Ignore software/HAL flag
    _Unit := Value and $FFFF ;
    Value := Value shr 16 ;
    Controller := Value and $FFFF ;
    Value := Value shr 16 ;
    Index := Value and $FF ;
    Value := Value shr 8 ;
    for I := 0 to _Devices.Count - 1 do
    begin
        Device := _Devices.Get_Device( I ) ;
        if(
            ( Device.Info.Controller = Controller )
            and
            ( Device.Info.Device_Unit = _Unit )
          ) then // Found the indicated device
        begin
            if( Device.Terminal <> nil ) then // If a terminal
            begin
                case Value of
                    0 : // I/O complete
                        begin
                            Got_Input := False ;
                            if( Device.Terminal.Input_Filter.Has_Space( 
                              Device.Terminal.Min_Record_Size ) ) then
                            begin
                                while( TFiP_Stream( Device.Stream ).Driver.Input( C ) ) do
                                begin
                                    Got_Input := True ;
                                    Device.Terminal.Input_Filter.New_Character( C ) ;
                                    if( not Device.Terminal.Input_Filter.Has_Space( 
                                          Device.Terminal.Min_Record_Size ) 
                                      ) then
                                    begin
                                        break ;
                                    end ;
                                end ;
                            end ;
                            if( TFiP_Stream( Device.Stream ).Driver.Pending_Input ) then
                            begin
                                Device.Terminal.Pause_Input ;
                            end ;
                            if( Got_Input ) then
                            begin
                                if( Device.IO_PID <> 0 ) then // A process blocked on this device
                                begin
                                    Kernel.USC.UnBlock( Device.IO_PID ) ;
                                end ;
                            end ;
                        end ; // I/O complete
                    1 : // Metrics change
                        begin
                            Device.Terminal.Max_Char_Rows := TFiP_Stream( Device.Stream ).Driver.Height ;
                            Device.Terminal.Max_Char_Columns := TFiP_Stream( Device.Stream ).Driver.Width ;
                        end ;
                end ; // case Value
            end ; // if( Device.Terminal <> nil )
            exit ;
        end ; // if
    end ; // for I := 0 to _Devices.Count - 1
end ; // TUOS_FiP.Interrupt
We covered this interrupt handler in the past, but here we've modified it to handle not only I/O completion, but notification of metrics changes. In this change, we query the device driver and set the terminal metrics appropriately. In the future, we will address how to let the running application also know about the metric change.

                    Device.Terminal.Char_Rows := TFiP_Stream( Device.Stream ).Driver.Height ;
                    Device.Terminal.Char_Columns := TFiP_Stream( Device.Stream ).Driver.Width ;
Finally, when we create the Terminal for the File Processor, we set the initial metrics to the values returned by the HAL terminal driver.

In the next article, we will look at the next UCL command.

 

Copyright © 2021 by Alan Conroy. This article may be copied in whole or in part as long as this copyright is included.