In the complicated and dynamic world of applications, Enterprises continued to find unique and interesting ways to approach software delivery with System Center Configuration Manager.  During the development of Package Conversion Manager (PCM), we quickly determined that one area that was ripe for investment was in helping customers to take advantage of PCM when using custom wrappers.

Custom wrappers are defined as non-MSI based capabilities such as scripts or executable that usually provide capabilities above and beyond what Configuration Manager offered.  With System Center Configuration Manager 2012, the application model provides much of the robustness that were found in these custom wrappers.  The PCM team wanted to ensure that we provided a low-cost solution for IT professionals (or developers) to automate and integrate with PCM’s analysis engine to allow wrappers to work effectively for migration.

By default, PCM’s analysis engine will interrogate programs and determine if it is MSI and if not will mark any non-MSI as Manual assuming AnalysisEngineRulesit meets 3 rules outlined in the below.  If the program has content, is a software distribution package, and contains at least 1 program it is marked as manual.

What does this mean for you?  If you are using a wrapper that is a script (.vbs) or exe then you will show all manual.

The PCM team was aware of this huge need and thus we approached this in a manner where we provide you a “hook” that will interact with our analysis engine.  The hook allows you to write a script, or code, that will interrogate your wrapper to pull out key pieces of data such as detection method or manipulate metadata (like contact help desk information) – and do it only once.

Thus, the plug in doesn’t just magically solve your problem.  The experts in wrappers is you and your team and we focused on enabling you to use this expert knowledge to develop a wrapper that would interrogate your “system” and figure out what needs to be extracted and provide it to the PCM engine.  This serialize & de-serialize process makes it very robust and capable of handling a multitude of scenarios.

Developing your Plug In:  A Sample to get Started

To get you started, I’m providing an attachment that has a set of applications that you will need to import into your Configuration Manager 2012 system.  The first step is to download using the link below included with this post.  It is called HRBusinessApps.zip.  Afterwards, let’s import then so let’s open the Administrator console and do the following -

  1. Click the Software Library Wonderbar
  2. Click Packages
  3. Right-click, select Folder, and then Create Folder and name it HR
  4. Click HR folder
  5. In the Ribbon, click Import
  6. Click Browse and locate the HRBusinessApps.zip downloaded from my blog
  7. Click Next
  8. Click Close

After completing it, you should see the summary and completion success.

image

Source Path:  Editing to location of Test MSI’s

I’ve provided as a sample a base Test MSI that you can download below in attachment MSISimple.zip.  This zip contains a simple MSI with a product ID and the association.  It is for testing purposes only.  The next step is to download it and put on your network (UNC) and then edit the source path for the imported packages.

Download & Put on Network Share
  1. Download to your local machine
  2. Upload to a share on the network
Edit the Program Source for the HR Business Apps

Because you are importing from a different environment, you will need to change the properties of the programs to point to the test MSI.  This is rather trivial and I outline the steps below.

  1. Click the Software Library Wonderbar
  2. Click Packages
  3. Click HR
  4. Right-click on the first package, click Properties
  5. Click the Data Source tab
  6. Under the “This package contrains source files, click Set
  7. Select Network path (UNC name)
  8. Enter the UNC path to the location \\server\share\ to the Simple MSI.  (EX:  \\Server\Test MSIs\MSI – Simple)

After this, you are ready to get started creating your plug-in and the first thing to make sure you see is that you analyze the packages and they return manual.

Plug In:  Why do I need to do this?

For this sample, I’ve provided you a PowerShell script that has been built by our development team.  The purpose of the powershell script is to interrogate information stored in XML streams that are utilized by Wrapper called “SoftwareInstaller.exe”.

You can either use the scripts which are included in the zip file available for download from this blog or create two new PowerShell scripts as outlined below.

  1. PlugInAppModel.ps1
  2. SharedFunctions.ps1

NOTE: If the Admin console is installed to someplace other than the default location you will need to modify the first entry in the SharedFunctions.ps1 to point to the appropriate location.

