This project is read-only.

Set-ActiveSetup and Remove-ActiveSetup: Per-user changes (as requested)

Topics: Archive - Toolkit Extensions
Sep 2, 2014 at 11:50 PM
Edited Nov 6, 2014 at 11:16 PM
I'll need these too at some point in the future and I had working code in CMD so I converted and contributed. Version: 0.5
Version: 0.6 (2014 Sep 17)
  • Added -Arguments parameter
  • $ActiveSetupPath renamed to $StubExePath
  • The function to make -Purge work is still not done and I have no time to finish it for now. Using -Purge will give you an error
Version: 0.7 (2014 Nov 6 )- Now uses ToolKit's functions throughout, has better comments, code and consistency. Solved issue to trigger changes for current user.
Function Set-ActiveSetup {
<#
.SYNOPSIS
    Creates an Active Setup entry in the registry 
    by simply giving the destination path of the thing doing the per-user changes
    
.DESCRIPTION
    Version: 0.7 - Author: CodePlex's That_annoying_guy - 2014 Nov 06 - BETA
    
    Active Setup is a built-in feature of Windows to handle per-user changes
    A registry key is created in HKLM and *as users login*, the user runs the "Stub" in the StubPath value and copies the Version value to HKCU in a matching Key
    If the Version value in HKLM is higher than the version value in HKCU, the StubPath is ran again when a user logs in.   
    This Function:
    -Creates the registry entries in HKLM:\Software\Microsoft\Active Setup\Installed Components\$installName
    -Creates StubPath value depending on the file extension of the $StubExePath parameter
    -Handles Version value with YYYYMMDDHHMMSS granularity to permit re-installs 
    in the same day and still trigger Active Setup as users logon.
    -Copies the Stub file to $StubExePath if not already there and if it's in $scriptDirectory
    (It ok if another process copies the StubExe to its destination on the target PC)
    -Runs the Stub for the user that is installing (no need to logout/login to trigger Active Setup)

.LINK
    http://www.sepago.de/d/helge/2010/04/22/active-setup-explained  
    
.SYNTAX
    Set-ActiveSetup -StubExePath <PathToStub> [-Arguments "MyArgs"][-Description "MyDesc"][-Key "KeyName"][-Locale "en"][-IsInstalled $false]
    
.EXAMPLE
    Set-ActiveSetup -StubExePath "PathToDesiredLocationOnTarget" -Arguments "MyArgs" -Description "MyDesc" -Key "KeyName" -Locale "en" -IsInstalled $false
    Set-ActiveSetup "C:\Program Files\MyApp\MyApp_v1r1_HKCU.exe"
    Set-ActiveSetup "C:\Program Files\MyApp\MyApp_v1r1_HKCU.VBS"
    Set-ActiveSetup "C:\Program Files\MyApp\MyApp_v1r1_HKCU.CMD"
    Set-ActiveSetup "C:\Program Files\MyApp\MyApp_v1r1_HKCU.PS1"
    Set-ActiveSetup "C:\Program Files\MyApp\MyApp_v1r1_HKCU.JS"

.BAD_EXAMPLE
    Set-ActiveSetup "MyApp_v1r1_HKCU.exe" 
    This will not work. We need a full path to where it is or will go!
    
.PARAMETER StubExePath
    Full path to the EXE/VBS/CMD/PS1/JS that will be launched for each new and existing user profile.
    This will be used as path of the StubPath value in Active Setup in HKLM
    If the file does not exists at $StubExePath, it is copied from $scriptDirectory
    Note: -StubExePath and -Arguments are combined in the StubPath value

.PARAMETER Arguments
    Optional Arguments to send to the EXE/VBS/CMD/PS1/JS file above 
    Note: -StubExePath and -Arguments are combined in the StubPath value

.PARAMETER Description
    Optional Description for the Active Setup. 
    Users will see "Setting up personalised settings for: $Description" at logon
    Defaults to $installName if blank

.PARAMETER Key
    Optional Name of registry Key name used for the Active Setup.
    Defaults to $installName if blank
    
.PARAMETER Locale
    Optional. Arbitrary string used to specify the installation language of the stub
    Not replicated to HKCU. Only used for identification purposes inn HKLM. Ignored if omitted.
    
.PARAMETER Purge
    Will load each HKCU hive (.Dat file) to remove Active setup key.
    For those who love overkill.

.PARAMETER IsInstalled
    Disables the Active Setup entry if set to False. (The StubExePath will not run) (DWORD=0)
    By default, the StubExePath will be run ONCE per user. This is the default and no value will be written.
    
.NOTES
    Your ActiveSetupStub file can be copied to $StubExePath via the MSI or other means. In which case it is only stubbed in HKLM
    If ActiveSetupStub file is in the pkg container and not in $StubExePath, it is copied to $StubExePath and stubbed in HKLM.

    How to add to AppDeployToolKit (The Too Lazy; Don't Read version):
    1-Edit \AppDeployToolkit\AppDeployToolkitExtensions.ps1 to add theses functions
    
    2-Add this line in your Deploy-Application.ps1 
        in $installPhase = "Installation" section:
            Set-ActiveSetup -StubExePath "<Destination path>\MyScript.vbs"
    
        In $installPhase = "Uninstallation"
            Remove-ActiveSetup 
    
    TODO: Add 32/64Bit support. (Current version is 64-bit only but I've been told this is important)
    TODO: Finish Remove-RegistryKeyFromAllHives function to make Purge parameter work
    TODO: prevent 2 or more ActiveSetups with the same name (prevent overwrite)(needed?)
    
.COMPONENT
    Depends on TK's Copy-File, Set-RegistryKey, Remove-RegistryKey, Execute-Process and Write-Log functions
#>
    Param(
        [Parameter(Mandatory = $true)]
        [string]$StubExePath = $(throw "StubExePath Param required"),
        [string]$Arguments,
        [string]$Description=$installName,
        [string]$Key=$installName,
        [string]$Locale,
        [bool]$IsInstalled
    )

    # CAVEAT: Version value is limited in # of digits and cannot do yyyyMMddHHmmss
    # Currently, it is known that 8 consecutive digits is one limit. (yyMMddHHmm)
    # Extra digits above 8 are ignored and will not trigger Active Setup
    # We are using commas to get more digits and it still triggers Active Setup
    [String]$Version=Get-Date -format "yyMM,ddHH,mmss"  #1405,1515,0522
    [String]$ActiveSetupKey="HKLM\Software\Microsoft\Active Setup\Installed Components\$Key"
    [String]$HKCU_ACTIVESETUPKEY="HKCU:\Software\Microsoft\Active Setup\Installed Components\$Key"


    #Copy file to StubExePath if needed
    #Your Stub file can also be copied to $StubExePath via the MSI or other means.(In which case it is only stubbed in HKLM)
    #If the Stub file is in $scriptDirectory and not in $StubExePath, it is copied to $StubExePath and stubbed in HKLM.
    [string]$StubExePath=[Environment]::ExpandEnvironmentVariables($StubExePath) # to handle %EnvVars%
    [string]$ActiveSetupFileName=[System.IO.Path]::GetFileName($StubExePath) #FYI: .Net does not care if $StubExePath exists
    If (Test-Path "$scriptDirectory\$ActiveSetupFileName") {
        #CAVEAT: This will overwrite the Stub file if $StubExePath already exists on target
        Copy-File -Path "$scriptDirectory\$ActiveSetupFileName" -Destination "$StubExePath" -ContinueOnError $false
    }   
    
    If (!(Test-Path $StubExePath)) { Throw "ERROR: ActiveSetup Stub file [$($ActiveSetupFileName)] is missing." }
    
    If ($Purge) {
        Write-Log "Removing old $ActiveSetupKey on all other User hives on machine..."
        #Cleans out previous ActiveSetup Key in each user profile (for those who love overkill )
        Remove-RegistryKeyFromAllHives $ActiveSetupKey
    }
    
    #set $StubPath according to file Extension of $StubExePath
    [String]$StubExeExt=$StubExePath.Substring($StubExePath.Length-3,3)
    If       ($StubExeExt -eq "exe") {
        [String]$StubPath="$StubExePath $Arguments"
        [String]$CUStubExePath=$StubExePath
        [String]$CUArguments=$Arguments
    } ElseIf ($StubExeExt -eq "vbs") { 
        [String]$StubPath="$envWinDir\System32\cscript.exe //nologo `"$StubExePath`" $Arguments"
        [String]$CUStubExePath="$envWinDir\System32\cscript.exe"
        [String]$CUArguments="//nologo `"$StubExePath`" $Arguments"
    } ElseIf ($StubExeExt -eq "cmd") { 
        [String]$StubPath="$envWinDir\System32\CMD.exe /C `"$StubExePath`" $Arguments"
        [String]$CUStubExePath="$envWinDir\System32\CMD.exe"
        [String]$CUArguments="/C `"$StubExePath`" $Arguments"
    } ElseIf ($StubExeExt -eq "ps1") { 
        [String]$StubPath="$envWinDir\System32\WindowsPowerShell\v1.0\powershell.exe `"$StubExePath`" $Arguments"
        [String]$CUStubExePath="$envWinDir\System32\WindowsPowerShell\v1.0\powershell.exe"
        [String]$CUArguments=" `"$StubExePath`" $Arguments"
    } ElseIf ($StubExeExt -eq ".js") { 
        [String]$StubPath="$envWinDir\System32\cscript.exe //nologo `"$StubExePath`" $Arguments"
        [String]$CUStubExePath="$envWinDir\System32\cscript.exe"
        [String]$CUArguments="//nologo `"$StubExePath`" $Arguments"
    } Else { 
        Throw "Unsupported ActiveSetup Stub file extension: $StubExeExt" 
    }

    Set-RegistryKey -Key $ActiveSetupKey -Name Stubpath -value $StubPath -ContinueOnError $false -Type ExpandString
    Set-RegistryKey -Key $ActiveSetupKey -name Version -value $Version -ContinueOnError $false
    Set-RegistryKey -Key $ActiveSetupKey -name '(Default)' -value $Description -ContinueOnError $false
    If ($Locale) { 
        Set-RegistryKey -Key $ActiveSetupKey -name Locale -value $Locale -ContinueOnError $false 
    }
    If ($IsInstalled) { 
        Set-RegistryKey -Key $ActiveSetupKey -name IsInstalled -value 0 -ContinueOnError $false -Type DWORD
    } else {
        Remove-RegistryKey -Key $ActiveSetupKey -Name IsInstalled -ContinueOnError $false
    }

    #CAVEAT: This "$RunningAsSystem" chunk is highly likely to be moved to another function
    $SIDCollection="LocalServiceSid","LocalSid","NetworkServiceSid","NetworkSid","NTAuthoritySid","LocalSystemSid"
    [bool]$RunningAsSystem=$false
    foreach ($Sid in $SIDCollection) {
        If ([System.Security.Principal.WindowsIdentity]::GetCurrent().User.IsWellKnown(
            [System.Security.Principal.WellKnownSidType]::$Sid)) {$RunningAsSystem=$true}
    }

    #Run ActiveSetup for Current User that is installing (This includes Citrix) but not SMS/SCCM
    If ($RunningAsSystem) { 
        Write-Log "INFO: System Account detected: Not Running ActiveSetup Stub for Current User."
    } Else {
        Write-Log "Launching StubPath for Current User..." # And test StubPath
        Execute-Process -FilePath $CUStubExePath -Arguments $CUArguments
    } 
    Write-Log "Set-ActiveSetup done.`n"
}
Sep 3, 2014 at 7:38 AM
Edited Sep 3, 2014 at 7:39 AM
Hope I can make a few remarks on your code here? :)
  • in setting the appropriate commandlines for the stubPath, the final Else might just want to take the existing value as-is (if it's not empty) instead of throwing an error? I think this would make the function more generalised
  • if I read your code correctly $activeSetupPath would only contain the path of the executable for stubPath. Does this not mean that you would need to add an -Arguments parameter to handle extra commandline stuff that needs to be appended to the stubPath command? Like what if I want to run "MyApp_v1r1_HKCU.exe /quiet /norestart /whatever" ?
  • Good idea about the -Purge option, but is this something that maybe is only needed in Remove-ActiveSetup?
  • I think running the stubpath for current user is unnecessary and might cause more trouble than it's worth. Especially taking into account many of these installers would run as LocalSystem.
Keep up the good work!
Sep 3, 2014 at 3:54 PM
Yup, I need to add a -Arguments parameter. Otherwise adding Arguments to $ActiveSetupPath will break the script host selection code. This also proves why I need that ELSE to be not so "generalized".

The -Purge in Set-ActiveSetup is for people who have no confidence in bumping the Version number able to trigger Active Setup. It is overkill even for Remove-ActiveSetup and I would be worried about using it because it might trash HKCUs if the power goes out while you are purging them. But I need it for buy-in here at my work. Please note that the -Purge code is only place-holder stuff. I need to create another function to do that.

As for running the StubPath for current user, it is quite necessary and HampusN specifically asked for it. I have place holder code to skip it if the current user is SMS/SCCM. Now I'm thinking I should just test if the current user is LocalSystem or equivalent.
Sep 17, 2014 at 3:18 PM
Edited Nov 6, 2014 at 11:00 PM
Version: 0.6
  • Added -Arguments parameter
  • $ActiveSetupPath renamed to $StubExePath
  • The function to make -Purge work is still not done and I have no time to finish it for now. Using -Purge will give you an error
EDIT: (Removed code and updated top post to prevent confusion)
Nov 6, 2014 at 11:03 PM
Version: 0.7
  • Now uses ToolKit's functions,
  • Better comments, code and consistency.
  • Solved issue to trigger changes for current user.
The updated code is in top post to prevent confusion
Nov 23, 2014 at 7:39 AM
Edited Nov 25, 2014 at 5:01 AM
With That_annoying_guy's permission, I have modified the Set-ActiveSetup function to be compatible with the latest 3.5 version of the toolkit. I have also made a few other modifications This function will most likely be making it into a future 4.0 release of the toolkit. Until then, please use/test and report any bugs.
Function Set-ActiveSetup {
<#
.SYNOPSIS
    Creates an Active Setup entry in the registry to execute a file for each user upon login.
.DESCRIPTION
    Active Setup allows handling of per-user changes registry/file changes upon login.
    A registry key is created in the HKLM registry hive which gets replicated to the HKCU hive when a user logs in.
    If the "Version" value of the Active Setup entry in HKLM is higher than the version value in HKCU, the file referenced in "StubPath" is executed.
    This Function:
    - Creates the registry entries in HKLM:SOFTWARE\Microsoft\Active Setup\Installed Components\$installName.
    - Creates StubPath value depending on the file extension of the $StubExePath parameter.
    - Handles Version value with YYYYMMDDHHMMSS granularity to permit re-installs on the same day and still trigger Active Setup after Version increase.
    - Copies/overwrites the StubPath file to $StubExePath destination path if file exists in 'Files' subdirectory of script directory.
    - Executes the StubPath file for the current user as long as not in Session 0 (no need to logout/login to trigger Active Setup).
.PARAMETER StubExePath
    Full destination path to the file that will be executed for each user that logs in.
    If this file exists in the 'Files' subdirectory of the script directory, it will be copied to the destination path.
.PARAMETER Arguments
    Arguments to pass to the file being executed.
.PARAMETER Description
    Description for the Active Setup. Users will see "Setting up personalised settings for: $Description" at logon. Default is: $installName.
.PARAMETER Key
    Name of the registry key for the Active Setup entry. Default is: $installName.
.PARAMETER Locale
    Optional. Arbitrary string used to specify the installation language of the file being executed. Not replicated to HKCU.
.PARAMETER PurgeActiveSetupKey
    Will load each logon user's HKCU registry hive to remove Active Setup entry.
.PARAMETER DisableActiveSetup
    Disables the Active Setup entry so that the StubPath file will not be executed.
.PARAMETER ContinueOnError
    Continue if an error is encountered.
.EXAMPLE
    Set-ActiveSetup -StubExePath 'C:\Users\Public\Company\ProgramUserConfig.vbs' -Arguments '/Silent' -Description 'Program User Config' -Key 'ProgramUserConfig' -Locale 'en'
.EXAMPLE
    Set-ActiveSetup -StubExePath 'C:\Program Files\MyApp\MyApp_v1r1_HKCU.exe'
.NOTES
    Original code borrowed from: Denis St-Pierre (Ottawa, Canada), Todd MacNaught (Ottawa, Canada)
.LINK
    http://psappdeploytoolkit.codeplex.com
#>
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$StubExePath,
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [string]$Arguments,
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [string]$Description = $installName,
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [string]$Key = $installName,
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [string]$Locale,
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [switch]$DisableActiveSetup = $false,
        [Parameter(Mandatory=$false)]
        [switch]$PurgeActiveSetupKey,
        [Parameter(Mandatory=$false)]
        [ValidateNotNullorEmpty()]
        [boolean]$ContinueOnError = $true
    )
    
    Begin {
        ## Get the name of this function and write header
        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header
    }
    Process {
        Try {
            ## Active Setup is not triggered if Version value has more than 8 consecutive digits. Commas will be used to get around this limitation.
            [string]$Version = (Get-Date -Format 'yyMM,ddHH,mmss').ToString()  # Ex: 1405,1515,0522
            [string]$ActiveSetupKey = "HKLM:SOFTWARE\Microsoft\Active Setup\Installed Components\$Key"
            [string]$HKCUActiveSetupKey = "HKCU:Software\Microsoft\Active Setup\Installed Components\$Key"
            
            ## Delete Active Setup registry entry for all logon user registry hives on the system
            If ($PurgeActiveSetupKey) {
                Write-Log -Message "Remove Active Setup entry [$HKCUActiveSetupKey] for all log on user registry hives on the system." -Source ${CmdletName}
                [scriptblock]$RemoveHKCUActiveSetupKey = { Remove-RegistryKey -Key $HKCUActiveSetupKey -SID $UserProfile.SID }
                Invoke-HKCURegistrySettingsForAllUsers -RegistrySettings $RemoveHKCUActiveSetupKey
                Return
            }
            
            ## Verify a file with a supported file extension was specified in $StubExePath
            [string[]]$StubExePathFileExtensions = '.exe', '.vbs', '.cmd', '.ps1', '.js'
            [string]$StubExeExt = [System.IO.Path]::GetExtension($StubExePath)
            If ($StubExePathFileExtensions -notcontains $StubExeExt) {
                Throw "Unsupported Active Setup StubPath file extension [$StubExeExt]."
            }
            
            ## Copy file to $StubExePath from the 'Files' subdirectory of the script directory (if it exists there)
            [string]$StubExePath = [Environment]::ExpandEnvironmentVariables($StubExePath)
            [string]$ActiveSetupFileName = [System.IO.Path]::GetFileName($StubExePath)
            [string]$StubExeFile = Join-Path -Path $dirFiles -ChildPath $ActiveSetupFileName
            If (Test-Path -Path $StubExeFile -PathType Leaf) {
                #  This will overwrite the StubPath file if $StubExePath already exists on target
                Copy-File -Path $StubExeFile -Destination $StubExePath -ContinueOnError $false
            }
            
            ## Check if the $StubExePath file exists
            If (-not (Test-Path -Path $StubExePath -PathType Leaf)) { Throw "Active Setup StubPath file [$ActiveSetupFileName] is missing." }
            
            ## Define Active Setup StubPath according to file extension of $StubExePath
            Switch ($StubExeExt) {
                '.exe' {
                    [string]$CUStubExePath = $StubExePath
                    [string]$CUArguments = $Arguments
                    [string]$StubPath = "$CUStubExePath"
                }
                {'.vbs','.js' -contains $StubExeExt} {
                    [string]$CUStubExePath = "$envWinDir\system32\cscript.exe"
                    [string]$CUArguments = "//nologo `"$StubExePath`""
                    [string]$StubPath = "$CUStubExePath $CUArguments"
                }
                '.cmd' {
                    [string]$CUStubExePath = "$envWinDir\system32\CMD.exe"
                    [string]$CUArguments = "/C `"$StubExePath`""
                    [string]$StubPath = "$CUStubExePath $CUArguments"
                }
                '.ps1' {
                    [string]$CUStubExePath = "$PSHOME\powershell.exe"
                    [string]$CUArguments = "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$StubExePath`""
                    [string]$StubPath = "$CUStubExePath $CUArguments"
                }
            }
            If ($Arguments) {
                [string]$StubPath = "$StubPath $Arguments"
                If ($StubExeExt -ne '.exe') { [string]$CUArguments = "$CUArguments $Arguments" }
            }
            
            ## Create the Active Setup entry in the registry
            Set-RegistryKey -Key $ActiveSetupKey -Name '(Default)' -Value $Description -ContinueOnError $false
            Set-RegistryKey -Key $ActiveSetupKey -Name 'StubPath' -Value $StubPath -Type 'ExpandString' -ContinueOnError $false
            Set-RegistryKey -Key $ActiveSetupKey -Name 'Version' -Value $Version -ContinueOnError $false
            If ($Locale) { Set-RegistryKey -Key $ActiveSetupKey -Name 'Locale' -Value $Locale -ContinueOnError $false }
            If ($DisableActiveSetup) {
                Set-RegistryKey -Key $ActiveSetupKey -Name 'IsInstalled' -Value 0 -Type 'DWord' -ContinueOnError $false
            }
            Else {
                Set-RegistryKey -Key $ActiveSetupKey -Name 'IsInstalled' -Value 1 -Type 'DWord' -ContinueOnError $false
            }
            
            ## Execute the StubPath file for the current user as long as not in Session 0
            If ($SessionZero) {
                Write-Log -Message 'Session 0 detected: Will not execute Active Setup StubPath file. Users will have to log off and log back into their account to execute Active Setup entry.' -Source ${CmdletName}
            }
            Else {
                Write-Log -Message 'Execute Active Setup StubPath file for the current user' -Source ${CmdletName}
                If ($CUArguments) {
                    $ExecuteResults = Execute-Process -FilePath $CUStubExePath -Arguments $CUArguments -PassThru
                }
                Else {
                    $ExecuteResults = Execute-Process -FilePath $CUStubExePath -PassThru
                }
            }
        }
        Catch {
            Write-Log -Message "Failed to set Active Setup registry entry. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName}
            If (-not $ContinueOnError) {
                Throw "Failed to set Active Setup registry entry: $($_.Exception.Message)."
            }
        }
    }
    End {
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer
    }
}