I'm writing a program with C# , that can create Users on remote Computers.
Actually it's done and working.
But I have one little problem.
In C# I use PowerShell to run a Script which runs then an Pexec, which executes a Batch file on a remote Computer.
C# :
private void executeScripts()
{
string _dirPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
string _sPath = Path.GetDirectoryName(_dirPath) + #"\ExecuteScripts\FileToExecute.ps1";
string _scriptPath = "& '" + _sPath + "'";
using (PowerShellProcessInstance pspi = new PowerShellProcessInstance())
{
string psfn = pspi.Process.StartInfo.FileName;
psfn = psfn.ToLowerInvariant().Replace("\\syswow64\\", "\\sysnative\\");
pspi.Process.StartInfo.FileName = psfn;
using (Runspace r = RunspaceFactory.CreateOutOfProcessRunspace(null, pspi))
{
r.Open();
using (PowerShell ps = PowerShell.Create())
{
ps.Runspace = r;
ps.AddScript(_scriptPath);
ps.Invoke();
}
}
}
}
PS Script :
#
# First there are some Copy-Items to the remote Computer
#
# Execute Above copied Bat File on remote Computer
[string] $IPAddress = "\\" + $XmlFile.ComputerSettings.LastChild.ChildNodes[1].InnerText
$PsTools = "\PsTools"
$PsToolsPath = Join-Path -path $ScriptParent -childpath $PsTools
& $PsToolsPath\PsExec.exe $IPAddress /accepteula -i -s -u $Login -p $LoginPassword Powershell C:\Path\ToBatFile\Execute.bat > log.txt
Exit
I use this PExec 3 other times in my Program, creating a User, updating a User and removing a User, i just execute different files, scripts or batch files.
And it works perfectly.
But with the Script above, the PExec executes everything but doesn't exit. Neiter does it log something.
I tried it also with the -d switch, but that didn't work either. I also put an exit /b in the batch file but no luck.
When running the script manually from Powershell it works, it executes and it exits, but when running it from my Program it doesn't.
After some waiting my C# returns a timed-out Exception end exits.
Anyone seeing what I'm doing wrong ?
Powershell class itself has a method called Stop() which makes it pretty easy to stop this.
If you want to do it asynchronously here is an example of implementation:
using(cancellationToken.Register(() => powershell.Stop())
{
await Task.Run(() => powershell.Invoke(powershellCommand), cancellationToken);
}
Related
Plan
The plan is to disable and subsequently enable a device from inside a windows forms application. To test the first building block of my plan, I open cmd with admin privileges and the following works perfectly:
> devcon hwids =ports
> devcon hwids *VID_10C4*
> devcon disable *VID_10C4*
> devcon enable *VID_10C4*
I can see the device being disabled and enabled again in device manager.
I can also achieve all of this by putting the commands into a batch file and running it from cmd with admin privileges. The above tells me that my plan is essentially good.
Application
However, what I actually want to do is achieve the same thing from inside a windows forms application:
I've set the following in the app manifest:
requestedExecutionLevel level="requireAdministrator" uiAccess="false"
For the sake of baby steps, I have checked this, just to ensure that there are no stupid mistakes in paths and whatnot. And it works just fine. The log file shows me the expected output from the dir command.
// Build String
string strCmdText =
"'/c cd " + prodPath +
" && dir " +
" > logs\\logFileEnablePrt.txt \"'";
// Run command
var p = new System.Diagnostics.Process();
var psi = new ProcessStartInfo("CMD.exe", strCmdText);
psi.Verb = "runas"; // admin rights
p.StartInfo = psi;
p.Start();
p.WaitForExit();
However, this does not work. It always returns an empty log file and does not change the device as expected:
// Build String
string strCmdText =
"'/c cd " + prodPath +
" && devcon hwids =ports " +
" > logs\\logFileEnablePrt.txt \"'";
// Run command
var p = new System.Diagnostics.Process();
var psi = new ProcessStartInfo("CMD.exe", strCmdText);
psi.Verb = "runas"; // admin rights
p.StartInfo = psi;
p.Start();
p.WaitForExit();
Error from cmd window is :
'devcon' is not recognized as an internal or external command,
operable program or batch file.
What's going on?
The above has me stumped. I've proved the commands work. I've proved my C# code works. But when I join the 2 together, it doesn't work...
NB: My C# program is running on my D: drive, if that makes any difference...
Updates Based on Comments
#Compo
Using your code, it does exactly the same as with mine. I see an empty log file & no changes made to the device. I've altered the /c to /k so I can see what going on the cmd terminal and I see this:
I've even tried your code C:\\Windows\\System32\\devcon hwids =usb pointing directly at devcon. Also tried \devcon.exe for completeness. The inexplicable error is :
I can see the flipping devcon.exe file sitting right there in the folder! Is there any reason it would not recognise it?
Also, with the command as you wrote it, the log file name is actually named logFileEnablePrt.txt'. I agree that your command looks right, so don't ask me why this happens!
#Panagiotis Kanavos
using your code, I get the following error:
This is at the line p.Start();. I tried putting in devcon.exe, and even the whole path (I checked the folder was in my PATH, and it is). Can't get past this. I actually stumbled on that answer you shared and arrived at this brick wall already.
Here is the code works for me, I don't have ports devices so I change it to usb.
public static void Main()
{
string prodPath = #"c:\devcon\x64";
// Build String
string strCmdText =
"/c \"cd /d " + prodPath +
" && devcon hwids =usb " +
" > log.txt \"";
// Run command
var p = new Process();
var psi = new ProcessStartInfo("CMD.exe", strCmdText);
psi.Verb = "runas"; // admin rights
p.StartInfo = psi;
p.Start();
p.WaitForExit();
}
Worked through a few steps and think I may have an answer...
Just specifying devcon fails as expected...windows cant find the exe as the folder it is in is not in the %PATH% variable in windows..
IF I specify the full path however it works...
It wasnt clear from your original code but if your copy of devcon is sitting in either System32 or Syswow directories you may be hitting an emulation issue as well...see here....
EDIT1:: A way to prove this would be to do Direcory.GetFiles(directory containing devcon) and see if the results line up with what you expect
As for passing arguments through to devcon I'd try something like this as opposed to trying to concatenate one giant cmd line..
A similar example but with netstat:
EDIT 2::Another example but with devcon:
The target platform here for the build was x64
EDIT3::
With my application build set to x86:
After working through the answers and comments above, I seem to have something that reliably works, which obviously I'd like to share back for scrutiny and future use.
So, my function ended up looking like this:
private int enablePort(string action)
{
while (true)
{
// Command Arg
string devconPath = #"c:\Windows\SysNative";
string strCmdText =
"'/c \"cd /d \"" +
devconPath +
"\" && c:\\Windows\\SysNative\\devcon " + action + " *VID_10C4* " +
"> \"" + prodPath + "\\logs\\logFileEnablePrt.txt\"\"";
// Process
var p = new Process();
var psi = new ProcessStartInfo()
{
Arguments = strCmdText,
Verb = "runas",
FileName = "CMD.exe",
UseShellExecute = true
};
p.StartInfo = psi;
p.Start();
p.WaitForExit();
// Grab log output
string logPath = prodPath + "\\logs\\logFileEnablePrt.txt";
Console.WriteLine("logPath = " + logPath);
string tempFile = System.IO.File.ReadAllText(logPath);
System.Console.WriteLine("Contents of WriteText.txt = \n{0}", tempFile);
// Check if it worked
var success = false;
if (tempFile.Contains(action))
{
success = true;
return 0;
}
// Error -> Allow user to try again!
if (MessageBox.Show("Was unable to " + action + " Test Jig COM port. Unlug & Replug USB. Check COM port is enabled if not working.", "COM Port Problem", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
{
return -1;
}
}
}
And the calling code was:
this.enablePort("disable");
int milliseconds = 3000;
await Task.Delay(milliseconds);
this.enablePort("enable");
As you can see in the code above, I've logged everything to see what was going on... Stepping through with the debugger, I can now see after the disable:
USB\VID_10C4&PID_EA60\0001 : Disabled
1 device(s) disabled.
And then after the enable:
USB\VID_10C4&PID_EA60\0001 : Enabled
1 device(s) are enabled.
The one extra thing I need to stress is that during testing, I thought I could hook a serial peripheral onto the port and determine whether it could disable and enable successfully by checking the connection. THIS DOES NOT WORK. The above code only works when the port is idle. Perhaps someone who understands the underlying software could hazard an explanation of why this is.
I have a powershell script which spits out file information for a given file. The script is executed in a process from a windows service like so:
Process p = new Process();
ProcessStartInfo s = new ProcessStartInfo();
s.FileName = "powershell.exe";
s.Arguments = "./script.ps1";
s.RedirectStandardOutput = true;
s.RedirectStandardError = true;
s.UseShellExecute = false;
s.CreateNoWindow = true;
p.StartInfo = s;
p.EnableRaisingEvents = true;
/* ... defined output handlers ... */
p.Start();
p.BeginOutputReadLine();
p.BeginErrorReadLine();
The powershell script is as follows:
function ChangeDir($dir)
{
try
{
echo ("Attempting to change directory: {0}" -f ($dir))
Set-Location -Path $dir -ErrorAction Stop
}
catch
{
echo $error[0].Exception
}
}
function OutputFileInfo($filePath)
{
try
{
echo ("Attempting to read file: {0}" -f ($filePath))
$file = #(Get-ChildItem $filePath -ErrorAction Stop)
for ($i = 0; $i -lt $file.Count; $i++)
{
echo ("{0},{1}" -f ($file[$i].Name, $file[$i].Length))
}
}
catch
{
echo $error[0].Exception
}
}
ChangeDir "/Windows/System32/drivers"
OutputFileInfo "tcpip.sys"
The output when running the powershell script from the command line is as I expect it to be:
Attempting to change directory: /Windows/System32/drivers
Attempting to read file: tcpip.sys
tcpip.sys,2773400
When the script executes via the windows service the output is this:
Attempting to change directory: /Windows/System32/drivers
Attempting to read file: tcpip.sys
Cannot find path 'C:\Windows\System32\drivers\tcpip.sys' because it does not exist.
For other files it works perfectly fine from the command line and the service. Could it have something to do with the service running the powershell script as SYSTEM which somehow doesn't have access to that file? Although if that were the case I would expect a permissions error instead of a file not found error.
Ok.... this was because system32/drivers is not accessible for 32bit applications that run on 64bit machines and I didn't realize I had my application set to the default in visual studio which I guess is 32bit. I changed it to 64bit and it works from the service now.
We have a C# application that executes PowerShell scripts as an extension point, we don't have access to change this code but it is pretty much the following:
string command = #"C:\temp.ps1";
var fileName = "powershell";
var args = $"-ExecutionPolicy unrestricted . '{command}'";
var process = CreateProcess(fileName, args);
ExecuteProcess(ref process);
private Process CreateProcess(string fileName, string args)
{
return new Process
{
StartInfo =
{
FileName = fileName,
Arguments = args,
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false
}
};
}
private int ExecuteProcess(ref Process proc)
{
proc.Start();
string text = string.Empty;
string text2 = string.Empty;
while (!proc.HasExited)
{
Thread.Sleep(1000);
text += proc.StandardOutput.ReadToEnd();
text2 += proc.StandardError.ReadToEnd();
}
proc.WaitForExit();
text2 += proc.StandardError.ReadToEnd();
text += proc.StandardOutput.ReadToEnd();
Console.WriteLine(text);
Console.WriteLine(text2);
return proc.ExitCode;
}
We have a PowerShell script that gets executed from this code that has a long-running process, to keep this simple we'll just use ping.exe with a -t argument.
ping -t google.com
We want to be able to fork the ping.exe process so that the c# application can resume execution as soon as possible and that the ping.exe in this example continues on it's merry way.
I've tried to run Start-Process inside the powershell script but this still just blocks the execution of the C# application until all the processes have fully executed (so it eventually runs to completion):
Start-Process ping -ArgumentList "-t","google.com" -RedirectStandardOutput ".\out.log" -RedirectStandardError ".\err.log"
I've also tried to run Start-Job and wrap the start process in a separate job, however, this seems to start the job but never completes
Start-Job -ScriptBlock { Start-Process ping -ArgumentList "-t","google.com" -RedirectStandardOutput ".\out.log" -RedirectStandardError ".\err.log" }
Is there any way to start a new process from within PowerShell and allow the C# application to continue executing?
I've kinda found a workaround - if I pass in -Verb Open to Start-Process it seems to resume execution to the C# application straight away. The only problem is that you can't redirect the standard out or error to files.
Start-Process ping -ArgumentList "-t","google.com" -Verb Open
I am trying to run powershell script in c# . program runs successfully but does not show any output.
try
{
string fileName = "D:\\Script\\script.psm1";
RunspaceConfiguration config = RunspaceConfiguration.Create();
Runspace myRs = RunspaceFactory.CreateRunspace(config);
myRs.Open();
RunspaceInvoke scriptInvoker = new RunspaceInvoke(myRs);
scriptInvoker.Invoke("Set-ExecutionPolicy Unrestricted");
/*using (new Impersonator("myUsername", "myDomainname", "myPassword"))
{
using (RunspaceInvoke invoker = new RunspaceInvoke())
{
invoker.Invoke("Set-ExecutionPolicy Unrestricted");
}
} */
Pipeline pipeline = myRs.CreatePipeline();
pipeline.Commands.AddScript(fileName);
//...
pipeline.Invoke();
var error = pipeline.Error.ReadToEnd();
myRs.Close();
string errors = "";
if (error.Count >= 1)
{
foreach (var Error in error)
{
errors = errors + " " + Error.ToString();
}
}
return errors;
}
Your program is only checking for error output. You typically get the "standard" output as the return value of the Invoke method e.g.
Collection<PSObject> results = pipeline.Invoke();
string output = "";
foreach (var result in results)
{
output += result.ToString();
}
You aren't doing yourself any favors with that big try {} block wrapped around everything, as you can't see the exceptions that are happening.
You will need to run Visual Studio as a local administrator in order for "Set-ExecutionPolicy Unrestricted" to work, and the final executable will also have that requirement since issuing that command requires access to a protected registry key.
The pipeline.Invoke() method returns a type of Collection<PSObject>.
Collection<PSObject> results = pipeLine.Invoke();
If your intent is to ignore the output of the pipeline and only look at errors, that is fine; but if there are no errors in the script, it would be normal not to see anything.
With the .psm1 file extension on the script, you will probably get null results. The proper extension should be .ps1. The .psm1 extension is for modules that are stored in special locations on the file system and which are loaded automatically (in PowerShell 3.0+).
By default, 'Stop' type errors in PowerShell will generate an Exception in the C# program, so wrapping with try/catch is one way to see them.
Collection<PSObject> results = null;
try
{
results = pipeline.Invoke();
// results returned from PowerShell can be accessed here but may not
// necessarily be valid since a 'Continue' error could have occurred
// which would not generate an exception
}
catch (RuntimeException e)
{
Debug.WriteLine("Error: " + e.Message);
}
You can test this by adding for example the following to your script.ps1:
throw "This is an error"
Working Example:
Note:
1. You will need to add a reference to System.Management.Automation.dll in order to run this code sample. If you are using Visual Studio, you can select Add Reference then the Browse... button and in the search box of the Browse dialog enter the name of the assembly and it will likely show up in the search results. If not you may need to download the .NET portion of the Windows SDK.
2. PowerShell scripts are disabled by default in Windows, and this is code that runs PowerShell scripts. There is plenty of information on the 'Net, but the standard way to enable scripts is to open a PowerShell command prompt as a local administrator and run Set-ExecutionPolicy RemoteSigned (if needed, Unrestricted can be used instead of RemoteSigned).
3. In some environments, you will need to unblock scripts downloaded from the Internet by right-clicking on the file in Windows Explorer, going to Properties, and clicking Unblock. If there is no Unblock button then the file is OK.
4. The first thing to try if you get access errors is to run Visual Studio and/or the executable as a local administrator. Please do not attempt to impersonate an administrator and embed a password in the executable. If you are in a corporate setting, group policy can be configured to allow PowerShell scripts to run. If you are at home, you should be a local administrator.
using System.Management.Automation;
using System.Collections.ObjectModel;
using System.Management.Automation.Runspaces;
using System.Diagnostics;
namespace PowerShell
{
class Program
{
static void Main(string[] args)
{
// Create and Open a Runspace
string fileName = #"D:\script.ps1";
RunspaceConfiguration config = RunspaceConfiguration.Create();
Runspace myRs = RunspaceFactory.CreateRunspace(config);
myRs.Open();
// Attempt to configure PowerShell so we can forcefully run a script.
RunspaceInvoke scriptInvoker = new RunspaceInvoke(myRs);
scriptInvoker.Invoke("Set-ExecutionPolicy Unrestricted -Scope Process -Force");
Pipeline pipeline = myRs.CreatePipeline();
pipeline.Commands.AddScript(fileName);
Collection<PSObject> results = null;
try
{
results = pipeline.Invoke();
// Read standard output from the PowerShell script here...
foreach (var item in results)
{
Debug.WriteLine("Normal Output: " + item.ToString());
}
}
catch (System.Management.Automation.RuntimeException e)
{
Debug.WriteLine("PowerShell Script 'Stop' Error: " + e.Message);
}
myRs.Close();
}
}
}
Trying to do the following in powershell in C#
$certThumbrint = "someLocationToACert"
$cert = get-item $certThumbrint
Get-RoleInstanceCount -ServiceName "someServiceName" -DeploymentSlot "someSlot" -RoleName "someRole" -SubscriptionId "someId" -Certificate $cert
This works perfectly when running them one by one in the powershell comandline. But I cannot figure out how to do this by code. So far Ive done this.
Pipeline pipeline = runspace.CreatePipeline();
pipeline.Commands.Add("$certThumbrint = \"someLocationToACert\"");
pipeline.Commands.Add(#"$cert = get-item $certThumbrint");
Command instanceCount = new Command("Get-RoleInstanceCount");
instanceCount.Parameters.Add(new CommandParameter("ServiceName", "someServiceName"));
....
instanceCount.Parameters.Add(new CommandParameter("Certificate", "$cert"));
I then get the following exception:
"The term '$certThumbrint = "someLocation"' is not recognized as the
name of a cmdlet, function, script file, or operable program. Check
the spelling of the name, or if a path was included, verify that the
path is correct and try again.
I've tried to add the varibles as "AddScrips" and I also used
SessionStateVariableEntry var2 = new SessionStateVariableEntry("cert", "get-item $certThumbrint", "Initial session state MyVar1 test");
initialSessionState.Variables.Add(var2);
Before creating the runspace. Nothing is working. Also added all the code into a string and tried to run it as a script.
I actually have no way of doing this and it feels like it's a really simple thing that must be able to do... thanks.
Edit: also tried the following:
const string getInstanceCountScript = "$certThumbrint = \"somecert\" \n " +
"$cert = get-item $certThumbrint \n " +
"Get-RoleInstanceCount -ServiceName someservicename" +
...
" -Certificate $cert";
pipeline.Commands.AddScript(getInstanceCountScript);
It runs but returns an empty string. If I put the same code into a ps1 file that I call with "Add()" it runs and gives me the right output. But I really dont want to have a load of ps1 files in my project just for 3 lines of code or less.
This code perfectly works for me. Are you shure, that PS1 file contains exact same code?
static void Main(string[] args)
{
using (Runspace rs = RunspaceFactory.CreateRunspace())
{
rs.Open();
var pipeline = rs.CreatePipeline();
pipeline.Commands.AddScript("$certThumbrint = \"c:\\1.txt\"\n" +
"$cert = get-item $certThumbrint\n" +
"Get-Content $cert");
foreach (var s in pipeline.Invoke())
{
Console.WriteLine(s);
}
}
Console.ReadLine();
}
Take a look at the New-Variable commandlet.