Inside a powershell script, I'm running a command which starts a new powershell as admin (if I'm not and if needed, depending on $arg) and then runs the script.
I'm trying to redirect stdout and stderr to the first terminal.
Not trying to make things easier, there are arguments too.
param([string]$arg="help")
if($arg -eq "start" -Or $arg -eq "stop")
{
if(![bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544"))
{
Start-Process powershell -Verb runas -ArgumentList " -file servicemssql.ps1 $arg"
exit
}
}
$Services = "MSSQLSERVER", "SQLSERVERAGENT", "MSSQLServerOLAPService", "SSASTELEMETRY", "SQLBrowser", `
"SQLTELEMETRY", "MSSQLLaunchpad", "SQLWriter", "MSSQLFDLauncher"
function startsql {
"starting SQL services"
Foreach ($s in $Services) {
"starting $s"
Start-Service -Name "$s"
}
}
function stopsql {
"stopping SQL services"
Foreach ($s in $Services) {
"stopping $s"
Stop-Service -Force -Name "$s"
}
}
function statussql {
"getting SQL services status"
Foreach ($s in $Services) {
Get-Service -Name "$s"
}
}
function help {
"usage: StartMssql [status|start|stop]"
}
Switch ($arg) {
"start" { startsql }
"stop" { stopsql }
"status" { statussql }
"help" { help }
"h" { help }
}
Using the following answers on SO doesn't work:
Capturing standard out and error with Start-Process
Powershell: Capturing standard out and error with Process object
How to deal with the double quote inside double quote while preserving the variable ($arg) expansion ?
PowerShell's Start-Process cmdlet:
does have -RedirectStandardOut and -RedirectStandardError parameters,
but syntactically they cannot be combined with -Verb Runas, the argument required to start a process elevated (with administrative privileges).
This constraint is also reflected in the underlying .NET API, where setting the .UseShellExecute property on a System.Diagnostics.ProcessStartInfo instance to true - the prerequisite for being able to use .Verb = "RunAs" in order to run elevated - means that you cannot use the .RedirectStandardOutput and .RedirectStandardError properties.
Overall, this suggests that you cannot directly capture an elevated process' output streams from a non-elevated process.
A pure PowerShell workaround is not trivial:
param([string] $arg='help')
if ($arg -in 'start', 'stop') {
if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {
# Invoke the script via -Command rather than -File, so that
# a redirection can be specified.
$passThruArgs = '-command', '&', 'servicemssql.ps1', $arg, '*>', "`"$PSScriptRoot\out.txt`""
Start-Process powershell -Wait -Verb RunAs -ArgumentList $passThruArgs
# Retrieve the captured output streams here:
Get-Content "$PSScriptRoot\out.txt"
exit
}
}
# ...
Instead of -File, -Command is used to invoke the script, because that allows appending a redirection to the command: *> redirects all output streams.
#soleil suggests using Tee-Object as an alternative so that the output produced by the elevated process is not only captured, but also printed to the (invariably new window's) console as it is being produced:
..., $arg, '|', 'Tee-Object', '-FilePath', "`"$PSScriptRoot\out.txt`""
Caveat: While it doesn't make a difference in this simple case, it's important to know that arguments are parsed differently between -File and -Command modes; in a nutshell, with -File, the arguments following the script name are treated as literals, whereas the arguments following -Command form a command that is evaluated according to normal PowerShell rules in the target session, which has implications for escaping, for instance; notably, values with embedded spaces must be surrounded with quotes as part of the value.
The $PSScriptRoot\ path component in output-capture file $PSScriptRoot\out.txt ensures that the file is created in the same folder as the calling script (elevated processes default to $env:SystemRoot\System32 as the working dir.)
Similarly, this means that script file servicemssql.ps1, if it is invoked without a path component, must be in one of the directories listed in $env:PATH in order for the elevated PowerShell instance to find it; otherwise, a full path is also required, such as $PSScriptRoot\servicemssql.ps1.
-Wait ensures that control doesn't return until the elevated process has exited, at which point file $PSScriptRoot\out.txt can be examined.
As for the follow-up question:
To go even further, could we have a way to have the admin shell running non visible, and read the file as we go with the Unix equivalent of tail -f from the non -privileged shell ?
It is possible to run the elevated process itself invisibly, but note that you'll still get the UAC confirmation prompt. (If you were to turn UAC off (not recommended), you could use Start-Process -NoNewWindow to run the process in the same window.)
To also monitor output as it is being produced, tail -f-style, a PowerShell-only solution is both nontrivial and not the most efficient; to wit:
param([string]$arg='help')
if ($arg -in 'start', 'stop') {
if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {
# Delete any old capture file.
$captureFile = "$PSScriptRoot\out.txt"
Remove-Item -ErrorAction Ignore $captureFile
# Start the elevated process *hidden and asynchronously*, passing
# a [System.Diagnostics.Process] instance representing the new process out, which can be used
# to monitor the process
$passThruArgs = '-noprofile', '-command', '&', "servicemssql.ps1", $arg, '*>', $captureFile
$ps = Start-Process powershell -WindowStyle Hidden -PassThru -Verb RunAs -ArgumentList $passThruArgs
# Wait for the capture file to appear, so we can start
# "tailing" it.
While (-not $ps.HasExited -and -not (Test-Path -LiteralPath $captureFile)) {
Start-Sleep -Milliseconds 100
}
# Start an aux. background that removes the capture file when the elevated
# process exits. This will make Get-Content -Wait below stop waiting.
$jb = Start-Job {
# Wait for the process to exit.
# Note: $using:ps cannot be used directly, because, due to
# serialization/deserialization, it is not a live object.
$ps = (Get-Process -Id $using:ps.Id)
while (-not $ps.HasExited) { Start-Sleep -Milliseconds 100 }
# Get-Content -Wait only checks once every second, so we must make
# sure that it has seen the latest content before we delete the file.
Start-Sleep -Milliseconds 1100
# Delete the file, which will make Get-Content -Wait exit (with an error).
Remove-Item -LiteralPath $using:captureFile
}
# Output the content of $captureFile and wait for new content to appear
# (-Wait), similar to tail -f.
# `-OutVariable capturedLines` collects all output in
# variable $capturedLines for later inspection.
Get-Content -ErrorAction SilentlyContinue -Wait -OutVariable capturedLines -LiteralPath $captureFile
Remove-Job -Force $jb # Remove the aux. job
Write-Verbose -Verbose "$($capturedLines.Count) line(s) captured."
exit
}
}
# ...
I appreciate you taking the time to read this.
My issue is as follows: I'm trying to create a program that uses powershell to do the following:
Take a table generated outside of powershell
Loop calls to a powershell script with the parameters from the table
The powershell script calls a special type of .cmd file and then runs commands on it that are located in a different shared location.
Now my problem is with the 3rd point.
I'm currently using the following to call my script (and the arguements are just hard coded to get it working, they'll be generated by the calls from step 2 later on):
powershell.exe -ExecutionPolicy Bypass -Command {invoke-command -file \\sharedlocation\test5.ps1 -computername server1121 -argumentlist 7058,Jason}
The inside of test5.ps1 is currently:
param(
[Parameter(Mandatory=$true)]
[string] $Var1,
[Parameter(Mandatory=$true)]
[string] $Var2
)
$CommandsPath = "\\sharedlocation\testcommands.cmd"
$path = "C:\"+$Var1+"\TOOLS\"+$Var2+"launchtool.cmd"
$scriptPath = [scriptblock]::Create($path)
$out | invoke-command {PARAM($MyArg) $scriptPath } -ArgumentList $CommandsPath
I've also tried using
$CommandsPath = "\\sharedlocation\testcommands.cmd"
$path = "C:\"+$Var1+"\TOOLS\"+$Var2+"\launchtool.cmd & " + $CommandsPath
$scriptPath = [scriptblock]::Create($path)
$out | invoke-command {$scriptPath }
I've also tried to call with hardcoded testcommands instead of them being in a file.
Now my problem is in both cases, it DOES run launchtool.cmd, but it doesn't pass the testcommands.cmd file.
However when on the machine i run
C:\7058\TOOLS\Jason\launchtool.cmd & \\sharedlocation\testcommands.cmd
It works fine.
Any ideas what I'm doing wrong?
Try, invoke-expression "cmd.exe /c C:\7058\TOOLS\Jason\launchtool.cmd & \sharedlocation\testcommands.cmd"
cmd.exe /c is my best way to ensure consistency between cmd and powershell
Is the UNC Path accessible from powershell? Copy the testcommands.cmd to a local path and try if it works!
$CommandsPath = "\\sharedlocation\testcommands.cmd"
if(Test-Path $CommandsPath)
{
$path = "C:\"+$Var1+"\TOOLS\"+$Var2+"\launchtool.cmd & " + $CommandsPath
$scriptPath = [scriptblock]::Create($path)
$out | invoke-command {$scriptPath }
}
I'm trying to start a job that creates a file. But it doesn't.
here's the very simple code:
start-job -ScriptBlock { "hi" | set-content "hi.txt" }
The file "hi.txt" never gets created.
Why not?
It is most likely creating the file, just not where you expect because you used a relative path. Try executing this to see where the file is going:
Start-Job {$pwd} | Receive-Job -Wait
Better yet, use an absolute path:
Start-Job { 'hi' > c:\hi.txt }
Or pass in the desired path:
Start-Job {param($path) 'hi' > "$path\hi.txt"} -arg $pwd
I'm trying to run a .bat file calling a shell .ps1 file.
I've tested my script directly in powershell and there it works.
But when I run the .bat, a error occurs saying to me something like [ The string started with: (...) does not contain the terminator " ]
My .bat file:
powershell.exe -command "& C:\Users\I\Desktop\teste.ps1"
My .ps1 file:
$scripts = 'C:\Users\I\Desktop\Teste_0.1\Teste\Teste_run.bat', 'C:\Users\I\Desktop\Teste_0.2\Teste\Teste_run.bat','C:\Users\I\Desktop\Teste_0.3\Teste\Teste_run.bat' |%{ Start-Job –scriptblock (iex "[Scriptblock] { $_ } ")}| wait-job
To call another script from powershell, I don't think you need to use -command. You should just be able to call the script directly.
powershell -nol -noe C:\Users\I\Desktop\teste.ps1
Alternately, here is another clever way to do it.
#echo off
more +4 "%~dpnx0" >> temp.ps1 && powershell -nol -noe .\temp.ps1
exit /b
$scripts = 'C:\Users\I\Desktop\Teste_0.1\Teste\Teste_run.bat', 'C:\Users\I\Desktop\Teste_0.2\Teste\Teste_run.bat','C:\Users\I\Desktop\Teste_0.3\Teste\Teste_run.bat' |%{ Start-Job –scriptblock (iex "[Scriptblock] { $_ } ")}| wait-job
I have a file template.txt which contains the following:
Hello ${something}
I would like to create a PowerShell script that reads the file and expands the variables in the template, i.e.
$something = "World"
$template = Get-Content template.txt
# replace $something in template file with current value
# of variable in script -> get Hello World
How could I do this?
Another option is to use ExpandString() e.g.:
$expanded = $ExecutionContext.InvokeCommand.ExpandString($template)
Invoke-Expression will also work. However be careful. Both of these options are capable of executing arbitrary code e.g.:
# Contents of file template.txt
"EvilString";$(remove-item -whatif c:\ -r -force -confirm:$false -ea 0)
$template = gc template.txt
iex $template # could result in a bad day
If you want to have a "safe" string eval without the potential to accidentally run code then you can combine PowerShell jobs and restricted runspaces to do just that e.g.:
PS> $InitSB = {$ExecutionContext.SessionState.Applications.Clear(); $ExecutionContext.SessionState.Scripts.Clear(); Get-Command | %{$_.Visibility = 'Private'}}
PS> $SafeStringEvalSB = {param($str) $str}
PS> $job = Start-Job -Init $InitSB -ScriptBlock $SafeStringEvalSB -ArgumentList '$foo (Notepad.exe) bar'
PS> Wait-Job $job > $null
PS> Receive-Job $job
$foo (Notepad.exe) bar
Now if you attempt to use an expression in the string that uses a cmdlet, this will not execute the command:
PS> $job = Start-Job -Init $InitSB -ScriptBlock $SafeStringEvalSB -ArgumentList '$foo $(Start-Process Notepad.exe) bar'
PS> Wait-Job $job > $null
PS> Receive-Job $job
$foo $(Start-Process Notepad.exe) bar
If you would like to see a failure if a command is attempted, then use $ExecutionContext.InvokeCommand.ExpandString to expand the $str parameter.
I've found this solution:
$something = "World"
$template = Get-Content template.txt
$expanded = Invoke-Expression "`"$template`""
$expanded
Since I really don't like the idea of One More Thing To Remember - in this case, remembering that PS will evaluate variables and run any commands included in the template - I found another way to do this.
Instead of variables in template file, make up your own tokens - if you're not processing HTML, you can use e.g. <variable>, like so:
Hello <something>
Basically use any token that will be unique.
Then in your PS script, use:
$something = "World"
$template = Get-Content template.txt -Raw
# replace <something> in template file with current value
# of variable in script -> get Hello World
$template=$template.Replace("<something>",$something)
It's more cumbersome than straight-up InvokeCommand, but it's clearer than setting up limited execution environment just to avoid a security risk when processing simple template. YMMV depending on requirements :-)