Unable to call script block from Powershell script - powershell

I am trying to execute a script when the user locks the computer. This is how my script looks like:
$OnLock =
{
Write-Host -ForeGround Green "System Locked"
}
$sysevent = [microsoft.win32.systemevents]
Register-ObjectEvent -InputObject $sysevent -EventName "SessionSwitch" -Action {$OnLock} -SourceIdentifier "ExecuteOnLock"
The problem is that it does not print anything on the console window but if i write the code in the Action switch, it works fine.
Register-ObjectEvent -InputObject $sysevent -EventName "SessionSwitch" -SourceIdentifier "ExecuteOnLock" -Action {Write-Host -ForeGround Green "System Locked"}
Is there something i am missing while calling $OnLock script block?

Remove the braces around $OnLock when you are calling it from the -Action parameter.

Event actions run in a separate runspace that can't access local variables. Try making it a function in the Global scope.
Function Global:OnLock {
Write-Host -ForeGround Green "System Locked" }
$sysevent = [microsoft.win32.systemevents]
Register-ObjectEvent -InputObject $sysevent -EventName "SessionSwitch" -Action {OnLock} -SourceIdentifier "ExecuteOnLock"

I've discovered today a funny way to call local functions of a script from a scriptblock passed in the Action parameter of the Register-ObjectEvent cmdlet by using the MessageData parameter the previously mentioned cmdlet without attributing these functions to global scope. Look at the next code snippet:
&{
function sesbeg{
'Lots of usfull actions in the "sesbeg" function'
}
function rqincs{
'Lots of usfull actions in the "rqincs" function'
}
function sesfin{
'Lots of usfull actions in the "sesfin" function'
}
function outincs{
'Lots of usfull actions in the "outincs" function'
}
function shincs{
sesbeg
$my.incs=rqincs
sesfin
if($my.incs){$my.incs;outincs;$refr=$my.refr*1000}
else{write-host 'The data was not received';$refr=1000}
$my.tim.Interval=$refr
}
function tmbeg{
$my.tim=New-Object System.Timers.Timer ($my.refr*1000)
$my.tim.AutoReset=$True
$my.subs=Register-ObjectEvent $my.tim Elapsed `
-Action {$Event.MessageData|%{$my=$_.my;&$_.fn}|Out-Host} `
-MessageData #{my=$my;fn=$function:shincs}
$my.tim.Start()
}
$my=#{refr=5}
tmbeg
shincs
}

Related

Can't call a class method inside a `Register-ObjectEvent` scriptblock

