SOLVED

Content Library Cleanup - System.NullReferenceException: Object reference not set to an instance of

Iron Contributor

Hi,

Short Story

we use a single server to host everything MECM.  

Our content library has a heap of entries that are greyed out and the majority of the entries appear to be from 5+ years ago for deployments we no longer have.

How do I clean out all the invalid entries from content library explorer

 

 

the Details

  I tried running the tool contentlibrarycleanup.exe in whatif mode, but after running for about an hour, it ends with this

 

Analyzing local files, 100.00% complete...
Loading package data from provider...
Loading local package data...
Loading distributed packages from provider...
System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.ConfigurationManager.ContentLibraryCleanup.CLContentLibrary.LoadOrphanData(Boolean whatIfMode)
at Microsoft.ConfigurationManager.ContentLibraryCleanup.CLContentLibrary.Cleanup(Boolean whatIfMode, Boolean quietMode, String logDir)
at Microsoft.ConfigurationManager.ContentLibraryCleanup.Program.Main(String[] args)

 

our environment is server 2019, sql 2019, MECM 2002

 

I found this document - https://docs.microsoft.com/en-us/mem/configmgr/core/plan-design/hierarchy/content-library-cleanup-to...

 

it has this entry which is a bit vague, saying it will cleanup the DP but not if the DP is on a site server??

 

"The tool only affects the content on the distribution point that you specify when you run the tool. The tool can't remove content from the content library on the site server."

 

the way I read this is a little vague, but it suggests to me that maybe, if you have a single primary Site server that also contains your DP, this tool is not able to assist with cleaning up the DP of invalid content. Is that correct?

 

Is there another option for this scenario if the tool doesn't support a single DP on the site server?

How do I get rid of all the invalid entries in my content library explorer?

 

16 Replies

@PaulKlerkx I attached an example of what I am talking about re orphaned files, showing one being an old office deployment.  On this page; https://www.reddit.com/r/SCCM/comments/dcwutx/content_library_cleanup_tool_orphaned_content/;  Jason Sandys states the following, but if they aren't orphaned, what are they?  

"It's not possible to have orphaned content in the site server's content library. Orphaned content happens in remote content libraries when content is not properly cleaned up because of one of a handful of reasons like connectivity loss."

 

this page - https://docs.microsoft.com/en-us/mem/configmgr/core/support/content-library-explorer suggests they can only be 3 things

  • The package is the Configuration Manager client upgrade. This package includes "ccmsetup.exe". (DEFINITELY NOT THIS - that is there too but appears as "INVALID" and different to the ones with asterixis )

  • Your user account can't access the package, likely due to role-based administration. For instance, the Application Author role can't see driver packages in the console, so any driver packages on the distribution point are marked as disabled. (NOT THIS - I AM MECM AND SERVER FULL ADMIN and have been for over 10 years)

  • The package is orphaned on the distribution point.

 

@PaulKlerkx Even with the DP role co-located on the site server, you should still be able to run the Content Library Cleanup Tool against it. You may be hitting a bug here. I'd suggest raising a support request with Microsoft to make sure.

@Michiel Overweel @ I logged a case with Microsoft and the tech went straight to the document I mentioned above that says the cleaner tool doesn't work when co-located on the site server and said it wouldn't work.  He is escalating to see if we have any other options for cleaning out the orphaned entries.  

@PaulKlerkx I would disagree with that statement... I am able to run the tool against my co-located site server/distribution point content library just fine. As a matter of fact, when I tried last week, it successfully deleted about 6 GB worth of orphaned software update-related content. Having said that, I also have a couple of these disabled (greyed-out) packages in my library, just like you do, and these weren't deleted. However, I didn't run into the error that you experienced either. In my opinion, even though it might not fix your problem, you should be able to run the tool successfully nevertheless.

@Michiel Overweel just a quick update that I haven't given up on this, I'm still waiting for a MS tech to get back to me about my support call. no progress on this so far.  I'll post back here when that happens.  

@Michiel Overweel - MS called this afternoon while I was grabbing a drink so I missed it.  I did a bit of a read through the doco on contentlibrarycleanup and found there is an option for a log file.  initially I figured that would just return what is shown in the cmd window, however for me it came back with one line. 

 

"Because this distribution point is co-located with its site server, packages may correctly exist in the content library that are not distributed to the distribution point. Package deletion has been disabled."

 

I don't understand that, in what situation would I correctly have a package in the content library that isn't distributed to a DP.  

 

I also figured out that if I locate the package ID referenced in content library explorer for a greyed out app, then look that up in content status, that will give me the app name.  If I then go and delete that application, that will remove it from content status in the console, however the greyed out entry remains in content library explorer so now I have an entry in content library explorer for an application that doesn't exist anywhere in my console.  I did a count too, I have 152 greyed out entries in content library explorer.  

@PaulKlerkx Interested if you get an update on this as I'm running into the same exact issue.

@Nick-86 , I got this from MS yesterday, will post results back here when available -

"Please be informed I will update you within next couple of days with an appropriate details for the case."

@Nick-86 - one other thing I found was that if I attempt to export an application from the console, I get a similiar message

"System.NullReferenceException
Object reference not set to an instance of an object."

 

I'm unsure if it is related, but being the same message, I'm now wondering if maybe there is a MECM dependency ( E.G. dotnet or WMI) that has a problem which is causing my issues.  

@PaulKlerkx Haven't tried to export an application but I can try that tomorrow when I log back in.

 

I'm currently trying to deal with an issue where my ADR for Windows 10 updates is creating thousands of content folders when it says there are only 433 updates. A lot of them look to be different languages so not sure if that's all it is but it's mind boggling. Need to get a handle on ADR best practices.

@Nick-86 your "issue" may actually be expected behaviour - https://docs.microsoft.com/en-us/answers/questions/139391/high-number-of-definition-updates-in-deplo... - our Win10 ADR deployment package contains 49 items, SUG has 12 updates and our ADR content folder has 371 directories. 

