This is a variation of a question that has been asked and answered before.
The variation lies in using UserName
and Password
to set up the System.Diagnostics.ProcessStartInfo object. In this case, we're not able to read the output and error streams of the process – which makes sense because the process does not belong to us!
But even so, we've spawned the process so it should be possible to capture the output.
I suspect that this is a duplicate but it seems to have been misunderstood in the answer section.
CodePudding user response:
You can capture the output streams from an (invariably non-elevated) process you've launched with a different user identity, as the following self-contained example code shows:
Note:
Does not work if you execute the command via PowerShell remoting, such as via
Invoke-Command -ComputerName
, including JEA.- Apparently, impersonating another user is then not allowed, resulting in an
Access denied
error.
- Apparently, impersonating another user is then not allowed, resulting in an
The code prompts for the target user's credentials.
The working directory must be set to a directory path that the target user is permitted to access, and defaults to their local profile folder below - assuming it exists; adjust as needed.
Stdout and stderr output are captured separately, in full, as multi-line strings.
If you want to merge the two streams, call your target program via a shell and use its redirection features (
2>&1
).The example call below performs a call to a shell, namely
cmd.exe
via its/c
parameter, outputting one line to stdout, the other to stderr (>&2
). If you modify theArguments = ...
line as follows, the stderr stream would be merged into the stdout stream:Arguments = '/c "(echo success & echo failure >&2) 2>&1"'
The code works in both Windows PowerShell and PowerShell (Core) 7 , and guards against potential deadlocks by reading the streams asynchronously.[1]
In the install-on-demand, cross-platform PowerShell (Core) 7 edition, the implementation is more efficient, as it uses dedicated threads to wait for the asynchronous tasks to complete, via
ForEach-Object
-Parallel
.In the legacy, ships-with-Windows Windows PowerShell edition, periodic polling, interspersed with
Start-Sleep
calls, must be used to see if the asynchronous tasks have completed.If you only need to capture one stream, e.g., only stdout, if you've merged stderr into it (as described above), the implementation can be simplified to a synchronous
$stdout = $ps.StandardOutput.ReadToEnd()
call, as shown in this answer.
# Prompt for the target user's credentials.
$cred = Get-Credential
# The working directory for the new process.
# IMPORTANT: This must be a directory that the target user is permitted to access.
# Here, the target user's local profile folder is used.
# Adjust as needed.
$workingDir = Join-Path (Split-Path -Parent $env:USERPROFILE) $cred.UserName
# Start the process.
$ps = [System.Diagnostics.Process]::Start(
[System.Diagnostics.ProcessStartInfo] @{
FileName = 'cmd'
Arguments = '/c "echo success & echo failure >&2"'
UseShellExecute = $false
WorkingDirectory = $workingDir
UserName = $cred.UserName
Password = $cred.Password
RedirectStandardOutput = $true
RedirectStandardError = $true
}
)
# Read the output streams asynchronously, to avoid a deadlock.
$tasks = $ps.StandardOutput.ReadToEndAsync(), $ps.StandardError.ReadToEndAsync()
if ($PSVersionTable.PSVersion.Major -ge 7) {
# PowerShell (Core) 7 : Wait for task completion in background threads.
$tasks | ForEach-Object -Parallel { $_.Wait() }
} else {
# Windows PowerShell: Poll periodically to see when both tasks have completed.
while ($tasks.IsComplete -contains $false) {
Start-Sleep -MilliSeconds 100
}
}
# Wait for the process to exit.
$ps.WaitForExit()
# Sample output: exit code and captured stream contents.
[pscustomobject] @{
ExitCode = $ps.ExitCode
StdOut = $tasks[0].Result.Trim()
StdErr = $tasks[1].Result.Trim()
} | Format-List
Output:
ExitCode : 0
StdOut : success
StdErr : failure
If running as a given user WITH ELEVATION (as admin) is required:
By design, you cannot both request elevation with
-Verb RunAs
and run with a different user identity (-Credential
) in a single operation - neither withStart-Process
nor with the underlying .NET API,System.Diagnostics.Process
.- If you request elevation and you're an administrator yourself, the elevated process will run as you - assuming you've confirmed the Yes / No form of the UAC dialog presented.
- Otherwise, UAC will present a credentials dialog, requiring you to provide an administrator's credentials - and there's no way to preset these credentials, not even the username.
By design, you cannot directly capture output from an elevated process you've launched - even if the elevated process runs with your own identity.
- However, if you launch a non-elevated process as a different user, you can capture the output, as shown in the top section.
To get what you're looking for requires the following approach:
You need two
Start-Process
calls:The first one to launch an - of necessity - non-elevated process as the target user (
-Credential
)A second one launched from that process to request elevation, which then elevates in the context of the target user, assuming they're an administrator.
Because you can only capture output from inside the elevated process itself, you'll need to launch your target program via a shell and use its redirection (
>
) features to capture output in files.
Unfortunately, this makes for a nontrivial solution, with many subtleties to consider.
Here's a self-contained example:
It executes the commands
whoami
andnet session
(which only succeeds in an elevated session) and captures their combined stdout and stderr output in fileout.txt
in the specified working directory.It executes synchronously, i.e. it waits for the elevated target process to exit before continuing; if that isn't a requirement Remove
-PassThru
and the enclosing(...).WaitForExit()
, as well as-Wait
from the nestedStart-Process
call.- Note: The reason that
-Wait
cannot also be used in the outerStart-Process
call is a bug, still present as of PowerShell 7.2.2. - see GitHub issue #17033.
- Note: The reason that
As instructed in the source-code comments:
when you're prompted for the target user's credentials, be sure to specify an administrator's credentials, to ensure that elevation with that user's identity succeeds.
in
$workingDir
, specify a working directory that the target user is permitted to access, even from a non-elevated session. The target user's local profile is used by default - assuming it exists.
# Prompt for the target user's credentials.
# IMPORTANT: Must be an *administrator*
$cred = Get-Credential
# The working directory for both the intermediate non-elevated
# and the ultimate elevated process.
# IMPORTANT: This must be a directory that the target user is permitted to access,
# even when non-elevated.
# Here, the target user's local profile folder is used.
# Adjust as needed.
$workingDir = Join-Path (Split-Path $env:USERPROFILE) $cred.UserName
(Start-Process -PassThru -WorkingDirectory $workingDir -Credential $cred -WindowStyle Hidden powershell.exe @'
-noprofile -command Start-Process -Wait -Verb RunAs powershell \"
-noexit -command `"Set-Location -LiteralPath \`\"$($PWD.ProviderPath)\`\"; & { whoami; net session } 2>&1 > out.txt`"
\"
'@).WaitForExit()
[1] The output from a process' redirected standard streams is buffered, and the process is blocked from writing more data when a buffer fills up, in which case it has to wait for the reader of the stream to consume the buffer. Thus, if you try to synchronously read to the end of one stream, you may get stuck if the other stream's buffer fills up in the meantime, and therefore blocks the process from finishing writing to the first stream.