Question:
I’m calling a self-elevating powershell script from C# code. The Script resets DNS Settings.
The script works fine when called from unelevated powershell, but takes no effect when called from C# code with no exceptions thrown.
My Execution policy is temporarily set on unrestricted and I’m running Visual Studio as Admin.
Does anyone know what’s wrong?
The C#:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Program { static void Main(string[] args) { var pathToScript = @"C:\Temp\test.ps1"; Execute(pathToScript); Console.ReadKey(); } public static void Execute(string command) { using (var ps = PowerShell.Create()) { var results = ps.AddScript(command).Invoke(); foreach (var result in results) { Console.WriteLine(result.ToString()); } } } } |
The script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# Get the ID and security principal of the current user account $myWindowsID = [System.Security.Principal.WindowsIdentity]::GetCurrent(); $myWindowsPrincipal = New-Object System.Security.Principal.WindowsPrincipal($myWindowsID); # Get the security principal for the administrator role $adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator; # Check to see if we are currently running as an administrator if ($myWindowsPrincipal.IsInRole($adminRole)) { # We are running as an administrator, so change the title and background colour to indicate this $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)"; $Host.UI.RawUI.BackgroundColor = "DarkBlue"; Clear-Host; } else { # We are not running as an administrator, so relaunch as administrator # Create a new process object that starts PowerShell $newProcess = New-Object System.Diagnostics.ProcessStartInfo "PowerShell"; # Specify the current script path and name as a parameter with added scope and support for scripts with spaces in it's path $newProcess.Arguments = "& '" + $script:MyInvocation.MyCommand.Path + "'" # Indicate that the process should be elevated $newProcess.Verb = "runas"; # Start the new process [System.Diagnostics.Process]::Start($newProcess); # Exit from the current, unelevated, process Exit; } # Run your code that needs to be elevated here... Set-DnsClientServerAddress -InterfaceIndex 9 -ResetServerAddresses |
Answer:
As you’ve just determined yourself, the primary problem was that script execution was disabled on your system, necessitating (at least) a process-level change of PowerShell’s execution policy, as the following C# code demonstrates, which calls
Set-ExecutionPolicy
-Scope Process -ExecutionPolicy Bypass
before invoking the script file (*.ps1
):
- For an alternative approach that uses the initial session state to set the per-process execution policy, see this answer.
- The approach below can in principle be used to persistently change the execution policy for the current user, namely by replacing
.AddParameter("Scope", "Process")
with.AddParameter("Scope", "CurrentUser")
- Caveat: When using a PowerShell (Core) 7+ SDK, persistent changes to the local machine‘s policy (
.AddParameter("Scope", "LocalMachine")
) – which require running with elevation (as admin) – are seen by that SDK project only; see this answer for details.
- Caveat: When using a PowerShell (Core) 7+ SDK, persistent changes to the local machine‘s policy (
Caveat: If the current user’s / machine’s execution policy is controlled by a GPO (Group Policy Object), it can NOT be overridden programmatically – neither per process, nor persistently (except via GPO changes).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
class Program { static void Main(string[] args) { var pathToScript = @"C:\Temp\test.ps1"; Execute(pathToScript); Console.ReadKey(); } public static void Execute(string command) { using (var ps = PowerShell.Create()) { // Make sure that script execution is enabled at least for // the current process. // For extra safety, you could try to save and restore // the policy previously in effect after executing your script. ps.AddCommand("Set-ExecutionPolicy") .AddParameter("Scope", "Process") .AddParameter("ExecutionPolicy", "Bypass") .Invoke(); // Now invoke the script and print its success output. // Note: Use .AddCommand() (rather than .AddScript()) even // for script *files*. // .AddScript() is meant for *strings // containing PowerShell statements*. var results = ps.AddCommand(command).Invoke(); foreach (var result in results) { Console.WriteLine(result.ToString()); } // Also report non-terminating errors, if any. foreach (var error in ps.Streams.Error) { Console.Error.WriteLine("ERROR: " + error.ToString()); } } } } |
Note that the code also reports any non-terminating errors that the script may have reported, via stderr (the standard error output stream).
Without the Set-ExecutionPolicy
call, if the execution policy didn’t permit (unsigned) script execution, PowerShell would report a non-terminating error via its error stream (.Streams.Error
) rather than throw an exception.
If you had checked .Streams.Error
to begin with, you would have discovered the specific cause of your problem sooner.
Therefore:
- When using the PowerShell SDK, in addition to relying on / catching exceptions, you must examine
.Streams.Error
to determine if (at least formally less severe) errors occurred.
Potential issues with your PowerShell script:
- You’re not waiting for the elevated process to terminate before returning from your PowerShell script.
- You’re not capturing the elevated process’ output, which you’d have to via the
.RedirectStandardInput
and.RedirectStandardError
properties of theSystem.Diagnostics.ProcessStartInfo
instance, and then make your script output the results. - See this answer for how to do that.
The following, streamlined version of your code addresses the first point, and invokes the powershell.exe
CLI via -ExecutionPolicy Bypass
too.
- If you’re using the Windows PowerShell SDK, this shouldn’t be necessary (because the execution policy was already changed in the C# code), but it could be if you’re using the PowerShell [Core] SDK, given that the two PowerShell editions have separate execution-policy settings.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# Check to see if we are currently running as an administrator $isElevated = & { net session *>$null; $LASTEXITCODE -eq 0 } if ($isElevated) { # We are running as an administrator, so change the title and background color to indicate this $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)" $Host.UI.RawUI.BackgroundColor = "DarkBlue" Clear-Host } else { # We are not running as an administrator, so relaunch as administrator # Create a new process object that starts PowerShell $psi = New-Object System.Diagnostics.ProcessStartInfo 'powershell.exe' # Specify the current script path and name as a parameter with and support for scripts with spaces in its path $psi.Arguments = '-ExecutionPolicy Bypass -File "{0}"' -f $script:MyInvocation.MyCommand.Path # Indicate that the process should be elevated. $psi.Verb = 'RunAs' # !! For .Verb to be honored, .UseShellExecute must be $true # !! In .NET Framework, .UseShellExecute *defaults* to $true, # !! but no longer in .NET Core. $psi.UseShellExecute = $true # Start the new process, wait for it to terminate, then # exit from the current, unelevated process, passing the exit code through. exit $( try { ([System.Diagnostics.Process]::Start($psi).WaitForExit()) } catch { Throw } ) } # Run your code that needs to be elevated here... Set-DnsClientServerAddress -InterfaceIndex 9 -ResetServerAddresses |