@Michiel Overweel @Nick-86 - I got my answer from Microsoft so posting here.  basically told that it was too hard to fix and they will refund for our case.  Seems like maybe related to some sort of corruption and suggested I would need a SQL query to go through each entry in the content library and clean out of the DB, but the query was too complicated for them to do.  

**********************

Last email from Microsoft....

 

I apologize for the delayed response. We wouldn't be able to prepare the query as it needs the testing for all the package's (More than 150 Packages) and it would take months to check and then deliver the result to you.

I would ask my manager to approve the refund on this case as we were not able to resolve your query.

Microsoft thanks for your time and patience in this matter.
**********************
I'll focus on getting our retired apps processed - nearly have a script completed that will do that which may help get rid of some of the corruption. Then I'll come back to this later. 

best response confirmed by PaulKlerkx (Iron Contributor)
Solution
Short story
- According to Microsoft Support, the content Library cleanup tool logs and from my experience, the Content Library Cleanup Tool Does not work for a single server set up. (or single server with CMG)
- Greyed out entries in Content Library explorer indicate EITHER MECM having a record of content in the Database, but there isn't any content actually there in the content library OR the applications are retired.

After going through all this, I located a script that assisted with retiring some of our applications which took me several weeks to get the way I liked and run for each application. Part of the script is to rename the application which made it very easy to identify them in the content library explorer.
I still get the error message when running content library cleanup tool with the message in the logfile being "Because this distribution point is co-located with its site server, packages may correctly exist in the content library that are not distributed to the distribution point. Package deletion has been disabled."
What I found when running my retirement script is that a large number of our retired applications didn't have content on the DP's or DPG's and the original content had also been deleted or moved. I believe our move to a restored server highlighted these issues in the content library explorer and by running my script which cleans any content from the Dp's and DPG's as well as updating the content path, this has cleared up most of the issues being seen.


@PaulKlerkx Would you be willing to share these scripts that you used? 

@sbridges7500,
Before I start, as all the posted scripts say - no warranty, use at your own risk, test first etc etc.  
The script I started with was at https://github.com/nickrod518/PowerShell-Scripts/blob/master/SCCM/Suspend-CMApplicationGUI.ps1 which is now a broken link, but it looks like it is now at https://github.com/nickrod518/PowerShell-Scripts/blob/master/SCCM/Retire-CMApplicationGUI.ps1 and depending on your requirements, that may be all you need.  (some of my notes may detail me changing stuff that didn't work but that was based on the old script, the new script may be different)

I have sanitized my revision to remove any inhouse references, and left in any notes I added as I was going so you can see some of what was changed and there are also some ideas about how to improve. The main things you'll need to modify are

  1. # Run the following command to list all the possible paths for all applications. (set the log file to what suits you first) - depending on how may apps you have, it may take a couple of hours, so run it and come back to it later. (run in MECM console powershell ise) This is also a good way to find random locations people might have used for application deployment files.  
    #Get-CMApplication | Get-CMDeploymentType | ForEach-Object {$xml = [xml]$_.SDMPackageXML; $xml.AppMgmtDigest.DeploymentType.Installer.Contents.Content.Location} | Out-File D:\MECM-AllContentPaths.log
  2. Once you have that output, In the script, look for "$SourceDir -match [Regex]::Escape(" and you'll find a section where I have added all the various paths to where our applications might live (sanitized). Use the output from the previous command to set your file paths as appropriate for your environment and delete any non-relevant elseif's
  3. Look through the section "The Script" - set the variables for $ScriptVersion ; $Year; $DestDirRoot ; $LogFilePath ; $LogFileName ; $LogFileName2
  4. Search for Site code entries "XXX" and replace with your own site code

You'll notice the script puts things into year folders, this allows us to keep old applications and the installation files scripts etc for a bit longer in case we realize they need to be reinstated.  Then when we run this each January, we just delete the oldest year retired app folders as part of that.  

  

We have a bit of an in-house process to how we deal with retirement.  The image below is how we have our retired applications - when we supersede an app, the old app gets moved to "Retired or Superseded" and just left there pending full retirement. Every January, they get moved to a year folder and the applications in the previous year folder is what we target with this script.  This means they all get at least a full year for any supersedence to complete before any clean-up happens.  

PaulKlerkx_0-1652137890659.png

I'm no powershell Guru, so there will be improvements that could be done. Hope it is of use.

 

 #####################################################################
#* NOTE   IF THERE ARE SPECIAL CHARACTERS IN THE NAME OF AN APPLICATION, THIS WILL CURRENTLY FAIL - BRACKETS, INVERTED COMMA'S ETC
#####################################################################
<#
.Synopsis
	Used to retire applications in MECM.
.DESCRIPTION
	Will open a GUI where you can paste the name of an MECM application to be processed to retirement.  
	See https://mitownsville.service-now.com/kb_view.do?sysparm_article=KB0000475 for documentation. 
.EXAMPLE
	before using each year, change the year and the Script version in the body of the script (near bottom)
	to use this script, connect to powershell ISE in the MECM console
	Open this script and run
	copy paste the name of the app you are targeting into the window presented.  
	Click the retire button.
.INPUTS
	Paste in the Name of an MECM Application
.OUTPUTS
	Issues will be logged
	General Processing output is logged only to the GUI window. 
.NOTES
	Original script from https://github.com/nickrod518/PowerShell-Scripts/blob/master/SCCM/Suspend-CMApplicationGUI.ps1
	                     https://github.com/nickrod518/PowerShell-Scripts/blob/master/SCCM/Retire-CMApplicationGUI.ps1

	# Ran this to list all the possible paths for all applications. - takes a couple of hours, so run it and come back to it later.  (see Rev. V.05 below)
		#Get-CMApplication | Get-CMDeploymentType | ForEach-Object {$xml = [xml]$_.SDMPackageXML; $xml.AppMgmtDigest.DeploymentType.Installer.Contents.Content.Location} | Out-File C:\SOE\MECM-AllContentPaths.log  
