From:	CSBVAX::MRGATE!@KL.SRI.Com,@wiscvm.wisc.edu:CHRIS@YMIR.BITNET@SMTP  7-OCT-1987 03:59
To:	EVERHART
Subj:	The long awaited AUTOLOGOUT program

Received: from wiscvm.wisc.edu by KL.SRI.COM with TCP; Tue 6 Oct 87 22:52:39-PDT
Received: from YMIR.BITNET by wiscvm.wisc.edu ; Wed, 07 Oct 87 00:52:14 CDT
Date: Mon, 5 Oct 87 18:49 PDT
From: Chris Yoder <CHRIS%YMIR.BITNET@wiscvm.wisc.edu>
Subject: The long awaited AUTOLOGOUT program
To: info-vax@kl.sri.COM
X-VMS-To: INFO-VAX-SUBMIT

     Poor KVC has been deluged with requests for Autologout...  So, since I am
the primary author for this beastie, I will distribute it.  I make no
guarantees about this program, it's been working at Hughes since I left and no
hit men have come to get me (yet...).  Simply cut between the dotted lines, @
the resulting file, and read 000readme.1st.  If you have any questions, please
send mail to *ME* not KVC.

-- Chris Yoder
     Harvey Mudd College        Bitnet ----- Chris@Ymir.Bitnet

--------------------------------------------------------------------------------
$!------------------------------------------------------------------------------
$ show default
$ check_sum = 1046375736
$ write sys$output "Creating 000README.1ST"
$ create 000README.1ST
$ DECK/DOLLARS="$*$*EOD*$*$"
     AUTOLOGOUT 1.1

General info:  (the commercial  :-)  )

     AUTOLOGOUT is a daemon process that automatically disconnects an
interactive job tree from a terminal if it has been idle for too long.  A job
tree includes a main process and all of its subprocesses and is defined as
interactive only if it is attached to a physical or network terminal.  Jobs
trees that are ignored include batch jobs, detached jobs and disconnected jobs.

     The main purpose in writing this daemon is to protect user's data and
enhance system security by logging out jobs that have been left alone and
unprotected.  Anyone who came up to that terminal would have full access to any
data or programs that the user who left himself logged in would have access to.
If the system manager would like to free up the process slot(s) taken up the
the job tree, he may setup Autologout to delete the connected process instead.
(NOTE: Disconnected jobs will *always* be ignored.)

     Because the Autologout daemon cannot ask the terminal if there is a user
sitting in front of it, users may set their own idle time limits within the
boundries set up by the system manager by using IDLESET.  Users who often use
terminals in a terminal room might want to set an idle time close to the
minimum.  Users who have secure terminals and want to leave a terminal logged
in all of the time or who might have several sessions active on the same
terminal at once might want to set thier idle limits higher than the default.

     AUTOLOGOUT sends out status messages to all terminals enabled to receive
OPER1 operator messages.  Status messages are sent out when a process is logged
out because it has exceeded it's maximum idle time limit, when a user is
granted an idle time limit greater than the system maximum, when a user
attempts to shutdown the AUTOLOGOUT daemon (the message will indicate the
success or failure of the shutdown attempt) and when most unexpected events
cause the AUTOLOGOUT daemon to crash.

     Please see IDLESET.HLP for more information on the IDLESET program and all
of it's functionality.

--------------------------------------------------------------------------------
     The algorithm:  (The technical stuff)

     The AUTOLOGOUT daemon runs as a detached process.  A general description
of the program is that it creates a mailbox that IDLESET passes messages into,
then it goes into an endless loop collecting information on the jobs on the
system, separating out processes that should be ignored, disconnecting or
deleting processes that have exceeded their idle limits, and then waiting for
some delta time.  While AUTOLOGOUT is waiting, if a message is delivered to
it's mailbox an AST routine wakes up and handles the request from IDLESET.  AST
delivery is blocked while AUTOLOGOUT is collecting information on all of the
processes on the system.

     In order to do it's job, AUTOLOGOUT needs to have a home directory where
the executable image and startup parameter file lives.  It also needs to be run
with the privileges PRMMBX, SYSNAM, WORLD, OPER, SHAR and CMKRNL.  IDLESET may
also live in this directory, but it doesn't have to (on our system the
AUTOLOGOUT image is held in a system software directory while the IDLESET image
lives in the public software directory).

     When AUTOLOGOUT is collecting information about the processes on the
system, it adds up the buffered I/O, direct I/O and CPU time for every process
and subprocess in the tree and compares this number to the previous totals for
that tree.  If there has been any change in these numbers or if the process is
disconnected the process is considered not to be idle and it's idle time is set
to zero.  Otherwise the delta time that AUTOLOGOUT waits is added to the
process' current idle time.

     AUTOLOGOUT ignores all detached, batch, and disconnected processes and
child processes of those processes when it selectes jobs to be deleted.
However, it does keep the disconnected processes information in it's database
so that it can save the idle limit that the user might have set between
connects.  The idle time of disconnected jobs is considered to be 0, even
though they are ignored, so that when a user reconnects that user isn't blown
away before getting a chance to do something.

--------------------------------------------------------------------------------
Setting up:  (The how-to)

     The simple way to set up autologout is:
$ @build_autologout
$ @install_autologout

     If you wish to do it by hand...

     Edit AUTOLOGOUT$STARTUP.PAR to reflect the way that you would like
Autologout to operate on your system.

     The first three lines are the idle limits for your system.  These limits
are listed in minutes.  The first line is the minimum idle limit that any user
may set.  The second line is the idle limit that users get by default. The
third line is the maximum idle limit that any user may set.  Note that this
limit may be overridden by a user who has the privilege EXQUOTA enabled in the
current process.

     The rest of the lines in AUTOLOGOUT$STARTUP.PAR reflect the way that
Autologout deals with a process past it's idle limit.  The forth line in the
file sets the Disconnect_to_Remove flag indicating the default action of
Autologout when the process passes it's idle limit.  TRUE indicates that
Autologout should DISCONNECT the process if possible (if Autologout cannot
DISCONNECT the process, it will delete it), and FALSE indicates that Autologout
should LOGOUT the process.  The fifth line is a comment indicating what the
rest of the lines in the file are.  Autologout expects at least 5 lines, so
this comment *must* be left in the file untouched.  Each of the following lines
contain one username per line.  Each of these usernames are treated by
Autologout as if the Disconnect_to_Remove flag was set opposite from it's
current setting.  Thus if the Disconnect_to_Remove flag is set to true, then
the user listed on the exception list will be deleted instead of disconnected,
and vice versa.

     Edit IDLESET.CLD so that the line defining where the IDLESET image lives
is correct for your system.  Add the IDLESET command to the system CLD tables
($ SET COMMAND/TABLE=SYS$Library:DCLTABLES -
        /OUTPUT=SYS$Library:DCLTABLES IDLESET
 $ MCR INSTALL SYS$LIBRARY:DCLTABLES/REPLACE)
or tell your users where it is so that they can use the command SET COMMAND
<dir>:IDLESET in their LOGIN.COM.

     Edit AUTOLOGOUT$STARTUP.COM so that it defines AUTOLOGOUT_DIR correctly
for your system.  Next add a line to your SYSTARTUP that executes
AUTOLOGOUT$STARTUP.COM.

     Copy AUTOLOGOUT$STARTUP.PAR, AUTOLOGOUT$STARTUP.COM and AUTOLOGOUT.EXE to
the directory where they will live.

     Finally execute AUTOLOGOUT$STARTUP.COM while running as SYSTEM and
AUTOLOGOUT is installed and nuking users...  :-)
$*$*EOD*$*$
$ checksum 000README.1ST
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 780094712
$ write sys$output "Creating AUTOLOGOUT$STARTUP.COM"
$ create AUTOLOGOUT$STARTUP.COM
$ DECK/DOLLARS="$*$*EOD*$*$"
$ define/system/exec autologout_dir pub_root:[private]
$ run   /detached -
        /process_name="Idle Daemon" -
        /privileges=(prmmbx,sysnam,world,oper,security,cmkrnl,share) -
                autologout_dir:autologout
$*$*EOD*$*$
$ checksum AUTOLOGOUT$STARTUP.COM
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 588980333
$ write sys$output "Creating AUTOLOGOUT$STARTUP.PAR"
$ create AUTOLOGOUT$STARTUP.PAR
$ DECK/DOLLARS="$*$*EOD*$*$"
5       !   5 minutes, the minimum idle limit
30      !  30 minutes, the default idle limit
240     ! 240 minutes (4 hours), the maximum idle limit
True    ! Disconnect is the default action on remove (boolean)
! usernames to treat opposite from the remove default above, one to a line.
$*$*EOD*$*$
$ checksum AUTOLOGOUT$STARTUP.PAR
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 795000728
$ write sys$output "Creating AUTOLOGOUT.PAS"
$ create AUTOLOGOUT.PAS
$ DECK/DOLLARS="$*$*EOD*$*$"
[ident('1.1'),
 inherit('SYS$Library:Starlet','SYS$Library:LIBRTL','SYS$Library:STRRTL',
                                                        'SYS$Library:OTSRTL')]
PROGRAM AutoLogout (Parameter,DEBUG_Info);
{
           This program will delete job trees that a user has left alone
        at a terminal for too long.

        Written by:     Chris Yoder
                for:    Hughes Aircraft Company
                        Space and Communications Group
                        Computing Technology Center
                        48-65-20
        starting on:    May 26, 1987
}

CONST   AutoLogout_Mailbox_Name = 'AutoLogout_Box';

        Debug_Info_Filename     = 'pub_root:[source.autologout]debug_info.dat';

{CONST Definitions
        AutoLogout_Mailbox_Name --- The name of the mailbox that we create
        Debug_Info_Filename ------- The name of the debug info filename.
}

TYPE    UnsignedByte = [Byte] 0..255;
        UnsignedWord = [Word] 0..65535;
        VUnsignedWord = [Volatile, Word] 0..65535;
        ThreeQuarterWord = [Bit(24),Unsafe] 0..16777215;
        UnsignedLongWord = [Bit(32)] Unsigned;
        UnsignedQuad = [QUAD,UNSAFE] RECORD
                         L0,L1:Unsigned;
                       END;

        Packed255       = Packed Array [1..255] of CHAR;

        String255       = Varying[255] of CHAR;
        String80        = Varying[ 80] of char;
        String12        = Varying[ 12] of char;

        JPIType = [Byte(12)] RECORD     (* This is the item list block that  *)
                                        (* is passed to $GETJPI.             *)
          BufferSize            : [Pos(00)] UnsignedWord;
          ItemCode              : [Pos(16)] UnsignedWord;
          BufferAddress         : [Pos(32), Unsafe] integer;
          ReturnLengthAddress   : [Pos(64), Unsafe] integer;
        END;

        IO_Block        = RECORD
                            IO_Stat, Count      : UnsignedWord;
                            Dev_Info            : INTEGER;
                          END; { RECORD IO_Block }

        User_Info_Rec   = RECORD
                            PID                 : Unsigned;
                            Master_PID          : Unsigned;
                            Minutes             : Integer;
                            Stat_MBox_Name      : String80;
                            Stat_MBox_Chan      : UnsignedWord;
                          END; { RECORD User_Info_Rec }

        Info_Ptr = [Volatile] ^Info_Type;
        Info_Type = RECORD
          Buffered_IO   : [Volatile] Integer;
          CPU_Time      : [Volatile] Integer;
          Direct_IO     : [Volatile] Integer;
          Job_Type      : [Volatile] Integer;
          Master_PID    : [Volatile] Integer;
          PID           : [Volatile] Integer;
          Terminal      : [Volatile] Varying [7] of CHAR;
          Phys_Term     : [Volatile] Varying [64] of CHAR;
          Username      : [Volatile] String12;
          Idle_Time     : [Volatile] Integer;
          Max_Idle_Time : [Volatile] Integer;
          Next          : Info_Ptr;
        END; { RECORD Info_Type }

        OPRList_Rec     = RECORD
                            OPC$B_MS_TYPE       : [Pos(00)] UnsignedByte;
                            OPC$B_MS_TARGET     : [Pos(08)] UnsignedByte;
                            OPC$L_MS_REQSTID    : [Pos(32)] Unsigned;
                            OPC$L_MS_TEXT       : [Pos(64)] Packed255;
                          END; { RECORD OPRList_Rec }

        User_List_Ptr   = ^User_List_Type;
        User_List_Type  = RECORD
                            Username    : String12;
                            Next        : User_List_Ptr;
                          END; { RECORD User_List_Type }

