PowerShell remoting is a great way of utilising commands and processing power of remote systems all from one console. It is also good at pulling information from remote systems and collating this together. There are plenty of examples of using PSSessions, and the Invoke-Command functions to manipulate remote machines, bring down remote modules to work with locally, etc.
One of the shortcomings that I have come across, is the apparent inability to create a long running job on a remote session.
For example, I have a function that performs some processing, which can take anywhere from 20 minutes to 6 hours – depending on the amount of information that is supplied. This job is self contained – and reports itself by email, so once it is started there is no interaction.
I attempted to create a PSSession, and use Invoke-Command. This started the remote job successfully, however, when I closed my local instance of the shell window, the remote process also stopped.
Using Invoke-Command to start a process on the remote machine, something like the snippet below, exhibited the same result.
$Script = {Start-Process -FilePath C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ArgumentList "-Command Get-Service"} Invoke-Command -ComputerName remotepc -ScriptBlock $Script
I tried a number of variations of this, exhausting all of the options relating to both sessions and invoked commands, but nothing I found actually achieved my goal.
Looking outside of these commands, I found that the WMI Classes expose the Win32_Process class, which include a Process.Create method. PowerShell can interact with WMI well, so after some quick testing, I found this method created a new process on the remote machine which did not terminate when my local client disconnected.
I was able to wrap this up into a nice little function that can be re-used. It exposes the computer name, credentials and command options. The example included shows how you can start a new instance of PowerShell on the remote machine which can then run a number of commands. This could be changed to run any number of commands, or, if the script gets too long you could just get PowerShell to run a pre-created script file.
# ---------------------------------------------------------------------------------------------------------- # PURPOSE: Starts a process on a remote computer that is not bound to the local PowerShell Session # # VERSION DATE USER DETAILS # 1 17/04/2015 Craig Tolley First version # # ---------------------------------------------------------------------------------------------------------- <# .Synopsis Starts a process on the remote computer that is not tied to the PowerShell session that called this command. Unlike Invoke-Command, the session that creates the process does not need to be maintained. Any processes should be designed such that they will end themselves, else they will continue running in the background until the targeted machine is restarted. .EXAMPLE Start-RemoteProcess -ComputerName remotepc -Command notepad.exe Starts Notepad on the remote computer called remotepc using the current session credentials .EXAMPLE Start-RemoteProcess -ComputerName remotepc -Command "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command ""Get-Process | Out-File C:\Processes.txt"" " -Credential DOMAIN\Username Starts Powershell on the remote PC, running the Get-Process command which will write output to C:\Processes.txt using the supplied credentials #> function Start-RemoteProcess { Param( [Parameter(Mandatory=$true, Position =0)] [String]$ComputerName, [Parameter(Mandatory=$true, Position =1)] [String]$Command, [Parameter(Position = 2)] [System.Management.Automation.CredentialAttribute()]$Credential = [System.Management.Automation.PSCredential]::Empty ) #Test that we can connect to the remote machine Write-Host "Testing Connection to $ComputerName" If ((Test-Connection $ComputerName -Quiet -Count 1) -eq $false) { Write-Error "Failed to ping the remote computer. Please check that the remote machine is available" Return } #Create a parameter collection to include the credentials parameter $ProcessParameters = @{} $ProcessParameters.Add("ComputerName", $ComputerName) $ProcessParameters.Add("Class", "Win32_Process") $ProcessParameters.Add("Name", "Create") $ProcessParameters.Add("ArgumentList", $Command) if($Credential) { $ProcessParameters.Add("Credential", $Credential) } #Start the actual remote process Write-Host "Starting the remote process." Write-Host "Command: $Command" -ForegroundColor Gray $RemoteProcess = Invoke-WmiMethod @ProcessParameters if ($RemoteProcess.ReturnValue -eq 0) { Write-Host "Successfully launched command on $ComputerName with a process id of $($RemoteProcess.ProcessId)" } else { Write-Error "Failed to launch command on $ComputerName. The Return Value is $($RemoteProcess.ReturnValue)" } }
Once caveat of this approach is the expansion of variables. Every variable will be expanded before it is piped to the WMI command. For straight values, strings, integers, dates, that is all fine. However any objects need to be created as part of the script in the remote session. Remember that the new PowerShell session is just that, new. Everything that you want to use must be defined.
This code can be used to run any process. Generally you will want to ensure that you specify the full path to any executables. Remember that any paths are relative to the remote server, so be careful when you specify them.
Should you use this code to run PowerShell commands or scripts then you will need to keep a check on any punctuation that you use when specifying the command. Quotes will need to be doubled to escape them for example. This requires testing.
Also be aware, that this code will start a process, but there is nothing to stop it. Any process should either be self-terminating, or you will need to have another method of terminating the process. If you start a PowerShell session, they will generally terminate once the commands specified have completed.