Print Spooler Self Healing 2.0

by Jeremy Saunders on October 21, 2008

I was driven to write this script for a client that was using Novell’s iPrint. iPrint was causing some threads to hang, ending the spoolsv.exe process, and therefore stopping the Print Spooler service. Those of you that are using such poor printing solutions would no doubt have clocked up a considerable number of headaches. The good news is that this script will provide you with some much deserved pain relief. Let’s call it the “Asprin for iPrint”. I’ll have to register a trade mark for that one 😉

So why is this script so good? Glad you asked!

It monitors the spoolsv.exe process. If the process ends, it then checks to see if the service has cleanly stopped with an “Exit Code” of 0. If it hasn’t, it will clean out the spooler folder and then restart the Spooler service, including any dependants that are not running, such as the “Citrix Print Manager” service. The cool thing is that it time stamps the events, writes messages to the Event Logs, and sends e-mail alerts. Very cool for monitoring a problematic printing environment.

The script does a whole lot more, specifically around iPrint, and can really be adapted for any printing solution. The important thing is that it quickly and neatly restarts the Spooler service, preventing any user interruptions what so ever.

I’m currently running the script from the good old “autoexnt” Resouce Kit tool, so it runs at Startup as the System account. But you could also run it as a Startup Script from a Group Policy Object.

It is executed by the Autoexnt.bat using the following line…

start cscript //nologo "%SystemRoot%\System32\MonitorProcess.vbs" spoolsv.exe spooler

It’s worth mentioning here that I use the FREE w3 JMail COM Object from Dimac to send SMTP messages from within VBScripts. And as such, you will see how I create an instance of the jmail.message object set objJMail = CreateOBject(“JMail.Message”) and then specify the sender, recipient, subject, body and mail server. Therefore, this script will fail as is if you do not install JMail, or at least place a copy of the jmail.dll on the server and register it. Alternatively, you can comment out any calls to the SendMail subroutine.

MonitorProcess.vbs

' The purpose of this script is to monitor the spoolsv.exe process. If it ends, restart the Spooler service.
' I have tried to make this as generic as possible so that it can be used to monitor other processes and
' services.

' This was specifically written for a client who had implemented Novell's iPrint into their Citrix XenApp 4.5
' environment. iPrint was causing a lot of grief when adding printer objects by ending the spoolsv.exe process.

' Version 1.0
' Written by Jeremy@jhouseconsulting.com on 14th July 2008.

Option Explicit

Dim objArgs, strProcess, strFile, strService, j, k, blnFirstRun, strComputer
Dim objWMIService, colProcesses, blnDebugger, intDebugRuns

strComputer = "."
Set objArgs = WScript.Arguments

' Check to make sure we received arguments.
if objArgs.Count = 0 Then
  WScript.Echo "USAGE: cscript //nologo ''MonitorProcess.vbs'' ''<process>'' ''<service>''" & VbCr & VbCr & _
               "Where 'process' is the process you wish to monitor, and 'service' is the" & VbCr & _
               "short name of the service that needs to be restarted." & VbCr & VbCr & _
               "For Example:"  & VbCr & _
               "cscript //nologo MonitorProcess.vbs spoolsv.exe spooler"
  WScript.Quit(0)
End If

Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")

' Check to make sure the script is not already running, as only one copy of this script should be running at
' any time.
Set colProcesses = objWMIService.ExecQuery _
  ("SELECT commandline FROM Win32_Process WHERE commandline LIKE '%" & wscript.scriptname & "%'")
If colProcesses.Count > 1 Then
  Set objArgs = Nothing
  Set objWMIService = Nothing
  Set colProcesses = Nothing
  wscript.quit(0)
End If

blnFirstRun = True

' Setting this value to True will trigger the user mode debugger.
blnDebugger = True

' This value represents the number of times the debugger should run.
intDebugRuns = 5

