{**************************************************************************
*   Activates or deactivates TSRs, while leaving them in memory.          *
*   Copyright (c) 1987 Kim Kokkonen, TurboPower Software.                 *
*   Released to the public domain for personal, non-commercial use only.  *
***************************************************************************
*   version 2.3 5/4/87                                                    *
*     first release. version number matches other TSR Utilities           *
*   version 2.4 5/17/87                                                   *
*     fix a bug during reactivate with more than one TSR deactivated      *
*     turn off interrupts during disable and restore                      *
*   version 2.5 6/2/87                                                    *
*     make warning messages a little more useful                          *
*   version 2.6 1/15/89                                                   *
*     convert to Turbo Pascal 5.0                                         *
*   version 2.7                                                           *
*     skipped                                                             *
*   version 2.8 3/10/89                                                   *
*     add option just to check for a TSR                                  *
*   version 2.9 3/18/89                                                   *
*     fix bug in countvecs                                                *
*     check for overlapping patches before disabling                      *
*       (takes care of problem with SK+)                                  *
*     add /O override option to disable TSR even if overlaps detected     *
***************************************************************************
*   telephone: 408-438-8608, CompuServe: 72457,2131.                      *
*   requires Turbo version 5 to compile.                                  *
***************************************************************************}

{$R-,S-}

program DisableTSR;
  {-Deactivate and reactivate memory resident programs}
  {-Leaving them in memory all the while}

uses
  Dos;

const
  Version = '2.9';
  MaxBlocks = 128;            {Max number of DOS allocation blocks supported}

  WatchID = 'TSR WATCHER';    {Marking string for WATCH}

  {Offsets into resident copy of WATCH.COM for data storage}
  WatchOffset = $81;
  NextChange = $104;
  ChangeVectors = $220;
  OrigVectors = $620;
  CurrVectors = $A20;
  MaxChanges = 128;           {Maximum number of vector changes stored in WATCH}

type
  {.F-}
  Block =
  record                      {Store info about each memory block}
    mcb : Word;
    psp : Word;
  end;

  BlockType = 0..MaxBlocks;
  BlockArray = array[BlockType] of Block;

  ChangeBlock =
  record                      {Store info about each vector takeover}
    VecNum : byte;
    case ID : byte of
      0, 1 : (VecOfs, VecSeg : Word);
      2    : (SaveCode : array[1..6] of byte);
      $FF  : (PspAdd : Word);
  end;
  {
  ID is interpreted as follows:
    00 = ChangeBlock holds the new pointer for vector vecnum
    01 = ChangeBlock holds pointer for vecnum but the block is disabled
    02 = ChangeBlock holds the code underneath the vector patch
    FF = ChangeBlock holds the segment of a new PSP
  }
  ChangeArray = array[0..maxchanges] of changeblock;

  HexString = string[4];
  Pathname = string[79];
  {.F+}

