Link to home
Start Free TrialLog in
Avatar of samiam41
samiam41Flag for United States of America

asked on

Delete dir after profile removed from PC

Hey Experts!  The script below deletes out the user's profile on the computers in a text file.  However the script isn't deleting out the user's AppData folder.  It's staying on the computer and I'm hopeful we can tweak this script so the AppData dir is deleted when the profile is wiped out.

Get-ADComputer -Filter 'Name -like "TRN3*"' -SearchBase "OU=COMPUTERS,OU=TRAINING,OU=DEPTS,DC=LOCAL" -Property * | Select-Object -ExpandProperty name | out-file c:\tools\logs\testlog.csv


$computerList = Get-Content c:\tools\logs\testlog.csv
$logFile = "c:\tools\logs\testlog.csv_$([DateTime]::Now.ToString('yyyyMMdd')).csv"

$dateTime = {[DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss')}
$computerList | ForEach-Object {
	$computerName = $_
	Write-Host "Processing $($computerName) ..."
	[PSCustomObject]@{
		ComputerName = $computerName
		Date = & $dateTime
		Type = 'Cleanup Start'
		Result = "PROFILE CLEANUP OF TRAINING ACCOUNTS START: $([DateTime]::Now)"
	}
	If (Test-Connection -ComputerName $computerName -Count 2 -Quiet) {
		Try {
			$diskC = Get-WmiObject  Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName $computerName -ErrorAction SilentlyContinue
			$diskSize = [math]::Round($diskC.Size / 1GB, 2)
			$freeSpaceBefore = [math]::Round($diskC.FreeSpace / 1GB, 2)
			$profiles = Get-WmiObject -Class Win32_UserProfile -Filter "(Special='False') And (Loaded='False') And (Not (SID Like '%500'))" -ComputerName $computerName -ErrorAction Stop
			If ($profiles) {
				$profiles | ForEach-Object {
					$out = $_ | Select-Object -Property @{n='ComputerName'; e={$computerName}}, @{n='Date'; e={& $dateTime}}, @{n='Type'; e={'Profile'}}, LocalPath, Result
					Try {
						[void]$_.Delete()
						$out.Result = "Profile deleted"
					} Catch {
						$out.Result = "ERROR: $($_.Exception.Message)"
					}
					$out
				}
			} Else {
				[PSCustomObject]@{
					ComputerName = $computerName
					Date = & $dateTime
					Type = 'Profile'
					Result = 'Found no profiles to delete'
				}
			}
			$diskC = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName $computerName -ErrorAction SilentlyContinue
			$freeSpaceAfter = [math]::Round($diskC.FreeSpace / 1GB, 2)
			[PSCustomObject]@{
				ComputerName = $computerName
				Date = & $dateTime
				Type = 'Disk Summary'
				Result = "SUMMARY: disk size $($diskSize)GB, free space before: $($freeSpaceBefore)GB, free space after: $($freeSpaceAfter)GB"
			}
		} Catch {
			[PSCustomObject]@{
				ComputerName = $computerName
				Date = & $dateTime
				Type = 'WMI Error'
				Result = "ERROR: $($_.Exception.Message)"
			}
		} Finally {
			[PSCustomObject]@{
				ComputerName = $computerName
				Date = & $dateTime
				Type = 'Cleanup End'
				Result = "PROFILE CLEANUP OF TRAINING ACCOUNTS END: $([DateTime]::Now)"
			}
		}
	} Else {
		[PSCustomObject]@{
			ComputerName = $computerName
			Date = & $dateTime
			Type = 'Remoting Error'
			Result = 'No ping response'
		}
		[PSCustomObject]@{
			ComputerName = $computerName
			Date = & $dateTime
			Type = 'Cleanup End'
			Result = "PROFILE CLEANUP OF TRAINING ACCOUNTS END: $([DateTime]::Now)"
		}
	}
} | Select-Object -Property ComputerName, Date, Type, LocalPath, Result | Export-Csv -NoTypeInformation -Path $logFile

Open in new window


Thanks Experts!
SOLUTION
Avatar of Alex
Alex
Flag of United Kingdom of Great Britain and Northern Ireland image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of samiam41

ASKER

Appreciate the help and suggestion!  Unfortunately we have a couple of computers that are in the Training OU that I don't want this policy to apply to.  I guess I could tweak the exclusion list of the GP for just those PC's that I don't want the policy applied to.  Let me test.
Create a new policy and have it above your previous one in the link order, it'll take precedence then delete the profiles. Then use a security group with only the training machines in it :-)
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
@Alex, that works, alternatively he could do an item level targeting as well, which might be a bit more preferable.

Then he can have the option to Apply only to systems which match:

[NOT] being in a a specific group
[NOT] matching a WMI Query
[NOT] being specific computer Objects
[NOT] being in a Sub OU
etc

I usually only use WMI filters at a Policy level as I have the ability to write a logical NOT into them a create sub-set selections.

So, perhaps I am incorrect, but I believe that Group targeting on the Policy scope would only allow for an inclusive assignment, requiring only the computers he wants to apply the policy to to be in the group, and therefore making the policy best applied at the root of the domain so that moving the objects to other OUs will not affect application of the policy.

IE: Effectively it defeats the purpose he had in keeping these computers in a separate OU with a separate GPO (ease of management) and makes him update a group with all fo the systems in the OU each time he adds more, except when he adds one that does not need the policy applied to it.

Ergo, if the system to exclude are a smaller subset, then using a method to exclude only those systems would be a preferable one to using an inclusive method in terms of administrative overhead and redundancy.
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Alternately, if you prefer to 'fix' the script:

Powershell isn't the best at doing File and directory deletes, although it's gotten a lot better, generally running the command twice solves the issue.

I tweaked your script slightly to accomplish that and also, to allow output to screen and log file, and allows you to run the script multiple times on a single day and see each log file separately as the time is used in the logfiel name.


Get-ADComputer -Filter 'Name -like "TRN3*"' -SearchBase "OU=COMPUTERS,OU=TRAINING,OU=DEPTS,DC=LOCAL" -Property * | Select-Object -ExpandProperty name | out-file c:\tools\logs\testlog.csv


$computerList = Get-Content c:\tools\logs\testlog.csv
$logFile = "c:\tools\logs\testlog.csv_$([DateTime]::Now.ToString('yyyyMMdd_HH.m.ss')).csv"




$dateTime = {[DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss')}

$Result = $computerList | ForEach-Object {
	$computerName = $_
	Write-Host "Processing $($computerName) ..."
	[PSCustomObject]@{
		ComputerName = $computerName
		Date = & $dateTime
		Type = 'Cleanup Start'
		Result = "PROFILE CLEANUP OF TRAINING ACCOUNTS START: $([DateTime]::Now)"
	}
	If (Test-Connection -ComputerName $computerName -Count 2 -Quiet) {
		Try {
			$diskC = Get-WmiObject  Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName $computerName -ErrorAction SilentlyContinue
			$diskSize = [math]::Round($diskC.Size / 1GB, 2)
			$freeSpaceBefore = [math]::Round($diskC.FreeSpace / 1GB, 2)
			$profiles = Get-WmiObject -Class Win32_UserProfile -Filter "(Special='False') And (Loaded='False') And (Not (SID Like '%500'))" -ComputerName $computerName -ErrorAction Stop
			If ($profiles) {
				$profiles | ForEach-Object {
					$out = $_ | Select-Object -Property @{n='ComputerName'; e={$computerName}}, @{n='Date'; e={& $dateTime}}, @{n='Type'; e={'Profile'}}, LocalPath, Result, Result2
					Try {
						[void]$_.Delete()
						$out.Result = "Profile deleted"
					} Catch {
						$out.Result = "ERROR: $($_.Exception.Message)"
					}
                     #Powershell isn't the best at deleting Directory Trees, which is why a lot of people still use CMD called from Powershell, however usually it just need a few goes and will work.
                                       Try {
						[void]$_.Delete()
						$out.Result2 = "Profile deleted"
					} Catch {
						$out.Result2 = "ERROR: $($_.Exception.Message)"
					}
					$out
				}
			} Else {
				[PSCustomObject]@{
					ComputerName = $computerName
					Date = & $dateTime
					Type = 'Profile'
					Result = 'Found no profiles to delete'
				}
			}
			$diskC = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" -ComputerName $computerName -ErrorAction SilentlyContinue
			$freeSpaceAfter = [math]::Round($diskC.FreeSpace / 1GB, 2)
			[PSCustomObject]@{
				ComputerName = $computerName
				Date = & $dateTime
				Type = 'Disk Summary'
				Result = "SUMMARY: disk size $($diskSize)GB, free space before: $($freeSpaceBefore)GB, free space after: $($freeSpaceAfter)GB"
			}
		} Catch {
			[PSCustomObject]@{
				ComputerName = $computerName
				Date = & $dateTime
				Type = 'WMI Error'
				Result = "ERROR: $($_.Exception.Message)"
			}
		} Finally {
			[PSCustomObject]@{
				ComputerName = $computerName
				Date = & $dateTime
				Type = 'Cleanup End'
				Result = "PROFILE CLEANUP OF TRAINING ACCOUNTS END: $([DateTime]::Now)"
			}
		}
	} Else {
		[PSCustomObject]@{
			ComputerName = $computerName
			Date = & $dateTime
			Type = 'Remoting Error'
			Result = 'No ping response'
		}
		[PSCustomObject]@{
			ComputerName = $computerName
			Date = & $dateTime
			Type = 'Cleanup End'
			Result = "PROFILE CLEANUP OF TRAINING ACCOUNTS END: $([DateTime]::Now)"
		}
	}
}
$Result | Select-Object -Property ComputerName, Date, Type, LocalPath, Result |FT
$Result | Select-Object -Property ComputerName, Date, Type, LocalPath, Result | Export-Csv -NoTypeInformation -Path $logFile

Open in new window

A mountain of very useful advice and guidance.  Thank you for the time it took to put that together along with the instructions and screenshots.  It's greatly appreciated.

I went ahead and created the WMI filtering, assigned it to a new GP and made the appropriate settings as outlined.  

I am waiting on the testing now and will report back what I find.  

Again, thank you!!
Glad to help :)  Let us know how it goes :)
Hey Experts!  I created a GP with WMI filter for the TRN Dept and verified it's being applied via (gpresult /r) (I see the policy listed as being applied to the computer which is where the setting is).  I set the value for "delete user profiles older than a specified.." setting to 6 days so I could tell if the setting would work.  Under the \users directory on the TRN pc's, there are profile folders for the users still there.  However when I go to Advanced > User Profiles > Settings, the profiles aren't there.

Is there a way to flush out the user profile directories under \User on each PC?  Thanks everyone
@SamIAm,

Edit some typos in script, etc

  To be honest, I have not used the GPO to delete a profile, I would imagine the system leaves mostly empty directories on the system, and that it never fully clears them off.

  I have been dealing with Profiles since Windows 2000 (even before in my 9x days as a kid), and to truly be assured of deleting all of the files and folders, you MUSt reboot the system ad log in as a different user.

  Otherwise you will will almost always leave some files and folders behind on delete because windows holds locks various system and temp files, as well as the registry hives once a profile logs in, and until rebooted.

Generally, when deleting profiles I prefer a simple CMD script.

  Did you try the amended powershell?

 To be certain all of the profile's files and directories are deleted would require a multi-step reboot process to make sure none are being held open.

  1. Delete the Profile (GPO or script)
  2. Reboot the system
  3. Run an RMDir command on he profile directory from an elevated command prompt.


Again, this is because if a Profile was logged in since the last reboot it's basically impossible to delete ALL of it's associated files, ONLY a freshly booted OS where the Profiles has not since logged on would allow this.  You can get around some of the file delete limitations by purposely unloading the registry, killing processes and restarting services, but it just makes the system unstable and gives no certainty.

So, are you trying this on systems which have been freshly booted?

If so, would it make more sense just to do a simple:

RMDir /S /Q "C:\Users\[Username]"

Open in new window


on each of the unwanted folders left over.

 Here this code should combine all aspects into one, but you will still want to reboot and re-run to be totally assured of deletion:

$Profile_Results=@()
$Folder_Results=@()

"================
Profiles to Keep:
"
$Profiles_Keep = Get-CimInstance -Class Win32_UserProfile -Filter "(Special='True') Or (Loaded='True') Or (Not (SID Like 'S-1-5-21%')) Or (SID Like '%-500')" -ComputerName $computerName -ErrorAction Stop
$Profiles_Keep | FT LocalPath, SID

"

================
Profiles to Delete:

"
$Profiles_Delete = Get-CimInstance -Class Win32_UserProfile -Filter "(Special='False') And (Loaded='False') And (SID Like 'S-1-5-21%') And (Not (SID Like '%-500') )" -ComputerName $computerName -ErrorAction Stop
$Profiles_Delete | FT LocalPath, SID
"
Deleting Profiles......
"
$Profile_Results = $Profiles_Delete | % {
    Try {
	    [void]$_.Delete()
	    $t_Result = "Profile deleted"
    } Catch {
	    $t_Result = "ERROR: $($_.Exception.Message)"
    }
    [PSCustomObject]@{
        ComputerName=$computerName;
        Date=$(get-date);
        Type="Profile";
        LocalPath=$($_.LocalPath);
        Result=$t_Result;

    }
}

"
=======================
Profile Delete Results:
-----------------------

"
$Profile_Results


"


----------------------------------------------------------------



================
Folders on System:
"
$Folders_CUsers = $(Get-ChildItem "C:\Users").fullname
$Folders_CUsers

"

================
Folders to Remove:

"
$Folders_Remove = $Folders_CUsers | ? {$_ -notin $($Profiles_Keep.Localpath)}
$Folders_Remove
"
Removing Folders......
"
$Folder_Results = $Folders_Remove | % {
    #&{"cmd /c `"RMDir /S /Q `"$_`""}
    #&{cmd /c `"RMDir /S /Q $_ `"}
    &{cmd /c `"RMDir /S /Q `"$_`"`"}
    Switch ($LastExitCode) {
          0 { $t_Result = "SUCCESS: Removed Path: `"$_`"" }
          2 { $t_Result = "ERROR $LastExitCode: The system cannot find the file specified. Path: `"$_`"" }
          5 { $t_Result = "ERROR $LastExitCode: Access is denied. Path: `"$_`"" }
         32 { $t_Result = "ERROR $LastExitCode: The process cannot access the file because it is being used by another process. Path: `"$_`"" }
        145 { $t_Result = "ERROR $LastExitCode: The directory is not empty. `"$_`"" }
    }
    [PSCustomObject]@{
        ComputerName=$computerName;
        Date=$(get-date);
        Type="Folder";
        LocalPath=$($_);
        Result=$t_Result;

    }
}

"
======================
Folder Remove Results:
----------------------

"
$Folder_Results

Open in new window

hey Ben!  I actually remember working with you when you were QCubed.  

I ran the script above on a couple of Pc's in the training department and it scrubbed all profiles out which was fine.  The problem was that the script also (somehow) blew up the AV product running on those samplings of Pc's.  So, in light of that, I'm going to stick with the group policy removing the profiles instead of the script.  

I appreciate everyone's time, effort and expertise!
Sorry for the delay in getting back to this.  One guy, multiple hats as are many of you can relate.
Hey SamIAm41!  I thought your name looked familiar to me :)

Glad to have helped on both solutions, and good to know the script solved the exact issue for you, if not for the AV bombing due to file deletes, might be trying to scan the folders with deleted files and running into an issue or might be the thing locking those directories in the first place.

  In any case, use what works, and glad to help with both. :)

Hope all is well :)


Ben
You know what, I think I have an Idea on the AV Issue.

  So I wanted to clean up folders for profiles previously deleted that had not been fully deleted.

  So I use the "Profiles to Keep" variable to exclude the paths of those profiles.

  However, there could be paths under the users folder that don't belong to a profile (there really shouldn't be, but there can be).

