PaperCut Client Launch or Logon Script for Windows

by Jeremy Saunders on September 11, 2012

Having deployed PaperCut print management software in a large University environment, we were faced with the challenge of how to ensure that the client (pc-client-local-cache.exe) launched successfully and consistently at every logon to meet all the use cases. We also had to consider how we were going to specify the different types of command line parameters. So I wrote a script 🙂

Here is the PaperCutClientLaunch.vbs script:

' This script will launch the PaperCut pc-client-local-cache.exe client during
' login for members of the following groups:
' - All-Students
' - All-Affiliate-Students
' - All-Staff
' - All-Affiliate-Staff
' - All-Affiliate-Visitors
' - All-Temporary-Accounts
' - All-IT-Admins
'
' The script will not launch the PaperCut client during login for members of the
' following groups:
' - Domain Admins (unless they are in the All-IT-Admins group)
' - All-Deny-Printing
'
' Use the inclusion and exclusion arrays to manage the launching of the client
' outside of these default groups.
'
' It launches...
' - normally for Students, and will open in the top right hand corner by default
' - minimized to the taskbar for Staff
' - minimized to the taskbar for Visitors
' - minimized to the taskbar for Temporary Accounts
' - minimized to the taskbar for IT Admin Accounts
' - minimized to the taskbar for any user accounts listed in the arrIncludeUsers
' array.
' - minimized to the taskbar for any user groups listed in the arrIncludeUserGroups
' array.
'
' The --minimized option tells the client to start minimized. On windows the
' client will be minimized to the task tray.
'
' The --cache <cache directory> option can be used to specify a location of the
' globally writable cache directory on the system's local hard drive. The cache
' is used to minimize network traffic on future launches. The default location is
' C:\Cache. Standard users will require WRITE and READ access to this directory.

' The User Client configuration options are documented here:
' http://www.papercut.com/products/ng/manual/apdx-tools-user-client.html
'
' The PaperCut documentation suggests that executing the client during the login
' process may actually result in it being terminated before or shortly after login,
' especially if the "Run logon scripts synchronously" group policy setting is
' enabled. So instead we write a value to the HKEY_CURRENT_USER "Run" key that will
' execute the client after login as expected.
'
' Release 1.6
' Written by Dave Rutherford and Jeremy@jhouseconsulting.com on 6th February 2012.
'------------------------------------------------------------------------------
' History Section
'------------------------------------------------------------------------------
'Date |Changed By |Comments
'------------------------------------------------------------------------------
'06/02/2012 J Saunders Added functions to test for group membership
'07/02/2012 J Saunders Added function to test running process in current
' session.
'09/02/2012 D Rutherford Changed from running the .exe to writing the .exe
' path to the Run key in HKCU.
'16/02/2012 J Saunders Added an array of users that don't fit into the
' default groups.
' Added a function for checking the array members.
'10/09/2012 J Saunders Enhanced the code so that it flows better.
' Added some "include" and "exclude" arrays to make
' the script far more flexible.
'---------- End History Section -----------------------------------------------

Option Explicit

Dim objShell, objFSO, strCommand, strPath, strEXE, strParameters, objNetwork
Dim strComputerName, strComputerDN, strUser, objSysInfo, strUserDN, strGroupTokens
Dim strGroup, strKey, strValue, arrIncludeUsers, arrIncludeUserGroups
Dim arrExcludeUserGroups, arrExcludeUsers, arrExcludeComputerGroups
Dim arrExcludeComputers, blnDeleteValue, blnDebug, blnLaunchWithoutParameters
Dim blnLaunchWithParameters

'******************* Variables to set *************************

strPath = "\\mydomain.com\data\moe\AppSource\PaperCutClient\client\win\"
strEXE = "pc-client-local-cache.exe"
strCommand = strPath & strEXE
strParameters = " --minimized"

' Value placed in the Run key
strValue = "PaperCutClient"

' This is an array of individual user accounts that don't fit into the default
' group framework, but should still execute the client at login, minimized to
' the taskbar.
arrIncludeUsers = Array("")

' This is an array of user groups that don't fit into the default framework, but
' should still execute the client at login, minimized to the taskbar.
arrIncludeUserGroups = Array("")