The code to copy is the following- PlugInAppModel.ps1

 
#   Microsoft Corporation.
#
#   PCM Plug-In Sample
#
#   This Sample modifies the application to be created,
#   it shows the way to alter the information such as 
#   Display Name, change the Deployment Type Priority, 
#   and provide a Detection Method.
#
#   Input Parameters:
#   packageID FullFileName SiteServerName
#
#   Example Call:
#
#   PS > .PCMPlugIn.ps1 PKGID0001 C:\3213321-3313213-32313.xml SCCMMachinName 
 
#Read Input parameters
$packageId      = $args[0]
$fullFileName   = $args[1]
$sccmServerName = $args[2]
 
$currentPath= Split-Path $MyInvocation.MyCommand.Path
import-module "$currentPath\SharedFunctions.ps1"
 
try
  {
     #Loads Application Model into Application Domain
    LoadSccmAssemblies
 
    #Deserialize XML file
    $applicaton = GetApplicationObject $fullFileName
 
    #Modifies Application Name
    $applicaton.Title = $applicaton.Title + "_ModifiedByPlugIn"
    $applicaton.DisplayInfo.Current.Title = $applicaton.DisplayInfo.Current.Title + "_ModifiedByPlugIn"
 
    #If there is any Deployment Type change its Installer properties
    if($applicaton.DeploymentTypes.Count -gt 0)
    {
        VerifyEmptyProductCode $applicaton.DeploymentTypes
        $deploymentType = $applicaton.DeploymentTypes[0]
 
        $deploymentType.Installer.InstallCommandLine = 'MSIExec.exe /i "SimpleMSI.msi" /q'
        $deploymentType.Installer.UninstallCommandLine = 'MSIExec.exe /uninstall "SimpleMSI.msi"'
 
        $deploymentType.Installer.DetectionMethod = [Microsoft.ConfigurationManagement.ApplicationManagement.DetectionMethod]::Enhanced
        Create-EnhancedDetectionMethod $deploymentType.Installer $sccmServerName    
 
        #Add a dependency to a predefined Application
        AddDTPreExistantDependency "Automatic01_ModifiedByPlugin" $sccmServerName $deploymentType
 
        #Adds a Predefined Requirement to the DT
        Add-DTRequirements $deploymentType "Test setting" "Test Value" $sccmServerName
    }
 
    #Change DT Priority
    ChangeDTPriority($applicaton)
 
    #Add Custom Predifined DT
    Add-DeploymentType $applicaton "Custom Deployment Type" $sccmServerName
 
 
    #Saves Modified Application into the same File
    SaveApplicationObject $applicaton $fullFileName
 
    #If everything was executed successfuly exit with 0
    Write-Host "The PCM Plug IN executed successfuly"
    exit 0
 
  }catch
 
  {
    #If we need to report a problem with the plug in exit with 1
    Write-Host "The PCM Plug IN executed with errors"
    exit 1
  }
 
 
 

The code to copy is the following – SharedFunctions.ps1

$sccmPath = "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\"
 
function Get-AuthoringScope($siteServerName)
{
  $returnValue = [string]::Empty  
  $connectionManager = new-object Microsoft.ConfigurationManagement.ManagementProvider.WqlQueryEngine.WqlConnectionManager
 
  if($connectionManager.Connect($siteServerName))
  {
      $result = $connectionManager.ExecuteMethod("SMS_Identification","GetSiteID", $null) 
      $returnValue = $result["SiteID"].StringValue.Replace("{", [string]::Empty)
      $returnValue = $returnValue.Replace("}", [string]::Empty)
 
      $returnValue = "ScopeId_" + $returnValue
      $connectionManager.Dispose()
  }
 
  $returnValue
}
 