So I adjusted the script so it will now only delete paths from profiles we chose to delete in the previous step, so if there are old profile folders or extra non-profile folders those won't be touched.

  Did you ever review the results of the paths to delete etc and see if there was one that was not a profile, or another profile we shouldn't delete?

- Ben

$Profile_Results=@()
$Folder_Results=@()

"================
Profiles to Keep:
"
$Profiles_Keep = Get-CimInstance -Class Win32_UserProfile -Filter "(Special='True') Or (Loaded='True') Or (Not (SID Like 'S-1-5-21%')) Or (SID Like '%-500')" -ComputerName $computerName -ErrorAction Stop
$Profiles_Keep | FT LocalPath, SID

"

================
Profiles to Delete:

"
$Profiles_Delete = Get-CimInstance -Class Win32_UserProfile -Filter "(Special='False') And (Loaded='False') And (SID Like 'S-1-5-21%') And (Not (SID Like '%-500') )" -ComputerName $computerName -ErrorAction Stop
$Profiles_Delete | FT LocalPath, SID
"
Deleting Profiles......
"
$Profile_Results = $Profiles_Delete | % {
    Try {
	    [void]$_.Delete()
	    $t_Result = "Profile deleted"
    } Catch {
	    $t_Result = "ERROR: $($_.Exception.Message)"
    }
    [PSCustomObject]@{
        ComputerName=$computerName;
        Date=$(get-date);
        Type="Profile";
        LocalPath=$($_.LocalPath);
        Result=$t_Result;

    }
}

