gerardog/gsudo

Issue: Invoke-Gsudo so slow it appears to hang

MathiasMagnus opened this issue · 4 comments

Issue Description

I was giving gsudo my first spin, and I don't know if I'm doing something wrong, or is the brave new 1.7.1 is broken.

Steps to Reproduce

PS C:\Users\mate> gcm sudo

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           sudo -> Invoke-Gsudo

PS C:\Users\mate> gmo gsudoModule

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Script     0.0                   gsudoModule                         {gsudo, Invoke-Gsudo}

PS C:\Users\mate> gmo gsudoModule | select -exp Path
C:\Program Files (x86)\gsudo\gsudoModule.psm1
PS C:\Users\mate> Get-Process -IncludeUserName
Get-Process: The 'IncludeUserName' parameter requires elevated user rights. Try running the command again in a session that has been opened with elevated user rights (that is, Run as Administrator).
PS C:\Users\mate> sudo -Debug { Get-Process -IncludeUserName }
DEBUG: User ScriptBlock :  Get-Process -IncludeUserName
DEBUG: Full Script to run on the isolated instance: {
        $InputObject = $([System.Management.Automation.PSSerializer]::Deserialize([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('PE9ianMgVmVyc2lvbj0iMS4xLjAuMSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vcG93ZXJzaGVsbC8yMDA0LzA0Ij4NCiAgPE9iaiBSZWZJZD0iMCI+DQogICAgPFROIFJlZklkPSIwIj4NCiAgICAgIDxUPlN5c3RlbS5PYmplY3RbXTwvVD4NCiAgICAgIDxUPlN5c3RlbS5BcnJheTwvVD4NCiAgICAgIDxUPlN5c3RlbS5PYmplY3Q8L1Q+DQogICAgPC9UTj4NCiAgICA8TFNUIC8+DQogIDwvT2JqPg0KPC9PYmpzPg=='))));
        $argumentList = $null;
        $expectingInput = $([System.Management.Automation.PSSerializer]::Deserialize([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('PE9ianMgVmVyc2lvbj0iMS4xLjAuMSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vcG93ZXJzaGVsbC8yMDA0LzA0Ij4NCiAgPEI+ZmFsc2U8L0I+DQo8L09ianM+'))));
        $sb = [Scriptblock]::Create($([System.Management.Automation.PSSerializer]::Deserialize([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('PE9ianMgVmVyc2lvbj0iMS4xLjAuMSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vcG93ZXJzaGVsbC8yMDA0LzA0Ij4NCiAgPFM+IEdldC1Qcm9jZXNzIC1JbmNsdWRlVXNlck5hbWUgPC9TPg0KPC9PYmpzPg==')))));
        Set-Location $([System.Management.Automation.PSSerializer]::Deserialize([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('PE9ianMgVmVyc2lvbj0iMS4xLjAuMSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vcG93ZXJzaGVsbC8yMDA0LzA0Ij4NCiAgPFM+QzpcVXNlcnNcbWF0ZTwvUz4NCjwvT2Jqcz4='))));
        $ErrorActionPreference=$([System.Management.Automation.PSSerializer]::Deserialize([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('PE9ianMgVmVyc2lvbj0iMS4xLjAuMSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vcG93ZXJzaGVsbC8yMDA0LzA0Ij4NCiAgPFM+Q29udGludWVfeDAwMERfX3gwMDBBXzwvUz4NCjwvT2Jqcz4='))));

        if ($expectingInput) {
                try {
                        ($InputObject | Invoke-Command $sb -ArgumentList $argumentList)
                } catch {throw $_}
        } else {
                try{
                        (Invoke-Command $sb -ArgumentList $argumentList)
                } catch {throw $_}
        }
 }

And the cursor just stands still for eternity.

kép

Context:

PS C:\Users\mate> Get-ComputerInfo | ft -Property OsName,OsVersion,OsLanguage

OsName                   OsVersion  OsLanguage
------                   ---------  ----------
Microsoft Windows 11 Pro 10.0.22621 hu-HU

PS C:\Users\mate> gsudo --version
gsudo v1.7.1 (Branch.tags-v1.7.1.Sha.12438bbef9a72e857840401995fc0317ea99c3b3)
Copyright(c) 2019-2022 Gerardo Grignoli and GitHub contributors

