Michael Conrad

NAME

Win32::PowerShell::IPC - Set up IPC between Perl and a PowerShell child process

VERSION

version 0.02

SYNOPSIS

  my $ps= Win32::PowerShell::IPC->new();
  
  # Set up MS Exchange remote session, which takes a dozen seconds
  
  $ps->run_or_die('$pw = "'.$pass.'" | ConvertTo-SecureString -AsPlainText -Force');
  $ps->run_or_die('$credential = New-Object System.Management.Automation.PSCredential'
                 .' -ArgumentList "'.$username.'", $pw');
  $ps->run_or_die('$session = New-PSSession -ConfigurationName Microsoft.Exchange'
                 .' -ConnectionUri https://ps.outlook.com/powershell'
                 .' -Credential $credential -Authentication Basic -AllowRedirection');
  $ps->run_or_die('Import-PSSession $session');
  
  # Now run all sorts of methods without waiting again!

DESCRIPTION

There's a lot of things in the Microsoft world that can't be done with perl. This is even more true with many Microsoft services offering PowerShell integration instead of more accessible Web APIs. While you can certainly write out a PowerShell script file and execute it, the session setup can be extremely slow and you might want to run numerous commands and take action based on the outcome. And you might rather do the logic in Perl than write all that in PowerShell, especially if it involves your database.

This module fires up a captive child PowerShell process to which you can submit commands, and receive the results. It's all text for now, but Perl excels at messy stuff like this.

PowerShell also seems to offer an option to exchange commands and results as XML, which would be a lot more reliable than text, but I haven't explored this yet. Patches welcome. (and good grief, haven't they discovered JSON over at Redmond, yet?)

This module is specific to Windows, and only tested on Strawberry perl so far. I was late to the party learning that PowerShell can run on Linux. On Linux, most of the problems solved by this module aren't problems, so you might as well just use IPC::Run. However, if I get around to implementing the XML communication protocol, I'll release another module for Linux.

ATTRIBUTES

running

Whether PowerShell is running

exe_options

Hashref of options to pass to PowerShell.exe The default is { -ExecutionPolicy => 'RemoteSigned' } and there is also an implied { -Command => "-" } which is required for the piping to work.

exe_cmdline

Lazy-built from exe_options. You can override this if you want, but make sure to include "-Command -"

exe_path

Full path to PowerShell.exe, lazy-built on demand from $PATH if you don't specify it. This attribute is writeable, but won't have any effect once the child process is started.

proc

The Win32::Process of PowerShell, initialized by "start_shell" or by calling any of the run/begin methods.

cleanup_delay

When this object is destroyed, wait up to this many milliseconds for the PowerShell process to exit. (we send it an "exit" command) Default is 2000. (2 seconds).

stdin

The write-side of PowerShell's STDIN pipe

stdout

The read-side of PowerShell's STDOUT pipe

stdout_h

The Windows Handle for the read handle of PowerShell's STDOUT pipe. The Windows Handle is needed for calling Win32 API calls via i.e. Win32::API wrappers, which can't accept a perl globref.

rbuf

The accumulated read buffer from STDOUT of PowerShell. Used to hold leftover stream contents that might follow the end of a command output.

METHODS

new

Standard Moo constructor. All attributes are optional. You might consider setting "cleanup_delay" or "exe_path"

start_shell

Start the PowerShell process. Dies on any failure. Returns true.

terminate_shell

Send "exit" command to shell, then wait up to "cleanup_delay" milliseconds for it to exit. If it doesn't, then kill it forcibly.

Note that terminate_shell is called when the object goes out of scope, or before global destruction at the END{} of the perl script.

begin_command

  $ps->begin_command("Text to execute");

This sends the text to the PowerShell instance, or dies trying. It does not wait for a result. It actually also sends an "echo" command that is used to detect the end of the output from your command.

collect_command

  my $output= $ps->collect_command;

This blocks until it receives the full output from your oldest pending command, and no other command. (you may have multiple pending commands) This module delimits the output with "echo" statements so that it can tell where the output of a command ends, but you shouldn't ever see signs of this implementation detail. I hope.

If you don't want to block, see "stdout_readable" and "read_more". Not that there's a complete solution there... but it will get you a little farther.

run_command

  my $output= $ps->run_command("My Command;");

Send the command and then wait for the result. This is like "begin_command" + "collect_command" except that it also discards the output of any previous commands to make sure that you're getting the output from this command.

run_or_die

Like "run_command", but if the output looks like a PowerShell exception report, die instead of return.

write_all

Lower-level method to write all data to the PowerShell pipe, but also die if PowerShell isn't running.

read_more

Lower-level method to read more data from the pipe into "rbuf" but also die if PowerShell isn't running.

stdout_readable

  if ($ps->stdout_readable) {
    $ps->read_more;
    ... # now inspect $ps->rbuf
  }

Calls "PeekNamedPipe" on $ps->stdout_h file handle, and returns true if there are bytes available.

FUNCTIONS

PeekNamedPipe

  Win32::PowerShell::IPC::PeekNamedPipe(
    $win32_handle, $buffer, $buffer_size,
    $bytes_read, $bytes_available, $bytes_remaining_always_zero
  );

The Windows API is really dismissive of the concept of non-blocking single-threaded operation that most of the Unix world loves so much. There is no way to perform a non-blocking read or select() on any windows handle other than a socket. Your options are either to dive into the crazy mess of the overlapped (asynchronous) I/O API, or make one thread per handle, or mess around with WaitForMultipleObjects which can only listen to 64 things at once.

The one little workaround I found available for pipes is the PeekNamedPipe function, which can tell you if there is any data on the pipe. You can't wait with a timeout, but it at least gives the option of a crude check/sleep loop.

This is not a method, but a regular function. The first argument is a Win32 handle (not perl globref), which you can get from "FdGetOsFHandle" in Win32API::File.

The second and third are the buffer and number of bytes to read. If the first is not defined I enlarge it to the specified size, and if the size is undefined I use the existing size of the buffer. (if both are undef, I pass NULL which doesn't read anything).

The $bytes_read receives the value of the number of bytes written to the buffer, but you can ignore it because I resize the buffer to match. Set to undef if you like.

The $bytes_available is the useful one, and receives the value of the number of bytes in the pipe.

The final argument isn't useful for byte stream pipes, but I included it anyway.

Returns true if it succeeded. Check Win32::FormatMessage(Win32::GetLastError()) otherwise.

SEE ALSO

IPC::Run

A well-maintained module for running child processes and bi-directional communication with them. However, the list of caveats for Win32 platform is somewhat alarming. (but I totally understand the difficulty it solves) I wasn't comfortable with using that in production, so I wrote this module with read/write on regular pipes.

PowerShell

A module to produce PowerShell command syntax using perl object methods.

Win32::Process

Used by this module. Wraps a background process for Win32 environments where fork/exec is broken, such as Strawberry Perl.

Win32::Job

I wish I'd found this module sooner, since it is something of an improvement over Win32::Process. However, it doesn't provide a method to check if a process is still running without killing the process, so ultimately not usable for this module.

AUTHOR

Michael Conrad <mike@nrdvana.net>

COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by Michael Conrad.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.