Configuring SCCM 2007 Reboot Countdown

When we author SCCM scripts here, we ensure they return the appropriate exit code to ConfigMgr. As a result of this the user is sometimes presented with a reboot prompt with a countdown timer (the default is 5 minutes in SCCM 2007) due to the installation returning a 3010 exit code.

5 minutes didn’t seem quite long enough to force a reboot, and it turns out you can change these settings in: Site Database > Site Manager > [Site] > Site Settings > Client Agent. Right-Click Computer Client Agent and click Properties. Then go to the Restart tab.

sccm_countdown

Publishing App-V 5 packages with SCCM 2007

Introduction

At the time of writing this article App-V 5 is not natively supported by SCCM 2007.  Publishing App-V 5 packages with SCCM 2007 requires either deploying the MSI which gets created by the sequencer, or create a custom Powershell script to perform the publishing tasks manually.

The limitation with deploying the MSI is that you cannot handle connection groups, and so you’ll end up with a hybrid approach of deploying MSIs for packages and powershell scripts for connection groups.  On top of this, I don’t believe there is a way (using the MSI) to apply DeploymentConfig and UserConfig files at add/publish time respectively.

To circumvent these issues I’ve created an App-V 5 administration script which can be used with SCCM 2007 programs.

Command Line Parameters are:

Parameter: -av5Verb                        Possible Value(s): “add” “remove”

Parameter: -av5Object                     Possible Value(s): “package” “connectionGroup”

Parameter: -av5PackageName        Possible Value(s): {Package Name – for example “Adobe-Reader-X”}

Parameter: -deploymentConfig         Possible Value(s): “true” “false”

Parameter: -userConfig                     Possible Value(s): “true” “false”

Usage

1.  Put the Powershell script inside the installation source folder, with any associated .appv/.xml files.  In our example we’ll call the script AV5Admin.ps1:

App-v 5 and SCCM 2007

2.  Command lines for the SCCM program are as follows (be careful of the quotes in this blog post!!  See ‘Things to Note’ below for more info!):

Add package (Publishes globally by default)

powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass .\AV5Admin.ps1 -av5Verb "add" -av5Object "package" -av5PackageName "Adobe-Reader-X"

Add package and apply deployment config xml

powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass .\AV5Admin.ps1 -av5Verb "add" -av5Object "package" -av5PackageName "Adobe-Reader-X" -deploymentConfig "true"

Add package and apply user config xml

powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass .\AV5Admin.ps1 -av5Verb "add" -av5Object "package" -av5PackageName "Adobe-Reader-X" -userConfig "true"

Remove package

powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass .\AV5Admin.ps1 -av5Verb "remove" -av5Object "package" -av5PackageName "Adobe-Reader-X"

Add connection group (connecting globally published packages)

powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass .\AV5Admin.ps1 -av5Verb "add" -av5Object "connectionGroup" -av5PackageName "Adobe-Reader-X"

Add connection group (connecting user published packages)

powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass .\AV5Admin.ps1 -av5Verb "add" -av5Object "connectionGroup" -av5PackageName "Adobe-Reader-X" -userConfig "true"

Remove connection group

powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass .\AV5Admin.ps1 -av5Verb "remove" -av5Object "connectionGroup" -av5PackageName "Adobe-Reader-X"

Things to Note

  • When creating your connection group XML file, the DisplayName attribute of the AppConnectionGroup tag should be set to the av5PackageName value. In the case of the example above, it should be set to “Adobe-Reader-X”.
  • Be careful of dodgy quotes when copying and pasting. For example:
    " - correct 
    “ - incorrect 
    ” - incorrect
  • By default, no XML (deployment or user) files are applied. If you specify a “true” value for -deploymentConfig the xx_deploymentConfig.xml file will be applied and the package will be published globally. If you specify a “true” value for -userConfig the xx_UserConfig.xml file will be applied and the package will NOT be published globally.
  • If any XML files are applied, scripting is enabled on the App-V client.
  • *UPDATE – Apparently Configuration Manager 2012 uses -ExecutionPolicy Bypass, as shown here: http://blogs.technet.com/b/virtualvibes/archive/2013/10/02/configuration-manager-2012-sp1-and-app-v-5-0-integration-more-than-meets-the-eye.aspx We have now used this in our command line, whilst also hiding the Powerhell console using -WindowStyle Hidden.
  • Log files get written to C:\Windows\Security\Logs. For example, if you try to publish a connection group and some of the connected packages are not published on the machine, the appropriate error message will be written to a log file called {packagename}_add.log.
  • Checks to see if the AppvClient module exists and is imported.
  • Returns a code of 0 for success, and 1 for an error.
  • **Important**  If running a 32-bit SCCM agent on a 64-bit platform see this post, which explains why we MUST use the Sysnative alias when pointing to the powershell.exe.