#####################################################################
.FUNCTIONALITY
		V.02 	- removed the option to move to a MAC folder as that wasn't required.  
				- Added code to move source files to old superceded
		V.03 	- Added code to update Deployment type content path to point to new location in old Superceded.
		V.04 	- Code Tidy up
		V.05 	- Error Control - the script will now be able to deal with most of the common application source paths ( all known at time of writing script.  )
				- NOTE - run the command in the script Notes above to get a full list and compare to what paths are currently dealt with . 
		V.06 	- Error Control - used to error when the content didn't exist, it will now notify the content didn't exist and leave the content path of the DT the way it was. 
		V.07 	- UnRetire wasn't working, resolved that.  also added check to identify the deployment type technology in use as only MSI's worked, should now deal with MSI, Script and WebApp.  
		V.08 	- Deal with deployment type Technologies - process if MSI or Script Technology used, otherwise skip moving content.  
		V.09	- Created external log just to write issues to, should only be 1 or 2 lines per application.  
		V.10.01 - updated DP and DPG components to now show which DP and DPG is being targeted.  , added "  - " to beginning of each log output and added completion line of ***'s to show end. 
				- added a note to processing content component to show that it may fail if there is no content path
		V.10.02 - Ran Analyze Script and resolved issues
				- replaced Select with Select-Object; replaced GWMI with Get-WMIObject, changed function from Update-Log to Edit-Log for legal verb; changed function Create-Log to New-Log for legal Verb
				- changed function Create-UtilityForm to New-UtilityForm for legal Verb; changed function Append-LogFile to Update-LogFile for legal Verb Update-LogFile
				- changed function Retire-CMApplication to Suspend-ConfigMgrApplication for legal Verb; 
		V.10.03	- found the "left" function wasn't actually being used, so removed it. 	
		V.10.04	- Added a check for an empty content path, now script proceeds rather than failing if content path empty.  
		V.10.05 - Now including appx deployment type
		V.10.06 - added new path to list of possible home directories
				- added begin, process end to the suspend-application function.  
		V.10.07 - added new path to list of possible home directories.  
				- change this line " $LogTextBox.ReadOnly = 'True'" to " $LogTextBox.ReadOnly = 'False'" to give the ability to copy paths from window out to explorer.  
				- rearranged input area of message box to make input box and retire button larger and move retire to the right of the input where it feels more logical to be. 
		V.10.08 - added version number to input box name, updated notes for DT content path updates and fixed an issue with DT content path for CMScriptDT.  
				- Added Error Listing at end of script.  
		V.10.09	- added cleanup of revisions
				- deal with multiple deployment types - update content path.  
		V.10.10	- added some output for various error codes from robocopy.  
		v.10.11 - Added a Count to revisions to avoid processing if not needed.  
				- Added process block to suspend app function
		v2021.02.01 
				- just changed year to 2021		
		v2022.01.01 
				- added some new paths to list of possible home directories
				- Added General Log with logging to match windowed output.  
				- Added more output for when changing msi content path fails.  
		
#####################################################################
Things to add - More Error Control
 - Check for the year folder for logs and create it if it doesn't already exist (Note that this is the year of the retired apps you are dealing with, so can't use PS to auto add the year)
 - Check Destination Path of Applications - if it already exists, ...force overwrite or append a number to the end of the directory name maybe?
 - add severity to logging to make failures red and success green - possibly something here - https://adamtheautomator.com/powershell-log-function/
 - if there are special characters in the name of the application, it fails, see if able to do something about that - E.G. "My App (v1)" will fail because of the brackets. 
 