"
=======================
Profile Delete Results:
-----------------------

"
$Profile_Results


"


----------------------------------------------------------------



================
Folders on System:
"
$Folders_CUsers = $(Get-ChildItem "C:\Users").fullname
$Folders_CUsers

"

================
Folders to Remove:

"
$Folders_Remove = $Folders_CUsers | ? {$_ -in $($Profiles_Delete.Localpath)}
$Folders_Remove
"
Removing Folders......
"
$Folder_Results = $Folders_Remove | % {
    &{cmd /c `"RMDir /S /Q `"$_`"`"}
    Switch ($LastExitCode) {
          0 { $t_Result = "SUCCESS: Removed Path: `"$_`"" }
          2 { $t_Result = "ERROR $LastExitCode: The system cannot find the file specified. Path: `"$_`"" }
          5 { $t_Result = "ERROR $LastExitCode: Access is denied. Path: `"$_`"" }
         32 { $t_Result = "ERROR $LastExitCode: The process cannot access the file because it is being used by another process. Path: `"$_`"" }
        145 { $t_Result = "ERROR $LastExitCode: The directory is not empty. `"$_`"" }
    }
    [PSCustomObject]@{
        ComputerName=$computerName;
        Date=$(get-date);
        Type="Folder";
        LocalPath=$($_);
        Result=$t_Result;

    }
}

"
======================
Folder Remove Results:
----------------------

"
$Folder_Results

Open in new window