Automation Examples

Prev Next

Automation Examples

Return Code Script

This sample script demonstrates how to add a return code to an automation. It is provided for example purposes only and is not part of a released automation.

ReturnCodeSample.ps1

<#
.SYNOPSIS
    ExampleAutomationReturnCode.ps1

.DESCRIPTION
    This script is used for Advanced Disk Clean that contains multiple user profiles.

.INPUTS
    None

.OUTPUTS
    All output is written into a log file in the same location from which the script is run.

.NOTES
    Version:            1.10.4
    Author:             Lakeside Software, carsten.giese@lakesidesoftware.com
    Requirements:       This script assumed to be run by the SysTrack agent (local SYSTEM account). The 
                        folder deletion action requires local administrator permissions.

    Changes:            1.0.0 - Initial Release
                        1.1.0 - Stability improvements, unique error codes, dynamic path discovery
                        1.2.0 - WMI-based profile deletion, safe junction handling
                        1.2.1 - Removed dangerous fallback for profiles directory discovery
                        1.2.2 - Refactored temp cleanup to use array-based approach
                        1.3.0 - Dynamic drive detection from profiles path using .NET DriveInfo
                        1.4.0 - Enhanced logging for all deletion operations
                        1.5.0 - Added diagnostic logging for profile evaluation and skip conditions
                        1.6.0 - Added check for missing timestamp data to identify orphaned profiles
                        1.6.1 - Replaced hardcoded orphaned age with boolean flag, improved profile logging
                        1.6.2 - Changed internal units to MB with explicit variable naming
                        1.7.0 - Refactored freed space thresholds into configurable hashtable
                        1.8.0 - Added dynamic unit formatting for freed space display
                        1.9.0 - Added protected system profile names to prevent deletion
                        1.9.1 - Fixed logic to allow temp cleanup for all profiles, preserve temp folder structure
                        1.9.2 - Optimized flow to cleanup first then check deletion, reduced error spam
                        1.10.0 - Added comprehensive temp cleanup logging for visibility
                        1.10.1 - Improved variable naming for clarity and safety
                        1.10.2 - Two-pass bottom-up deletion for better locked-file handling
                        1.10.3 - Added threshold sorting for robust return code determination
                        1.10.4 - Further improved variable naming for code clarity
                
                        Copyright 2020 LAKESIDE SOFTWARE, INC.

.LINK
    http://www.lakesidesoftware.com
#>

# Clean the output screen and reset all cached variables
cls
Remove-Variable * -ea 0

# Set the default for what we want to happen if an unexpected error occurs
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7.3#erroractionpreference
$ErrorActionPreference = "Stop"

# Initialize script-level variables
$script:resetLog = $true
$scriptPath = $null
$logfile = $null

# Logging function with error-safe file operations
function log($text) {
    $line = ''
    if ($text) {
        $time = [datetime]::Now.ToUniversalTime().tostring('s')
        $line = [string]::Join(' - ', $time, $text)
    }
    
    # Console output if available
    if ($psISE -or -not [Console]::IsOutputRedirected) {
        Write-Output $line
    }
    
    # File output with error suppression to prevent logging failures from crashing script
    try {
        if ($script:resetLog) {
            $line | Out-File $logfile -Encoding utf8 -Force -ErrorAction Stop
            $script:resetLog = $false
        } else {
            $line | Out-File $logfile -Encoding utf8 -Append -ErrorAction Stop
        }
    }
    catch {
        # Silently continue if logging fails - don't crash the script
    }
}

# Format disk size from MB to appropriate unit for display
function Format-DiskSize($sizeInMB) {
    if ($sizeInMB -ge 1024) {
        return "$([math]::Round($sizeInMB/1024, 2))GB"
    } elseif ($sizeInMB -ge 1) {
        return "$([math]::Round($sizeInMB, 2))MB"
    } elseif ($sizeInMB -ge 0.001) {
        return "$([math]::Round($sizeInMB * 1024, 2))KB"
    } else {
        return "$([math]::Round($sizeInMB * 1024 * 1024, 2))bytes"
    }
}

# Main script body
$scriptExitCode = 0
$profileDeletionAgeThresholdInDays = 60
$skipNonADProfiles = $true

# Define system profile names to protect from deletion
$protectedProfileNames = @(
    "WsiAccount",
    "DefaultAccount",
    "WDAGUtilityAccount",
    "Administrator",
    "Guest",
    "Public",
    "Default"
)