Things to add - Features
 - list any supersedence information - maybe at beginning so it can be dealt with prior to retiring???
     - is there any way to remove supersedence entries maybe??? - Get-CMDeploymentTypeSupersedence
 - list any dependency information - maybe at beginning so it can be dealt with prior to retiring???
      - is there any way to remove Dependency entries maybe??? Get-CMDeploymentTypeDependency and Get-CMDeploymentTypeDependencyGroup
 - replace Get-WMIObject with Get-CimInstance (WMI being deprecated) - (this isn't an option with CIM though - "appWMI.SetIsExpired") OR use Suspend-CMApplication instead
 - set-cmMSI... doesn't work to set the new content path for MSI Deployment Types - apparently this is a known thing - you need to specify the path to an existing MSI in the content path and when you do, 
   it is able to write that content path, however it will also wipe your current installation command and replace it with a default Installation command
 - figure out an optional method of having the input box grab from a list ( csv maybe) so rather than doing one at a time, we can target them in groups.
 - move paths for applications to body, outside function and use variables instead.  Maybe see if this can be done as an array instead.  
 #>

# references for building forms
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") 
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

# Update Logs 
function Edit-Log ($text) 
{
    $LogTextBox.AppendText("$text")
    $LogTextBox.Update()
    $LogTextBox.ScrollToCaret()
}

# user form
function New-UtilityForm 
{
    $objForm = New-Object System.Windows.Forms.Form 
    $objForm.Text = "MECM Application Retirement v$ScriptVersion"
    $objForm.Size = New-Object System.Drawing.Size(1460, 640) 
    $objForm.StartPosition = "CenterScreen"

    # Creates output log
    #https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.richtextbox?view=net-5.0
    $LogTextBox = New-Object System.Windows.Forms.RichTextBox
    $LogTextBox.Location = New-Object System.Drawing.Size(12, 50) 
    $LogTextBox.Size = New-Object System.Drawing.Size(1420, 500)
    $LogTextBox.ReadOnly = 'False'
    $LogTextBox.BackColor = 'Black'
    $LogTextBox.ForeColor = 'White'
    $LogTextBox.Font = 'Consolas'
    $objForm.Controls.Add($LogTextBox)

    # app retire button
    $AppRetireButton = New-Object System.Windows.Forms.Button
    $AppRetireButton.Location = New-Object System.Drawing.Size(350, 14)
    $AppRetireButton.Size = New-Object System.Drawing.Size(200, 22)
    $AppRetireButton.Text = "Retire"
    $AppRetireButton.Add_Click(
        { Script:Suspend-ConfigMgrApplication $appTextBox.Text })
    $objForm.Controls.Add($AppRetireButton)

    # app name input box
    $appTextBox = New-Object System.Windows.Forms.TextBox 
    $appTextBox.Location = New-Object System.Drawing.Size(15, 15) 
    $appTextBox.Size = New-Object System.Drawing.Size(320, 20)
    $objForm.Controls.Add($appTextBox)
    
    # clear log button
    $clearButton = New-Object System.Windows.Forms.Button
    $clearButton.Location = New-Object System.Drawing.Size(570, 14)
    $clearButton.Size = New-Object System.Drawing.Size(75, 22)
    $clearButton.Text = "Clear Log"
    $clearButton.Add_Click(
        { $LogTextBox.Clear() })
    $objForm.Controls.Add($clearButton)

    $objForm.Add_Shown({$objForm.Activate()})
    [void] $objForm.ShowDialog()
}

#Function to create a .log log file.  
	Function New-Log ($LogFile, $LogFilePath, $LogFileName)
	{
		Set-Location C:	
		# Create the logfile directory if it doesn't exist.  
		if (!(Test-Path -Path $LogFilePath))
			{
	            #create the log file folder if it doesn't exist
	            New-Item -path $LogFilePath -ItemType "Directory"
	        }

		    if (!(Test-Path -Path $LogFile))
		        {
		            #create the log if it doesn't exist
		            New-Item -path $LogFilePath -name $LogFileName -ItemType "file"
		        }
	   	Set-Location -Path XXX:
	}
	
Function Update-LogFile ($IssuesLog, $ErrorLevel, $App, $Message)
	{
	    Set-Location C:	
	    Add-Content $IssuesLog "$ErrorLevel $App $Message" 
	    Set-Location XXX:
	}

Function Update-GeneralLogFile ($GeneralLog, $App, $Message)
	{
	    Set-Location C:	
	    Add-Content $GeneralLog "$App $Message" 
	    Set-Location XXX:
	}

function Suspend-ConfigMgrApplication 
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $RetiringApps
    )
	# Process Block Contains the main functionality of the function. Required and used to specify the code that will continually execute on every object that might be passed to the function.
	process 
	{
    # for each provided app name, remove deployments, rename, and retire
    foreach ($app in $RetiringApps) 
    {
        if ($RetiringApp = Get-CMApplication -Name $app) 
        {
            Edit-Log "****Retiring $app****`n".ToUpper()
            Update-GeneralLogFile $GeneralLog "****Retiring $app****"
            
            # checking retired status, if already retired, setting to active so that we can make changes
	            if ($RetiringApp.IsExpired) 
	            {
	                $appWMI = Get-WmiObject -Namespace Root\SMS\site_XXX -class SMS_ApplicationLatest -Filter "LocalizedDisplayName = '$app'"
	                $appWMI.SetIsExpired($false) | Out-Null
	                Edit-Log "  - Setting Status of $app to Active so that changes can be made.`n"
	            }
            $UnwantedDeployments = Get-CMDeployment -SoftwareName $RetiringApp.LocalizedDisplayName
			########################################################################################################################################
            # remove all deployments for the app
	            if ($UnwantedDeployments) 
	            {
	                $UnwantedDeployments | ForEach-Object 
	                {
	                    Remove-CMDeployment -ApplicationName $app -DeploymentId $_.DeploymentID -Force
	                }
	                Edit-Log "  - Removed $($UnwantedDeployments.Count) deployments of $app.`n"
	                Update-GeneralLogFile $GeneralLog " - Removed $($UnwantedDeployments.Count) deployments of $app."
	            }
	        ########################################################################################################################################
            # remove content from all dp's and dpg's
	            Edit-Log "  - Removing content from all distribution points.... `n"
	            Update-GeneralLogFile $GeneralLog " - Removing content from all distribution points.... "
	            $DPs = Get-CMDistributionPoint
	            foreach ($DP in $DPs) 
	            {
	                $DPName = $DP.NetworkOSPath
	                try 
		                {
		                    Remove-CMContentDistribution -Application $RetiringApp -DistributionPointName ($DP).NetworkOSPath -Force -ErrorAction SilentlyContinue
		                    Edit-Log "  -  - $DPName done `n"
		                    Update-GeneralLogFile $GeneralLog " - $DPName done"
		                } 
	                catch 
		                { 
		                	Edit-Log "  - - No Content on $DPName `n"
		                	Update-GeneralLogFile $GeneralLog " - No Content on $DPName"
		                	$error.Clear()	# no need to remember this if it errors
		                }
	            }
	            Edit-Log "  - Removing content from all distribution point groups.... `n"
	            Update-GeneralLogFile $GeneralLog " - Removing content from all distribution point groups...."
	            $DPGs = Get-CMDistributionPointGroup
	            foreach ($DPG in $DPGs) 
		            {
		                $DPGName = $DPG.Name
		                try 
		                	{
		                    	Remove-CMContentDistribution -Application $RetiringApp -DistributionPointGroupName ($DPG).Name -Force -ErrorAction SilentlyContinue
		                    	Edit-Log "  - - ** $DPGName **done`n"
		                    	Update-GeneralLogFile $GeneralLog " - ** $DPGName ** done"
		                	} 
		                catch 
		                	{ 
		                		Edit-Log "  - - No Content in group ** $DPGName **`n"
		                		Update-GeneralLogFile $GeneralLog " - No Content in group $DPGName"
		                		$error.Clear()	# no need to remember this if it errors
		                	}
		            }
           	 # move the application in MECM
                Move-CMObject -FolderPath "Application\Retired Applications-Processed" -InputObject $RetiringApp
                Edit-Log "  - Moved application to Retired Application-Processed in MECM.`n"
                Update-GeneralLogFile $GeneralLog " - Moved application to Retired Application-Processed in MECM."
	########################################################################################################################################
	########################################################################################################################################
			# Deal with Deployment Types - Figure out how many deployment types there are, if appx, MSI or script tech used, get the content path for each one and move the files to the old superseded folder.      
				# Get a count of how many Revisions Exist
					$Revisionstoprocess = Get-CMApplication -Name $app | Get-CMApplicationRevisionHistory
					$RevisionsNumber = $Revisionstoprocess.Count
					# $RevisionsNumberToClean = ([int]$RevisionsNumber -[int]1)	# remove 1 as we need to keep the last revision. 
					# Remove all except last application Revision otherwise paths from all revisions will still be in the XML - https://social.technet.microsoft.com/Forums/en-US/52b82bcd-2e83-4455-b442-2162ef8c174a/programmatically-remove-all-but-lastest-application-revision-history-instances
                If ($RevisionsNumber -gt 1)
                {
                	Edit-Log " - $RevisionsNumber /s Excess Application Revisions for $app to clean `n"
                	Update-GeneralLogFile $GeneralLog " - $RevisionsNumber /s Excess Application Revisions for $app to clean"
	                $Revisionstoprocess = Get-CMApplication -Name $app |
	    			Get-CMApplicationRevisionHistory | Where-Object -FilterScript{$_.IsLatest -eq $false}
						$RevisionsToProcess |  Select-Object -Property 'LocalizedDisplayName','SDMPackageVersion','IsLatest'
						foreach ($item in $RevisionsToProcess)
						{
						    Remove-CMApplicationRevisionHistory -InputObject $item -force 	# -WhatIf
						}
				}
				Else
				{
					 Edit-Log " - $RevisionsNumber Application Revisions for $app to clean `n"
					 Update-GeneralLogFile $GeneralLog " - $RevisionsNumber Application Revisions for $app to clean"
				}
				#Get the application from the application name
					$Application = Get-CMApplication -Name $app
					$ApplicationName =  $Application.LocalizedDisplayName   # This is the app name (Localized rather than software center name)
				# Get a list of all the deployment Types and process them.
					$DeploymentTypes = Get-CMDeploymentType -ApplicationName $ApplicationName
					$DTCount = $DeploymentTypes.Count	# get a count of how many deployment types there are
					Edit-Log "  - $DTCount Deployment Type/s `n"
					Update-GeneralLogFile $GeneralLog " - $DTCount Deployment Type`/`s"
				If ($DTCount -ge 1)	 # only process deployment types if they exist. 
					{
						# Deal with Each individual Deployment Type
						$ApplicationName = @(Get-CMApplication -Name $app)
						foreach ($Application in $ApplicationName)  
							{
								$AppMgmt = ([xml]$Application.SDMPackageXML).AppMgmtDigest
			        			$AppName = $AppMgmt.Application.DisplayInfo.FirstChild.Title
					            foreach ($DeploymentType in $AppMgmt.DeploymentType) 
					            {
					                $SourceDir = $DeploymentType.Installer.Contents.Content.Location
					                Edit-Log " - Content path is $SourceDir `n"
					                Update-GeneralLogFile $GeneralLog " - Content path is $SourceDir"
					                
					                $DeploymentTypeName = $DeploymentType.Title.InnerText
					                Edit-Log " - DT Name is $DeploymentTypeName `n"
					                Update-GeneralLogFile $GeneralLog " - DT Name is $DeploymentTypeName"
					                
					                $DTTechnology = $DeploymentType.Installer.Technology
					                Edit-Log " - DT Technology is $DTTechnology `n"
					                Update-GeneralLogFile $GeneralLog " - DT Technology is $DTTechnology"
					                
							# check for a blank Source Directory, nothing to process if there is nothing in here.  
						    if ([string]::IsNullOrEmpty($SourceDir))
							   	    {
								    	Edit-Log " - No Content path in Application Deployment type `n "
								    	Update-GeneralLogFile $GeneralLog " - No Content path in Application Deployment type"
								    }
							    Else
							    	{
				    				If ($DTTechnology -match "Script" -or $DTTechnology -match "MSI" -or $DTTechnology -match "Windows8App")
										{
				    						 Edit-Log "  - Processing Content for $DTTechnology type `n"
				    						 Update-GeneralLogFile $GeneralLog " - Processing Content for $DTTechnology type"
											# Figure out what the Source Home Path is 
												# return source files location - used for AppX, MSI and Scripts, other technologies like webapp don't have content and this will error if run against those technologies.  
								       				$SourceDir = $SourceDir.trimend('\')# need to remove trailing backslash if this is part of application path.
													$sourceHome = "\\MyServer\MyPath\Source1\Applications"	# The part of the source path to remove to find the relevant path to use for the Destination files. Start with the standard path.   
												If ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source1\Applications"))
													{
													    $SourceHome = "\\MyServer\MyPath\Source1\Applications"
													}
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyOtherServer\MyotherPath\Source2\Applications"))
													{
													    $SourceHome = "\\MyOtherServer\MyotherPath\Source2\Applications"
													}
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source3\Applications"))
													{
													    $SourceHome = "\\MyServer\MyPath\Source3\Applications"
													}
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source3\Applications"))
													{
													    $SourceHome = "\\MyServer\MyPath\Source3\Applications"
													}
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source4\Applications"))
													{
													    $SourceHome = "\\MyServer\MyPath\Source4\Applications"
													}
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source5\Applications"))
													{
													    $SourceHome = "\\MyServer\MyPath\Source5\Applications"
													}
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source6\Applications"))
													{
													    $SourceHome = "\\MyServer\MyPath\Source6\Applications"
													}
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source7\Applications"))
													{
													    $sourceHome = "\\MyServer\MyPath\Source7\Applications"
													}
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source8\Applications"))
													{
													    $sourceHome = "\\MyServer\MyPath\Source8\Applications"
													}
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source9\Applications"))
													{
													    $sourceHome = "\\MyServer\MyPath\Source9\Applications"
													}	
												ElseIf ($SourceDir -match [Regex]::Escape("\\MyServer\MyPath\Source10\Applications"))
													{
													    $sourceHome = "\\MyServer\MyPath\Source10\Applications"
													}																										
												Else
													{
													    $sourceHome = "Non-Standard Home Path"
													    Edit-Log "  - Non-Standard Home Path -  $SourceHome, Moving of files and new content path will need to be done manually; Update the script to include this path for future apps `n"
													    Update-GeneralLogFile $GeneralLog " - Non-Standard Home Path -  $SourceHome, Moving of files and new content path will need to be done manually; Update the script to include this path for future apps"
													}
										       		Edit-Log "  - The Content Source Home location is $SourceHome `n"
										       		Update-GeneralLogFile $GeneralLog " - The Content Source Home location is $SourceHome"
										       		
										       		# the FULL path the old source files will be moved to
											 			$RetiredContentDir = ($SourceDir -replace [regex]::Escape($sourceHome), $DestDir)	
											 			Edit-Log "  - location to change content path to - $RetiredContentDir `n"
											 			Update-GeneralLogFile $GeneralLog " - location to change content path to - $RetiredContentDir"
														Edit-Log "  - Source Directory checking for - $SourceDir `n"
														Update-GeneralLogFile $GeneralLog " - Source Directory checking for - $SourceDir"
												# exit the MECM site drive or you won't be able to see the content path.  
													Set-Location C: 
										 		# check if the source files home path has a standard prefix, if not skip moving files.  
											 		If ($sourceHome -match "Non-Standard Home Path")
											 			{
											 				Edit-Log "  - As Source Home is non standard, any movement of files or reconfig of path must be done manually.  `n"
											 				Update-LogFile $IssuesLog "$ErrorLevel $App - $sourceHome is a non standard path, update the script with this path or manually complete this app.  "	
											 				Update-GeneralLogFile $GeneralLog "******* NON-STANDARD SOURCE PATH, FILES WILL NEED TO BE MOVED MANUALLY.  ******* "
											 			}
											 		Else
											 			{
												 			$SourceContentPathExists = Test-Path -Path $SourceDir
												 			if ($SourceContentPathExists)
																{
																	Edit-Log "  - $SourceDir Found`n"
																	Edit-Log "  - Moving $app content using RoboCopy`n"
																	Edit-Log "  - from $SourceDir `n"
																	Edit-Log "  - ...to $RetiredContentDir `n"
																	
																	Update-GeneralLogFile $GeneralLog " - $SourceDir Found"
																	Update-GeneralLogFile $GeneralLog " - Moving $app content using RoboCopy"
																	Update-GeneralLogFile $GeneralLog " - from $SourceDir"
																	Update-GeneralLogFile $GeneralLog " - ...to $RetiredContentDir"
																					
																	#Create the new Retired Content Directory for this application.  
																		Try
																			{
																				New-Item -ItemType Directory -Path $RetiredContentDir # -ErrorAction SilentlyContinue 
																				Edit-Log "  - Created new folder $RetiredContentDir `n"
																				Update-GeneralLogFile $GeneralLog " - Created new folder $RetiredContentDir"
																			}
																		Catch
																			{
																			Edit-Log "  - $RetiredContentDir already exists.  `n"
																			Update-LogFile $IssuesLog "$ErrorLevel $App - $RetiredContentDir Already exists, not created"
																			Update-GeneralLogFile $GeneralLog " - $RetiredContentDir Already exists, not created"
																			}
																	# Move Content to new location in old superceded.   
																		robocopy $SourceDir $RetiredContentDir /MIR /move /LOG:$LogFilePath\$APP-Robocopy.log
																	
														    		# Check if copy worked
														    			Edit-Log "  - RoboCopy moving files - check https://ss64.com/nt/robocopy-exit.html for codes if needed. `n"
														    			Update-GeneralLogFile $GeneralLog " - RoboCopy moving files - check https://ss64.com/nt/robocopy-exit.html for codes if needed."
														    			If ($LASTEXITCODE -eq 1)
															    			{
																				Edit-Log "  - RoboCopy Successfully moved files, Exit Code is $LASTEXITCODE which is Success. `n"
																				Update-GeneralLogFile $GeneralLog " - RoboCopy Successfully moved files, Exit Code is $LASTEXITCODE which is Success."
																			}
																		ElseIf ($LASTEXITCODE -eq 0)
																			{
																				Edit-Log "  - - ERROR - Exit Code $LASTEXITCODE means the files already existed so nothing was moved, you will need to remove the originals manually. `n"	
																				Update-GeneralLogFile $GeneralLog " - ERROR - Exit Code $LASTEXITCODE means the files already existed so nothing was moved, you will need to remove the originals manually "
																			}
																		ElseIf ($LASTEXITCODE -eq 8)
																			{
																				Edit-Log "  - - ERROR - Exit Code $LASTEXITCODE may indicate copy of an a file like autorun.inf was blocked by AV `n"	
																				Update-GeneralLogFile $GeneralLog " - ERROR - Exit Code $LASTEXITCODE may indicate copy of an a file like autorun.inf was blocked by AV"
																			}
																		ElseIf ($LASTEXITCODE -eq 16)
																			{
																				Edit-Log "  - - ERROR - Exit Code $LASTEXITCODE is a major failure, copy the files manually `n"	
																				Update-GeneralLogFile $GeneralLog " - ERROR - Exit Code $LASTEXITCODE is a major failure, copy the files manually"
																			}
																		Else
																			{
																				Edit-Log "  - - ERROR - Exit Code $LASTEXITCODE - check https://ss64.com/nt/robocopy-exit.html for More Info. `n"	
																				Update-GeneralLogFile $GeneralLog " - ERROR - Exit Code $LASTEXITCODE - check https://ss64.com/nt/robocopy-exit.html for More Info."
																			}
																		$ContentFilesExist = "ContentExists" 
												    			}
															Else
																{
														        Edit-Log "  - $SourceDir **DOESN'T EXIST**, it must have been deleted or moved already.  `n"
														        Update-GeneralLogFile $GeneralLog " - $SourceDir **DOESN'T EXIST**, it must have been deleted or moved already."
														        Update-LogFile $IssuesLog "$ErrorLevel $App - $SourceDir Doesn't exist, it must have been deleted or moved already.  "
														        $ContentFilesExist = "NoContent"
														        }
														}
										
										# Switch back to MECM site drive so ready to process the next item. 
									       	Set-Location XXX:	 
												# Change the Content Path for the Deployment Type to the new Path.  
													# from https://docs.microsoft.com/en-au/archive/blogs/configmgrdogs/package-application-source-modification-scripts (OLD command deprecated, replaced with new)
													# Note that the MECM server computer account must have access to both the old and new shares for this to work.  
															If ($ContentFilesExist -eq "ContentExists")
																{
																	#Identify the Deployment Type Technology in use, then set the new content path.  
																	$ApplicationName = @(Get-CMApplication -Name $app)
																	foreach ($Application in $ApplicationName)  
    																{
    																	$AppMgmt = ([xml]$Application.SDMPackageXML).AppMgmtDigest
																        $AppName = $AppMgmt.Application.DisplayInfo.FirstChild.Title
																            foreach ($DeploymentType in $AppMgmt.DeploymentType) 
																            {
																                $AppName = $AppName
																                $SourceDir = $DeploymentType.Installer.Contents.Content.Location
																                $DeploymentTypeName = $DeploymentType.Title.InnerText
																                $DTTechnology = $DeploymentType.Installer.Technology
																                
																                if ($DTTechnology -Match 'MSI')
																			    {
																				Edit-Log "  - Deployment Type Name for $app is ....$DeploymentTypeName `n"		
																				Update-GeneralLogFile $GeneralLog " - Deployment Type Name for $app is ....$DeploymentTypeName"
# 																					# Get Path/s for Deployment Types to verify a single Deployment type
# 																						$DeploymentTypeCleanPath = $ApplicationXML.AppMgmtDigest.DeploymentType.Installer.Contents.Content.Location[0]
# 																					# Get Path for Apps with single Deployment Type
# 																						If($DeploymentTypeCleanPath -eq "\")
# 																						{
																					       	try
																					       	{
																					       		Set-CMMSIDeploymentType –ApplicationName "$app" –DeploymentTypeName "$DeploymentTypeName" –ContentLocation "$RetiredContentDir" -ErrorAction Silentlycontinue
																					       		 Edit-Log " - Changed Content Path to $RetiredContentDir for Deployment Type - $DeploymentTypeName`N"
																					       		 Update-GeneralLogFile $GeneralLog " - Changed Content Path to $RetiredContentDir for Deployment Type - $DeploymentTypeName"																					       		 
																					       	}
																					       	Catch
																					       	{
																					       		Edit-Log "  - FAILED to update content path. `n"
																					       		Update-LogFile $IssuesLog "$ErrorLevel $App - **FAILURE REQUIRES REVIEW** - FAILED to update content path in MECM Deployment Type $DeploymentTypeName"
																					       		Update-GeneralLogFile $GeneralLog " Known issue - https://techcommunity.microsoft.com/t5/configuration-manager/set-cmmsideploymenttype/m-p/3096573#M1192"
																					       		Update-GeneralLogFile $GeneralLog " - ****FAILURE REQUIRES REVIEW - FAILED to update content path in MECM Deployment Type $DeploymentTypeName****"
																					       		Update-GeneralLogFile $GeneralLog " - - Deployment Type Name is *$DeploymentTypeName*"
																					       		Update-GeneralLogFile $GeneralLog " - - Retired Content New Directory to set as content path is *$RetiredContentDir*"
																					       	}
# 																					    }
																				    }
																				 ElseIf($DTTechnology -Match 'Script')
																				    {
																				       	try
																				       	{
																				       		Set-CMScriptDeploymentType –ApplicationName "$app" –DeploymentTypeName "$DeploymentTypeName" –ContentLocation "$RetiredContentDir" 
																				       		Edit-Log "  - content path Updated. `n"
																				       		Update-GeneralLogFile $GeneralLog " - content path Updated."
																				       	}
																				       	Catch
																				       	{
																				       		Edit-Log "  - FAILED to update content path. `n"
																				       		Update-LogFile $IssuesLog "$ErrorLevel $App - Failed to update content path in MECM Deployment Type $DeploymentTypeName"
																				       		Update-GeneralLogFile $GeneralLog "$ErrorLevel - Failed to update content path in MECM Deployment Type $DeploymentTypeName "
																				       	}
																				    }
																				 ElseIf ($DTTechnology -match 'Windows8App')
																				 	{
																				 		try
																				       	{
																				       		Set-CMWindowsAppxdeploymentType –ApplicationName "$app" –DeploymentTypeName "$DeploymentTypeName" –ContentLocation "$RetiredContentDir" 
																				       		Edit-Log "  - content path Updated. `n"
																				       		Update-GeneralLogFile $GeneralLog " - content path Updated."
																				       	}
																				       	Catch
																				       	{
																				       		Edit-Log "  - FAILED to update content path. `n"
																				       		Update-LogFile $IssuesLog "$ErrorLevel $App - *****************Failed to update content path in MECM Deployment Type $DeploymentTypeName"
																				       		Update-GeneralLogFile $GeneralLog "$ErrorLevel - *****************Failed to update content path in MECM Deployment Type $DeploymentTypeName"
																				       	}
																				 	}
																            }
																            
																    }
																}
															Else
																{
																Edit-Log "  - ***********THE CONTENT DIDN'T EXIST**, so the path was left as it was.  `n "
																Update-GeneralLogFile $GeneralLog " - ***********THE CONTENT DIDN'T EXIST**, so the path was left as it was."
																Update-LogFile $IssuesLog "$ErrorLevel $App - The content didn't exist so the path was left as it was.  "
										}
										}
								  	Else
									    {
									        Edit-Log "  - $DTTechnology Technology not supported for moving content, no Content moved. `n"
									        Update-LogFile $IssuesLog "$ErrorLevel $App - $DTTechnology Technology not supported for moving content, no content moved"
									        Update-GeneralLogFile $GeneralLog "$ErrorLevel - $DTTechnology Technology not supported for moving content, no content moved"
									    }
								}
							}
						}
					}
				Else
					{
						Edit-Log "  - No Deployment types, skipping moving of files and reconfig of DT `n"
						Update-LogFile $IssuesLog "$ErrorLevel $App - No Deployment types, skipping moving of files and reconfig of DT"
						Update-GeneralLogFile $GeneralLog "$ErrorLevel - No Deployment types, skipping moving of files and reconfig of DT"
					}
		########################################################################################################################################
		# rename the application in MECM (need to rename before retiring, can't rename a retired application.  
			Edit-Log "  - Renaming $app in MECM"
			Update-GeneralLogFile $GeneralLog " - Renaming $app in MECM"
		            $OldName = $app 
		            $NewName = "Retired_$Year - $app"
		            	try {
							Set-CMApplication -Name $OldName -NewName $NewName
		            		$appNewName = "$("Retired_$Year - ")$app"
	            		Edit-Log "....renamed to $NewName `n" 
	            		Update-GeneralLogFile $GeneralLog " - - renamed to ** $NewName **"            	
		            		} 
		            	catch 
		            		{ 
		            		Edit-Log "....**FAILED**`n"
		            		Update-LogFile $IssuesLog "$ErrorLevel $App - FAILED to rename application in MECM "
		            		Update-GeneralLogFile $GeneralLog "$ErrorLevel - ****FAILED to rename application in MECM****"
		            		}
########################################################################################################################################
	# retire the app
			Edit-Log "  - Retiring $appNewName in MECM"
			Update-GeneralLogFile $GeneralLog " - Retiring ** $appNewName ** in MECM"
          	try {
             		Suspend-CMApplication $appNewName 
             		Edit-Log "....Successful `n "
             		Update-GeneralLogFile $GeneralLog "  - - Successful"
            	} 
            	catch
            	{
            		Edit-Log "....**FAILED** `n "
            		Update-LogFile $IssuesLog "$ErrorLevel $App - FAILED to set status to Retired, manual retirement will be required."
            		Update-GeneralLogFile $GeneralLog "$ErrorLevel - ****FAILED to set status to Retired, manual retirement will be required.****"
          		}

########################################################################################################################################	
			Edit-Log "***** Completed $app ***** `n"
			Edit-Log "**************************************************************************************************** `n"
			Update-GeneralLogFile $GeneralLog "***** Completed $app *****"
			Update-GeneralLogFile $GeneralLog "**************************************************************************************************** "
			
			$ErrorCount = $error.Count 
			If($ErrorCount -ge 1)
			{
				Edit-Log "$ErrorCount NON-TERMINATING ERRORS DETECTED `n "
				Edit-Log "**************************************************************************************************** `n"
				Update-GeneralLogFile $GeneralLog "$ErrorCount NON-TERMINATING ERRORS DETECTED"
				Update-GeneralLogFile $GeneralLog "****************************************************************************************************"
			}
			$ErrorCount = $error.clear()
		}
		Else
			{
			Edit-Log "**************************************************************************************************** `n"
			Edit-Log "*****$App Not Found, have you got the name right? *****`n"
		 	Edit-Log "**************************************************************************************************** `n"
		 		$ErrorCount = $error.Clear()
			}	 
	} 
	}
} 
#####################################################################
# The Script 
#####################################################################	
# Setup 
	$ScriptVersion = '2022.01.01'
	
