TMGPAT3  ;TMG/kst/Patching tools ;09/17/08
         ;;1.0;TMG-LIB;**1**;09/17/08
 ;
 ;"Kevin Toppenberg MD
 ;"GNU General Public License (GPL) applies
 ;"9/26/08

 ;"=======================================================================
 ;" API -- Public Functions.
 ;"=======================================================================
 ;"NEWPACK -- Install a new package from ftp server.
 ;"CONSOLE --show how many patches for a package are available and have not been installed yet
 ;"RESCAN -- show how many patches for a package are available and have not been installed yet
 ;"ShowPatches(PckInit,Ver) -- show installed patches, using scroll box.
 ;"EditNotes -- launch an editor for editing notes about patching.

 ;"=======================================================================
 ;"Private Functions
 ;"=======================================================================
 ;"PrepAvail(pArray,Option) -- prepair an array with patch status, for use with Scroller^TMGUSRIF
 ;"HndOnSel(pArray,Option,Info) -- handle ON SELECT event from Scroller^TMGUSRIF
 ;"HndOnCmd(pArray,Option,Info) -- handle ON SELECT event from Scroller
 ;"StoreMissing(PckInit,pArray) store the list of missing patches with the pending patches
 ;"DownPck(PatchName,Option,Msg) -- Given a package name, ensure all pending patches are local.
 ;"$$Rpt1Avail^TMGPAT3(PatchName)
 ;"$$RptAvail^TMGPAT3(PckInit)
 ;"Scan4New(MaxDays,Option) -- scan all packages and determine how many patches are pending for each
 ;"Scan41(PckInit,MaxDays,Option) -- scan one package and determine how many patches are pending
 ;"Scan41a1Ver(PckInit,Ver,MaxDays,Option) -- scan one package and determine how many patches are pending
 ;"GetNew(PckInit,Ver,pArray,RefreshNeeded,Option) -- Get array of **just** patches still to be installed for a given package/version
 ;"GetAvail(PckInit,Ver,pArray,RefreshNeeded,Option) -- return array of all patches for a given package/version
 ;"GetPList(PckInit,Ver,pArray) -- get a list of applied patches, from PACKAGE file, into Array
 ;"PrepPatchList(PckInit,Ver,pShowArray,ByPatchNum) -- prepair the patch list for display in scroll box.
 ;"HndOnPCmd(pArray,Option,Info) -- handle ON SELECT event from Scroller
 ;"ShowAvail -- Show data that tallies the available patches.
 ;"IncLineCt(lineCount,pageLen)
 ;"=======================================================================
 ;"=======================================================================

 ;"NOTE: This Module should be re-written.  Rather than store the data in the global ^TMG(...
 ;"      the Fileman file 22709 should be used.  As it is now, it is a duplication of organization.

NEWPACK
        ;"Purpose: Install a new package from ftp server.

        new %,DIR,PckInit,Ver,X,Y,Msg

        do Logo^TMGPAT1
       set %=1
        write "Install a NEW PACKAGE from ftp.va.gov" do YN^DICN write !
        if %'=1 goto NPDone
        set DIR(0)="F^2:4"
        set DIR("A")="Enter PACKAGE prefix (? for help)"
        set DIR("?")="Enter Namespace initials."
        set DIR("?",1)="Enter namespace package prefix initials."
        set DIR("?",2)="E.g. for Fileman, enter: DI"
        set DIR("?",3)="Enter ^ to abort."
        do ^DIR write ! ;"results in X and Y
        if Y="^" goto NPDone
        set PckInit=Y

        new Array,result
        write "Fetching info from VA ftp server..."
        set result=$$GetPckList^TMGKERNL(PckInit,.Array)
        write "  Done.",!
        if result=0 goto NPDone
        new IEN9d4 set IEN9d4=+$order(^DIC(9.4,"C",PckInit,""))
        if IEN9d4'>0 do  goto NPDone
        . do AddMsg^TMGPAT2("Can't find PACKAGE named '"_PckInit_"'",1,.Msg)

NPDone
        if $$ShowMsg^TMGPAT2(.Msg)
        write "Goodbye.",!
        quit

;"====================================================================