var
  Blocks : BlockArray;
  WatchBlock, BlockNum : BlockType;
  Regs : Registers;
  Changes : ChangeArray;
  ChangeMax, ActualMax, WatchSeg, PspHex, StartMCB : Word;
  Action : (aDeactivate, aActivate, aCheckFor);
  Override : Boolean;
  TsrName : Pathname;

  procedure Abort(msg : String; ErrorLevel : Byte);
    {-Halt in case of error}
  begin
    WriteLn(msg);
    Halt(ErrorLevel);
  end;

  function StUpcase(s : String) : String;
    {-Return the uppercase string}
  var
    i : Byte;
  begin
    for i := 1 to Length(s) do
      s[i] := UpCase(s[i]);
    StUpcase := s;
  end;

  function HexW(i : Word) : HexString;
    {-Return HexW representation of Word}
  const
    hc : array[0..15] of Char = '0123456789ABCDEF';
  var
    l, h : Byte;
  begin
    l := Lo(i);
    h := Hi(i);
    HexW[0] := #4;
    HexW[1] := hc[h shr 4];
    HexW[2] := hc[h and $F];
    HexW[3] := hc[l shr 4];
    HexW[4] := hc[l and $F];
  end;

  procedure FindTheBlocks;
    {-Scan memory for the allocated memory blocks}
  const
    MidBlockID = $4D;         {Byte DOS uses to identify part of MCB chain}
    EndBlockID = $5A;         {Byte DOS uses to identify last block of MCB chain}
  var
    mcbSeg : Word;         {Segment address of current MCB}
    nextSeg : Word;        {Computed segment address for the next MCB}
    gotFirst : Boolean;       {True after first MCB is found}
    gotLast : Boolean;        {True after last MCB is found}
    idbyte : Byte;            {Byte that DOS uses to identify an MCB}

    function GetStartMCB : Word;
      {-Return the first MCB segment}
    begin
      Regs.ah := $52;
      MsDos(Regs);
      GetStartMCB := MemW[Regs.es:(Regs.bx-2)];
    end;

    procedure StoreTheBlock(var mcbSeg, nextSeg : Word;
                            var gotFirst, gotLast : Boolean);
      {-Store information regarding the memory block}
    var
      nextID : Byte;
      PspAdd : Word;       {Segment address of the current PSP}
      mcbLen : Word;       {Size of the current memory block in paragraphs}

    begin

      PspAdd := MemW[mcbSeg:1]; {Address of program segment prefix for MCB}
      mcbLen := MemW[mcbSeg:3]; {Size of the MCB in paragraphs}
      nextSeg := Succ(mcbSeg+mcbLen); {Where the next MCB should be}
      nextID := Mem[nextSeg:0];

      if gotLast or (nextID = EndBlockID) or (nextID = MidBlockID) then begin
        inc(BlockNum);
        gotFirst := True;
        with Blocks[BlockNum] do begin
          mcb := mcbSeg;
          psp := PspAdd;
        end;
      end;

    end;

  begin

    {Initialize}
    StartMCB := GetStartMCB;
    mcbSeg := StartMCB;
    gotFirst := False;
    gotLast := False;
    BlockNum := 0;

    {Scan all memory until the last block is found}
    repeat
      idbyte := Mem[mcbSeg:0];
      if idbyte = MidBlockID then begin
        StoreTheBlock(mcbSeg, nextSeg, gotFirst, gotLast);
        if gotFirst then
          mcbSeg := nextSeg
        else
          inc(mcbSeg);
      end else if gotFirst and (idbyte = EndBlockID) then begin
        gotLast := True;
        StoreTheBlock(mcbSeg, nextSeg, gotFirst, gotLast);
      end else
        {Start block was invalid}
        Abort('Corrupted allocation chain or program error....', 255);
    until gotLast;

  end;

  function FindMark(markId : String;
                    markoffset : Word;
                    var b : BlockType) : Boolean;
    {-Find the last memory block matching idstring at offset idoffset}
  var
    found : Boolean;

    function HasIDstring(segment : Word;
                         idString : String;
                         idOffset : Word) : Boolean;
      {-Return true if idstring is found at segment:idoffset}
    var
      tString : String;
      len : Byte;
    begin
      len := Length(idString);
      tString[0] := Chr(len);
      Move(Mem[segment:idOffset], tString[1], len);
      HasIDstring := (tString = idString);
    end;

  begin
    {Scan from the last block down}
    b := BlockNum;
    found := False;
    repeat
      if Blocks[b].psp = PrefixSeg then
        {Assure this program's command line is not matched}
        b := Pred(b)
      else if HasIDstring(Blocks[b].psp, markId, markoffset) then
        {mark found}
        found := True
      else
        {Not a mark}
        b := Pred(b);
    until (b < 1) or found;
    FindMark := found;
  end;

  function ExecutableBlock(PspHex : Word) : Boolean;
    {-Return true if psphex corresponds to an executable code block}
  var
    b : BlockType;
  begin
    for b := BlockNum downto 1 do
      {Search back to find executable rather than environment block}
      if Blocks[b].psp = PspHex then begin
        ExecutableBlock := True;
        Exit;
      end;
    ExecutableBlock := False;
  end;

  procedure InitChangeArray(WatchBlock : BlockType);
    {-Initialize information regarding the WATCH data block}
  var
    watchindex : Word;
    p : ^ChangeBlock;
  begin
    {Store the segment of the WATCH data area}
    WatchSeg := Blocks[WatchBlock].psp;

    {Maximum offset in WATCH data area}
    ActualMax := MemW[WatchSeg:NextChange];

    {Transfer changes from WATCH into a buffer array}
    watchindex := 0;
    ChangeMax := 0;
    while watchindex < ActualMax do begin
      p := Ptr(WatchSeg, ChangeVectors+watchindex);
      Move(p^, Changes[ChangeMax], SizeOf(ChangeBlock));
      watchindex := watchindex+SizeOf(ChangeBlock);
      if watchindex < ActualMax then
        inc(ChangeMax);
    end;
  end;

  procedure PutWatch(chg : ChangeBlock; var watchindex : Word);
    {-Put a change block back into WATCH}
  var
    p : ^ChangeBlock;
  begin
    p := Ptr(WatchSeg, ChangeVectors+watchindex);
    Move(chg, p^, SizeOf(ChangeBlock));
    watchindex := watchindex+SizeOf(ChangeBlock);
  end;

  procedure ActivateTSR(PspHex : Word);
    {-Patch out the active interrupt vectors of a specified TSR}
  var
    nextchg, chg, watchindex : Word;
    checking, didsomething : Boolean;
  begin
    didsomething := False;
    watchindex := 0;
    chg := 0;

    {Scan looking for the specified PSP}
    while chg <= ChangeMax do begin
      with Changes[chg] do
        case ID of

          $FF :               {This record starts a new PSP}
            begin
              checking := (PspAdd = PspHex);
              nextchg := Succ(chg);
              if checking then
                {Turn off interrupts}
                inline($FA)
              else
                {Turn on interrupts}
                inline($FB);
            end;

          $01 :               {This record has an inactive vector redefinition}
            if checking then begin
              {We're in the proper PSP}
              didsomething := True;
              {Change the ID to indicate that vector is active}
              ID := 0;
              {Put the original vector code back in place}
              nextchg := Succ(chg);
              if (Changes[nextchg].ID <> 2) or (Changes[nextchg].VecNum <> VecNum) then
                Abort('Program error in Activate, patch record not found', 255);
              {Restore the patched over code}
              Move(Changes[nextchg].SaveCode, Mem[VecSeg:VecOfs], 6);
              {Don't output the following patch record}
              inc(nextchg);
            end else
              nextchg := Succ(chg);

        else
          nextchg := Succ(chg);
        end;

      {Put the change block back into WATCH}
      PutWatch(Changes[chg], watchindex);
      {Advance to the next change record}
      chg := nextchg;
    end;

    {Store the count back into WATCH}
    MemW[WatchSeg:NextChange] := watchindex;

    if not(didsomething) then
      Abort('No changes were needed to activate '+HexW(PspHex), 1);

  end;

  procedure DeactivateTSR(PspHex : Word);
    {-Patch out the active interrupt vectors of a specified TSR}
  var
    newchange : ChangeBlock;
    chg, watchindex, curpsp : Word;
    putrec, checking, didsomething : Boolean;
    name : pathname;

    procedure PutPatch(vecn : Byte; vecs, veco, curpsp : Word);
      {-Patch vector entry point with JMP to previous controlling vector}
    label
      90;
    var
      vec : ^Word;
      chg : Word;
    begin
      {Get the original vector from WATCH}
      Move(Mem[WatchSeg:(OrigVectors+(vecn shl 2))], vec, 4);

      {Scan the Changes array to look for redefinition of this vector}
      for chg := 0 to ChangeMax do begin
        with Changes[chg] do
          case ID of
            0, 1 :            {This is or was a redefined vector}
              if vecn = VecNum then
                {It's the vector we're interested in}
                {Store the latest value of the vector}
                Move(VecOfs, vec, 4);
            $FF :             {This record starts a new PSP}
              if PspAdd = curpsp then
                {Stop when we get to the PSP that is being disabled}
                goto 90;
          end;
      end;
90:
      {Patch the vector entry point into a JMP FAR vec}
      Mem[vecs:veco] := $EA;
      Move(vec, Mem[vecs:Succ(veco)], 4);
    end;

    function CountVecs(chg : Word) : Word;
      {-Return count of vectors taken over by the PSP starting at changeblock chg}
    var
      count : Word;
      ID : Byte;
    begin
      count := 0;
      repeat
        {Skip over the first one, which defines the current PSP}
        inc(chg);
        ID := Changes[chg].ID;
        if (ID = 0) and (chg <= ChangeMax) then {!!}
          inc(count);
      until (ID = $FF) or (chg >= ChangeMax); {!!}
      CountVecs := count;
    end;

    function ValidToPatch(chg : Word) : Boolean;
      {-Assure that there is space to place 6-byte patches}
    var
      First : Word;
      Next : Word;
      I : Word;
      J : Word;
      IAddr : LongInt;
      JAddr : LongInt;
    begin
      ValidToPatch := True;
      if Override then
        Exit;

      {First vector to patch}
      First := chg+1;

      {Last vector to patch}
      Next := First;
      while (Next <= ChangeMax) and (Changes[Next].ID <> $FF) do
        inc(Next);

      {Any to patch?}
      if Next = First then
        Exit;

      {Compare each pair to assure enough space for patch}
      for I := First to Next-1 do begin
        with Changes[I] do
          IAddr := (LongInt(VecSeg) shl 4)+VecOfs;
        for J := First to Next-1 do
          if I <> J then begin
            with Changes[J] do
              JAddr := (LongInt(VecSeg) shl 4)+VecOfs;
            if Abs(IAddr-JAddr) < 6 then begin
              ValidToPatch := False;
              Exit;
            end;
          end;
      end;
    end;

  begin

    {Scan looking for the specified PSP}
    didsomething := False;
    watchindex := 0;

    for chg := 0 to ChangeMax do begin
      putrec := True;
      with Changes[chg] do
        case ID of

          $FF :               {This record starts a new PSP}
            begin
              checking := (PspAdd = PspHex);
              if checking then begin
                {Store the current PSP}
                curpsp := PspAdd;
                {Make sure WATCH has room for the extra changes}
                if watchindex+(CountVecs(chg)*SizeOf(ChangeBlock)) >
                MaxChanges*SizeOf(ChangeBlock) then
                  Abort('Insufficient space in WATCH data area', 255);
                {Make sure the patches will be valid}
                if not ValidToPatch(chg) then
                  Abort('Insufficient space between vectors to patch TSR', 255);
                {Turn off interrupts}
                inline($FA);
              end else
                {Turn on interrupts}
                inline($FB);
            end;

          $00 :               {This record has an active vector redefinition}
            if checking then begin
              {We're in the proper PSP}
              didsomething := True;

              {Change the ID to indicate that vector is inactive}
              ID := 1;
              {Output the record now so that the new record can immediately follow}
              PutWatch(Changes[chg], watchindex);
              putrec := False;

              {Output a new change record so we can reactivate later}
              {Indicate this is a patch record}
              newchange.ID := 2;
              {Save which vector it goes with}
              newchange.VecNum := VecNum;
              {Save the code we'll patch over}
              Move(Mem[VecSeg:VecOfs], newchange.SaveCode, 6);
              {Output the record to the WATCH area}
              PutWatch(newchange, watchindex);
              {Patch in a JMP to the previous vector}
              PutPatch(VecNum, VecSeg, VecOfs, curpsp);
            end;

        end;
      if putrec then
        {Put the change block back into WATCH}
        PutWatch(Changes[chg], watchindex);
    end;

    {Store the count back into WATCH}
    MemW[WatchSeg:NextChange] := watchindex;

    if not(didsomething) then
      Abort('No changes were needed to deactivate '+tsrname, 1);

  end;

  procedure GetOptions;
    {-Analyze command line for options}
  var
    arg : String;
    arglen : Byte absolute arg;
    i, code : Word;

    procedure WriteHelp;
      {-Show the options}
    begin
      WriteLn;
      WriteLn('DISABLE allows you to selectively disable and reenable a TSR while leaving it');
      WriteLn('in memory. To run DISABLE, you must have previously installed the TSR utility');
      WriteLn('WATCH.');
      WriteLn;
      WriteLn('DISABLE is command-line driven. You specify a single TSR by its name (if you');
      WriteLn('are running DOS 3.0 or later) or by its address as determined from a MAPMEM');
      WriteLn('report. Addresses must be preceded by a dollar sign "$" and specified in hex.');
      WriteLn;
      WriteLn('DISABLE accepts the following command line syntax:');
      WriteLn;
      WriteLn('  DISABLE TSRname|$PSPaddress [Options]');
      WriteLn;
      WriteLn('Options may be preceded by either / or -. Valid options are as follows:');
      WriteLn;
      WriteLn('     /A     reActivate the specified TSR.');
      WriteLn('     /C     Check whether TSR is installed.');
      WriteLn('     /O     Disable the TSR even if dangerous (Override).');
      WriteLn('     /?     Write this help screen.');
      Halt(1);
    end;

    function DOSversion : Byte;
      {-return the major version number of DOS}
    var
      reg : Registers;
    begin
      reg.ah := $30;
      MsDos(reg);
      DOSversion := reg.al;
    end;

    function Owner(envseg : Word) : Pathname;
      {-return the name of the owner program of an MCB}
    type
      chararray = array[0..32767] of Char;
    var
      e : ^chararray;
      i : Word;
      t : Pathname;

      function LongPos(m : Pathname; var s : chararray) : Word;
        {-return the position number of m in s, or 0 if not found}
      var
        mlen : Byte absolute m;
        mc : Char;
        ss : Pathname;
        i, maxindex : Word;
        found : Boolean;
      begin
        i := 0;
        maxindex := SizeOf(s)-mlen;
        ss[0] := m[0];
        if mlen > 0 then begin
          mc := m[1];
          repeat
            while (s[i] <> mc) and (i <= maxindex) do
              inc(i);
            if s[i] = mc then begin
              Move(s[i], ss[1], Length(m));
              found := (ss = m);
              if not(found) then
                inc(i);
            end;
          until found or (i > maxindex);
          if not(found) then
            i := 0;
        end;
        LongPos := i;
      end;

      procedure StripNonAscii(var t : Pathname);
        {-return an empty string if t contains any non-printable characters}
      var
        ipos : Byte;
        goodname : Boolean;
      begin
        goodname := True;
        for ipos := 1 to Length(t) do
          if (t[ipos] <> #0) and ((t[ipos] < ' ') or (t[ipos] > '}')) then
            goodname := False;
        if not(goodname) then
          t := '';
      end;

      procedure StripPathname(var pname : Pathname);
        {-remove leading drive or path name from the input}
      var
        spos, cpos, rpos : Byte;
      begin
        spos := Pos('\', pname);
        cpos := Pos(':', pname);
        if spos+cpos = 0 then
          Exit;
        if spos <> 0 then begin
          {find the last slash in the pathname}
          rpos := Length(pname);
          while (rpos > 0) and (pname[rpos] <> '\') do
            rpos := Pred(rpos);
        end else
          rpos := cpos;
        Delete(pname, 1, rpos);
      end;

      procedure StripExtension(var pname : Pathname);
        {-remove the file extension}
      var
        dotpos : Byte;
      begin
        dotpos := Pos('.', pname);
        if dotpos <> 0 then
          Delete(pname, dotpos, 64);
      end;

    begin
      {point to the environment string}
      e := Ptr(envseg, 0);

      {find end of the standard environment}
      i := LongPos(#0#0, e^);
      if i = 0 then begin
        {something's wrong, exit gracefully}
        Owner := '';
        Exit;
      end;

      {end of environment found, get the program name that follows it}
      t := '';
      i := i+4;               {skip over #0#0#args}
      repeat
        t := t+e^[i];
        inc(i);
      until (Length(t) > 64) or (e^[i] = #0);

      StripNonAscii(t);
      if t = '' then
        Owner := 'N/A'
      else begin
        StripPathname(t);
        StripExtension(t);
        if t = '' then
          t := 'N/A';
        Owner := StUpcase(t);
      end;

    end;

    function FindOwner(name : String) : Word;
      {-Return segment of executable block with specified name}
    var
      b : BlockType;
    begin
      name := StUpcase(name);
      {Scan the blocks in reverse order}
      for b := BlockNum downto 1 do
        with Blocks[b] do
          if Succ(mcb) = psp then
            {This block is an executable block}
            if Owner(MemW[psp:$2C]) = name then begin
              {Found it}
              FindOwner := psp;
              Exit;
            end;
      FindOwner := $FFFF;
    end;

  begin
    {Initialize defaults}
    PspHex := 0;
    Action := aDeactivate;
    Override := False;

    i := 1;
    while i <= ParamCount do begin
      arg := ParamStr(i);
      if (arg[1] = '?') then
        WriteHelp
      else if (arg[1] = '-') or (arg[1] = '/') then
        case arglen of
          1 : Abort('Missing command option following '+arg, 254);
          2 : case UpCase(arg[2]) of
                '?' : WriteHelp;
                'A' : Action := aActivate;
                'C' : Action := aCheckFor;
                'E' : Action := aActivate;
                'O' : Override := True;
              else
                Abort('Unknown command option: '+arg, 254);
              end;
        else
          Abort('Unknown command option: '+arg, 254);
        end
      else begin
        {TSR to change}
        if arg[1] = '$' then begin
          {Treat as hex address}
          Val(arg, PspHex, code);
          if code <> 0 then
            Abort('Invalid hex address specification: '+arg, 254);
        end else if DOSversion >= 3 then
          {Treat as PSP owner name - scan to find proper PSP}
          PspHex := FindOwner(arg)
        else
          Abort('Must have DOS 3.0+ to find TSRs by name', 254);
        TsrName := StUpcase(arg);
      end;
      inc(i);
    end;

    if PspHex = 0 then
      Abort('No TSR name or address specified', 254)
    else if PspHex = $FFFF then
      Abort('No such TSR found', 2);
  end;

begin
  WriteLn('DISABLE ', Version, ', by TurboPower Software');

  {Get all allocated memory blocks in normal memory}
  {Must do first to support TSRs by name in GetOptions}
  FindTheBlocks;

  {Analyze command line for options}
  GetOptions;

  {Find the watch block}
  if not FindMark(WatchID, WatchOffset, WatchBlock) then
    Abort('WATCH must be installed in order to use DISABLE', 255);

  {Assure PspHex corresponds to an executable block}
  if not ExecutableBlock(PspHex) then
    Abort('No such TSR found', 2);

  {Initialize information regarding the WATCH data block}
  InitChangeArray(WatchBlock);

  {Activate or deactivate the TSR}
  case Action of
    aDeactivate:DeactivateTSR(PspHex);
    aActivate:ActivateTSR(PspHex);
  end;

  {Write success message}
  case Action of
    aDeactivate:Write('Deactivated');
    aActivate:Write('Activated');
    aCheckFor:Write('Found');
  end;
  Write(' ');
  if TsrName[1] = '$' then
    Write('TSR at ');
  WriteLn(TsrName);

end.
