Public/Psrunner/Start-ProcessAsAdmin.ps1
function Start-ProcessAsAdmin { <# .SYNOPSIS Runs a process with administrative privileges. If `-ExeToRun` is not specified, it is run with PowerShell. .NOTES Administrative Access Required. .INPUTS None .OUTPUTS None .PARAMETER Statements Arguments to pass to `ExeToRun` or the PowerShell script block to be run. .PARAMETER ExeToRun The executable/application/installer to run. Defaults to `'powershell'`. .PARAMETER Elevated Indicate whether the process should run elevated/aS Admin. Available in 0.10.2+. .PARAMETER Minimized Switch indicating if a Windows pops up (if not called with a silent argument) that it should be minimized. .PARAMETER NoSleep Used only when calling PowerShell - indicates the window that is opened should return instantly when it is complete. .PARAMETER ValidExitCodes Array of exit codes indicating success. Defaults to `@(0)`. .PARAMETER WorkingDirectory The working directory for the running process. Defaults to `Get-Location`. If current location is a UNC path, uses `$env:TEMP` for default as of 0.10.14. Available in 0.10.1+. .PARAMETER SensitiveStatements Arguments to pass to `ExeToRun` that are not logged. Note that only licensed versions of Chocolatey provide a way to pass those values completely through without having them in the install script or on the system in some way. Available in 0.10.1+. .PARAMETER IgnoredArguments Allows splatting with arguments that do not apply. Do not use directly. .EXAMPLE Start-ProcessAsAdmin -Statements "$msiArgs" -ExeToRun 'msiexec' .EXAMPLE Start-ProcessAsAdmin -Statements "$silentArgs" -ExeToRun $file .EXAMPLE Start-ProcessAsAdmin -Statements "$silentArgs" -ExeToRun $file -ValidExitCodes @(0,21) .EXAMPLE > # Run PowerShell statements $psFile = Join-Path "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" 'someInstall.ps1' Start-ProcessAsAdmin "& `'$psFile`'" .EXAMPLE # This also works for cmd and is required if you have any spaces in the paths within your command $appPath = "$env:ProgramFiles\myapp" $cmdBatch = "/c `"$appPath\bin\installmyappservice.bat`"" Start-ProcessAsAdmin $cmdBatch cmd # or more explicitly Start-ProcessAsAdmin -Statements $cmdBatch -ExeToRun "cmd.exe" .LINK Install-DotfilePackage .LINK Install-DotfilePackage #> [CmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory = $false, Position = 0)][string[]] $statements, [parameter(Mandatory = $false, Position = 1)][string] $exeToRun = 'powershell', [parameter(Mandatory = $false)][switch] $elevated, [parameter(Mandatory = $false)][switch] $minimized, [parameter(Mandatory = $false)][switch] $noSleep, [parameter(Mandatory = $false)] $validExitCodes = @(0), [parameter(Mandatory = $false)][string] $workingDirectory = $null, [parameter(Mandatory = $false)][string] $sensitiveStatements = '' ) DynamicParam { $DynamicParams = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() #region IgnoredArguments $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $attributes = [System.Management.Automation.ParameterAttribute]::new(); $attHash = @{ Position = 8 ParameterSetName = '__AllParameterSets' Mandatory = $False ValueFromPipeline = $true ValueFromPipelineByPropertyName = $true ValueFromRemainingArguments = $true HelpMessage = 'Allows splatting with arguments that do not apply. Do not use directly.' DontShow = $False }; $attHash.Keys | ForEach-Object { $attributes.$_ = $attHash.$_ } $attributeCollection.Add($attributes) # $attributeCollection.Add([System.Management.Automation.ValidateSetAttribute]::new([System.Object[]]$ValidateSetOption)) # $attributeCollection.Add([System.Management.Automation.ValidateRangeAttribute]::new([System.Int32[]]$ValidateRange)) # $attributeCollection.Add([System.Management.Automation.ValidateNotNullOrEmptyAttribute]::new()) # $attributeCollection.Add([System.Management.Automation.AliasAttribute]::new([System.String[]]$Aliases)) $RuntimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new("IgnoredArguments", [Object[]], $attributeCollection) $DynamicParams.Add("IgnoredArguments", $RuntimeParam) #endregion IgnoredArguments return $DynamicParams } Process { $PsCmdlet.MyInvocation.BoundParameters.GetEnumerator() | ForEach-Object { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue' } [string]$statements = $statements -join ' ' # Log Invocation and Parameters used. $MyInvocation, $PSBoundParameters if ($null -eq $workingDirectory) { # $pwd = $(Get-Location -PSProvider 'FileSystem') if ($pwd -eq $null -or $null -eq $pwd.ProviderPath) { Write-Debug "Unable to use current location for Working Directory. Using Cache Location instead." $workingDirectory = $env:TEMP } $workingDirectory = $pwd.ProviderPath } $alreadyElevated = $false [bool]$IsAdmin = $((New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)); if ($IsAdmin) { $alreadyElevated = $true } else { Write-Warning "$fxn : [!] It seems You're not Admin [!] " break } $dbMessagePrepend = "Elevating permissions and running" if (!$elevated) { $dbMessagePrepend = "Running" } try { if ($null -ne $exeToRun) { $exeToRun = $exeToRun -replace "`0", "" } if ($null -ne $statements) { $statements = $statements -replace "`0", "" } } catch { Write-Debug "Removing null characters resulted in an error - $($_.Exception.Message)" } if ($null -ne $exeToRun) { $exeToRun = $exeToRun.Trim().Trim("'").Trim('"') } $wrappedStatements = $statements if ($null -eq $wrappedStatements) { $wrappedStatements = '' } if ($exeToRun -eq 'powershell') { $exeToRun = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe" $importChocolateyHelpers = "& import-module -name '$helpersPath\chocolateyInstaller.psm1' -Verbose:`$false | Out-Null;" $block = @" `$noSleep = `$$noSleep #`$env:dotfilesEnvironmentDebug='false' #`$env:dotfilesEnvironmentVerbose='false' $importChocolateyHelpers try{ `$progressPreference="SilentlyContinue" $statements if(!`$noSleep){start-sleep 6} } catch{ if(!`$noSleep){start-sleep 8} throw } "@ $encoded = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($block)) $wrappedStatements = "-NoLogo -NonInteractive -NoProfile -ExecutionPolicy Bypass -InputFormat Text -OutputFormat Text -EncodedCommand $encoded" $dbgMessage = @" $dbMessagePrepend powershell block: $block This may take a while, depending on the statements. "@ } else { $dbgMessage = @" $dbMessagePrepend [`"$exeToRun`" $wrappedStatements]. This may take a while, depending on the statements. "@ } Write-Debug $dbgMessage $exeIsTextFile = [System.IO.Path]::GetFullPath($exeToRun) + ".istext" if ([System.IO.File]::Exists($exeIsTextFile)) { Set-PowerShellExitCode 4 throw "The file was a text file but is attempting to be run as an executable - '$exeToRun'" } if ($exeToRun -eq 'msiexec' -or $exeToRun -eq 'msiexec.exe') { $exeToRun = "$($env:SystemRoot)\System32\msiexec.exe" } if (!([System.IO.File]::Exists($exeToRun)) -and $exeToRun -notmatch 'msiexec') { Write-Warning "May not be able to find '$exeToRun'. Please use full path for executables." # until we have search paths enabled, let's just pass a warning #Set-PowerShellExitCode 2 #throw "Could not find '$exeToRun'" } # Redirecting output slows things down a bit. $writeOutput = { if ($null -ne $EventArgs.Data) { Write-Verbose "$($EventArgs.Data)" } } $writeError = { if ($null -ne $EventArgs.Data) { Write-Error "$($EventArgs.Data)" } } $process = New-Object System.Diagnostics.Process $process.EnableRaisingEvents = $true Register-ObjectEvent -InputObject $process -SourceIdentifier "LogOutput_ChocolateyProc" -EventName OutputDataReceived -Action $writeOutput | Out-Null Register-ObjectEvent -InputObject $process -SourceIdentifier "LogErrors_ChocolateyProc" -EventName ErrorDataReceived -Action $writeError | Out-Null #$process.StartInfo = New-Object System.Diagnostics.ProcessStartInfo($exeToRun, $wrappedStatements) # in case empty args makes a difference, try to be compatible with the older # version $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $exeToRun if ($wrappedStatements -ne '') { $psi.Arguments = "$wrappedStatements" } if ($null -ne $sensitiveStatements -and $sensitiveStatements -ne '') { Write-Info "Sensitive arguments have been passed. Adding to arguments." $psi.Arguments += " $sensitiveStatements" } $process.StartInfo = $psi # process start info $process.StartInfo.RedirectStandardOutput = $true $process.StartInfo.RedirectStandardError = $true $process.StartInfo.UseShellExecute = $false $process.StartInfo.WorkingDirectory = $workingDirectory if ($elevated -and !$alreadyElevated -and [Environment]::OSVersion.Version -ge (New-Object 'Version' 6, 0)) { # this doesn't actually currently work - because we are not running under shell execute Write-Debug "Setting RunAs for elevation" $process.StartInfo.Verb = "RunAs" } if ($minimized) { $process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Minimized } if ($PSCmdlet.ShouldProcess("`$process", "Start Process")) { $process.Start() | Out-Null if ($process.StartInfo.RedirectStandardOutput) { $process.BeginOutputReadLine() } if ($process.StartInfo.RedirectStandardError) { $process.BeginErrorReadLine() } $process.WaitForExit() # For some reason this forces the jobs to finish and waits for # them to do so. Without this it never finishes. Unregister-Event -SourceIdentifier "LogOutput_ChocolateyProc" Unregister-Event -SourceIdentifier "LogErrors_ChocolateyProc" # sometimes the process hasn't fully exited yet. for ($loopCount = 1; $loopCount -le 15; $loopCount++) { if ($process.HasExited) { break; } Write-Debug "Waiting for process to exit - $loopCount/15 seconds"; Start-Sleep 1; } $exitCode = $process.ExitCode $process.Dispose() Write-Debug "Command [`"$exeToRun`" $wrappedStatements] exited with `'$exitCode`'." } $exitErrorMessage = '' $errorMessageAddendum = " This is most likely an issue with the '$env:dotfilesPackageName' package and not with Chocolatey itself. Please follow up with the package maintainer(s) directly." switch ($exitCode) { 0 { break } 1 { break } 3010 { break } # NSIS - http://nsis.sourceforge.net/Docs/AppendixD.html # InnoSetup - http://www.jrsoftware.org/ishelp/index.php?topic=setupexitcodes 2 { $exitErrorMessage = 'Setup was cancelled.'; break } 3 { $exitErrorMessage = 'A fatal error occurred when preparing or moving to next install phase. Check to be sure you have enough memory to perform an installation and try again.'; break } 4 { $exitErrorMessage = 'A fatal error occurred during installation process.' + $errorMessageAddendum; break } 5 { $exitErrorMessage = 'User (you) cancelled the installation.'; break } 6 { $exitErrorMessage = 'Setup process was forcefully terminated by the debugger.'; break } 7 { $exitErrorMessage = 'While preparing to install, it was determined setup cannot proceed with the installation. Please be sure the software can be installed on your system.'; break } 8 { $exitErrorMessage = 'While preparing to install, it was determined setup cannot proceed with the installation until you restart the system. Please reboot and try again.'; break } # MSI - https://msdn.microsoft.com/en-us/library/windows/desktop/aa376931.aspx 1602 { $exitErrorMessage = 'User (you) cancelled the installation.'; break } 1603 { $exitErrorMessage = "Generic MSI Error. This is a local environment error, not an issue with a package or the MSI itself - it could mean a pending reboot is necessary prior to install or something else (like the same version is already installed). Please see MSI log if available. If not, try again adding `'--install-arguments=`"`'/l*v c:\$($env:dotfilesPackageName)_msi_install.log`'`"`'. Then search the MSI Log for `"Return Value 3`" and look above that for the error."; break } 1618 { $exitErrorMessage = 'Another installation currently in progress. Try again later.'; break } 1619 { $exitErrorMessage = 'MSI could not be found - it is possibly corrupt or not an MSI at all. If it was downloaded and the MSI is less than 30K, try opening it in an editor like Notepad++ as it is likely HTML.' + $errorMessageAddendum; break } 1620 { $exitErrorMessage = 'MSI could not be opened - it is possibly corrupt or not an MSI at all. If it was downloaded and the MSI is less than 30K, try opening it in an editor like Notepad++ as it is likely HTML.' + $errorMessageAddendum; break } 1622 { $exitErrorMessage = 'Something is wrong with the install log location specified. Please fix this in the package silent arguments (or in install arguments you specified). The directory specified as part of the log file path must exist for an MSI to be able to log to that directory.' + $errorMessageAddendum; break } 1623 { $exitErrorMessage = 'This MSI has a language that is not supported by your system. Contact package maintainer(s) if there is an install available in your language and you would like it added to the packaging.'; break } 1625 { $exitErrorMessage = 'Installation of this MSI is forbidden by system policy. Please contact your system administrators.'; break } 1632 { $exitErrorMessage = 'Installation of this MSI is not supported on this platform. Contact package maintainer(s) if you feel this is in error or if you need an architecture that is not available with the current packaging.'; break } 1633 { $exitErrorMessage = 'Installation of this MSI is not supported on this platform. Contact package maintainer(s) if you feel this is in error or if you need an architecture that is not available with the current packaging.'; break } 1638 { $exitErrorMessage = 'This MSI requires uninstall prior to installing a different version. Please ask the package maintainer(s) to add a check in the chocolateyInstall.ps1 script and uninstall if the software is installed.' + $errorMessageAddendum; break } 1639 { $exitErrorMessage = 'The command line arguments passed to the MSI are incorrect. If you passed in additional arguments, please adjust. Otherwise followup with the package maintainer(s) to get this fixed.' + $errorMessageAddendum; break } 1640 { $exitErrorMessage = 'Cannot install MSI when running from remote desktop (terminal services). This should automatically be handled in licensed editions. For open source editions, you may need to run change.exe prior to running Chocolatey or not use terminal services.'; break } 1645 { $exitErrorMessage = 'Cannot install MSI when running from remote desktop (terminal services). This should automatically be handled in licensed editions. For open source editions, you may need to run change.exe prior to running Chocolatey or not use terminal services.'; break } } if ($exitErrorMessage) { $errorMessageSpecific = "Exit code indicates the following: $exitErrorMessage." Write-Warning $exitErrorMessage } else { $errorMessageSpecific = 'See log for possible error messages.' } if ($validExitCodes -notcontains $exitCode) { Set-PowerShellExitCode $exitCode throw "Running [`"$exeToRun`" $wrappedStatements] was not successful. Exit code was '$exitCode'. $($errorMessageSpecific)" } else { $chocoSuccessCodes = @(0, 1605, 1614, 1641, 3010) if ($chocoSuccessCodes -notcontains $exitCode) { Write-Warning "Exit code '$exitCode' was considered valid by script, but not as a Chocolatey success code. Returning '0'." $exitCode = 0 } } Write-Debug "Finishing '$($MyInvocation.InvocationName)'" return $exitCode } } <# $ShimGen = [System.IO.Path]::GetFullPath($ShimGen) $process = New-Object System.Diagnostics.Process $process.StartInfo = new-object System.Diagnostics.ProcessStartInfo($ShimGen, $ShimGenArgs) $process.StartInfo.RedirectStandardOutput = $true $process.StartInfo.RedirectStandardError = $true $process.StartInfo.UseShellExecute = $false $process.StartInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $process.Start() | Out-Null $process.WaitForExit() #> <# Start PROCESS UTF8 # $standardOut = @(Start-Utf8Process $script:OMPExecutable @("print", "tooltip", "--pwd=$cleanPWD", "--shell=powershell", "--pswd=$cleanPSWD", "--config=$env:POSH_THEME", "--command=$command", "--shell-version=$script:PSVersion")) function Start-Utf8Process { param( [string]$FileName, [string[]]$Arguments = @() ) $Process = New-Object System.Diagnostics.Process $StartInfo = $Process.StartInfo $StartInfo.FileName = $FileName if ($StartInfo.ArgumentList.Add) { # ArgumentList is supported in PowerShell 6.1 and later (built on .NET Core 2.1+) # ref-1: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.argumentlist?view=net-6.0 # ref-2: https://docs.microsoft.com/en-us/powershell/scripting/whats-new/differences-from-windows-powershell?view=powershell-7.2#net-framework-vs-net-core $Arguments | ForEach-Object -Process { $StartInfo.ArgumentList.Add($_) } } else { # escape arguments manually in lower versions, refer to https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85) $escapedArgs = $Arguments | ForEach-Object { # escape N consecutive backslash(es), which are followed by a double quote, to 2N consecutive ones $s = $_ -replace '(\\+)"', '$1$1"' # escape N consecutive backslash(es), which are at the end of the string, to 2N consecutive ones $s = $s -replace '(\\+)$', '$1$1' # escape double quotes $s = $s -replace '"', '\"' # quote the argument "`"$s`"" } $StartInfo.Arguments = $escapedArgs -join ' ' } $StartInfo.StandardErrorEncoding = $StartInfo.StandardOutputEncoding = [System.Text.Encoding]::UTF8 $StartInfo.RedirectStandardError = $StartInfo.RedirectStandardInput = $StartInfo.RedirectStandardOutput = $true $StartInfo.UseShellExecute = $false if ($PWD.Provider.Name -eq 'FileSystem') { $StartInfo.WorkingDirectory = $PWD.ProviderPath } $StartInfo.CreateNoWindow = $true [void]$Process.Start() # we do this to remove a deadlock potential on Windows $stdoutTask = $Process.StandardOutput.ReadToEndAsync() $stderrTask = $Process.StandardError.ReadToEndAsync() [void]$Process.WaitForExit() $stderr = $stderrTask.Result.Trim() if ($stderr -ne '') { $Host.UI.WriteErrorLine($stderr) } $stdoutTask.Result #> |