How to do something if a job has finished - powershell

I'm trying to implement a GUI to my PowerShell script to simplify a certain process for other users. I have following PowerShell script:
if ($checkBox1.Checked) {
Try{
Start-Job { & K:\sample\adp.cmd }
$listBox1.Items.Add("ADP-Job started...")
}catch [System.Exception]{
$listBox1.Items.Add("ADP --> .cmd File not found!")}
}
if ($checkBox2.Checked) {
Try{
Start-Job { & K:\sample\kdp.cmd }
$listBox1.Items.Add("KDP-Job started...")
}catch [System.Exception]{
$listBox1.Items.Add("KDP --> .cmd File not found!")}
}
Is there a way to continuously check all running Jobs and do something for each Job that has finished? For Example to print out something like this in my listbox: ADP-Files have been uploaded
Since each Job takes around 5 minutes - 4 hours I thought of a while Loop that checks every 5 minutes if a Job is finished, but I can't figure out how to distinguish each Job to do something specific.

You can either specifiy a name for the job using the -Name parameter:
Start-Job { Write-Host "hello"} -Name "HelloWriter"
And receive the job status using the Get-Job cmdlet:
Get-Job -Name HelloWriter
Output:
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
3 HelloWriter BackgroundJob Completed True localhost Write-Host "hello"
Or you assign the Start-Job cmdlet to a variable and use it to retrieve the job:
$worldJob = Start-Job { Write-Host "world"}
So you can just write $woldJob and receive:
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
7 Job7 BackgroundJob Completed True localhost Write-Host "world"
You also don't have to poll the Job state. Instead use the Register-ObjectEvent cmdlet to get notificated when the job has finished:
$job = Start-Job { Sleep 3; } -Name "HelloJob"
$jobEvent = Register-ObjectEvent $job StateChanged -Action {
Write-Host ('Job #{0} ({1}) complete.' -f $sender.Id, $sender.Name)
$jobEvent | Unregister-Event
}

Multiple possible ways here:
$Var = Start-Job { & K:\sample\kdp.cmd }
an then check
$Var.State
Or give the job a name
Start-Job { & K:\sample\kdp.cmd } -Name MyJob
and then check
Get-Job MyJob

Related

Fire an event from job