The Script

Param(
  [string]$av5Verb,
  [string]$av5Object,
  [string]$av5PackageName,
  [string]$deploymentConfig,
  [string]$userConfig
)
#possible values are:
#
#av5Verb - "add", "remove"
#av5Object - "package", "connectionGroup"
#av5PackageName - "{package brick name}"
#deploymentConfig - "true", "false"
#userConfig - "true", "false"

#Kae Travis - Alkane Solutions Ltd
#15/11/2013
#Version 1.0 - Initial Release
#29/11/2013
#Version 2.0 - Added more error handling
#02/12/2013
#Version 3.0 - Bug fix when using deploymentConfig param
#29/01/2013
#Version 4.0 - Now mounts the packages, changed import-module message since it only imports for current session, cannot specify deploymentConfig and userConfig both as true

#declare variables for use throughout script

$scriptPath = Split-Path -parent $MyInvocation.MyCommand.Definition;
$avFile = $scriptPath + "\" + $av5PackageName + ".appv";
$cgfile = $scriptPath + "\" + $av5PackageName + ".xml";
$logfile = $env:windir + "\security\logs\" + $av5PackageName + "_" + $av5verb + ".log";

$appvmodule = "AppvClient";

#Import-Module imports a module only into the current session. 
#To import the module into all sessions, add an Import-Module command to your Windows PowerShell profile. 
#For more information about profiles, see about_Profiles (http://go.microsoft.com/fwlink/?LinkID=113729).

function detectAppvClientModule
{

	#detect if appvclient module is imported

	if(-not(Get-Module -name $appvmodule)) 
	{ 
		if(Get-Module -ListAvailable | Where-Object { $_.name -eq $appvmodule }) 
		{ 
			try
			{
				#module available, so import 
				Import-Module -Name $appvmodule -ErrorAction Stop | Out-Null;				
				Add-Content $logfile -value "'$appvmodule' module has been imported for this session.`r`n`r`n";
				return $true;
			}
			catch
			{
				Add-Content $logfile -value "Error: Unable to import '$appvmodule'.`r`n`r`n$($_.Exception.Message)";
				return $false;
			}
		} 
		else 
		{ 
			#module not available, so we write to log and exit script
			Add-Content $logfile -value "Error: Powershell module '$appvmodule' does not exist.  Is the App-V 5 Client installed?";
			return $false;
		} 
	}
	else
	{
		return $true;
	}
}

function targetFileExists
{
	#detect if target file (.app package, .xml connection group exists)
	Param(
	  [string]$targetFile
	)

	if (Test-Path $targetfile)
	{
		return $true;
	}
	else
	{
		Add-Content $logfile -value "Error: '$targetfile' does not exist.";
		return $false;
	}	

}

function addPackage
{

	#attempt to add/publish application

	try {
		#if success we write results to log
		if (targetFileExists($avfile) -eq $true)
		{

			if ($deploymentConfig -eq "true")
			{
				$deploymentFile = $scriptPath + "\" + $av5PackageName + "_DeploymentConfig.xml";

				if (targetFileExists($deploymentFile))
				{
					Set-AppvClientConfiguration -EnablePackageScripts $true -ErrorAction Stop | Out-Null;
				} else {
					#otherwise we write exception to log
					Add-Content $logfile -value "Error: Cannot add/publish package.`r`n`r`nDeployment config file does not exist";
					return $false;
				}
			}
			else
			{
				$deploymentFile = "";
			}

			if ($userConfig -eq "true")
			{
				$userFile = $scriptPath + "\" + $av5PackageName + "_UserConfig.xml";

				if (targetFileExists($userFile))
				{
					Set-AppvClientConfiguration -EnablePackageScripts $true -ErrorAction Stop | Out-Null;
				} else {
					#otherwise we write exception to log
					Add-Content $logfile -value "Error: Cannot add/publish package.`r`n`r`nUser config file does not exist";
					return $false;
				}
			}
			else
			{
				$userFile = "";
			}

			if (($userConfig -eq "true") -And ($deploymentConfig -eq "true"))
			{
				Add-Content $logfile -value "Error: Cannot add/publish a package in both user and machine contexts.  userConfig and deploymentConfig cannot both be set to true.";
				return $false;
			}
			elseif (($userConfig -eq "true") -And ($deploymentConfig -ne "true"))
			{
				Add-AppvClientPackage -Path $avfile -ErrorAction Stop | Publish-AppvClientPackage -DynamicUserConfigurationPath $userFile -ErrorAction Stop | Mount-AppvClientPackage | Out-File -FilePath $Logfile | Out-Null;
			}
			elseif (($userConfig -ne "true") -And ($deploymentConfig -eq "true"))
			{
				Add-AppvClientPackage -Path $avfile -DynamicDeploymentConfiguration $deploymentFile -ErrorAction Stop | Publish-AppvClientPackage -Global -ErrorAction Stop | Mount-AppvClientPackage | Out-File -FilePath $Logfile | Out-Null;
			}
			elseif (($userConfig -ne "true") -And ($deploymentConfig -ne "true"))
			{
				Add-AppvClientPackage -Path $avfile -ErrorAction Stop | Publish-AppvClientPackage -Global -ErrorAction Stop | Mount-AppvClientPackage | Out-File -FilePath $Logfile | Out-Null;
			}

			return $true;
		}
	} 
	catch 
	{
		#otherwise we write exception to log
		Add-Content $logfile -value "Error: Cannot add/publish package.`r`n`r`n$($_.Exception.Message)";
		return $false;
	}

}

