How to handle close event of PowerShell window if user clicks on Close('X') button - powershell

I want to run some code before PowerShell 2.0 window is closed. For this I tried:
PS > register-engineevent PowerShell.Exiting -action {get-process | out-file c:\work\powershellexiteventcalled.txt}
It works fine if user closes PowerShell window using exit command (i.e. exit ). But it does not work if user closes it by clicking on Close ('X') button on top right.
I could not find any way to handle this. I also tried to do it the following way, but this does not work either:
PS > $query = "Select * from __InstanceDeletionEvent WITHIN 5 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.Name='powershell.exe'"
PS > Register-WmiEvent -Query $query -Action {get-process | out-file c:\work\powershellexiteventcalled.txt}
Please guide how I can achieve this task.
UPDATE: With some useful input from a helpful professional online I also tried the following:
$appCurrentDomain = [System.AppDomain]::CurrentDomain
Register-ObjectEvent -Action {get-process | out-file c:\work\powershellexiteventcalled.txt} -InputObject $appCurrentDomain -EventName DomainUnload
But again, it does not work. As per Microsoft "DomainUnload event occurs when an AppDomain is about to be unloaded.". But it does not work when I close the window ( or even type exit for that matter).
UPDATE:
With some help from other professionals online & a little effort I could achieve this with following code.
PS..> $code = #"
using System;
using System.Runtime.InteropServices;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
namespace MyNamespace
{
public static class MyClass
{
public static void SetHandler()
{
SetConsoleCtrlHandler(new HandlerRoutine(ConsoleCtrlCheck), true);
}
private static bool ConsoleCtrlCheck(CtrlTypes ctrlType)
{
switch (ctrlType)
{
case CtrlTypes.CTRL_C_EVENT:
Console.WriteLine("CTRL+C received!");
return false;
case CtrlTypes.CTRL_CLOSE_EVENT:
Console.WriteLine("CTRL_CLOSE_EVENT received!");
return true;
case CtrlTypes.CTRL_BREAK_EVENT:
Console.WriteLine("CTRL+BREAK received!");
return false;
case CtrlTypes.CTRL_LOGOFF_EVENT:
Console.WriteLine("User is logging off!");
return false;
case CtrlTypes.CTRL_SHUTDOWN_EVENT:
Console.WriteLine("User is shutting down!");
return false;
}
return false;
}
[DllImport("Kernel32")]
public static extern bool SetConsoleCtrlHandler(HandlerRoutine Handler, bool Add);
// A delegate type to be used as the handler routine
// for SetConsoleCtrlHandler.
public delegate bool HandlerRoutine(CtrlTypes CtrlType);
// An enumerated type for the control messages
// sent to the handler routine.
public enum CtrlTypes
{
CTRL_C_EVENT = 0,
CTRL_BREAK_EVENT,
CTRL_CLOSE_EVENT,
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT
}
}
}"#
PS..> $text = Add-Type -TypeDefinition $code -Language CSharp
PS..> $rs = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace
PS..> [MyNamespace.MyClass]::SetHandler()
BUT THERES AN ISSUE I AM FACING.... If I run any cmdlet on console after registering this handler (e.g. get-date, get-process). Then the application will crash whenever an event occurs (e.g. Ctrl+C, close). Can someone please help me with this?