I'm very new to powershell.
My question is very simple if you know anything about powershell.
In the code below I'm trying to fire an event from a piece of code running asynchronously as a job. For some reason, the code doesn't work.
$callback = {
param($event);
Write "IN CALLBACK";
};
$jobScript = {
while ($true) {
sleep -s 1;
"IN JOB SCRIPT";
New-Event myEvent;
}
}
Register-EngineEvent -SourceIdentifier myEvent -Action $callback;
Start-Job -Name Job -ScriptBlock $jobScript;
while ($true) {
sleep -s 1;
"IN LOOP";
}
Expected output:
IN LOOP
IN JOB SCRIPT
IN CALLBACK
IN LOOP
IN JOB SCRIPT
IN CALLBACK
...
Actual output:
IN LOOP
IN LOOP
IN LOOP
IN LOOP
...
After some reading, I changed this line
Start-Job -Name Job -ScriptBlock $jobScript
to
Start-Job -Name Job -ScriptBlock $jobScript | Wait-Job | Receive-Job;
and I get no output at all, because job never finishes.
It's kind of asynchoronous, but not really.
It would be fairly simple to acomplish in JS.
const fireEvent = (eventName) => { ... }
const subscribeToEvent = (eventName, callback) => { ... }
const callback = () => console.log('IN CALLBACK')
subscribeToEvent('myEvent', callback);
setInterval(() => {
console.log('IN LOOP')
fireEvent('myEvent');
}, 1000)
Please, help!
Running:
while ($true) {
sleep -s 1;
"IN LOOP";
}
will always only give you:
IN LOOP
IN LOOP
IN LOOP
IN LOOP
Running this block:
$callback = {
param($event);
Write "IN CALLBACK";
};
$jobScript = {
while ($true) {
sleep -s 1;
"IN JOB SCRIPT";
New-Event myEvent;
}
}
Register-EngineEvent -SourceIdentifier myEvent -Action $callback;
Start-Job -Name Job -ScriptBlock $jobScript;
gives you a job called Job. This job will run untill you stop it. Output of the job will be something like this:
get-job Job | receive-job
IN JOB SCRIPT
RunspaceId : cf385728-926c-4dda-983e-6a5cfd4fd67f
ComputerName :
EventIdentifier : 1
Sender :
SourceEventArgs :
SourceArgs : {}
SourceIdentifier : myEvent
TimeGenerated : 6/15/2019 3:35:09 PM
MessageData :
IN JOB SCRIPT
RunspaceId : cf385728-926c-4dda-983e-6a5cfd4fd67f
ComputerName :
EventIdentifier : 2
Sender :
SourceEventArgs :
SourceArgs : {}
SourceIdentifier : myEvent
TimeGenerated : 6/15/2019 3:35:10 PM
MessageData :
...
JavaScript is really Greek to me, but it seems your issue is that the event is not registered in the scope of the job. If you move the registration to the job, it behaves a bit more like you expect.
If you do this:
$jobScript = {
$callback = {
Write-Host "IN CALLBACK"
}
$null = Register-EngineEvent -SourceIdentifier myEvent -Action $callback
while ($true) {
sleep -s 1
Write-Host "IN JOB SCRIPT"
$Null = New-Event myEvent
}
}
$Null = Start-Job -Name Job -ScriptBlock $jobScript;
while ($true) {
sleep -s 1
"IN LOOP"
Get-Job -Name Job | Receive-Job
}
When running $null = Register-EngineEvent ... or $null = Start-Job ... you avoid the objects these commands creates to be displayed at the console. Furthermore you do not need to terminate your lines with ; in PowerShell.
To complement Axel Anderson's helpful answer:
Register-EngineEvent subscribes to an event in the current session, whereas commands launched with Start-Job run in a background job that is a child process, which by definition is a separate session.
Similarly, any events you raise with New-Event are only seen in the same session, which is why the calling session never saw the events.
By moving all event logic into the background job, as in Axel's answer, the events are processed in the background job, but there's an important limitation - which may or may not matter to you:
You won't be able to capture output from the event handler: while you can make its output print to the console using Write-Host, that output cannot be captured for programmatic processing. Also, such Write-Host output cannot be suppressed.
By contrast, output sent to the success output stream directly from the background job - such as "IN JOB SCRIPT" is, implicitly (implied use of Write-Output) - can be captured via Receive-Job, which retrieves the output from background jobs.
Therefore, perhaps the following is sufficient in your case, which doesn't require use of events at all:
# Code to execute in the background, defined a script block.
$jobScript = {
while ($true) {
Start-Sleep -Seconds 1
"JOB OUTPUT #$(($i++))"
}
}
# Launch the background job and save the job object in variable $job.
$job = Start-Job -Name Job -ScriptBlock $jobScript;
# Periodically relay the background job's output.
while ($true) {
Start-Sleep -Seconds 1
"IN LOOP, about to get latest job output:"
$latestOutput = Receive-Job $job
"latest output: $latestOutput"
}
For an explanation of the $(($i++)) construct, see this answer.
The above yields the following:
IN LOOP, about to get latest job output:
latest output:
IN LOOP, about to get latest job output:
latest output: JOB OUTPUT #0
IN LOOP, about to get latest job output:
latest output: JOB OUTPUT #1
IN LOOP, about to get latest job output:
latest output: JOB OUTPUT #2
IN LOOP, about to get latest job output:
latest output: JOB OUTPUT #3
IN LOOP, about to get latest job output:
latest output: JOB OUTPUT #4
IN LOOP, about to get latest job output:
latest output: JOB OUTPUT #5
IN LOOP, about to get latest job output:
...

Powershell: How to stop start-job?

