FreakoutITGeek's Blog

Random IT postings from Freakz

Tag Archives: user

Deleting user profiles

In the past I have worked within several educational establishments and there has been a recurring IT issue.

Users on a Windows PC will get an error message (eg “We can’t sign into your account”) and are unable to log in. They usually then come to IT and say something like they can’t get in / locked out / forgotten their password and can they have their password reset (ie nothing to do with the issue).

When investigating the issue, it’s clear that the PC has run out of disk space and can not create (or update) the user profile as there is insufficient disk space.

This is most likely to happen in a shared location, not usually in a class/lecture room as students tend to sit at the same computer. It most often happens in a shared space such as a general use space or a library where several different users use the computers over a relatively short period of time. This leads to multiple user profiles being created on the computers which are unlikely to be used again.

With users nowadays using more space, usually with tools such as OneDrive and Teams (although user programs also take up considerable amounts of space), the internal disk of the PC quickly fills up after a few months.

In the old days, pre Windows 10, one of the most common ways of dealing with this from a support point of view (without having rights to such tools as SCCM or Active Directory policies – which may have ways to automatically delete the profiles on such computers) was to use DelProf from Sysinternals. This program could be run on a PC to delete any profiles older than a specified number of days. The tool would be scheduled during school holidays (either by hacking the registry to run on reboot or using tools such as Altiris or PDQ) or sometimes even manually.

Unfortunately Microsoft made changes in Windows 10 and later, which stopped DelProf from working and whilst someone created a replacement DelProf2, I never found that it worked as well. Any attempts that I made to clear the profiles using PowerShell commands (which you can find using a quick Google) never worked as well as they were supposed to or just did not work in the education environments that I was using them within.

So, as Usual, after various attempts to use methods suggested online, I decided to do some hacking of my own… Whilst the end result are not what I had hoped for, it does the job to a suitable level, although I am disappointed that there is not an easier method (which doesn’t involve being logged into Windows as local admin ) to perform such a necessary task. I suppose the easiest alternative would be to just re-image the machines at set periods, use some program such as DeepFreeze (or similar ) to keep the PCs clean of the profiles in the first place or use a [Cloud based] Virtual PC or Citrix to prevent such issues in the first place [tools which are not common in the UK/Scottish education establishments I have worked in]. Whilst these are possible scenarios I suspect they could create undue strain on the network, when it would be better served for other purposes (such as pushing out Windows updates / Security fixes) or delaying the next user from logging in (whilst the profiles were cleared).

So here is my workaround to this frustrating issue…

I used one of my favourite tools, AutoIt routine IsAdmin() to check that the person running the program is a local administrator (you can’t delete the user profiles without this), then the script opens “System Properties Advanced” and “User Profiles”

Using AutoIt’s functions the program reads the “User Profiles” window (into an array) and compares each entry to the expected “DOMAIN\UNIIQUE_Start” (this could be something like “School1\21” where the domain is called School1 and the pupil profiles start with 21 for the year that the pupil year group started, it will be different depending on the environment, but places I have worked in have had a unique prefix for pupil accounts or year group [to differentiate them from the staff accounts]).

The initial program worked well, but with some of the machines I found that there were a few hundred profiles. This meant that Windows took a while to display the “User Profiles” window, causing the program to fail. I realised I needed a good way to create a delay dependant on the number of profiles. The solution was to use another AutoIt built in function _FileListToArrayRec to find out how many profiles are in “c\users” (the default location for User profiles) and use that for an estimated delay. This could be expanded (possibly with a GUI ?) to show the space used/saved using this program?

To aid the detection of issues and for potential future improvements, the script includes debug statements, which can either be displayed on screen [Debug=1], saved to a debug text file [Debug=2], any other value will disable debugging [See Func Debug( $mesage = “”)].

I hope this helps someone in a similar role to myself, somewhere and if you want to take the script and improve it feel free… I may look at creating something similar using PowerShell at a later date as some systems [AntiVitus applications such as Sophos and Microsoft Defender] have issues with AutoIt, flagging/blocking/deleting as a PUA [Potentially Unwanted Application].