# Configure the target path to direct retired application source files.  	
	$Year = "2022" # this is just to create a subfolder in the destination dirctory to split up the apps to make future cleanups easier.  for us, we separate into folders for the year they are retired.
    $DestDirRoot = "\\MyServer\MyPath\OLD_SupercededSoftware\1_RetiredApplications"	# the root folder where all retired apps will be moved to, change each year. 
	$DestDir = "$DestDirRoot\$Year"	# the  folder where all retired apps will be moved to, should change each year. 

#Create the issues Log - generic log for all applications to add warnings to
	$LogFilePath = "$DestDirRoot\$Year\1_LOGS\"
	$LogFileName = "1_ApplicationRetirement_IssuesLog-$Year.log"
	$IssuesLog = $LogFilePath + $LogFileName
	New-Log $IssuesLog $logFilePath $LogFileName
	$ErrorLevel ="WARNING" #  Use this as the default, options are WARNING, INFO, ERROR
	$Message = "Something went wrong" 

# C

 

 

1 best response

Accepted Solutions
best response confirmed by PaulKlerkx (Iron Contributor)
Solution
Short story
- According to Microsoft Support, the content Library cleanup tool logs and from my experience, the Content Library Cleanup Tool Does not work for a single server set up. (or single server with CMG)
- Greyed out entries in Content Library explorer indicate EITHER MECM having a record of content in the Database, but there isn't any content actually there in the content library OR the applications are retired.

After going through all this, I located a script that assisted with retiring some of our applications which took me several weeks to get the way I liked and run for each application. Part of the script is to rename the application which made it very easy to identify them in the content library explorer.
I still get the error message when running content library cleanup tool with the message in the logfile being "Because this distribution point is co-located with its site server, packages may correctly exist in the content library that are not distributed to the distribution point. Package deletion has been disabled."
What I found when running my retirement script is that a large number of our retired applications didn't have content on the DP's or DPG's and the original content had also been deleted or moved. I believe our move to a restored server highlighted these issues in the content library explorer and by running my script which cleans any content from the Dp's and DPG's as well as updating the content path, this has cleared up most of the issues being seen.


View solution in original post