{Type definitions
        UnsignedWord ---- Declaration for an unsigned word type.
        VUnsignedWord --- Declaration for a volatile unsigned word type.
        UnsignedByte ---- Declaration for an unsigned byte type.
        JPIType --------- Declaration for an itemlist to pass to $GETJPI.
        IO_Block -------- An IO_Block to pass to various system services.
        User_Info_Rec --- A record of information from the user.
        Packed255 ------- A packed array type to equivalence to...
        String255 ------- A string 255 characters long
        String80 -------- A string type 80 characters long
        String12 -------- A string type 12 characters long
        Info_Ptr -------- A pointer to an Info_Type record.
            Buffered_IO ------- The ammount of buffered IO that the job has done
            CPU_Time ---------- The ammount of CPU Time that the job has gotten
            Direct_IO --------- The ammount of Direct IO that the job has gotten
            Job_Type ---------- The job type that this job is
            Master_PID -------- The Master PID of the job in question
            PID --------------- The PID of the current job
            Terminal ---------- The terminal associated with the job
            Phys_Term --------- The physical terminal associated with the job
            Username ---------- The username of the process
            Idle_Time --------- The idle time that the entire job has
            Max_Idle_Time ----- The max idle time that the process should get
            Next -------------- Pointer to the next record
        Info_Type ------- A record to hold all of the information about a
                                process in.
        OPRList_Rec ----- A list to send to $SNDOPR
        User_List_Ptr --- A pointer to a User_List_Type record
        User_List_Type -- An element of a list containing usernames
}

VAR     JPIFunction             : Array [1..9] of JPIType;
        DVIFunction             : Array [1..2] of JPIType;
        ReturnStatus            : Integer;

        Cur_Info_Top            : Info_Ptr;
        Old_Info_Top            : Info_Ptr;

        Subproc_Top             : Info_Ptr;
        Main_Proc_Top           : Info_Ptr;
        Ignore_Top              : Info_Ptr;

        CPU_Delta               : Integer :=  2;
        Buff_Delta              : Integer :=  1;
        Direct_Delta            : Integer :=  1;

        Delta_Time              : Integer := 60; {  1 minute  }

        AutoLogout_Channel      : [Volatile] UnsignedWord;
        QIO_Stat_Block          : [Volatile] IO_Block;

        Receive_String          : [Volatile] String80;

        Idle_Low                : [Volatile] Integer;
        Idle_Def                : [Volatile] Integer;
        Idle_High               : [Volatile] Integer;

        Parameter               : Text;

        DEBUG_Info              : [Volatile] Text;

        DEBUG_On                : [Volatile] Boolean := False;

        Exiting                 : [Volatile] Boolean := False;

        Msg_Desc                : [Volatile] DSC1$TYPE;
        Request_Id              : [Volatile] Integer := 0;

        Disco_to_Remove         : Boolean;
        Exception_Top           : User_List_Ptr := NIL;

{VAR Definitions
        JPIFunction ---------- Array of itemlist type so that the definition of
                                $GETJPIW in Starlet.Pas doesn't break.
        DVIFunction ---------- Array of itemlist type to pass an itemlist to
                                $GETDVI.
        ReturnStatus --------- Hold the status of the call to $GETJPIW.
        Cur_Info_Top --------- The top of the current information list.
        Old_Info_Top --------- The top of the list of processes that we knew
                                about last time.
        Subproc_Top ---------- The top of the subprocess list
        Main_Proc_Top -------- The top of the main process list
        Ignore_Top ----------- The top of the ignored process list
        CPU_Delta ------------ Amount of CPU time needed to be considered active
        Buff_Delta ----------- Amount of Buffered IO needed to be considered
                                active
        Direct_Delta --------- Amount of Direct IO needed to be considered
                                active
        Delta_Time ----------- The amount of time between checks
        AutoLogout_Channel --- A channel to talk to the Mailbox over.
        Receive_String ------- The string passed through the mailbox.
        Idle_Low ------------- The lowest value that idle time can have.
        Idle_Def ------------- The default value for idle time.
        Idle_High ------------ The highest amount of idle time that a user can
                                have.
        Parameter ------------ A file variable to open up the parameter file.
        DEBUG_Info ----------- A file variable to send information into.
        DEBUG_On ------------- True if we're in debug mode.
        Exiting -------------- Holds the value of whether we are exiting or not.
        Msg_Desc ------------- A message descriptor to send through $SNDOPR
        Request_Id ----------- The request id (for $SNDOPR messages)
        Disco_to_Remove ------ True if the default is to disconnect a process,
                                False if the default is to delete the process.
        Exception_Top ------- List of usernames who should be treated as if the
                                Disco_to_Remove flag was set opposite.
}

{******************************************************************************}

[Asynchronous, Unbound]
Procedure Get_User_Info;
  Forward;
  {
           This definition is needed by Queue_Mailbox_Read.
  }

{******************************************************************************}

  [asynchronous, unbound]
Function UnLink_VT (Device : [class_s] Packed Array [l1..u1:integer] of char)
                                                        : integer; extern;

{******************************************************************************}

Function Exclusive_Or (Condition1, Condition2 : Boolean) : Boolean;
  {
           This function provides an exclusive or in Pascal.  It is true iff one
        or the other condition is true, but not both.
  }

  BEGIN { Function Exclusive_Or }
    Exclusive_Or := ((Condition1 OR Condition2) AND
                     NOT (Condition1 AND Condition2));
  END; { Function Exclusive_Or }

{******************************************************************************}

[Asynchronous, Unbound]
Procedure Check (Status : Unsigned);
  {
           This procedure will make sure that it's OK to go on from here.  If
        we encountered an error, then we exit.
  }

  VAR   Msg_Buff        : OPRList_Rec;
        Message         : Varying [255] of CHAR;

  {VAR definitions
        Msg_Buff --- A list to send to $SNDOPR
        Message ---- A message to send out
  }

  BEGIN { Procedure Check }
    IF NOT ODD(Status)
      THEN
        BEGIN
          { Delete the mailbox if we can... }
          $DelMBX (AutoLogout_Channel);

          WRITEV (Message,CHR(7),'The Autologout Daemon is dead.');
          Message := PAD (Message,' ',255);

          Msg_Buff.OPC$B_MS_TYPE        := OPC$_RQ_RQST;
          Msg_Buff.OPC$B_MS_TARGET      := OPC$M_NM_OPER1;
          Msg_Buff.OPC$L_MS_REQSTID     := Request_Id;
          Request_Id := Request_Id + 1;
          Msg_Buff.OPC$L_MS_TEXT        := Message.Body;

          Msg_Desc.DSC$A_POINTER        := iaddress (Msg_Buff);
          Msg_Desc.DSC$W_MAXSTRLEN      := 41;

          { Don't care about the return status, just bomb if we can't gasp out
                this message. }
          $SNDOPR(%REF(Msg_Desc),);
          IF Debug_On
            THEN
              BEGIN
                Writeln (Debug_Info,'Autologout died unexpectedly.');
                Writeln (Debug_Info,'Status is: ',HEX(Status));
                Close (Debug_Info);
                LIB$Signal (Status);
              END;

          $Exit (Status);
        END;
  END; { Procedure Check }

{******************************************************************************}

[Asynchronous, Unbound]
Procedure Send_Oper1 (Message : String255);
  {
           Send a message out to all terminals with OPER1 enabled.
  }

  VAR   Msg_Buff        : OPRList_Rec;
        Mess_Text       : String255;
        Ret_Status      : Integer;

  {VAR definitions
        Message ------ A message to send out
        Msg_Buff ----- A list to send to $SNDOPR
        Mess_Text ---- The padded version of the message
        Ret_Status --- A status from a system routine
  }

  BEGIN { Procedure Send_Oper1 }
    Mess_Text := PAD (Message,' ',255);

    Msg_Buff.OPC$B_MS_TYPE      := OPC$_RQ_RQST;
    Msg_Buff.OPC$B_MS_TARGET    := OPC$M_NM_OPER1;
    Msg_Buff.OPC$L_MS_REQSTID   := Request_Id;
    Request_Id := Request_Id + 1;
    Msg_Buff.OPC$L_MS_TEXT      := Mess_Text.Body;

    Msg_Desc.DSC$A_POINTER      := iaddress (Msg_Buff);
    Msg_Desc.DSC$W_MAXSTRLEN    := 8 + LENGTH(Message);

    Ret_Status := $SNDOPR(%REF(Msg_Desc),);
    Check (Ret_Status :: Unsigned);
  END; { Procedure Send_Oper1 }

{******************************************************************************}

[Optimize(Inline),Asynchronous,Unbound]
Procedure Queue_Mailbox_Read;
  {
           Queue a read to a the mailbox.  This is here because calling a QIO
        is an ugly piece of code...
  }

  BEGIN { Procedure Queue_Mailbox_Read }
    Check ($QIO (       Chan    := AutoLogout_Channel,
                        Func    := IO$_READVBLK,
                        IOSB    := QIO_Stat_Block,
                        P1      := Receive_String.Body,
                        P2      := SIZE(Receive_String.Body),
                        ASTADR  := Get_User_Info));
  END; { Procedure Queue_Mailbox_Read }

{******************************************************************************}

Function User_on_Exception_List (Username : String12) : Boolean;
  {
           This function checks to see if the username passed in is on the
        exception list.
  }

  VAR   Found           : Boolean;
        User_Walker     : User_List_Ptr;

  {VAR Definitions
        Found --------- Hold the result of the function 'till the end
        User_Walker --- A pointer to walk the list
  }

  BEGIN { Function User_on_Exception_List }
    Found := False;
    User_Walker := Exception_Top;

    WHILE NOT Found AND (User_Walker <> NIL) DO
      IF (Username = User_Walker^.Username)
        THEN Found := TRUE
        ELSE User_Walker := User_Walker^.Next;

     User_on_Exception_List := Found;
  END; { Function User_on_Exception_List }

{******************************************************************************}

Procedure Dispose_List (VAR List_Top : Info_Ptr);
  {
           Dispose of the list handed to us, pass back a NIL pointer.
  }

  VAR   Walker          : Info_Ptr;

  {VAR Descriptions
        List_Top --- The top of the list to be disposed of.
        Walker ----- A pointer to walk an info list.
  }

  BEGIN { Procedure Dispose_List }
    WHILE (List_Top <> NIL) DO
      BEGIN
        Walker := List_Top;
        List_Top := List_Top^.Next;

        Walker^.Next := NIL;
        DISPOSE (Walker);
      END; { looping through the list }

    List_Top := NIL; { Just to make sure... }
  END; { Procedure Dispose_List }

{******************************************************************************}