' This is an array of user groups that should not launch the PaperCut client
' when the user logs in.
arrExcludeUserGroups = Array("")

' This is an array of users that should not launch the PaperCut client when
' they log in.
arrExcludeUsers = Array("")

' This is an array of computers groups that should not launch the PaperCut client
' when any users log in.
arrExcludeComputerGroups = Array("")

' This is an array of computers that should not launch the PaperCut client when
' any users log in.
arrExcludeComputers = Array("")

blnDebug = False

'**************************************************************

If isProcessRunning(strEXE) Then
 wscript.quit(0)
ENd If

Set objShell = CreateObject("Wscript.Shell")
Set objFSO = CreateObject("Scripting.FileSystemObject")

If objFSO.FileExists(strCommand) Then

' Get current user and computer information
 set objNetwork = createobject("wscript.network")
 strComputerName = objNetwork.ComputerName
 strUser = objNetwork.Username
 Set objSysInfo = CreateObject("ADSystemInfo")
 strUserDN = objSysInfo.UserName
 strComputerDN = objSysInfo.ComputerName

' Get tokens of member groups
 strGroupTokens = GetTokenGroups(strUserDN)

strKey = "HKCU\Software\Microsoft\Windows\CurrentVersion\Run"

'************** Process the default groups ********************

If TokenListFindSid(strGroupTokens, GetSidByID("Domain Admins")) Then
 blnDeleteValue = True
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = False
 End If

If TokenListFindSid(strGroupTokens, GetSidByID("All-Students")) OR _
 TokenListFindSid(strGroupTokens, GetSidByID("All-Affiliate-Students")) Then
 blnDeleteValue = False
 blnLaunchWithoutParameters = True
 blnLaunchWithParameters = False
 End If

If TokenListFindSid(strGroupTokens, GetSidByID("All-Affiliate-Visitors")) OR _
 TokenListFindSid(strGroupTokens, GetSidByID("All-Temporary-Accounts")) Then
 blnDeleteValue = False
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = True
 End If

If TokenListFindSid(strGroupTokens, GetSidByID("All-Staff")) OR _
 TokenListFindSid(strGroupTokens, GetSidByID("All-Affiliate-Staff")) Then
 blnDeleteValue = False
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = True
 End If

If TokenListFindSid(strGroupTokens, GetSidByID("All-Kiosk-Accounts")) Then
 blnDeleteValue = True
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = False
 End If

If TokenListFindSid(strGroupTokens, GetSidByID("All-Deny-Printing")) Then
 blnDeleteValue = True
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = False
 End If

If TokenListFindSid(strGroupTokens, GetSidByID("All-IT-Admins")) Then
 blnDeleteValue = False
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = True
 End If

'************* Process the inclusion arrays *******************

If IsArray(arrIncludeUserGroups) Then
 If Trim(arrIncludeUserGroups(0)) <> "" Then
 For Each strGroup in arrIncludeUserGroups
 If TokenListFindSid(strGroupTokens, GetSidByID(strGroup)) Then
 blnDeleteValue = False
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = True
 End If
 Next
 End If
 End If

If IsArray(arrIncludeUsers) Then
 If InArray(strUser,arrIncludeUsers) Then
 blnDeleteValue = False
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = True
 End If
 End If

'************* Process the exclusion arrays *******************

If IsArray(arrExcludeUserGroups) Then
 If Trim(arrExcludeUserGroups(0)) <> "" Then
 For Each strGroup in arrExcludeUserGroups
 If TokenListFindSid(strGroupTokens, GetSidByID(strGroup)) Then
 blnDeleteValue = True
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = False
 End If
 Next
 End If
 End If

If IsArray(arrExcludeUsers) Then
 If InArray(strUser,arrExcludeUsers) Then
 blnDeleteValue = True
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = False
 End If
 End If

If IsArray(arrExcludeComputerGroups) Then
 If Trim(arrExcludeComputerGroups(0)) <> "" Then
 For Each strGroup in arrExcludeComputerGroups
 If IsMemberOf(strComputerDN, GetDNByID(Trim(strGroup))) Then
 blnDeleteValue = True
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = False
 End If
 Next
 End If
 End If

