C# Process "Persistence" - c#

I've been working on a System.Diagnostics.Process project (interractive powershell through network).
I want to be able to send a command from one host to another, so that the second runs it and sends the stdout + stderr to the first host.
The problem is, for example if i want to connect to a FTP server, list all files and get something for example, this should be done in one liner, because every time i send a command to another host, the Process start a new and no ftp connection "in this session of powershell" is present. Therefore "no persistence".
I'd like to run a powershell process on a user's instance once, redirect stdin, stdout and stderr to a variable, and from those be able to recieve command strings and send the output from that to the "main host".
P.S. All solutions i've found were just to run for every command a new instance of a Process and lose the "persistence" of that.

Unless your goal is to re-invent PowerShell Remoting, I'd strongly suggest just using a remote runspace instead, see this (oversimplified) example:
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Linq;
public class PersistentCommandRunner
{
private Runspace remoteRunspace;
public PersistentCommandRunner(string remoteMachine)
{
# Connect to remote machine using a runspace, save the runspace reference
var connectionInfo = new WSManConnectionInfo(new Uri("http://remoteMachine:5985/WSMan"));
remoteRunspace = RunspaceFactory.CreateRunspace(connectionInfo);
}
public IEnumerable<PSObject> RunScript(string script)
{
# Make sure runspace is ready to execute remote commands
EnsureConnection();
using(var shell = PowerShell.Create())
{
# Make sure our script runs in the remote runspace
shell.Runspace = remoteRunspace;
shell.AddScript(script);
foreach(var result in shell.Invoke())
{
yield return result;
}
}
}
private void EnsureConnection()
{
if(remoteRunspace.RunspaceStateInfo.State != RunspaceState.Open)
{
if(remoteRunspace.RunspaceStateInfo.State == RunspaceState.BeforeOpen)
{
remoteRunspace.Open();
return;
}
throw new Exception("Runspace is in an unusable state...");
}
}
}
Re-using the same runspace for subsequent scripts allows you to maintain state on the remote endpoint, ie.:
var serverRunner = new PersistentCommandRunner();
# Stage variable in the remote session
serverRunner.InvokeScript(#"$remoteVariable = 123");
# Retrieve it again, should print "123"
Console.WriteLine(serverRunner.InvokeScript("$remoteVariable").First()?.Base as int)

Related

Execute Powershell cmdlet that have user prompts within C# WPF application

I am attempting to create a WPF application that will execute some powershell commands using a 3rd party module (ShareGate). After extensive research and banging my head on the keyboard, I have gotten the application to at least execute the cmdlets I have asked for. The cmdlet in question, if run in powershell, will prompt the user to log into a web service using edge I believe. When running the cmdlet from the application, it throws an error which is misleading "during the last update edge was not able to be installed...."
I think that this error is coming up because this implementation isn't allowing powershell to pop open the browser like it does within a powershell window.
My question is this: "How can I redirect the user prompt to come up within the wpf application? or can I?"
here is my method:
public Task StartSGMigrations(IProgress<string> progress)
{
var sharegatePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Apps\\ShareGate\\Sharegate.Automation.dll";
if (client != null)
{
try
{
InitialSessionState iss = InitialSessionState.CreateDefault();
iss.ImportPSModule(new string[] { sharegatePath });
using (Runspace myRunSpace = RunspaceFactory.CreateRunspace(iss))
{
myRunSpace.Open();
using (PowerShell powershell = PowerShell.Create())
{
Dictionary<string, string> parameters = new Dictionary<string, string>();
powershell.AddScript("New-CopySettings -OnContentItemExists IncrementalUpdate");
powershell.AddScript("Connect-box -email " + _admin + " -admin");
powershell.AddScript("Connect-Site -Url \"https://xxxx-admin.sharepoint.com\" -Browser");
powershell.Runspace = myRunSpace;
var results = powershell.Invoke();
var errors = myRunSpace.SessionStateProxy.PSVariable.GetValue("Error");
foreach (var result in results)
{
progress.Report(result.ToString() + Environment.NewLine);
}
}
myRunSpace.Close();
}
}
catch (Exception ex)
{
progress.Report(ex.Message);
}
}
else
{
progress.Report("not connected to Box");
}
return Task.CompletedTask;
}
}
This will be fairly tricky to do if the commands you are calling do not support noninteractive execution.
What's going on:
Your application using the PowerShell api to call your scripts. This part's good. The problem is your scripts are using functionality of the PowerShell host (possibly prompting for credentials). Because the runspace is not associated with a host, any interactive capabilities will simply fail.
So you'd need to attach a host in order for it to work as expected (and that host would need to work the same as PowerShell.exe/pwsh.exe for whatever purposes your underlying cmdlets need).
If there were lots of implementations of a PowerShell host in the wild, I'd link you to them. Unfortunately, there are not. So unless you want to go down a deep rabbit hole, I'd suggest these alternatives:
If the cmdlet supports providing credentials directly, try this
If it does not, see if it "persists" credentials for a given user. (That is, open up a shell, login, close the shell, open another shell, and see if you can use the module without providing credentials).
If credentials do persist (and you can't do option 1), you should be able to call PowerShell.exe/pwsh.exe once to log in, and then load code normally.
If the credentials do not persist, you're stuck in a much more unfortunate situation, leaving you with paths 3,4, or 5:
Call powershell.exe/pwsh.exe (hopefully in in a minimized window) and send the output back via JSON or CLIXML.
Go down the rabbit hole and build yourself a host.
Beg the cmdlet authors to better support noninteractive scenarios.
Between those options, I'd start with the last one.
Best of luck.

How to pass arguments to PowerShell via C#

I having issues with passing arguments to PowerShell via C#
I am getting the following exception:
"A command that prompts the user failed because the host program or
the command type does not support user interaction. Try a host program
that supports user interaction, such as the Windows PowerShell Console
or Windows PowerShell ISE, and remove prompt-related commands from
command types that do not support user interaction, such as Windows
PowerShell workflows"
cs:
private static void RunPowershellScript(string scriptFile, string scriptParameters)
{
string scriptParameters = "param1 param2";
RunspaceConfiguration runspaceConfiguration = RunspaceConfiguration.Create();
Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration);
runspace.Open();
RunspaceInvoke scriptInvoker = new RunspaceInvoke(runspace);
Pipeline pipeline = runspace.CreatePipeline();
Command scriptCommand = new Command(scriptFile);
Collection<CommandParameter> commandParameters = new Collection<CommandParameter>();
foreach (string scriptParameter in scriptParameters.Split(' '))
{
CommandParameter commandParm = new CommandParameter(null, scriptParameter);
commandParameters.Add(commandParm);
scriptCommand.Parameters.Add(commandParm);
}
pipeline.Commands.Add(scriptCommand);
Collection<PSObject> psObjects;
psObjects = pipeline.Invoke();
}
ps:
Function Foo ($arg1, $arg2)
{
Write-Host $arg1
Write-Host $arg2
}
Foo $args[0] $args[1]
What am i missing here? how can i make this work?
The exception is not about arguments. Either do not use commands that require host UI implemented (Write-Host included) or implement you own custom host (PSHost) and this UI (PSHostUserInterface). Here is the example of a simple host (and there is much more on MSDN about this, if you choose this way):
http://msdn.microsoft.com/en-us/library/windows/desktop/ee706559(v=vs.85).aspx
For simple tasks implementing a host with UI is too much, perhaps. You may consider simply to define a function Write-Host with the same arguments and implement it so that it works in your specific case (e.g. does [Console]::WriteLine(...)). This function should be defined in the script or better made available for it in a different way, e.g. invoke another script with it defined in the global scope.
P.S. And if you have your custom host then use one of the CreateRunspace() overloads that takes a host instance as an argument in order to link the host and the runspace.

