alt-icons 1.2.7

A Windows executable that permanently changes its own icon, iOS-style: a closed set declared at build time, switched by name.
Documentation
# Embedded in the crate and run by system Windows PowerShell without a window.
# The caller defines ownerId, ownerStarted (UTC FILETIME), oldPath, and launchPath.
$ErrorActionPreference = 'Stop'

if ($ownerId -ne 0) {
    $owner = $null
    try {
        $owner = [System.Diagnostics.Process]::GetProcessById($ownerId)
        # Do not wait on an unrelated process if Windows has already reused the PID.
        if ($owner.StartTime.ToUniversalTime().ToFileTimeUtc() -eq $ownerStarted) {
            $owner.WaitForExit()
        }
    } catch {
        # The owner may have exited before the helper opened it.
    } finally {
        if ($null -ne $owner) { $owner.Dispose() }
    }
}

# Retry short-lived locks (for example an antivirus scan), but do not keep a
# helper alive forever for an inaccessible file. Startup cleanup remains a fallback.
$deadline = [DateTime]::UtcNow.AddSeconds(15)
while ($true) {
    try {
        [System.IO.File]::Delete($oldPath)
        break
    } catch {
        # Module inspection can still report the original launch path after a
        # rename. Wait for instances at either path. This can also wait for an
        # instance using the new image, but never deletes an image still in use.
        $readers = 0
        foreach ($candidate in [System.Diagnostics.Process]::GetProcesses()) {
            try {
                $modulePath = $candidate.MainModule.FileName
                if ($modulePath -eq $oldPath -or $modulePath -eq $launchPath) {
                    $readers++
                    $candidate.WaitForExit()
                }
            } catch {
                # A process can exit during enumeration or deny module inspection.
            } finally {
                $candidate.Dispose()
            }
        }
        if ($readers -gt 0) {
            $deadline = [DateTime]::UtcNow.AddSeconds(15)
        } elseif ([DateTime]::UtcNow -ge $deadline) {
            break
        }
        [System.Threading.Thread]::Sleep(200)
    }
}