I have the following job in powershell:
$DoIt = '...'
....
If ([IntPtr]::size -eq 8) {
start-job {
param($a) IEX $a
} -RunAs32 -Argument $DoIt | wait-job | Receive-Job
Get-Job | Stop-Job
Exit
Write-Host "exit powershell" -NoNewLine -ForegroundColor Green
}
else {
IEX $DoIt
}
simply put, how to stop or close powershell after executing the start-job function. What should I do?Tried some ways to find that it didn't work.
Remove-Job. Here's an example. If the job completes, you don't have to run stop-job. Although invoke-expression is almost always the wrong answer, and you have to watch user input to it. I'm in osx.
PS /Users/js> $job = start-job { param($a) sleep $a } -Args 3600
PS /Users/js> get-job
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
11 Job11 BackgroundJob Running True localhost param($a) sleep $a
PS /Users/js> stop-job $job
PS /Users/js> remove-job $job # or just remove-job -force
PS /Users/js> get-job # none
how to stop or close powershell after executing the start-job function.
If you exit your session before the background job launched with Start-Job has completed, you'll stop (abort) it implicitly, because PowerShell cleans up pending background jobs automatically when the session that created them exits.
Therefore, if you want to launch a background operation that keeps running when the session that initiated it exits, you'll have to use a different mechanism, such as Start-Process -WindowStyle Hidden ... (Windows-only).
To wait for the background job to finish, use Wait-Job or - combined with retrieving its output - Receive-Job -Wait.
Note that this is only useful if you perform other actions in between starting the background job and waiting for its completion - otherwise, you could just run the operations directly, without the need for a background job.

Powershell Register-Object

I have a problem in running the below Powershell script in console.
function startMonitor {
$null = Register-ObjectEvent -InputObject ([Microsoft.Win32.SystemEvents]) -EventName "SessionSwitch" -Action {
switch($event.SourceEventArgs.Reason) {
'SessionLock'
{
---------do something----
}
'SessionUnlock'
{
--------do something----
}
}
}
}
startMonitor
When I run this in powershell ISE it works fine and output is as expected. When the session is locked or unlocked, output is generated perfectly.
But I want to run this as a script that starts up during logon.
I put this script in the startup folder as
powershell.exe -noexit -windowstyle hidden "E:\sources\lock.ps1"
The script runs fine. But, it does not generate the output (the other functions in this code generates output properly, except this function). When I try the command (without the switch -windowstyle):
Get-EventSubscriber
Shows that event is registered.
How do I run this script using the powershell.exe -noexit?
I am unable to use task scheduler for this purpose because of limitations in my environment.
In order to run in background you have to keep it within a loop and check for events, test with this little example...
[System.Reflection.Assembly]::LoadWithPartialName("System.Diagnostics")
# ------ init ------
function Start-mySession {
$null = Register-ObjectEvent -InputObject ([Microsoft.Win32.SystemEvents]) -EventName "SessionSwitch" -Action {
add-Type -AssemblyName System.Speech
$synthesizer = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer
switch($event.SourceEventArgs.Reason) {
'SessionLock' { $synthesizer.Speak("Lock!") }
'SessionUnlock' { $synthesizer.Speak("Unlock!") }
}
}
}
# ------ stop ------
function End-mySession {
$events = Get-EventSubscriber | Where-Object { $_.SourceObject -eq [Microsoft.Win32.SystemEvents] }
$jobs = $events | Select-Object -ExpandProperty Action
$events | Unregister-Event
$jobs | Remove-Job
}
# ===== keep process in alive for 10 hours =====
[TimeSpan]$GLOBAL:timeout = new-timespan -Hours 10
$GLOBAL:sw = [diagnostics.stopwatch]::StartNew()
while ($GLOBAL:sw.elapsed -lt $GLOBAL:timeout){
[Array]$GLOBAL:XX_DEBUG_INFO += "DEBUG: initialize debug!"
# Loop
Start-mySession
start-sleep -seconds 10
#.. do something, or check results
End-mySession
}
# teardown after timeout
Write-Output "INFO: $( get-date -format "yyyy-MM-dd:HH:mm:ss" ) : $env:USERNAME : Timeout (logout/timout)"
End-mySession

What's the best way to pass values to a running/background script block?