function Get-ExecuteWqlQuery($siteServerName, $query)
{
  $returnValue = $null
  $connectionManager = new-object Microsoft.ConfigurationManagement.ManagementProvider.WqlQueryEngine.WqlConnectionManager
 
  if($connectionManager.Connect($siteServerName))
  {
      $result = $connectionManager.QueryProcessor.ExecuteQuery($query)
 
      foreach($i in $result.GetEnumerator())
      {
        $returnValue = $i
        break
      }
 
      $connectionManager.Dispose() 
  }
 
  $returnValue
}
 
 
function Create-EnhancedDetectionMethod($installer, $siteServerName)
{
    $authScope = Get-AuthoringScope $siteServerName
    $refName = "SettingRef_" + [Guid]::NewGuid().ToString()
 
    $cfgItemType = [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItemPartType]::File
    $setting = Create-EDMSetting $cfgItemType
 
    $logicalName = $setting.LogicalName
 
    $dataType = [Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.ScalarDataType]::GetDataTypeFromDotNetTypeName("String")
    $settingSourceType = [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItemSettingSourceType]::File
    $settingReference = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.SettingReference -ArgumentList $authScope, $refName, 0, $logicalName, $dataType, $settingSourceType , $false
 
    $settingReference.MethodType = [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItemSettingMethodType]::Count
 
    $constantValue = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.ConstantValue -ArgumentList "0",$dataType
 
    $operands = new-object "Microsoft.ConfigurationManagement.DesiredConfigurationManagement.CustomCollection``1[Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.ExpressionBase]"
    $operands.Add($settingReference)    
    $operands.Add($constantValue)
 
    $expressionOperator = [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ExpressionOperators.ExpressionOperator]::NotEquals
    $rootExp = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.Expression -ArgumentList $expressionOperator, $operands
 
    $installer.EnhancedDetectionMethod = new-object Microsoft.ConfigurationManagement.ApplicationManagement.EnhancedDetectionMethod
 
    $installer.EnhancedDetectionMethod.Settings.Add($setting)
 
    $severity = [Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.NoncomplianceSeverity]::Critical
    $installer.EnhancedDetectionMethod.Rule = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.Rule -ArgumentList "exp1", $severity , $null, $rootExp
}
 
