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 UCL Command execution
62 UCL Command execution, part 2
63 UCL Command Abbreviation
64 ASTs
65 UCL Expressions, Part 1
66 UCL Expressions, Part 2: Support code
67 UCL Expressions, part 3: Parsing
68 SYS_GETJPIW and SYS_TRNLNM
69 UCL Expressions, part 4: Evaluation
70 PROCESS_SCAN

Glossary/Index


Download sources
Download binaries

UCL Command execution, part 2

In this article, we continue our discussion of UCL command processing in the Process procedure. We left off with the setting up of the parser and checking for error conditions at the beginning of our processing loop. UCL command lines consist of assignments (setting the value of a symbol), comments, labels, and commands. So, let's handle these conditions.

            Sym := Edit( Get_Token, 512 ) ;
            if( Sym = '' ) then // All done
            begin
                exit ;
            end ;
We get the next (or first) token from the parser and put it in the Sym variable. The reason the variable is named Sym is that quite often the first token encountered in a command line is a symbol. It may turn out not to be one, but it seemed a better name than just using S. If there is no token, that means that the parser has exhausted all of the tokens in the line - in other words, we've reached the end of the command - and this is one of the ways in which we exit the loop.

            if( Sym = '@' ) then
            begin
                Sym := Parser.Grab_Line ;
                if( Sym = '' ) then
                begin
                    Output_Line( RH_SysError, '%UCL-W-NULFIL, missing or invalid file specification - respecify'  ) ;
                    exit ;
                end ;
                if( Process_At ) then
                begin
                    exit ;
                end ;
            end else
At this point, we know we have a token and it is the start of a command. So we see if the token is an at-sign (@). This indicates to run another UCL command file. In that case, we assume that the rest of the line is the file specification of the command file, so we use Parser.Grab_Line to get it. If the file specification is null we indicate an error and exit. Otherwise, we call the Process_At function to handle the command file. We will discuss this function in a future article. In the case that the function returns true, we exit immediately.

            begin
                // Process line...
                S := Get_Token ;
                if( S = ':' ) then // A label
                begin
                    if( Interactive ) then
                    begin
                        Output_Line( RH_SysError, '%UCL-W-NOLBLS, label ignored - use only within command procedures'  ) ;
                        Output_Line( RH_SysError, '    \' + Sym + ':\'  ) ;
                    end ;
                    continue ;
                end ;
Next, if we aren't executing a command file, we grab the next token and see if it is a single colon (:). If so, this indicates a label. However, labels are not allowed in interactive mode - so in that case, we display an error and continue with our processing loop. For non-interactive mode, we ignore the label and continue.

SYS_GETDVIW
Let's take a detour here and look at code for the the Interactive function:

var Descriptor : array[ 0..1 ] of TDVI_Descriptor ;
    Buff, BufLen : int64 ;

function Interactive : boolean ;

begin
    Buff := 0 ;
    BufLen := 0 ;
    fillchar( Descriptor, sizeof( Descriptor ), 0 ) ;
    Descriptor[ 0 ].Buffer_Length := sizeof( Buff ) ;
    Descriptor[ 0 ].Item_Code := DVI_DEVCHAR ; // DVIDEF item
    Descriptor[ 0 ].Buffer_Address := integer( @Buff ) ;
    Descriptor[ 0 ].Return_Length_Address := integer( @BufLen ) ;
    SYS_GETDVIW( 0, RH_SysCommand, '', integer( @Descriptor ), integer( @IOSB ),
        0, 0, '', 1 ) ;
    Result := ( ( Buff and DEV_V_TRM ) <> 0 ) ; // Terminal device
end ;
To determine if the sys$command device is a terminal (ie interactive), we can call the SYS_GETDVIW system call (GETDVIW = Get device information and wait for a result). This call can return multiple pieces of device information, which can differ for each kind of device. So, the call takes a list of descriptors that indicate what information is requested and where to return it. A list of descriptors is like an array, and can be defined as one. The descriptors must exist in contiguous memory and the last descriptor should be a special terminal descriptor that lets UOS know where the descriptor lists ends. For now, we will only concern ourselves with the DVI_DEVCHAR data request, which return device characteristics for the passed handle.

First we initialize Buff and BufLen to 0. These are the variables that will receive the returned data. Next we initialize the Descriptor array to 0. This will set the second (and last) descriptor to nulls, which indicate a terminal descriptor (it also sets the first descriptor to nulls, but we immediately change those values). Then we set the first descriptor in the array to the necessary values. The Buffer_Length indicates the maximum length of the receiving buffer, the item code indicates the information we want, the buffer address indicates the address of the receiving buffer, and the return length address indicates where the actual returned data length is stored.

You might think that this is an awful lot of effort to go to in order to retrieve a simple integer value containing device flags. So why do it this way? The simple answer is: because it is the way that VMS does it. But we aren't just slavishly following VMS - there are some good reasons why VMS does it this way. First, there are dozens of different pieces of information that can be returned about a given device. If there were separate system calls for each piece, the system API would quickly become unmanageably large. Second, we've noted that calls into ring 0 have overhead associated with them and - as we will see in future articles - there are cases where a program will be asking for many pieces of information about a device. Using our approach, all of that information can be queried with a single system call into ring 0. This results in much less overhead, which translates into higher system performance. Third, one might be tempted to have a single system call that returned every possible bit of information about a device. While that might solve the first two problems, there are two reasons why this would be a bad choice: 1) The amount of data returned will be different for different types of devices and even different for difference devices of the same type. Some of this information is variable in length and it would be nigh to impossible to provide everything about a device without querying UOS for the size of the resulting data, building an image of the data in the executive so we can respond with that size, and then the call to get it, which would involve building the image of the data again. But even worse, there is some overhead associated with gathering the information about a device. The more information we return, the more overhead is involved. If we only want one or two pieces, why waste the CPU cycles on gathering data that is not going to be used by the caller? Not to mention the time to transfer much more data to the application. But even ignoring the overhead of transferring more data, we can't return a record structure from the executive to the user because the information isn't all contained in a single structure - and even if it were, that would create a coupling between the application and the specific implementation in the executive. Thus, the VMS approach has the added benefit of decoupling the internal implementation with the interface for retrieving that data. This is another example of how a little bit of extra effort and discipline can pay off in multiple ways.

