Starting .ps1 Script from PowerShell with Parameters and Credentials and getting output from it - powershell

I think my problem has a simple solution. But now i'm a bit confused.
I have Java Code, that starts 1 Powershell Script. This Powershell Script must start other scripts.
Java -> Powershell.ps1 ->
Script1.ps1
Script2.ps1
Script3.ps1
Script....
Script1,2,..etc performing multiple tasks and return String Values.
I've tried
Start-Process, Invoke-Command and Invoke-Expression
Assuming script1.ps1 is:
$a = 1+2
$a
Start-Process would work the best for me but im not getting the output:
$arguments = "C:\..\script1.ps1" + " -ClientName" + $DeviceName
$output = Start-Process powershell -ArgumentList $arguments -Credential $credentials
$output
$output ist NULL.
Thank you very much!

Start-Process produces no output by default.
(The only way to make it produce output directly is to use -PassThru, which then doesn't return the script's output, but a System.Diagnostics.Process instance representing the newly created process - see below.)
The only way to capture output from your script via Start-Process is to use the -RedirectStandardOutput and
-RedirectStandardError parameters to capture the script's output as text, in files.[1][2]
You can then read those files in PowerShell after the new process has completed, which you can ensure in one of two ways:
Also pass the -Wait switch to Start-Process, to make the invocation synchronous, which means that when Start-Process returns, the output has already been captured in the specified file(s).
Use -PassThru to obtain a System.Diagnostics.Process instance and pass it to Wait-Process later (or use its .WaitForExit() method directly; property .HasExited can be used to check whether the process is still running).
Here's what may work in your situation:
$arguments = "-File C:\...\script1.ps1" + " -ClientName" + $DeviceName
# Launch the script in a new window running as the given user,
# capture its standard output in file ./out.txt,
# and wait for it to finish.
Start-Process -Wait -RedirectStandardOutput ./out.txt powershell -ArgumentList $arguments -Credential $credentials
"Running script1.ps1 produced the following output:"
Get-Content ./out.txt
The PowerShell CLI, regrettably, reports all of PowerShell's 6 output streams, via standard output (see this answer), so the above captures all output from your script, including error output.
However, you can use, e.g., -RedirectStandardError ./err.txt to capture the error stream separately.
[1] Calling another PowerShell instance via its CLI offers a structured alternative to capturing unstructured text (the for-display output as it would print to the console, which is what happens by default):
-OutputFormat xml (or -of xml / -o xml) makes PowerShell format its output in CLIXML format, which is the same XML-based serialization format used in PowerShell remoting and background jobs for serializing rich objects, which you can "rehydrate" with a later Import-Clixml call.
Note: For most complex objects there is a loss of type fidelity: that is, they are serialized as emulations of the original objects; in short as "property bags" without methods, which, however may be sufficient - see this answer.
Here's a quick demonstration, using a [datetime] instance, which does deserialize with type fidelity:
# Call Get-Date via the PowerShell CLI and save the output
# in CLIXML format in file ./out.xml
Start-Process -Wait -RedirectStandardOutput ./out.xml powershell '-of xml -c Get-Date'
# Import the CLIXML file and convert its content back to a [datetime] instance.
"Type of the CLIXML-serialized and deserialized `Get-Date` output:"
(Import-CliXml ./out.xml).GetType().FullName # -> System.DateTime
[2] The character encoding of the output files is determined by the encoding stored in [Console]::OutputEncoding, which reflects the current console output code page, which defaults to the system's active legacy OEM code page.

Related

Command Line Command Output in start-process from exe file

Here is the program. I am using dell command | configure. The command-line command is as follows:
"C:\Program Files (x86)\Dell\Command Configure\X86_64>cctk.exe" --wakeonlan
In Powershell you can navigate to the folder and run:
./cctk.exe --wakeonlan
I can pipe the above command into a variable and get the information I need. This requires my shell to cd into the folder accordingly and run accordingly.
$test = ./cctk.exe --wakeonlan
This will give you an output. However when you use start-process, you get no output as this is a command-line command. A cmd screen appears and runs the command. So, I added a -nonewwindow and -wait flags. The output now appears on the screen, but I can't seem to capture it.
$test = start-process "C:\Program Files (x86)\Dell\Command Configure\X86_64\cctk.exe" -ArgumentList #("--wakeonlan") -NoNewWindow -Wait
At this point test is empty. I tried using the Out-File to capture the information as well. No success. The command outputs to the screen but nowhere else.
I also tried the cmd method where you pipe the information in using the /C flag.
$test = Start-Process cmd -ArgumentList '/C start "C:\Program Files (x86)\Dell\Command Configure\X86_64\cctk.exe" "--wakeonlan"' -NoNewWindow -Wait
However, I have tried many variations of this command with no luck. Some say C:\Program is not recognized. Some just open command prompt. The above says --wakeonlan is an unknown command.
Any pointers would help greatly.
There are various ways to run this without the added complication of start-process.
Add to the path temporarily:
$env:path += ';C:\Program Files (x86)\Dell\Command Configure\X86_64;'
cctk
Call operator:
& 'C:\Program Files (x86)\Dell\Command Configure\X86_64\cctk'
Backquote all spaces and parentheses:
C:\Program` Files` `(x86`)\Dell\Command` Configure\X86_64\cctk
To elaborate on js2010's helpful answer:
In short: Because your executable path is quoted, direct invocation requires use of &, the call operator, for syntactic reasons - see this answer for details.
To synchronously execute console applications or batch files and capture their output, call them directly ($output = c:\path\to\some.exe ... or $output = & $exePath ...), do not use Start-Process (or the System.Diagnostics.Process API it is based on) - see this answer for more information.
If you do use Start-Process, which may be necessary in special situations, such as needing to run with a different user identity:
The only way to capture output is in text files, via the -RedirectStandardOutput / -RedirectStandardError parameters. Note that the character encoding of the output files is determined by the encoding stored in [Console]::OutputEncoding[1], which reflects the current console output code page, which defaults to the system's active legacy OEM code page.
By contrast, even with -NoNewWindow -Wait, directly capturing output with $output = ... does not work, because the launched process writes directly to the console, bypassing PowerShell's success output stream, which is the one variable assignments capture.
[1] PowerShell uses the same encoding to decode output from external programs in direct invocations - see this answer for details.

Start-Process, Invoke-Command or?

Using the program got your back or GYB. I run the following command
Start-Process -FilePath 'C:\Gyb\gyb.exe' -ArgumentList #("--email <Email Address>", "--action backup", "--local-folder $GYBfolder", "--service-account", "--batch-size 4") -Wait
The issue is that when the process is done my script does not complete.
$GYBfolder = $GYBfolder.Replace('"', "")
$output = [PSCustomObject]#{
Name = $SourceGYB
Folder = $GYBfolder
}
$filename = "C:\reports\" + $SourceGYB.Split("#")[0] + "_Backup.csv"
$output | Export-Csv $filename -NoTypeInformation | Format-Table text-align=left -AutoSize
Return $filename
For some reason the script stops right before the return.
I am curious to know if I should be using a different command to run GYB?
Any thoughts on why the script does not process the return?
There's great information in the comments, but let me attempt a systematic overview:
To synchronously execute external console applications and capture their output, call them directly (C:\Gyb\gyb.exe ... or & 'C:\Gyb\gyb.exe' ...), do not use Start-Process - see this answer.
Only if gyb.exe were a GUI application would you need **Start-Process -Wait in order to execute it synchronously**.
A simple, but non-obvious shortcut is to pipe the invocation to another command, such as Out-Null, which also forces PowerShell to wait (e.g. gyb.exe | Out-Null) - see below.
When Start-Process is appropriate, the most robust way to pass all arguments is as a single string encoding all arguments, with appropriate embedded "..." quoting, as needed; this is unfortunate, but required as a workaround for a long-standing bug: see this answer.
Invoke-Command's primary purpose is to invoke commands remotely; while it can be used locally, there's rarely a good reason to do so, as &, the call operator is both more concise and more efficient - see this answer.
When you use an array to pass arguments to an external application, each element must contain just one argument, where parameter names and their values are considered distinct arguments; e.g., you must use #(--'action', 'backup', ...) rather than
#('--action backup', ...)
Therefore, use the following to run your command synchronously:
If gyb.exe is a console application:
# Note: Enclosing #(...) is optional
$argList = '--email', $emailAddress, '--action', 'backup', '--local-folder', $GYBfolder, '--service-account', '--batch-size', 4
# Note: Stdout and stderr output will print to the current console, unless captured.
& 'C:\Gyb\gyb.exe' $argList
If gyb.exe is a GUI application, which necessitates use of Start-Process -Wait (a here-string is used, because it makes embedded quoting easier):
# Note: A GUI application typically has no stdout or stderr output, and
# Start-Process never returns the application's *output*, though
# you can ask to have a *process object* returned with -PassThru.
Start-Process -Wait 'C:\Gyb\gyb.exe' #"
--email $emailAddress --action backup --local-folder "$GYBfolder" --service-account --batch-size 4
#"
The shortcut mentioned above - piping to another command in order to force waiting for a GUI application to exit - despite being obscure, has two advantages:
Normal argument-passing syntax can be used.
The automatic $LASTEXITCODE variable is set to the external program's process exit code, which does not happen with Start-Process. While GUI applications rarely report meaningful exit codes, some do, notably msiexec.
# Pipe to | Out-Null to force waiting (argument list shortened).
# $LASTEXITCODE will reflect gyb.exe's exit code.
# Note: In the rare event that the target GUI application explicitly
# attaches to the caller's console and produces output there,
# pipe to `Write-Output` instead, and possibly apply 2>&1 to
# the application call so as to also capture std*err* output.
& 'C:\Gyb\gyb.exe' --email $emailAddress --action backup | Out-Null
Note: If the above unexpectedly does not run synchronously, the implication is that gyb.exe itself launches another, asynchronous operation. There is no generic solution for that, and an application-specific one would require you to know the internals of the application and would be nontrivial.
A note re argument passing with direct / &-based invocation:
Passing an array as-is to an external program essentially performs splatting implicitly, without the need to use #argList[1]. That is, it passes each array element as its own argument.
By contrast, if you were to pass $argList to a PowerShell command, it would be passed as a single, array-valued argument, so #argList would indeed be necessary in order to pass the elements as separate, positional arguments. However, the more typical form of splatting used with PowerShell commands is to use a hashtable, which allows named arguments to be passed (parameter name-value pairs; e.g., to pass a value to a PowerShell command's
-LiteralPath parameter:
$argHash = #{ LiteralPath = $somePath; ... }; Set-Content #argHash
[1] $args and #args are largely identical in this context, but, strangely, #argList, honors use of --%, the stop-parsing symbol operator, even though it only makes sense in a literally specified argument list.

How to run an executable (exe) by providing a config file in powershell

I'm trying to run an exe in the background by providing a config file (yml in my case)
Tried the below, however this is not pushing the execution to background. -
./my.exe start --config-file $my_config_file
Found 'start-process' command which are specifically used for this case. With argument list is there any way to send the config file?
Start-Process -Wait -FilePath "my.exe" -ArgumentList
Remove the -Wait argument and pass the process arguments as an array via -ArgumentList parameter:
Start-Process -FilePath "my.exe" -ArgumentList 'start', '--config-file', "`"$my_config_file`""
The strange quoting for $my_config_file is required because a path may contain spaces. Start-Process does not do automatic quoting. From the docs:
If parameters or parameter values contain a space, they need to be surrounded with escaped double quotes.
Note that you won't receive output of the started process, if that matters to you. You can redirect to a file, using parameters -RedirectStandardOutput and -RedirectStandardError, but you can't (easily) store the output in a variable.
A way to start a process in the background, while being able to receive its output, is to create a job.

Max size of ScriptBlock / InitializationScript for Start-Job in PowerShell

When you start a new job with Start-Job, you can pass it a ScriptBlock and a InitializationScript for example:
Function FOO {
Write-Host "HEY"
}
Start-Job -ScriptBlock {FOO} -InitializationScript {
Function Foo { $function:FOO }
} | Wait-Job | Receive-Job
There seems to be a limit to the size of the initialization script you can pass, if it is too big then you get an error such as
[localhost] An error occurred while starting the background process. Error
reported: The filename or extension is too long.
+ CategoryInfo : OpenError: (localhost:String) [], PSRemotingTransportException
+ FullyQualifiedErrorId : -2147467259,PSSessionStateBroken
Behind the scenes, PowerShell is creating a new process and passing InitializationScript as a Base64 encoded command line parameter.
According to the Win32 CreateProcess() function, the max size of the command ine is 32,768 characters. So obviously if your Base64 encoded InitializationScript is getting near this size then you will probably get an error.
I haven't yet found a limit for the size of the ScriptBlock parameter. Can someone confirm that there is no limit?
I assume that there is no limit because it looks like the ScriptBlock is transmitted to the child process via standard input?
Your guess was correct.
PowerShell translates a Start-Job call into a PowerShell CLI call behind the scenes (to powershell.exe for Windows PowerShell, and to pwsh for PowerShell (Core) 7+), that is, it achieves parallelism via a child process that the calling PowerShell session communicates with, using the standard in- and output streams:
Because the -InitializationScript script block is translated to a Base64-encoded string representing the bytes of the UTF-16LE encoding of the block's string representation, which is passed to the CLI's -EncodedCommand parameter, its max. length is limited by the overall length limit of a process command line.
That limit is 32,766 characters (Unicode characters, not just bytes), because a terminating NUL character is required in the underlying WinAPI call (to quote from the CreateProcess() WinAPI function documentation you link to: "The maximum length of this string is 32,767 characters, including the Unicode terminating null character").
Note that the full path of the PowerShell executable is included in this limit, in double-quoted form (see below), and it really is the entire resulting command line that matters; therefore, given that PowerShell (Core) 7+ can be installed in any directory, its installation location has an effect on the effective limit, as does the length of the path of the current directory (see next point).
In Windows PowerShell, whose location is fixed, and whose CLI parameter values are of fixed length in the invocation (see below), this leaves 32,655 characters for the Base64-encoded string (32766 - 111 characters for the executable path and fixed parameters and the -EncodedCommand parameter name); while similar, no fixed number can be given for PowerShell (Core) 7+, due to differing install locations and the length of the -wd (working-directory) argument depending on the current location.
Base64-encoding the bytes of a UTF-16LE-encoded strings results in a ca. 2.67-fold increase in length, which*makes the **max. length of a script block passed to -InitializationScript 12,244 characters[1] for Windows PowerShell; for PowerShell (Core) 7+, it'll be slightly lower, depending on the installation location and the length of the current directory's path.
By contrast, the -ScriptBlock argument, i.e. the operation to perform in the background, is sent via stdin (the standard input stream) to the newly launched PowerShell process, and therefore has no length limit.
For instance, the following Start-Job call:
Start-Job -ScriptBlock { [Environment]::CommandLine } -InitializationScript { 'hi' > $null } |
Receive-Job -Wait -AutoRemoveJob
reveals that the background-job child process was launched as follows, when run from Windows PowerShell:
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -Version 5.1 -s -NoLogo -NoProfile -EncodedCommand IAAnAGgAaQAnACAAPgAgACQAbgB1AGwAbAAgAA==
As you can see, the -ScriptBlock argument's text is not present in the resulting command line (it was sent via stdin), whereas the -InitializationScript argument's is, as the Base64-encoded string passed to -EncodedCommand, which you can verify as follows:
# -> " 'hi' > $null ", i.e. the -InitializationScript argument, sans { and }
[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('IAAnAGgAaQAnACAAPgAgACQAbgB1AGwAbAAgAA=='))`)
As for the other parameters:
-s is short for -servermode, and it is an undocumented parameter whose sole purpose is to facilitate background jobs (communication with the calling process via its standard streams); see this answer for more information.
-Version 5.1 applies only to Windows PowerShell, and isn't strictly necessary.
-NoLogo is also not strictly necessary, because it is implied by the use of -EncodedCommand (as it would be with -Command and -File).
In PowerShell (Core) 7+, you'd also see a -wd (short for: -WorkingDirectory) parameter, because background jobs there now sensibly use the same working directory as the caller.
[1]
[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes('x' * 12244)).Length yields 32652, which is the closest you can come to the 32655 limit; an input length of 12245 yields 32656.
the script block actually have limit. and you can run command with scriptblock with 3 way :
$scriptblock = '
get-help
get-command
dir
get-help *command*
get-command *help*
'
iex $scriptblock
or use it :
$scriptblock = {
get-help
get-command
dir
get-help *command*
get-command *help*
}
Start-Process powershell.exe -ArgumentList "Command $scriptblock"
or use this :
Start-Process powershell {iex '
get-help
get-command
dir
get-help *command*
get-command *help*
'
}
the limit of script that can pass to powershell.exe is 12190 bytes.
but for script i never get limit from powershell more than 4000 line code.

Run Powershell script that uses -List (parameter) with alternate credentials that

I was struggling to get this simple (?) function working today.
I have a PowerShell script that reads computer names from a txt-based file. It works fine when run from a PowerShell session by the following one-liner:
./"Server Health Check.ps1" -List One-off.txt
As you can see, it's got a long file name, so it's wrapped with quotes.
However, I'm building a PowerShell GUI form with radio boxes that will pass on a choice for a text file that will be used to a call the script. Trick is, the script needs to be run with alternate admin account, and i'm not clear how to make that work.
For another script I've got that doesn't use I know i can use something along the lines of the following, this uses the old DOS "runas", however, it doesn't work with the -list function.
invoke-command -scriptblock {runas.exe /user:domain\$Env:Username"admin" "powershell.exe -file \"\\Server\c$\LONG FOLDER\Server Health Check.PS1""}
So, in a nutshell, how do get a script to launch with alternate credentials that reads a parameter (-List) from the command line? I'm also keen to preserve my directory structure, which includes folders with spaces. The script is titled: "Server health check.ps1"
The last thing I tried was the following
$ScriptPath = "C:\SCRIPTS FOLDER\Server Health Check.ps1"
$ArgList = "-List C:\SCRIPTS FOLDER\One-off.txt"
Invoke-Command -filepath $ScriptPath -Credential DragonBallDomain\$Env:UserName"Admin" -ArgumentList $ArgList
The result was the following message:
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
I'm almost certain this is do-able by invoke-command or start-process, it's just a matter of getting the correct formatting? I'm probably missing a / or a ' or "" somewhere in my trials with start-process or invoke-command.
Any help appreciated!
Update for April 30:
I've tried some more to make this work, i'm close, but still not quite there.
$LongScriptPath = resolve-path Script.ps1
$LongFolderPath = \\UNC\PATH TO FOLDER\WITH LONG NAME\
start-process -filepath powershell.exe -argumentlist " -file``"$($FilePath.path)`"" -cred DOMAIN\USERID -WorkingDirectory "$LongFolderPath"
Adding the -credential is what causes an error that states that the -file parameter is invalid. I'm sure there's a way to do this.
Note: Completely rewritten after the requirements became clearer.
To run a command as a different user locally, use Start-Process -Credential ...
That is what you've attempted in your update in principle, but there are problems with how you're passing parameters; try this instead:
$LongScriptPath = resolve-path Script.ps1
$LongFolderPath = '\\UNC\PATH TO FOLDER\WITH LONG NAME\'
start-process `
powershell.exe `
-ArgumentList '-file', $LongScriptPath, '-List', 'One-off.txt' `
-Credential DOMAIN\USERID `
-WorkingDirectory $LongFolderPath
The key to making this work is to pass all parameters to pass to powershell.exe as an array via Start-Process's -ArgumentList parameter, which means that the parameters must be ,-separated.
Note how an array is always parsed in expression mode, which means that literal string elements such as -file and -List must be quoted.
It is important in general to understand the difference between PowerShell's two fundamental parsing modes, argument mode and expression mode, and which is applied when - see https://technet.microsoft.com/en-us/library/hh847892.aspx
Add -Wait to wait for the script to finish; Start-Process is asynchronous by default (all PS cmdlets named Start-* are).
Caveat: For commands invoked as a different user, you can only wait from an elevated prompt.
If it isn't, the command will still execute, but will do so asynchronously, and you'll get an Access denied error message in the current console; in effect, -Wait is ignored.
Only if not running as a different user: Add -NoNewWindow -Wait if you want to run the script in the current console window; Start-Process opens a new window by default for console applications such as powershell.exe and cmd.exe.
If you do run the command as a different user, -NoNewWindow is quietly ignored.
As for the original symptom and why using Invoke-Command to run a command locally as a different user is ill-advised:
Invoke-Command -Credential ... requires that the -ComputerName parameter be specified too.
Run Get-Help Invoke-Command to see all parameter sets that involve the -Credential parameter. The OP's original command had only -Credential, but not -ComputerName, which caused PS to complain that no parameter set could be unambiguously identified.
Once you use -ComputerName, PowerShell remoting is invariably used, even if you specify . - the local computer - as the only computer to target.
Using remoting has two implications:
Remoting is not available by default, and must be configured on the target computer (the local computer, in this case).
Using remoting requires invocation with admin privileges.
In short:
While you can perform purely local invocations with Invoke-Command, you cannot do so as another user, because that invariably involves remoting.
Start-Process, by contrast, solely exists to run commands locally, optionally as a different user.