# Define profile subdirectories to clean for all profiles
$profileSubfoldersToClean = @(
    "AppData\Local\Temp",
    "AppData\Local\Temporary Internet Files"
)

# Define freed space thresholds and corresponding return codes
$freedSpaceReturnCodes = @(
    @{ ThresholdMB = 100; ReturnCode = 101; Description = "Less than 100MB freed" }
    @{ ThresholdMB = 500; ReturnCode = 102; Description = "100MB to 500MB freed" }
    @{ ThresholdMB = [int]::MaxValue; ReturnCode = 103; Description = "500MB or more freed" }
)

try {
    # Determine script location
    if ($PSScriptRoot) {
        $scriptPath = $MyInvocation.MyCommand.Path
    } elseif ($psISE -and $psISE.CurrentFile) {
        if ($psISE.CurrentFile.IsSaved -and $psISE.CurrentFile.FullPath) {
            $scriptPath = $psISE.CurrentFile.FullPath
        } else {
            throw "Script is not saved in ISE"
        }
    } else {
        throw "Unable to determine script location"
    }
    $logfile = $scriptPath + ".log"
    
    # Log script initialization
    log "Script starting ..."
    log "Powershell Version: $($PSVersionTable.PSVersion)"
    log ".NET CLR Version: $([System.Environment]::Version)"

    # --- START OF VERSION CHECK ---
    $requiredPSMajorVersion = 5
    $currentPSMajorVersion = $PSVersionTable.PSVersion.Major

    if ($currentPSMajorVersion -lt $requiredPSMajorVersion) {
        log "ERROR: Minimum required PowerShell Major version is $($requiredPSMajorVersion)"
        $scriptExitCode = 1
        throw
    }
    
    # Log execution context
    $CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    log "Running As: $($CurrentUser.Name)"
    if (([Security.Principal.WindowsPrincipal]$CurrentUser).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
        log "Is an Administrator"
    } else {
        log "ERROR: Script needs to be started as admin or local System."
        $scriptExitCode = 2
        throw
    }
    
    # Check architecture
    log "Architecture: $($Env:PROCESSOR_ARCHITECTURE)"
    if ($env:PROCESSOR_ARCHITEW6432 -eq "AMD64") {
        log "Running 32-bit Powershell on 64-bit Windows"
    }
    
    # Discover user profiles directory from registry
    log "Discovering user profiles directory from registry"
    try {
        $profilesKey = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" -Name ProfilesDirectory -ErrorAction Stop
        $usersPath = $profilesKey.ProfilesDirectory
        
        if (-not $usersPath -or -not (Test-Path $usersPath)) {
            throw "ProfilesDirectory registry value is invalid or path does not exist"
        }
        
        log "User profiles directory: $usersPath"
    }
    catch {
        log "ERROR: Failed to discover user profiles directory from registry"
        $scriptExitCode = 3
        throw
    }
    
    # Extract drive letter from profiles path
    $driveLetter = Split-Path -Qualifier $usersPath
    log "Target drive: $driveLetter"
    
    # Get initial disk information
    log "Getting initial disk information"
    try {
        $driveInfo = [System.IO.DriveInfo]::new($driveLetter)
        $freeSpace_before_MB = [math]::Round($driveInfo.AvailableFreeSpace/1MB, 2)
        log "Disk Free Space before script run: $([math]::Round($freeSpace_before_MB/1024, 2))GB`n"
    }
    catch {
        log "ERROR: Failed to get initial disk information"
        $scriptExitCode = 4
        throw
    }
    
    # Enumerate WMI user profiles
    log "Enumerating WMI user profiles"
    try {
        $wmiProfiles = Get-WMIObject -class Win32_UserProfile -ErrorAction Stop
        log "Found $($wmiProfiles.Count) WMI user profiles"
    }
    catch {
        log "ERROR: Failed to enumerate WMI user profiles"
        $scriptExitCode = 5
        throw
    }
    
    # Enumerate registry profile entries
    log "Enumerating registry ProfileList entries"
    try {
        $profileListKey = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\*"
        $registryProfiles = Get-ItemProperty $profileListKey -ErrorAction Stop
        log "Found $($registryProfiles.Count) registry profile entries`n"
    }
    catch {
        log "ERROR: Failed to enumerate registry ProfileList entries"
        $scriptExitCode = 6
        throw
    }
    
    # Process each registry profile entry
    $currentDateTime = [datetime]::Now
    $profilesProcessed = 0
    $profilesSkipped = 0
    
    foreach ($profileRegistry in $registryProfiles) {
        $profileSID = $profileRegistry.PSChildName
        $profilePath = $profileRegistry.ProfileImagePath
        
        # Log profile identification with path if available, otherwise SID
        if ($profilePath) {
            log "Evaluating profile: $profilePath ($profileSID)"
        } else {
            log "Evaluating profile SID: $profileSID (no ProfileImagePath)"
        }
        
        # Get corresponding WMI profile object
        $wmiProfile = $wmiProfiles | Where-Object {$_.SID -eq $profileSID}
        
        # Perform temp cleanup for ALL profiles first
        if ($profilePath -and (Test-Path $profilePath -PathType Container -ea 0)) {
            foreach ($profileSubfolder in $profileSubfoldersToClean) {
                $subfolderPath = Join-Path $profilePath $profileSubfolder
                if (Test-Path $subfolderPath -PathType Container) {
                    log "Attempting to clean folder: $profileSubfolder"
                    $successCount = 0
                    $failureCount = 0
                    
                    try {
                        # Get all files in subfolder
                        $filesToDelete = Get-ChildItem -Path $subfolderPath -Recurse -Force -File -ErrorAction SilentlyContinue
                        
                        # Delete all files first
                        foreach ($file in $filesToDelete) {
                            try {
                                [System.IO.File]::Delete($file.FullName)
                                $successCount++
                            }
                            catch {
                                $failureCount++
                            }
                        }
                        
                        # Get all directories and sort by path length (deepest first)
                        $directoriesToDelete = Get-ChildItem -Path $subfolderPath -Recurse -Force -Directory -ErrorAction SilentlyContinue
                        $directoriesSorted = $directoriesToDelete | Sort-Object { $_.FullName.Length } -Descending
                        
                        # Delete folders deepest-first
                        foreach ($directory in $directoriesSorted) {
                            try {
                                [System.IO.Directory]::Delete($directory.FullName, $false)
                                $successCount++
                            }
                            catch {
                                $failureCount++
                            }
                        }
                        
                        log "Cleaned $successCount items, $failureCount items could not be deleted (in use by other processes)"
                    }
                    catch {
                        log "Failed to access subfolder: ..\$profileSubfolder"
                    }
                } else {
                    log "Subfolder does not exist: ..\$profileSubfolder"
                }
            }
        }
        
        # Now check deletion criteria
        # Skip special profiles from deletion
        if ($wmiProfile.Special) {
            log "Special profile - skipping deletion`n"
            $profilesSkipped++
            continue
        }
        
        # Skip non-AD profiles from deletion if configured
        if ($skipNonADProfiles -and !$profileSID.StartsWith('S-1-5-21')) {
            log "Non-AD profile - skipping deletion`n"
            $profilesSkipped++
            continue
        }
        
        # Skip protected system profiles from deletion
        if ($profilePath) {
            $profileFolderName = Split-Path -Leaf $profilePath
            if ($protectedProfileNames -contains $profileFolderName) {
                log "Protected system profile: $profileFolderName - skipping deletion`n"
                $profilesSkipped++
                continue
            }
        }
        
        # Calculate age and check for orphaned status (only now after filters pass)
        $isOrphaned = $false
        $profileAgeInDays = 0
        
        if ($profileRegistry.State -eq $null) {
            # Orphaned profile with no state
            $isOrphaned = $true
            log "Orphaned profile detected (no State value)"
        } elseif ($null -eq $profileRegistry.PSObject.Properties['LocalProfileLoadTimeHigh']) {
            # Orphaned profile with missing timestamp data
            $isOrphaned = $true
            log "Orphaned profile detected (missing load timestamp)"
        } else {
            # Calculate age from LocalProfileLoadTime
            $high = [uint64]$profileRegistry.LocalProfileLoadTimeHigh
            $low = [uint64]$profileRegistry.LocalProfileLoadTimeLow
            $lastLoadTime = [datetime]::FromFileTime($high -shl 32 -bor $low)
            $profileAgeInDays = ($currentDateTime - $lastLoadTime).TotalDays
        }
        
        # Skip profiles with unexpected age values from deletion
        if (!$isOrphaned -and (!$profileAgeInDays -or $profileAgeInDays -gt 5000)) {
            log "Profile has invalid age ($profileAgeInDays days) - skipping deletion`n"
            $profilesSkipped++
            continue
        }
        
        $profilesProcessed++
        
        # Check if profile should be deleted based on age or orphaned status
        if ($isOrphaned -or $profileAgeInDays -gt $profileDeletionAgeThresholdInDays) {
            # Delete old or orphaned profile
            if ($isOrphaned) {
                log "Profile marked for deletion (orphaned)"
            } else {
                log "Profile marked for deletion - Age: $([math]::Round($profileAgeInDays, 2)) days - Exceeds threshold"
            }
            
            if (Test-Path $profilePath -PathType Container) {
                log "Attempting to remove profile folder and data"
                try {
                    # Using .NET Directory.Delete() to safely handle junctions without following them into different locations
                    [System.IO.Directory]::Delete($profilePath, $true)
                    log "Successfully deleted profile folder: $profilePath"
                }
                catch {
                    log "Failed to delete profile folder: $profilePath"
                }
            } else {
                log "Profile folder does not exist, will remove WMI object only"
            }
            
            # If folder was successfully deleted or didn't exist, remove WMI profile object
            if (-not (Test-Path $profilePath -PathType Container)) {
                log "Attempting to remove WMI profile object"
                try {
                    $wmiProfile | Remove-WmiObject -ErrorAction Stop
                    log "Successfully removed WMI profile object`n"
                }
                catch {
                    log "Failed to remove WMI profile object"
                    log $_.Exception.Message
                }
            } else {
                log "Skipping WMI removal - profile folder still exists`n"
            }
        }
        else {
            # Profile is below age threshold
            log "Active profile - Age: $([math]::Round($profileAgeInDays, 2)) days - Below threshold`n"
        }
    }
    
    log "Profile processing summary: $profilesProcessed processed, $profilesSkipped skipped`n"
    
    # Get final disk information
    log "Getting final disk information"
    try {
        $driveInfo = [System.IO.DriveInfo]::new($driveLetter)
        $freeSpace_after_MB = [math]::Round($driveInfo.AvailableFreeSpace/1MB, 2)
    }
    catch {
        log "ERROR: Failed to get final disk information"
        $scriptExitCode = 7
        throw
    }
    
    $FreedSpace_MB = [math]::Max(0, $freeSpace_after_MB - $freeSpace_before_MB)
    log "Disk Free Space after script run: $([math]::Round($freeSpace_after_MB/1024, 2))GB"
    log "Total freed space: $(Format-DiskSize $FreedSpace_MB)"
    
    # Determine return code based on freed space thresholds
    $sortedThresholds = $freedSpaceReturnCodes | Sort-Object { $_.ThresholdMB }
    foreach ($threshold in $sortedThresholds) {
        if ($FreedSpace_MB -lt $threshold.ThresholdMB) {
            $scriptExitCode = $threshold.ReturnCode
            break
        }
    }
}
catch {
    # General exception handler
    if ($scriptExitCode -eq 0) {
        log "ERROR: Unexpected exception occurred during script execution"
        log $error[0].FullyQualifiedErrorId
        log $error[0].Exception.ToString()
        log $error[0].ScriptStackTrace
        $scriptExitCode = 6
    }
}
finally {
    log "Script complete returning ($scriptExitCode)"
    Exit $scriptExitCode
}