Here’s a copy of the script (it’s untidy and the formatting is off (WordPress issues).

You will need to add the includes for MsgBoxConstants.au3, GUIConstantsEx.au3, GuiListView.au3, Array.au3, AutoItConstants.au3, File.au3, FileConstants.au3 and WinAPIFiles.au3 as WordPress doesn’t like the formatting for the AutoIt include statement [Interpreting as HTML], so I have had to remove it for now.

[ \/ AutoIt code starts here \/ ]

#INCLUDES STATEMENTS GO HERE

;variables

Global $advancedSettings = “SystemPropertiesAdvanced”
Global $advancedSettingsWindowName = “System Properties”
Global $userProfilesWindow = “User Profiles”

Global $ProfilesArray[]
Global $AProfileArray[]
Global $PID

Global $debug =2
Global $debugFile = “/debug.txt”
Global $delay = 500
Global $Profiles = 0
Global $StudentProfiles = 0

Global $IncreasedProfileTrigger = 10

Global $StringToFind = “DOMAIN\UNIIQUE_Start”

;Program here \/

If IsAdmin() Then
# MsgBox($MB_SYSTEMMODAL, “”, “IsAdmin” & @CRLF & “Admin rights are detected.”)

GetProfilesInDir2()
FindStudentProfiles()
debug("There are " & $StudentProfiles & " Student profiles.")

IF $StudentProfiles < 1 then
MsgBox($MB_SYSTEMMODAL, “Error”, “No Student profiles were detected on this PC.” & @CRLF & “Program will now exit.”)

Exit

EndIf

#Minimise all Windows
Local $oShell = ObjCreate("shell.application")
$oShell.MinimizeAll    

OpenAdvancedSystem()
OpenUserProfiles()

IF $Profiles > $IncreasedProfileTrigger then
IncreasedProfileDelay()
;managing issues where there are a large number of profiles and the system may stop responding
Debug(“More than “& $IncreasedProfileTrigger &” Profiles detected, will start delay.”)
EndIf

GetUserProfiles()

FindStudents()

CloseProfiles()

Else

MsgBox($MB_SYSTEMMODAL, "Administrator Access Required", "Administrator permissions is required to run this application." & @CRLF & "Please run with Admin Rights.")
Exit

EndIf

;functions here \/

Func IncreasedProfileDelay()

Debug ("Starting IncreasedProfileDelay")
For $profileDelay = 0 To $Profiles - 1
    sleep(10000)
    Debug("Delay "& $profileDelay &" of " & $Profiles)
    if WinExists($userProfilesWindow) = 1 Then
        $profileDelay = $Profiles - 1
        debug ("User Profiles Window Detected")
    EndIf

Next


Debug ("Finished IncreasedProfileDelay")

EndFunc

Func GetProfilesInDir()
Debug (“Starting GetProfilesInDir.”)

    Local $hTimer = TimerInit()

    Local $aSize = DirGetSize("c:\users",$DIR_EXTENDED)
    If Not @error Then
            Local $iDiff = Round(TimerDiff($hTimer) / 1000) ; time in seconds
            debug( "Dirs: " & $aSize[2] & @CRLF _
                             & "TimeDiff(Sec): " & $iDiff)
    EndIf

Debug ("Finished GetProfilesInDir.") 

EndFunc

Func GetProfilesInDir2()

Debug ("Starting GetProfilesInDir2") 

Local $hTimer = TimerInit()

Local $iDiff = Round(TimerDiff($hTimer) / 1000) ; time in seconds
    $AProfileArray = _FileListToArrayRec("c:\users", "*", $FLTAR_FOLDERS, $FLTAR_NORECUR, $FLTAR_SORT)
    #_ArrayDisplay($AProfileArray, "'")
debug( " " & $AProfileArray[0] & " User Dirs found."& @CRLF _
                             & "TimeDiff(Sec): " & $iDiff)
$Profiles  = $AProfileArray[0]


Debug ("Finished GetProfilesInDir.") 

EndFunc

func OpenAdvancedSystem()
Debug (“Starting OpenAdvancedSystem.”)

$PID = Run(@ComSpec & " /c " & "start "&$advancedSettings)
    While WinWaitActive($advancedSettingsWindowName, "", 1) = 0

        WinActivate($advancedSettingsWindowName)
        Debug ("Activating : "&$advancedSettingsWindowName) 
    WEnd

Debug ("Finished OpenAdvancedSystem.") 

EndFunc

func CloseProfiles()

Debug ("Starting CloseProfiles") 
    Debug("Closing." )
    WinActivate($userProfilesWindow)
    send("!{F4}")
    sleep (100)
    Debug("Closed: User Profiles Window")
    WinActivate($advancedSettingsWindowName)
    ;ProcessWaitClose($PID,60) 
    send("!{F4}")

    Debug("Closed: Advanced Settings Window")
    sleep (100)

Debug ("Finished CloseProfiles") 

EndFunc

func FindStudentProfiles()

Debug ("Starting FindStudentProfiles") 

Debug (“Checking Domain Profiles”)
For $a = 0 To UBound($AProfileArray)- 1
Debug( “Profile ” & $a & “: ” & $AProfileArray[$a] )

    If stringInStr($AProfileArray[$a],$StringToFind) > 0 then
        Debug("Student " & $AProfileArray[$a] & " found." )   

        $StudentProfiles = $StudentProfiles +1
    EndIf
Next

Debug (“Checking Non Domain Profiles”)
;check for non domain
IF stringInStr($StringToFind,”\”) >0 Then

$anonDomain = StringSplit($StringToFind,"\")
$nonDomain = $anonDomain[2]

For $a = 0 To UBound($AProfileArray)- 1
    Debug( "Profile " & $a & ": " & $AProfileArray[$a] )

    If stringInStr($AProfileArray[$a],$nonDomain) > 0 then
        Debug("Student " & $AProfileArray[$a] & " found." )   

        $StudentProfiles = $StudentProfiles +1
    EndIf
Next

EndIf

Debug (“Finished FindStudentProfiles”)

EndFunc

func FindStudents()

Debug ("Starting FindStudents") 

For $a = 0 To UBound($ProfilesArray)- 1
    Debug( "Profile " & $a & ": " & $ProfilesArray[$a] )

    If stringInStr($ProfilesArray[$a],$StringToFind) > 0 then
        Debug("Student " & $ProfilesArray[$a] & " found." )   

        #find entry in list
        MovetoProfile( $ProfilesArray[$a] )
    EndIf
Next

Debug ("Finished FindStudents") 

EndFunc

func OpenUserProfiles()

Debug ("Starting OpenUserProfiles")     

WinWaitActive($advancedSettingsWindowName, "", 10)
WinActivate($advancedSettingsWindowName)
sleep(5000) ; sleep 5 seconds
send("!e")

Debug ("Finished OpenUserProfiles") 

EndFunc

Func MovetoProfile( $profile = “”)

Debug ("Starting MovetoProfile")    

$hWnd = WinWaitActive($userProfilesWindow, "", 10)
; $hWnd - is the window handle

WinActivate($userProfilesWindow)
sleep(5000) ; sleep 5 seconds


;Profiles are in a ListView


Local $hControl = ControlGetHandle($hWnd, "", "SysListView321")

# derived from https://www.autoitscript.com/forum/topic/165450-read-value-from-listview/


Local $SelectedItemIndex = -1
Local $GetCount = _GUICtrlListView_GetItemCount($hControl)
Debug( "Found " & $GetCount & " Profiles.")

    ;#cs
For $a = 0 To $GetCount - 1
    $LineInfo = _GUICtrlListView_GetItemTextArray($hControl, $a)
    #_ArrayDisplay($LineInfo)

    IF stringInStr($profile, $LineInfo[1]) > 0  Then
        ;click on this profile
        _GUICtrlListView_ClickItem($hControl, $a)
        Debug("Profile " & $LineInfo[1] & " found. " )


        WinActivate($userProfilesWindow)
        ;send Delete keys
        send("!D") ; delete
        Send("y") ; yes
        sleep(10000)
        $a = $GetCount - 1

    EndIf


Next

Debug ("Finished MovetoProfile") 

EndFunc

func GetUserProfiles()

    Debug ("Starting GetUserProfiles") 

$hWnd = WinWaitActive($userProfilesWindow, "", 100)
; $hWnd - is the window handle

WinActivate($userProfilesWindow)
sleep(5000) ; sleep 5 seconds
debug("Opened User Profiles window.")

;Profiles are in a ListView


Local $hControl = ControlGetHandle($hWnd, "", "SysListView321")

# derived from https://www.autoitscript.com/forum/topic/165450-read-value-from-listview/

Local $SelectedItemIndex = -1
Local $GetCount = _GUICtrlListView_GetItemCount($hControl)
Debug("Found " & $GetCount & " Profiles.")

Local $Profiles[$GetCount]
;#cs
For $a = 0 To $GetCount - 1
    $LineInfo = _GUICtrlListView_GetItemTextArray($hControl, $a)
    #_ArrayDisplay($LineInfo)

    $Profiles[$a] = $LineInfo[1]

    Debug("Profile " & $a & ": " & $Profiles[$a] & "" )

Next

;_ArrayDisplay($Profiles)

$ProfilesArray = $Profiles
;_ArrayDisplay($ProfilesArray)



;#ce
        Debug ("Finished GetUserProfiles") 

EndFunc

Func Debug( $mesage = “”)

If $debug = 1 Then
    MsgBox($MB_SYSTEMMODAL, "Debug", $mesage, 10 )
    sleep($delay)
EndIf


If $debug = 2 Then
    Local Const $sFilePath = @ScriptDir & "/" & $debugFile
    Local $hFileOpen = FileOpen($sFilePath, $FO_APPEND)

    If $hFileOpen = -1 Then
            MsgBox($MB_SYSTEMMODAL, "", "An error occurred whilst writing the debug file " & $sFilePath)
            Return False
    EndIf

    FileWrite($hFileOpen, @MDAY & "/" & @MON & "/" & @YEAR & " " & @HOUR & ":" & @MIN & ":" & @SEC & " - " & $mesage & @CRLF )

    ; Close the handle returned by FileOpen.
    FileClose($hFileOpen)
EndIf

EndFunc

#End Functions