Procedure Print_Info (List_Top : Info_Ptr);
  {
           Print the list handed to us.
  }

  VAR   Walker          : Info_Ptr;

  {VAR Descriptions
        List_Top --- The top of the list to be printed.
        Walker ----- A pointer to walk an info list.
  }

  BEGIN { Procedure Print_Info }
    Walker := List_Top;

    Writeln (Debug_Info,
'Master_PID   Username   Idle_Time Max_Idle_Time Direct_IO Buffered_IO CPU_Time'
   );
    Writeln (Debug_Info,
'---------- ------------ --------- ------------- --------- ----------- --------'
   );

    WHILE (Walker <> NIL) DO
      BEGIN
        With Walker^ DO
          BEGIN
            Writeln (Debug_Info,HEX(Master_PID):10,'|',Username:12,'|'
                ,TRUNC(Idle_Time/60):9,'|',TRUNC(Max_Idle_Time/60):13,'|'
                ,Direct_IO:9,'|',Buffered_IO:11,'|',CPU_Time);
          END;
        Walker := Walker^.Next;
      END; { looping through the list }

  END; { Procedure Print_Info }

{******************************************************************************}

Procedure Add_Lists (VAR List1_Top : Info_Ptr; VAR List2_Top : Info_Ptr);
  {
           This procedure will add the two lists together so that we preserve
        the information.
  }

  VAR   Info_Walker     : Info_Ptr;

  {VAR Descriptions
        Info_Walker --- A pointer to walk an info list.
  }

  BEGIN { Procedure Add_Lists }
    Info_Walker := List2_Top;
    WHILE (Info_Walker <> NIL) DO
      BEGIN
        List2_Top := List2_Top^.Next;

        Info_Walker^.Next := List1_Top;
        List1_Top := Info_Walker;

        Info_Walker := List2_Top;
      END; { WHILE (Info_Walker <> NIL) DO }
  END; { Procedure Add_Lists }

{******************************************************************************}

Procedure Get_Information (VAR List_Top : Info_Ptr);
  {
           This procedure will get all of the information that we need.
  }

  VAR   Info_Walker     : Info_Ptr;
        PID_Address     : Integer;
        Ret_Status      : Integer;

  {VAR Descriptions
        Info_Walker --- A pointer to walk an info list.
        PID_Address --- The PID that we are currently working on.
        Ret_Status ---- The status of a system call.
  }

  BEGIN { Procedure Get_Information }
    PID_Address := -1;
    List_Top := NIL;

    REPEAT { UNTIL ReturnStatus = SS$_NOMOREPROC }
      NEW (Info_Walker);
      Info_Walker^.Next := NIL;

      WITH Info_Walker^ DO
        BEGIN
          JPIFunction[1].ItemCode := JPI$_BUFIO;
          JPIFunction[1].BufferAddress := (iaddress (Buffered_IO)) :: integer;
          JPIFunction[1].BufferSize := 4;

          JPIFunction[2].ItemCode := JPI$_CPUTIM;
          JPIFunction[2].BufferAddress := (iaddress (CPU_Time)) :: integer;
          JPIFunction[2].BufferSize := 4;

          JPIFunction[3].ItemCode := JPI$_DIRIO;
          JPIFunction[3].BufferAddress := (iaddress (Direct_IO)) :: integer;
          JPIFunction[3].BufferSize := 4;

          JPIFunction[4].BufferSize := 4;
          JPIFunction[4].ItemCode := JPI$_JOBTYPE;
          JPIFunction[4].BufferAddress := (iaddress (Job_Type)) :: integer;

          JPIFunction[5].BufferSize := 4;
          JPIFunction[5].ItemCode := JPI$_MASTER_PID;
          JPIFunction[5].BufferAddress := (iaddress (Master_Pid)) :: integer;

          JPIFunction[6].BufferSize := 4;
          JPIFunction[6].ItemCode := JPI$_PID;
          JPIFunction[6].BufferAddress := (iaddress (Pid)) :: integer;

          JPIFunction[7].BufferSize := 7;
          JPIFunction[7].ItemCode := JPI$_TERMINAL;
          JPIFunction[7].BufferAddress := (iaddress (Terminal.Body)) :: integer;
          JPIFunction[7].ReturnLengthAddress := (iaddress (Terminal.Length))
                                                                :: integer;

          JPIFunction[8].BufferSize := 12;
          JPIFunction[8].ItemCode := JPI$_USERNAME;
          JPIFunction[8].BufferAddress := (iaddress (Username.Body)) :: integer;
          JPIFunction[8].ReturnLengthAddress := (iaddress (Username.Length))
                                                                :: integer;

          JPIFunction[9].BufferSize := 0;
          JPIFunction[9].ItemCode := 0;
          JPIFunction[9].BufferAddress := 0;
          JPIFunction[9].ReturnLengthAddress := 0;

          ReturnStatus := $GETJPIW (,PID_Address :: Unsigned,,JPIFunction,,,);

        END; { WITH Info_Walker^ DO }

      IF ReturnStatus <> SS$_NOMOREPROC
        THEN
          BEGIN
            Check (ReturnStatus);

            Info_Walker^.Phys_Term := '';

            IF LENGTH(Info_Walker^.Terminal) > 0
              THEN
                BEGIN
                  DVIFunction[1].ItemCode := DVI$_TT_PHYDEVNAM;
                  DVIFunction[1].BufferSize := 64;
                  DVIFunction[1].BufferAddress := (iaddress
                                (Info_Walker^.Phys_Term.Body)) :: integer;
                  DVIFunction[1].ReturnLengthAddress := (iaddress
                                (Info_Walker^.Phys_Term.Length)) :: integer;

                  DVIFunction[2].ItemCode := 0;
                  DVIFunction[2].BufferSize := 0;
                  DVIFunction[2].BufferAddress := 0;
                  DVIFunction[2].ReturnLengthAddress := 0;

                  Ret_Status := $GetDVI (,,Info_Walker^.Terminal,
                                                        DVIFunction,,,,);
                  IF NOT ODD (Ret_Status)
                    THEN Info_Walker^.Phys_Term := '';
                END; { Finding out the physical device name. }

            Info_Walker^.Next := List_Top;
            List_Top := Info_Walker;
          END  { Writing out information if we haven't reached the end of the
                list of processes. }
        ELSE DISPOSE (Info_Walker);  { We didn't want this information, free
                                        up the memory. }
    UNTIL ReturnStatus = SS$_NOMOREPROC;
  END; { Procedure Get_Information }

{******************************************************************************}

Procedure Prune_Information (VAR Info_Top : Info_Ptr);
  {
           This procedure will prune out all of the cruft in the list,
        including all of the detached, network, and batch processes.
        It *will* keep all processes associated with a terminal of any sort,
        including the subprocesses associated with it.
  }

  VAR   Info_Walker     : Info_Ptr;
        Info_Walker2    : Info_Ptr;
        Flag            : Boolean;

  {VAR Descriptions
        Info_Walker ---- A pointer to walk an info list.
        Info_Walker2 --- A pointer to walk an info list.
        Flag ----------- A boolean flag to be used in determining if we should
                                go on or not.
  }

  BEGIN { Procedure Prune_Information }
    { First we find an info_top that is good. }
    Flag := (Info_Top <> NIL);
    IF Flag
      THEN Flag := (Info_Top^.Job_Type <> JPI$K_LOCAL) AND
                   (Info_Top^.Job_Type <> JPI$K_DIALUP) AND
                   (Info_Top^.Job_Type <> JPI$K_REMOTE);

    WHILE Flag DO
      BEGIN
        Info_Walker := Info_Top;
        Info_Top := Info_Top^.Next;

        Info_Walker^.Next := NIL;
        DISPOSE (Info_Walker);

        Flag := (Info_Top <> NIL);
        IF Flag
          THEN Flag := (Info_Top^.Job_Type <> JPI$K_LOCAL) AND
                       (Info_Top^.Job_Type <> JPI$K_DIALUP) AND
                       (Info_Top^.Job_Type <> JPI$K_REMOTE);
      END; { Checking the top of the list }

    Info_Walker := Info_Top;
    WHILE (Info_Walker <> NIL) DO
      IF (Info_Walker^.Next <> NIL)
        THEN
          BEGIN
            Info_Walker2 := Info_Walker^.Next;
            IF (Info_Walker2^.Job_Type <> JPI$K_LOCAL) AND
               (Info_Walker2^.Job_Type <> JPI$K_DIALUP) AND
               (Info_Walker2^.Job_Type <> JPI$K_REMOTE)
              THEN { We want to throw this record away }
                BEGIN
                  Info_Walker^.Next := Info_Walker2^.Next;

                  Info_Walker2^.Next := NIL;
                  DISPOSE (Info_Walker2);
                END { throwing the record away }
              ELSE Info_Walker := Info_Walker^.Next;
          END { Running through the rest of the list }
        ELSE Info_Walker := NIL;
  END; { Procedure Prune_Information }

{******************************************************************************}