function removePackage
{
	#attempt to add/publish application

	try {

		if(Get-AppvClientPackage -Name $av5PackageName) 
		{ 
			#if success we write results to log
			if ($userConfig -eq "true")
			{
				Unpublish-AppvClientPackage -name $av5PackageName -ErrorAction Stop | Remove-AppvClientPackage -ErrorAction Stop | Out-File -FilePath $Logfile | Out-Null;
			}
			else
			{		
				if((Get-AppvClientPackage -Name $av5PackageName).IsPublishedGlobally) 
				{ 
					Unpublish-AppvClientPackage -name $av5PackageName -Global -ErrorAction Stop | Remove-AppvClientPackage -ErrorAction Stop | Out-File -FilePath $Logfile | Out-Null;
				}
				else
				{
					Unpublish-AppvClientPackage -name $av5PackageName -ErrorAction Stop | Remove-AppvClientPackage -ErrorAction Stop | Out-File -FilePath $Logfile | Out-Null;
				}	
			}
			return $true;
		}
		else
		{
			#otherwise we write exception to log
			Add-Content $logfile -value "Error: Cannot unpublish/remove package.  Package does not exist.";
			return $false;
		}
	} 
	catch 
	{
		#otherwise we write exception to log
		Add-Content $logfile -value "Error: Cannot unpublish/remove package.`r`n`r`n$($_.Exception.Message)";
		return $false;
	}

}

function addConnectionGroup
{
	#attempt to add/enable connection group

	try {
		#if success we write results to log
		if (targetFileExists($cgfile) -eq $true)
		{		

			#we check that all the apps in the cg are published, and that they are all published in the same context (user/global)

			[xml]$av5Xml = Get-Content $cgfile;

			$av5Namespace = new-object Xml.XmlNamespaceManager $av5Xml.NameTable
			$av5Namespace.AddNamespace("appv", "http://schemas.microsoft.com/appv/2010/virtualapplicationconnectiongroup")

			$cgUnpublished = "";
			$cgGlobalCount = 0;
			$cgUserCount = 0;
			$cgUnpublishedApp = $false;

			$av5packages = $av5Xml.SelectNodes("/appv:AppConnectionGroup/appv:Packages/appv:Package",$av5Namespace)
			foreach ($av5package in $av5packages) {

			 	if(Get-AppvClientPackage -PackageId $av5package.PackageId -VersionId $av5package.VersionId)
				{
					if((Get-AppvClientPackage -PackageId $av5package.PackageId -VersionId $av5package.VersionId).IsPublishedGlobally)
					{
						$cgGlobalCount = $cgGlobalCount + 1;
					}
					else
					{
						$cgUserCount = $cgUserCount + 1;
					}
				}
				else
				{
					$cgUnpublished = "PackageId: " + $av5package.PackageId + " VersionId: " + $av5package.VersionId + "`r`n"
					$cgUnpublishedApp = $true;
				}
			}

			#if we get to this point, all packages in cg are published.  we should check they are published in same context now.

			if ($cgGlobalCount -gt 0 -And $cgUserCount -eq 0 -And $cgUnpublishedApp -eq $false)
			{
				#all pubbed globally

				if ($userConfig -eq "true")
				{
					Add-Content $logfile -value "Error: Connection Group should be published in a global context!";
					return $false;
				}
			}

			if ($cgGlobalCount -eq 0 -And $cgUserCount -gt 0 -And $cgUnpublishedApp -eq $false)
			{
				#all pubbed to user
				if ($userConfig -ne "true")
				{
					Add-Content $logfile -value "Error: Connection Group should be published in a user context!";
					return $false;
				}
			}

			if ($cgGlobalCount -gt 0 -And $cgUserCount -gt 0 -And $cgUnpublishedApp -eq $false)
			{
				#mixture of global and user packages
				Add-Content $logfile -value "Error: Packages in this Connection Group are published in both global and user contexts.  They should all be published in the same context!";
				return $false;
			}

			if ($cgUnpublishedApp)
			{
				#unpublished apps				
				Add-Content $logfile -value "Error: The following packages in this Connection Group have not been published:`r`n`r`n$cgUnpublished";
				return $false;
			}

			#add and publish the connection group

			if ($userConfig -eq "true")
			{
				Add-AppvClientConnectionGroup -path $cgfile -ErrorAction Stop | Enable-AppvClientConnectionGroup -ErrorAction Stop | Out-File -FilePath $Logfile | Out-Null;
			}
			else
			{
				Add-AppvClientConnectionGroup -path $cgfile -ErrorAction Stop | Enable-AppvClientConnectionGroup -Global -ErrorAction Stop | Out-File -FilePath $Logfile | Out-Null;
			}
			return $true;
		}
	} 
	catch 
	{
		#otherwise we write exception to log
		Add-Content $logfile -value "Error: Cannot add/enable connection group.  Are all packages in the connection group published to the same context?  And are you enabling this connection group in the same context as the packages? (Global or User)`r`n`r`n$($_.Exception.Message)";
		return $false;
	}
}