Here is the definition of the TDVI_Descriptor structure:

type TDVI_Descriptor = packed record
                           Buffer_Length : cardinal ; // Length of result buffer
                           Item_Code : cardinal ; // DVIDEF item
                           Buffer_Address : int64 ; // Address of result buffer
                           Return_Length_Address : int64 ; // Where to write result length
                       end ;

Now let's look at the SYS_GETDVIW function in the Sys unit.

procedure SYS_GETDVIW( efn, Handle : int64 ; Device : string ;
    itmlst, IOSB, ASTAdr, ASTPrm : int64 ; Pathname : string ; Siz : integer ) ;

var Status : byte ;
    SysRequest : TFile_Request ;

begin
    fillchar( SysRequest, sizeof( SysRequest ), 0 ) ;
    SysRequest.Request.Subsystem :=  UOS_Subsystem_FIP ;
    SysRequest.Request.Request := UOS_FIP_GetDVIW ;
    SysRequest.Request.Length := sizeof( SysRequest.FRB ) ;
    SysRequest.Request.Status := integer( @Status ) ;
    SysRequest.FRB.Handle := Handle ;
    SysRequest.FRB.Buffer := ItmLst ;
    SysRequest.FRB.Length := Siz ;
    SysRequest.SRB.Buffer := integer( PChar( Device ) ) ;
    SysRequest.SRB.Length := length( Device ) ;

    Call_To_Ring0( integer( @SysRequest ) ) ;
end ;
SYS_GETDVIW uses the TFile_Request structure that we introduced in article 53. A pointer to the device name, and its length, as passed along with the handle, a pointer to the descriptor list, and the number of descriptors passed. You may wonder why we pass a count of descriptors if we also have a terminating descriptor. The fact is that we can do either: UOS will stop processing descriptors if a terminating descriptor is encountered even if the passed count is larger than that. In other words, UOS will process until the terminating descriptor or until it has processed as many descriptors as we indicate - whichever happens first. It is simply good practice to always include a terminating descriptor as a guard against programming errors.

You may also wonder why we pass both a device name and a handle. In fact, we need to only pass one of them. The function allows you to reference a device by name or by handle. In the above example, we pass a null string for the device name, so that it uses the handle. If both are passed, the device name will take precedence. Note: using the handle is slightly more efficient that passing the device name ("sys$command" in this case). The device name is used when we don't have a handle to the device, but still want information about it.

