ps1 script to deploy custom mouse cursors works, but it needs additional manual push

Vladimir Jovanovski 0 Reputation points
2025-11-05T23:25:04.1033333+00:00

The script

<#
.SYNOPSIS
    Automates deployment of custom mouse cursors via Active Directory GPO.

.DESCRIPTION
    Creates a Group Policy Object that deploys custom cursor files and configures
    registry settings to apply them.

.PARAMETER GPOName
    Name of the GPO to create. Default: "Custom Mouse Cursors"

.PARAMETER Target
    Distinguished Name of the OU or domain to link the GPO to.
    Example: "DC=contoso,DC=com"

.PARAMETER SourcePath
    UNC path where cursor files are located.
    Example: "\\domain.com\share\cursors"

.EXAMPLE
    .\Deploy-CursorGPO.ps1 -Target "DC=contoso,DC=com" -SourcePath "\\domain.com\share\cursors"
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory=$false)]
    [string]$GPOName = "Custom Mouse Cursors",
    
    [Parameter(Mandatory=$true)]
    [string]$Target,
    
    [Parameter(Mandatory=$true)]
    [string]$SourcePath
)

# Import required modules
Import-Module GroupPolicy -ErrorAction Stop
Import-Module ActiveDirectory -ErrorAction Stop

# Define cursor mappings
$cursorMappings = @(
    @{
        Name = "Normal Select"
        SourceFile = "Normal Select.cur"
        RegistryValue = "Arrow"
    },
    @{
        Name = "Help Select"
        SourceFile = "Help Select.cur"
        RegistryValue = "Help"
    },
    @{
        Name = "Working in Background"
        SourceFile = "Working in Background.ani"
        RegistryValue = "AppStarting"
    },
    @{
        Name = "Busy"
        SourceFile = "Busy.ani"
        RegistryValue = "Wait"
    },
    @{
        Name = "Text Select"
        SourceFile = "Text Select.cur"
        RegistryValue = "IBeam"
    },
    @{
        Name = "Handwriting"
        SourceFile = "Handwriting.cur"
        RegistryValue = "Crosshair"
    },
    @{
        Name = "Link Select"
        SourceFile = "Link Select.cur"
        RegistryValue = "Hand"
    }
)

