LaunchScript.ps1

[CmdletBinding(SupportsShouldProcess)]
param (
    [Parameter(Mandatory)]
    [string[]]$Client,
    
    [Parameter(Mandatory)]
    [ValidateSet('Install', 'Upgrade', 'Uninstall', 'Detect')]
    [string]$Type,
    
    [Parameter(Mandatory)]
    [ValidateScript({ Test-Path -Path $_ -PathType Container })]
    [string]$ServerSideClientDeploymentFolderPath,
    
    [Parameter()]
    [string]$ServerModuleFolderPath = 'C:\Demos\DeploymentShare',
    
    [Parameter()]
    [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
    [string]$ModuleFilePath = "$ServerModuleFolderPath\SoftwareInstallManager.psm1",
    
    [Parameter()]
    [string]$AllowedLaunchServer = 'WINSRV2012R2-1.MYLAB.LOCAL',
    
    [Parameter()]
    [string]$ClientDeploymentFolder = 'C:\MyDeployment'
    
)

$ErrorActionPreference = 'Stop'
try
{
    #region Configuration Values
    
    #endregion
    
    #region Testing the waters
    
    ## Test to ensure our shared module exists where we think it does
    if (-not (Test-Path -Path $ModuleFilePath -PathType Leaf))
    {
        throw "SoftwareInstallManager module: BAD"
    }
    else
    {
        Write-Verbose -Message "SoftwareInstallManager module: GOOD"
    }
    ## Test to ensure the deployment script is where we think it is
    if (-not (Test-Path -Path $ServerSideClientDeploymentFolderPath -PathType Container))
    {
        throw "Deployment folder: BAD"
    }
    else
    {
        Write-Verbose -Message "Deployment folder: GOOD"
    }
    if (-not (Test-Path -Path "$ServerSideClientDeploymentFolderPath\$Type.ps1" -PathType Leaf))
    {
        throw "Deployment script: BAD"
    }
    else
    {
        Write-Verbose -Message "Deployment script: GOOD"
    }
    ## Test to ensure this script is launching on the "launch server"
    if ("$env:COMPUTERNAME.$env:USERDNSDOMAIN" -ne $AllowedLaunchServer)
    {
        throw "Appropriate launch server: BAD"
    }
    else
    {
        Write-Verbose -Message "Appropriate launch server: GOOD"
    }
    #endregion
    
    #region Inserting the Import-Module and Start-Log lines into the top of the deployment script
    
    $DeploymentScriptText = Get-Content -Path "$ServerSideClientDeploymentFolderPath\$Type.ps1"
    if ((($DeploymentScriptText | Select-Object -First 2) -join '|') -ne "Import-Module $ClientDeploymentFolder\SoftwareInstallManager.psm1|Start-Log")
    {
        ## The deployment script doesn't have the required Import-Module as the first line and Start-Log as the second line
        ## Insert Import-Module as the first line and Start-Log as the second line
        
        ## If there's already some other Import-Module line, delete it
        if (($DeploymentScriptText | Select-Object -First 1) -like 'Import-Module *')
        {
            ## Remove the top Import-Module line
            $DeploymentScriptText = ($DeploymentScriptText | Select-Object -Skip 1)
        }
        ## If some other Start-Log line then delete it also
        if (($DeploymentScriptText | Select-Object -First 1) -like 'Start-Log*')
        {
            ## Remove the top Start-Log line
            $DeploymentScriptText = ($DeploymentScriptText | Select-Object -Skip 1)
        }
        ## If some other working dir line then delete it also
        if (($DeploymentScriptText | Select-Object -First 1) -like '$WorkingDir*')
        {
            ## Remove the top Start-Log line
            $DeploymentScriptText = ($DeploymentScriptText | Select-Object -Skip 1)
        }
        
        ## Insert the new Import-Module line and the Start-Log line at the top of the deployment script
        $DeploymentScriptText = "Import-Module $ClientDeploymentFolder\SoftwareInstallManager.psm1", 'Start-Log', '$WorkingDir = $MyInvocation.MyCommand.Path | Split-Path -Parent', $DeploymentScriptText
        
        Set-Content -Path "$ServerSideClientDeploymentFolderPath\$Type.ps1" â€“value $DeploymentScriptText
    }
    
    #endregion
    
    #region Removing existing jobs
    Write-Verbose -Message 'Cleaning up any pre-existing jobs'
    Get-Job | Remove-Job -Force
    #endregion
    
    #region Testing and deployment
    
    ## We will be creating a folder in the root of the C drive called MyDeployment. We'll then copy
    ## all files including the deployment script and all installer files into that folder.
    ## Once copied, we'll then use PS remoting to execute the script
    ## We need to make sure all the services that require this are available; ping, c$ share
    ## available and PS remoting is working
    foreach ($pc in $Client)
    {
        try
        {
            if (-not (Test-Connection -ComputerName $pc -Quiet -Count 1))
            {
                throw "$pc`: OFFLINE"
                continue
            }
            else
            {
                Write-Verbose -Message "$pc`: ONLINE"
                ## Convert the mydeployment local path to a UNC so it can be tested and created
                $RemoteFilePathDrive = ($ClientDeploymentFolder | Split-Path -Qualifier).TrimEnd(':')
                $RemoteServerSideClientServerSideClientDeploymentFolderPath = "\\$pc\$RemoteFilePathDrive`$$($ClientDeploymentFolder | Split-Path -NoQualifier)"
                if (-not (Test-Path -Path "\\$pc\c$"))
                {
                    throw "$pc`: C`$ share is NOT available"
                    continue
                }
                else
                {
                    Write-Verbose -Message "$pc`: C`$ share is available"
                    if (-not (Test-WsMan -ComputerName $pc))
                    {
                        throw "$pc`: PS Remoting Problem"
                        continue
                    }
                    else
                    {
                        Write-Verbose -Message "$pc`: PS Remoting OK"
                        if ($PsCmdlet.ShouldProcess($pc, 'Invoke deployment on client'))
                        {
                            if (-not (Test-Path -Path $RemoteServerSideClientServerSideClientDeploymentFolderPath -PathType Container))
                            {
                                Write-Verbose -Message "The deployment folder $ClientDeploymentFolder does not exist on client $pc. Creating it."
                                $null = mkdir $RemoteServerSideClientServerSideClientDeploymentFolderPath
                            }
                            Write-Verbose -Message "$pc`: Copying deployment scripts to client"
                            Copy-Item -Path $ServerSideClientDeploymentFolderPath\* -Destination $RemoteServerSideClientServerSideClientDeploymentFolderPath -Force
                            Write-Verbose -Message "$pc`: Copying module folder to client"
                            Copy-Item -Path $ServerModuleFolderPath\* -Destination $RemoteServerSideClientServerSideClientDeploymentFolderPath -Force -Recurse
                            Write-Verbose -Message "$pc`: Launching deployment script"
                            $null = Invoke-Command -ComputerName $pc -ScriptBlock { & "$using:ClientDeploymentFolder\$using:Type.ps1" } -AsJob -ThrottleLimit 5
                            Write-Verbose -Message "$pc`: Deployment script launched"
                        }
                    }
                }
            }
        }
        catch
        {
            Write-Warning "$pc - $($_.Exception.Message)"
        }
    }
    
    #endregion
    
    #region Deployment monitoring
    $Timeout = 240
    $RunningTime = 0
    if ($PsCmdlet.ShouldProcess('Running deployments', 'Check status'))
    {
        do
        {
            $JobsLeft = (Get-Job | Where-Object { $_.State -in @('Running', 'NotStarted') }).Count
            if ($JobsLeft -gt 1)
            {
                Write-Verbose -Message "Waiting for all the deployments to finish up. There are $JobsLeft left."
            }
            else
            {
                Write-Verbose -Message 'Waiting for all the deployments to finish up. There is 1 left.'
            }
            Start-Sleep -Seconds 5
            $RunningTime += 5
            if ($RunningTime -eq $Timeout)
            {
                Write-Warning -Message "Timeout of $Timeout seconds has been exceeded waiting for jobs"
                break
            }
        }
        while (Get-Job | Where-Object { $_.State -in @('Running', 'NotStarted') })
    }
    #endregion
    
    #region Deployment result and cleanup
    
    Get-Job | Receive-Job
    
    Write-Verbose 'Cleaning up deployment folders on clients'
    foreach ($pc in $Client)
    {
        Remove-Item -Path "\\$pc\c$\$($ClientDeploymentFolder | Split-Path -Leaf)" -Recurse -ErrorAction SilentlyContinue
    }
    
    #endregion
    
}
catch
{
    Write-Warning $_.Exception.Message
    
}