function Create-EDMSetting([Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItemPartType]$configItemType)
{
    if($configItemType -eq [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItemPartType]::File)
    {
        $setting = new-object Microsoft.ConfigurationManagement.DesiredConfigurationManagement.FileOrFolder -ArgumentList $configItemType, $null
        $setting.FileOrFolderName = "TestFile.txt" 
        $setting.Path = "C:\ProgramFiles\SomeSoftware"
    }
 
    $setting
}
 
 
#Deserialize XML into Application
function GetApplicationObject([string]$xmlFilePath)
{
   $fileContent = get-content $xmlFilePath -readcount 0   
  [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::DeserializeFromString($fileContent,$false)
}
 
function SaveApplicationObject($application, [string]$fullFileName)
{ 
 
  if($applicaton -ne $null)
  {
    [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::Serialize($application, $false).Save($fullFileName)    
  }
}
 
 
#Load Sccm Assemblies
function LoadSccmAssemblies()
{
    $filesToLoad = "Microsoft.ConfigurationManagement.ApplicationManagement.dll","AdminUI.WqlQueryEngine.dll", "AdminUI.DcmObjectWrapper.dll" 
 
    Set-Location $sccmPath
    [System.IO.Directory]::SetCurrentDirectory($sccmPath)
 
    foreach($fileName in $filesToLoad)
    {
       $fullAssemblyName = [System.IO.Path]::Combine($sccmPath, $fileName)
       if([System.IO.File]::Exists($fullAssemblyName ))
       {   
           $FileLoaded = [Reflection.Assembly]::LoadFrom($fullAssemblyName )
       }
       else
       {
            Write-Host ([System.String]::Format("File not found {0}",$fileName )) -backgroundcolor "red"
       }
    }
}
 
#Changes the Deployment Type Priority
function ChangeDTPriority($application)
{
    if($application.DeploymentTypes.Count -gt 1)
    {    
        $reorderedDT = new-object "Microsoft.ConfigurationManagement.ApplicationManagement.NamedObjectCollection``1[[Microsoft.ConfigurationManagement.ApplicationManagement.DeploymentType]]"
 
        for($i=$application.DeploymentTypes.Count-1; $i -ge 0; $i--)
        {
            #Inverse DT Priority on the firs two DTs
            $temp = $application.DeploymentTypes[$i]
            $application.DeploymentTypes.Remove($temp)
            $reorderedDT.Add( $temp )            
        }
 
        $application.DeploymentTypes = $reorderedDT
    }
}
 
 
function AddDTPreExistantDependency($appName, $siteServerName, $deploymentType)
{
 $app = Get-ApplicationObjectFromServer $appName $siteServerName
 
 if($app -ne $null )
    {
     $appAuthScope = $app.Scope
     $appLogicalName = $app.Name  
     $dtAppVersion = $deploymentType.Application.Version  
     $dtAuthScope = $app.DeploymentTypes[0].Scope 
     $dtLogicalName = $app.DeploymentTypes[0].Name 
     $dtVersion = $deploymentType.Version 
     $DTDesiredState = [Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.DeploymentTypeDesiredState]::Required 
 
     $intentExpression = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.DeploymentTypeIntentExpression -ArgumentList $appAuthScope, $appLogicalName, $dtAppVersion, $dtAuthScope, $dtLogicalName, $dtVersion, $DTDesiredState, $true
 
     $operands = new-object "Microsoft.ConfigurationManagement.DesiredConfigurationManagement.CustomCollection``1[[Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.DeploymentTypeIntentExpression]]"
     $operands.Add($intentExpression)
 
     $expression = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.DeploymentTypeExpression -ArgumentList ([Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ExpressionOperators.ExpressionOperator]::Or),$operands
 
     $anno =  new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.Annotation
     $anno.Description = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.LocalizableString -ArgumentList "Description", $appLogicalName, $null
     $anno.DisplayName = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.LocalizableString -ArgumentList "DisplayName", $appLogicalName, $null
 
     $rule = new-object "Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.DeploymentTypeRule" -ArgumentList ([Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.NoncomplianceSeverity]::Critical, $anno, $expression)
     $deploymentType.Dependencies.Add($rule)
    }
}
 
function Get-ApplicationObjectFromServer($appName,$siteServerName)
{    
    $resultObject = Get-ExecuteWqlQuery $siteServerName "select thissitecode from sms_identification" 
    $siteCode = $resultObject["thissitecode"].StringValue
 
    $path = [string]::Format("\\{0}\ROOT\sms\site_{1}", $siteServerName, $siteCode)
    $scope = new-object System.Management.ManagementScope -ArgumentList $path
 
    $query = [string]::Format("select * from sms_application where LocalizedDisplayName='{0}'", $appName.Trim())
 
    $oQuery = new-object System.Management.ObjectQuery -ArgumentList $query
    $obectSearcher = new-object System.Management.ManagementObjectSearcher -ArgumentList $scope,$oQuery
    $applicationFoundInCollection = $obectSearcher.Get()    
    $applicationFoundInCollectionEnumerator = $applicationFoundInCollection.GetEnumerator()
 
    if($applicationFoundInCollectionEnumerator.MoveNext())
    {
        $returnValue = $applicationFoundInCollectionEnumerator.Current
        $getResult = $returnValue.Get()        
        $sdmPackageXml = $returnValue.Properties["SDMPackageXML"].Value.ToString()
        [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::DeserializeFromString($sdmPackageXml)
    }
}
 
#Add Predifined DT
function Add-DeploymentType($application, $DTName, $siteServerName)
{
 
    $objectId= new-object Microsoft.ConfigurationManagement.ApplicationManagement.ObjectId -ArgumentList ([Guid]::NewGuid.ToString()), ("Transformed DT " + $DTName)
 
    $deploymentType = new-object Microsoft.ConfigurationManagement.ApplicationManagement.DeploymentType -ArgumentList $objectId , ([Microsoft.ConfigurationManagement.ApplicationManagement.MsiInstallerTechnology]::TechnologyId)
    $deploymentType.Title = $DTName;
    $deploymentType.Version = 1;
    $deploymentType.Enabled = $true
    $deploymentType.Name = "DeploymentType_" + [Guid]::NewGuid().ToString()
    $deploymentType.Description = "DT Added by PCM PLug In"
    #$deploymentType.Hosting = HostingTechnology.Registrar.First();
    $deploymentType.Scope = Get-AuthoringScope $siteServerName;
    #$deploymentType.Application.AutoInstall = $true
 
    $application.DeploymentTypes.Add($deploymentType)
 
 
    $deploymentType.Installer.InstallCommandLine = 'MSIExec.exe /i "SimpleMSI.msi" /q'
    $deploymentType.Installer.UninstallCommandLine = 'MSIExec.exe /uninstall "SimpleMSI.msi"'
 
    $deploymentType.Installer.DetectionMethod = [Microsoft.ConfigurationManagement.ApplicationManagement.DetectionMethod]::Enhanced
    Create-EnhancedDetectionMethod $deploymentType.Installer $sccmServerName    
 
}
 
function Add-DTRequirements($deploymentType, $requirementName, $requirementValue, $siteServerName)
{ 
     $deploymentType.Requirements.Clear()
 
     $setting = Get-DcmSettingReference $requirementName $siteServerName
 
     $operands = new-object "Microsoft.ConfigurationManagement.DesiredConfigurationManagement.CustomCollection``1[[Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.ExpressionBase]]"
     $operands.Add($setting)
 
     $constant = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.ConstantValue($requirementValue, [Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.DataType]::String)
     $operands.Add($constant)
 
     $expression = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.Expression -ArgumentList ([Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ExpressionOperators.ExpressionOperator]::IsEquals), $operands
     $appLogicalName = $requirementName + " Equals " + $requirementValue
     $anno =  new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.Annotation 
     $anno.DisplayName = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.LocalizableString -ArgumentList "DisplayName", $appLogicalName, $null
 
     $rule = new-object "Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.Rule" -ArgumentList ( ("Rule_" + [Guid]::NewGuid().ToString()), [Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.NoncomplianceSeverity]::None, $anno, $expression)
     $deploymentType.Requirements.Add($rule)
 
}
 
 
function Get-DcmSettingReference($settingName, $siteServerName)
{     
    $referencedSetting = Create-DcmSetting $settingName $siteServerName
 
    Build-SettingReference $referencedSetting
}
 
function Build-SettingReference($setting)
{
     $authScope =  Get-AuthoringScope $siteServerName
     $gsrLogicalName = $setting.ParentConfigItem.LogicalName
     $dataType = [Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.DataType]::String
     $gsr = new-object  Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.GlobalSettingReference -ArgumentList $authScope, $gsrLogicalName, $dataType, $setting.LogicalName, ([Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItemSettingSourceType]::WqlQuery)
     $gsr
}
 
function Create-DcmSetting ($settingName, $siteServerName)
{
    #Create a DcmConnection
    $connectionManager = new-object Microsoft.ConfigurationManagement.ManagementProvider.WqlQueryEngine.WqlConnectionManager  
    $isConnected = $connectionManager.Connect($siteServerName)
    $dcmConnectionObj = new-object Microsoft.ConfigurationManagement.AdminConsole.DesiredConfigurationManagement.ConsoleDcmConnection -ArgumentList $connectionManager, $null
 
    #The object was implemented with explicit interfaces
    $dcmConnection = Get-Interface $dcmConnectionObj ([Microsoft.ConfigurationManagement.DesiredConfigurationManagement.IDcmStoreConnection])               
 
    [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.GlobalSettingsConfigurationItem]$gsci = [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.GlobalSettingsConfigurationItem]::CreateNew($dcmConnectionObj, [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItemType]::GlobalSettings)
 
    $gsci.Description = "Test Global Setting added by PCM Plug In"
    $gsci.Name = $settingName
    $gsci.Type = [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItemType]::GlobalSettings
    #set category ID: IS this always the same? as in hardcoded within SCCM?
    $gsci.CategoryIDs = [string[]] "GlobalCondition:08347928-D747-4b2e-B99C-94876C0D3D74"
    $setting = Get-Setting $gsci
    #add it to the ci
    $gsci.Settings.ChildSimpleSettings.Add($setting)
 
    #Assuming we want to add a new Global setting to Sccm.
    $newItem = $dcmConnection.StoreNewItem([Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItem]$gsci)
 
    $setting
}
 
function Get-Setting($parent)
{
    $setting = new-object Microsoft.ConfigurationManagement.DesiredConfigurationManagement.WqlQuerySetting $parent
 
    $setting.SettingDataType = [Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.DataType]::String
    $setting.WqlClass = "SMS_R_System";
    $setting.WqlNamespace = "ROOT\ccm";
    $setting.WhereClause = "";
    $setting.WqlProperty = "ClientType";
 
    $setting.Description = "SMS_R_System\ClientType"
    $setting.Name = "SMS_R_System\ClientType"
    $setting
 
}
 
 
#In order for the application to Serialize, the Installer.ProductCode is being set to " ",
#this is only when PCM is unable to locate the product code.
function VerifyEmptyProductCode($DeploymentTypes)
{
    foreach($i in $DeploymentTypes)
    {
        if($i.Installer.ProductCode -eq " ")
        {
           $i.Installer.ProductCode = [Guid]::NewGuid().ToString()
        }
    }
}
 
function Get-Interface {
 
#.Synopsis
#   Allows PowerShell to call specific interface implementations on any .NET object. , Workaround for Powershell Bug : 249840
#.Description
#   Allows PowerShell to call specific interface implementations on any .NET object. 
#
#   As of v2.0, PowerShell cannot cast .NET instances to a particular interface. This makes it
#   impossible (shy of reflection) to call methods on explicitly implemented interfaces.   
#.Parameter Object
#   An instance of a .NET class from which you want to get a reference to a particular interface it defines.
#.Parameter InterfaceType
#   An interface type, e.g. [idisposable], implemented by the target object.
#.Example
#   // a class with explicitly implemented interface   
#   public class MyObj : IDisposable {
#      void IDisposable.Dispose()
#   }
#   
#   ps> $o = new-object MyObj
#   ps> $i = get-interface $o ([idisposable])
#   ps> $i.Dispose()      
#.ReturnValue
#   A PSCustomObject with ScriptMethods and ScriptProperties representing methods and properties on the target interface.
#.Notes
#   LASTEDIT:  2009-03-25 11:35:23
 
    [CmdletBinding()]
    param(
        [ValidateNotNull()]
        $Object,
 
        [ValidateScript( { $_.IsInterface } )]
        [type]$InterfaceType
    )
 
    $private:t  = $Object.GetType()
 
    try {
 
        $private:m  = $t.GetInterfaceMap($InterfaceType)
 
    } catch [argumentexception] {
 
        throw "Interface $($InterfaceType.Name) not found on ${t}!"
    }
 
    $private:im = $m.InterfaceMethods
    $private:tm = $m.TargetMethods
 
    # TODO: use param blocks in functions instead of $args
    #       so method signatures are visible via get-member
 
    $body = [scriptblock]::Create(@"
        param(`$o, `$i)    
 
        `$script:t  = `$o.GetType()
        `$script:m  = `$t.GetInterfaceMap(`$i)
        `$script:im = `$m.InterfaceMethods
        `$script:tm = `$m.TargetMethods
 
        # interface methods $($im.count)
        # target methods $($tm.count)
 
        $(
            for ($ix = 0; $ix -lt $im.Count; $ix++) {
                $mb = $im[$ix]
                @"
                function $($mb.Name) {
                    `$tm[$ix].Invoke(`$o, `$args)
                }
 
                $(if (!$mb.IsSpecialName) {
                    @"
                    Export-ModuleMember $($mb.Name)
 
"@
                })
"@
            }
        )
"@)
   # write-verbose $body.tostring()    
 
    # create dynamic module
    $module = new-module -ScriptBlock $body -Args $Object, $InterfaceType 
 
    # generate method proxies - all exported members become scriptmethods
    # however, we are careful not to export getters and setters.
    $custom = $module.AsCustomObject()
 
    # add property proxies - need to use scriptproperties here.
    # modules cannot expose true properties, only variables and 
    # we cannot intercept variables get/set.
 
    $InterfaceType.GetProperties() | % {
 
        $propName = $_.Name
        $getter   = $null
        $setter   = $null
 
        if ($_.CanRead) {
 
            # where is the getter methodinfo on the interface map?
            $ix = [array]::indexof($im, $_.GetGetMethod())
 
            # bind the getter scriptblock to our module's scope
            # and generate script to call target method
            $getter = $module.NewBoundScriptBlock(
                [scriptblock]::create("`$tm[{0}].Invoke(`$o, @())" -f $ix))
        }
 
        if ($_.CanWrite) {
 
            # where is the setter methodinfo on the interface map?
            $ix = [array]::indexof($im, $_.GetSetMethod())
 
            # bind the setter scriptblock to our module's scope
            # and generate script to call target method
            $setter = $module.NewBoundScriptBlock(
                [scriptblock]::create(
                    "param(`$value); `$tm[{0}].Invoke(`$o, `$value)" -f $ix))
        }
 
        # add our property to the pscustomobject
        $prop = new-object management.automation.psscriptproperty $propName, $getter, $setter
        $custom.psobject.properties.add($prop)
    }
 
    # insert the interface name at the head of the typename chain (for get-member info)
    $custom.psobject.TypeNames.Insert(0, $InterfaceType.FullName)
 
    # dump our pscustomobject to pipeline
    $custom
}

Lastly, store these files in a location that you note as you will need to change the configuration for the Plug In in a later step.

Housekeeping:  Configuring PCM “Host” for PowerShell Execution

Prior to moving to setup and execution phase, we need to do two pieces of key changes to your system.  This is because we decided to build this PowerShell using the .NET 4.0 framework rather than 2.0 as well as to have you change the Execution Policy.

Create & Edit x86 PowerShell Configuration File

In order to utilize PowerShell from within the Configuration Manager console, you must create a new configuration file called powershell.exe.config and store it in the following path - C:\Windows\SysWOW64\WindowsPowerShell\v1.0.

<?xml version="1.0"?>
<configuration>
    <startup useLegacyV2RuntimeActivationPolicy="true">
        <supportedRuntime version="v4.0.30319"/>
        <supportedRuntime version="v2.0.50727"/>
    </startup>
</configuration>

This allows you to execute the PowerShell that I’ve provided above.

NOTE:  In order do get SPA to work with the PCM Plugin you will need to configure a few additional things. However, this will be covered in a different blog post.

Change Execution Policy to Unrestricted

In order to execute the PowerShell you’ve created above, you will need to change the value for the default execution-policy.  It is very important to do this with the x86 version of PowerShell.  The easiest method on Windows 2008/7 is to do the following-

  1. Click Start
  2. In Search box, enter PowerShell
  3. It should have Windows PowerShell (x86), right-click and select Run As Administrator
  4. Click Yes to elevate

image

To do this, please open PowerShell and issue the following command-

Set-ExecutionPolicy Unrestricted

This should allow your PowerShell scripts to run when executed within the context of PCM.

NOTE:  These changes are only for the temporary purposes of this blog.  I’d not recommend them in a production environment and you should look into PowerShell script signing capabilities for future plug-in enhancements you make.

Changing Analysis Engine to utilize Plug-In

After you’ve created the plug-in, the next step is to enable the PlugIn to be utilized by the PCM Analysis Engine.  All the configuration such as logging trace levels and plug-in settings are stored in the Configuration Manager Administration Console’s configuration file.  A colleague of mine has put together a good blog post covering this topic here. However, we will delve into what you need to change in this post as well. Navigate to the location you installed your ConfigMgr console. This is normally C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin, but you may have installed it to a different location.  The configuration file is named Microsoft.ConfigurationManagement.exe.config.

image

Inside of this file you will need to follow these steps in order to configure the plug-in:

1. Add the following child node to configSections.

<section name="Microsoft.ConfigurationManagement.UserCentric.Workflow.Properties.Settings" 
               type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 

requirePermission="false" />

PCMpluginConfig1

2. Add in a UserCentric node in the ApplicationSettings section.

 

<Microsoft.ConfigurationManagement.UserCentric.Workflow.Properties.Settings>

</Microsoft.ConfigurationManagement.UserCentric.Workflow.Properties.Settings>

 

3. Add the rest of the settings within the UserCentric node just added.

 

      <setting name="PcmPlugIn" serializeAs="String">
        <!-- <value></value> -->
        <value>powershell.exe E:\PlugIn\PlugInAppModel.ps1</value>
      </setting>
      <setting name="PcmPlugInTimeoutMilliseconds" serializeAs="String">
        <value>90000</value>
      </setting>
      <setting name="PcmPluginExitCode" serializeAs="String">
        <value>0</value>
      </setting>
 

As you can see, I’ve got an element configuration that is defined in the <value /> setting for PcmPlugIn.  This calls PowerShell and loads the PlugIn.ps1. An example configuration would be <value>powershell.exe "C:\sample\PlugInAppModel.ps1"</value>.

 

PCMpluginConfig2

Execution Time:  Changing value to have threshold that accommodates your PlugIn

A key area of focus for you in your plug in is to understand exactly what the time it takes for execution.  There are a lot of variables that are required for you to take into account.  These include the speed of the machine as well whether it is making any network calls and the speed of the external resources.  It is important to note that the PCM analysis engine is spawning this process and will continue to “pause” while it is executing.  Thus, as part of the plugin model, we built functionality to say how long for the engine to “pause” and this is stored in the <value /> setting for PcmPlugInTimeoutMilliseconds.

For our example, I set it at 90000 to allow a 90 seconds for it to complete.  If it doesn’t complete with success and return a blog of data to the analysis engine this PCM will error out.

Copy New Configuration to BIN Directory

You will need to have administrative level access (UAC) to change the configuration file for Configuration Manager.  The simple way to effect this is to copy the file to your desktop (or wherever) and then edit the file.  You can then copy this file to the C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin directory.

image

Re-Executing using Plug In & Readiness State Change

The last step is to see the plug in in action.  This requires you to close the console, re-open with the new configuration in place, and then execute analysis.  The end result should be a package completes that was now manual and is instead completely automatic.

Before Plug-In:

This shows the packages/programs in the Manual state…

image

After Plug-In:

This shows the package/programs after running analyze with our plug-in…

image

Putting it all Together

Wow.  I can’t believe the length of this blog.  It has almost worn me out and I can’t imagine for those who are reading it.  Is it difficult to actually do the work for a plug-in?  No, but it is lengthy to share with you all the nuisances in a blog.  The goals of today’s blog was to help you better understand all the steps but better yet walk you through using the Plug-In to impact and change the readiness status.  As you should have witnessed, we were able to use a plug-in that interrogated our custom wrapper and pulled the detection method from it.  Before this plug-in, all packages were showing Manual meaning you would need to do them one-by-one.  With a plug-in, do the work once, migrate hundreds or thousands easily!

Enjoy!

Thanks,

Chris