Hi,
It is not hang, It is just too slow serializing and deserializing all the process tree, which is a relatively big object graph. If you leave enough time, it will finish.

You can try this experiment without involving gsudo. Open a Pwsh console and in the powershell prompt run exactly pwsh {Get-Process}.
It will probably take less than 2 seconds, because Pwsh is just serializing the text output. Now run exactly (pwsh {Get-Process}). The extra () will force a full object serialization & deserealization, which can easily take 100 seconds.

❯ pwsh {Get-Process}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
(...)

gerar C:\git\gsudo master ⇡ 337  1.31s                                       <== 1.31 seconds...
❯

VS

❯ (pwsh {Get-Process})

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
 
gerar C:\git\gsudo master ⇡ 337  1m 35.211s                        <== 95 seconds...
❯

Currently, Invoke-gsudo is always using a manual version of full serialization and deserialization, which results slow for this command.

invoke-gsudo {Get-Process}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
(...)

gerar C:\git\gsudo master ⇡ 337  2m 6.571s
❯

A month ago with the v1.6.0 release, a new syntax was added to gsudo: gsudo {scriptblock} (instead of Invoke-gsudo). This syntax took advantage of Pwsh's own serialization method. and leverages pwsh magic trick of only serializing the text output (instead of the full object graph) if the caller is not trying to capture the output (e.g. no parentheses) which is very fast, and transparently transition to full serialization only if needed.

You may need to remove your sudo alias, or point it to gsudo (instead of invoke-gsudo) to use this.

Let's see how it performs

gsudo.exe {Get-Process -IncludeUserName}

     WS(M)   CPU(s)      Id UserName                       ProcessName
     -----   ------      -- --------                       -----------

gerar C:\git\gsudo master ⇡ 337  2.607s          <== not bad!

But with ()

❯ (gsudo.exe {Get-Process -IncludeUserName})

     WS(M)   CPU(s)      Id UserName                       ProcessName
     -----   ------      -- --------                       -----------
(...)

gerar C:\git\gsudo master ⇡ 337  2m 12.125s    <== at least it works. lol
❯

TL;DR

  1. Powershell suffers serializing big object graphs, and gsudo feels the pain.
  2. In some situations gsudo {ScriptBlock} is much much faster than Invoke-gsudo {ScriptBlock}.

Not sure how can I improve the situation. Maybe improve the docs? Suggestions welcomed.

Thanks @gerardog for the throrough reply.

I think there are two aspects to improving the situation:

  1. Educate people on the status quo, why things are the way they are and why this is the state of the art. (Most of the root causes are historic, and it's not like Windows admins have had to suffer for decades... it's just asking the Windows security model to do something which it wasn't designed to do.) gsudo's README is a good place for that. Perhaps even a link to the related PS issue.
  2. Improving the current bottlenecks to allow more *nix-style administering, elevating per-command as needed. Improving object serialization/deserialization is an obvious first to investigate.

You've already done a tremendous job, gsudo is mighty useful, but IMHO we're still not in cross-plat admin nirvana.

  • gps -inc | where -prop UserName -Match $env:USERNAME is an established muscle/mind reflex equivalent to
  • ps aux | grep $LOGNAME

and committing the mistake of accidentally issuing such a command and it blocks the terminal for a solid minute... it just means there's a constant background process of "can I use sudo here?" running in our mind which is just unnecessary mental burden.

  1. Educate people on the status quo, why things are the way they are and why this is the state of the art.

Take a look at the modifications in:

  1. Improving the current bottlenecks...

I wish I had the time or resources to improve PowerShell serialization.

You've already done a tremendous job, gsudo is mighty useful, but IMHO we're still not in cross-plat admin nirvana.

  • gps -inc | where -prop UserName -Match $env:USERNAME is an established muscle/mind reflex equivalent to
  • ps aux | grep $LOGNAME

Thanks! BTW, you can still do:

gsudo {gps -inc | where -prop UserName -Match $env:USERNAME}.

The filter will run before serialization, so the result is narrowed first, making the response faster.

I'm closing this issue now, but please don't hesitate to reply with any suggestions.

Thanks