CONSOLE
        new Array,Option
        do PrepAvail("Array",.Option)
        set Option("FOOTER",1,1)="^ Exit"
        set Option("FOOTER",1,2)="? Help"
        set Option("FOOTER",1,3)="[F1] SHOW Compl"
        set Option("FOOTER",1,4)="[F3] Hx"
        set Option("FOOTER",1,5)="[F4] Downld Pak"
        set Option("FOOTER",1,6)="[F5] Notes"
        set Option("FOOTER",1,7)="[F6] Add Waiting"
        set Option("ON SELECT")="HndOnSel^TMGPAT3"
        set Option("ON CMD")="HndOnCmd^TMGPAT3"
        write #
        do Scroller^TMGUSRIF("Array",.Option)
        quit

PrepAvail(pArray,Option)
        ;"Purpose: To prepair an array with patch status, for use with Scroller^TMGUSRIF
        ;"Input: pArray -- PASS BY NAME.  Array to put info into.  Prior data is killed.
        ;"       Option -- PASS BY REFERENCE.  Prior data is NOT killed.  See Scroller^TMGUSRIF for details
        ;"                 Option("HIDE EMPTY")=0 OPTIONAL. Default is 0.  If 1 then, entries with no patches.
        ;"       Also-- Uses global variable...
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version)=Count
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"DATE REFRESHED")=Last date server checked.
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"PATCHES",######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"PATCHES",######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,"FULL NAME")=Package name
        ;"Results: None

        set pArray=$get(pArray) goto:(pArray="") pAvDone
        kill @pArray
        new Hinder,Blocked
        new oneLine,lineCt set lineCt=1
        new PckInit set PckInit=""
        new grandTotal set grandTotal=0
        new hideEmpty set hideEmpty=$get(Option("HIDE EMPTY"),1)
        for  set PckInit=$order(^TMG("KIDS","PENDING PATCHES",PckInit)) quit:(PckInit="")  do
        . new total set total=0
        . new Ver set Ver=""
        . new PackageName set PackageName=$get(^TMG("KIDS","PENDING PATCHES",PckInit,"FULL NAME"))
        . for  set Ver=$order(^TMG("KIDS","PENDING PATCHES",PckInit,Ver)) quit:(+Ver'>0)  do
        . . set total=total+$get(^TMG("KIDS","PENDING PATCHES",PckInit,Ver))
        . set grandTotal=grandTotal+total
        . if (total=0)&(hideEmpty=1) quit
        . set oneLine="("_PckInit_") "_PackageName_"  "
        . set oneLine=$$LJ^XLFSTR($extract(oneLine,1,40),40)_"--> "_$$RJ^XLFSTR(total,3)_" patches. "
        . new tempArray,current,maxVer
        . set maxVer=0,Ver="",current=""
        . for  set Ver=$order(^TMG("KIDS","PENDING PATCHES",PckInit,Ver)) quit:(+Ver'>0)  do
        . . if Ver'>maxVer quit
        . . new temp set temp=$$GetLastPackage^TMGPAT1(PckInit,Ver)
        . . if temp'="" set maxVer=Ver,current=temp
        . set oneLine=oneLine_"Currently @ "_current
        . set @pArray@(lineCt,oneLine)=$piece(current,"*",1,2)
        . set lineCt=lineCt+1
        . new i set i=""
        . for  set i=$order(^TMG("KIDS","PENDING PATCHES",PckInit,"WAITING FOR",i)) quit:(i="")  do
        . . set oneLine="    Waiting for "_i
        . . set @pArray@(lineCt,oneLine)=$piece(i,"*",1,2)
        . . set lineCt=lineCt+1
        . . new init set init=$piece(i,"*",1)
        . . set Hinder(init,PckInit)=""
        . . set Blocked(PckInit)=1

        if '$data(Hinder) goto pAV2

        new count for count=1:1:5 do
        . set init="" for  set init=$order(Hinder(init)) quit:(init="")  do
        . . new init1,init2 set init1=init,init2=""
        . . for  set init2=$order(Hinder(init,init2)) quit:(init2="")  do
        . . . quit:(init2=init)
        . . . merge Hinder(init,init2)=Hinder(init2)

        set @pArray@(lineCt,"--- SUMMARY ------------------------------")=""
        set lineCt=lineCt+1
        new spaces set $piece(spaces," ",20)=" "
        new ref set ref="Hinder"
        new hideArray
        set init=""
        for  set ref=$query(@ref) quit:(ref="")  do
        . if $$OREF^DILF($query(@ref))[$$OREF^DILF(ref) quit
        . new count,node,done set done=0
        . for count=1:1:$qlength(ref) do  quit:done
        . . set node=$qsubscript(ref,count)
        . . set oneLine=$select((count=1):"#",1:"")
        . . set oneLine=oneLine_$extract(spaces,1,count*3)_"Package "_node
        . . if (count=1)&($get(Blocked(node))=1) set done=1 quit
        . . if count=1 set oneLine=oneLine_" is hindering..."
        . . else  if count<$qlength(ref) set oneLine=oneLine_", which is hindering..."
        . . if $get(hideArray(count))=oneLine quit
        . . set hideArray(count)=oneLine
        . . set @pArray@(lineCt,oneLine)="",lineCt=lineCt+1

pAV2    set Option("HEADER",1)="TMG Patch Helper--  "_grandTotal_" Patches to be installed in all packages."

pAvDone
        quit


HndOnSel(pArray,Option,Info)
        ;"Purpose: handle ON SELECT event from Scroller^TMGUSRIF
        ;"Input: pArray,Option,Info -- see documentation in Scroller^TMGUSRIF
        ;"       Info has this:
        ;"          Info("CURRENT LINE","NUMBER")=number currently highlighted line
        ;"          Info("CURRENT LINE","TEXT")=Text of currently highlighted line
        ;"          Info("CURRENT LINE","RETURN")=return value of currently highlighted line

        new PatchName,PckInit,Ver
        set PatchName=$get(Info("CURRENT LINE","RETURN"))
        do ParsePatchName^TMGPAT2(PatchName,.PckInit,.Ver)
        if (PckInit="")&(Ver="") do  goto HOSDone
        . write "?? The line selected specify any command ??",!
        do DONEXTPK^TMGPAT1(PckInit,Ver)
        do PrepAvail(pArray,.Option)
HOSDone
        do PressToCont^TMGUSRIF
        write #
        quit


HndOnCmd(pArray,Option,Info)  ;"Part of TestScrl
        ;"Purpose: handle ON SELECT event from Scroller
        ;"Input: pArray,Option,Info -- see documentation in Scroller
        ;"       Info has this:
        ;"          Info("USER INPUT")=input
        ;"          Info("CURRENT LINE","NUMBER")=number currently highlighted line
        ;"          Info("CURRENT LINE","TEXT")=Text of currently highlighted line
        ;"          Info("CURRENT LINE","RETURN")=return value of currently highlighted line

        new input set input=$$UP^XLFSTR($get(Info("USER INPUT")))
        if input["F2" do
        . set Option("FOOTER",1,3)="[F1] SHOW compl"
        . set Option("HIDE EMPTY")=1
        else  if input="RESCAN" do
        . do RESCAN
        else  if input["F1" do
        . set Option("FOOTER",1,3)="[F2] HIDE compl"
        . set Option("HIDE EMPTY")=0
        else  if input["F3" do
        . new PatchName set PatchName=$get(Info("CURRENT LINE","RETURN")) quit:(PatchName="")
        . new PckInit,Ver
        . do ParsePatchName^TMGPAT2(PatchName,.PckInit,.Ver)
        . if $$ShowPatches(PckInit,Ver)
        else  if input["F4" do
        . new PatchName set PatchName=$get(Info("CURRENT LINE","RETURN")) quit:(PatchName="")
        . new Option set Option("VERBOSE")=1
        . new % set %=1
        . write "Ensure that all pending patches for ",PatchName," have been downloaded"
        . do YN^DICN write !
        . if %'=1 quit
        . do DownPck(PatchName,.Option)
        . do PressToCont^TMGUSRIF
        else  if input["F5" do
        . do EditNotes
        . do PressToCont^TMGUSRIF
        else  if input["F6" do  ;"Add Waiting"
        . if Info("CURRENT LINE","TEXT")'["-->" do  quit
        . . write !,"Please first select containing '-->'",!
        . . do PressToCont^TMGUSRIF
        . new PatchName set PatchName=$get(Info("CURRENT LINE","RETURN")) quit:(PatchName="")
        . new PckInit set PckInit=$piece(PatchName,"*",1)
        . new % set %=1
        . write !,"Manually add a 'Waiting For' entry for ",PatchName
        . do YN^DICN write !
        . if %'=1 quit
        . new DIR,DIRUT set DIR(0)="F",DIR("A")="Enter what "_PatchName_" is waiting for"
        . do ^DIR write ! if $data(DIRUT) quit
        . do AddMissing(PckInit,Y)
        . do PressToCont^TMGUSRIF
        else  if input="NEWPACK" do
        . do NEWPACK
        else  if input="?" do
        . write !,"Use UP and DOWN cursor keys to select package, then ENTER to work on.",!
        . write "Enter 'NEWPACK' to install a NEW package.",!
        . write "Enter 'RESCAN' to rescan the ftp.va.gov server",!
        . write "Enter ^ at the ':' prompt to quit",!
        . do PressToCont^TMGUSRIF
        else  if input'="" do
        . write !,"Input ",$get(Info("USER INPUT"))," not recognized.",!
        . do PressToCont^TMGUSRIF

        do PrepAvail(pArray,.Option)
        write #
        quit


StoreMissing(PckInit,pArray)
        ;"Purpose: to store the list of missing patches with the pending patches
        kill ^TMG("KIDS","PENDING PATCHES",PckInit,"WAITING FOR")
        merge ^TMG("KIDS","PENDING PATCHES",PckInit,"WAITING FOR")=@pArray
        quit

AddMissing(PckInit,PatchName)
        ;"Purpose: Add a missing patche to pending patches
        set ^TMG("KIDS","PENDING PATCHES",PckInit,"WAITING FOR",PatchName)=""
        quit

DownPck(PatchName,Option,Msg)
        ;"Purpose: given a patch name, ensure all pending patches are local.
        ;"Input: PatchName -- patch name, e.g. ABC*1.0*123
        ;"       Option -- Optional.  PASS BY REFERENCE.
        ;"                  Option("VERBOSE")=1 --> puts output to console
        ;"       Msg -- PASS BY REFERANCE, an OUT PARAMETER
        ;"              Errors are stored in Msg("ERROR",x)=Message
        ;"                                   Msg("ERROR")=count of last error
        ;"              Message are store in Msg(x)=Message
        ;"                                   Msg=count of last message+1
        ;"Results: none

        new PckInit,Ver,PatchNum,seqNum,Info
        do ParsePatchName^TMGPAT2(PatchName,.PckInit,.Ver,.PatchNum,.seqNum)
        do Scan41a1Ver(PckInit,Ver,90)
        new total set total=+$get(^TMG("KIDS","PENDING PATCHES",PckInit,Ver))
        new count set count=1
        new patch set patch=""
        for  set patch=$order(^TMG("KIDS","PENDING PATCHES",PckInit,Ver,"PATCHES",patch)) quit:(patch="")  do
        . new patchName set patchName=$get(^TMG("KIDS","PENDING PATCHES",PckInit,Ver,"PATCHES",patch))
        . if $get(Option("VERBOSE"))=1 write count,"/",total,".  ---- ",patchName," ----",!
        . new IENS set IENS=$$GetIENS^TMGPAT2(patchName) quit:(IENS="")
        . if $$EnsureLocal^TMGPAT2(IENS,.Info,.Msg,.Option)=0 do
        . . do AddMsg^TMGPAT2("Unable to download patch to local file system.",1,Msg)
        . set count=count+1

        if $get(Option("VERBOSE"))=1 do
        . if $$ShowMsg^TMGPAT2(.Msg)

        quit

Rpt1Avail(PatchName)
        ;"Purpose: given a patch name (e.g. ABC*1.0*123), return pending patches.
        new PckInit,Ver,PatchNum,seqNum
        new count set count=-1
        do ParsePatchName^TMGPAT2(PatchName,.PckInit,.Ver,.PatchNum,.seqNum)
        if ($get(PckInit)="")!($get(Ver)="") goto Rpt1Done
        do Scan41a1Ver(PckInit,Ver,90)
        set count=+$get(^TMG("KIDS","PENDING PATCHES",PckInit,Ver))
Rpt1Done
        quit count

RptAvail(PckInit)
        ;"Purpose: given a package (e.g. ABC), return pending patches.
        new total set total=0
        new Ver set Ver=""
        for  set Ver=$order(^TMG("KIDS","PENDING PATCHES",PckInit,Ver)) quit:(+Ver'>0)  do
        . set total=total+$get(^TMG("KIDS","PENDING PATCHES",PckInit,Ver))
        quit total

RESCAN
        ;"Purpose: To show how many patches for a package are available and have not been installed yet
        write !
        new DUOUT,DIR
        set DIR("A")="Search ftp server if data is older than __ days old? (SLOW!)"
        set DIR("B")=90
        set DIR(0)="N^0:999:0"
        do ^DIR write !
        new Option
        set Option("VERBOSE")=0
        if $get(DUOUT) quit
        do Scan4New(+Y,.Option)
        ;"if '$get(DUOUT) do ShowAvail
SADone  quit



Scan4New(MaxDays,Option)
        ;"Purpose: to scan all packages and determine how many patches are pending for each
        ;"Input: MaxDays -- the number of days that old days can be used.  If last refresh
        ;"                  was greater than this number, then ftp.va.gov is queried again.
        ;"       Option -- Optional.  PASS BY REFERENCE.
        ;"                  Option("VERBOSE")=1 --> puts output to console
        ;"Output: Results will be stored:
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version)=Count
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"DATE REFRESHED")=Last date server checked.
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"PATCHES",######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"PATCHES",######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,"FULL NAME")=Package name
        ;"
        ;"Results: none

        ;"NOTE: This function should be re-written.  Rather than store the data in the global ^TMG(...
        ;"      the Fileman file 22709 should be used.  As it is now, it is a duplication of organization.

        set MaxDays=+$get(MaxDays)
        new PackageName set PackageName=""
        for  set PackageName=$order(^DIC(9.4,"B",PackageName)) quit:(PackageName="")  do
        . new IEN9d4 set IEN9d4=+$order(^DIC(9.4,"B",PackageName,"")) quit:(IEN9d4'>0)
        . new PckInit set PckInit=$piece($get(^DIC(9.4,IEN9d4,0)),"^",2) ;"0;2 = Package prefix
        . do Scan41(PckInit,MaxDays,.Option)

        quit

Scan41(PckInit,MaxDays,Option)
        ;"Purpose: to scan one package and determine how many patches are pending
        ;"Input: PckInit -- Package Initials/prefix
        ;"       MaxDays -- the cutoff for when to requery the server
        ;"       Option -- PASS BY REFERENCE.
        ;"              Option("VERBOSE")=1 to be verbose.
        ;"Output: Results will be stored:
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version)=Count
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"DATE REFRESHED")=Last date server checked.
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"PATCHES",######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"PATCHES",######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,"FULL NAME")=Package name
        ;"Results: none

        new IEN9d4 set IEN9d4=+$order(^DIC(9.4,"C",PckInit,""))
        new PackageName set PackageName=""
        if IEN9d4>0 set PackageName=$piece($get(^DIC(9.4,IEN9d4,0)),"^",1)
        if $get(Option("VERBOSE"))=1 write "Checking Package: ",PackageName," (",PckInit,")...",!
        set ^TMG("KIDS","PENDING PATCHES",PckInit,"FULL NAME")=PackageName
        ;"kill ^TMG("KIDS","PENDING PATCHES",PckInit,"DATE REFRESHED") ;"force refresh
        set Ver=""
        for  set Ver=$order(^DIC(9.4,IEN9d4,22,"B",Ver)) quit:(Ver="")  do
        . do Scan41a1Ver(PckInit,Ver,MaxDays,.Option)

        quit


Scan41a1Ver(PckInit,Ver,MaxDays,Option)
        ;"Purpose: to scan one package and determine how many patches are pending
        ;"Input: PckInit -- Package Initials/prefix
        ;"       Ver -- The version of the Package
        ;"       MaxDays -- the cutoff for when to requery the server
        ;"       Option -- PASS BY REFERENCE.
        ;"              Option("VERBOSE")=1 to be verbose.
        ;"Output: Results will be stored:
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version)=Count
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"DATE REFRESHED")=Last date server checked.
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"PATCHES",######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,Version,"PATCHES",######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"          ^TMG("KIDS","PENDING PATCHES",PackageInitials,"FULL NAME")=Package name
        ;"Results: none

        ;"kill ^TMG("KIDS","PENDING PATCHES",PckInit,"DATE REFRESHED") ;"force refresh
        if $get(PckInit)="" goto S41Done
        if $get(Ver)="" goto S41Done
        set MaxDays=+$get(MaxDays,90)
        if $get(Option("VERBOSE"))=1 write "  Ver: ",Ver,"  ",!
        new RefreshNeeded
        new lastCheck set lastCheck=$get(^TMG("KIDS","PENDING PATCHES",PckInit,"DATE REFRESHED"))
        if lastCheck'="" do
        . new X,Y,%DT,X1,X2
        . set X=lastCheck,%DT="TS"
        . do ^%DT ;"result in Y
        . set X=0
        . do NOW^%DTC ;"returns date in X
        . set X1=X,X2=Y
        . do ^%DTC  ;"returns X=X1-X2
        . set RefreshNeeded=(X>MaxDays)
        else  set RefreshNeeded=1
        new pArray set pArray=$name(^TMG("KIDS","PENDING PATCHES",PckInit,Ver,"PATCHES"))
        new count set count=$$GetNew(PckInit,Ver,pArray,RefreshNeeded,.Option)
        if $get(Option("VERBOSE"))=1 write "  ",count," patches to be installed.",!
        set ^TMG("KIDS","PENDING PATCHES",PckInit,Ver)=count
        if RefreshNeeded do
        . new %,%I,X,Y
        . do NOW^%DTC set Y=%
        . X ^DD("DD") ;"result in Y
        . set ^TMG("KIDS","PENDING PATCHES",PckInit,"DATE REFRESHED")=Y

S41Done
        quit


GetNew(PckInit,Ver,pArray,RefreshNeeded,Option)
        ;"Purpose: Get array of **just** patches still to be installed for a given package/version
        ;"Input: PckInit -- this is the namespace of the package to get patches for, e.g. 'DI' for fileman
        ;"       Ver -- the package version
        ;"       pArray -- PASS BY NAME. An OUT PARAMETER.  Format:
        ;"              @pArray@(######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"              @pArray@(######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"       NeedsRefresh -- 0 if refreshing not needed (just ensure file exists)
        ;"       Option -- Optional.  PASS BY REFERENCE.
        ;"                   Option("VERBOSE")=1 --> puts output to console
        ;"Results: Number of patches still to be installed.

        new count set count=0

        do GetAvail(PckInit,Ver,pArray,.RefreshNeeded,.Option)
        new LastPck set LastPck=$$GetLastPackage^TMGPAT1(PckInit,Ver)
        new lastSeq set lastSeq=+$piece(LastPck,"SEQ #",2)
        if lastSeq="" kill @pArray goto GNDone
        new i set i=""
        for  set i=$order(@pArray@(i)) quit:(i="")  do
        . if +i'>lastSeq kill @pArray@(i) quit
        . set count=count+1
GNDone
        quit count



GetAvail(PckInit,Ver,pArray,RefreshNeeded,Option)
        ;"Purpose: return array of all patches for a given package/version
        ;"Input: PckInit -- this is the namespace of the package to get patches for, e.g. 'DI' for fileman
        ;"       Ver -- the package version
        ;"       pArray -- PASS BY NAME. An OUT PARAMETER.  Format:
        ;"              @pArray@(######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"              @pArray@(######)=AAAA*NN.NN*NNNN SEQ #1234"
        ;"       NeedsRefresh -- 0 if refreshing not needed (just ensure file exists)
        ;"       Option -- Optional.  PASS BY REFERENCE.
        ;"                   Option("VERBOSE")=1 --> puts output to console
        ;"results: none

        kill @pArray
        if $$RefreshPackge^TMGPAT2(PckInit,.Msg,.RefreshNeeded,.Option)

        new IEN9d4 set IEN9d4=+$order(^DIC(9.4,"C",PckInit,""))
        if IEN9d4'>0 goto GADone
        new PckIEN set PckIEN=+$order(^TMG(22709,"B",IEN9d4,""))
        if PckIEN'>0 goto GADone
        new VerIEN set VerIEN=+$order(^TMG(22709,PckIEN,1,"B",Ver,""))
        if VerIEN'>0 set VerIEN=+$order(^TMG(22709,PckIEN,1,"B",$piece(Ver,".0",1),""))
        if VerIEN'>0 goto GADone

        new patchIEN,nextSeq
        set patchIEN=0
        for  set patchIEN=$order(^TMG(22709,PckIEN,1,VerIEN,1,patchIEN)) quit:(patchIEN'>0)  do
        . new node0 set node0=$get(^TMG(22709,PckIEN,1,VerIEN,1,patchIEN,0)) quit:(node0="")
        . new patchNum set patchNum=$piece(node0,"^",2)
        . new oneSeqNum set oneSeqNum=$piece(node0,"^",2)
        . if oneSeqNum'>0 set oneSeqNum=0
        . set @pArray@($$RJ^XLFSTR(oneSeqNum,6,"0"))=PckInit_"*"_Ver_"*"_patchNum_" SEQ #"_oneSeqNum

GADone
        quit


TestPList  ;"temp function.
        new PckInit,Ver,Array
        do GetPckVer^TMGPAT1(.PckInit,.Ver)
        if Ver="^" goto TPLDone
        if $$GetPList(PckInit,Ver,"Array")=0 goto TPLDone
        zwr Array(*)
TPLDone quit


GetPList(PckInit,Ver,pArray)
        ;"Purpose: to get a list of applied patches, from PACKAGE file, into Array
        ;"Input:
        ;"Input: PckInit -- this is the namespace of the package to get patches for, e.g. 'DI' for fileman
        ;"       Ver -- the package version
        ;"       pArray -- PASS BY NAME. An OUT PARAMETER.  Format:
        ;"              @pArray@(OrderNum)=PatchName
        ;"              @pArray@(OrderNum,"i",".01")=PatchName
        ;"              @pArray@(OrderNum,"i",".02")=Patch Date
        ;"              @pArray@(OrderNum,"i",".03")=Applied By
        ;"              @pArray@(OrderNum)=PatchName
        ;"Results: 1 if OK, 0 if error
        ;
        new result set result=1
        ;
        new IEN9d4,IEN9d49
        if $$GetPVIEN^TMGPAT1(PckInit,Ver,.IEN9d4,.IEN9d49)=0 set result=0 goto GPLDone
        ;
        new orderNum set orderNum=1
        new patchIEN set patchIEN=0
        for  set patchIEN=$order(^DIC(9.4,IEN9d4,22,IEN9d49,"PAH",patchIEN)) quit:(+patchIEN'>0)  do
        . new s set s=$get(^DIC(9.4,IEN9d4,22,IEN9d49,"PAH",patchIEN,0))
        . set @pArray@(orderNum)=$piece(s,"^",1)  ;"0;1=.01    --PATCH APPLICATION HISTORY
        . new IENS set IENS=patchIEN_","_IEN9d49_","_IEN9d4_","
        . new TMGDATA,TMGMSG
        . do GETS^DIQ(9.4901,IENS,".01;.02;.03","","TMGDATA","TMGMSG")
        . merge @pArray@(orderNum,"i")=TMGDATA("9.4901",IENS)
        . ;"zwr TMGDATA(*)
        . set orderNum=orderNum+1
        ;
GPLDone quit result


SHOWPLST
        ;"Purpose: query user for package and version, then show patches.
        new PckInit,Ver,Array
        do GetPckVer^TMGPAT1(.PckInit,.Ver)
        if Ver="^" goto SLPDone
        if $$ShowPatches(PckInit,Ver)
SLPDone quit


ShowPatches(PckInit,Ver)
        ;"Purpose: to show installed patches, using scroll box.
        ;"Input: PckInit -- this is the namespace of the package to get patches for, e.g. 'DI' for fileman
        ;"       Ver -- the package version
        ;"Results: 1 if OK, 0 if error

        new TMGPCKI set TMGPCKI=PckInit  ;"used in HndOnPCmd^TMGPAT3
        new TMGPVER set TMGPVER=Ver      ;"used in HndOnPCmd^TMGPAT3
        new TMGSORT set TMGSORT=2        ;"sort by SEQ number.  Used in HndOnPCmd^TMGPAT3
        new TMGSARRAY
        set TMGSARRAY(0)="SORT by IEN order"
        set TMGSARRAY(1)="SORT by PATCH num"
        set TMGSARRAY(2)="SORT by SEQ num"
        new Option,ShowArray,result
        set Option("HEADER",1)="Applied patches for package "_PckInit_"   "_TMGSARRAY(TMGSORT)
        set Option("FOOTER",1,1)="^ Done"
        set Option("FOOTER",1,2)="? Help"
        set Option("FOOTER",1,3)="[F1] "_TMGSARRAY(0)
        set Option("FOOTER",1,4)="[F2] Fix Missing PATCH"
        set Option("FOOTER",1,5)="[F3] Fix Missing SEQ"
        set Option("ON CMD")="HndOnPCmd^TMGPAT3"
        set Option("SHOW INDEX")=1

        do PrepPatchList(PckInit,Ver,"ShowArray",TMGSORT)
        if $data(ShowArray)=0 do
        . do AskVer^TMGPAT1(PckInit,.Ver)
        . do PrepPatchList(PckInit,Ver,"ShowArray",TMGSORT)

        write #
        do Scroller^TMGUSRIF("ShowArray",.Option)

SPDone  quit 1

PrepPatchList(PckInit,Ver,pShowArray,Mode)
        ;"Purpose: to prepair the patch list for display in scroll box.
        ;"Input: PckInit -- this is the namespace of the package to get patches for, e.g. 'DI' for fileman
        ;"       Ver -- the package version
        ;"       pShowArray -- PASS BY NAME, an OUT PARAMATER
        ;"       Mode -- OPTIONAL.  0: Otherwise by IEN order
        ;"                          1: Then sorted by patch number,
        ;"                          2: Otherwise by SEQ Num

        set ByPatchNum=+$get(ByPatchNum)
        new index set index=1
        new showI set showI=1
        kill @pShowArray
        new Array,tempA
        if $$GetPList(.PckInit,.Ver,"Array")=0 goto PPLDone
        if Mode=0 goto PPL2

        new Num
        for  set index=$order(Array(index)) quit:(index="")  do
        . if Mode=1 set Num=+$piece(Array(index)," ",1)
        . else  set Num=+$piece(Array(index),"SEQ #",2)
        . new s,patch
        . set patch=PckInit_"*"_Ver_"*"_$get(Array(index))
        . set s=$$LJ^XLFSTR(patch,25)
        . set s=s_" Applied: "_$get(Array(index,"i",".02"))_"  "
        . set s=s_" By: "_$get(Array(index,"i",".03"))
        . set tempA(Num)=s
        set Num=""
        for  set Num=$order(tempA(Num)) quit:(Num="")  do
        . new s set s=$get(tempA(Num)) quit:(s="")
        . set @pShowArray@(showI,s)=$piece(s," Applied",1)
        . set showI=showI+1
        goto PPLDone

PPL2    for  set index=$order(Array(index)) quit:(index="")  do
        . new s,patch
        . set patch=PckInit_"*"_Ver_"*"_$get(Array(index))
        . set s=$$LJ^XLFSTR(patch,25)
        . set s=s_" Applied: "_$get(Array(index,"i",".02"))_"  "
        . set s=s_" By: "_$get(Array(index,"i",".03"))
        . set @pShowArray@(showI,s)=patch
        . set showI=showI+1

PPLDone quit

HndOnPCmd(pArray,Option,Info)
        ;"Purpose: handle ON SELECT event from Scroller
        ;"Input: pArray,Option,Info -- see documentation in Scroller
        ;"       Info has this: Info("USER INPUT")=input
        ;"NOTE: uses global-scope vars set up in ShowPatches:
        ;" TMGPCKI,TMGPVER,TMGSORT

        new input set input=$$UP^XLFSTR($get(Info("USER INPUT")))
        if input["F1" do
        . set Option("HEADER",1)="Applied patches for package "_PckInit_"   "_TMGSARRAY(TMGSORT)
        . do PrepPatchList(TMGPCKI,TMGPVER,pArray,TMGSORT)
        . set TMGSORT=TMGSORT+1
        . if TMGSORT=3 set TMGSORT=0
        . set Option("FOOTER",1,3)="[F1] "_TMGSARRAY(TMGSORT)
        else  if input["F2" do
        . do FixMisInit^TMGPAT1(TMGPCKI,TMGPVER,0)
        . do PressToCont^TMGUSRIF
        else  if input["F3" do
        . do FixMisInit^TMGPAT1(TMGPCKI,TMGPVER,1)
        . do PrepPatchList(TMGPCKI,TMGPVER,pArray,TMGSORT)
        . do PressToCont^TMGUSRIF
        else  if input="?" do
        . write !,"Use UP and DOWN cursor keys to scroll.",!
        . write "Press F1 or F2 to change sorting",!
        . write "Enter ^ at the ':' prompt when done",!
        . do PressToCont^TMGUSRIF
        else  if input'="" do
        . write !,"Input ",$get(Info("USER INPUT"))," not recognized.",!
        . do PressToCont^TMGUSRIF

        write #
        quit

EditNotes
        ;"Purpose: to launch an editor for editing notes about patching.
        new FPName set FPName=$get(^TMG("KIDS","PATCH DIR"),"/tmp/")_"Patch_Notes.txt"
        if $$EditHFSFile^TMGKERNL(FPName)
        quit