Procedure Split_Information (   VAR Orig_List_Top       : Info_Ptr;
                                VAR Master_Top          : Info_Ptr;
                                VAR Ignore_Top          : Info_Ptr;
                                VAR Subproc_Top         : Info_Ptr);

  {
           This procedure will split the original list up into a list of
        subprocesses and master processes that are currently connected to a
        physical device and ignored processes.
  }

  VAR   Walker          : Info_Ptr;
        Walker2         : Info_Ptr;

  {VAR Descriptions
        Orig_List_Top --- The top of the original list
        Subproc_Top ----- The top of the new subprocess list
        Master_Top ------ The top of the list of master processes
        Ignore_Top ------ The top of the list of ignored processes
        Walker ---------- A pointer to walk an info list.
        Walker2 --------- A pointer to walk an info list.
  }

  BEGIN { Procedure Split_Information }
    Walker := Orig_List_Top;
    Subproc_Top := NIL;
    Master_Top := NIL;
    Ignore_Top := NIL;

    WHILE (Walker <> NIL) DO
      IF (LENGTH (Walker^.Terminal) = 0)
        THEN { It's a subprocess. }
          BEGIN
            Walker2 := Walker;
            Walker := Walker^.Next;

            Walker2^.Next := Subproc_Top;
            Subproc_Top := Walker2;
          END { Adding a subprocess to the list }
        ELSE
          IF (LENGTH (Walker^.Phys_Term) <> 0)
            THEN { The job is connected to a real live terminal. }
              BEGIN
                Walker2 := Walker;
                Walker := Walker^.Next;

                Walker2^.Next := Master_Top;
                Master_Top := Walker2;
              END { Adding a master process to the list }
            ELSE { It's disconnected. }
              BEGIN
                Walker2 := Walker;
                Walker := Walker^.Next;
                Walker2^.Next := Ignore_Top;
                Ignore_Top := Walker2;
              END; { Collecting the discoed jobs. }

  END; { Procedure Split_Information }

{******************************************************************************}

Procedure Combine_Trees (VAR Master_Top : Info_Ptr;
                         VAR Subproc_Top : Info_Ptr);
  {
           Combine the master and subprocess lists so that only one Master PID
        is in the list.
  }

  VAR   Walker          : Info_Ptr;
        Walker2         : Info_Ptr;

  {VAR Descriptions
        Walker ---------- A pointer to walk an info list.
        Walker2 --------- A pointer to walk an info list.
        Master_Top ------ Top of the list of master processes.
        Subproc_Top ----- Top of the list of subprocesses.
  }

  BEGIN { Procedure Combine_Trees }
    Walker := Master_Top;
    WHILE (Walker <> NIL) DO
      BEGIN
        Walker2 := Subproc_Top;
        WHILE (Walker2 <> NIL) DO
          BEGIN
            IF (Walker^.PID = Walker2^.Master_PID)
              THEN
                BEGIN
                  Walker^.Buffered_IO := Walker^.Buffered_IO
                                                        + Walker2^.Buffered_IO;
                  Walker^.Direct_IO := Walker^.Direct_IO + Walker2^.Direct_IO;
                  Walker^.CPU_Time := Walker^.CPU_Time + Walker2^.CPU_Time;
                END;

            Walker2 := Walker2^.Next;
          END; { Looping through the list of subprocesses. }

        Walker^.Idle_Time := 0;
        Walker := Walker^.Next;
      END; { Looping through the processes in the list }

  END; { Procedure Combine_Trees }

{******************************************************************************}

Procedure Get_Idle_Time (VAR New_Top : Info_Ptr; Old_Top : Info_Ptr);
  {
           Compare the new information with the old information that we've got
        saved up.  If we have the old process stored away, then check if it's
        done anything between then and now.  If yes, reset the idle time to 0,
        if no, add delta time to the old idle time.  Once we are finished
        comparing, store all of the information in the new list.
  }

  VAR   Walker          : Info_Ptr;
        Walker2         : Info_Ptr;

        Found           : Boolean;

  {VAR Descriptions
        New_Top --------- This information that we just collected.
        Old_Top --------- The old information.
        Walker ---------- A pointer to walk an info list.
        Walker2 --------- A pointer to walk an info list.
        Found ----------- Indicates that we found a record.
  }

  BEGIN { Procedure Get_Idle_Time }
    Walker := New_Top;
    WHILE (Walker <> NIL) DO
      BEGIN
        Walker2 := Old_Top;

        Found := False;
        WHILE (Walker2 <> NIL) DO
          BEGIN
            IF (Walker^.Master_PID = Walker2^.Master_PID)
              THEN
                BEGIN
                  Found := TRUE;

                  IF (Walker^.CPU_Time >= Walker2^.CPU_Time + CPU_Delta) OR
                     (Walker^.Buffered_IO >= Walker2^.Buffered_IO+Buff_Delta) OR
                     (Walker^.Direct_IO >= Walker2^.Direct_IO + Direct_Delta) OR
                     (LENGTH (Walker^.Phys_Term) = 0) { Discoed aren't idle. }
                    THEN Walker^.Idle_Time := 0
                    ELSE Walker^.Idle_Time := Walker2^.Idle_Time + Delta_Time;

                  Walker^.Max_Idle_Time := Walker2^.Max_Idle_Time;
                END; { Processing like Master_PIDs }

            IF NOT Found
              THEN Walker2 := Walker2^.Next
              ELSE Walker2 := NIL; { Bounce out of the loop a bit more quickly }

          END; { WHILE (Walker2 <> NIL) DO }

        IF NOT Found
          THEN Walker^.Max_Idle_Time := Idle_Def;

        Walker := Walker^.Next;
      END; { WHILE (Walker <> NIL) DO }

  END; { Procedure Get_Idle_Time }

{******************************************************************************}

Procedure Nuke_Idle_Users (List_Top : Info_Ptr);
  {
           This procedure will figure out who's being idle and will blow away
        the user...
  }

  VAR   Walker          : Info_Ptr;

        Message         : String255;

        Hold_Action     : String80;

        BRDCST_Flags    : Flags_Input$Type;   { From Starlet }

        Ret_Status      : Integer;

  {VAR definitions
        List_Top ------- The top of the list of users to be nuked.
        Walker --------- A pointer to walk an info list.
        Message -------- The message that gets sent to the user.
        Hold_Action ---- Holds the action that we did to the user.
        BRDCST_Flags --- Hold the flags to be used with a call to $BRKTHRU
        Ret_Status ----- Hold a return status 'till we figure out what to do
                                with it...
  }

  BEGIN { Procedure Nuke_Idle_Users }
    Walker := List_Top;
    WHILE (Walker <> NIL) DO
      BEGIN
        IF (Walker^.Idle_Time >= (Walker^.Max_Idle_Time/2) - Delta_Time) AND
           (Walker^.Idle_Time < (Walker^.Max_Idle_Time/2))
          THEN
            BEGIN
              WriteV (Message,CHR(7),'WARNING: You have exceeded 1/2 of your ',
                      'idle time limit of ',TRUNC(Walker^.Max_Idle_Time/60):1,
                      ' minutes.');
              Ret_Status := $BRKTHRU (,Message,Walker^.Phys_Term,
                                                        BRK$C_DEVICE,,,,,,,);
            END; { 1/2 time warning. }

        IF (Walker^.Idle_Time >= Walker^.Max_Idle_Time - 300 - Delta_Time) AND
           (Walker^.Idle_Time < Walker^.Max_Idle_Time - 300) AND
           (Walker^.Max_Idle_Time > 600)
          THEN
            BEGIN
              WRITEV (Message,CHR(7),'WARNING: Your process will exceed it''s ',
                        'idle limit in 5 minutes');
              Ret_Status := $BRKTHRU (,Message,Walker^.Phys_Term,
                                                        BRK$C_DEVICE,,,,,,,);
            END; { Five minute warning... }

        IF (Walker^.Idle_Time >= Walker^.Max_Idle_Time - Delta_Time) AND
           (Walker^.Idle_Time < Walker^.Max_Idle_Time)
          THEN
            BEGIN
              WRITEV (Message,CHR(7),CHR(7),CHR(7),'WARNING: Your process ',
                        'will exceed it''s idle limit in ',Delta_Time:1,
                        ' seconds.');
              Ret_Status := $BRKTHRU (,Message,Walker^.Phys_Term,
                                                        BRK$C_DEVICE,,,,,,,);
            END; { Last chance! }

        IF (Walker^.Idle_Time >= Walker^.Max_Idle_Time)
          THEN
            BEGIN
              { Send the user a message telling him that he's gone. }
              WriteV (Message,CHR(7),'WARNING: You have exceeded your ',
                      ' idle time limit of ',TRUNC(Walker^.Max_Idle_Time/60):1,
                      ' minutes.');

              BRDCST_Flags.BRK$V_Erase_Lines := 24;
              BRDCST_Flags.BRK$V_Screen := True;
              BRDCST_Flags.BRK$V_Bottom := True;
              BRDCST_Flags.BRK$V_NoRefresh := True;
              BRDCST_Flags.BRK$V_Cluster := False;
              Ret_Status := $BRKTHRU (,Message,Walker^.Phys_Term,BRK$C_DEVICE,,,
                        %IMMED BRDCST_Flags,,,,);

              { Zero out his idle time just in case we *do* disconnect him to
                avoid problems if he quickly reconnects. }
              Walker^.Idle_Time := 0;

              { Actually get rid of the idle user. }
              IF Exclusive_Or ( Disco_To_Remove,
                                User_on_Exception_List(Walker^.Username))
                THEN { Disco the user }
                  BEGIN
                    { Try our best to disconnect the user, if that fails,
                        blow the user away. }
                    Ret_Status := UnLink_VT (Walker^.Terminal);
                    IF NOT ODD (Ret_Status)
                      THEN
                        BEGIN
                          Ret_Status := $DelPrc (Walker^.Master_PID :: Unsigned,
   );
                          Hold_Action := 'deleted';
                        END { If there was an error, just blow the user away. }
                      ELSE Hold_Action := 'disconnected';
                  END { Disconnecting the user. }
                ELSE { Just blow the user out of the water. }
                  BEGIN
                    Ret_Status := $DelPrc (Walker^.Master_PID :: Unsigned,);
                    Hold_Action := 'deleted';
                  END; { Blowing the user away. }

              WRITEV (Message,'User ',Walker^.Username,' on terminal ',
                        Walker^.Terminal,' was ',Hold_Action,' for exceeding ',
                        'personal idle time limit of ',
                        TRUNC(Walker^.Max_Idle_Time/60):1,' minutes.');

              Send_Oper1 (Message);
            END; { Bye bye user }

        Walker := Walker^.Next;
      END; { WHILE (Walker <> NIL) DO }
  END; { Procedure Nuke_Idle_Users }

{******************************************************************************}

[Asynchronous, Unbound]
Function Trim_All_Spaces (Source_String : String80) : String80;
  {
           This function will trim all spaces from the beginning and end of
        Source_String.
  }

  VAR   Hold_Result     : String80;
        Where           : Integer;
        Len             : Integer;
        Ret_Status      : Unsigned;
        Flag            : Boolean;

  {VAR Description
        Source_String --- The original string
        Hold_Result ----- Hold the result of the function while we play with it
        Where ----------- Hold the location of the first non-space
        Len ------------- Hold the length of a string
        Ret_Status ------ Hold the return status of a system call
        Flag ------------ Hold a boolean flag
  }

  BEGIN  { Function Trim_All_Spaces }
    Hold_Result := Source_String;

    { Get rid of any spaces on the front end of the string. }
    Len := LENGTH (Hold_Result);
    Where := STR$Find_First_Not_In_Set (Hold_Result,' ');

    IF Where <= Len
      THEN Hold_Result := SUBSTR (Hold_Result,Where,Len-Where+1);

    { Now clean off the end. }
    Len := LENGTH(Hold_Result);
    Flag := Len > 0;
    IF Flag
      THEN Flag := Hold_Result[Len] = ' ';

    WHILE Flag DO
      BEGIN
        Len := Len - 1;
        Flag := Len > 0;
        IF Flag
          THEN Flag := Hold_Result[Len] = ' ';
      END; { WHILE Flag DO }

    Hold_Result := SUBSTR (Hold_Result,1,Len);

    Trim_All_Spaces := Hold_Result;
  END; { Function Trim_All_Spaces }

{******************************************************************************}

[Asynchronous, Unbound]
Function Convert_STR_INT (In_String : String80) : INTEGER;
  {
           This function will convert a string into an integer.
  }
  VAR   Hold_Res        : INTEGER;
        Len             : INTEGER;
        Ret_Status      : UNSIGNED;

  {VAR Descriptions
        In_String ---- The input string
        Hold_Res ----- The result of the function (hold 'till finished)
        Len ---------- The length of a string
        Ret_Status --- A status from a system routine
  }

  BEGIN { Function Convert_STR_INT }
    Ret_Status := OTS$CVT_TI_L (In_String,Hold_Res);

    { Sombody is trying to mess with our mind, so pass back a 0. }
    IF (Ret_Status = OTS$_INPCONERR)
      THEN Hold_Res := 0;

    Convert_STR_INT := Hold_Res;
  END; { Function Convert_STR_INT }

{******************************************************************************}

[Asynchronous, Unbound]
Procedure Set_Idle_Limit (      VAR Error_String : String80;
                                VAR User_Info : User_Info_Rec);
  {
           This procedure will take the input information and actually change
        the correct tree's Max_Idle_Time.
  }

  VAR   Walker  : Info_Ptr;

        Found   : Boolean;

  {VAR Descriptions
        Error_String --- The error string that might get passed back to the user
        User_Info ------ The user information record
        Walker --------- A pointer to walk an info list
        Found ---------- Tells whether we've found something or not
  }

  BEGIN { Procedure Set_Idle_Limit }
    Walker := Old_Info_Top;
    Found := False;

    WHILE (Walker <> NIL) AND NOT Found DO
      BEGIN
        IF Walker^.Master_PID = User_Info.Master_PID
          THEN
            BEGIN
              Walker^.Max_Idle_Time := User_Info.Minutes * 60;
              Found := True;
            END;

        Walker := Walker^.Next;
      END; { WHILE (Walker <> NIL) AND NOT Found DO }

    IF NOT Found
      THEN { This user *just* logged in, and isn't in our records yet. }
        BEGIN
          Walker := NIL;
          NEW (Walker);
          Walker^.Master_PID := User_Info.Master_PID :: Integer;
          Walker^.Max_Idle_Time := User_Info.Minutes * 60;
          Walker^.Idle_Time := 0;
          Walker^.Buffered_IO := 0;
          Walker^.CPU_Time := 0;
          Walker^.Direct_IO := 0;
          Walker^.Next := Old_Info_Top;
          Old_Info_Top := Walker;
        END;
  END; { Procedure Set_Idle_Limit }

{******************************************************************************}

[Asynchronous, Unbound]
Function Get_Idle_Limit (User_Info : User_Info_Rec) : INTEGER;
  {
           This function will find the idle limit for a user who we only know
        the username and pid for.
  }

  VAR   Walker          : Info_Ptr;

        Found           : Boolean;

        Hold_Res        : Integer;

  {VAR Descriptions
        User_Info --- The user information record
        Walker ------ A pointer to walk an info list
        Found ------- Tells whether we've found something or not
        Hold_Res ---- Holds the result of the function
  }

  BEGIN { Function Get_Idle_Limit }
    Walker := Old_Info_Top;
    Found := False;

    WHILE (Walker <> NIL) AND NOT Found DO
      BEGIN
        IF Walker^.Master_PID = User_Info.Master_PID
          THEN
            BEGIN
              Hold_Res := Walker^.Max_Idle_Time;
              Found := True;
            END;

        Walker := Walker^.Next;
      END; { WHILE (Walker <> NIL) AND NOT Found DO }

    IF NOT Found
      THEN { This user *just* logged in, and isn't in our records yet. }
        Hold_Res := Idle_Def;

    Get_Idle_Limit := Hold_Res;
  END; { Function Get_Idle_Limit }

{******************************************************************************}

Procedure Get_User_Info;
  {
           This is the routine that the user interfaces with.  It reads the
        mailbox and does the appropriate thing with the message.
  }

  TYPE  Action_Type = (Set_Idle,Check_Limits,Shutdown);

  {TYPE Definitions
        Action_Type --- This is the type of action that we want to perform
  }

  VAR   User_Info       : User_Info_Rec;

        Message         : String255;
        Hold_String     : String80;
        Error_String    : String80;

        Ret_Status      : Integer;
        I,J             : Integer;

        JPIFun          : Array [1..3] of JPIType;
        Username        : Varying [12] of CHAR;
        Priv_Mask       : [Volatile] PRV$TYPE;

        Action          : Action_Type;

        Cur_Idle        : Integer;

  {VAR Definitions
        User_Info ------ Information passed from a user though a mailbox.
        Hold_String ---- A string to hold a string while we process it.
        Error_String --- A string to hold error messages to the user in.
        I,J ------------ Looping variables.
        Ret_Status ----- Hold the return status of a system service.
        JPIFun --------- Hold a call to $GETJPI.
        Message -------- The message that gets sent to the user.
        Priv_Mask ------ Holds a privilege mask.
        Action --------- The action that the user is requesting.
  }

  BEGIN { Prcedure Get_User_Info }
    Check (QIO_Stat_Block.IO_Stat);

    User_Info.PID := QIO_Stat_Block.Dev_Info;

    Receive_String.Length := QIO_Stat_Block.Count;
    Receive_String := Trim_All_Spaces (Receive_String :: String80);

    IF Debug_On
      THEN Writeln (Debug_Info,'AST Delivered.');

    Message := '';

    IF LENGTH (Receive_String) < 5
      THEN Hold_String := PAD (Receive_String,' ',5)
      ELSE
        BEGIN
          Hold_String := '';
          FOR I := 1 to 5 DO Hold_String := Hold_String + Receive_String[I];
        END;

    IF (Hold_String = 'CHECK')
      THEN Action := Check_Limits
      ELSE
        IF (Hold_String = 'STOP ')
          THEN Action := Shutdown
          ELSE Action := Set_Idle;

    CASE Action OF
      Set_Idle : { Set the user's idle time limit. }
        BEGIN
          IF Debug_On
            THEN Writeln (Debug_Info,'Setting idle limit.');

          { Figure out Master_PID of sender. }
          Check (LIB$GETJPI (JPI$_MASTER_PID,User_Info.PID,,
                                                User_Info.Master_PID,,));

          { Figure out the idle time sent by the user and where to send his
                status to. }
          I := 1;
          Hold_String := '';
          REPEAT
            IF (Receive_String[I] = ' ')
              THEN Hold_String := SUBSTR (Receive_String,1,I-1);

            I := I + 1;
          UNTIL ((I >= LENGTH(Receive_String)) OR (LENGTH(Hold_String) > 0));
          User_Info.Minutes := Convert_STR_INT (Hold_String);

          IF LENGTH (Receive_String) > I
            THEN User_Info.Stat_MBox_Name :=
                        SUBSTR (Receive_String,I,LENGTH(Receive_String)-I+1)
            ELSE User_Info.Stat_MBox_Name := '';

          Error_String := '';

          { Parse the amount of time given to us. }
          IF (User_Info.Minutes < 1)
            THEN
              BEGIN
                User_Info.Minutes := TRUNC(Idle_Def/60);
                Error_String := 'Default used. ';
              END; { IF (User_Info.Minutes < 1) }

          IF (User_Info.Minutes * 60 < Idle_Low)
            THEN
              BEGIN
                User_Info.Minutes := TRUNC(Idle_Low/60);
                Error_String := 'Min idle time exceeded. ';
              END; { IF (User_Info.Minutes < Idle_Low) }

          IF (User_Info.Minutes * 60 > Idle_High)
            THEN
              BEGIN
                JPIFun[1].BufferSize := 8;
                JPIFun[1].ItemCode := JPI$_PROCPRIV;
                JPIFun[1].BufferAddress := (iaddress (Priv_Mask)) :: integer;

                JPIFun[2].BufferSize := 12;
                JPIFun[2].ItemCode := JPI$_USERNAME;
                JPIFun[2].BufferAddress := (iaddress (Username.Body))
                                                                :: integer;
                JPIFun[2].ReturnLengthAddress := (iaddress (Username.Length))
                                                                :: integer;

                JPIFun[3].BufferSize := 0;
                JPIFun[3].ItemCode := 0;
                JPIFun[3].BufferAddress := 0;
                JPIFun[3].ReturnLengthAddress := 0;

                Check ($GETJPI (,User_Info.PID,,JPIFun,,,));

                IF (Priv_Mask.PRV$V_SYSPRV OR Priv_Mask.PRV$V_EXQUOTA)
                  THEN
                    BEGIN
                      WRITEV (Message,CHR(7),'Process',HEX(User_Info.PID),
                        ', username ',Username,' is exceeding system maximum.');
                      Send_Oper1 (Message);
                    END { Telling on a user...  :-) }
                  ELSE
                    BEGIN
                      User_Info.Minutes := TRUNC(Idle_High/60);
                      Error_String := 'Max idle time exceeded. ';
                    END; { IF the user doesn't have EXQUOTA or SYSPRV }
              END; { IF (User_Info.Minutes > Idle_High) }

          { Actually set the user's information. }
          Set_Idle_Limit (Error_String,User_Info);

          { Now we send back a status report! } { BUT Don't fail if we have
                                                        a bogus MBox!!!! }
          Hold_String := '';
          Hold_String := PAD (Hold_String,' ',80);
          Hold_String := '';

          WRITEV (Hold_String,Error_String,'Idle limit set to ',
                User_Info.Minutes:1,' minutes.');
          Hold_String := PAD (Hold_String,' ',80);

          IF Debug_On
            THEN
              BEGIN
                Writeln (Debug_Info,'Master_PID => ',HEX(User_Info.Master_PID));
                Writeln (Debug_Info,Hold_String);
              END;

          Ret_Status := $Assign (       Devnam := User_Info.Stat_MBox_Name,
                                        Chan := User_Info.Stat_MBox_Chan);
          IF Ret_Status = SS$_NORMAL
            THEN
              BEGIN
                Ret_Status := $QIOW (   Chan    := User_Info.Stat_MBox_Chan,
                                        Func    := IO$_WRITEVBLK,
                                        IOSB    := QIO_Stat_Block,
                                        P1      := Hold_String.Body,
                                        P2      := LENGTH(Hold_String.Body));
                Ret_Status := $Dassgn (User_Info.Stat_MBox_Chan);
              END; { Sending back a status report. }

          { Requeue the mailbox read QIO }
          Queue_Mailbox_Read;
        END; { IF (Receive_String <> 'STOP') THEN }
      Shutdown :
        { See if the Master PID has SYSPRV turned on, if yes, then exit. }
        BEGIN
          IF Debug_On
            THEN Writeln (Debug_Info,'Attempt to shutdown Autologout.');

          JPIFun[1].BufferSize := 8;
          JPIFun[1].ItemCode := JPI$_PROCPRIV;
          JPIFun[1].BufferAddress := (iaddress (Priv_Mask)) :: integer;

          JPIFun[2].BufferSize := 12;
          JPIFun[2].ItemCode := JPI$_USERNAME;
          JPIFun[2].BufferAddress := (iaddress (Username.Body)) :: integer;
          JPIFun[2].ReturnLengthAddress := (iaddress (Username.Length))
                                                                :: integer;

          JPIFun[3].BufferSize := 0;
          JPIFun[3].ItemCode := 0;
          JPIFun[3].BufferAddress := 0;
          JPIFun[3].ReturnLengthAddress := 0;

          Check ($GETJPI (,User_Info.PID,,JPIFun,,,));

          IF (Priv_Mask.PRV$V_SYSPRV AND Priv_Mask.PRV$V_SECURITY)
            THEN
              BEGIN
                Exiting := TRUE;
                IF Debug_On
                  THEN
                    BEGIN
                      Writeln (Debug_Info,'Autologout shutdown.');
                      Close (Debug_Info);
                    END;

                { Send a message to OPER1 terminals when we *do* exit. }
                WRITEV (Message,CHR(7),'Autologout daemon shutdown by process',
                        HEX(User_Info.PID),', username ',Username,'.');
                Send_Oper1 (Message);
                Check ($DelMBX (AutoLogout_Channel));
                $Exit (SS$_Normal);
              END { Shutting down }
            ELSE { Requeue the QIO }
              BEGIN
                IF Debug_On
                  THEN Writeln (Debug_Info,'Failed shutdown.');

                WRITEV (Message,CHR(7),'Failed attempt to shutdown Autologout ',
                        'daemon by process',HEX(User_Info.PID),', username ',
                        Username,'.');
                Send_Oper1 (Message);

                Queue_Mailbox_Read;
              END; { Telling the world that some doof tried to shut us down. }
        END; { Dealing with a shutdown attempt. }
      Check_Limits :
        BEGIN;
          IF Debug_On
            THEN Writeln (Debug_Info,'Checking limits.');

          { Figure out Master_PID of sender. }
          Check (LIB$GETJPI (JPI$_MASTER_PID,User_Info.PID,,
                                                User_Info.Master_PID,,));

          IF LENGTH (Receive_String) > 6
            THEN User_Info.Stat_MBox_Name :=
                        SUBSTR (Receive_String,7,LENGTH(Receive_String)-6)
            ELSE User_Info.Stat_MBox_Name := '';

          Cur_Idle := Get_Idle_Limit (User_Info);

          Message := '';
          Message := PAD (Message,' ',SIZE(Message.Body));
          Message := '';

          WRITEV (Message,'      Maximum Idle time limits:',CHR(13),CHR(10),
                'Minimum = ',ROUND(Idle_Low/60):1,' minutes, ',CHR(13),CHR(10),
                'Current = ',ROUND(Cur_Idle/60):1,' minutes, ',CHR(13),CHR(10),
                'Maximum = ',ROUND(Idle_High/60):1,' minutes.');
          Ret_Status := $Assign (       Devnam := User_Info.Stat_MBox_Name,
                                        Chan := User_Info.Stat_MBox_Chan);
          IF Ret_Status = SS$_NORMAL
            THEN
              BEGIN
                Ret_Status := $QIOW (   Chan    := User_Info.Stat_MBox_Chan,
                                        Func    := IO$_WRITEVBLK,
                                        IOSB    := QIO_Stat_Block,
                                        P1      := Message.Body,
                                        P2      := LENGTH(Message.Body));
                Ret_Status := $Dassgn (User_Info.Stat_MBox_Chan);
              END; { Sending back a status report. }

          IF Debug_On
            THEN
              BEGIN
                Writeln (Debug_Info,'Master PID => ',HEX(User_Info.Master_PID));
                Writeln (Debug_Info,Message);
              END;

          { Requeue the mailbox read QIO }
          Queue_Mailbox_Read;
        END; { Telling the user what the various options are. }
    END; { CASE Action OF }
  END; { Procedure Get_User_Info }

{******************************************************************************}

Procedure Initialize;
  {
           This procedure will initialize a few things for us.
  }

  VAR   User_Walker     : User_List_Ptr;

  {VAR Descriptions
        User_Walker --- Hold an exception list element while we build the list.
  }

  BEGIN { Procedure Initialize }
  IF Debug_On
    THEN
      BEGIN
        Open (Debug_Info,Debug_Info_Filename,New,SHARING := ReadOnly);
        Rewrite (Debug_Info);
      END; { Opening up the debug info file. }

    Check ($CREMBX (    Prmflg := 1,
                        Chan := AutoLogout_Channel,
                        Lognam := AutoLogout_Mailbox_Name));

    Msg_Desc.DSC$B_DTYPE := DSC$K_DTYPE_T;
    Msg_Desc.DSC$B_CLASS := DSC$K_CLASS_S;

    Open (Parameter,'AutoLogout_Dir:AutoLogout$Startup.Par',OLD);
    Reset (Parameter);
    ReadLn (Parameter,Idle_Low);
    ReadLn (Parameter,Idle_Def);
    ReadLn (Parameter,Idle_High);
    ReadLn (Parameter,Disco_to_Remove);
    ReadLn (Parameter);

    Exception_Top := NIL;
    WHILE NOT EOF(Parameter) DO
      BEGIN
        { Get a username. }
        New (User_Walker);
        ReadLn (Parameter,User_Walker^.Username);

        {    Do this for compares later.  We need the input username string to
          look like the username string that GETJPI returns. }
        Check (STR$Upcase (User_Walker^.Username, User_Walker^.Username));
        User_Walker^.Username := PAD (User_Walker^.Username, ' ', 12);

        { Push the item on the list. }
        User_Walker^.Next := Exception_Top;
        Exception_Top := User_Walker;
        User_Walker := NIL;
      END; { WHILE NOT EOF(Parameter) DO }

    Close (Parameter);

    Idle_Low := Idle_Low * 60;
    Idle_Def := Idle_Def * 60;
    Idle_High := Idle_High * 60;

    Queue_Mailbox_Read;
  END; { Procedure Initialize }

{******************************************************************************}

BEGIN { MAIN Program AutoLogout }
  Initialize;
  Old_Info_Top := NIL;

  WHILE NOT Exiting DO
    BEGIN
      { Carefully turn off delivery of ASTs while we get information. }
      Check ($SetAST(0));

      IF Debug_On
        THEN Writeln (Debug_Info,'Getting information.');
      Get_Information (Cur_Info_Top);
      Prune_Information (Cur_Info_Top);
      Split_Information (Cur_Info_Top,Main_Proc_Top,Ignore_Top,Subproc_Top);
      Combine_Trees (Main_Proc_Top,Subproc_Top);

      IF Debug_On
        THEN Writeln (Debug_Info,'Getting idle time.');
      Get_Idle_Time (Main_Proc_Top,Old_Info_Top);
      Get_Idle_Time (Ignore_Top,Old_Info_Top);

      Nuke_Idle_Users (Main_Proc_Top);
      IF Debug_On
        THEN
          BEGIN
            Writeln (Debug_Info,'Processes to ignore.');
            Print_Info (Ignore_Top);
            Writeln (Debug_Info,'Processes available for nuking...');
            Print_Info (Main_Proc_Top);
            Writeln (Debug_Info,'Combining the ignored and nuked lists');
          END;

      Add_Lists (Main_Proc_Top,Ignore_Top);

      Dispose_List (Subproc_Top);
      Dispose_List (Ignore_Top);
      Dispose_List (Old_Info_Top);

      Old_Info_Top := Main_Proc_Top;
      Main_Proc_Top := NIL;

      { All the info is safely stored away, now turn ASTs back on while we
        wait. }
      Check ($SetAST(1));

      Check (LIB$Wait(Delta_Time));
    END; { MAIN PROGRAM Loop }
END. { MAIN Program AutoLogout }
$*$*EOD*$*$
$ checksum AUTOLOGOUT.PAS
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 1846548607
$ write sys$output "Creating BUILD_AUTOLOGOUT.COM"
$ create BUILD_AUTOLOGOUT.COM
$ DECK/DOLLARS="$*$*EOD*$*$"
$ pascal /NOLIST idleset.pas
$ message idleset_err.msg
$ link /NOMAP/NOTRACE idleset.obj,idleset_err.obj
$ runoff idleset.rnh
$ pascal /NOLIST AutoLogout.pas
$ macro UnLink_VT.mar
$ link /NOMAP/NOTRACE AutoLogout.obj,UnLink_VT.obj,sys$system:sys.stb/select
$ write sys$output "AutoLogout:  Build finished."
$*$*EOD*$*$
$ checksum BUILD_AUTOLOGOUT.COM
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 1694854242
$ write sys$output "Creating IDLESET.CLD"
$ create IDLESET.CLD
$ DECK/DOLLARS="$*$*EOD*$*$"
!
! Command definition for IDLESET utility.
!
define syntax DEBUG
  image pub_root:[source.autologout]idleset
  parameter P1,
    label=LIMIT,
    value(type=$NUMBER,DEFAULT=0)
  qualifier CHECK_LIMITS
  qualifier SHUTDOWN
  disallow CHECK_LIMITS and SHUTDOWN

define verb idleset
  image pub:idleset
  qualifier DEBUG,
    syntax = DEBUG
  parameter P1,
    label=LIMIT,
    value(type=$NUMBER,DEFAULT=0)
  qualifier CHECK_LIMITS
  qualifier SHUTDOWN
  disallow CHECK_LIMITS and SHUTDOWN
$*$*EOD*$*$
$ checksum IDLESET.CLD
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 240893446
$ write sys$output "Creating IDLESET.RNH"
$ create IDLESET.RNH
$ DECK/DOLLARS="$*$*EOD*$*$"
..RM 64
..NO NUMBER
..NO PAGING
..STHL 5,1,1
..lm 0
1 AUTOLOGOUT
..lm 1
AUTOLOGOUT is a daemon process that automatically disconnects an interactive
job tree that has been idle for too long.  A job tree includes a main process
and all of its subprocesses and is defined as interactive only if it is
attached to a physical or network terminal.  Jobs trees that are ignored
include batch jobs, detached jobs and disconnected jobs.
..s
The main purpose in writing this daemon is to protect user's data and enhance
system security by logging out jobs that have been left alone and unprotected.
Anyone who came up to that terminal would have full access to any data or
programs that the user who left himself logged in would have access to.
..s
Because the Autologout daemon cannot ask the terminal if there is a user
sitting in front of it, users may set their own idle time limits within the
boundries set up by the system manager by using IDLESET.  Users who often use
terminals in a terminal room might want to set an idle time close to the
minimum.  Users who have secure terminals and want to leave a terminal logged
in all of the time or who might have several sessions active on the same
terminal at once might want to set thier idle limits higher than the default.
..s
Autologout sends out status messages to all terminals enabled to receive OPER1
operator messages.
..s
..lm 0
2 Author
..lm 1
The main program and most of the routines were writen by Chris Yoder of Hughes
Aircraft Company.  The kernal mode disconnect routine was writen by Forrest A.
Kenney of digital and modified by Kevin Carosso of Hughes Aircraft Company.
..lm 0
1 IDLESET
..lm 1
..lm +5
IDLESET#[maximum-idle-time]
..lm -5
..s
IDLESET is a utility that allows a user to tell AUTOLOGOUT how many minutes of
idle time that process may aquire before it is automatically disconnected from
the terminal.  The system manager will set up upper, lower and default idle
limits.
..s
The user is warned when half of the idle time limit has passed and one minute
prior to the idle time limit.  If the user has an idle time limit of greater
than 10 minutes, a warning is also sent 5 minutes prior to reaching that limit.
If the user's terminal is set /NOBROADCAST, these warning messages will not be
delivered.
..lm 0
2 Parameter
..lm 1
The number of minutes before AUTOLOGOUT considers a process to be idle and logs
it out.  If not specified, the system wide default will be used.  If a value
smaller than the minimum limit and higher than 0 is specified, then the minimum
limit will be used.  If a value higher than the maximum limit is specified,
then the maximum limit will be used unless the user has the privilege EXQUOTA
enabled for the current process.
..s
NOTE: Minimum, maximum, and default values are specified by the system manager
and are read only at the startup of the AUTOLOGOUT daemon.
..lm 0
2 /CHECK
..lm 1
The /CHECK command queries the AUTOLOGOUT daemon for the system wide minimum
and maximum idle limits as well as for current idle limit for the current
process.
..lm 0
2 /SHUTDOWN
..lm 1
The /SHUTDOWN command shuts down the AUTOLOGOUT daemon.  The AUTOLOGOUT daemon
will only shutdown if the process that isses the request has with both the
SYSPRV and SECURITY privileges currently enabled.
..lm 0
2 Examples
..lm 1
..nofill
..lm +5
..s
$ IDLESET 15
Idle limit set to 15 minutes.
..lm -5
..s
This command will set the idle time of the current job to 15 minutes.
..lm +5
..s
$ IDLESET 0
Default used. Idle limit set to 20 minutes.
..lm -5
..s
The user specified 0, thus the default value (in this case 20 minutes) was
used.
..lm +5
..s
$ IDLESET/CHECK
Maximum Idle time limits:
Minimum = 5 minutes,
Current = 20 minutes,
Maximum = 90 minutes.
..lm -5
This command might have been issued directly after the previous IDLESET command
to check on the current state of the system.  The minimum amount of time that
a user might allow before being logged out is 5 minutes, the maximum is 90
minutes and the user's current idle limit is 20 minutes.
..fill
..lm 0
2 Author
..lm 1
This program was writen by Chris Yoder of Hughes Aircraft Company.
$*$*EOD*$*$
$ checksum IDLESET.RNH
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 407418073
$ write sys$output "Creating IDLESET.PAS"
$ create IDLESET.PAS
$ DECK/DOLLARS="$*$*EOD*$*$"
[ident('1.1'),
 inherit('SYS$Library:Starlet','SYS$Library:STRRTL','SYS$Library:LIBRTL')]
Program IdleSet (Output,AST_Outfile);
{
           This program will talk to the idle process killer.  It will suggest
        an idle time limit and wait for the idle process killer to respond with
        what the idle time was actually set to.
}

CONST   AutoLogout_Mailbox_Name = 'AutoLogout_Box';

{
 CLI constants.
}
        CLI$_ABSENT     = %x'000381F0';
        CLI$_COMMA      = %x'0003FD39';
        CLI$_CONCAT     = %x'0003FD29';
        CLI$_DEFAULTED  = %x'0003FD21';
        CLI$_LOCNEG     = %x'00038230';
        CLI$_LOCPRES    = %x'0003FD31';
        CLI$_NEGATED    = %x'000381F8';
        CLI$_PRESENT    = %x'0003FD19';

{CONST Definitions
        AutoLogout_Mailbox_Name --- The name of the mailbox that we send to.
}

TYPE    UnsignedWord    = [WORD] 0..65535;

        UnsignedQuad    = [Quad,Unsafe] Record
                                L0,L1   : Unsigned;
                          End; { Record UnsignedQuad }

        IO_Block        = RECORD
                            IO_Stat, Count      : UnsignedWord;
                            Dev_Info            : INTEGER;
                          END; { RECORD IO_Block }

        Action_Type     = (Set_Idle,Check_Limits,Shutdown);

        String80        = Varying [ 80] OF CHAR;
        String255       = Varying [255] OF CHAR;

{TYPE Definitions
        UnsignedWord --- An unsigned word type.
        UnsignedQuad --- An unsigned quad word type.
        IO_Block ------- An IO_Block to pass to various system services.
        Action_Type ---- This is the type of action that we want to perform.
        String80 ------- A string  80 characters long
        String255 ------ A string 255 characters long
}

VAR     AutoLogout_Channel      : [Volatile] UnsignedWord;
        Stat_Block              : IO_Block;

        Send_String             : String80;
        Rec_String              : String255;
        Hold_String             : String80;

        Stat_MBox_Name          : String80;
        Stat_Channel            : UnsignedWord;

        Action                  : Action_Type;

        Ret_Status              : Integer;

        Binary_Wait_Time        : UnsignedQuad;
        AST_Outfile             : [Volatile] Text;

        IdleSet__NoDaemon       : [External, Value] Integer;

{VAR Definitions
        AutoLogout_Channel --- A channel to talk to the Mailbox over.
        Stat_Block ----------- Hold a status block from a system service call.
        Send_String ---------- The string passed through the mailbox.
        Rec_String ----------- The string passed back through the mailbox.
        Stat_MBox_Name ------- The name of the status mailbox.
        Stat_Channel --------- The channel to receive a status over.
        Action --------------- The type of action specified.
        Ret_Status ----------- Holds a return status from a system call.
        Binary_Wait_Time ----- The binary version of the time to wait.
        AST_Outfile ---------- An output file to use from an AST routine.
        IdleSet__NoDaemon ---- What to signal when we want to tell the world
                                that there is no daemon running.
}

{******************************************************************************}

[EXTERNAL, UNBOUND]
FUNCTION CLI$Present (
        EntityDesc : [CLASS_S] Packed Array [ $l1..$u1 : Integer ] Of Char
        ) : Integer;    External;

{******************************************************************************}

[EXTERNAL, UNBOUND]
FUNCTION CLI$Get_Value (
        EntityDesc : [CLASS_S] Packed Array [ $l1..$u1 : Integer ] Of Char;
        RetDesc : [CLASS_S] Packed Array [ $l2..$u2 : Integer ] Of Char;
        VAR RetLength : [UNSAFE] UnsignedWord := %Immed 0
        ) : Integer;    External;

{******************************************************************************}

[Asynchronous, Unbound]
Procedure Check (Status : Unsigned);
  {
           This procedure will check the condition passed in, and signal an
        error if it's illegal.
  }

  BEGIN { Procedure Check }
    IF NOT ODD (Status)
      THEN $Exit (Status);
  END; { Procedure Check }

{******************************************************************************}

Function Trim_All_Spaces_255 (Source_String : String255) : String255;
  {
           This function will trim all spaces from the beginning and end of
        Source_String.
  }

  VAR   Hold_Result     : String255;
        Where           : Integer;
        Len             : Integer;
        Ret_Status      : Unsigned;

  {VAR Description
        Source_String --- The original string
        Hold_Result ----- Hold the result of the function while we play with it
        Where ----------- Hold the location of the first non-space
        Len ------------- Hold the length of a string
        Ret_Status ------ Hold the return status of a system call
  }

  BEGIN  { Function Trim_All_Spaces_255 }
    Hold_Result := Source_String;

    { Get rid of any spaces on the front end of the string. }
    Len := LENGTH (Hold_Result);
    Where := STR$Find_First_Not_In_Set (Hold_Result,' ');

    IF (Where <= Len)
      THEN Hold_Result := SUBSTR (Hold_Result,Where,Len-Where+1);

    { Now clean off the end. }
    Ret_Status := STR$Trim (Hold_Result,Hold_Result,Hold_Result.Length);

    IF NOT ODD (Ret_Status)
      THEN $Exit (Ret_Status);

    Trim_All_Spaces_255 := Hold_Result;
  END; { Function Trim_All_Spaces_255 }

{******************************************************************************}

[Asynchronous, Unbound]
Procedure Timeout_Routine;
  {
           Procedure to handle the logical name existing, but nobody listening.
  }

  BEGIN { Procedure Timeout_Routine }
    Check ($Cancel (Autologout_Channel));
    $Exit (IdleSet__NoDaemon);
  END; { Procedure Timeout_Routine }

{******************************************************************************}

BEGIN { Program IdleSet }
  Send_String := '';

  IF (CLI$Present ('SHUTDOWN') = CLI$_Present)
    THEN Action := Shutdown
    ELSE
      IF (CLI$Present ('CHECK_LIMITS') = CLI$_Present)
        THEN Action := Check_Limits
        ELSE Action := Set_Idle;

  CASE Action OF
    Set_Idle :
      BEGIN
        Ret_Status := CLI$Get_Value (   'LIMIT',
                                        Send_String.Body,
                                        Send_String.Length);

        Stat_MBox_Name := 'Stat_AutoLogout_MBox';
        Check ($CREMBX (Prmflg := 0,
                        Chan := Stat_Channel,
                        Lognam := Stat_MBox_Name));

        Check (LIB$SYS_TRNLOG (Stat_MBox_Name,,Hold_String,,,));
        Stat_MBox_Name := Hold_String;
        Send_String := Send_String + ' ' + Stat_MBox_Name;

      END; { CASE Set_Idle }
    Check_Limits :
      BEGIN
        Stat_MBox_Name := 'Stat_AutoLogout_MBox';
        Check ($CREMBX (Prmflg := 0,
                        Chan := Stat_Channel,
                        Lognam := Stat_MBox_Name));

        Check (LIB$SYS_TRNLOG (Stat_MBox_Name,,Hold_String,,,));
        Stat_MBox_Name := Hold_String;
        Send_String := 'CHECK ' + Stat_MBox_Name;
      END; { CASE Set_Idle }
    Shutdown : Send_String := 'STOP';
  END; { CASE Action OF }

  Send_String := PAD (Send_String,' ',80);

  Ret_Status := $Assign (       Devnam := AutoLogout_Mailbox_Name,
                                Chan := AutoLogout_Channel);
  IF (Ret_Status <> SS$_Normal)
    THEN
      BEGIN
        IF (Ret_Status = SS$_IVDEVNAM)
          THEN $Exit (IdleSet__NoDaemon)
          ELSE $Exit (Ret_Status);
      END;

  Check ($BinTim ('0 00:00:10',Binary_Wait_Time));
  Check ($SeTimr (,Binary_Wait_Time,Timeout_Routine,));

  Ret_Status := $QIOW ( Chan    := AutoLogout_Channel,
                        Func    := IO$_WRITEVBLK,
                        IOSB    := Stat_Block,
                        P1      := Send_String.Body,
                        P2      := LENGTH(Send_String.Body));
  Check ($CanTim (,));
  Check (Ret_Status :: Unsigned);
  Check (Stat_Block.IO_Stat);

  IF (Action <> Shutdown)
    THEN
      BEGIN
        { This is to insure that we have cleared out Rec_String. }
        Rec_String := '';
        Rec_String := PAD (Rec_String,' ',255);

        Check ($QIOW (  Chan    := Stat_Channel,
                        Func    := IO$_READVBLK,
                        IOSB    := Stat_Block,
                        P1      := Rec_String.Body,
                        P2      := SIZE(Rec_String.Body)));
        Check (Stat_Block.IO_Stat);

        Rec_String.Length := Stat_Block.Count;
        Rec_String := Trim_All_Spaces_255 (Rec_String);
        WRITELN (Rec_String);
        Check ($DelMBX (Stat_Channel));
      END; { IF NOT Exiting }

  Check ($Dassgn (AutoLogout_Channel));
END. { Program IdleSet }
$*$*EOD*$*$
$ checksum IDLESET.PAS
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 1061040783
$ write sys$output "Creating IDLESET_ERR.MSG"
$ create IDLESET_ERR.MSG
$ DECK/DOLLARS="$*$*EOD*$*$"
..Title         IdleSet Warning Messages
..Facility      IDLESET,1313/prefix=IDLESET__
..Ident         'IDLESET Version 1.1'

..Severity      Warning

NODAEMON        <Autologout daemon not running.>

..End
$*$*EOD*$*$
$ checksum IDLESET_ERR.MSG
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 2050269762
$ write sys$output "Creating INSTALL_AUTOLOGOUT.COM"
$ create INSTALL_AUTOLOGOUT.COM
$ DECK/DOLLARS="$*$*EOD*$*$"
$ ! INSTALL_AUTOLOGOUT.COM
$ !
$ !        This command procedure will install Autologout on the current
$ !     system.  It makes the assumption that all of the files that it needs
$ !     exist in the current directory.
$ !
$ !             Written by:     Chris Yoder
$ !                     for:    Hughes Aircraft Company
$ !                             Space and Communications Group
$ !                             Computing Technology Center
$ !                     on:     July 9, 1987
$ !
$       type sys$input

     This command procedure will install Autologout on the current system.  It
will allow the user to specify the home directory of the files needed to
operate Autologout.  It will also create your original parameter file and start
up the autologout daemon if desired.

$ !
$ Priv_Check:
$ !
$       req_priv = "SYSPRV,PRMMBX,WORLD,SYSNAM,CMKRNL,OPER,SECURITY"
$       prev_priv = f$setprv (req_priv)
$       if .not. f$privilege (req_priv) then goto No_Priv
$ !
$ ADir_Ask:
$ !
$       read sys$command adir-
                /prompt="Please enter the home directory of Autologout: "
$       if adir .eqs. "" then goto ADir_Ask
$ !
$ Test_ADir:
$ !
$       on error then goto ADir_Ask
$       on warning then goto ADir_Ask
$       open/write test 'adir'test.tmp
$       close test
$       delete 'adir'test.tmp;*
$ !
$       on error then exit
$       on warning then exit
$ !
$ PFile_Ask1:
$ !
$       open/write pfile 'adir'Autologout$Startup.Par
$       read sys$command low_lim-
                /prompt="Minimum allowed idle limit (in minutes): [5] "
$       if low_lim .eqs. "" then low_lim = "5"
$       write pfile low_lim,"   ! ''low_lim' minutes, minimum idle time."
$ !
$ PFile_Ask2:
$ !
$       read sys$command def_lim-
                /prompt="Default idle limit (in minutes): [30] "
$       if def_lim .eqs. "" then def_lim = "30"
$       write pfile def_lim,"   ! ''def_lim' minutes, default idle time."
$ !
$ PFile_Ask3:
$ !
$       read sys$command top_lim-
                /prompt="Maximum allowed idle limit (in minutes): [240] "
$       if top_lim .eqs. "" then top_lim = "240"
$       write pfile top_lim,"   ! ''top_lim' minutes, maximum idle time."
$ !
$ PFile_Ask4:
$ !
$       type sys$input

     Autologout will either DISCONNECT or LOGOUT users who have exceeded their
idle limit.  If it trys to DISCONNECT a process that cannot be disconnected,
then it will log that process out.

$       read sys$command disco_flag-
                /prompt="Disconnect users when idle limit is exceeded?  [Yes] "
$       if disco_flag .eqs. "" then disco_flag = "Yes"
$       if disco_flag then write pfile "True    ! Disconnect is the "+-
                                "default action on remove (boolean flag) "
$       if .not. disco_flag then write pfile "False     ! Disconnect is the "+-
                                "default action on remove (boolean flag) "
$       write pfile -
"! usernames to treat opposite from the remove default above, one to a line."
$       type sys$input

     You may choose to have some usernames treated as if you answered the above
question in the opposite way.  Enter a RETURN when you wish to stop adding
users to this list.

$       if disco_flag then exclude_action = "logout"
$       if .not. disco_flag then exclude_action = "disconnect"
$ !
$ PFile_Ask5:
$ !
$       read sys$command username-
           /prompt="Username to ''exclude_action' when idle limit is exceeded: "
$       if username .eqs. "" then goto End_PFile_Create
$       write pfile username
$       goto PFile_Ask5
$ !
$ End_PFile_Create:
$ !
$       close pfile
$ !
$ CLD_Ask:
$ !
$       read sys$command CLD_Install-
        /prompt="Install the CLD file into SYS$Library:DCLTABLES? [Yes] "
$       if CLD_Install .eqs. "" then CLD_Install = "Y"
$ !
$       type sys$input

     All of the questions that Autologout will ask have been asked.  The rest
of the installation will continue automatically for a few minutes.

$ !
$ CLD_Write_It:
$ !
$       open/write CLD_File 'adir'IdleSet.CLD
$       write CLD_File "define verb idleset"
$       write CLD_File "  image ''adir'idleset"
$       write CLD_File "  parameter P1,"
$       write CLD_File "    label=LIMIT,"
$       write CLD_File "    value(type=$NUMBER,DEFAULT=0)"
$       write CLD_File "  qualifier CHECK_LIMITS"
$       write CLD_File "  qualifier SHUTDOWN"
$       write CLD_File "  disallow CHECK_LIMITS and SHUTDOWN"
$       close CLD_File
$ !
$ Startup_Write_It:
$ !
$       open/write sfile 'adir'Autologout$Startup.Com
$       write sfile "$ define/system/executive_mode autologout_dir ''adir'"
$       write sfile "$ run      /detached -"
$       write sfile "   /process_name=""Idle Daemon"" -"
$       write sfile "   /privileges=(prmmbx,sysnam,world,oper,security) -"
$       write sfile "           autologout_dir:autologout"
$       close sfile
$ !
$ Move_Files:
$ !
$       copy autologout.exe 'adir'autologout.exe
$       copy idleset.exe 'adir'idleset.exe
$ !
$ Install_CLD:
$ !
$       if .not. CLD_Install then goto Test_Autologout
$       set command/table=sys$library:dcltables/output=sys$library:dcltables -
                'adir'idleset
$       mcr install sys$library:dcltables/replace
$ !
$ Test_Autologout:
$ !
$       on error then goto Failed_Test
$       on warning then goto Failed_Test
$       @'adir'Autologout$Startup
$       set command 'adir'idleset
$       idleset/check
$ !
$ Passed_Test:
$ !
$       write sys$output " "
$       write sys$output -
"   Autologout has been successfully installed.  You will need to add the line:"
$       write sys$output -
        "$ @''adir'Autologout$Startup"
$       write sys$output -
"to your SYSTARTUP procedure to startup Autologout."
$       write sys$output " "
$       goto Bye_Bye
$ !
$ Failed_Test:
$ !
$       type sys$input

     Autologout failed to install correctly.  Please read 000readme.1st to
attempt to figure out what went wrong.

$       goto Bye_Bye
$ !
$ No_Priv:
$ !
$       write sys$output "You need ''req_priv'"
$       write sys$output "to execute this installation procedure."
$       goto Bye_Bye
$ !
$       Bye_Bye:
$ !
$       prev_priv = f$setprv (prev_priv)
$       exit
$*$*EOD*$*$
$ checksum INSTALL_AUTOLOGOUT.COM
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 1730227660
$ write sys$output "Creating UNLINK_VT.MAR"
$ create UNLINK_VT.MAR
$ DECK/DOLLARS="$*$*EOD*$*$"
        .TITLE  UNLINK_VT a set of routines to unlike a VT
        .IDENT  /V1.0-000/
        .LIBRARY /SYS$LIBRARY:LIB.MLB/

;++
;
;               These routines are a system hack to allow a privileged user
;       to disconnect a VT with outstanding I/O from it's physical device.
;       The correct way to perform this would be to modify the code in TTDRIVER
;       that does the disconnect.  For testing purpose this is a more expediant
;       way to accomplist this goal.
;
;       NOTE:
;               The required privs to use this are:
;
;                       1) CMKRNL
;                       2) SHARE
;
; CALLING SEQUENCE:
;
;       ret_stat = UNLINK_VT (device_name)
;
;       device_name     -       address of a string descriptor, the string must
;                               contain name of VT to be disconnected (ex VTA0:)
;
; AUTHOR:
;
;       Forrest A. Kenney       28-August-1986
;
; REVISION HISTORY:
;
;       Kevin Carosso           14-JUL-1987     Hughes Aircraft, SCG/CTC
;               Modified a bit for local use, don't bother with
;               the privilege checks, must have CMKRNL and SHARE only.
;               Raise to device IPL while mucking in the UCB as
;               Forrest Kenney suggested.
;
;               Clear R2 before calling IOC$SEARCH.  Since we now run
;               above IPL 2, we have to lock our kernel code into memory.
;
;--









        .SBTTL  External and local symbol definitions

;
; External symbols
;

        $CCBDEF                         ; Define CCB
        $DVIDEF                         ; Device information
        $IPLDEF                         ; Define various CPU IPL levels
        $JPIDEF                         ; Process information
        $SSDEF                          ; System status codes
        $UCBDEF                         ; Unit control block
        $TTYDEFS                        ; TTY specific definitions
        $TTYMACS                        ; Define terminal macros
        $TTYUCBDEF                      ; TTY UCB definitions


;+
; A simple macro to help build item list items
;-
        .MACRO  ITEM    LENGTH,CODE,BUFF_ADDR,RET_LEN=0

        .WORD           LENGTH
        .WORD           CODE
        .ADDRESS        BUFF_ADDR
        .ADDRESS        RET_LEN

        .ENDM   ITEM









        .SBTTL  Allocate local storage


        .PSECT  $DATA   LONG,NOEXE,RD,WRT

DEVNAM: .BLKB   64                      ; Block hold PHYDEV name
        DEVNAM_LEN = .-DEVNAM
DEVNAM_SIZ:                             ; Storage for length of PHYDEV name
        .LONG   0

DISC_FLAG:                              ; Is device disconnectable
        .LONG   0

CHANNEL:
        .WORD   0                       ; Channel number for assign
IOSB:   .BLKW   4                       ; IOSB for $GETDVI

DVILST: ITEM    DEVNAM_LEN,DVI$_TT_PHYDEVNAM,DEVNAM,DEVNAM_SIZ
        ITEM    4,DVI$_TT_DISCONNECT,DISC_FLAG
        .LONG   0

lock_range:                             ; For $LKWSET call
        .address        begin_lock
        .address        end_lock









        .PSECT  $CODE   LONG,PIC,NOWRT,RD,EXE,CON,REL,LCL,SHR
        .SBTTL  Validate device & request
;+
;
;               This routine will validate the device to be disconnected is a
;       virtual terminal and the the user has the correct privs to do it.  The
;       sequence is listed below:
;
;               1) Assign a channel to the device
;               2) Determine if device can be disconnected
;               3) Calls KERNEL mode routine (to get UCB address and disconnect
it)
;
;       INPUTS:
;               4(AP)   - Address of a descriptor containing device name
;
;       OUTPUT:
;               R0      - Status of operation
;
;                               SS$_IVDEVNAM
;                               SS$_NOPRIV
;                               any possible returns from $ASSIGN or $GETDVI
;
;-

        .ENTRY  UNLINK_VT,^M<>

;+
; Lock down our kernel mode, high IPL code.
;-
        $LKWSET_S       INADR = lock_range
        blbs    r0, 5$
        ret
;+
; Get a channel to work with
;-
5$:     CLRQ    -(SP)                   ; Default acmode & no mailbox
        MOVAW   CHANNEL,-(SP)           ; Address of word to hold channel #
        MOVL    4(AP),-(SP)             ; Address of device name string
        CALLS   #4,G^SYS$ASSIGN         ; Assign channel
        BLBS    R0,10$                  ; Success continue
        RET                             ; Failed return with reason
;+
; Now get the device information and make sure it can be disconnected,
; also make sure that don't try to disconnect an already disconnected device
;-
10$:    $GETDVIW_S      CHAN=CHANNEL,-  ; Get device information
                        ITMLST=DVILST,- ;
                        IOSB=IOSB       ;
        BLBS    R0,20$                  ; Setup ok continue
        PUSHL   R0                      ; Save error reason
        BRW     1000$                   ; Branch to exit code
20$:    BLBS    IOSB,30$                ; OK continue
        MOVZWL  IOSB,-(SP)              ; Store failure reason
        BRW     1000$                   ; Branch to exit code
30$:    BLBS    DISC_FLAG,40$           ; Device disconnectable cont
        MOVZWL  #SS$_IVDEVNAM,-(SP)     ; Inproper device for requested operatio
n
        BRW     1000$                   ; Branch to exit code
40$:    TSTL    DEVNAM_SIZ              ; See if got physical device name
        BNEQ    80$                     ; If zero length then already disconnect
ed
        MOVZWL  #SS$_NORMAL,-(SP)       ; Store failure reason
        BRW     1000$                   ; Branch to exit code
;+
; Now build arg list & call Kernel mode routine
;-
80$:    MOVL    AP,-(SP)                ; Store device argument list on stack
        MOVAL   KRNL_CODE,-(SP)         ; Store address of Kernel routine on sta
ck
        CALLS   #2,SYS$CMKRNL           ; Invoke kernel mode routine
        PUSHL   R0                      ; Save status reason

;+
; We have a channel to free before exiting
;-
1000$:  $DASSGN_S       CHAN=CHANNEL    ; Free channel
        BLBS    R0,1010$                ; Ok just exit with correct reason
        MOVL    R0,R1                   ; Save channel DASSGN error
        POPL    R0                      ; Get previous status code
        BLBC    R0,1020$                ; Use first error
        MOVL    R0,R1                   ; Use DASSGN error
        RET                             ; Return
1010$:  POPL    R0                      ; Restore reason
1020$:  RET                             ; Return to caller

;+
; This section of code needs to run at elevated IPL to prevent process
; deletion while owning I/O database MUTEX.  It also will use a backdoor
; hook into the TTDRIVER to UNLINK the VT.
;
;       Note:   R4 contains the current processes PCB address it is suppiled by
;               the change mode dispatcher.  It is needed by SCH$IOLOCKR &
;               SCH$IOUNLOCK
;
;-
begin_lock:

        .ENTRY  KRNL_CODE,^M<R3,R5>
        DSBINT  #IPL$_ASTDEL            ; Raise IPL to prevent process deletion
        JSB     G^SCH$IOLOCKR           ; Lock the I/O database for read access
        MOVL    4(AP),R1                ; Get device descriptor string address
        CLRL    R2                      ; No flags
        CLRL    R3                      ; No lock value block
        JSB     G^IOC$SEARCH            ; Now find the devices UCB
        CMPW    R0,#SS$_DEVALLOC        ; See if device allocated
        BEQL    10$                     ; If allocated proceeded
        BLBC    R0,30$                  ; If no device then exit

10$:    DSBINT  UCB$B_DIPL(R1)          ; Raise to device IPL
        MOVL    UCB$L_TL_PHYUCB(R1),R5  ; Get device's physical UCB address
        BEQL    20$                     ; Device already disconnected just exit
        PUSHL   R4                      ; Save PCB address
        MOVL    UCB$L_TT_CLASS(R5),R4   ; Get class dispatch table address
        JSB     @CLASS_DISCONNECT(R4)   ; Now call disconnect code
        POPL    R4                      ; Restore PCB address
20$:    ENBINT                          ; drop back down
        MOVZWL  #SS$_NORMAL,R0          ; Store success as exit reason

30$:    PUSHL   R0                      ; Save reason
        JSB     G^SCH$IOUNLOCK          ; Unlock I/O database
        POPL    R0                      ; Restore reason
        ENBINT                          ; Lower IPL back to calling level
        RET
;
; Lock down pages between "begin_lock" and here so we don't pagefault
; at high IPL
;
end_lock:
        .END
$*$*EOD*$*$
$ checksum UNLINK_VT.MAR
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ exit