try {
    Write-Host "Starting GPO creation process..." -ForegroundColor Cyan
    
    # Validate source path
    if (-not (Test-Path $SourcePath)) {
        throw "Source path '$SourcePath' does not exist or is not accessible."
    }
    
    # Validate cursor files exist
    Write-Host "Validating cursor files..." -ForegroundColor Yellow
    foreach ($cursor in $cursorMappings) {
        $filePath = Join-Path $SourcePath $cursor.SourceFile
        if (-not (Test-Path $filePath)) {
            throw "Cursor file not found: $filePath"
        }
    }
    Write-Host "All cursor files validated successfully." -ForegroundColor Green
    
    # Create GPO
    Write-Host "Creating GPO '$GPOName'..." -ForegroundColor Yellow
    $gpo = New-GPO -Name $GPOName -Comment "Deploys Archaeological cursor theme to domain computers"
    Write-Host "GPO created with GUID: $($gpo.Id)" -ForegroundColor Green
    
    # Link GPO to target
    Write-Host "Linking GPO to: $Target..." -ForegroundColor Yellow
    New-GPLink -Name $GPOName -Target $Target -LinkEnabled Yes | Out-Null
    Write-Host "GPO linked successfully." -ForegroundColor Green
    
    # Configure registry preferences
    Write-Host "Configuring registry preferences..." -ForegroundColor Yellow
    $registryKey = "HKEY_CURRENT_USER\Control Panel\Cursors"
    
    foreach ($cursor in $cursorMappings) {
        $destinationPath = "C:\Windows\Cursors\$($cursor.SourceFile)"
        
        Set-GPPrefRegistryValue -Name $GPOName `
            -Context User `
            -Action Update `
            -Key $registryKey `
            -ValueName $cursor.RegistryValue `
            -Value $destinationPath `
            -Type String | Out-Null
        
        Write-Host "  - Set registry value: $($cursor.RegistryValue)" -ForegroundColor Gray
    }
    Write-Host "Registry preferences configured successfully." -ForegroundColor Green
    
    # Get domain information
    $domain = Get-ADDomain
    $domainDN = $domain.DistinguishedName
    $domainName = $domain.DNSRoot
    
    # Build GPO path in SYSVOL
    $gpoPath = "\\$domainName\SYSVOL\$domainName\Policies\{$($gpo.Id)}"
    $machinePrefsPath = Join-Path $gpoPath "Machine\Preferences\Files"
    
    # Create Files preference directory
    Write-Host "Creating Files preference directory..." -ForegroundColor Yellow
    if (-not (Test-Path $machinePrefsPath)) {
        New-Item -Path $machinePrefsPath -ItemType Directory -Force | Out-Null
    }
    
    # Generate Files.xml for file copy preferences
    Write-Host "Generating Files.xml for file copy operations..." -ForegroundColor Yellow
    
    $filesXml = @"
<?xml version="1.0" encoding="utf-8"?>
<Files clsid="{215B2E53-57CE-475c-80FE-9EEC14635851}">
"@
    
    foreach ($cursor in $cursorMappings) {
        $sourceFile = Join-Path $SourcePath $cursor.SourceFile
        $destFile = "C:\Windows\Cursors\$($cursor.SourceFile)"
        $uid = [guid]::NewGuid().ToString("B").ToUpper()
        
        $filesXml += @"

    <File clsid="{50BE44C8-567A-4ed1-B1D0-9234FE1F38AF}" name="$($cursor.SourceFile)" status="$($cursor.SourceFile)" image="2" changed="$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" uid="$uid" userContext="0" removePolicy="0">
        <Properties action="U" fromPath="$sourceFile" targetPath="$destFile" readOnly="0" archive="1" hidden="0" suppress="0"/>
    </File>
"@
    }
    
    $filesXml += @"

</Files>
"@
    
    # Write Files.xml
    $filesXmlPath = Join-Path $machinePrefsPath "Files.xml"
    $filesXml | Out-File -FilePath $filesXmlPath -Encoding utf8 -Force
    Write-Host "Files.xml created successfully." -ForegroundColor Green
    
    # Update GPT.ini version numbers
    Write-Host "Updating GPT.ini version..." -ForegroundColor Yellow
    $gptIniPath = Join-Path $gpoPath "GPT.ini"
    $gptContent = Get-Content $gptIniPath -Raw
    
    # Increment both machine and user versions
    if ($gptContent -match 'Version=(\d+)') {
        $currentVersion = [int]$matches[1]
        $newVersion = $currentVersion + 65537  # Increment both machine (65536) and user (1)
        $gptContent = $gptContent -replace 'Version=\d+', "Version=$newVersion"
        $gptContent | Out-File -FilePath $gptIniPath -Encoding ascii -Force
    }
    
    Write-Host "GPT.ini updated successfully." -ForegroundColor Green
    
    Write-Host "`n========================================" -ForegroundColor Cyan
    Write-Host "GPO Deployment Complete!" -ForegroundColor Green
    Write-Host "========================================" -ForegroundColor Cyan
    Write-Host "GPO Name: $GPOName" -ForegroundColor White
    Write-Host "GPO GUID: {$($gpo.Id)}" -ForegroundColor White
    Write-Host "Linked to: $Target" -ForegroundColor White
    Write-Host "`nNext Steps:" -ForegroundColor Yellow
    Write-Host "1. Verify GPO settings in Group Policy Management Console" -ForegroundColor White
    Write-Host "2. Run 'gpupdate /force' on client computers to apply immediately" -ForegroundColor White
    Write-Host "3. Or wait 90-120 minutes for automatic policy refresh" -ForegroundColor White
    
} catch {
    Write-Host "`nError: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host "Stack Trace: $($_.ScriptStackTrace)" -ForegroundColor Red
    exit 1
}

After running the script and gpupdate /force, the key registries get setup and in the GPO in Computer Configuration > Preferences > Windows Settings > Files the entries for the cursors files are present with correct source and target paths. But next to their names there are tiny yellow triangles. I open properties for any of the files and just click Ok without changing anything.

GPO And after this when I run gpupdate /force on client PCs or on the server all the files are sent/copied to the destination folder.

When I configure the same GPO using the GUI console I don't face with this issue.

What I'm missing in the script?

Windows for business | Windows Server | Directory services | Deploy group policy objects
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Quinnie Quoc 7,400 Reputation points Independent Advisor
    2025-11-06T00:21:47.41+00:00

    Hello,

    Thank you for your detailed explanation and for sharing the script. The behavior you're seeing—yellow warning triangles next to file entries in the GPO—is typically caused by missing metadata in the XML configuration, which the GUI console automatically populates when you manually configure the GPO.

    In your script, the <File> elements in the Files.xml may be missing required attributes or values that the Group Policy engine expects. While clicking "OK" in the GUI resolves it by refreshing the entry, you can avoid this by ensuring the XML includes all necessary fields, such as status, image, and changed values formatted correctly.

    As a workaround, consider using the Set-GPPrefFile cmdlet instead of manually generating Files.xml, or let the GUI console handle file preference creation while your script manages registry and linking tasks.

    If this guidance helps resolve the issue, feel free to hit ACCEPT THE ANSWER to support me more!!

    Thank you so much!!

    QQ


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.