function removeConnectionGroup
{

	#attempt to disable/remove connection group

	try {

		if(Get-AppvClientConnectionGroup -Name $av5PackageName) 
		{
			#if success we write results to log
			if((Get-AppvClientConnectionGroup -Name $av5PackageName).IsEnabledGlobally) 
			{
				Disable-AppvClientConnectionGroup -name $av5PackageName -Global -ErrorAction Stop | Remove-AppvClientConnectionGroup -ErrorAction Stop | Out-File -FilePath $Logfile | Out-Null;
			}
			elseif((Get-AppvClientConnectionGroup -Name $av5PackageName).IsEnabledToUser) 
			{
				Disable-AppvClientConnectionGroup -name $av5PackageName -ErrorAction Stop | Remove-AppvClientConnectionGroup -ErrorAction Stop | Out-File -FilePath $Logfile | Out-Null;
			}
			else
			{
				#if not enabled at all!
				Remove-AppvClientConnectionGroup -name $av5PackageName -ErrorAction Stop | Out-File -FilePath $Logfile | Out-Null;			
			}

			return $true;
		}
		else
		{
			#seems to be a bug whereby if a cg is added but not enabled, Get-AppvClientConnectionGroup -Name $av5PackageName doesnt find it!
			#if this is the case, we will try to just remove (and not disable) the connection group here
			try
			{
				Remove-AppvClientConnectionGroup -name $av5PackageName -ErrorAction Stop | Out-File -FilePath $Logfile | Out-Null;
				return $true;				
			} catch {
				#otherwise we write exception to log
				Add-Content $logfile -value "Error: Cannot unpublish/remove connection group.  Connection group does not exist.";
				return $false;
			}			
		}
	} 
	catch 
	{
		#otherwise we write exception to log
		Add-Content $logfile -value "Error: Cannot disable/remove connection group.`r`n`r`n$($_.Exception.Message)";
		return $false;
	}
}

#main program

#assign a default value
$av5Result = $false;

#create blank log file
Set-Content $logfile -value "";

#validate arguments

if ($av5Verb -ne "add" -and $av5Verb -ne "remove")
{
	Add-Content $logfile -value "Error: av5Verb parameter should be ""Add"" or ""Remove""";
	Exit 1
}

if ($av5Object -ne "package" -and $av5Object -ne "connectiongroup")
{
	Add-Content $logfile -value "Error: av5Object parameter should be ""package"" or ""connectiongroup""";
	Exit 1
}

if ($av5PackageName -eq "")
{
	Add-Content $logfile -value "Error: av5PackageName parameter must be provided";
	Exit 1
}

if ($deploymentConfig -ne "")
{
	if ($deploymentConfig -ne "true" -and $deploymentConfig -ne "false")
	{
		Add-Content $logfile -value "Error: deploymentConfig parameter should be ""true"" or ""false""";
		Exit 1
	}
}

if ($userConfig -ne "")
{
	if ($userConfig -ne "true" -and $userConfig -ne "false")
	{
		Add-Content $logfile -value "Error: userConfig parameter should be ""true"" or ""false""";
		Exit 1
	}
}

if (detectAppvClientModule -eq $true)
{
	if ($av5Verb -eq "add")
	{
		if ($av5Object -eq "package")
		{
			$av5Result = addPackage;
		}
		elseif ($av5Object -eq "connectiongroup")
		{
			$av5Result = addConnectionGroup;
		}
	}
	elseif ($av5Verb -eq "remove")
	{	
		if ($av5Object -eq "package")
		{
			$av5Result = removePackage;
		}
		elseif ($av5Object -eq "connectiongroup")
		{
			$av5Result = removeConnectionGroup;
		}
	}
}
else
{
	#exit with error
	Exit 1
}

if ($av5Result -eq $true)
{
	#exit with no error
	Exit 0
}
else
{
	#exit with error
	Exit 1
}