ReturnCodeSample.bat

@ECHO OFF
PUSHD %~dps0

SET "SCRIPTFILE=ReturnCodeSample.ps1"
IF NOT EXIST "%SCRIPTFILE%" SET "SCRIPTFILE=%~n0.ps1"

:: Execute the PowerShell script:
SET "SYS32=System32"
IF "%PROCESSOR_ARCHITECTURE%" == "x86" SET "SYS32=SysNative"
SET "POWERSHELL=%WINDIR%\%SYS32%\WindowsPowerShell\v1.0\powershell.exe"
"%POWERSHELL%" -NOPROFILE -EXECUTIONPOLICY BYPASS -FILE "%SCRIPTFILE%" %* > "%~nx0.log" 2>&1
SET "EXITCODE=%ERRORLEVEL%"

POPD
EXIT /B %EXITCODE%

Script Output

Sample Hello World Script

This is an automation example. The HelloWorldLogs.bat script is not maintained and is not guaranteed to work. Use at your own risk.

@echo off
PowerShell -ExecutionPolicy Bypass -File "HelloWorldLogs.ps1"
EXIT /B %ERRORLEVEL%

HelloWorldLogs.ps1
<#
.SYNOPSIS
    HelloWorld.PS1

.DESCRIPTION
    This script will pop up a Hello World Window

