Differences

This shows you the differences between two versions of the page.

Link to this comparison view

monitors:listprn [2012/05/04 08:28] (current)
Line 1: Line 1:
 +====== listprn.vbs ======
 +
 +^ Author | [[ goldfndr@gmail.com | Richard Finegold ]] |
 +^ Compatibility | Xymon/​Hobbit/​Big Brother |
 +^ Requirements | VBScript and (BBWin or BBNT) on client |
 +^ Download | None |
 +^ Last Update | 2012-05-03 |
 +
 +===== Description =====
 +This lists TCP/​IP-connected printers on a print server, their drivers (including versions), their pingability,​ print jobs, and port setups. The information gathered is quite lengthy, enough that I found a memory leak in BBWin 0.12.
 +
 +===== Installation =====
 +=== Client side ===
 +  * Confirm that the client is actually being used as a print server. On Windows XP, one would check by opening Printers, File/Server Properties, and on the Ports tab, check that IP_* ports (typically described as "​Standard TCP/IP Port") are in use.
 +  * Copy listprn.vbs (below) to your BBWin'​s ''​ext''​ folder.
 +  * Customize the IPpattern and MYDOMAIN values to match your network'​s configuration.
 +  * Optionally try running it at the command line (cscript listprn.vbs),​ confirm it displays output and writes a listprn file.
 +  * Edit your BBWin'​s ''​etc/​bbwin.cfg''​ file:
 +    * Add a line in the ''​externals''​ section:<​code xml>
 + <load value="​cscript.exe ..\ext\listprn.vbs"​ />
 +</​code>​
 +    * If the externals agent was disabled in the ''​bbwin''​ section (e.g. commented out or deleted), enable:<​code xml>
 +        <load name="​externals"​ value="​externals.dll"​ />
 +</​code>​
 +
 +=== Server side ===
 +Optionally add a composite script that makes a report of all printers. prntx.sh depends on prnt.sh; run something like the following:<​code bash>​prnt.sh > $BBWWW/​prnt.html;​ prntx.sh</​code>​
 +== prnt.sh ==
 +If the listing isn't long enough to warrant stand-alone versions of Combined/​Print-Jobs/​Ports,​ edit out the prnt-____.html references at the top.
 +<hidden onHidden="​Show Code ⇲" onVisible="​Hide Code ⇱">​
 +<code bash>
 +COLUMN=prnt
 +LOGFILE=/​tmp/​$COLUMN.log1
 +
 +#Filter out some totals
 +echo "<​html><​head><​title>​Printers</​title>​
 +<script src=\"​menu/​sorttable.js\"></​script></​head><​body>​
 +<​ul><​li><​a href=\"#​each\">​Each Office</​a>​ <​small>"​
 +for S in `$BB localhost "​hobbitdboard test=$COLUMN"​ | sed '​s/​|.*//'​`;​ do
 +        s=`echo $S | tr a-z A-Z`
 +        echo "| <a href=\"#​$s\">​$s</​a>"​
 +done
 +#NOTE: If the length of this isn't enough to warrant stand-alone pages
 +#NOTE: then remove the links to prnt-{combined|ports|jobs}.html and prnt-names.txt here
 +#NOTE: and don't bother using prntx.sh.
 +echo "</​small>​
 +  <​li><​a href=\"#​combined\">​Combined</​a>​ | <a href=prnt-combined.html>​(stand-alone)</​a>​
 +  <​li><​a href=\"#​jobs\">​Print Jobs</​a>​ | <a href=prnt-jobs.html>​(stand-alone)</​a>​
 +  <​li><​a href=\"#​ports\">​Ports</​a>​ | <a href=prnt-ports.html>​(stand-alone)</​a>​
 +  <li> | <a href=prnt-names.txt>​Names</​a>​
 +</​ul><​hr>​
 +
 +<a name=each><​h1>​Each Office</​h1></​a>"​
 +
 +#The output of this is essentially identical to what one would get if `cat`-ing the $BBLOGS if
 +# "​hobbitd_filestore --status"​ was launched.
 +for S in `$BB localhost "​hobbitdboard test=$COLUMN"​ | sed '​s/​|.*//'​`;​ do $BB localhost "​hobbitdlog $S.$COLUMN"​ | tail -n +2; echo "";​ done > $LOGFILE
 +
 +#Get each list of printers, including first line of status
 +sed '
 +        /Printers list/,/​\/​table/​!d
 +        /Printers list/​s/​\[\(.*\)\]/​[<​a name=\1>​\1<​\/​a>​]/​
 +        s/​^green/<​p>​green/​
 +        s/​^yellow/<​p>​yellow/​
 +        s/​^red/<​p>​red/​
 +        s/&​clear/<​img src="​gifs\/​clear.gif">/​g
 +        s/&​green/<​img src="​gifs\/​green.gif">/​g
 +        s/​^&​yellow/<​br><​img src="​gifs\/​yellow.gif">/​g
 +        s/&​yellow/<​img src="​gifs\/​yellow.gif">/​g
 +        s/​^&​red/<​br><​img src="​gifs\/​red.gif">/​g
 +        s/&​red/<​img src="​gifs\/​red.gif">/​g
 +        #s/table border=1/​table border=1 class=sortable/​
 +        s/<​th>​MAC/<​th class="​sorttable_alpha">​MAC/​
 +        s/<​th>​Port/<​th class="​sorttable_alpha">​Port/​
 +' $LOGFILE
 +
 +#Calculate some statistics
 +S=`awk ' /Printers list/ {
 +                shares += substr($9,​2);​
 +                MACs += $12;
 +                unassigned+=$14;​
 +                ports+=$17;
 +                jobs+=$20
 +        } END {
 +                print "​Totals:​ " shares " shares on " MACs " MACs, " unassigned " unassigned MACS; " ports " ports; " jobs " jobs"
 +        }' $LOGFILE`
 +
 +#Get each list of printers, but rather than listing each separately, combine into one table
 +#S=`sed '/​Printers list/,/​\/​table/​!d;/<​td>/​!d'​ $LOGFILE | wc -l`
 +echo "<​hr>​
 +
 +<a name=combined><​h1>​Combined</​h1></​a>​
 +`echo $S | sed 's/\; [0-9]* jobs//'​`
 +<table border=1 class=sortable><​tr><​th>​Share<​th class=\"​sorttable_alpha\">​Port<​th>​Res<​th>​Driver<​th>​Version<​th>​Processor<​th>​Capabilities<​th>​Status<​th>​Error<​th>​Ping<​th>​Comment"​
 +sed '
 +        /Printers list/,/​\/​table/​!d
 +        /<​td>/​!d
 +        s/&​clear/<​img src="​gifs\/​clear.gif">/​g
 +        s/&​green/<​img src="​gifs\/​green.gif">/​g
 +        s/&​yellow/<​img src="​gifs\/​yellow.gif">/​g
 +        s/&​red/<​img src="​gifs\/​red.gif">/​g
 +' $LOGFILE
 +echo "</​table>"​
 +
 +#Get each list of print jobs, but rather than listing each separately, combine into one table
 +echo "<​hr>​
 +
 +<a name=jobs><​h1>​Print Jobs</​h1></​a>​
 +`echo $S | sed '​s/​.*ports./​Total:/'​`
 +<table border=1 class=sortable><​tr><​th>​Status<​th>​Owner/<​br>​Notify<​th>​Total<​br>​Pages<​th>​Size<​th>​TimeSubmitted<​th>​Document<​th>​Description<​br>​(Printer,​Job)<​th>​Host<​br>​PrintQueue<​th>​Ping<​th>​Notes"​
 +sed '
 +        s/... DATA TRUNCATED .../... DATA TRUNCATED ...\
 +<​\/​table>/​
 +        /​TimeSubmitted/,/​\/​table/​!d
 +        #Put a non-breaking space in the date/time stamp
 +        s/​\([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]\) \([0-9][0-9][0-9][0-9][0-9][0-9]\)-[0-9][0-9][0-9]/​\1\&​nbsp\;​\2/​
 +        /<​td>/​!d
 +        s/&​green/<​img src="​gifs\/​green.gif">/​g
 +        s/&​yellow/<​img src="​gifs\/​yellow.gif">/​g
 +        s/&​red/<​img src="​gifs\/​red.gif">/​g
 +        s/&​purple/<​img src="​gifs\/​purple.gif">/​g
 +        s/&​clear/<​img src="​gifs\/​clear.gif">/​g
 +        s/ UNKNOWN/​Unknown/​
 +' $LOGFILE
 +echo "</​table>"​
 +
 +#More statistics (for ports list)
 +S=`sed '
 +        s/... DATA TRUNCATED .../... DATA TRUNCATED ...\
 +<​\/​table>/​
 +        /​Port.LPR\,/,/​\/​table/​!d
 +        /<​td>/​!d
 +        ' $LOGFILE | wc -l`
 +
 +#Get each list of ports
 +echo "<​hr>​
 +
 +
 +<a name=ports><​h1>​Ports</​h1></​a>​
 +Total: `echo $S` ports
 +<table border=1 class=sortable><​tr><​th>​Reservation<​th>​Reservation Description<​th>​Address<​th>​Ping<​th>​MAC<​th>​TCP,​ Port|LPR, Name<​th>​SNMP<​th>​Driver"​
 +sed '
 +        s/... DATA TRUNCATED .../... DATA TRUNCATED ...\
 +<​\/​table>/​
 +        /​Port.LPR\,/,/​\/​table/​!d
 +        /<​td>/​!d
 +        s/&​green/<​img src="​gifs\/​green.gif">/​g
 +        s/&​yellow/<​img src="​gifs\/​yellow.gif">/​g
 +        s/&​red/<​img src="​gifs\/​red.gif">/​
 +        s/public, 1/​public\/​1/​
 +' $LOGFILE
 +echo "</​table>"​
 +echo "<​p>​As of `date`</​p>"​
 +echo "</​body></​html>"​
 +</​code>​
 +</​hidden>​
 +== prntx.sh ==
 +Customize as you like; at this company, we use a convention of //​city//​-//​department//#​ with an optional -PS or -PCL, yours might be different and the prnt-names.txt counts by department might not be useful/​desirable.
 +<hidden onHidden="​Show Code ⇲" onVisible="​Hide Code ⇱">​
 +<code bash>
 +echo "<​html><​head><​title>​Printers combined</​title>​
 +<script src=\"​menu/​sorttable.js\"></​script></​head><​body>​
 +
 +<​h1>​Combined printers</​h1>​
 +`sed '/​name=combined/,/​name=jobs/​!d;/​a name=/​d;/​\<​hr\>/​d'​ < $BBHOME/​www/​prnt.html`
 +</​body></​html>"​ > $BBHOME/​www/​prnt-combined.html
 +
 +echo "<​html><​head><​title>​Print jobs</​title>​
 +<script src=\"​menu/​sorttable.js\"></​script></​head><​body>​
 +
 +<​h1>​Print jobs</​h1>​
 +`sed '/​name=jobs/,/​name=ports/​!d;/​a name=/​d;/​\<​hr\>/​d'​ < $BBHOME/​www/​prnt.html`
 +</​body></​html>"​ > $BBHOME/​www/​prnt-jobs.html
 +
 +echo "<​html><​head><​title>​Printer ports</​title>​
 +<script src=\"​menu/​sorttable.js\"></​script></​head><​body>​
 +
 +<​h1>​Printer ports</​h1>​
 +`sed '/​name=ports/,/​^\<​p\>​As of /!d;/a name=/​d;/​\<​hr\>/​d'​ < $BBHOME/​www/​prnt.html`
 +</​body></​html>"​ > $BBHOME/​www/​prnt-ports.html
 +
 +echo "​Counts of printer names (regardless of site)" > $BBHOME/​www/​prnt-names.txt
 +sed '​1,​6d;​$d;​s/​^.tr..td.//;​s/<​td>​.*//'​ $BBHOME/​www/​prnt-combined.html | tr A-Z a-z | sed '​$d;/​-ps$/​d;/​-pcl/​d;/​-universal/​d;​s/​^[^-]*-//;​s/​-.*//'​ | sort | uniq -c >> $BBHOME/​www/​prnt-names.txt
 +echo "
 +Counts of printer base names" >> $BBHOME/​www/​prnt-names.txt
 +sed '​1,​6d;​$d;​s/​^.tr..td.//;​s/<​td>​.*//'​ $BBHOME/​www/​prnt-combined.html | tr A-Z a-z | sed '​$d;/​-ps$/​d;/​-pcl/​d;/​-universal/​d;​s/​^[^-]*-//;​s/​-.*//;​s/​[0-9].*//'​ | sort | uniq -c >> $BBHOME/​www/​prnt-names.txt
 +echo "
 +
 +Notes:
 + * This excludes the following duplicate entries:
 +    *-PS names (e.g. 5 named COPIER1-PS would give 14 instead of 9 COPIER1)
 +    *-PCL* (usually PCL5 or PCL6, mostly at PDX)
 +    *-Universal (a duplicate using the HP Universal driver, all in ONT)
 +" >> $BBHOME/​www/​prnt-names.txt
 +</​code>​
 +</​hidden>​
 +
 +===== Source =====
 +==== listprn.vbs ====
 +
 +<hidden onHidden="​Show Code ⇲" onVisible="​Hide Code ⇱">​
 +<code vb>
 +Option Explicit
 +'List printers (for Xymon/​Hobbit/​Big Brother), by goldfndr
 +'Note: This script assumes that one uses Share names identical to Printer names. (WMIC Printer GET Name,​Sharename)
 +'(If printer names have spaces, they will defy pinging, and the print jobs ping column will be less useful.)
 +'​FIXME:​ Determine what voodoo allows LOCALSYSTEM to DHCP dump remotely (netsh.exe -c dhcp -r dhcpserv server dump)
 +
 +Dim TimerStart : TimerStart = Timer
 +Dim FSO : Set FSO = CreateObject("​Scripting.FileSystemObject"​)
 +Dim WSH : Set WSH = CreateObject("​WScript.Shell"​)
 +Dim WMI, wmiq, p : Set WMI = GetObject("​winmgmts:​\\.\root\cimv2"​)
 +'Dim me : me = WSH.Environment("​Process"​)("​COMPUTERNAME"​)
 +const ssfSYSTEM = 37
 +Dim SHA : Set SHA = CreateObject("​Shell.Application"​)
 +Const dhcplog = "​dhcp.txt"​ '​Not normally deleted, left for debugging purposes
 +
 +const dbg=true
 +
 +Dim subcolor, color : color = "​green"​
 +Const td = "<​td>"​ '​change to vbTab as needed, for inter-field separator
 +Const th = "<​th>"​ '​change to vbTab as needed, for inter-field separator
 +Const D = "<​br>"​ '​Change to "; " as needed, for intra-field delimiter/​separator (notes)
 +Const IPpattern = "​\.205\.|\.1\d\.2\d\d$"​ '​A regexp valid for your printers'​ IP addrs, for DHCP
 +Const MYDOMAIN = "​.example.com"​
 +Const DHCPDescriptions = True
 +
 +
 +'​***********************************************************
 +Phase "​Determine DHCP server"​
 +'Note: This check assumes that the DHCP server is in the DNS search path.
 +'Note: Duplication will result if there are multiple print servers within one DHCP subnet.
 +'Note: The user that this script runs under needs permissions to do remote netsh checks.
 +'Note: DNS Search Order should be either null (no DNS) or an array; null might be from a virtual adapter.
 +Dim dhcpserver : dhcpserver = ""​
 +Set wmiq = WMIQuery("​IPAddress,​ DHCPServer, DNSServerSearchOrder"​ _
 + & "; Win32_NetworkAdapterConfiguration Where IPEnabled = True")
 +'​***********************************************************
 +Dim t, f : For each p in wmiq
 + For each t in p.IPAddress
 + wscript.echo "IP Address="​ & t
 + 'if t = p.DHCPServer then dhcpserver = ""​ : exit for
 + '​wscript.echo "​VarType="​ & VarType(p.DNSServerSearchOrder)
 + If IsArray(p.DNSServerSearchOrder) then
 + For each f in p.DNSServerSearchOrder
 + wscript.echo "DNS Server="​ & f
 + if t = f then '​If this print server is a DNS server
 + dhcpserver = ""​ '​then query is local, not remote
 + exit for
 + end if
 + if ""​ = dhcpserver then dhcpserver = f
 + Next
 + End If
 + Next
 +Next
 +wscript.echo "​Determined DHCP server="""​ & dhcpserver & """"​
 +'​wscript.quit
 +
 +'​***********************************************************
 +Phase "Dump DHCP " & dhcpserver
 +t = "​netsh.exe -c dhcp server dump"
 +if ""​ <> dhcpserver then t = Replace(t, "-c dhcp", "-c dhcp -r " & dhcpserver) '​FIXME:​ Permissions?​
 +WSH.Run "​cmd.exe /c " & t & " 2>" & dhcplog & ".err | find ""​reservedip""​ | sort > " & dhcplog, 0, 1
 +Phase "Parse DHCP, gathering Ports (IP Addresses), MAC Addresses, and Reservations (Reserved Names)"​
 +'​***********************************************************
 +Dim ports : Set ports = CreateObject("​Scripting.Dictionary"​)
 +Dim macs : Set macs = CreateObject("​Scripting.Dictionary"​)
 +Dim reservations : Set reservations = CreateObject("​Scripting.Dictionary"​)
 +Dim resdescriptions : Set resdescriptions = CreateObject("​Scripting.Dictionary"​)
 +Dim tcpips : Set tcpips = CreateObject("​Scripting.Dictionary"​)
 +Dim pingables : Set pingables = CreateObject("​Scripting.Dictionary"​)
 +
 +if FSO.GetFile(dhcplog).Size Then
 + Set f = FSO.OpenTextFile(dhcplog,​ 1)
 + Do
 + Dim s : s = f.ReadLine
 + t = Split(s)
 + ports.Item(t(7)) = t(7)
 + macs.Item(t(7)) = t(8)
 + dim reservation : reservation = Mid(t(9), 2, len(t(9)) - 2)
 + reservations.Item(t(7)) = reservation
 + '​strip off MYDOMAIN if present, case-insensitive search starting at 4th character
 + if Instr(4, reservation,​ MYDOMAIN, 1) then
 + reservations.Item(t(7)) = left(reservation,​ len(reservation) - len(MYDOMAIN))
 + end if
 + resdescriptions.Item(t(7)) = Split(s, """​ """​)(1)
 + Loop until f.AtEndOfStream
 + f.Close
 +End If
 +
 +Dim ErrorStateTxt : ErrorStateTxt = Array("",​ "​Other",​ "No Error",​ "Low Paper",​ "No Paper" _
 + , "Low Toner",​ "No Toner",​ "Door Open", "​Jammed",​ "​Offline",​ "​Service Requested",​ "​Output Bin Full")
 +Dim ErrorStateVal : ErrorStateVal = Array(0, 1, 0, 1, 2, 1, 2, 2, 2, 2, 2, 2) '​0=green,​ 1=yellow, 2=red
 +Dim PrinterStatusTxt : PrinterStatusTxt = Array("",​ "​Other",​ "​Unknown",​ "​Idle",​ "​Printing"​ _
 + , "​Warming Up", "​Stopped printing",​ "​Offline"​)
 +Dim colors : colors = Array("​green",​ "​yellow",​ "​red"​)
 +
 +
 +Dim exps : Set exps = CreateObject("​Scripting.Dictionary"​) '​Expansion for port
 +Dim drivers : Set drivers = CreateObject("​Scripting.Dictionary"​)
 +Dim processors : Set processors = CreateObject("​Scripting.Dictionary"​)
 +Dim datatypes : Set datatypes = CreateObject("​Scripting.Dictionary"​)
 +Dim totals(3) : totals(0) = 0 : totals(1) = 0 : totals(2) = 0 : totals(3) = 0
 +Dim versions : Set versions = CreateObject("​Scripting.Dictionary"​)
 +
 +Dim LINE : LINE = ""​
 +
 +'​***********************************************************
 +Phase "​Evaluate the TCP/IP Ports"
 +WMI.Security_.Privileges.AddAsString "​SeLoadDriverPrivilege"​ '​Required for SNMP/etc.
 +Set wmiq = WMIQuery("​Win32_TCPIPPrinterPort"​)
 +'​***********************************************************
 +For Each p in wmiq
 + t = "<​tr>"​ & td & reservations(p.HostAddress) & td & resdescriptions(p.HostAddress) _
 + & td & p.HostAddress & td & "&"​ & pingable(p.HostAddress) _
 + & td & MACs(p.HostAddress) & td
 + Select Case p.Protocol
 + Case 1: t = t & "Raw, "
 + if p.PortNumber <> 9100 then _
 + t = t & "<​b>"​ & p.PortNumber & "</​b>" ​ else t = t & "​9100"​
 + Case 2: t = t & "LPR, " & p.Queue
 + if p.ByteCount then t = t & " <​i>​(Counted)<​i>"​
 + Case Else: t = t & "<​i>​Unknown</​i>​ (" & p.Protocol & "​)"​
 + End Select
 + t = t & td
 + if p.SNMPEnabled then t = t & p.SNMPCommunity & ", " & p.SNMPDevIndex
 + wscript.echo t
 + tcpips(p.HostAddress) = t
 +next
 +
 +
 +Phase "​Determine ""​Product Version""​ and ""​File Version""​ column indexes"​
 +dim productversion,​ fileversion
 + Set f = SHA.NameSpace(ssfSYSTEM)
 + For t = -1 to 50
 + if "​product version"​ = LCase(f.GetDetailsOf(Nothing,​ t)) then productversion = t
 + if "file version"​ = LCase(f.GetDetailsOf(Nothing,​ t)) then fileversion = t
 + Next
 +
 +Function DriverVersion(p)
 + 'The logic in here is from analysis of HP and Canon drivers. YMMV.
 + wscript.echo "​Checking " & p.DriverPath
 + Dim f, v : f = FSO.GetFileName(p.DriverPath)
 + if UCase(f) <> "​UNIDRV.DLL"​ and UCase(f) <> "​PSCRIPT5.DLL"​ then
 + v = ProductVer(p.DriverPath)
 + '​FIXME:​ Add version checking for Oce drivers that say "see ocewpdid.dll"​ (really!)
 + else
 + f = FSO.GetFileName(p.ConfigFile)
 + if UCase(f) <> "​UNIDRVUI.DLL"​ and UCase(f) <> "​PS5UI.DLL"​ then
 + v = ProductVer(p.ConfigFile)
 + if len(v) >= 3 then v = v & " (" & Replace(LCase(f),​ "​.dll",​ ""​) & "​)"​
 + end if
 + '​Sometimes the ProductVer is unavailable,​ e.g. hpzpi4wm.dll@2007-02-13
 + if UCase(f) = "​UNIDRVUI.DLL"​ or UCase(f) = "​PS5UI.DLL"​ or Len(v) < 3 then
 + wscript.echo "​Checking dependent files" & vbTab & p.Name & " " & p.SupportedPlatform
 + v = DependentFilesVersion(p.DependentFiles)
 + if len(v) < 3 then
 + wscript.echo "Still no version, getting date of " & p.DataFile
 + f = FSO.GetFile(p.DataFile).DateLastModified
 + v = "​["​ & LCase(FSO.GetExtensionName(p.DataFile)) & "​~"​ _
 + & Year(f) & "​-"​ & Right(0 & Month(f), 2) & "​-"​ & Right(0 & Day(f), 2) & "​]"​
 + end if
 + end if
 + end if
 +
 + f = Left(p.Name,​ Len(p.Name) - 2 - Len(p.SupportedPlatform) - Len(p.Version))
 +
 + '​wscript.echo vbTab & v & vbTab & """"​ & f & """"​
 + DriverVersion = Array(v, f)
 + '​wscript.echo f & vbTab & versions(f)
 +End Function
 +
 +
 +Private Function DependentFilesVersion(df) '​Pass the array of dependent files; only called by DriverVersion
 + '​Prefer x*ui*.dll but settle for *ui*.dll, or *.dll; but at least capture the first filename
 + Dim a, fn : a = Filter(df, "​.dll",​ True, vbTextCompare)
 + if UBound(a) >= 0 then
 + fn = a(0)
 + a = Filter(a, "​ui",​ True, vbTextCompare)
 + if UBound(a) >= 0 then
 + fn = a(0)
 + a = Filter(a, "​\x",​ True, vbTextCompare)
 + if UBound(a) >= 0 then
 + fn = a(0)
 + end if
 + end if
 + else
 + DependentFilesVersion = ""​
 + Exit Function
 + end if
 +
 + wscript.echo "​Checking dependent file " & fn
 + Dim v : v =  ProductVer(fn)
 + if Instr(v, ","​) > 0 then v = FileVer(fn) '​if commas in Product Version then use File Version
 + if len(v) >= 3 then v = v & " [" & LCase(FSO.GetBaseName(fn)) & "​]"​ '​credit/​blame
 + DependentFilesVersion = v
 +End Function
 +
 +
 +Function ProductVer(filename)
 + Dim nmsp : Set nmsp = SHA.Namespace(FSO.GetParentFolderName(FSO.GetAbsolutePathName(filename)))
 + Productver = nmsp.GetDetailsOf(nmsp.ParseName(FSO.GetFileName(filename)),​ productversion)
 +End Function
 +
 +Function FileVer(filename)
 + Dim nmsp : Set nmsp = SHA.Namespace(FSO.GetParentFolderName(FSO.GetAbsolutePathName(filename)))
 + Filever = nmsp.GetDetailsOf(nmsp.ParseName(FSO.GetFileName(filename)),​ fileversion)
 +End Function
 +
 +'​***********************************************************
 +Set wmiq = WMIQuery("​Name,​ ConfigFile, DataFile, DriverPath, DependentFiles,​ SupportedPlatform,​ Version"​ _
 + & "; Win32_PrinterDriver"​)
 +Phase "​Enumerate Printer drivers and determine their product versions"​
 +'​***********************************************************
 +For Each p in wmiq
 + dim a : a = DriverVersion(p) : f = a(1)
 + if versions.exists(f) then
 + versions(f) = versions(f) & "<​br>"​
 + else
 + versions(f) = ""​
 + end if
 + versions(f) = versions(f) & Mid(p.SupportedPlatform,​ InStrRev(p.SupportedPlatform,​ " ") + 1) & "​="​ & a(0)
 +Next
 +
 +
 +Function DontShout(drivername) '​These companies like to SHOUT their names, but their websites use lcase. Rude!
 + DontShout = drivername
 + Dim s : for each s in Split("​Konica Minolta Ricoh Sharp"​)
 + DontShout = Replace(DontShout,​ UCase(s), s)
 + next
 +End Function
 +
 +Function ShareStatus(ps,​ s)
 + ShareStatus = PrinterStatusTxt(ps) & Replace(",​ " & s, ", Unknown",​ ""​) '​Don'​t list Status if Unknown
 + ShareStatus = Replace(ShareStatus,​ "​Other,​ ", "​=>"​) '​Don'​t list PrinterStatus if Other
 + if "<"​ = Left(td, 1) then ShareStatus = Replace(ShareStatus,​ "​=>",​ "​=&​gt;"​) '​Use HTML if needed
 +End Function
 +
 +
 +'''''''''''''''''''''''''''''''''''''''''''''''''''''''''​
 +'The WMI Query line is where servers sometimes get stuck.
 +'It is similar to the command: ​   wmic printer list brief
 +'One could check WMI goodness with:           wmic server
 +'Note: restarting Print Spooler service doesn'​t unstuck.
 +'''''''''''''''''''''''''''''''''''''''''''''''''''''''''​
 +'​***********************************************************
 +Set wmiq = WMIQuery("​Win32_Printer"​)
 +Phase "​Enumerate expansions for each known printer, and add its driver/​processor/​datatype by sharename"​
 +'​***********************************************************
 +For each p in wmiq
 + Phase "Check printer port " & p.PortName
 + totals(0) = totals(0) + 1
 + '​WScript.Echo p.DeviceID & vbTab & " MarkingTechnology:​ " & p.MarkingTechnology
 + 'if len(p.ShareName) then
 + dim c : c = ""​
 + if not IsNull(p.CapabilityDescriptions) then
 + '​Collate:​ True if a printer has collating bins.
 + '​Color:​ True if a printer can print in color. (but see LaserJet note below)
 + '​Duplex:​ Indicates the type of duplex support a printer has.
 + '​(HP claims Color on 4M, 4 Plus, 5Si MX, 8000, 8100, 8150, 9050 as of Server 2003.)
 + '​(Oce clams Color on TDS400, TDS450, TDS600, TDS700 as of Server 2003.)
 + '​(Try it yourself: WMIC Printer GET CapabilityDescriptions,​DriverName)
 + '​FIXME:​ add honesty override for manufacturers setting this on non-color devices?
 + Phase "Check port " & p.PortName & " capabilities"​
 + for each t in p.CapabilityDescriptions
 + '​Never seen one that lacked "​Copies",​ so filter that out
 + if t <> "​Copies"​ then  c = c & t & " "
 + next
 + c = Replace(Trim(c),​ " ", "​|"​)
 + end if
 + dim port : port = p.PortName
 + dim mac : mac = ""​
 + if "​IP_"​ = Left(p.PortName,​ 3) then
 + port = Mid(p.PortName,​ 4) '​Strip off the "​IP_"​ if present
 + if not ports.Exists(port) then ports(port) = port
 + if macs.Exists(port) then
 + mac = macs(port)
 + '​ macs.Remove(port) '​Possibly reused?
 + '​ reservations.Remove(port)
 + end if
 + end if
 + dim ping : ping = pingable(port)
 + Phase "Check printer'​s error state, complete expansion"​
 + subcolor = colors(ErrorStateVal(p.DetectedErrorState))
 + dim res : res = p.HorizontalResolution
 + if res <> p.VerticalResolution then res = res & "​x"​ & p.VerticalResolution
 + Dim errtxt : errtxt = ErrorStateTxt(p.DetectedErrorState)
 + exps(port) = "<​tr>"​ _
 + & td & p.ShareName _
 + & td & port _
 + & td & res _
 + & td & DontShout(p.DriverName) & td & versions(p.DriverName) _
 + & td & p.PrintProcessor _
 + & td & c _
 + & td & ShareStatus(p.PrinterStatus,​ p.Status) _
 + & td & "&"​ & subcolor & " " & errtxt _
 + & td & "&"​ & ping _
 + & td & p.Comment _
 + & Replace(td, "<",​ "</"​) & vbCRLF & exps(port)
 +
 +if dbg then wscript.echo port & vbTab & exps(port)
 + if "​green"​ = ping then
 + if 9 = p.DetectedErrorState then '​Offline
 + if p.Status = "​Degraded"​ and 1 = p.PrinterStatus then 'Other
 + LINE = LINE & "&​red " & p.ShareName & " has Other/​Degraded/​Offline"​ _
 + & ", hallmarks of Spooler service restart required."​ & vbCRLF
 + color = "​red"​
 + else
 + LINE = LINE & "&​red " & p.ShareName & " can be pinged at " & port _
 + & " but claims " & errtxt & "; restart Spooler service?"​ & vbCRLF
 + if color <> "​red"​ then  color = "​yellow"​
 + end if
 + end if
 + end if
 + Phase "Add share " & p.ShareName & " to drivers list"
 + drivers(p.ShareName) = p.DriverName
 + Phase "Add share " & p.ShareName & " to processors list"
 + processors(p.ShareName) = p.PrintProcessor
 + Phase "Add share " & p.ShareName & " to datatypes list"
 + datatypes(p.ShareName) = p.PrintJobDataType
 + Phase "​Finished with Adding share " & p.ShareName & " to lists"
 + 'end if
 +Next
 +
 +
 +'​Oddly,​ if Win32_Printer isn't requeried, "​SWbemObjectSet:​ Unspecified error"
 +Set wmiq = WMIQuery("​Win32_Printer"​)
 +Phase "​Remove MACs with corresponding printers from DHCP reservation list"
 +'​WARNING:​ Script can sometimes hang right at this next line; next Phase line twice for debugging
 +For Each p in wmiq
 + Phase "Check printer MAC"
 + Phase "Check printer MAC " & p.PortName
 + 'if len(p.ShareName) then
 + port = Mid(p.PortName,​ 4) '​Strip off the "​IP_"​
 + if macs.Exists(port) then
 + Phase "Check printer MAC for " & port
 + totals(2) = totals(2) + 1
 + macs.Remove(port)
 + reservations.Remove(port)
 + end if
 + 'end if
 +Next
 +
 +
 +'​***********************************************************
 +Phase "List each printer share with its expansion"​
 +'​***********************************************************
 +if len(LINE) > 1 then LINE = LINE & vbCRLF
 +LINE = LINE & "<​table id=shares border=1 cellpadding=3 class=sortable>"​ & vbCRLF _
 + & "<​tr>"​ &​th&​ "​Share"​ &​th&​ "​Port"​ &​th&​ "​Res"​ _
 + &​th&​ "​Driver"​ &​th&​ "​Version"​ &​th&​ "​Processor"​ &​th&​ "​Capabilities"​
 +'if Instr(th, "<"​) > 0 then LINE = LINE & "<​th colspan=2>"​ else LINE = LINE & th
 +LINE = LINE & th
 +LINE = LINE & "​Status"​ & th & "​Error"​ & th & "​Ping"​ & th & "​Comment"​ & vbCRLF
 +for each p in ports.Keys
 + '​wscript.echo vbTab & p & vbTab & exps(p)
 +
 + if len(exps(p)) then LINE = LINE & exps(p)
 +next
 +LINE = LINE & vbCRLF & "</​table>"​ & vbCRLF
 +
 +
 +'Some credit to http://​blogs.msdn.com/​b/​ericlippert/​archive/​2004/​12/​03/​274360.aspx
 +Function CULng(ByVal x)
 +  If x < 0 Then CULng = x + 2^32 Else CULng = x
 +End Function
 +
 +
 +
 +'​***********************************************************
 +Dim jobs : jobs = ""​
 +Set wmiq = WMIQuery("​Win32_PrintJob"​)
 +Phase "​Enumerate Print jobs"
 +'​***********************************************************
 +For Each p in wmiq
 + totals(3) = totals(3) + 1
 + Dim eltime : eltime = ""​
 + if not IsNull(p.ElapsedTime) then
 + eltime = CLng(Left(p.ElapsedTime,​14))
 + if 0 = eltime then eltime=""​
 + end if
 + Select case p.Status
 + Case "​Error":​ subcolor = "​yellow"​ : if color <> "​red"​ then color = "​yellow"​
 + LINE = "&​yellow """​ & p.Document & """​ at " & p.Description & " has error."​ _
 + & vbCRLF & LINE
 + Case "​UNKNOWN":​ subcolor = "​clear"​
 + Case "​Degraded":​subcolor = "​yellow"​ '​Usually paused jobs so don't propagate
 + Case "​OK":​ subcolor = "​green"​
 + Case Else:​ subcolor = "​clear"​ : if color = "​green"​ then color = "​clear"​
 + '​Pred Fail,​Starting,​Stopping,​Service,​Stressed,​NonRecover,​No Contact,​Lost Comm
 + End Select
 + jobs = jobs & "<​tr>"​ & td & "&"​ & subcolor & " " & p.Status
 + if p.Owner <> p.Notify then
 + jobs = jobs &​td&​ p.Owner &"/"&​ p.Notify
 + else
 + jobs = jobs &​td&​ p.Owner
 + end if
 + 'Some print jobs are over 2 GB, so make sure the size is an Unsigned Long.
 + jobs = jobs &​td&​ p.TotalPages &​td&​ CULng(p.Size) &​td&​ dt(p.TimeSubmitted) _
 + &​td&​ p.Document &​td&​ p.Description &​td&​ p.HostPrintQueue
 + Dim k, pinged : pinged = "&​clear"​ : for each k in exps.keys
 + If Instr(exps(k),​ td & Split(p.Description,​ ","​)(0) & td) > 0 then
 + '​wscript.echo p.Document & vbTab & k & vbTab
 + pinged = "&"​ & pingable(k)
 + End If
 + next
 + '​Expansions won't be found if the printer name differs from the share name.
 + if pinged = "&​clear"​ then Phase "​Unable to find expansion for " & p.Description
 + jobs = jobs & td & pinged & " &" & pingable(Replace(p.HostPrintQueue,​ "​\\",​ ""​))
 +
 + Dim notes : notes = ""​
 + if p.PagesPrinted > 0 then notes = notes & D & "​PagesPrinted="​ & p.PagesPrinted
 + if len(p.StartTime) > 0 then notes = notes & D & "​StartTime="​ & p.StartTime
 + if len(eltime) > 0 then notes = notes & D & "​ElapsedTime="​ & eltime
 + if len(p.UntilTime) <> 0 then notes = notes & D & "​UntilTime="​ & p.UntilTime
 + if len(p.Parameters) > 0 then notes = notes & D & "​Parameters="​ & p.Parameters
 + if p.Priority <> 1 then notes = notes & D & "​Priority="​ & p.Priority
 + if len(p.JobStatus) > 0 then notes = notes & D & "​JobStatus="""​ & p.JobStatus & """"​
 + if p.StatusMask <> 0 then notes = notes & D & "​StatusMask="​ & p.StatusMask
 + if len(p.InstallDate) <> 0 then notes = notes & D & "​InstallDate="​ & p.InstallDate
 +
 + 'We have the expected processor/​driver/​datatype by share, so note it if it doesn'​t match
 + dim sn : sn = left(p.Description,​ Instr(p.Description,​ ","​) - 1) '​Share Name
 + if p.PrintProcessor <> processors(sn) then notes = notes & D & "​PrintProcessor="""​ & p.PrintProcessor & """"​
 + if p.DriverName <> drivers(sn) then notes = notes & D & "​Driver="""​ & p.DriverName & """"​
 + if p.DataType <> datatypes(sn) then notes = notes & D & "​DataType="""​ & p.DataType & """"​
 +
 + if len(notes) > 0 then notes = Mid(notes, 1+len(D)) '​Remove initial delimiter
 + jobs = jobs &​td&​ notes & vbCRLF
 +Next
 +
 +'​***********************************************************
 +Phase "If there are any print jobs then add them to the report"​
 +'​***********************************************************
 +if len(jobs) > 0 then LINE = LINE & "<​br></​pre><​table id=jobs border=1 cellpadding=3 class=sortable>"​ & vbCRLF _
 + & "<​tr>"​ & th & "​Status"​ & th & "​Owner/<​br>​Notify"​ _
 + & th & "​Total<​br>​Pages"​ & th & "​Size"​ & th & "​TimeSubmitted"​ _
 + & th & "​Document"​ & th & "​Description<​br>​(Printer,​ Job)" & th & "​Host<​br>​PrintQueue"​ _
 + & th & "​Ping"​ & th & "​Notes"​ & vbCRLF & jobs & "</​table><​pre>"​
 +
 +
 +
 +'​***********************************************************
 +Phase "​Expand MACs without corresponding printers (e.g. digital senders) as unused"​
 +'​***********************************************************
 +Dim r : set r = New RegExp
 +r.Pattern = IPpattern
 +for each p in macs.Keys
 + 'set c = r.Execute(p) : wscript.echo vbTab & c.Count & vbTab & p
 + if r.Execute(p).Count > 0 then
 + '​totals(1) = totals(1) + 1
 + '​exps(p) = "<​tr>"​ & td & reservations(p) & td & resdescriptions(p) & td & macs(p) & td & p & td _
 + '​ &​ td & "<​div align=right>​(unused at " & WSH.Environment("​Process"​)("​COMPUTERNAME"​) & "​)</​div>"​ _
 + '​ &​ td & td & td & td & td & td & td & "&"​ & pingable(p) & " " & td & vbCRLF & exps(p)
 + if 0 = len(tcpips(p)) then
 + tcpips(p) = "<​tr>"​ & td & reservations(p) & td & resdescriptions(p) _
 + & td & p & td & "&"​ & pingable(p) & td & macs(p) & td _
 + & divright("​unused"​) & td
 + end if
 + else
 + if ports.Exists(p) then  ports.Remove(p)
 + end if
 + if dbg then wscript.echo vbTab & p & vbTab & """"​ & exps(p) & """"​
 +next
 +
 +
 +'​***********************************************************
 +Phase "​Output the ports/MACs list, including unassigned DHCP reservations"​
 +'​***********************************************************
 +LINE = LINE & vbCRLF & vbCRLF & "<​table id=ports border=1 cellpadding=3 class=sortable>"​ & vbCRLF _
 + & "<​tr>"​ & th & "​Reservation"​ & th & "​Reservation Description"​ & th & "​Address"​ & th & "​Ping"​ & th & "​MAC"​ _
 + & th & "TCP, Port|LPR, Name" & th & "​SNMP"​ & th & "​Driver"​
 +for each p in tcpips.Keys
 + wscript.echo vbTab & p & vbTab & tcpips(p)
 + if len(tcpips(p)) then
 + LINE = LINE & vbCRLF & tcpips(p)
 + if len(exps(p)) then
 + LINE = LINE & td & Split(exps(p),​ td)(4)
 + else
 + LINE = LINE & td & divright("​unused"​)
 + end if
 + end if
 +next
 +LINE = LINE & vbCRLF & "</​table>"​ & vbCRLF
 +
 +
 +Function divright(s)
 + divright = "<​div align=right><​i>​("​ & s & " on " & WSH.Environment("​Process"​)("​COMPUTERNAME"​) & "​)</​i></​div>"​
 +End Function
 +
 +
 +
 +'​***********************************************************
 +Phase "​Actually output the report (" & len(LINE) & "​)"​
 +'​***********************************************************
 +WriteStatus "​prnt",​ color, "​Printers list (" _
 + & totals(0) & " shares on " & totals(2) & " MACs, " _
 + & totals(1) & " unassigned MACs; " & ports.Count & " ports total; " & totals(3) & " jobs)",​ LINE
 +'if dbg then wscript.echo LINE
 +
 +Function dt(t) '​Colorize by age
 + dt = ""​
 + if not IsNull(t) then
 + Dim dtt : dtt = CLng(Left(t,​ 8)) '​Date of submission
 + Dim d : d = Now : d = 10000 * Year(d) + 100 * Month(d) + Day(d)
 + Select Case d - dtt
 + Case 0 : dt = "&​green"​ '​today
 + Case 1 : dt = "&​yellow"​ '​yesterday
 + Case 2,3,4,5,6 : dt = "&​red" ​  ​ '​past week
 + Case Else : dt = "&​purple"​ '​long ago
 + End Select
 + dt = dt & dtt & " " & Mid(t, 9, 6) & Right(t,4)
 + end if
 +End Function
 +
 +Function pingable(ip)
 + if not IsNumeric(Left(ip,​ 1)) then
 + pingable = "​clear"​ '​Disable test if not an IP address
 + Exit Function
 + end if
 + if pingables.Exists(ip) then
 + Phase "​Already tried to ping " & ip & ", " & pingables(ip)
 + pingable = pingables(ip)
 + exit function
 + end if
 + '​pingable = "​green"​ : exit function
 + Phase "Try to ping " & ip
 + 'Dim WMI_ : Set WMI_ = GetObject("​winmgmts:​{impersonationLevel=impersonate}"​)
 + 'The following should be a WMI Get rather than a query, as only one response is usable
 + Dim pings : Set pings = WMI.ExecQuery("​select * from Win32_PingStatus where address = '"​ & ip & "'"​)
 + Dim pi : for each pi in pings
 + pingables(ip) = "​green"​
 + if 0 <> pi.StatusCode then pingables(ip) = "​red"​
 + pingable = pingables(ip)
 + exit function
 + next
 +End Function
 +
 +'######################################################################​
 +'Write out the status; depends on FSO and WSH; will use TimerStart if available for duration
 +Sub WriteStatus(column,​ color, quickie, line)
 +'Why so long? Assume that client might collect file while writing, so make temp file and rename it.
 +'######################################################################​
 + Dim FSO : Set FSO = CreateObject("​Scripting.FileSystemObject"​)
 + Dim WSH : Set WSH = CreateObject("​WScript.Shell"​)
 + Dim HKLMSoft : HKLMSoft="​HKLM\SOFTWARE"​
 + if WSH.Environment("​SYSTEM"​)("​PROCESSOR_ARCHITECTURE"​) <> "​x86"​ then HKLMSoft = HKLMSoft & "​\Wow6432Node"​
 + Const LEAKYMAX = 8500, BBWinreg = "​\BBWin\tmpPath"​
 + Const Questreg = "​\Quest Software\BigBrother\bbnt\ExternalPath\"​
 + On error resume next
 + '​Assume that, if BBWin is installed, they'​re using it; if not then they'​re using Quest BB client.
 + Dim colfn, src, leaky : colfn = ""​ : colfn = WSH.RegRead(HKLMSoft & BBWinreg) : src = "​bbwin"​
 + If ""​ = colfn then : colfn = WSH.RegRead(HKLMSoft & Questreg) : src = ""​ : end if
 + If ""​ = colfn then Exit Sub '​Abort if nothing in the registry for location.
 + colfn = colfn & "​\"​ & column '​Add the column name to make it a real colfile.
 + Dim tmpfn : tmpfn = colfn & "​.$$$"​ '​Temporary file; use temporary name for safety!
 + If FSO.FileExists(tmpfn) Then WScript.Quit 183 '​Bail out if already there (i.e. previous stuck)
 + Phase "​Create temporary file with status"​
 + With FSO.CreateTextFile(tmpfn,​ True)
 + if err then WScript.Quit err '​Bail out if unable to create file.
 + if Len(quickie) + Len(line) > LEAKYMAX and src = "​bbwin"​ then
 + leaky = True
 + .Write "​status " & WSH.RegRead(HKLMSoft & "​\BBWin\hostname"​) & "​."​ & column & " "
 + end if
 + .WriteLine color & " " & WeekdayName(Weekday(Now),​True) & " " _
 + & Mid(Now,​1,​InStr(Now,"​ ")-1) & " " & TimeValue(Now) _
 + & " [" & WSH.Environment("​Process"​)("​COMPUTERNAME"​) & "] " & quickie
 + .WriteLine line
 + .Write "<​center><​small>"​ & WScript.ScriptName & " (modified " & CDate(CLng( _
 + FSO.GetFile(WScript.ScriptFullName).DateLastModified))
 + if not IsNull(TimerStart) then .Write "; run time " & Timer - TimerStart & "​s"​ '​midnight bug
 + .Close
 + End With
 + With FSO.OpenTextFile(tmpfn,​ 8)
 + .Write "; len ~" & FSO.GetFile(tmpfn).Size + 29 & "​)</​small></​center>"​ '​assume 1-9kB
 + .Close
 + End With
 + '​***********************************************************
 + '​Rename file so client will see it (taking care to remove any unprocessed file first)
 + 'Or send directly with bbwincmd.exe if over LEAKYMAX and using bbwin
 + '​***********************************************************
 + Phase "​Rename temporary file for automatic send -OR- send directly"​
 + on error goto 0
 +'​wscript.echo len(line)
 + if leaky then '​if FSO.GetFile(tmpfn).Size > LEAKYMAX and src = "​bbwin"​ then
 + Dim XML : Set XML = CreateObject("​Microsoft.XMLDOM"​) '​Requires MSIE 5 or later
 + If XML.Load(WSH.RegRead(HKLMSoft & "​\BBWin\etcPath"​) & "​\bbwin.cfg"​) then
 + Phase "​Status2XML,​ len=" & FSO.GetFile(tmpfn).Size
 + Dim x, s : for each x in XML.SelectNodes("//​configuration/​bbwin/​setting[@name='​bbdisplay'​]"​)
 + s = x.Attributes.GetNamedItem("​value"​).Value
 + Phase "​upload to " & s & " " & tmpfn
 + WSH.Run "​..\bin\bbwincmd.exe " & s & " uploadmessage """​ & tmpfn & """",​ 1, True
 + next
 + Else
 + WScript.Echo "Error parsing " & WSH.RegRead(HKLMSoft & "​\BBWin\etcPath"​) & "​\bbwin.cfg"​
 + End If
 + FSO.DeleteFile tmpfn
 + else
 + if FSO.FileExists(colfn) then FSO.DeleteFile(colfn)
 + FSO.MoveFile tmpfn, colfn
 + end if
 +End Sub
 +
 +'If the script gets stuck, we can use Process Explorer to examine its environment
 +Sub Phase(n)
 + if dbg then wscript.echo vbTab & n
 + CreateObject("​WScript.Shell"​).Environment("​Process"​)("​P-"​ & WScript.ScriptName) = n & " (" & Now & "​)"​
 +End Sub
 +
 +Function WMIQuery(ByVal s)
 + Phase "WMI Query: " & s
 + if Instr(s, ";"​) > 1 then s = Replace("​Select " & s, ";",​ " from ") else s = "​Select * from " & s
 + Set WMIQuery = WMI.ExecQuery(s) '​This did have a ,,48 but I'm not sure it's worthwhile
 + Phase "WMI Queried: " & s
 +End Function
 +</​code>​
 +</​hidden>​
 +
 +===== Known  Bugs and Issues =====
 +  * Figure out what credentials are required to remotely retrieve a DHCP server'​s reservation list.
 +  * Determine why a WMI query for Win32_Printer sometimes gets stuck.
 +
 +===== To Do =====
 +  * Confirm that changing td/th to tabs actually does produce readable (non-HTML) output.
 +  * Figure out why the server sometimes claims a printer has gone offline when it hasn'​t,​ and won't come back online unless the Spooler service is restarted. This script will consider a printer in this situation (offline but pingable) as having a red status, with a need to restart the Print Spooler service likely.
 +  * Perhaps find more methods that printer driver creators use to embed the version numbers in their drivers.
 + 
 +===== Credits =====
 +(see source code)
 +
 +===== Changelog =====
 +
 +  * **2012-05-03**
 +    * Initial release
  
  • monitors/listprn.txt
  • Last modified: 2012/05/04 08:28
  • (external edit)