Jump to content

How to run multiple PowerShell Cmdlet in the same instance under AutoIt?


Recommended Posts

Posted (edited)

Hello, I'm working on a big AutoIt script that takes information from the network, from the user computer, from other places and sets up some Windows configurations for all users in the department. This scripts runs directly in the user's computer, not remotely, all computers runs the same AutoIt script. At this moment everything is working well, but, the steps that calls PowerShell are not good enough.

Problem 1: each one of the PowerShell calls is totally independent of the others, which means, every step that uses these smalls PowerShell Cmdlet, it creates a new instance of the PS, executes the Cmdlet and closes. This takes around 2 to 5 seconds, depending the Cmdlet. This happens a lot.

Problem 2: some of the PS Cmdlet needs to be elevated to runs as Admin, so, the user have to allow it to run it as Admin many times. Really annoying.

Is there a way to create an instance of the PowerShell, fills the Cmdlet, run it, get the response and then fills a new Cmdlet, run again, get the new response, and repeat over and over again with no need to elevate and restart the PS every new step? If is possible, with no big dependencies, other wise I have to install the dependencies in many computers.

One example of the Cmdlet:

; Change region settings. In the second instance of PowerShell there is no way to get the STDOUT values.
$sCommands = "powershell -Command Start-Process powershell -verb runas -ArgumentList {" & _        ; Runs a second instance of PowerShell in elevated level (admin).
      "   Set-ItemProperty -Path 'HKCU:\Control Panel\International' -Name sList -Value '.';" & _
      "   $culture = Get-Culture;" & _
      "   $culture.DateTimeFormat.ShortDatePattern = 'dd/mm/yyyy';    ; 'Short date' in 'Region' window.;" & _
      "   Set-Culture $culture;" & _
      " }  "    ; End of command line.

RunWait(@ComSpec & " /c " & $sCommands, "", @SW_HIDE, $STDERR_CHILD + $STDOUT_CHILD)
   
; Check if it's ok.
$sCommands = "powershell -Command "" " & _      ; Runs the PowerShell in the regular way.
      "   if ((Get-ItemProperty -Path 'HKCU:\Control Panel\International').sShortDate -eq 'dd/mm/yyyy')" & _
      "   {" & _
      "      Write-Output 'Result: ok';" & _
      "   }" & _
      "   else{" & _
      "      Write-Output 'Result: error';" & _
      "   }" & _
      " "" "    ; End of command line.

$iPID = Run(@ComSpec & " /c " & $sCommands, "", @SW_HIDE, $STDERR_CHILD + $STDOUT_CHILD)

Do
   Sleep(1000)
Until (Not ProcessExists($iPID))

$sOutput &= StdoutRead($iPID)

; Any error.
If (StringInStr($sOutput, "error") > 0) Then
   Return 0     ; Error.
EndIf

Return 1    ; Ok.

 

I'm thinking to do something like this, but I didn't find any information about how to do it exactly:

; First run.
Local $oPwrS = ObjCreate("PowerShell.exe")
Local $sCmd = "Get-ItemPropertyValue 'Registry::HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'EnableLUA'"
Local $sResult = $oPwrS.Execute($sCmd)

If ($sResult... do somethink

; Second run.
$sCmd = "Get-ItemPropertyValue 'Registry::HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'EnableLUA'"
$sResult = $oPwrS.Execute($sCmd)

If ($sResult... do somethink

Any clue?

 

Thanks in advance.

Edited by Chuckero
Link to post
Share on other sites
  • Moderators

My first question would be why you need to mix languages? If you're just getting/setting registry entries, why bother with PowerShell when you can do so natively with AutoIt? And if you are doing more, what is it that you are doing with PS that you cannot do with AutoIt?

"Profanity is the last vestige of the feeble mind. For the man who cannot express himself forcibly through intellect must do so through shock and awe" - Spencer W. Kimball

How to get your question answered on this forum!

Link to post
Share on other sites
16 hours ago, JLogan3o13 said:

My first question would be why you need to mix languages? If you're just getting/setting registry entries, why bother with PowerShell when you can do so natively with AutoIt? And if you are doing more, what is it that you are doing with PS that you cannot do with AutoIt?

Hello @JLogan3o13, I'm working in 5 different AutoIt scripts (for now), all of them have dozens of specific tasks and a user interface with check boxes to select each task the user needs to do. So I'm using the same sketch for all scripts, very similar each other, this is very ease to do and maintain in AutoIt, in really 95% of the tasks were done in AutoIt native code, but there are some tasks much much easier to do in PowerShell, mainly when I have to set some Windows configuration. So why not use the best of the both worlds?

PS: That code I shared is a very small part with no sensitive information.

Link to post
Share on other sites

Maybe this ?

#include <Array.au3>

Local $objShell = ObjCreate("WScript.Shell")
Local $FileName = "data.txt"
Local $cmdString = "Powershell get-childItem -recurse > " & $FileName
$objShell.Run($cmdString, 0, True)
;Local $array = FileReadToArray($FileName) ; put result in array
;_ArrayDisplay($array)
Local $sResult = FileRead($FileName) ; put result in string
ConsoleWrite ($sResult & @CRLF)

Just make a function that wraps the run method the way you want.

Link to post
Share on other sites
Posted (edited)
On 5/11/2021 at 11:56 AM, Nine said:

Maybe this ?

#include <Array.au3>

Local $objShell = ObjCreate("WScript.Shell")
Local $FileName = "data.txt"
Local $cmdString = "Powershell get-childItem -recurse > " & $FileName
$objShell.Run($cmdString, 0, True)
;Local $array = FileReadToArray($FileName) ; put result in array
;_ArrayDisplay($array)
Local $sResult = FileRead($FileName) ; put result in string
ConsoleWrite ($sResult & @CRLF)

Just make a function that wraps the run method the way you want.

Thanks @Nine, but this approach looks like the same I'm already doing. Each time I call $objShell.Run() it seems to open a new PS session. And I didn't figure out a way to elevate it to run admin Cmdlet.

Other than that, I found this way to get the results, a little bit easier but with the same behavior:

$sResult = $objShell.Exec($cmdString).StdOut.ReadAll

 

Edited by Chuckero
Link to post
Share on other sites
6 hours ago, Chuckero said:

Is there a way to call PowerShell as a COM Object?

...I do not know, however you could use a running powershell instance started with redirected I/O streams and use it to execute multiple commands without having to rerun it with each new command. Since it is necessary to be able to identify the beginning and the end of the output stream of the various commands, I thought of "injecting" two "flags", one for the beginning and one for the end, for each command executed, so as to be able to extract only the body of the output of the different commands.

This little script perhaps clarifies it better than many words. I hope it will be useful to you

#include <constants.au3>
#include <string.au3>
#RequireAdmin

; Use RunAs if you want use a fixed username/password within the script:
; Global $hPowerShellPID = RunAs("UserName", "Domain", "Password", "", "powershell.exe", '', @SW_HIDE, BitOR($STDIN_CHILD, $STDERR_MERGED))

; or use #RequireAdmin and a simple Run Statement to enter Admin credentials (if needed) at runtime
; Here we start a "permanent" powershell prompt with redirected streams
Global $hPowerShellPID = Run("powershell.exe", '', @SW_HIDE, BitOR($STDIN_CHILD, $STDERR_MERGED))

; this little loop is to wait for the "welcome" message from powershell
; just a way to be sure that the powershell is running and "streaming"
Do
    StdoutRead($hPowerShellPID)
Until @extended ; out stream started

; example:
; Here we "execute a command via the Powershell that is running in background...
; ...and we get back to resulting output
Local $Out = _Run_Cmdlet("Get-ItemPropertyValue 'Registry::HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'EnableLUA'", $hPowerShellPID)
ConsoleWrite($Out)

; here another command
ConsoleWrite(_Run_Cmdlet("dir c:\", $hPowerShellPID))

; .... and so on ...

; this function execute the passed powershell command and returns the resulting output
Func _Run_Cmdlet($sCmd, ByRef $hPS)
    Local $sStdOut = ''
    ; we insert a start and an end message in order to identify the "body" of the result of the command executed
    $sCmd = 'write-host "->StartOfStream<-"; ' & $sCmd & '; write-host "->EndOfStream<-"' & @CRLF
    StdinWrite($hPS, $sCmd)
    Do
        Sleep(250)
        $sStdOut &= StdoutRead($hPS)
        ; the presence of the known end message signals the end of the execution
    Until StringInStr($sStdOut, "->EndOfStream<-" & @LF)
    ; return only the body of the outpu of the passed command
    Return _StringBetween($sStdOut, "->StartOfStream<-" & @LF, "->EndOfStream<-" & @LF)[0]
EndFunc   ;==>_Run_Cmdlet

 

small minds discuss people average minds discuss events great minds discuss ideas.... and use AutoIt....

Link to post
Share on other sites

Batch processing: Send all your powershell commands to a temporary file to execute from powershell, and then once the AutoIt processes are complete, run this one temporary file in powershell before deleting it. One lua process to elevate, run, and downgrade and it is done.

Link to post
Share on other sites
Posted (edited)
On 5/12/2021 at 7:34 PM, Chimp said:

...I do not know, however you could use a running powershell instance started with redirected I/O streams and use it to execute multiple commands without having to rerun it with each new command. Since it is necessary to be able to identify the beginning and the end of the output stream of the various commands, I thought of "injecting" two "flags", one for the beginning and one for the end, for each command executed, so as to be able to extract only the body of the output of the different commands.

This little script perhaps clarifies it better than many words. I hope it will be useful to you

 

Wow, that's a great idea using StdinWrite() and StdoutRead()!

It's working almost perfectly, it's just missing the admin rights for PowerShell. I tried RunAs() but I have to provide the user and password, this is not a good option because the script will run in different computers and the user doesn't have the admin password (and there are different passwords).

The other option I tried is something like this:

Local $objShell = ObjCreate("Shell.Application")
$sRet = $objShell.ShellExecute("powershell.exe", "", "", "runas", 1)    ; 0=hide, 1=normal, 2=minimized, 3=maximized,

In this case I have the admin rights (with that Windows message asking to allow the app to make changes, and YES and NO buttons), but I can't send or get anything from that window, the functions  StdinWrite() and StdoutRead() doesn't work, maybe there is some parameters in ShellExecute() that I didn't figured out?

 

Edited by Chuckero
Link to post
Share on other sites
13 minutes ago, Chuckero said:

but I loose the returns of ConsoleWrite()

You don't have to.  Just add directive 

#AutoIt3Wrapper_Change2CUI=y

Compile your script, run from DOS console, there you go...

Link to post
Share on other sites

even if you use  #requireadmin and the current user is not an administrator, you still need to provide an administrator username and password to allow the program to run. Simply pressing OK is not enough ...

small minds discuss people average minds discuss events great minds discuss ideas.... and use AutoIt....

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...