Do Until j = 999
' Note how we never increment j. Therefore, it takes on the default value of 0, and never increments, meaning
' that this will loop forever.
  strProcess = objArgs(0)
  strFile = objArgs(0) & " monitoring.txt"

  if objArgs.Count > 1 Then
    strService = objArgs(1)
  End If

  If blnFirstRun Then
    wscript.echo Now() & vbTab & "The script has initiated."
  End If

  Call MonitorProcess(strProcess,strService,strFile,blnFirstRun,blnDebugger)
  blnFirstRun = False

  If K = intDebugRuns-1 Then
    blnDebugger = False
  End If
  k = k + 1
Loop

Set objArgs = Nothing
Set objWMIService = Nothing
Set colProcesses = Nothing

WScript.Quit(0)

Sub MonitorProcess(strProcess,strService,strFile,blnFirstRun,blnDebugger)

' This subroutine will monitor the process.

  Dim objWMIService, colProcesses, objProcess, strTimeStamp, strMessage
  Dim strComputer, strProcessPath, process, i, objFSO, intExitCode, strServiceMessage

  Const HKEY_LOCAL_MACHINE = &H80000002

  strComputer = "."

  Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
  Set objFSO = CreateObject("Scripting.FileSystemObject") 

' Check to see if the process is running.
  Set colProcesses = objWMIService.ExecQuery _
  ("SELECT * FROM Win32_Process WHERE Name = '" & strProcess & "'")
  If colProcesses.Count > 0 Then

  If blnDebugger Then
    Call RunDebugger(strProcess,blnFirstRun)
  End If

