Powershell : Deploy "file/software" with a tricky filename


Note : This post has been edited off blogger and imported as HTML to make the code easier to view with correct formatting

This post included the though process to deploy software across multiple servers which was quite challenging task due to the "tricky" filename that cannot be changed as this acts as a security token, this outlines the challenges overcome including but not limited to:

  • Handling files with special characters
  • Remote software installation
  • Cross-version compatibility
  • Robust error logging

The Deployment Challenges

Before we explore the script, let's understand the key challenges in enterprise software deployment:

  1. Special Character Filenames: Many enterprise software installers have cryptic, complex filenames with special characters.
  2. Remote Deployment: Installing software across multiple servers requires a consistent, automated approach.
  3. User Account Control (UAC): Navigating security prompts programmatically.
  4. Logging and Troubleshooting: Capturing detailed installation logs for verification.

Process Flow

Sorry about the crazy colours but this is what I had to work with, but the process flow is accurate:


Data Flow

The script is quite simple to execute but it took lots of "out of the box" thinking" to get this working like it should so you can see what talks to what.

Script Deployment

The deployment script is divided into two main sections:

  • Main Deployment Script: Orchestrates server-wide deployment
  • Remote Installation Script: Handles installation on individual servers

Main Deployment Script Flow

  1. Server Discovery

    $servers = Get-Content ".\servers.txt"
    
    • Reads a list of target servers from a text file
    • Enables easy server management without modifying the script
  2. File Preparation

    $robocopyCmd = "robocopy `"$scriptPath`" `"$remotePath`" `"$executableName`" 
    /J /R:1 /W:1"
    
    • Uses robust robocopy to copy installation files
    • Handles large files and network transfer challenges
    • Provides retry mechanisms for unstable networks
  3. Remote Script Execution

    $psExecCmd = "psexec.exe"
    $psExecArgs = @("\\$server", "-i", "-s", "powershell.exe", "-ExecutionPolicy",
     "Bypass", "-File", $remoteScriptPath)
    
    • Leverages PsExec for interactive, system-level remote script execution
    • Bypasses typical PowerShell remoting limitations

Remote Installation Script Innovations

  1. Wildcard-Proof File Matching

    $files = [System.IO.Directory]::GetFiles($installerDir)
    $exactMatchFile = $null
    foreach ($file in $files) {
        $fileName = $file -replace '.*\\', ''
        if ($fileName -eq $installerName) {
            $exactMatchFile = $file
            break
        }
    }
    
    • Uses .NET methods to bypass PowerShell's wildcard parsing (due to certain chracters)
    • Ensures exact filename matching across different PowerShell versions
  2. Elevated Process Launch

    $processInfo = New-Object System.Diagnostics.ProcessStartInfo
    $processInfo.FileName = $exactMatchFile
    $processInfo.Verb = 'runas'
    $process = [System.Diagnostics.Process]::Start($processInfo)
    
    • Launches installer with administrative privileges
    • Compatible with User Account Control (UAC)
  3. Comprehensive Logging

    function Write-DebugLog {
        param([string]$Message)
        $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
        Add-Content -Path $logPath -Value "[$timestamp] $Message"
        Write-Host $Message
    }
    
    • Creates detailed, timestamped logs
    • Enables precise troubleshooting
    • Logs are created both on the local and remote systems

Cross-Version Compatibility

The script is engineered to work across multiple Windows Server versions:

  1. Windows Server 2012 R2 (PowerShell 4.0)
  2. Windows Server 2016
  3. Windows Server 2019
  4. Windows Server 2022

Error Handling and Resilience

  1. Captures and logs detailed error information
  2. Provides multiple file and process launch strategies
  3. Implements timeout mechanisms to prevent hanging installations

Security Considerations

  • Uses runas verb for elevated privileges
  • Handles UAC prompts automatically
  • Provides installation logs for audit purposes

Real-World Usage Example

# servers.txt
Workstation1.bear.local
Workstation1.bear.local
Workstation1.bear.local
# Deploy script .\Deploy.ps1

Tricky Filename

This was the tricky filename I was dealing with:


bomgar-bear-w0eec30dwxgd71g66ghgf7w1fg2fzfy8z1x7zyjbqs0ij[2df[8j7[8^d777c40hc90.exe

This then required a script to see what Powershell violations this would break which is shown below, this also confirms that without -LiteralPath this code would fail.

Script : FileNameAnalyser.ps1

# Find the file starting with bomgar-bear
$foundFile = Get-ChildItem -Path "." -File | Where-Object { $_.Name -match '^bomgar-bear' } | Select-Object -First 1
if (-not $foundFile) {
    Write-Host "ERROR: No file starting with 'bomgar-pec' found."
    exit 1
}

# Define the filename
$executableName = $foundFile.Name
Write-Host "Found and testing with filename: $executableName"

# Test 1: Simple existence check with -LiteralPath
Write-Host "`nTest 1: Test-Path with -LiteralPath"
$exists = Test-Path -LiteralPath ".\$executableName"
Write-Host "File exists (Test-Path -LiteralPath): $exists"

# Test 2: Get item with -LiteralPath
Write-Host "`nTest 2: Get-Item with -LiteralPath"
try {
    $item = Get-Item -LiteralPath ".\$executableName" -ErrorAction Stop
    Write-Host "Get-Item -LiteralPath succeeded: $($item.FullName)"
} catch {
    Write-Host "Get-Item -LiteralPath failed: $($_.Exception.Message)"
}

# Test 3: Resolve-Path with -LiteralPath
Write-Host "`nTest 3: Resolve-Path with -LiteralPath"
try {
    $resolvedPath = Resolve-Path -LiteralPath ".\$executableName" -ErrorAction Stop
    Write-Host "Resolve-Path -LiteralPath succeeded: $resolvedPath"
} catch {
    Write-Host "Resolve-Path -LiteralPath failed: $($_.Exception.Message)"
}

# Test 4: Using Join-Path
Write-Host "`nTest 4: Join-Path approach"
$currentPath = (Get-Location).Path
$joinedPath = Join-Path -Path $currentPath -ChildPath $executableName
Write-Host "Joined path: $joinedPath"
$joinedExists = Test-Path -LiteralPath $joinedPath
Write-Host "Joined path exists: $joinedExists"

# Test 5: Test with escaped wildcards
Write-Host "`nTest 5: Testing with escaped wildcards"
$escapedName = $executableName -replace '\[', '`[' -replace '\]', '`]' -replace '\^', '`^'
Write-Host "Escaped filename: $escapedName"
try {
    $escapedExists = Test-Path ".\$escapedName"
    Write-Host "File exists with escaped name: $escapedExists"
} catch {
    Write-Host "Escaped name test failed: $($_.Exception.Message)"
}

# Test 6: Original Resolve-Path without -LiteralPath
Write-Host "`nTest 6: Resolve-Path without -LiteralPath (this will likely fail)"
try {
    $originalResolve = Resolve-Path ".\$executableName" -ErrorAction Stop
    Write-Host "Original Resolve-Path succeeded: $originalResolve"
} catch {
    Write-Host "Original Resolve-Path failed: $($_.Exception.Message)"
}

# Test 7: Test deployment script approach
Write-Host "`nTest 7: Test deployment script approach"
try {
    $sourceFile = Resolve-Path -LiteralPath ".\$executableName"
    Write-Host "Deployment approach succeeded: $sourceFile"
} catch {
    Write-Host "Deployment approach failed: $($_.Exception.Message)"
}

# Provide recommendations
Write-Host "`n-----------------------------------------------"
Write-Host "Based on the test results, here's what to use in your deployment script:"
Write-Host "-----------------------------------------------"
Write-Host "Option 1: Use Resolve-Path with -LiteralPath"
Write-Host '    $sourceFile = Resolve-Path -LiteralPath ".\$executableName"'
Write-Host "`nOption 2: Use Join-Path"
Write-Host '    $currentDir = (Get-Location).Path'
Write-Host '    $sourceFile = Join-Path -Path $currentDir -ChildPath $executableName'

This would then confirm that without -LiteralPath this would fail:

Test 6: Resolve-Path without -LiteralPath (this will likely fail)
Original Resolve-Path failed: The specified wildcard character pattern is not valid: bomgar-bear-w0eec30dwxgd71g66ghgf7w1fg2fzfy8z1x7zyjbqs0ij[2df[8j7[8^d777c40hc90.exe

Working Script Directory

This is the working script directory with they key files highlighted in blue:


Remote Deploy Script 

The script would copy over the file, with robocopy and this "remote" script that would run on that server:

# Explicit error handling
$ErrorActionPreference = 'Stop'

# Create log file
$logPath = '\\deploy.bear.local\C$\Temp\Install\remote_debug.log'
function Write-DebugLog {
    param([string]$Message)
    $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    Add-Content -Path $logPath -Value "[] $Message"
    Write-Host $Message
}

Write-DebugLog "Starting installation process"

# Bypass PowerShell wildcard parsing completely
$installerDir = '\\deploy.bear.local\C$\Temp\Install'
$installerName = 'bomgar-bear-w0eec30dwxgd71g66ghgf7w1fg2fzfy8z1x7zyjbqs0ij[2df[8j7[8^d777c40hc90.exe'

# Use .NET to list files and find exact match
$files = [System.IO.Directory]::GetFiles($installerDir)
$exactMatchFile = $files | Where-Object { 
    $_ -replace '.*\\', '' -eq $installerName 
}

if ($exactMatchFile) {
    Write-DebugLog ("Installer found: " + $exactMatchFile)
    
    try {
        # Use .NET Process class to launch
        $processInfo = New-Object System.Diagnostics.ProcessStartInfo
        $processInfo.FileName = $exactMatchFile
        $processInfo.WorkingDirectory = $installerDir
        $processInfo.UseShellExecute = $false
        
        # Attempt to run with elevated privileges
        $processInfo.Verb = 'runas'
        
        $process = [System.Diagnostics.Process]::Start($processInfo)
        
        Write-DebugLog ("Process launched. Process ID: " + $process.Id)
        
        # Wait for process to potentially exit
        $process.WaitForExit(60000)  # Wait up to 1 minute
        
        Write-DebugLog ("Process exit code: " + $process.ExitCode)
    }
    catch {
        Write-DebugLog ("ERROR launching process: " + $_.Exception.Message)
        $_ | Out-File '\\deploy.bear.local\C$\Temp\Install\launch_error.log'
    }
}
else {
    Write-DebugLog "ERROR: Installer NOT FOUND"
    "Exact match not found" | Out-File '\\deploy.bear.local\C$\Temp\Install\not_found.log'
}

Write-DebugLog "Installation script completed"

Logs Remote Computer

This would then also create a log file when the install runs to confirm all worked as it should then it passes the exit code "0" back to the host script:

[] Starting installation process
[] Installer found: \\deploy.bear.local\C$\Temp\Install\bomgar-bear-w0eec30dwxgd71g66ghgf7w1fg2fzfy8z1x7zyjbqs0ij[2df[8j7[8^d777c40hc90.exe
[] Process launched. Process ID: 4524
[] Process exit code: 0
[] Installation script completed

Where is the script?

The script is not available on this blog because it’s quite lengthy so if you would like it, please email me at scripts@a6n.co.uk
Previous Post Next Post

نموذج الاتصال