If IsArray(arrExcludeComputers) Then
 If InArray(strComputerName,arrExcludeComputers) Then
 blnDeleteValue = True
 blnLaunchWithoutParameters = False
 blnLaunchWithParameters = False
 End If
 End If

'*********** Set or delete the registry value *****************

If blnLaunchWithoutParameters Then
 'Write the exe path to the HKCU\...\Run key
 objShell.RegWrite strKey & "\" & strValue, chr(34) & strCommand & chr(34)
 'objShell.Run chr(34) & strCommand & chr(34),0,False
 If blnDebug Then
 wscript.echo chr(34) & strCommand & chr(34)
 End If
 End If

If blnLaunchWithParameters Then
 'Write the exe path to the HKCU\...\Run key
 objShell.RegWrite strKey & "\" & strValue, chr(34) & strCommand & chr(34) & strParameters
 'objShell.Run chr(34) & strCommand & chr(34) & strParameters,0,False
 If blnDebug Then
 wscript.echo chr(34) & strCommand & chr(34) & strParameters
 End If
 End If

If blnDeleteValue Then
 If RegValueExists(strKey & "\" & strValue) Then
 objShell.RegDelete strKey & "\" & strValue
 If blnDebug Then
 wscript.echo "Deleting the " & strValue & " value."
 End If
 End If
 End If

'**************************************************************

Set objNetwork = Nothing
 Set objSysInfo = Nothing

Else

If blnDebug Then
 wscript.echo "The program does not exist"
 End If

End If

Set objShell = Nothing
Set objFSO = Nothing

wscript.quit(0)

' Function to check if a process is running in the current session
Function isProcessRunning(byval strProcessName)

Dim strComputer, objWMIService, colProcesses, objProcess
 Dim ProcessId, colLogonSessions, LogonSession, strLogonID

strComputer = "."

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" _
 & strComputer & "\root\cimv2")
 Set colProcesses = objWMIService.ExecQuery( _
 "Select * from Win32_Process " _
 & "Where Name = '" & strProcessName & "'")

For Each objProcess in colProcesses
 ProcessId = objProcess.ProcessId
 Set colLogonSessions = objWMIService.ExecQuery _
 ("Associators of {Win32_Process='" _
 & ProcessId & "'} Where" _
 & " Resultclass = Win32_LogonSession" _
 & " Assocclass = Win32_SessionProcess", "WQL", 48)
 isProcessRunning = false
 For Each LogonSession in colLogonSessions
 strLogonID = LogonSession.LogonId
 isProcessRunning = true
 Next
 Next

Set objWMIService = Nothing
 Set colProcesses = Nothing
 Set colLogonSessions = Nothing

End Function

Function InArray(item,myarray)
 Dim i
 For i=0 To UBound(myarray) Step 1
 If strcomp(item,myarray(i),1) = 0 Then
 InArray=True
 Exit Function
 End If
 Next
 InArray=False
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

' =====================================
' Token query routines
' =====================================

' Is DN a member of security group?
' Usage: <bool> = IsMemberOf(<DN of object>, <DN of group>)
Function IsMemberOf(dnObject, dnGroup)
 IsMemberOf = TokenListFindSid(GetTokenGroups(dnObject), GetSidByDN(dnGroup))
End Function

' Gets tokenGroups attribute from the provided DN
' Usage: <Array of tokens> = GetTokenGroups(<DN of object>)
Function GetTokenGroups(dnObject)
 Dim adsObject

 ' Setup query of tokenGroup SIDs from dnObject
 Set adsObject = GetObject(LdapUri(dnObject))
 adsObject.GetInfoEx Array("tokenGroups"), 0
 GetTokenGroups = adsObject.GetEx("tokenGroups")
End Function