My goal is to call a class method when the job received or simply to notify outside method that the job is complete.
Is there any way to do this? This is the sample script:
class Test {
$_Trigger
$_Event
$_Job
$_Result
Start() {
$this._Job = Start-Job -Name JobTest -ScriptBlock { Start-Sleep -Seconds 2; return 100; }
$this._Event = Register-ObjectEvent $this._Job StateChanged -Action {
$script:_Result = Receive-Job -Id $sender.Id -Keep
$this.Trigger() # this line here
}
}
Trigger() {
<#
CALL OTHER METHODS
#>
Write-Host "job complete"
}
}
I also try passing the method when calling the job like this
Start-Job -Name JobTest -ScriptBlock { Start-Sleep -Seconds 2; return 100; } -ArgumentList $this._Trigger
and want to access it via $Sender since there is the $sender variable as stated by the docs, but my problem is how to call it or how can I "view" this automatic variables inside the script block?
The value of the Action parameter can include the $Event, $EventSubscriber, $Sender, $EventArgs, and $Args automatic variables. These variables provide information about the event to the Action script block. For more information, see about_Automatic_Variables.
A couple of things.
If you want to debug your script or just your script to work when not ran from VsCode (eg: in a production context), you need to make sure that your script do not exit too soon. The simplest way to make sure this does not happen is to have some sort of loop at the end.
If you attempted to place a breakpoint within the Register-ObjectEvent -Action scriptblock and did have a message stating that the "breakpoint could not be hit", this is because your script already had exited and the debugger detached at the moment when you reached the actual event handler. Since VsCode maintain the session even when your script exited, the event handler will trigger, but that won't be a debuggable version.
$this will be $null from within the Register-ObjectEvent -Action scriptblock context. You need to pass the class as message data -MessageData #{This = $this } and access it from within the action block as $Event.MessageData.This
You don't have any Try/Catch in the Action scriptblock. Just be aware that any error will kill the event handler and it won't trigger afterward if any error is thrown.
Here is your example, adapted to fix these issues.
(Note: I also added Cyan)
class Test {
$_Trigger
$_Event
$_Job
$_Result
Start() {
$this._Job = Start-Job -Name JobTest -ScriptBlock { Start-Sleep -Seconds 2; return 100; }
$this._Event = Register-ObjectEvent $this._Job StateChanged -Action {
try {
$MyObject = $Event.MessageData.This
$MyObject._Result = Receive-Job -Id $sender.Id -Keep
$MyObject.Trigger() # this line here
}
catch {
}
} -MessageData #{This = $this }
}
Trigger() {
<#
CALL OTHER METHODS
#>
Write-Host "job complete" -ForegroundColor Cyan
}
}
$MyTestInstance = [Test]::new()
$MyTestInstance.Start()
# IMPORTANT
# You need some kind of loop to prevent the script of exiting immediately.
# If you don't have any, It will appear to work in vscode but won't work in production contexts
# You also won't be able to debug the code if the script debugger exitted
# We stay in the loop until we have a result.
while (!$$MyTestInstance._Result) {
Start-Sleep -Milliseconds 100
}
References
VsCode and Powershell: How to debug and trace into code called by Register-ObjectEvent
Register-ObjectEvent
About Automatic Variables
Note: See Sage Pourpre's helfpul answer for additional information.
Script blocks passed to the -Action parameter of Register-ObjectEvent run in a dynamic module, which doesn't have access to the caller's scope.
However, you can pass arbitrary data to the script block via the -MessageData parameter, which you can access via $Event.MessageData there, so you can pass the custom-class instance at hand as -MessageData $this:
class Test {
$_Event
$_Job
$_Result
Start() {
$this._Job = Start-Job -Name JobTest -ScriptBlock { Start-Sleep -Seconds 2; return 100; }
# Note the use of -MessageData and $Event.MessageData
$this._Event =
Register-ObjectEvent $this._Job StateChanged -MessageData $this -Action {
$Event.MessageData._Result = Receive-Job -Id $sender.Id -Keep
$Event.MessageData.Trigger() # this line here
}
}
Trigger() {
<#
CALL OTHER METHODS
#>
Write-Host "job complete"
}
}

How to provide options to event registration?

I don't understand why the following code generates error messages. Powershell seems difficult to learn.
$fsw = New-Object IO.FileSystemWatcher ...
$Action = {Param($option)
if ($option -eq "Copy")
{Write-Host "Copy was specified"}
}
Register-ObjectEvent $fsw Created -SourceIdentifier FileCreated -Action $Action
Register-ObjectEvent $fsw Changed -SourceIdentifier FileChanged -Action $Action -ArgumentList Copy
(This is an updated version of question How to provide options to script blocks?)
When in doubt, read the documentation. The Register-ObjectEvent cmdlet does not have a parameter -ArgumentList, so getting an error when trying to call the cmdlet with a parameter that it doesn't have is hardly surprising. And as I already told you in my comment to your previous question, passing arguments to an event action the way you're trying to do does not make much sense in the first place.
If your actions are so similar that defining different action script blocks would yield too much boilerplate you can easily discriminate by event inside the scriptblock, since that information is passed to the action automatically (via the variable $Event).
$Action = {
$name = $Event.SourceEventArgs.Name
$type = $Event.SourceEventArgs.ChangeType
switch ($type) {
'Created' { Write-Host "file ${file} was created" }
'Changed' { Write-Host "file ${file} was changed" }
}
}

Why "Write-Host" works differently from PowerShell function and timer handler?

I have 1809 Windows 10 box with PowerShell Core 6.1.1
Given following code sample:
function Test() {
Write-Host "Test"
}
function Invoke-Test() {
$timer = New-Object System.Timers.Timer
$timer.AutoReset = $false
$timer.Interval = 1
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {
Test
}
$timer.Enabled = $true
}
If I invoke "Test" function, I get "Test" output as expected:
But if I schedule invocation with a timer, command prompt is completely messed up:
I vaguely understand that it's something related to internal "readline" and console mechanic, but is it any way to produce newline output followed by a command prompt from a timer/handle in powershell?
Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier Timer.Test -Action {
Test
}
$timer.Enabled = $true
PS C:\> $Subscriber = Get-EventSubscriber -SourceIdentifier Timer.Test
PS C:\> $Subscriber.action | Format-List -Property *
The command property should hold the results of your “test” function.
The url below has detailed explanations.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-eventsubscriber?view=powershell-6