Load ActiveDirectory PowerShell Module in Runspace without AD Drive Letter

I'm trying to load the ActiveDirectory module inside a custom SnapIn that I'm working on. However, when I do so I get the annoying error
"Error initializing default drive: 'Unable to find a default server
with Active Directory Web Services running.'"
which takes a good 15 seconds or so to timeout. From within a normal PowerShell console I realize you can set a variable to disable the AD: drive mapping but, I cannot seem to get that working from within C# code.
InitialSessionState initial = InitialSessionState.CreateDefault();
initial.Variables.Add(new SessionStateVariableEntry("ADPS_LoadDefaultDrive",
0,
string.Empty));
initial.ImportPSModule(new string[] { "ActiveDirectory" });
using (Runspace runspace = RunspaceFactory.CreateRunspace(initial))
{
runspace.Open();
using (Pipeline p = runspace.CreatePipeline())
{
Command getGroup = new Command("Get-ADGroup");
getGroup.Parameters.Add("Filter", this.Group);
p.Commands.Add(getGroup);
var results = p.Invoke();
this.WriteObject(results, true);
}
}
I've included what I think should work but, the ADPS_LoadDefaultDrive setting seems to be ignored as each time I try to make a call into the ActiveDirectory module I get the same web services error (along with a painful timeout)
Try to set ADPS_LoadDefaultDrive as an Environment variable, not a regular session variable.

