jborean93 / pypsrp

PowerShell Remoting Protocol for Python
MIT License
324 stars 49 forks source link

Powershell invocations fail after #MaxConcurrentCommandsPerShell sequential commands #148

Open antaln opened 2 years ago

antaln commented 2 years ago

Powershell invocations fail after #MaxConcurrentCommandsPerShell sequential commands. From what I can tell, this is not really a pypsrp issue, but a Windows WSMV implementation behavior.

Example

Consider the following snippet:

with wsman, RunspacePool(wsman) as pool:

    for x in range(2000):
        ps = PowerShell(pool)
        ps.add_script("$PSVersionTable.Count")
        output = ps.invoke()
        print("x={}, out={}".format(x, output)) 

This fails after on my Win 2012R2 box after 1000 calls with following exception:

pypsrp.exceptions.WSManFaultError: Received a WSManFault message. (Code: w:InternalError, Reason: The WS-Management service cannot process the request. The maximum number of concurrent commands per shell has been exceeded. Retry the request later or raise the Maximum Commands per Shell quota.)

Analysis

This appears to be governed by Plugin\microsoft.powershell\Quotas\MaxConcurrentCommandsPerShell quota.

From what I can tell, this looks like it's intentional:

MS:WSMV, Product Behavior note 118 states:

Section 3.1.4.12: Windows implementations of the Shell processor do not decrement the MaxConcurrentOperationsPerUser counter when a Signal request with a Terminate code is issued to a Text-based Command Shell.

but is in violation of the protocol spec:

If the control code of the Signal request (section 2.2.4.38) is http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/Terminate, the Shell processor MUST decrement the server-side counter for MaxConcurrentOperationsPerUser.<118>

I am somewhat perplexed that a counter preventing concurrent commands would be actually used to limit sequential commands in a remote shell. Am I missing something here?

antaln commented 2 years ago

The limit applies only to MS:WSMV Text-based shells. AFAIK, PowerShell is considered a custom shell.

PSRP also requires sending terminate signal after conclusion of each command.

jborean93 commented 2 years ago

Hmm I thought I added the signal to the PowerShell class to do this but looking at the code that does not seem to be the case. The simplest solution seems to be to add a close() method that sends the Terminate signal that will decrement the command counter. It would be best to do it automatically but at this particular point in time I cannot as it's designed to be as efficient for tools like Ansible. Ansible gets away with it because they close the shell which also ends the counter.

As a workaround for now you can do

from pypsrp.wsman import SignalCode, WSMan
from pypsrp.powershell import PowerShell, RunspacePool

with wsman, RunspacePool(wsman) as pool:
    for x in range(2000):
        ps = PowerShell(pool)
        ps.add_script("$PSVersionTable.Count")
        output = ps.invoke()
        pool.shell.signal(SignalCode.TERMINATE, str(ps.id).upper())
        print("x={}, out={}".format(x, output)) 

Also as an FYI, the new psrp namespace that's being written right now does do this by default but this is still in development.

antaln commented 2 years ago

Cool, thanks!