Processes in Powershell

I have got a problem.
1.The first situation
Process has already started.
And I want to do something after the current process is completed.
For example
$process=[System.Diagnostics.Process]::GetProcessesByName("notepad")
And
Register-ObjectEvent $process Exited -Action {}
But in this case, it's not working
How Can I Register Event "Exited"?
2. The Second situation
The Process has not yet been started.
How I can to wait for start process?)
First part is answered by Mathias R. Jessen with [System.Diagnostics.Process]::GetProcessesByName("notepad") | foreach {Register-ObjectEvent $_ Exited -Action { ... } }
For second part, the solution is below. Credit goes here PDQ.com - Register-ObjectEvent
You need to use WQL queries to monitor for process start event, then set up a query watcher that will fire an event in response of some process getting started, then listen with Register-ObjectEvent on that watcher. Code-copy-paste:
$Query = New-Object System.Management.WqlEventQuery "__InstanceCreationEvent", (New-Object TimeSpan 0,0,1), "TargetInstance isa 'Win32_Process'"
$ProcessWatcher = New-Object System.Management.ManagementEventWatcher $query
$Action = { New-Event "PowerShell.ProcessCreated" -Sender $Sender -EventArguments $EventArgs.NewEvent.TargetInstance }
register-objectEvent -InputObject $ProcessWatcher -EventName "EventArrived" -Action $Action

How can I check for variable changes in a System.Windows.Form

I am displaying a form that runs another script after a button is clicked. I need to check for the state of the script completion so that I can update the text of the button.
$ButtonEmailInfo.Add_Click({
$ButtonEmailInfo.Text = "Sending info"
$ButtonEmailInfo.Enabled = $false
$Form.Refresh()
Write-Host("Running start-job")
$global:SendClicked = $true
$global:SJob = Start-Job -filepath ($path + "\Send-Info.ps1")
$ButtonEmailInfo.Text = "Info sent"
$Form.Refresh()
})
The problem with the above code is that the button text is set to "Info sent" before the called script Send-Info.ps1 has completed.
I can test $global:SJob.Finished too see if the script has completed but I'm not sure how you do this in a form. Is there the equivalent of an update() method that gets checked periodically?
Thanks,
Rich
You can use Register-ObjectEvent to handle the job and perform an action when it has completed. You would add the following right after you begin the job.
Edit You will need to setup a Timer object to update the Window and allow the thread to handle the Event output from Register-ObjectEvent.
$global:SJob = Start-Job -filepath ($path + "\Send-Info.ps1")
Register-ObjectEvent -InputObject $Global:SJob -EventName StateChanged -SourceIdentifier JobWatcher -Action {
#Job completed actions go here
Write-Host "Job Completed!"
# $Event.Sender is the actual job object that you can either remove or retrieve data from
#Perform cleanup of event subscriber
Unregister-Event -SourceIdentifier $Event.SourceIdentifier
Remove-Job -Name $Event.SourceIdentifier -Force
}
Here is an example that you can run to see it in action without using it in a form that notifies when completed and then proceeds to remove the job and the event subscription.
$SJob = Start-Job {start-sleep -seconds 10} -Name TESTJOB
Register-ObjectEvent -InputObject $SJob -EventName StateChanged -SourceIdentifier JobWatcher -Action {
#Job completed actions go here
Write-Host "Job $($Event.Sender.Name) Completed!"
#Remove job
Remove-Job $Event.Sender
#Perform cleanup of event subscriber and its job
Unregister-Event -SourceIdentifier $Event.SourceIdentifier
Remove-Job -Name $Event.SourceIdentifier -Force
}
Edit You will need to setup a Timer object to update the Window and allow the thread to handle the Event output from Register-ObjectEvent. Add the following lines to your UI to create the timer as the window loads and a timer stop when it closes.
$Window.Add_Loaded({
##Configure a timer to refresh window##
#Create Timer object
$Script:timer = new-object System.Windows.Threading.DispatcherTimer
#Fire off every 5 seconds
$timer.Interval = [TimeSpan]"0:0:1.00"
#Add event per tick
$timer.Add_Tick({
[Windows.Input.InputEventHandler]{ $Script:Window.UpdateLayout() }
})
#Start timer
$timer.Start()
If (-NOT $timer.IsEnabled) {
$Window.Close()
}
})
$Window.Add_Closed({
$Script:timer.Stop()
})