.INPUTS
    None

.OUTPUTS
    When enabled, a log file will be created in the same location from which the script is run.

.NOTES
    Version:            1.0
    Author:             Lakeside Software
    Requirements:       This script is assumed to be run by the SysTrack agent (local SYSTEM account).
                        This script is NOT maintained and is not guaranteed to work and is meant as an example
                        
                        USE AT YOUR OWN RISK

                        Copyright 2023 LAKESIDE SOFTWARE, INC.

.LINK
    http://www.lakesidesoftware.com
#>

#####
# Begin Functions
#####

    #####
    # Function: LogWrite (Logging)
    #####
    function LogWrite([string]$info)
    {       
        if($script:log -eq $true)
        {   
            # Write to logfile date - message
            "$(get-date -format "yyyy-MM-dd HH:mm:ss") -  $info" >> $logfile
            # Any logged Start or Exit statements also write to host
            If ( ($info.contains("Starting Script")) -or ($info.contains("Exiting Script")) ) {
                Write-Host $info
            }
            # Any logged Warning or Error statements also write to host
            If ( ($info.contains("Error:")) -or ($info.contains("Warning"))) {
                Write-Host "  " $info  "(See Log For Details)"
            }
        }
        else  {
            Write-Host $info
        }
    }              

    #####
    # Function: NewFile (Logging, Location)
    #####
    function NewFile($data, $loc)
    {  
        If (test-path $loc) {Remove-Item $loc}
        "$data" >> $loc  
        LogWrite "Data saved to $loc"
    }   
    
    #####
    # Function: ExitScript
    #####
    function ExitScript($msg, $ExitCode)
    {
        ### Cleanup
        If ($Silent) {
            Write-Progress -Activity "Working..." `
            -Completed -Status $msg
        }
        
        ### Log Exit
        LogWrite "$msg ($ExitCode)"
        LogWrite "Exiting Script..."
        Logwrite ""
        #Exit
        Exit($ExitCode)
    }   

#####
# End Functions
#####

#####
# Advanced Variables
#####   
        #logging
    If (!$Logging) {$Logging = $true}
    $ErrorActionPreference = "Stop"
    $debug = $false
    $retain = $false # new log on each run
    
    $ScriptName = & { $myInvocation.ScriptName }
    $ScriptPath = Split-Path -parent $ScriptName
    $ScriptName = Split-Path $ScriptName -Leaf
    $ScriptName = $ScriptName.Replace(".PS1","")
    
    $logfile = "$ScriptPath\$ScriptName" + "Log.txt"
    $script:log = $Logging
    $Dev = "False"
    
#####
# Start Script
#####
    cls
    
    ### Delete existing log files > 1mb
    If (test-path $logfile) {
        If ($retain -eq $true) {
            If ((Get-Item $logfile).length -gt 1mb) {
                Remove-Item $logfile
            }
        }
        Else {
            Remove-Item $logfile
        }
    }
    
#####
# Start Script
#####

    ### Start Script
    LogWrite ""
    LogWrite "Starting Script..."
    
    $WhoAmI = [Environment]::UserName
    $Is64Bit = [Environment]::Is64BitProcess
    
    LogWrite "Running As:  $WhoAmI"
    LogWrite "64bit Process:  $Is64Bit"
    
    #LogWrite "About to try writing hello world to a file..."
    try{
        LogWrite "Start Hello World! Invoking Forms."
        Add-Type -AssemblyName System.Windows.Forms
        LogWrite "Hello World!!"
    }
    catch{
        LogWrite "Unable to popup Hello World prompt due"
        LogWrite $error[0].FullyQualifiedErrorId
        LogWrite $error[0].Exception.ToString()
        LogWrite $error[0].ScriptStackTrace
        ExitScript -msg "Script Failed" -code 1
    }

#####
# Exit Script
#####
    
    ExitScript -msg "Script Completed" -code 0