Run a Remote Powershell Script on a Different Remote Server in a C# Runspace

I need to be able to execute a PS1 script that resides on a remote machine against another remote machine through a C# runspace.
To be clear what I mean by this: The service I'm creating resides on server A. It creates a remote runspace to server B using the method below. Through the runspace I'm trying to call a script residing on server C against server B. If it helps, currently server A IS server C, but it's not guaranteed that will always be the case.
Here's the method I'm using to make the remote call:
internal Collection<PSObject> RunRemoteScript(string remoteScript, string remoteServer, string scriptName, out bool scriptSuccessful)
{
bool isLocal = (remoteServer == "localhost" || remoteServer == "127.0.0.1" || remoteServer == Environment.MachineName);
WSManConnectionInfo connectionInfo = null;
if (!isLocal)
{
connectionInfo = new WSManConnectionInfo(new Uri("http://" + remoteServer + ":5985"));
}
PsHostImplementation myHost = new PsHostImplementation(scriptName);
using (Runspace remoteRunspace = (isLocal ? RunspaceFactory.CreateRunspace(myHost) : RunspaceFactory.CreateRunspace(myHost, connectionInfo)))
{
remoteRunspace.Open();
using (PowerShell powershell = PowerShell.Create())
{
powershell.Runspace = remoteRunspace;
Pipeline pipeline = remoteRunspace.CreatePipeline();
pipeline.Commands.AddScript(remoteScript);
Collection<PSObject> results = pipeline.Invoke();
remoteRunspace.Close();
scriptSuccessful = myHost.ScriptSuccessful;
return results;
}
}
}
"remoteScript" is set to the Powershell script I want to run. For example:
"& \"\\\\remoteserveraddress\\PathToScript\\Install.ps1\" -Parameter;Import-Module Modulename;CustomCommand-FromModule -parameter(s) -ErrorAction stop"
If I'm on the remote machine that I want to run the script on, in the powershell console I can just give the following command:
& "\\remoteserverC\PathToScript\Install.ps1" -Parameter
However this simply refuses to work for me if I try to run it through the c# runspace.
If I send in the following as a parameter to "remoteScript":
"& \"\\\\remoteserverC\\PathToScript\\Install.ps1\" -Parameter"
I get the following error:
The term '\remoteserverC\PathToScript\Install.ps1' 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 with and without '&' and with and without the parameter. I can already call a script that resides directly on the remote machine "c:\...\Install.ps1" instead of "\\remoteserver\...\Install.ps1", but it would be greatly beneficial to be able to call the remote script directly.
I've searched many many pages in google and here on stackoverflow, but I haven't been able to find anything that helps to overcome this issue. Any help would be appreciated! Thanks!
I never did get this to work directly and seems to be a security "feature" that you can't access a third machine using a UNC address while working remotely. I was however able to find a workaround that worked great for me.
Instead of trying to call directly to a \\server\share address, I dynamically map a network drive on the machine I'm trying to run the script against to a share on the machine that has the script. (Running remotely from A, map a drive on B to a share on C). Then I call my scripts through that drive and it works like a charm. This string is what I pass in to the RunRemoteScript method above:
"$net = new-object -ComObject WScript.Network;" +
"if(!($net.EnumNetworkDrives() -contains \"S:\"))" +
"{Write-Host \"S: Drive Not Currently Mapped. Mapping to \\\\" + RemoteServerC + "\\Share.\";" +
"$net.MapNetWorkDrive(\"S:\",\"\\\\" + RemoteServerC + "\\Share\",$false,\"username\",\"password\")};" +
"Get-PSDrive | Write-Verbose;" +
"& \"S:\\PathToScript\\Install.ps1\" -noPrompt;"
RemoteServerC is a variable I pass in that is defined in a user config file.
Here is the same code as just powershell script if anyone needs it (replacing RemoteServerC with a powershell variable you'd need to set before or just hardcode:
$net = new-object -ComObject WScript.Network
if(!($net.EnumNetworkDrives() -contains "S:"))
{ Write-Host "S: Drive Not Currently Mapped. Mapping to \\remoteserverC\Share."
$net.MapNetWorkDrive("S:","\\remoteserverC\Share",$false,"username","password")
}
Get-PSDrive | Write-Verbose
& "S:\\PathToScript\\Install.ps1\" -noPrompt;"
First I set up the object to be able to map the drive. I then check if there is already an "s" drive mapped on the remote machine. If it hasn't I then map the share to the "s" drive on the remote machine using a name and password we set up in active directory to run this service.
I included the Get-PSDrive command because it appears to force powershell to reload the list of available drives. This seems to only matter the very first time you try to run this in a powershell session (and through c sharp I don't know if it is truly necessary or not, I included it to be safe). Apparently powershell does not recognize a drive addition in the same session it was created if you use MapNetworkDrive.

How to execute process on remote machine, in C#

How can I start a process on a remote computer in c#, say computer name = "someComputer", using System.Diagnostics.Process class?
I created a small console app on that remote computer that just writes "Hello world" to a txt file, and I would like to call it remotely.
Console app path: c:\MyAppFolder\MyApp.exe
Currently I have this:
ProcessStartInfo startInfo = new ProcessStartInfo(string.Format(#"\\{0}\{1}", someComputer, somePath);
startInfo.UserName = "MyUserName";
SecureString sec = new SecureString();
string pwd = "MyPassword";
foreach (char item in pwd)
{
sec.AppendChar(item);
}
sec.MakeReadOnly();
startInfo.Password = sec;
startInfo.UseShellExecute = false;
Process.Start(startInfo);
I keep getting "Network path was not found".
Can can use PsExec from http://technet.microsoft.com/en-us/sysinternals/bb897553.aspx
Or WMI:
object theProcessToRun() = { "YourFileHere" };
ManagementClass theClass = new ManagementClass(#"\\server\root\cimv2:Win32_Process");
theClass.InvokeMethod("Create", theProcessToRun);
Use one of the following:
(EDIT) Remote Powershell
WMI (see Ivan G's answer)
Task Scheduler API (http://msdn.microsoft.com/en-us/library/windows/desktop/aa383606%28v=vs.85%29.aspx)
PsExec
WshRemote object with a dummy script. Chances are, it works via DCOM, activating some of scripting objects remotely.
Or if you feel like it, inject your own service or COM component. That would be very close to what PsExec does.
Of all these methods, I prefer task scheduler. The cleanest API of them all, I think. Connect to the remote task scheduler, create a new task for the executable, run it. Note: the executable name should be local to that machine. Not \servername\path\file.exe, but c:\path\file.exe. Delete the task if you feel like it.
All those methods require that you have administrative access to the target machine.
ProcessStartInfo is not capable of launching remote processes.
According to MSDN, a Process object only allows access to remote processes not the ability to start or stop remote processes. So to answer your question with respect to using this class, you can't.
An example with WMI and other credentials as the current process, on default it used the same user as the process runs.
var hostname = "server"; //hostname or a IpAddress
var connection = new ConnectionOptions();
//The '.\' is for a local user on the remote machine
//Or 'mydomain\user' for a domain user
connection.Username = #".\Administrator";
connection.Password = "passwordOfAdministrator";
object[] theProcessToRun = { "YourFileHere" }; //for example notepad.exe
var wmiScope = new ManagementScope($#"\\{hostname}\root\cimv2", connection);
wmiScope.Connect();
using (var managementClass = new ManagementClass(wmiScope, new ManagementPath("Win32_Process"), new ObjectGetOptions()))
{
managementClass.InvokeMethod("Create", theProcessToRun);
}
I don't believe you can start a process through a UNC path directly; that is, if System.Process uses the windows comspec to launch the application... how about you test this theory by mapping a drive to "\someComputer\somePath", then changing your creation of the ProcessStartInfo to that? If it works that way, then you may want to consider temporarily mapping a drive programmatically, launch your app, then remove the mapping (much like pushd/popd works from a command window).

Categories

Resources