I am writing a C# winform application, that controls a service. I tried to control this service with System.Diagnostics.Process and System.ServiceProcess.ServiceController.
Because of the required AdminRights to make changes at a service and a few post i read about make this happen with the "runas"-verb, I decided to use System.Diagnostics.Process.
System.Diagnostics.ProcessStartInfo startInfo =
new System.Diagnostics.ProcessStartInfo();
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.FileName = "cmd.exe";
startInfo.Arguments = "/C net start " + SERVICE_NAME;
startInfo.RedirectStandardError = true; //// <<<<<<<<<<<<<<<<<
startInfo.UseShellExecute = true;
startInfo.Verb = "runas";
Process process = new Process();
process.StartInfo = startInfo;
process.ErrorDataReceived += cmd_DataReceived;
process.Start();
process.BeginOutputReadLine();
process.WaitForExit();
But it seems that the runas-Verb and the redirection of the StandardErrorOutput doesn't work. It works if I comment out the line, but I need this to decide if the command was executed successfully or not.
Is there a way how to start/ stop a service (executed with temporary admin rights) and to get a result of success?
Your best option is to start the service yourself, inside your own application.
The problem of course is that your application normally isn't run as an administrator (nor do you want it to be). That means you can't start or stop services. That is why you should temporarily elevate, perform the task needed, then exit.
In other words, launch a second copy of your app, passing a command line option instructing yourself to start the service. The second copy does only that, then exits.
The Code
First we have a button the user can click to start the service. We first check if the user already is an administrator, in which case we don't have to do anything special:
private void button1_Click(object sender, EventArgs e)
{
//If we're an administrator, then do it
if (IsUserAnAdmin())
{
StartService("bthserv"); //"Bluetooth Support Service" for my sample test code
return;
}
//We're not running as an administrator.
//Relaunch ourselves as admin, telling us that we want to start the service
ExecuteAsAdmin(Application.ExecutablePath, "/serviceStart");
}
//helper function that tells us if we're already running with administrative rights
private Boolean IsUserAnAdmin()
{
//A user can be a member of the Administrator group, but not an administrator.
//Conversely, the user can be an administrator and not a member of the administrators group.
//Check if the current user has administrative privelages
var identity = WindowsIdentity.GetCurrent();
return (null != identity && new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator));
}
private void ExecuteAsAdmin(string Filename, string Arguments)
{
//Launch process elevated
ProcessStartInfo startInfo = new ProcessStartInfo(Filename, Arguments);
startInfo.Verb = "runas"; //the runas verb is the secret to making UAC prompt come up
System.Diagnostics.Process.Start(startInfo);
}
Then, during startup, we just need to check if we're being called with the command line option. If so, start the service:
public Form1()
{
InitializeComponent();
//Ideally this would be in program.cs, before the call to Application.Run()
//But that would require me to refactor code out of the Form file, which is overkill for a demo
if (FindCmdLineSwitch("serviceStart", true))
{
StartService("bthserv"); //"Bluetooth Support Service"
Environment.Exit(0);
}
}
private bool FindCmdLineSwitch(string Switch, bool IgnoreCase)
{
foreach (String s in System.Environment.GetCommandLineArgs())
{
if (String.Compare(s, "/" + Switch, IgnoreCase) == 0)
return true;
if (String.Compare(s, "-" + Switch, IgnoreCase) == 0)
return true;
}
return false;
}
And finally to start the service:
private void StartService(String ServiceName)
{
TimeSpan timeout = TimeSpan.FromMilliseconds(30000); //30 seconds
using (ServiceController service = new ServiceController(ServiceName))
{
try
{
service.Start();
}
catch (Exception e)
{
MessageBox.Show(e.Message, "Error starting service");
return;
}
service.WaitForStatus(ServiceControllerStatus.Running, timeout);
}
}
Extra fancy
You can add arbitrary amounts of fancy code. Detect if the user is not an admin, and if not then use the proper Windows API (BCM_SETSHIELD message) to add a UAC shield to your button:
That way users know to expect UAC to appear; because you followed the Microsoft UI guidelines:
Guidelines
UAC shield icon
Display controls with the UAC shield to indicate that the task requires immediate elevation when UAC is fully enabled, even if UAC isn't currently fully enabled. If all paths of a wizard and page flow require elevation, display the UAC shield at the task's entry point. Proper use of the UAC shield helps users predict when elevation is required.
Bonus Reading
BCM_SETSHIELD message
Windows Design Guidelines - User Account Control
Designing UAC Applications for Windows Vista (Especially Step 4: Redesign Your UI for UAC Compatibility)
http://msdn.microsoft.com/en-us/library/windows/desktop/aa511445.aspx
Note: Any code is released into the public domain. No attribution required.
You could use MT.exe which is a utility provided by the framework ( In the SDK sub-directory ) to generate a manifest that forces your application to run in administrator mode. In which case you shouldn't have problem dealing with your protected service. The trade off of course is that ... you're running in admin mode.
mt.exe -manifest "your-path\your-app.exe.manifest" -updateresource:"$(TargetDir)$(TargetName).exe;#1
Hopefully this helps.
Related
I need my app to require admin rights before running a process asynchronously. It used to work well with the following configuration in the app.manifest:
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
However, since now processes were added that should not require admin rights to be run, this elegant solution no longer cuts it. I expected this to do the trick:
process.StartInfo.UseShellExecute = true;
process.StartInfo.Verb = "runas";
This is what I have, but there must be an error somewhere, since this code runs the process as expected, but doesn't actually require admin rights to do so:
public async Task ExecuteElevatedProcessAsync(string executablePathArg)
{
using (var process = new Process())
{
process.StartInfo.FileName = executablePathArg;
process.StartInfo.UseShellExecute = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.Verb = "runas";
await RunAsync(process);
};
}
private Task RunAsync(Process processArg)
{
var taskCompletionSrc = new TaskCompletionSource<object>();
processArg.EnableRaisingEvents = true;
processArg.Exited += (s, e) => taskCompletionSrc.TrySetResult(null);
if (!processArg.Start())
{
taskCompletionSrc.SetException(new Exception("Some descriptive error-message."));
}
return taskCompletionSrc.Task;
}
Do you know how to fix this?
By running it on another computer I’ve learned that the above code does indeed work as intended: Before executing the asynchronous process, it triggers the Windows UAC-popup that asks the user to give the app permission to make changes to the device.
The popup was never triggered on my Windows machine because the user account control was disabled entirely, which isn’t a smart choice to begin with...
In my C# WPF Application in statup process I'm checking whether the installation of new version is required. If so, I want to break current process and start installer. The installer is developed using NSIS package.
The problem is that sometimes only User Account Control dialog from NSIS installer appears and install process breaks.
How to ensure that the installation process is executed every time?
Here is my code on Application Startup.
protected override void OnStartup(StartupEventArgs e)
{
try
{
//Disable shutdown when the dialog closes
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
if ( IfUpdateRequired())
{
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo(sessionCtx.AutoUpdateVersionInfo.SetupPath);
//This should not block current program
startInfo.UseShellExecute = true;
startInfo.Verb = "runas";
System.Diagnostics.Process.Start(startInfo);
this.Shutdown();
}
else
{
base.OnStartup(e);
}
}
catch (Exception ex)
{
}
}
My only guess is that the child process has not fully started before your process quits. ShellExecute is allowed to perform its operation asynchronously.
If this is the cause then you should be able to work around it by sleeping a bit before calling this.Shutdown(). Wait 10 seconds or so perhaps? Or call WaitForInputIdle(9999) on the process. Or maybe you could check the Responding process property?
Rules that i need to hold on to are the following:
run command prompt command as administrator
change directory (path is always the same)
run command net start something
Any thoughts on how can i do this programatically using C# ?
Just for the record i am running console application built in .net 4.0
I am using this function
private static void commandtorun(string commandexecuted)
{
string currentstatus;
ProcessStartInfo startInfo = new ProcessStartInfo();
Process myprocess = new Process();
try
{
startInfo.FileName = "cmd"; //
startInfo.RedirectStandardInput = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false; //'required to redirect
//startInfo.CreateNoWindow = true; // '<---- creates no window, obviously
myprocess.StartInfo = startInfo; //
myprocess.Start(); //
System.IO.StreamReader SR;
System.IO.StreamWriter SW;
Thread.Sleep(200);
SR = myprocess.StandardOutput;
SW = myprocess.StandardInput;
SW.WriteLine(commandexecuted); // 'the command you wish to run.....
SW.WriteLine("exit"); // 'exits command prompt window
Thread.Sleep(200);
currentstatus = SR.ReadToEnd();
SW.Close();
SR.Close();
}
catch (Exception e)
{
Console.WriteLine("{0} Exception caught.", e);
Console.ReadLine();
}
// throw new NotImplementedException();
}
and i get access is denied when i call the function using this command:
commandtorun(#"cd c:\Program Files (x86)\folder1\folder2"); // change the folder because cmd always runs on different path
commandtorun("net start something"); run the COMMAND
I tried to run both commands in one statement but got the same error.
There are lots of problems here.
You want to run the other process elevated, but your code does not take steps to make that happen. In order to do that you need to invoke your process with the runas verb, and UseShellExecute set to true. But you also want to re-direct which is why you set UseShellExecute to false.
I see no compelling reason to redirect, so I think you can use true for UseShellExecute. You are pumping an exit command into cmd.exe to terminate the process. You don't need to do that. Simply pass cmd.exe the /c argument and the process will close when it is done.
These changes will allow you to remove those calls to Sleep(). Any time you call Sleep() you should ask yourself why you are doing it and if it can be avoided. Very few problems have Sleep() as the optimum solution.
You are trying to specify the working directory by executing cd. That again is the wrong way to do it. You can specify the working directory in the ProcessStartInfo object. That's how you should do it.
Your design has you executing each command in a separate process. That's a really poor way to go. You want to execute all the commands one after the other in a single instance of cmd.exe. Put the commands into a .bat or .cmd file and get cmd.exe to process that.
Imagine if you carried on processing each command as a separate process. How would the second process remember the working directory change that you made? Would you really want your user to see a UAC dialog for each separate command?
Having said all of that though, you are going about this the wrong way. You are just trying to start a service. Yes, you do that with net start when you are working in the command line. But from a program you use the service API to start a service. In other words I believe that you should throw away all of this code and call the service API.
I have an app that has some installer inside I want to reload everything associated to the app therefor I want to restart the process. I've searched and saw the Application.Restart() and it's drawbacks and wondered what's the best way to do what I need - closing the process and restarting it. or if there's any better way to reinitialize all objects.
I would start a new instance and then exit the current one:
private void Restart()
{
Process.Start(Application.ExecutablePath);
//some time to start the new instance.
Thread.Sleep(2000);
Environment.Exit(-1);//Force termination of the current process.
}
private static void Main()
{
//wait because we maybe here becuase of the system is restarted so give it some time to clear the old instance first
Thread.Sleep(5000);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(...
}
Edit: However you should also consider adding some sort of mutex to allow only one instance of the application to run at time, Like:
private const string OneInstanceMutexName = #"Global\MyUniqueName";
private static void Main()
{
Thread.Sleep(5000);
bool firstInstance = false;
using (System.Threading.Mutex _oneInstanceMutex = new System.Threading.Mutex(true, OneInstanceMutexName, out firstInstance))
{
if (firstInstance)
{
//....
}
}
}
In my WPF application (single instance by a mutex), I use Process.Start with a ProcessStartInfo, which send a timed cmd command to restart the app:
ProcessStartInfo Info = new ProcessStartInfo();
Info.Arguments = "/C ping 127.0.0.1 -n 2 && \"" + Application.GetCurrentProcess()+ "\"";
Info.WindowStyle = ProcessWindowStyle.Hidden;
Info.CreateNoWindow = true;
Info.FileName = "cmd.exe";
Process.Start(Info);
ShellView.Close();
The command is sent to the OS, the ping pauses the script for 2-3 seconds, by which time the application has exited from ShellView.Close(), then the next command after the ping starts it again.
Note: The \" puts quotes around the path, incase it has spaces, which cmd can't process without quotes.
(My code references this answer)
I think starting a new process and closing the existing process is the best way. In this way you have the ability to set some application state for the existing process in between starting and closing processes.
This thread discusses why Application.Restart() may not work in some cases.
System.Diagnostics.Process.Start(Application.ResourceAssembly.Location);
// Set any state that is required to close your current process.
Application.Current.Shutdown();
Or
System.Diagnostics.Process.Start(Application.ExecutablePath);
// Set any state that is required to close your current process.
Application.Exit();
I am attempting to wrap a 3rd party command line application within a web service.
If I run the following code from within a console application:
Process process= new System.Diagnostics.Process();
process.StartInfo.FileName = "some_executable.exe";
// Do not spawn a window for this process
process.StartInfo.CreateNoWindow = true;
process.StartInfo.ErrorDialog = false;
// Redirect input, output, and error streams
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardInput = true;
process.EnableRaisingEvents = true;
process.ErrorDataReceived += (sendingProcess, eventArgs) => {
// Make note of the error message
if (!String.IsNullOrEmpty(eventArgs.Data))
if (this.WarningMessageEvent != null)
this.WarningMessageEvent(this, new MessageEventArgs(eventArgs.Data));
};
process.OutputDataReceived += (sendingProcess, eventArgs) => {
// Make note of the message
if (!String.IsNullOrEmpty(eventArgs.Data))
if (this.DebugMessageEvent != null)
this.DebugMessageEvent(this, new MessageEventArgs(eventArgs.Data));
};
process.Exited += (object sender, EventArgs e) => {
// Make note of the exit event
if (this.DebugMessageEvent != null)
this.DebugMessageEvent(this, new MessageEventArgs("The command exited"));
};
process.Start();
process.StandardInput.Close();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
int exitCode = process.ExitCode;
process.Close();
process.Dispose();
if (this.DebugMessageEvent != null)
this.DebugMessageEvent(this, new MessageEventArgs("The command exited with code: " + exitCode));
All events, including the "process.Exited" event fires as expected. However, when this code is invoked from within a web service method, all events EXCEPT the "process.Exited" event fire.
The execution appears to hang at the line:
process.WaitForExit();
Would anyone be able to shed some light as to what I might be missing?
As it turns out the problem was caused by the executable that I was trying to invoke.
Unfortunately, this 3rd party executable was a port of a UNIX command that was being run through a kind of emulator. The executable was designed to output messages to the output and error streams as one would expect. However, the toolkit our vendor used to port the binaries over to Windows does not use the standard output streams.
When I stepped through the web service and manually invoked the process from the command line, I saw the emulator display an error dialog box. From the C# point of view, the process does not complete unless the [OK] button is clicked on the dialog box hence the "Exited" event never fires.
After speaking with our vendor for the executable I learned it is not fully supported in 64-bit Windows. I installed the web service on a 32-bit environment and everything was fine.
What's the process you are running in there? Since you mentioned its a console application, perhaps, it was waiting for more input? Since running this as a webservice, is the executable running under the same permissions as the ASP webservice? It could be that the web-service is not releasing the executable as that is loaded in process, or that the web-service is designated to run forever until IIS gets restarted and then the Process's Exit event may get fired.
It has come to my attention also, that there is no using clause around the Process's instantiated object i.e.
using (Process proc = new Process())
{
}
Edit: Also, please see here a link to a similar concept. The only thing after comparing the results is that the property WindowStyle is set...
ps.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;