Resources/Install.ps1
|
#Requires -RunAsAdministrator <# .SYNOPSIS Installs an application based on logic defined in Install.json. Simple alternative to PSAppDeployToolkit. .DESCRIPTION This script reads package installation configuration from Install.json in the current directory and executes the installation, pre-installation tasks (stopping processes, uninstalling previous versions), and post-installation tasks (copying files, running additional scripts). Includes automatic 64-bit process restart if running in 32-bit session on 64-bit OS. .EXAMPLE PS C:\> .\Install.ps1 Executes the installation using configuration from Install.json in the current directory. .OUTPUTS System.Int32 Returns the exit code from the installer (0 for success). .NOTES Author: Aaron Parker - Requires Install.json configuration file in the current working directory - Logs are written to $Env:ProgramData\Microsoft\IntuneManagementExtension\Logs\PSPackageFactoryInstall.log - Supports both EXE and MSI installers #> [CmdletBinding(SupportsShouldProcess = $true)] param () # Set strict mode and error handling Set-StrictMode -Version "Latest" $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop $InformationPreference = [System.Management.Automation.ActionPreference]::Continue $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 # Log file path. Parent directory should exist if device is enrolled in Intune $Script:LogFile = "$Env:ProgramData\Microsoft\IntuneManagementExtension\Logs\PSPackageFactoryInstall.log" #region Logging Function function Write-LogFile { <# .SYNOPSIS This function creates or appends a line to a log file .DESCRIPTION This function writes a log line to a log file in the form synonymous with ConfigMgr logs so that tools such as CMtrace and SMStrace can easily parse the log file. It uses the ConfigMgr client log format's file section to add the line of the script in which it was called. .PARAMETER Message The message parameter is the log message you'd like to record to the log file .PARAMETER LogLevel The logging level is the severity rating for the message you're recording. Like ConfigMgr clients, you have 3 severity levels available; 1, 2 and 3 from informational messages for FYI to critical messages that stop the install. This defaults to 1. .EXAMPLE PS C:\> Write-LogFile -Message 'Value1' -LogLevel 'Value2' This example shows how to call the Write-LogFile function with named parameters. .NOTES Constantin Lotz; Adam Bertram, https://github.com/adbertram/PowerShellTipsToWriteBy/blob/f865c4212284dc25fe613ca70d9a4bafb6c7e0fe/chapter_7.ps1#L5 #> [CmdletBinding(SupportsShouldProcess = $false)] param ( [Parameter(Position = 0, ValueFromPipeline = $true, Mandatory = $true)] [System.String] $Message, [Parameter(Position = 1, Mandatory = $false)] [ValidateSet(1, 2, 3)] [System.Int16] $LogLevel = 1 ) process { ## Build the line which will be recorded to the log file $TimeGenerated = "$(Get-Date -Format HH:mm:ss).$((Get-Date).Millisecond)+000" $LineFormat = $Message, $TimeGenerated, (Get-Date -Format "yyyy-MM-dd"), "$($MyInvocation.ScriptName | Split-Path -Leaf):$($MyInvocation.ScriptLineNumber)", $LogLevel $Line = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="" type="{4}" thread="" file="">' -f $LineFormat Write-Information -MessageData $Message -InformationAction "Continue" Add-Content -Value $Line -Path $Script:LogFile } } #endregion #region Restart if running in a 32-bit session if (!([System.Environment]::Is64BitProcess)) { if ([System.Environment]::Is64BitOperatingSystem) { # Create a string from the passed parameters [System.String]$ParameterString = "" foreach ($Parameter in $PSBoundParameters.GetEnumerator()) { $ParameterString += " -$($Parameter.Key) $($Parameter.Value)" } # Execute the script in a 64-bit process with the passed parameters $Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$($MyInvocation.MyCommand.Definition)`"$ParameterString" $ProcessPath = $(Join-Path -Path $Env:SystemRoot -ChildPath "\Sysnative\WindowsPowerShell\v1.0\powershell.exe") Write-LogFile -Message "Restarting in 64-bit PowerShell." Write-LogFile -Message "File path: $ProcessPath." Write-LogFile -Message "Arguments: $Arguments." $params = @{ FilePath = $ProcessPath ArgumentList = $Arguments Wait = $true WindowStyle = "Hidden" } Start-Process @params exit 0 } } #endregion #region Installer functions function Get-InstallConfig { [CmdletBinding()] param ( [System.String] $File = "Install.json", [System.Management.Automation.PathInfo] $Path = $PWD ) try { $InstallFile = Join-Path -Path $Path -ChildPath $File Write-LogFile -Message "Read package install config: $InstallFile" $Config = Get-Content -Path $InstallFile -Raw | ConvertFrom-Json if ($null -eq $Config.PSObject.Properties["LogPath"] -or $null -eq $Config.LogPath) { $Config | Add-Member -MemberType NoteProperty -Name "LogPath" -Value "" -Force } if ($null -eq $Config.PSObject.Properties["InstallTasks"] -or $null -eq $Config.InstallTasks) { $Config | Add-Member -MemberType NoteProperty -Name "InstallTasks" -Value ([PSCustomObject]@{}) -Force } if ($null -eq $Config.InstallTasks.PSObject.Properties["ArgumentList"] -or $null -eq $Config.InstallTasks.ArgumentList) { $Config.InstallTasks | Add-Member -MemberType NoteProperty -Name "ArgumentList" -Value "" -Force } if ($null -eq $Config.InstallTasks.PSObject.Properties["UninstallMsi"] -or $null -eq $Config.InstallTasks.UninstallMsi) { $Config.InstallTasks | Add-Member -MemberType NoteProperty -Name "UninstallMsi" -Value @() -Force } if ($null -eq $Config.InstallTasks.PSObject.Properties["Wait"] -or $null -eq $Config.InstallTasks.Wait) { $Config.InstallTasks | Add-Member -MemberType NoteProperty -Name "Wait" -Value 0 -Force } if ($null -eq $Config.PSObject.Properties["PostInstall"] -or $null -eq $Config.PostInstall) { $Config | Add-Member -MemberType NoteProperty -Name "PostInstall" -Value ([PSCustomObject]@{}) -Force } if ($null -eq $Config.PostInstall.PSObject.Properties["StopPath"] -or $null -eq $Config.PostInstall.StopPath) { $Config.PostInstall | Add-Member -MemberType NoteProperty -Name "StopPath" -Value @() -Force } if ($null -eq $Config.PostInstall.PSObject.Properties["Remove"] -or $null -eq $Config.PostInstall.Remove) { $Config.PostInstall | Add-Member -MemberType NoteProperty -Name "Remove" -Value @() -Force } if ($null -eq $Config.PostInstall.PSObject.Properties["CopyFile"] -or $null -eq $Config.PostInstall.CopyFile) { $Config.PostInstall | Add-Member -MemberType NoteProperty -Name "CopyFile" -Value @() -Force } if ($null -eq $Config.PostInstall.PSObject.Properties["Run"] -or $null -eq $Config.PostInstall.Run) { $Config.PostInstall | Add-Member -MemberType NoteProperty -Name "Run" -Value @() -Force } return $Config } catch { Write-LogFile -Message "Get-InstallConfig: $($_.Exception.Message)" -LogLevel 3 throw $_ } } function Get-Installer { [CmdletBinding()] param ( [System.String] $File, [System.Management.Automation.PathInfo] $Path = $PWD ) $Installer = Get-ChildItem -Path $Path -Filter $File -Recurse | Select-Object -First 1 if ($null -eq $Installer -or [System.String]::IsNullOrEmpty($Installer.FullName)) { Write-LogFile -Message "File not found: $File" -LogLevel 3 throw [System.IO.FileNotFoundException]::New("File not found: $File") } else { Write-LogFile -Message "Found installer: $($Installer.FullName)" return $Installer.FullName } } function Copy-File { [CmdletBinding(SupportsShouldProcess = $true)] param ( [System.Array] $File, [System.Management.Automation.PathInfo] $Path = $PWD ) process { foreach ($Item in $File) { try { $FilePath = Get-ChildItem -Path $Path -Filter $Item.Source -Recurse if ($null -eq $FilePath) { Write-LogFile -Message "Copy-File: Source file not found: $($Item.Source)" -LogLevel 2 continue } Write-LogFile -Message "Copy-File: Source: $($FilePath.FullName)" Write-LogFile -Message "Copy-File: Destination: $($Item.Destination)" $params = @{ Path = $FilePath.FullName Destination = $Item.Destination Container = $false Force = $true } Copy-Item @params } catch { Write-LogFile -Message "Copy-File: $($_.Exception.Message)" -LogLevel 3 Write-Warning -Message $_.Exception.Message } } } } function Remove-Path { [CmdletBinding(SupportsShouldProcess = $true)] param ( [System.String[]] $Path ) process { foreach ($Item in $Path) { try { if (Test-Path -Path $Item -PathType "Container") { $params = @{ Path = $Item Recurse = $true Force = $true } Remove-Item @params Write-LogFile -Message "Remove-Item: $Item" } else { $params = @{ Path = $Item Force = $true } Remove-Item @params Write-LogFile -Message "Remove-Item: $Item" } } catch { Write-LogFile -Message "Remove-Path error: $($_.Exception.Message)" -LogLevel 3 Write-Warning -Message $_.Exception.Message } } } } function Stop-PathProcess { [CmdletBinding(SupportsShouldProcess = $true)] param ( [System.String[]] $Path, [System.Management.Automation.SwitchParameter] $Force ) process { foreach ($Item in $Path) { try { Get-Process | Where-Object { $_.Path -like $Item } | ForEach-Object { Write-LogFile -Message "Stop-PathProcess: $($_.ProcessName)" } if ($PSBoundParameters.ContainsKey("Force")) { Get-Process | Where-Object { $_.Path -like $Item } | ` Stop-Process -Force } else { Get-Process | Where-Object { $_.Path -like $Item } | ` Stop-Process } } catch { Write-LogFile -Message "Stop-PathProcess error: $($_.Exception.Message)" -LogLevel 2 Write-Warning -Message $_.Exception.Message } } } } function Uninstall-Msi { [CmdletBinding(SupportsShouldProcess = $true)] param ( [System.String[]] $ProductName, [System.String] $LogPath ) process { foreach ($Item in $ProductName) { if ($PSCmdlet.ShouldProcess($Item)) { try { $Product = Get-CimInstance -Class "Win32_InstalledWin32Program" | Where-Object { $_.Name -like $Item } if ($null -eq $Product) { Write-LogFile -Message "Product not found: $Item" -LogLevel 2 continue } $params = @{ FilePath = "$Env:SystemRoot\System32\msiexec.exe" ArgumentList = "/uninstall `"$($Product.MsiProductCode)`" /quiet /log `"$LogPath\Uninstall-$($Item -replace " ").log`"" NoNewWindow = $true PassThru = $true Wait = $true } $result = Start-Process @params Write-LogFile -Message "$Env:SystemRoot\System32\msiexec.exe /uninstall `"$($Product.MsiProductCode)`" /quiet /log `"$LogPath\Uninstall-$($Item -replace " ").log`"" Write-LogFile -Message "Msiexec result: $($result.ExitCode)" return $result.ExitCode } catch { Write-LogFile -Message "Uninstall-Msi error: $($_.Exception.Message)" -LogLevel 3 Write-Warning -Message $_.Exception.Message } } } } } #endregion #region Install logic # Trim log if greater than 50 MB if (Test-Path -Path $Script:LogFile) { if ((Get-Item -Path $Script:LogFile).Length -gt 50MB) { Clear-Content -Path $Script:LogFile Write-LogFile -Message "Log file size greater than 50MB. Clearing log." -LogLevel 2 } } # Get the install details for this application $Install = Get-InstallConfig $Installer = Get-Installer -File $Install.PackageInformation.SetupFile if ([System.String]::IsNullOrEmpty($Installer)) { Write-LogFile -Message "File not found: $($Install.PackageInformation.SetupFile)" -LogLevel 3 throw [System.IO.FileNotFoundException]::New("File not found: $($Install.PackageInformation.SetupFile)") } else { # Stop processes before installing the application if ($null -ne $Install.InstallTasks.StopPath -and $Install.InstallTasks.StopPath.Count -gt 0) { Stop-PathProcess -Path $Install.InstallTasks.StopPath } # Uninstall the application if ($null -ne $Install.InstallTasks.UninstallMsi -and $Install.InstallTasks.UninstallMsi.Count -gt 0) { Uninstall-Msi -ProductName $Install.InstallTasks.UninstallMsi -LogPath $Install.LogPath } if ($null -ne $Install.InstallTasks.Remove -and $Install.InstallTasks.Remove.Count -gt 0) { Remove-Path -Path $Install.InstallTasks.Remove } # Create the log folder if (Test-Path -Path $Install.LogPath -PathType "Container") { Write-LogFile -Message "Directory exists: $($Install.LogPath)" } else { Write-LogFile -Message "Create directory: $($Install.LogPath)" New-Item -Path $Install.LogPath -ItemType "Directory" | Out-Null } # Build the argument list $ArgumentList = $Install.InstallTasks.ArgumentList -replace "#SetupFile", $Installer $ArgumentList = $ArgumentList -replace "#LogName", $Install.PackageInformation.SetupFile $ArgumentList = $ArgumentList -replace "#LogPath", $Install.LogPath $ArgumentList = $ArgumentList -replace "#PWD", $PWD.Path $ArgumentList = $ArgumentList -replace "#SetupDirectory", ([System.IO.Path]::GetDirectoryName($Installer)) try { # Perform the application install $result = @{ ExitCode = 0 } switch ($Install.PackageInformation.SetupType) { "EXE" { Write-LogFile -Message "Installer: $Installer" Write-LogFile -Message "ArgumentList: $ArgumentList" $params = @{ FilePath = $Installer ArgumentList = $ArgumentList NoNewWindow = $true PassThru = $true Wait = $true } if ($PSCmdlet.ShouldProcess($Installer, $ArgumentList)) { $result = Start-Process @params } } "MSI" { Write-LogFile -Message "Installer: $Env:SystemRoot\System32\msiexec.exe" Write-LogFile -Message "ArgumentList: $ArgumentList" $params = @{ FilePath = "$Env:SystemRoot\System32\msiexec.exe" ArgumentList = $ArgumentList NoNewWindow = $true PassThru = $true Wait = $true } if ($PSCmdlet.ShouldProcess("$Env:SystemRoot\System32\msiexec.exe", $ArgumentList)) { $result = Start-Process @params } } default { Write-LogFile -Message "$($Install.PackageInformation.SetupType) not found in the supported setup types - EXE, MSI." -LogLevel 3 throw "$($Install.PackageInformation.SetupType) not found in the supported setup types - EXE, MSI." } } # If wait specified, wait the specified seconds if ($null -ne $Install.InstallTasks.Wait -and $Install.InstallTasks.Wait -gt 0) { Start-Sleep -Seconds $Install.InstallTasks.Wait } # Stop processes after installing the application if ($null -ne $Install.PostInstall.StopPath -and $Install.PostInstall.StopPath.Count -gt 0) { Stop-PathProcess -Path $Install.PostInstall.StopPath } # Perform post install actions if ($null -ne $Install.PostInstall.Remove -and $Install.PostInstall.Remove.Count -gt 0) { Remove-Path -Path $Install.PostInstall.Remove } if ($null -ne $Install.PostInstall.CopyFile -and $Install.PostInstall.CopyFile.Count -gt 0) { Copy-File -File $Install.PostInstall.CopyFile } # Execute run tasks if ($null -ne $Install.PostInstall.Run -and $Install.PostInstall.Run.Count -gt 0) { foreach ($Task in $Install.PostInstall.Run) { & $Task } } } catch { Write-LogFile -Message $_.Exception.Message -LogLevel 3 throw $_ } finally { Write-LogFile -Message "Install.ps1 complete. Exit Code: $($result.ExitCode)" exit $result.ExitCode } } #endregion |