'   Get the path of the running process.
    for each process in colProcesses
      strProcessPath = Left(process.ExecutablePath, InstrRev(process.ExecutablePath, "\"))
'     wscript.echo strProcessPath
    Next
    strFile = strProcessPath & strFile

    If blnFirstRun Then
      If objFSO.FileExists(strFile) Then
        objFSO.DeleteFile(strFile), True
      End If
    End If

'   Wait for the process to end.
    Set colProcesses = objWMIService.ExecNotificationQuery _
      ("Select * From __InstanceDeletionEvent " _
              & "Within 1 Where TargetInstance ISA 'Win32_Process'")

    Do Until i = 999
'   Note how we never increment i. Therefore, it takes on the default value of 0,
'   and never increments, meaning that this will loop forever until the process ends.
    Set objProcess = colProcesses.NextEvent
      If lcase(objProcess.TargetInstance.Name) = lcase(strProcess) Then
        Exit Do
      End If
    Loop

    intExitCode=ServiceExitCode(strService)
    If intExitCode <> 0 Then
      strTimeStamp="on " & Date() & " at " & Time()
      strMessage="Process: " & strProcess & VbCr & "Status: Ended" & VbCr & "Timestamp: " & strTimeStamp
      wscript.echo Now() & vbTab & "Process has ended."
    Else
      wscript.echo Now() & vbTab & "Process has ended because the service was cleanly stopped."
    End If

  Else
      strTimeStamp="on " & Date() & " at " & Time()
      strMessage="Process: " & strProcess & VbCr & "Status: Not running" & VbCr & "Timestamp: " & strTimeStamp
      wscript.echo Now() & vbTab & "Process not running."
      Call WaitForProcessToStart(strProcess)
  End If
  If intExitCode <> 0 Then
    Call WriteToEventLog(strMessage)
    wscript.echo Now() & vbTab & "Message written to event log."
    Call WriteToLogFile(strFile,strMessage)
    wscript.echo Now() & vbTab & "Message written to log file."
    Call SendMail("Process",strMessage)
    wscript.echo Now() & vbTab & "E-mail alert sent."
    If strService <> "" Then
      Call RestartService(strService,strFile)
    End If
  End If

  set objWMIService = Nothing
  Set objFSO = Nothing
  Set colProcesses = Nothing

End Sub

Function ServiceExitCode(strService)

' This function gets the exit code for the service and passes it back as a return value so that we know if the
' service has cleanly stopped.

  Dim strComputer, objWMIService, colListOfServices, objService
  strComputer = "."
  Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
  Set colListOfServices = objWMIService.ExecQuery _
        ("Select * from Win32_Service where Name='" & strService & "'")
  For Each objService in colListOfServices
    ServiceExitCode=objService.ExitCode
  Next
  Set objWMIService = Nothing
  Set colListOfServices = Nothing
End Function

Sub WaitForProcessToStart(strProcess)

' This subroutine waits for the process to start

  Dim strComputer, objWMIService, colMonitoredProcesses, l, objLatestProcess, strTimeStamp, strMessage
  strComputer = "."
  Set objWMIService = GetObject("winmgmts:" _
    & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
  Set colMonitoredProcesses = objWMIService. _
    ExecNotificationQuery("select * from __instancecreationevent " _
        & " within 1 where TargetInstance isa 'Win32_Process'")
  Do Until l = 999
    Set objLatestProcess = colMonitoredProcesses.NextEvent
    If lcase(objLatestProcess.TargetInstance.Name) = lcase(strProcess) Then
      strTimeStamp="on " & Date() & " at " & Time()
      strMessage="Process: " & strProcess & VbCr & "Status: Started" & VbCr & "Timestamp: " & strTimeStamp
      wscript.echo Now() & vbTab & "Process started."
      Exit Do
    End If
  Loop
  Set objWMIService = Nothing
  Set colMonitoredProcesses = Nothing
  Set objLatestProcess = Nothing
End Sub

Sub RestartService(strService,strFile)

' This subroutine will restart the service if it has terminated abnormally, and will
' attempt to start any other stopped services that have this service set as a dependency.

  Dim strComputer, objWMIService, objItem, objService, colListOfServices, return, strTimeStamp
  Dim strMessage, objServiceList, objDependService
  strComputer = "."
  Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
  Set colListOfServices = objWMIService.ExecQuery _
  ("Select * from Win32_Service Where Name ='" & strService & "'")
  If colListOfServices.count > 0 Then
    For Each objService in colListOfServices
      If (lcase(objService.State)="stopped") AND (objService.ExitCode<>0) Then
'       As an example, when the spoolsv.exe process ends, the exit code for the spooler
'       service is 1067, which means that the process terminated unexpectedly.
        wscript.echo Now() & vbTab & "The Service has stopped with an exit code of " & objService.ExitCode & "."
      Else
        return=objService.StopService()
        If return=0 Then
          wscript.echo Now() & vbTab & "Service successfully stopped."
          strTimeStamp="on " & Date() & " at " & Time()
          strMessage="Service: " & objService.Name & VbCr & "Status: Stopped" & VbCr & "Timestamp: " & strTimeStamp
          Call WriteToEventLog(strMessage)
          wscript.echo Now() & vbTab & "Message written to event log."
          Call WriteToLogFile(strFile,strMessage)
          wscript.echo Now() & vbTab & "Message written to log file."
          Call SendMail("Service",strMessage)
          wscript.echo Now() & vbTab & "E-mail alert sent."
        End If
      End If

      Select Case LCase(strService)
        Case "spooler"
          Call CleanOutSpoolerFolder()
          Call CheckforiPrint()
      End Select

      return=objService.StartService()
      If return=0 Then
        wscript.echo Now() & vbTab & "The " & objService.Name & " service successfully started."
      End If
      strTimeStamp="on " & Date() & " at " & Time()
      strMessage="Service: " & objService.Name & VbCr & "Status: Started" & VbCr & "Timestamp: " & strTimeStamp
      Call WriteToEventLog(strMessage)
      wscript.echo Now() & vbTab & "Message written to event log."
      Call WriteToLogFile(strFile,strMessage)
      wscript.echo Now() & vbTab & "Message written to log file."
      Call SendMail("Service",strMessage)
      wscript.echo Now() & vbTab & "E-mail alert sent."

'     Start dependent services
      Set objServiceList = objWMIService.ExecQuery("Associators of " _
                 & "{Win32_Service.Name='" & strService & "'} Where " _
                 & "AssocClass=Win32_DependentService " & "Role=Antecedent" )
      For Each objDependService In objServiceList
        If objDependService.State = "Stopped" Then
          return=objDependService.StartService()
          If return=0 Then
            wscript.echo Now() & vbTab & "The " & objDependService.Name & " service successfully started."
          End If
          strTimeStamp="on " & Date() & " at " & Time()
          strMessage="Service: " & objDependService.Name & VbCr & "Status: Started" & VbCr & "Timestamp: " & strTimeStamp
          Call WriteToEventLog(strMessage)
          wscript.echo Now() & vbTab & "Message written to event log."
          Call WriteToLogFile(strFile,strMessage)
          wscript.echo Now() & vbTab & "Message written to log file."
          Call SendMail("Service",strMessage)
          wscript.echo Now() & vbTab & "E-mail alert sent."
        End If
      Next

    Next
  Else
    strTimeStamp="on " & Date() & " at " & Time()
    strMessage="Service: " & strService & VbCr & "Status: Not found" & VbCr & "Timestamp: " & strTimeStamp
    Call WriteToEventLog(strMessage)
    wscript.echo Now() & vbTab & "Message written to event log."
    Call WriteToLogFile(strFile,strMessage)
    wscript.echo Now() & vbTab & "Message written to log file."
    wscript.quit 0
  End If
  Set objWMIService  = Nothing
  Set colListOfServices = Nothing
  Set objServiceList = Nothing
End Sub

Sub WriteToEventLog(strMessage)

' This subroutine will write an alert to the event logs.

  Dim WshShell
  Set WshShell = CreateObject("WScript.Shell")
  WshShell.LogEvent 2, strMessage
  Set WshShell = Nothing
End Sub

Sub WriteToLogFile(strFile,strMessage)

' This subroutine will write the entries to a text file

  Dim objFSO, objFile
  Const ForAppending = 8
  Set objFSO = CreateObject("scripting.filesystemobject")
  Set objFile = objFSO.OpenTextFile(strFile,ForAppending,True,0)
  strMessage = Replace(strMessage,VbCr,vbCrLf) & vbCrLf
  objFile.WriteLine strMessage
  objFile.Close
  Set objFile = Nothing
End Sub

Sub SendMail(strServiceorProcess,strMessage)

' This subroutine will send off an e-mail alert using the JMail COM Object from www.Dimac.net

  Dim WshNetwork, sComputerName, strSMTPServer, strReceipent1, strSubject, strBody
  Dim objJMail

  Set WshNetwork = WScript.CreateObject("WScript.Network")
  sComputerName = WshNetwork.ComputerName

  strSMTPServer = "smtp.mydomain.com"
  strReceipent1 = "CitrixAlerts@mydomain.com"
  strSubject = "A " & strServiceorProcess & " has changed state on " & sComputerName & "."

  strBody = Replace(strMessage,VbCr,vbCrLf)

  set objJMail = CreateOBject("JMail.Message")

  objJMail.From = "server.alerts@mydomain.com"
  objJMail.FromName = "Server Alerts"

  objJMail.AddRecipient strReceipent1
  objJMail.Subject = strSubject
  objJMail.Body = strBody

  objJMail.Send(strSMTPServer)

  Set WshNetwork = Nothing
  set objJMail = Nothing
End Sub

Sub CleanOutSpoolerFolder()

' This subroutine will clean put the Spooler folder.

  Dim oReg, strComputer, objFSO, strKeyPath, strValueName, strValue, objFolder, Subfolder

  Const HKEY_LOCAL_MACHINE = &H80000002
  StrComputer="."

  Set oReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _
  strComputer & "\root\default:StdRegProv")
  Set objFSO = CreateObject("Scripting.FileSystemObject") 

  strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Printers"
  strValueName = "DefaultSpoolDirectory"
  oReg.GetStringValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,strValue
  Wscript.Echo Now() & vbTab & "The spooler folder is: " & strValue

  objFSO.DeleteFile(strValue & "\*.*"), True
  Set objFolder = objFSO.GetFolder(strValue)
  For Each Subfolder in objFolder.SubFolders
    objFSO.DeleteFolder Subfolder.Path, True
  Next
  Wscript.Echo Now() & vbTab & "Deleted the contents of the spooler folder."                   

  Set objFolder = Nothing
  Set oReg = Nothing
  Set objFSO = Nothing
End Sub

Sub CheckforiPrint()

' This subroutine checks for the existence of iPrint.
' If it's installed it checks to see if Trace is enabled.
' If Trace is enabled it checks for the Trace file.
' If the Trace file exists, it renames it before restarting the Spooler service.
' NOTE that iPrint should be doing this, but I found that it was unreliable. It
' was perhaps a timing issue!

  Dim oReg, strComputer, objFSO, strKeyPath, strValueName, dwValue
  Dim WshShell, sRegValue, sRegKey, strSystemDrive

  Const HKEY_LOCAL_MACHINE = &H80000002
  StrComputer="."

  Set WshShell = CreateObject("WScript.Shell")
  Set oReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _
  strComputer & "\root\default:StdRegProv")
  Set objFSO = CreateObject("Scripting.FileSystemObject")
  strSystemDrive = WshShell.ExpandEnvironmentStrings("%SystemDrive%")

  sRegKey = "HKLM\SOFTWARE\Novell-iPrint"
  sRegValue = "HKLM\SOFTWARE\Novell-iPrint\Settings\TraceOn" 

  If RegKeyExists(sRegKey) Then
    Wscript.Echo Now() & vbTab & "iPrint is installed."
    If RegValueExists(sRegValue) Then
'     Check to see if Trace is enabled
      strKeyPath = "SOFTWARE\Novell-iPrint\Settings"
      strValueName = "TraceOn"
      oReg.GetDWORDValue HKEY_LOCAL_MACHINE,strKeyPath,strValueName,dwValue
      Select Case dwValue
        Case 0
          Wscript.Echo Now() & vbTab & "iPrint Trace is disabled."
        Case 1
          Wscript.Echo Now() & vbTab & "iPrint Trace is enabled."
'         Check for existance Trace file.
          If objFSO.FolderExists(strSystemDrive & "\NDPS") Then
            If objFSO.FileExists(strSystemDrive & "\NDPS\ippTrace.txt") Then
              Wscript.Echo Now() & vbTab & "An iPrint Trace File exists."
              If objFSO.FileExists(strSystemDrive & "\NDPS\ippTrace.previous.txt") Then
                objFSO.DeleteFile strSystemDrive & "\NDPS\ippTrace.previous.txt", 1
              End If
              objFSO.MoveFile strSystemDrive & "\NDPS\ippTrace.txt", strSystemDrive & "\NDPS\ippTrace.previous.txt"
              Wscript.Echo Now() & vbTab & "Renamed iPrint Trace file to " & strSystemDrive & "\NDPS\ippTrace.previous.txt"
            Else
              Wscript.Echo Now() & vbTab & "The iPrint Trace file is missing."
            End If
          End If
        Case Else
          Wscript.Echo Now() & vbTab & "iPrint Trace setting is invalid."
      End Select
    End If
  Else
    Wscript.Echo Now() & vbTab & "iPrint is not installed."
  End If
  Set WshShell = Nothing
  Set oReg = Nothing
  Set objFSO = Nothing
End Sub

Function RegKeyExists(ByVal sRegKey) 

' Returns True or False based on the existence of a registry key.

  Dim sDescription, oShell
  Set oShell = CreateObject("WScript.Shell")
  RegKeyExists = True
  sRegKey = Trim (sRegKey)
  If Not Right(sRegKey, 1) = "\" Then
    sRegKey = sRegKey & "\"
  End If
  On Error Resume Next
  oShell.RegRead "HKEYNotAKey\"
  sDescription = Replace(Err.Description, "HKEYNotAKey\", "")
  Err.Clear
  oShell.RegRead sRegKey
  RegKeyExists = sDescription <> Replace(Err.Description, sRegKey, "")
  On Error Goto 0
  Set oShell = Nothing
End Function

Function RegValueExists(sRegValue) 

' Returns True or False based of the existence of a registry value. 

  Dim oShell, RegReadReturn
  Set oShell = CreateObject("WScript.Shell")
  RegValueExists = True  ' init value
  On Error Resume Next
  RegReadReturn = oShell.RegRead(sRegValue)
  If Err.Number <> 0 Then
    RegValueExists = False
  End if
  On Error Goto 0
  Set oShell = Nothing
End Function 

Sub RunDebugger(strProcess,blnFirstRun)

' ADPlus is a vbscript included with the Debugging Tools for Windows. To enable the
' monitoring of a process and Wait for a crash, use the following command:
' ADPlus will only provide a user mode dump. Refer to KB article 286350 for further
' information.

  Dim WshShell, objFSO, strSystemDrive, strProgramFiles, strCommandLine
  Set WshShell = CreateObject("WScript.Shell")
  Set objFSO = CreateObject("Scripting.FileSystemObject")
  strSystemDrive = WshShell.ExpandEnvironmentStrings("%SystemDrive%")
  strProgramFiles = WshShell.ExpandEnvironmentStrings("%ProgramFiles%")
  If objFSO.FileExists(strProgramFiles & "\Debugging Tools for Windows\adplus.vbs") Then
  If blnFirstRun Then
    If objFSO.FolderExists(strSystemDrive & "\" & strProcess & "_dumps\") Then
      objFSO.DeleteFolder strSystemDrive & "\" & strProcess & "_dumps", True
    End If
  End If
  If NOT objFSO.FolderExists(strSystemDrive & "\" & strProcess & "_dumps\") Then
      objFSO.CreateFolder(strSystemDrive & "\" & strProcess & "_dumps")
    End If
    strCommandLine = "cscript " & chr(34) & strProgramFiles & "\Debugging Tools for Windows\adplus.vbs" & chr(34) & " -quiet -crash -pn " & strProcess & " -o " & strSystemDrive & "\" & strProcess & "_dumps"
    WshShell.Run strCommandLine, 1, FALSE
    wscript.echo Now() & vbTab & "Running the debugger to monitor for a " & strProcess & " crash using the following command line..." & VbCrLf & strCommandLine
  End If
  Set WshShell = Nothing
  Set objFSO = Nothing
End Sub
Jeremy Saunders

Jeremy Saunders

Technical Architect | DevOps Evangelist | Software Developer | Microsoft, NVIDIA, Citrix and Desktop Virtualisation (VDI) Specialist/Expert | Rapper | Improvisor | Comedian | Property Investor | Kayaking enthusiast at J House Consulting
Jeremy Saunders is the Problem Terminator. He is a highly respected IT Professional with over 35 years’ experience in the industry. Using his exceptional design and problem solving skills with precise methodologies applied at both technical and business levels he is always focused on achieving the best business outcomes. He worked as an independent consultant until September 2017, when he took up a full time role at BHP, one of the largest and most innovative global mining companies. With a diverse skill set, high ethical standards, and attention to detail, coupled with a friendly nature and great sense of humour, Jeremy aligns to industry and vendor best practices, which puts him amongst the leaders of his field. He is intensely passionate about solving technology problems for his organisation, their customers and the tech community, to improve the user experience, reliability and operational support. Views and IP shared on this site belong to Jeremy.
Jeremy Saunders
Jeremy Saunders

Previous post:

Next post: