[Back to TSR SWAG index]  [Back to Main SWAG index]  [Original]

{
TSR.PAS
  Sample TSR application written in Turbo Pascal.
  IMPORTANT!
  This TSR operates only in TEXT mode, and, as
  written, supports only 80 x 25 sized screens
  (not 43- or 50- line display modes).

  This TSR will only operate on DOS 3.x or newer
  versions of DOS.

  Use this code as an example to write your own TSR code.
  Modify the $M compiler directive, below, to specify
  the maximum stack size, minimum heap and maximum heap
  required for your TSR application.

  Please see the text and other source comments
  for important restrictions.

  TSRs can be DANGEROUS, so be careful.
}

{$S-}
{$M 3072, 0, 512}
uses
  Crt, Dos;

type
  { Defines an array to store the screen image.
  For greater efficiency, your code may want to save only
  the portion of the screen that is changed by your TSR
  code. This implementation saves the entire screen image,
  at popup time, for the greatest flexibility. }
  TSavedVideo=Array[0..24, 0..79] of Word;
  PSavedVideo=^TSavedVideo;


{ The following items define the TSR's identification
string. }
type
  String8=String[8];
const
  IdStr1:String8='TP7RTSR';
  IdStr2:String8='TSRInUse';

var
  CPURegisters: Registers;
    { General register structure for Intr calls. }
  CursorStartLine: Byte;
    { Stores cursor shape information. }
  CursorEndLine: Byte;
    { Stores cursor shape information. }
  DiskInUse: Word;
    { Tracks INT 13 calls in progress. }
  MadeActive: Boolean;
    { TRUE if this TSR has been asked to pop up. }
  OurSP: Word;
  OurSS: Word;
    { Saved copies of our SS and SP registers. }
  PInt09: Pointer;
    { Saved address of keyboard handler. }
  PInt12: Pointer;
    { Saved address of GetMemorySize interrupt. }
  PInt1B: Pointer;
    { Ctrl-Break interrupt address. }
  PInt24: Pointer;
    { DOS Critical error handler. }
  PInt28: Pointer;
    { Saved address of background task scheduler. }
  PInt1C: Pointer;
    { Saved address of timer handler. }
  PInDosFlag: ^Word;
    { Points to DOS's InDos flag. }
  VideoMem: PSavedVideo;
    { Points to actual video memory area. }
  SavedVideo:TSavedVideo;
    { Stores the video memory when TSR is popped up. }
  SavedWindMin: Word;
   { Holds saved copy of WindMin for restoring window. }
  SavedWindMax: Word;
   { Holds saved copy of WindMax for restoring window. }
  SavedSS,
  SavedSP: Word;
   { Saves caller stack registers; must be global
   to store in fixed memory location, not on local
   stack of interrupted process. }
  SavedX,
  SavedY: Word;
    { Stores X, Y cursor prior to TSR popup }
    { for restoration when TSR goes away. }
  TempPtr: Pointer;
    { Used internally to DoUnInstall. }
  TSRInUse: Boolean;
    { Set TRUE during processing to avoid double
    activation. }



procedure SaveDisplay;
{ Copies the content of video memory to an internal
  array structure. Saves the cursor location and cursor
  shape definitions. }
var
  CursorLines: Byte;
begin
  { Save cursor location. }
  SavedX := WhereX;
  SavedY := WhereY;
  { Saved existing window values. }
  SavedWindMin := WindMin;
  SavedWindMax := WindMax;

  { Get and save current cursor shape. }
  with CPURegisters do
  begin
    AH := $03;
    BH := 0;
    Intr($10, CPURegisters);
    CursorStartLine := CH;
    CursorEndLine:= CL;
  end;

  { Get equipment-type information. If Monochrome
    adapter in use, then point to $B000; otherwise use the
    color memory area. }
  Intr( $11, CPURegisters );
  if  ((CPURegisters.AX shr 4) and 7) = 3 then
  begin
    VideoMem := Ptr( $B000, 0 );
    CursorLines := 15;
  end
  else
  begin
    VideoMem := Ptr( $B800, 0 );
    CursorLines := 7;
  end;
  SavedVideo := VideoMem^;

  { Change cursor shape to block cursor. }
  with CPURegisters do
  begin
    AH := $01;
    CH := 0;
    CL := CursorLines;
    Intr($10, CPURegisters);
  end;

end; { SaveDisplay }


procedure RestoreDisplay;
{ Always called sometime after calling SaveDisplay.
  Restores the video display to its state prior to the
  TSR popping up. }
begin
  { Restore screen content. }
  VideoMem^ := SavedVideo;

  { Restore cursor shape. }
  with CPURegisters do
  begin
    AH := $01;
    CH := CursorStartLine;
    CL := CursorEndLine;
    Intr($10, CPURegisters);
  end;

  { Resize window so the GotoXY (below) will work. }
  Window (Lo(SavedWindMin)+1, Hi(SavedWindMin)+1,
          Lo(SavedWindMax)+1, Hi(SavedWindMax)+1);
  Gotoxy ( SavedX, SavedY );

end; { Restore Display }


procedure TrapCriticalErrors;
assembler;
{ INT 24H }
{
This handler is enabled only while the TSR is popped
up on-screen.

This handler exists solely to catch any DOS
critical errors and is a crude method of doing so. Since
this routine does nothing, any critical errors that
occur while the TSR is popped up are ignored--which
could be very dangerous. Also, if another TSR or ISR
pops up after this one, it may get the critical
error that was intended for this TSR.

NOTE: Normally, this could be an "interrupt" type
procedure. However, Turbo Pascal pushes and then pops
all registers prior to the IRET instruction. By writing
this as an assembler routine, this generates the IRET
directly, followed by one superfluous RET instruction
generated by the assembler, resulting in substantial
code savings.
}
asm
  IRET
end; { TrapCriticalErrors }




procedure CBreakCheck;
assembler;
{ INT 1BH }

{ This routine results in a no operation; when hooked to
the INT 1B Ctrl-Break interrupt handler, it causes
nothing to happen when Ctrl-Break or Ctrl-C are pressed.
This routine is hooked only when the TSR is popped up. }
asm
        IRET
end; { CBreakCheck }




function GetKey : Integer;
{ Pauses for input of a
single keystroke from the keyboard and returns the ASCII
value. In the case where an Extended keyboard key is
pressed, GetKey returns the ScanCode + 256. The Turbo
Pascal ReadKey function is called to perform the
keystroke input. This routine returns a 0 when an Extended
key has been typed (for example, left or right arrow)
and we must read the next byte to determine the Scan
code.
}
var
  Ch : Char;
begin
  { While waiting for a key to be pressed, call
  the INT $28 DOS Idle interrupt to allow background
  tasks a chance to run. }
  repeat
    asm
      int $28
    end;
  until KeyPressed;
  Ch := ReadKey;
  If Ord(Ch) <> 0 then
    GetKey := Ord(Ch)        { Return normal ASCII value. }
  else
    { Read the DOS Extended SCAN code that follows. }
    GetKey := Ord(ReadKey) + 256;
end;{GetKey}




procedure  DoPopUpFunction;
{ This procedure is the "guts" of the popup
  application. You can code your own application here, if
  you want. Be sure to read the text for important
  restrictions on what can be written in a TSR.

  As implemented here, this popup displays a table
  of ASCII values.
}

const
  UpperLeftX=10;
  UpperLeftY=5;
    { Define upper-left corner of TSR's popup window. }

  LowerLeftX=70;
  LowerRightY=20;
    { Define lower-right corner of TSR's popup window. }

  Width=LowerLeftX - UpperLeftX + 1;
  Height=LowerRightY - UpperLeftY + 1;
    { Calculated width and height of popup window. }
  MinX=6;
    { Distance from left edge to display ASCII table. }
  RightEdge=8;
    { Marks the right edge (Width-RightEdge) of table. }
  MinY=4;
    { Distance from top of window to start ASCII Table. }
  ValuesPerLine=Width-8 - MinX;
    { Computed number of ASCII values in each line. }
  NumLines=255 div ValuesPerLine;
    { Computed number of lines in the ASCII table. }
  KEY_LEFTARROW = 331;
    { Keystroke values for extended keyboard codes. }
  KEY_RIGHTARROW = 333;
  KEY_DOWNARROW = 336;
  KEY_UPARROW = 328;
  KEY_ESCAPE = 27;
  KEY_ENTER = 13;


procedure PutChar( X, Y: Integer; ChCode: Char );
{ Writes the single character ChCode to the screen
at (X, Y), where (X, Y) is relative to the TSR popup
window. This routine is used for displaying the ASCII
table because the usual Pascal Write() translates ASCII
7 to a bell ring, and ASCII 13 and 10 to carriage return
and line feed. By using the PC BIOS routine directly, we
bypass Pascal's translation of these characters. }
begin
  with        CPURegisters  do
  begin
    { Move the cursor to adjusted (X, Y). }
    AH := $02;
    BH := 0;
    DH := UpperLeftY + Y - 2;
    DL := UpperLeftX + X - 2;
    Intr($10, CPURegisters);

    { Output the character to the current cursor location. }
    AH := $09;
    AL := byte(ChCode);
    BH := 0;
    BL := 3 shl 4 + 14;
      { Background=color 3; Foreground=color 14 }
    CX := 1;
    Intr($10, CPURegisters);
  end;
end; {PutChar}


var
  ASCIICode: Integer;
    { Computed from X, Y location in table. }
  I: Integer;
    { For loop index variable. }
  TextLine: String[Width];
    { Buffer to hold width of window's text. }
  X, Y: Integer;
    { Tracks cursor location in ASCII table. }
  Ch : Integer;
    { Holds the keystroke typed. }

begin {DoPopUpFunction}
  { Select White text on Cyan background for
  TSR's text (except table). }
  TextColor( White );
  TextBackground( Cyan );

  { Set up a viewing window; makes calculation
  of X, Y easier. }
  Window(UpperLeftX, UpperLeftY, LowerLeftX, LowerRightY);

  { Enclose the window by drawing a border around
  it and filling the interior with blanks. }
  FillChar( TextLine[1], Width, ' ');
  TextLine[0] := Chr( Width );
  TextLine[1] := chr( 179 );
  TextLine[Width] := chr( 179 );

  for I := 2 to Height - 2 do
  begin
    Gotoxy(1, I);
    Write( TextLine );
  end;

  FillChar( TextLine[1], Width, Chr(196));
  TextLine[1] := Chr( 218 );
  TextLine[Width] := Chr( 191 );
  Gotoxy( 1 , 1 );
  Write( TextLine );

  TextLine[1] := Chr( 192 );
  TextLine[Width] := Chr( 217 );
  Gotoxy ( 1, Height - 1 );
  Write( TextLine );

  { Display window title }
  Gotoxy ( Width div 2 -10, 2 );
  Write( 'Table of ASCII Values' );

  { Draw the ASCII table on the display }
  X := MinX;
  Y := MinY;
  for I := 0 to 255 do
  begin
    PutChar( X, Y, Chr(I) );
    Inc(X);
    If        X = (Width-RightEdge)  then
    begin
      Inc( Y );
      X := MinX;
    end;
  end;

  Gotoxy ( Width div 2 - 20 , 11 );
  Write('Use arrow keys to navigate; Esc when done');
  X := MinX; Y := MinY;
  repeat
    { Compute ASCII code and value at X, Y }
    ASCIICode := (X-MinX + (Y-MinY)*ValuesPerLine);
    Gotoxy (Width div 2 - 11 , 13);
    { NOTE: This allows display of
    "ASCII codes" greater than 256 if cursor
    moves into blank area in table. }
    Write('Character= ', '  ASCII=',ASCIICode:3);
    PutChar(Width div 2, 13, Chr(ASCIICode));
    { Display that value, below }
    Gotoxy ( X, Y );
    Ch := GetKey;
    Case  Ch  Of
      KEY_LEFTARROW:  if  X > MinX  then  Dec(X);
      KEY_RIGHTARROW:
        if  X < (Width - RightEdge - 1)  then  Inc(X);
      KEY_DOWNARROW: if  Y < (MinY+NumLines)  then Inc(Y);
      KEY_UPARROW:  if          Y > MinY  then    Dec(Y);
    end;
  until  Ch = 27;

  { End of TSR popup code. }
end; {DoPopUpFunction}



procedure RunPopUp;
{ Switches from system stack to TSR's stack. Calls
DoPopUpFunction to run the actual TSR application. This
keeps all the ugly details separate from the
application. Note that while the TSR is up on the
screen, and only while the TSR is up, we trap the
Ctrl-Break and DOS critical errors interrupt. We do
nothing when we see them except return, thereby ignoring
the interrupts. }
begin
 { Switch stacks. }
   asm
     CLI
   end;
   SavedSS := SSeg;
   SavedSP := SPtr;
   asm
     MOV   SS, OurSS
     MOV   SP, OurSP
     STI
   end;
   GetIntVec( $1B, PInt1B );
     { Disable Ctrl-Break checking. }
   SetIntVec( $1B, @CBreakCheck );
   GetIntVec( $24, PInt24 );
     { Trap DOS critical errors. }
   SetIntVec( $24, @TrapCriticalErrors );
   SaveDisplay;

   DoPopUpFunction;

   RestoreDisplay;
   SetIntVec( $24, PInt24 );
     { Reenable DOS critical error trapping. }
   SetIntVec( $1B, PInt1B );
     { Reenable Ctrl-C trapping. }
   { Restore stacks. }
   asm
     CLI
     MOV   SS, SavedSS
     MOV   SP, SavedSP
     STI
   end;
end; {RunPopUp}


procedure BackgroundInt;
interrupt;
{ INT 28H }
{ This routine is hooked in the DOS INT 28H chain,
known variously as the DOSOK or DOSIdle interrupt. The
idea is that when applications are doing nothing except
waiting for a keystroke, they can call INT 28
repeatedly. INT 28 runs through a chain of applications
that each get a crack at running.

The keyboard interrupt handler watches for the
magic TSR popup key. Some of the time it runs the TSR
popup directly; when DOS is doing something, however, it
can't run the TSR. So, it sets a flag saying "Hey, INT
28, if you see this flag set, then do the TSR." So the
INT 28 code, here, examines the flag. If the flag is set, INT 28
knows that the TSR was activated, so it calls it now.
We can do this because DOS calls INT 28 only if it's safe
for something else to run.

}
begin
  { Call saved INT 28H handler. }
  asm
    PUSHF
    CALL PInt28
  end;
  if  MadeActive  then
  begin
    TSRInUse := True;
    MadeActive := False;
    RunPopUp;
    TSRInUse := False;
  end;
end; {BackgroundInt}



procedure KeyboardInt;
interrupt;
{ INT 09H }
{ Examines all keyboard interrupts. First calls the
existing interrupt handler. If our TSR is NOT currently
running, then it checks for the magic pop keystrokes. If
the TSR is already running, then we do not want to
activate it again, so we ignore keystroke checking when
the TSR is already alive and on-screen.

Several bytes in low memory contain keyboard status
information. By checking the values in these bytes,
various "not normal" key combinations can be detected.
As implemented here, the TSR is made active by pressing
the left Alt key, plus the SysRq key (Print Screen on my
PC). You can change these keystrokes to something else.
}
const
  CallTSRMask = 6;
  { ACTIVATE TSR = left Alt key + SysRq key }
var
  ScanCode: byte absolute $40:$18;
    { One of the keyboard status bytes. }
begin
  { Call existing keyboard interrupt handler. }
  asm
    PUSHF
    CALL PINT09
  end;

  if  not TSRInUse  then
    if        (ScanCode and CallTSRMask) = CallTSRMask  then
    begin
      { The TSR has been activated. }
      TSRInUse := True;
        { Set to TRUE to prevent reactivation of this TSR. }
      if  (PInDosFlag^ = 0)  then
      begin
        { If in "Safe" DOS area, then pop up now. }
        MadeActive := False; { So INT $28 won't call us. }
        RunPopUp;
        TSRInUse := False;
      end
      else
        MadeActive := True;
          { o/w, set flag let INT 28 call us when ok. }
    end;
end; {KeyboardInt}




procedure DoUnInstall ( var Removed: Boolean ); forward;


{ These are "local" to OurInt12; it is safer to store them in the
TSR's global data area than to place them on the stack as
local variables. }
var
  IdStr : ^String8;
  MessageNum : Integer;

procedure OurInt12
  (_AX, BX, CX, DX, SI, DI, DS, ES, BP:Word);
interrupt;
{ INT 12H }
{ Intercepts INT 12H calls. If the ES:BX register
just happens to point to IdStr1, then the command-line
TSR program just called in. If this is the case, the
other registers could be used to pass a message to the
running TSR. Here, it's used by the running TSR to return
a pointer to another string, confirming that the TSR is
indeed running.
}

var
  DeInstallOk: Boolean;
begin
  IdStr := Ptr( ES, BX );
    { Check to see if ES:BX points to the }
    { magic ID string }
  If  IdStr^ = IdStr1  then
    MessageNum := CX
  else
  begin
    MessageNum := 0;
      { No message rcvd; this is normal DOS call. }
    asm
       pushf
       call    PInt12
       mov     _AX, ax
         { AX returns the memory size value. }
    end;
  end;
  if  MessageNum > 0   then
  { Process a message directed to this TSR. }
  begin
    case  MessageNum  of
      1:  begin
          { Returns pointer to IdStr2 indicating this
          TSR is here. }
            ES := Seg(IdStr2);
            BX := Ofs(IdStr2);
          end;
      2:  begin { Performs request to uninstall the TSR. }
            DoUnInstall (DeInstallOk);
            if         DeInstallOk  then
              CX := 0 { Report success. }
            else
              CX := 1; { Report failure. }
          end;
    end;
  end;
end; {OurInt12}



procedure DoUnInstall ( var Removed: Boolean );
begin
  { See if any TSRs have loaded in memory after us.
  If another TSR has loaded after us, then we really
  cannot safely terminate this TSR. Why? Because when they
  terminate, they may reset their interrupts to point back
  to this TSR. And if this TSR is no longer in memory,
  uh-oh... }

  Removed := True;
  GetIntVec( $28, TempPtr );
  if  TempPtr <> @BackgroundInt  then
    Removed := False;

  GetIntVec( $12, TempPtr );
  If  TempPtr <> @OurInt12  then
    Removed := False;

  GetIntVec( $09, TempPtr );
  if  TempPtr <> @KeyBoardInt  then
    Removed := False;

  if  Removed  then
  begin
    { Restore interrupts }
    SetIntVec( $28, PInt28 );
    SetIntVec( $12, PInt12 );
    SetIntVec( $09, PInt09 );

    { Free up memory allocated to this program using
    INT 21 Func=49H "Release memory". }
    CPURegisters.AH := $49;
    CPURegisters.ES := PrefixSeg;{ Current program's PSP }
    Intr( $21, CPURegisters );
  end;
end; {DoUnInstall}





procedure InstallTSR (var AlreadyInstalled: Boolean );
{ Installs all interrupt handlers for this TSR. }

var
  PSPPtr : ^Word;
  IdStr : ^String8;
begin

  { Check to see if the TSR is already running by
  executing INT 12 with ES:BX pointing to IdStr1. If,
  after calling INT 12, ES:BX points to IdStr2, then the
  TSR is running since it must have intercepted INT 12 and
  set ES:BX to those return values. }

  with        CPURegisters  do
  begin
    ES := Seg(IdStr1);
    BX := Ofs(IdStr1);
    CX := 1;
    Intr( $12, CPURegisters );
    IdStr := Ptr( ES, BX );
    if        IdStr^ = IdStr2  then
    begin
       AlreadyInstalled := True;
       Exit;
    end;
    { TSR hasn't been installed, so install our
    INT 12 driver. }
    asm
      cli
    end;
    GetIntVec( $12, PInt12 );
    SetIntVec( $12, @OurInt12 );
    asm
      sti
    end;
  end;
  AlreadyInstalled := False;
  MadeActive := False;
  DiskInUse := 0;

  { Check to see if TSR is already installed. }
  TSRInUse := False;

  { Deallocate DOS Environment block if not needed. }
  { Comment out this code if you need to access
    environment strings. }
  PSPPtr := Ptr( PrefixSeg, $2C );
  CPURegisters.AX := $4900;
  CPURegisters.ES := PSPPtr^;
  Intr( $21, CPURegisters);

  asm
    cli
  end;

  { Save and set INT 28H, background process interrupt. }
  GetIntVec( $28, PInt28 );
  SetIntVec( $28, @BackgroundInt );

  { Save and set INT 09H, the keyboard interrupt handler. }
  GetIntVec( $09, PInt09 );
  SetIntVec( $09, @KeyboardInt );
  asm
    sti
  end;

  { Initialize pointer to DOS's InDos flag. }
  { Uses INT 21H Function 34H to retrieve a pointer to the
  InDos flag. The result is returned in the ES:BX
  registers. }
  CPURegisters.AH := $34;
  Intr( $21, CPURegisters );
  PInDosFlag := Ptr( CPURegisters.ES, CPURegisters.BX );

  asm
    CLI
  end;
  { Save our SS:SP for later use. }
  OurSS := SSeg;
  OurSP := SPtr;
  asm
    STI
  end;
end; {InstallTSR}

var
  InstallError: Boolean;

begin {program}
  InstallTSR( InstallError );
  if  InstallError  then
  begin
    { This means that the TSR is already running.
      In that case, see if the command line requests
      an uninstall. }
    if  (ParamStr(1) = '/u') or (ParamStr(1) = '/U')  then
      { Send an Uninstall message to the TSR }
      with  CPURegisters  do
      begin
        ES := Seg(IdStr1);
        BX := Ofs(IdStr1);
        CX := 2;
        Intr( $12, CPURegisters );
        if  CX = 0  then
          Writeln('TSR is now uninstalled.')
        else
          Writeln('Unable to uninstall.');
        Exit;
      end
    else
      Writeln('!!!TSR is already installed!!!');
  end
  else
  begin
    Writeln('TSR is now resident.');
    Keep( 0 );
     { Exit to DOS, leaving program memory-resident. }
  end;
end. {program}

[Back to TSR SWAG index]  [Back to Main SWAG index]  [Original]