manidos
manidos

Reputation: 3464

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!

Upvotes: 1

Views: 864

Answers (2)

mklement0
mklement0

Reputation: 439682

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:
...

Upvotes: 1

Axel Andersen
Axel Andersen

Reputation: 1128

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.

Upvotes: 4

Related Questions