Unfortunately, you can't do this. The only time an exiting event will get called is if you type "exit" at the PowerShell prompt.
This is how I hook up my exiting event:
$null = Register-EngineEvent -SourceIdentifier `
([System.Management.Automation.PsEngineEvent]::Exiting) -Action { # Put code to run here }

The unhandled exception occurs because garbage collection has already disposed your handler by the time the unmanaged method is called. You can get around that by storing it in a static field:
private static HandlerRoutine s_rou;
public static void SetHandler()
{
if (s_rou == null)
{
s_rou = new HandlerRoutine(ConsoleCtrlCheck);
SetConsoleCtrlHandler(s_rou, true);
}
}

When you click the X to close you are closing the application hosting PowerShell. This application would need to handle the exit situation. I believe the default PS host is the Windows console which is obviously not doing what you need. You could host PowerShell in a custom host and handle the exit events. I'm on a Mac right now but maybe running under the PS ISE would handle this for you?

Related

Powershell Runspace error when executing for the first time for the day

"Exception getting "BaseName": "There is no Runspace available to run scripts in this thread.
You can provide one in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type.
The script block you attempted to invoke was: $this.Name""
Now, the c# code to create the runspace are as below
without credentials (Localhost)
return RunspaceFactory.CreateRunspace();
with credentials
var secureString = new SecureString();
PowerShell instance = PowerShell.Create();
instance.AddScript(string.Format("ConvertTo-SecureString {0} -AsPlainText -Force", _strapppwd));
foreach (PSObject psOutput in instance.Invoke())
{
secureString = psOutput.BaseObject as SecureString;
}
var remoteCredential = new PSCredential(_strappuname, secureString);
var connectionInfo = new WSManConnectionInfo();
connectionInfo.ComputerName =_strappip; ;
connectionInfo.Credential = remoteCredential;
var runspace = RunspaceFactory.CreateRunspace(connectionInfo);
return runspace
Anyone else face the same issue? Need to fix this as soon as possible
If I ran the application class which throws the error, second time it works fine without any changes in code. No error is thrown. This is strange.
We do have a flush and close method for stoping the runspace.
Waiting for the proper solution for it.

PDFCreator to convert pdf to txt in PowerShell

Updated Comment: I'm attempting to use PDFCreator to convert pdf files into txt files via PowerShell but it still doesn't seem to be working.
Any help is appreciated!
$PDFCreator = New-Object -ComObject PDFCreator.JobQueue
$PDF = 'C:\Users\userName\Downloads\SampleACORD.pdf'
$TXT = 'C:\Users\userName\Downloads\SampleACORD.txt'
try {
$PDFCreator.initialize()
if($PDFCreator.WaitForJob(5)){
$PJ = $PDFCreator.NextJob
}
if($PJ){
$PJ.PrintFile($PDF)
$PJ.ConvertTo($TXT)
}
} catch {
$_
Break
}
finally {
if($PDFCreator){
$PDFCreator.ReleaseCom()
}
}
You are getting that because $PJ is $null. NextJob isn't returning anything.
To guard against this, WaitForJob(int) returns a bool, $true if a job arrived and $false if not, so you should know after WaitForJob completes whether there is a job to get or not:
if( $PDFCreator.WaitForJob(5) ){
$PJ = $PDFCreator.NextJob
$PJ.allowDefaultPrinterSwitch('C:\Users\userName\Downloads\SampleACORD.txt', $true)
$PJ.ConvertTo($TXT)
} else {
# Handle the no jobs case here
}
You could also do a null check against $PJ before trying to call $PJ.allowDefaultPrinterSwitch:
if( $PJ ){
$PJ.allowDefaultPrinterSwitch('C:\Users\userName\Downloads\SampleACORD.txt', $true)
$PJ.ConvertTo($TXT)
}
Here is some more information on the PDFCreator.JobQueue API, which you may find useful.
To address your issue in the comments, where the file is not being produced, this page of the documentation explains the logical flow of how the conversion process should work:
Call the Initialize() method with your COM Object.
Call WaitForJob(timeOut) if you are waiting for one print job to get in the queue. The parameter timeOut specifies the maximum time the queue waits for the print job to arrive.
Now you are able to get the next job in the queue by calling the property NextJob.
Setup the profile of the job with the method SetProfileByGuid(guid). The guid parameter is used to assign the appropriate conversion profile.
Start the conversion on your print job with ConvertTo(path). The path parameter includes the full path to the location where the converted file should be saved and its full name.
The property IsFinished informs about the conversion state. If the print job is done, IsFinished returns true.
If you want to know whether the job was successfully done, consider the property IsSuccessful. It returns true if the job was converted successfully otherwise false.
In your case, I'm not sure how essential the profile would be, but it does look like your code fails to wait for completion. The following code will wait for the conversion job to finish (and check for success if you need to):
# Wait for completion
while( -Not $PJ.IsFinished ){
Start-Sleep -Seconds 5
}
# Check for success
if( $PJ.IsSuccessful ){
# success case
} else {
# failure case
}
Unrelated, but good practice, wrap your code in a try/finally block, and put your COM release in that block. This way your COM connection closes cleanly even in the event of a terminating error:
$PDFCreator = New-Object -ComObject PDFCreator.JobQueue
try {
# Handle PDF creator calls
} finally {
if( $PDFCreator ){
$PDFCreator.ReleaseCom()
}
}
The finally block is guaranteed to execute before returning to a parent scope, so whether the code succeeds or fails, the finally block will be run.

How to verify an SCCM action has completed via PowerShell

The code below is used to trigger the SCCM action 'SoftwareUpdateScan' cycle:
$CimMethodParameters = #{
Namespace = 'ROOT\ccm'
Class = 'SMS_CLIENT'
MethodName = 'TriggerSchedule'
}
Invoke-CimMethod #CimMethodParameters -Arguments #{sScheduleID = '{00000000-0000-0000-0000-000000000113}' }
After the above code is run, I do not know how to determine the following:
That the action was successfully started?
When the action completes?
Was the action completed successfully or not?
Found a function for each action that incorporates the verification of completion or failure of the action.
https://github.com/MicksITBlogs/PowerShell/blob/master/SCCMActions.ps1

Why doesn't Pester catch errors using a trap

I'm wondering why I get the following behaviour when running this script. I have the script loaded in PowerShell ISE (v4 host) and have the Pester module loaded. I run the script by pressing F5.
function Test-Pester {
throw("An error")
}
Describe "what happens when a function throws an error" {
Context "we test with Should Throw" {
It "Throws an error" {
{ Test-Pester } | Should Throw
}
}
Context "we test using a try-catch construct" {
$ErrorSeen = $false
try {
Test-Pester
}
catch {
$ErrorSeen = $true
}
It "is handled by try-catch" {
$ErrorSeen | Should Be $true
}
}
Context "we test using trap" {
trap {
$ErrorSeen = $true
}
$ErrorSeen = $false
Test-Pester
It "is handled by trap" {
$ErrorSeen | Should Be $true
}
}
}
I then get the following output:
Describing what happens when a function throws an error
Context we test with Should Throw
[+] Throws an error 536ms
Context we test using a try-catch construct
[+] is handled by try-catch 246ms
Context we test using trap
An error
At C:\Test-Pester.ps1:2 char:7
+ throw("An error")
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (An error:String) [], RuntimeException
+ FullyQualifiedErrorId : An error
[-] is handled by trap 702ms
Expected: {True}
But was: {False}
at line: 40 in C:\Test-Pester.ps1
40: $ErrorSeen | Should Be $true
Question
Why is the trap{} apparently not running in the final test?
Here are two solutions to the problem based on the comments/suggested answers from #PetSerAl and #Eris. I have also read and appreciated the answer given to this question:
Why are variable assignments within a Trap block not visible outside it?
Solution 1
Although variables set in the script can be read within the trap, whatever you do within the trap happens to a copy of that variable, i.e. only in the scope that is local to the trap. In this solution we evaluate a reference to $ErrorSeen so that when we set the value of the variable, we are actually setting the value of the variable that exists in the parent scope.
Adding a continue to the trap suppresses the ErrorRecord details, cleaning up the output of the test.
Describe "what happens when a function throws an error" {
Context "we test using trap" {
$ErrorSeen = $false
trap {
Write-Warning "Error trapped"
([Ref]$ErrorSeen).Value = $true
continue
}
Test-Pester
It "is handled by trap" {
$ErrorSeen | Should Be $true
}
}
}
Solution 2
Along the same lines as the first solution, the problem can be solved by explicitly setting the scope of the $ErrorSeen variable (1) when it is first created and (2) when used within the trap {}. I have used the Script scope here, but Global also seems to work.
Same principle applies here: we need to avoid the problem where changes to the variable within the trap only happen to a local copy of the variable.
Describe "what happens when a function throws an error" {
Context "we test using trap" {
$Script:ErrorSeen = $false
trap {
Write-Warning "Error trapped"
$Script:ErrorSeen = $true
continue
}
Test-Pester
It "is handled by trap" {
$ErrorSeen | Should Be $true
}
}
}
According to this blog, you need to tell your trap to do something to the control flow:
The [...] thing you notice is that when you run this as script, you will receive both your error message and the red PowerShell error message.
. 'C:\Scripts\test.ps1'
Something terrible happened!
Attempted to divide by zero.
At C:\Scripts\test.ps1:2 Char:3
+ 1/ <<<< null
This is because your Trap did not really handle the exception. To handle an exception, you need to add the "Continue" statement to your trap:
trap { 'Something terrible happened!'; continue }
1/$null
Now, the trap works as expected. It does whatever you specified in the trap script block, and PowerShell does not get to see the exception anymore. You no longer get the red error message.

Powershell Invoke method neither throwing exception nor returning result

I am trying to build an ASP.Net, c# application to expose few IIS management related activities through web interface for a distributed Admin group.
I am making use of System.Management.Automation V3.0 library to wrap power shell commands. As a first step I wanted to list all Web Applications that are currently up and running on local IIS by invoking Get-WebApplication command.
This is where I am facing the issue. Method call is neither throwing any exception nor its returning the result. Does anyone know the root cause of this issue? Please share your experience of building such interface using System.Management.Automation.dll.
var shell = PowerShell.Create();
shell.Commands.AddScript("Get-WebApplication | Out-String");
try
{
var results = shell.Invoke();
if (results.Count > 0)
{
var builder = new StringBuilder();
foreach (var psObject in results)
{
builder.Append(psObject.BaseObject.ToString() + "\r\n");
}
}
}
catch(Exception ex)
{
throw;
}
PS: Get-Service in place of Get-WebApplication works absolutely fine by returning list of services available on the machine.
PowerShell.Create() does not create new PowerShell process. If you does not specify Runspace for it, then it will create new in-process Runspace. Since it run in your process, it will match your process bitness and you can not change that. To create Runspace with different bitness you need to create out of process Runspace. Here is a sample console application, which demonstrate how you can do that:
using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
public static class TestApplication {
public static void Main() {
Console.WriteLine(Environment.Is64BitProcess);
using(PowerShellProcessInstance pspi = new PowerShellProcessInstance()) {
string psfn = pspi.Process.StartInfo.FileName;
psfn=psfn.ToLowerInvariant().Replace("\\syswow64\\", "\\sysnative\\");
pspi.Process.StartInfo.FileName=psfn;
using(Runspace r = RunspaceFactory.CreateOutOfProcessRunspace(null, pspi)) {
r.Open();
using(PowerShell ps = PowerShell.Create()) {
ps.Runspace=r;
ps.AddScript("[Environment]::Is64BitProcess");
foreach(PSObject pso in ps.Invoke()) {
Console.WriteLine(pso);
}
}
}
}
}
}
If you compile this application as x32, it still will use x64 out of process Runspace on x64 operation system.