Finally, let's look at the additional FiP code to respond to this call. First the new code in the API method.

        UOS_FIP_GetDVIW:
            begin
                if( not Enter_Standard_FRB_Call ) then
                begin
                    exit ;
                end ;
                try
                    Get_Device_Info( SysReqFRB.FRB.Handle, SysReqFRB.FRB.Buffer, SysReqFRB.FRB.Length, SysReqFRB.SRB ) ;
                finally
                    Exit_Standard_FRB_Call ;
                end ;
            end ;
Nothing more here than to call the Get_Device_Info with the appropriate values.

And here is the Get_Device_Info method code.

procedure TUOS_FiP.Get_Device_Info( Handle, Buffer, Length : int64 ; SRB : TSRB ) ;

var Buff : PAnsiChar ;
    Descriptor : TDVI_Descriptor ;
    Device_Name : TUOS_String ;
    Loop, Status : integer ;
    Offset : int64 ;
    PID : TPID ;
    Res : cardinal ;
    Resource : TResource ;
    Temp : Ansistring ;

begin
    // Setup...
    if( Length = 0 ) then
    begin
        exit ;
    end ;
    PID := Kernel.PID ;
    Handle := USC.Translate_Handle( PID, Handle ) ;
    Resource := TResource( Handle ) ;
The first thing we do is check if the passed number of descriptors is 0. If so, we exit immediately to avoid doing extra work for nothing. Then we get the current PID and translate the passed handle. Note that, for now, we are ignoring the case of a device being passed by name. Then we get the resource associated with the handle.

    try
        for Loop := 0 to Length do
        begin
            move( Buff[ Offset ], Descriptor, sizeof( Descriptor ) ) ;
            if(
                ( Descriptor.Buffer_Length = 0 )
                or
                ( Descriptor.Buffer_Address = 0 )
                or
                ( Descriptor.Return_Length_Address = 0 )
              ) then // End of list
            begin
                exit ;
            end ;
Now we loop through the descriptors until we reach the passed length of the descriptor list. We move the data from the user memory to a local Descriptor variable. If any of the address or length values are zero, we consider this a terminating descriptor and exit.

            case Descriptor.Item_Code of
                DVI_DEVCHAR : begin
                                  if( Descriptor.Buffer_Length > sizeof( Res ) ) then
                                  begin
                                      Descriptor.Buffer_Length := sizeof( Res ) ;
                                  end ;
                                  Res := 0 ;
                                  if( TFiP_File( Resource._File ) is TFiP_Terminal_File ) then
                                  begin
                                      Res := DEV_V_TRM ;
                                  end ;
                                  
                                  setlength( Temp, Descriptor.Buffer_Length ) ;
                                  move( Res, PChar( Temp )[ 0 ], Descriptor.Buffer_Length ) ;
                                  SRB.Buffer := Descriptor.Buffer_Address ;
                                  SRB.Length := Descriptor.Buffer_Length ;
                                  Status := Set_User_String( Kernel, PID, SRB, Temp ) ;
                                  if( Status = UE_Error ) then
                                  begin
                                      if( MMC.Last_Error = nil ) then
                                      begin
                                          Generate_Exception( UOSErr_Memory_Address_Error ) ;
                                      end ;
                                      exit ;
                                  end ;
                              end ;
            end ;
Now we check the item code (note that we are only handling DVI_DEVCHAR at present). If the passed length is greater than the size of a cardinal, we adjust it down. Res is going to be the set of flags that we return, so we set it to 0 to clear them. If the resouces is a terminal, we set the DEV_V_TRM flag. We will look at other flags in the future. When we are finished setting the flags, we set the size of Temp and copy the flags into the string. This is so we can use the Set_User_String call to copy the data back to the user. That is also the reason for setting up SRB. After the Set_User_String call, we check for an error code. If there was an error, we generate a memory error and exit.

            Offset := Offset + sizeof( Descriptor ) ; // Move to next descriptor
        end ; // for Loop := 0 to Length
        finally
            MMC.UnMap_Pages( 0, Buffer, Length * sizeof( Descriptor ) ) ;
        end ;
    finally
        MMC.Unlock_Pages( PID, Buffer, Length * sizeof( Descriptor ) ) ;
    end ;
end ; // TUOS_FiP.Get_Device_Info
Finally, we update the offset to point to the next descriptor and loop back to process it. After the loop ends, we unmap and unlock pages, as usual.

In the next article, we will continue our discussion of UCL command processing.

 

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