I have script which start several running script blocks by start-job.
What's the best approach to pass some variables/values to the running background script block?
There are some options like service broker/queue, files, etc. Is there a lighter way?
For example,
$sb = {
$Value = $args[0] # initial value
while ($true)
{
# Get more values from caller
$Value = .....
}
}
start-job -ScriptBlock $sb -ArgumentList $initValue
# There are more values to send to the script after the script block is started.
while (moreVaulesAvailable)
{
# $sb.Value = .... newly generated values ?
}
Start-Job started another PowerShell process. Is there any built-in mechanism to pass values between PS processes?
You can use MSMQ to do this. There is a MSMQ module that comes with PowerShell V3. Here's an example of how to pass messages to a background task using MSMQ:
$sb = {
param($queueName)
$q = Get-MsmqQueue $queueName
while (1) {
$messages = #(try {Receive-MsmqQueue -InputObject $q -RetrieveBody} catch {})
foreach ($message in $messages)
{
"Job received message: $($message.Body)"
if ($message.Body -eq '!quit')
{
return
}
}
Start-Sleep -Milliseconds 1000
"Sleeping..."
}
}
$queueName = 'JobMessages'
$q = Get-MsmqQueue $queueName
if ($q)
{
"Clearing the queue $($q.QueueName)"
$q | Clear-MsmqQueue > $null
}
else
{
$q = New-MsmqQueue $queueName
"Created queue $($q.QueueName)"
}
$job = Start-Job -ScriptBlock $sb -ArgumentList $queueName -Name MsgProcessingJob
"Job started"
$msg = New-MsmqMessage "Message1 for job sent at: $(Get-Date)"
Send-MsmqQueue -Name $q.Path -MessageObject $msg > $null
Receive-Job $job
$msg = New-MsmqMessage "Message2 for job sent at: $(Get-Date)"
Send-MsmqQueue -Name $q.Path -MessageObject $msg > $null
$msg = New-MsmqMessage "!quit"
Send-MsmqQueue -Name $q.Path -MessageObject $msg > $null
Wait-Job $job -Timeout 30
Receive-Job $job
Get-Job $job.Name
Remove-Job $job
When I run this script I get the following output:
C:\PS> .\MsmqQueue.ps1
Clearing the queue private$\jobmessages
Job started
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
4 MsgProcessin... BackgroundJob Completed True localhost ...
Job received message: Message1 for job sent at: 12/15/2012 17:53:39
Sleeping...
Job received message: Message2 for job sent at: 12/15/2012 17:53:39
Sleeping...
Job received message: !quit
4 MsgProcessin... BackgroundJob Completed False localhost ...

Powershell start-job -scriptblock cannot recognize the function defined in the same file?

I have the following code.
function createZip
{
Param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
try {
Start-Job -ScriptBlock { createZip "abd" "acd" }
}
catch {
$_ | fl * -force
}
Get-Job | Wait-Job
Get-Job | receive-job
Get-Job | Remove-Job
However, the script returns the following error.
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
309 Job309 Running True localhost createZip "a...
309 Job309 Failed False localhost createZip "a...
Receive-Job : The term 'createZip' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:17 char:22
+ Get-Job | receive-job <<<<
+ CategoryInfo : ObjectNotFound: (function:createZip:String) [Receive-Job], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
It seems the function name cannot be recognized inside the script block of start-job. I tried function:createZip too.
Start-Job actually spins up another instance of PowerShell.exe which doesn't have your createZip function. You need to include it all in a script block:
$createZip = {
param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"
An example returning an error message from the background job:
$createZip = {
param ([String] $source, [String] $zipfile)
$output = & zip.exe $source $zipfile 2>&1
if ($LASTEXITCODE -ne 0) {
throw $output
}
}
$job = Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"
$job | Wait-Job | Receive-Job
Also note that by using a throw the job object State will be "Failed" so you can get only the jobs which failed: Get-Job -State Failed.
If you are still new to using start-job and receive-job, and want to debug your function more easily, try this form:
$createZip = {
function createzipFunc {
param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
#other funcs and constants here if wanted...
}
# some secret sauce, this defines the function(s) for us as locals
invoke-expression $createzip
#now test it out without any job plumbing to confuse you
createzipFunc "abd" "acd"
# once debugged, unfortunately this makes calling the function from the job
# slightly harder, but here goes...
Start-Job -initializationScript $createZip -scriptblock {param($a,$b) `
createzipFunc $a $b } -ArgumentList "abc","def"
All not made simpler by the fact I did not define my function as a simple filter as you have, but which I did because I wanted to pass a number of functions into my Job in the end.
Sorry for digging this thread out, but it solved my problem too and so elegantly at that. And so I just had to add this little bit of sauce which I had written while debugging my powershell job.