I'm trying to run some of my custom code asynchronously in Powershell. The following tries to check for updates in a background thread:
Function CheckUpdates($manager)
{
. "$PSScriptRoot\..\commands\upgradecmd.ps1";
$upgradeCmd = New-Object UpgradeCmd -ArgumentList $manager;
[bool]$upgradesAvailable = $false;
try
{
$Global:silentmode = $true;
$upgradeCmd.preview = $true;
Start-Job -ArgumentList $upgradeCmd -ScriptBlock { param($upgradeCmd) $upgradeCmd.Run(); };
Get-Job | Receive-Job;
$upgradesAvailable = $upgradeCmd.upgradesAvailable;
}
finally
{
$Global:silentmode = $false;
}
if ($upgradesAvailable)
{
WriteWarning "Upgrades detected.";
WriteWarning "Please, run the upgrade command to update your Everbot installation.";
}
}
The problem is that inside the job (in the ScriptBlock), PS doesn't recognize anything about my custom "Run()" method, so it doesn't know how to call it. I've tried to "include" the class in the job using the -InitializationScript parameter with little success.
After searching the web, it seems that the way to do this is using PS Jobs, there's no thread handling in PS or something like "async". The point is that I just want to run a method of some class of my PS code asynchronously.
Why don't you dot source inside the scriptblock?
Function CheckUpdates($manager)
{
try
{
$Global:silentmode = $true
$Scriptblock = {
param($manager)
. "<absolutepath>\upgradecmd.ps1"; #better to replace this with absolute path
$upgradeCmd = New-Object UpgradeCmd -ArgumentList $manager;
[bool]$upgradesAvailable = $false
$upgradeCmd.preview = $true
$upgradeCmd.Run()
$upgradesAvailable = $upgradeCmd.upgradesAvailable;
Return $upgradesAvailable
}
Start-Job -ArgumentList $manager -ScriptBlock $Scriptblock
$upgradesAvailable = #(Get-Job | Wait-Job | Receive-Job) #This will probably not be so cut and dry but you can modify your code based on the return value
}
finally
{
$Global:silentmode = $false;
}
if ($upgradesAvailable)
{
WriteWarning "Upgrades detected.";
WriteWarning "Please, run the upgrade command to update your Everbot installation.";
}
}
I have added a Wait-Job as well before receiving it so your script would wait for the jobs to finish.
Related
At [Asynchronous start][1] I had a question about starting a power-shell script asynchronously which creates a form. As answered in that question this can be solved using start-job
eg
Start-Job -ScriptBlock { test NW -NoWarning -Paranoia:2 }
So I have tried to write the Test.PS1 script routine so it re-calls itself with "Start-Job Test -NoSpawn" The switch nospawn then means it runs without a second call. I have tested this with the example code the above line now has to be and it works
Start-Job -ScriptBlock { test NW -NoSpawn -NoWarning -Paranoia:2 }
However I'm struggling to get the parameters from the original command line to passthrough to the job
I have tried creating a string in the correct format , an array , list the arguments manually , I either get repeated arguments being passed or all of the string ending up in the first Parameter $ComputerList -
A summary of the parameters and the attempts are
Param ([string]$ComputerList = 'status\edi.csv',[switch]$NoSpawn,[switch]$NoWarning,[switch]$Debug,[INT]$Paranoia=6)
...... <Snip>
Start-Job -ScriptBlock { test $ComputerList -NoSpawn -NoWarning:$NoWarning -Paranoia:$Paranoia }
Doesn't work due to scope - also switches are wrong way to do this
Start-Job -ScriptBlock { test -NoSpawn $Args } -argumentlist $ComputerList
Insufficent arguments but works - But I think One Argument is possible ?
Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist #("-NoWarning:$NoWarning","-ComputerList:$ComputerList","-Paranoia:$Paranoia")
Everything ends up in $ComputerList
Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist "-NoWarning:$NoWarning -ComputerList:$ComputerList -Paranoia:$Paranoia"
Everything ends up in $ComputerList
Full code follows
Param ([string]$ComputerList = 'status\edi.csv',[switch]$NoSpawn,[switch]$NoWarning,[switch]$Debug,[INT]$Paranoia=6)
$Log_Paranoia=$Paranoia
If ($Debug) { $debugPreference="Continue"} #enable debug messages if -debug is specified
If ($NoWarning) { $WarningPreference="SilentlyContinue"} #turn off warning messages
function Write-Paranoid($Level, $message) {
$CS=Get-PSCallStack
$Caller = $CS[1]
$Module = "$($Caller.FunctionName)[$($Caller.ScriptLineNumber)]"
$Diff=$level - $Log_Paranoia
$MSG= "$Module($($Level),$($Log_Paranoia)):$message"
if ($level - $Log_Paranoia -le 0 ) {
Write-host $MSG
}
if($Error.Count -gt 0 ) {
$MSG= "$Module($Level)ERROR:$Error[0]"
Write-Error $MSG
}
$error.clear()
}
Function AddStatusBar($form , $Txt) {
Write-Paranoid 10 "Enter"
$statusBar = New-Object System.Windows.Forms.StatusBar
$statusBar.DataBindings.DefaultDataSourceUpdateMode = 0
$statusBar.TabIndex = 4
$statusBar.Size = SDS 428 22
$statusBar.Location = SDP 0 337
$statusBar.Text = $Txt
$form.Controls.Add($statusBar)
$statusBar
Write-Paranoid 10 "Exit"
}
Function Create-Form ($Title)
{
Write-Paranoid 10 "Enter"
$form1 = New-Object System.Windows.Forms.Form
$form1.Text = $Title
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$form1.ClientSize = SDS 890 359
$form1.StartPosition = 0
$form1.BackColor = [System.Drawing.Color]::FromArgb(255,185,209,234)
$form1
Write-Paranoid 10 "Exit"
}
Function GenerateTestForm
{
Write-Paranoid 10 "Enter"
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
$Form1 = Create-Form "Test Form"
$Alist = Get-CommandLine
$StatusBar = AddStatusBar $form1 $AList
$form1.ShowDialog() | Out-Null # Suspends calller
Write-Paranoid 10 "Exit"
}
if ($NoSpawn )
{
Write-Paranoid 3 " NoSpawn "
Write-Paranoid 5 "Call GenerateForm"
if ($Test) {
GenerateTestForm
} else {
GenerateTestForm
}
} else {
Write-Paranoid 3 "NOT NoSpawn restarting as job"
# note that test.ps1 is in the path so it will restart this script
# Start-Job -ScriptBlock { test $ComputerList -NoSpawn -NoWarning:$NoWarning -Paranoia:$Paranoia } #Wrong scope
# Start-Job -ScriptBlock { test -NoSpawn $Args } -argumentlist $ComputerList # Insufficent aruments but works - ONLY One Argument possible -
# Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist #("-NoWarning:$NoWarning","-ComputerList:$ComputerList","-Paranoia:$Paranoia") # Everything ends up in $ComputerList
# Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist "-NoWarning:$NoWarning -ComputerList:$ComputerList -Paranoia:$Paranoia" # Everything ends up in $ComputerList
}
Your problem can be reduced to this:
How can I re-invoke the script at hand as a background job, passing all original arguments (parameter values, including default parameter values) through?
A simplified example:
param (
[string] $ComputerList = 'status\edi.csv',
[switch] $NoSpawn,
[switch] $NoWarning,
[switch] $Debug,
[int] $Paranoia=6
)
if ($NoSpawn) { # already running as a background job.
"I'm now running in the background with the following arguments:"
$PSBoundParameters
} else { # must re-invoke via a background job
# Add *default* parameter values, if necessary, given that
# they're *not* reflected in $PSBoundParameters.
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
if (-not $PSBoundParameters.ContainsKey($paramName)) {
$defaultValue = Get-Variable -Scope Local -ValueOnly $paramName
if (-not ($null -eq $defaultValue -or ($defaultValue -is [switch] -and -not $defaultValue))) {
$PSBoundParameters[$paramName] = $defaultValue
}
}
}
# Start a background job that reinvokes this script with the original
# arguments / default values.
Start-Job {
$argsHash = $using:PSBoundParameters
& $using:PSCommandPath -NoSpawn #argsHash
} |
Receive-Job -Wait -AutoRemoveJob
}
Note:
For demonstration purposes, the initial call waits for the re-invocation via a background job to finish, using Receive-Job -Wait -AutoRemoveJob
In your real code, you can simply discard Start-Job's output (a job-information object) with $null = Start-Job { ... }, and then rely on the job getting cleaned up when the caller's session as a whole exits.
The extra code needed to propagate parameter default values is somewhat cumbersome, but necessary, given that the automatic $PSBoundParameters variable does not reflect default values.
GitHub issue #3285 discusses this limitation, and suggests a potential future solution.
$GAMCheck = invoke-command -ScriptBlock { C:\GAMADV-XTD3\GAM.exe version checkrc }
If ($GAMCheck) {
$current = $GAMCheck.split(":")[19].Trim()
$latest = $GAMCheck.split(":")[21].Trim()
If ($LASTEXITCODE -eq 1) {
Try {
$NeedUpGradeCode = $LASTEXITCODE
$client = new-object System.Net.WebClient
$client.DownloadFile("https://github.com/taers232c/GAMADV-XTD3/releases/download/v$latest/GAMadv-xtd3-$latest-windows-x86_64.msi", "C:\Temp\GAMadv-xtd3-$latest-windows-x86_64.msi")
Start-Process -Filepath "C:\Temp\GAMadv-xtd3-$latest-windows-x86_64.msi" -ArgumentList "/passive" | Wait-Process -Timeout 75
Remove-Item "C:\Temp\GAMadv-xtd3-$latest-windows-x86_64.msi"
$GAMCheck = $null
$GAMCheck = invoke-command -ScriptBlock { C:\GAMADV-XTD3\GAM.exe version checkrc }
$newCurrent = $GAMCheck.split(":")[19].Trim()
$resultsarray = [PSCustomObject]#{
CurrentVersion = $current
LatestVersion = $latest
NeedUpgradeCode = $NeedUpGradeCode
Upgraded = $true
NewCurrent = $newCurrent
AfterUpgradeCode = $LASTEXITCODE
}
}
Catch {
Write-Warning "Problem with site or command. Maybe go to https://github.com/taers232c/GAMADV-XTD3/releases and download the current GAM and then install GAM in C:\GAMADV-XTD3\ again"
}
}
}
lately I have been noticing that the | Wait-process 75 above is causing an error.
If I run the command with out it everything is fine.
Is there another way to wait for the install ?
To launch a process with Start-Process and wait for it to exit, use the -Wait switch.
Piping a Start-Process call to Wait-Process would only work as intended if you included the -PassThru switch, which makes Start-Process - which by default produces no output - emit a System.Diagnostics.Process instance representing the newly launched process, on whose termination Wait-Process then waits.
Note that, surprisingly, the behavior of these two seemingly equivalent approaches is not the same, as discussed in GitHub issue #15555.
The Powershell code bellow uses a XML document as a pipeline to share an object between two process. Actually for test propose I run this code in two different Powershell session to see two process.
The property $logoff_processObj just works fine. It means that after it is updated in the process 1, it is successfully read and stopped in the process 3.
However, it does not work with the property $LastUpdate_SheetRange and I cannot understand why.
When I open the XML file after finish the process 1, I do not see the value set for $LastUpdate_SheetRange there.
What is the problem?
class XMLpipe
{
hidden $pp_path = (Join-Path $ENV:temp 'HPCMpipe.xml')
hidden [System.Object]$logoff_processObj
hidden [string]$LastUpdate_SheetRange
XMLpipe()
{
if([System.IO.File]::Exists($this.pp_path))
{
$this = Import-Clixml -Path $this.pp_path
}
}
[void]setObj_toXML()
{
$this | Export-Clixml -Path $this.pp_path
}
[System.Object]getObj_fromXML()
{
$this = (Import-Clixml -Path $this.pp_path)
return $this
}
[void]set_processObj($value)
{
$this.logoff_processObj = $value
$this.setObj_toXML()
}
[void]set_LastUpdate_SheetRange($value)
{
$this.LastUpdate_SheetRange = $value
$this.setObj_toXML()
}
[System.Object]get_processObj()
{
$this = $this.getObj_fromXML()
return $this.logoff_processObj
}
[string]get_LastUpdate_SheetRange()
{
$this = $this.getObj_fromXML()
return $this.LastUpdate_SheetRange
}
}
Process 1:
$myobj = New-Object -TypeName XMLpipe
#This starts a 2nd process
$ProcessObj = Start-Process notepad -PassThru
$myobj.set_processObj($ProcessObj)
$myobj.set_LastUpdate_SheetRange("ABC")
Process 3:
$myobj = New-Object -TypeName XMLpipe
#This gets the process object started in the 1st process
$ProcessObj = $myobj.get_processObj()
$LastUpdate_SheetRange = $myobj.get_LastUpdate_SheetRange()
#This stops the 2nd process
$ProcessObj | Stop-Process
Write-Host "ProcessObj: "$ProcessObj
Write-Host "get_LastUpdate_SheetRange: "$LastUpdate_SheetRange
This lambda function executes as expected:
$WriteServerName = {
param($server)
Write-Host $server
}
$server = "servername"
$WriteServerName.invoke($server)
servername
However, using the same syntax, the following script prompts for credentials and then exits to the command line (running like this: .\ScriptName.ps1 -ConfigFile Chef.config), implying that the lambda functions aren't executing properly (for testing, each should just output the server name).
Why does the former lambda function return the server name, but the ones in the script don't?
Param(
$ConfigFile
)
Function Main {
#Pre-reqs: get credential, load config from file, and define lambda functions.
$jobs = #()
$Credential = Get-Credential
$Username = $Credential.username
$ConvertedPassword = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.password)
$Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($ConvertedPassword)
$Config = Get-Content $ConfigFile -Raw | Out-String | Invoke-Expression
#Define lambda functions
$BootStrap = {
param($Server)
write-host $server
}
$RunChefClient = {
param($Server)
write-host $server
}
$SetEnvironment = {
param($Server)
write-host $server
}
#Create bootstrap job for each server and pass lambda functions to Scriptblock for execution.
if(($Username -ne $null) -and ($Password -ne $null))
{
ForEach($HashTable in $Config)
{
$Server = $HashTable.Server
$Roles = $HashTable.Roles
$Tags = $HashTable.Tags
$Environment = $HashTable.Environment
$ScriptBlock = {
param ($Server,$BootStrap,$RunChefClient,$SetEnvironment)
$BootStrap.invoke($Server)
$RunChefClient.invoke($Server)
$SetEnvironment.invoke($Server)
}
$Jobs += Start-Job -ScriptBlock $ScriptBlock -ArgumentList #($Server,$BootStrap,$RunChefClient,$SetEnvironment)
}
}
else {Write-Host "Username or password is missing, exiting..." -ForegroundColor Red; exit}
}
Main
Without testing, I am going to go ahead and say it's because you are putting your scriptblock executions in PowerShell Jobs and then not doing anything with them. When you start a job, it starts a new PowerShell instance and executes the code you give it along with the parameters you give it. Once it completes, the completed PSRemotingJob object sits there and does nothing until you actually do something with it.
In your code, all the jobs you start are assigned to the $Jobs variable. You can also get all your running jobs with Get-Job:
Get-Job -State Running
If you want to get any of the data returned from your jobs, you'll have to use Receive-Job
# Either
$Jobs | Receive-Job
# Or
Get-Job -State Running | Receive-Job
I want to use the same function in a background process that I use in the body of my main code.
If I write it this way, the "add" function works for the background process but I cannot use it in the main code.
$add = { function add($a,$b) { return $a+$b } }
$job = Start-Job -Name "test" -ArgumentList #(2,4) -InitializationScript $add -ScriptBlock { return add $args[0] $args[1] }
sleep 1
Receive-Job -Name "test"
Remove-Job -Name "test" -force
add 2 4
If I remove the {} around the "add" function definition, it works for the main body, but not for the background process.
$add = function add($a,$b) { return $a+$b }
$job = Start-Job -Name "test" -ArgumentList #(2,4) -InitializationScript $add -ScriptBlock { return add $args[0] $args[1] }
sleep 1
Receive-Job -Name "test"
Remove-Job -Name "test" -force
add 2 4
How can I use my function in both the background process and the main code?
Dot-source your "library" scriptblock in your main code.
$add = { function add($a,$b) { return $a+$b } }
. $add
add 2 4