Public/Install-vlApplication.ps1

Function Install-vlApplication {
    <#
.SYNOPSIS
    Installs an application either locally or remotely
.PARAMETER Installer
    Specifies the path to the installer. Supported installers are .exe, .msi, and .ps1 files.
    When the installer requires additional files the parameter '-Sourcefiles' is needed, too.
.PARAMETER InstallerArgument
    Specifies the silent installer arguments. 'Install-vlApplication' handles required silent arguments for msis und PowerShell scripts automatically.
    Arguments for PowerShell skripts are not supported.
.PARAMETER Sourcefiles
    Only needed when the installer needs additional files. If you specify this parameter, the installer has to be in the root of your sourcefiles.
        - Correct: Install-vlApplication -Sourcefiles C:\myapp -Installer Setup.exe
        - Wrong: Install-vlApplication -Sourcefiles C:\myapp -Installer C:\myapp\subfolder\Setup.exe
.PARAMETER Computer
    A computer name or a list of computer names where the application should be installed. Defaults to the local computer. Pipeline input is supported.
.PARAMETER Credential
    Specifies the credentials when installation on a remote computer is desired.
.PARAMETER RebootAfterSuccess
    Reboots the targeted computer if the installation was successful
.PARAMETER ValidExitCodes
    Specifies the exit code for a successful installation.
.EXAMPLE
    PS C:\> Install-vlApplication -Computer PC1 -Installer Setup.exe -InstallerArguments '/silent /noreboot' -Credential (Get-Credential)
    Simple application install on a remote machine
.EXAMPLE
    PS C:\> 'PC1', 'PC2', 'PC3' | Install-vlApplication -Installer Setup.msi -Credential (Get-Credential)
    'Install-vlApplication' accepts pipeline input for the '-Computer' parameter , which makes it easy to mass-deploy applications.
.INPUTS
    Function accepts pipeline input for '-Computer'
.OUTPUTS
    Outputs a PowerShell object containing results of the installation(s)
.NOTES
    Author: vast limits GmbH
#>

    [OutputType([System.Management.Automation.PSObject])]
    [CmdletBinding()]
    PARAM
    (
        [Parameter(Mandatory = $true)]
        [System.String]$Installer,
    
        [Parameter()]
        [System.String]$InstallerArguments,

        [Parameter()]
        [ValidateScript( {
                if (-Not ($_ | Test-Path) ) {
                    throw "Folder does not exist" 
                }
                if (-Not ($_ | Test-Path -PathType Container) ) {
                    throw "The sourcefiles argument must be a folder. File paths are not allowed."
                }
                return $true
            })]
        [System.IO.FileInfo]$Sourcefiles,

        [Parameter(ValueFromPipeline = $True)]
        [Alias('Computername', '__Server', 'CN')]
        [System.String[]]$Computer = $env:computername,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,
        
        [Parameter()]
        [switch]$RebootAfterSuccess,

        [Parameter()]
        [int32[]]$ValidExitCodes = @("0", "3010")
    )

    Begin {
        $ErrorActionPreference = 'Stop'
        Try {
            # Initialize output
            $Results = @()

            # Sourcefiles and installer checks
            $InstallerIsURL = $false
            If ($Sourcefiles) {
                $FullInstallerPath = Join-Path -Path $Sourcefiles -ChildPath $Installer.split('\\')[-1]
                if (-not (Test-Path $FullInstallerPath)) {
                    Throw "Cannot find installer. Installer has to be in the root of $Sourcefiles. Current full path: $FullInstallerPath"
                }
            }
            Else {
                if ($Installer -match '^https?://') {
                    Write-Verbose "Installer is an URL. Download installer."
                    $OutFile = Join-Path -Path $env:TMP -ChildPath $Installer.split('/')[-1]
                    if (-not(Test-Path $OutFile)) {
                        Invoke-WebRequest -UseBasicParsing -Uri $Installer -OutFile $OutFile
                        if ($OutFile.split('.')[-1] -eq 'ps1'){
                            Write-Verbose "Installer is a PowerShell script. Unblock file."
                            Unblock-File -Path $OutFile
                        }
                    }
                    else {
                        Write-Verbose "Installer $OutFile already exists"
                    }
                    $Installer = $OutFile
                    $InstallerIsURL = $true
                }
            }

            # Reset variables depending on installer
            $InstallerExtension = $Installer.Split('.')[-1]
            Write-Verbose "Installer extension = $InstallerExtension"

            switch ($InstallerExtension) {
                msi {
                    $Log = $Installer.split('\\')[-1] + '.log' # take only the installer filename and append .log
                    $PSBoundParameters['InstallerArguments'] -replace '/i', '' -replace '/qn', '' -replace '/qb', '' -replace '/quiet', '' -replace '/passive', '' -replace '/qr', '' -replace '/qf', ''
                    $PSBoundParameters['InstallerArguments'] = '/i' + ' ' + $Installer.split('\\')[-1] + ' ' + "/qn /norestart /l*v `"C:\Windows\temp\$Log`"" + ' ' + $InstallerArguments
                    $PSBoundParameters['Installer'] = 'msiexec.exe'
                }

                ps1 {
                    If ($InstallerArguments) {
                        Throw 'Running PowerShell files with arguments is not supported'
                    }
                    $PSBoundParameters['InstallerArguments'] = '-executionpolicy bypass -NoProfile -file ' + $Installer.split('\\')[-1]
                    $PSBoundParameters['Installer'] = Join-Path -Path $env:windir -ChildPath "System32\WindowsPowerShell\v1.0\powershell.exe"
                }

                exe {
                    $PSBoundParameters['Installer'] = $Installer.split('\\')[-1]
                }

                Default {
                    Throw "$InstallerExtension files are not supported"
                }
            }

            #
        }
        Catch {
            Write-Error -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"
        }
    }
   
    Process {
        Try {
            $scriptBlock = {
                If ($args[0].ContainsKey('Verbose')) {
                    $VerbosePreference = 'Continue'
                }
                $args[0].GetEnumerator() | ForEach-Object { New-Variable -Name $_.Key -Value $_.Value }
                Write-Verbose "Start installation"
                Write-Verbose "Installer: $Installer"
                Write-Verbose "Installer arguments: $InstallerArguments"
                
                $InstallFolder = "C:\Windows\temp\vl_sourcefiles"
                Set-Location $InstallFolder
                $ExitCode = (Start-Process -FilePath $Installer -ArgumentList $InstallerArguments -Wait -Passthru).ExitCode
                Set-Location $env:SystemRoot

                Remove-Item $InstallFolder -Recurse -Force

                Write-Output -InputObject $ExitCode
            }

            if ($Computer -eq $env:COMPUTERNAME) {
                $Mode = 'local'
                $c = $env:COMPUTERNAME
                
                Write-Verbose "Clear mess of old installations if any"
                Remove-Item "C:\Windows\Temp\vl_Sourcefiles" -Recurse -Force -ErrorAction SilentlyContinue

                Write-Verbose "Copy sourcefiles"
                New-Item -Path "C:\Windows\Temp\vl_sourcefiles" -Force -ItemType Directory | Out-Null
                If ($Sourcefiles) {
                    Copy-Item -Path "$Sourcefiles\*" -Destination "C:\Windows\Temp\vl_sourcefiles" -Recurse -Force
                }
                Else {
                    Copy-Item -Path $Installer -Destination "C:\Windows\Temp\vl_sourcefiles" -Force
                }

                $ExitCode = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $PSBoundParameters

                # after installation
                if ($ValidExitCodes -notcontains $ExitCode) {
                    $Results += [PSCustomObject]@{Computer = $c; Success = $false }
                    Write-Error "Installation not successful. Exit code: $ExitCode"
                }
                else {
                    Write-Verbose "Installation successful. Exit code: $ExitCode"
                    $Results += [PSCustomObject]@{Computer = $c; Success = $true }
                    If ($RebootAfterSuccess) {
                        Write-Verbose "Reboot desired by parameter. Reboot $c."
                        Restart-Computer -ComputerName $c -Timeout 30
                    }
                }
            }
            else {
                $Mode = 'remote'
                foreach ($c in $Computer) {
                    Write-Verbose "Test connectivitiy to computer $c"
                    if (Test-vlConnection -Computer $c -TestAdminShare) {

                        Write-Verbose "Clear mess of old installations if any"
                        Invoke-Command -ComputerName $c -ScriptBlock { Remove-Item "C:\Windows\Temp\vl_sourcefiles" -Recurse -Force -ErrorAction SilentlyContinue } -Credential $Credential -Verbose:$VerbosePreference

                        Write-Verbose "Copy sourcefiles to target"
                        $FreeDrive = (68..90 | ForEach-Object { $Letter = [char]$_; if ((Get-PSDrive).Name -notContains $Letter) { $Letter } })[0] # Get free drive from D onwards
                        $RemoteDrive = (New-PSDrive -Name $FreeDrive -PSProvider FileSystem -Root "\\$c\c$\windows\temp" -Credential $Credential).Root

                        New-Item -Path $RemoteDrive -Name 'vl_sourcefiles' -Force -ItemType Directory | Out-Null
                        If ($Sourcefiles) {
                            Copy-Item -Path "$Sourcefiles\*" -Destination "$RemoteDrive\vl_sourcefiles" -Recurse -Force
                        }
                        Else { 
                            Copy-Item -Path $Installer -Destination "$RemoteDrive\vl_sourcefiles" -Force
                        }
                        Remove-PSDrive -Name $FreeDrive -Force

                        $ExitCode = Invoke-Command -ComputerName $c -ScriptBlock $scriptBlock -ArgumentList $PSBoundParameters -Credential $Credential -Verbose:$VerbosePreference

                        # after installation
                        if ($ValidExitCodes -notcontains $ExitCode) {
                            $Results += [PSCustomObject]@{Computer = $c; Success = $false }
                            Write-Error "Installation not successful. Exit code: $ExitCode"
                        }
                        else {
                            Write-Verbose "Installation successful on computer $c. Exit code: $ExitCode"
                            $Results += [PSCustomObject]@{Computer = $c; Success = $true }
                            If ($RebootAfterSuccess) {
                                Write-Verbose "Reboot desired by parameter. Reboot $c."
                                Restart-Computer -ComputerName $c -Timeout 30
                            }
                        }
                    }
                }
            }
        }
        catch {
            $Results += [PSCustomObject]@{Computer = $c; Result = $false }
            Write-Error -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"

            # cleanup
            if ($Mode -eq 'local') {
                Remove-Item "c:\windows\temp\vl_sourcefiles" -Recurse -Force -ErrorAction SilentlyContinue
            }
            elseif ($Mode -eq 'remote') {
                Invoke-Command -ComputerName $c -ScriptBlock { Remove-Item "C:\Windows\Temp\vl_sourcefiles" -Recurse -Force -ErrorAction SilentlyContinue } -Credential $Credential
                Remove-PSDrive -Name $FreeDrive -Force -ErrorAction SilentlyContinue
            }
        }

        Finally {
            
        }
        
    }
    End {
        if ($InstallerIsURL) {
            Remove-Item -Path $OutFile -Force -ErrorAction SilentlyContinue
        }
        Write-Output $Results
    }
}