' Checks if the SID of a DN is found in an array of tokens.
' Usage: <bool> = TokenListFindSid(<Array of tokens>, <Object SID Byte()>)
Function TokenListFindSid(arrTokens, objectSid)
 Dim nSidSize, vSidHex, e
 TokenListFindSid = False

 If TypeName(objectSid) = "Byte()" Then
 ' Scan token array for object SID
 nSidSize = UBound(objectSid)
 vSidHex = ByteArrToHexString(objectSid)
 For Each e in arrTokens
 If UBound(e) = nSidSize Then
 If ByteArrToHexString(e) = vSidHex Then
 TokenListFindSid = True
 Exit For
 End If
 End If
 Next
 End If
End Function

' Encode Byte() to hex string
Function ByteArrToHexString(bytes)
 Dim i
 ByteArrToHexString = ""
 For i = 1 to Lenb(bytes)
 ByteArrToHexString = ByteArrToHexString & Right("0" & Hex(Ascb(Midb(bytes, i, 1))), 2)
 Next
End Function

' Format a DN into a valid LDAP URI
Function LdapUri(DN)
 LdapUri = "LDAP://" & Replace(DN, "/", "\/")
End Function

' =====================================
' Query helper routines
' =====================================

' Get object's SID by DN
' Usage: <SID Byte()> = GetSidByDN(<DN>)
Function GetSidByDN(objectDN)
 On Error Resume Next
 GetSidByDN = GetObject(LdapUri(objectDN)).Get("objectSid")
 On Error GoTo 0
End Function

' Get object's SID by ID
' Usage: <SID Byte()> = GetSidByID(<Object ID>)
Function GetSidByID(ID)
 Dim rs
 Set rs = QueryLDAP(GetRootDN, "(sAMAccountName=" & ID & ")", "objectSid", "subtree")
 If Not rs.EOF Then GetSidByID = rs("objectSid")
 rs.Close
 Set rs = Nothing
End Function

' Get DN by sAMAccountName
' Usage: <DN> = GetDNByID(<User or Group ID>)
Function GetDNByID(ID)
 Dim rs
 Set rs = QueryLDAP(GetRootDN, "(sAMAccountName=" & ID & ")", "distinguishedName", "subtree")
 If Not rs.EOF Then GetDNByID = rs("distinguishedName")
 rs.Close
 Set rs = Nothing
End Function

' Get sAMAccountName by object's SID
' Usage: <sAMAccountName> = GetIDBySid(<SID Byte()>)
Function GetIDBySid(objectSid)
 If TypeName(objectSid) = "Byte()" Then
 GetIDBySid = GetObject("LDAP://<SID=" & ByteArrToHexString(objectSid) & ">").Get("sAMAccountName")
 End If
End Function

' =====================================
' LDAP routines
' =====================================

' Get Root DN of logged in domain (e.g. DC=yourdomain,DC=com)
' Usage: <DN> = GetRootDN
Function GetRootDN
 GetRootDN = GetObject("LDAP://RootDSE").Get("defaultNamingContext")
End Function

' Get/create singleton LDAP ADODB connection object
' Usage: <Connection object ref> = LDAPConnection
' or: LDAPConnection.<property or method>
Dim l_LDAPConnection
Function LDAPConnection
 If IsEmpty(l_LDAPConnection) Then
 Set l_LDAPConnection = CreateObject("ADODB.Connection")
 l_LDAPConnection.Provider = "ADSDSOObject"
 l_LDAPConnection.Open "ADs Provider"
 End If
 Set LDAPConnection = l_LDAPConnection
End Function

' Close the LDAPConnection singleton object
' Usage: CloseLDAPConnection
Sub CloseLDAPConnection
 If IsObject(l_LDAPConnection) Then
 If l_LDAPConnection.State = 1 Then l_LDAPConnection.Close
 End If
 l_LDAPConnection = Empty
End Sub

' Query LDAP helper, return RecordSet
' Usage: <RecordSet object ref> = QueryLDAP(<DN>, <LDAP Filter>, <Attributes CSV>, <Scope>
' Scope can be: "subtree", "onelevel", or "base"
' Be sure to close the RecordSet object when done with it
Function QueryLDAP(DN, Filter, AttributeList, Scope)
 Set QueryLDAP = LDAPConnection.Execute("<" & LdapUri(DN) & ">;" & Filter & ";" & AttributeList & ";" & Scope)
End Function

Enjoy!

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: