Using memory maps with a service - c#

I built an application that can also be ran as a service (using a -service) switch. This works perfectly with no issues when I'm running the service from a command prompt (I have something set up that lets me debug it from a console when not being ran as a true service). However, when I try to run it as a true service then use my application to open the existing memory map, I get the error...
Unable to find the specified file.
How I run it as a service or in console:
[STAThread]
static void Main(string[] args)
{
//Convert all arguments to lower
args = Array.ConvertAll(args, e => e.ToLower());
//Create the container object for the settings to be stored
Settings.Bag = new SettingsBag();
//Check if we want to run this as a service
bool runAsService = args.Contains("-service");
//Check if debugging
bool debug = Environment.UserInteractive;
//Catch all unhandled exceptions as well
if (!debug || debug)
{
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
}
if (runAsService)
{
//Create service array
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new CRSService()
};
//Run services in interactive mode if needed
if (debug)
RunInteractive(ServicesToRun);
else
ServiceBase.Run(ServicesToRun);
}
else
{
//Start the main gui
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainGUI());
}
}
In my application I have a service side and an application side. The application's purpose is just to control the service. I do all the controlling using memory mapping files and it seems to work great and suits my needs. However, when I run the application as a true service I see from my debug logs it is creating the memory map file with the correct name and access settings. I can also see the file getting created where it should be. Everything seems to working exactly the same in the service as it does when I debug via console. However, my application (when ran as an application instead of the service) tells me it can not find the memory map file. I have it toss the file name path in the error as well so I know it's looking in the right place.
How I open the memory map (where the error is thrown):
m_mmf = MemoryMappedFile.OpenExisting(
m_sMapName,
MemoryMappedFileRights.ReadWrite
);
Note: The service is running as the same account as I run Visual Studio in. As an example the image below shows my task manager, the services.msc gui and my currently identified account.
How can I get my client application to see the memory map file after the service creates it? Why does it work when I run it as a console service and not when I run it as a true service?

Windows Services run in isolation, in Session 0, whilst your Console application runs in a user session, so for them to communicate with each other, the memory mapped file must be created in the Global\ namespace, to make it accessible to other sessions. e.g.
var file = MemoryMappedFile.CreateOrOpen(#"Global\MyMemoryMappedFile", ...
You should also set the appropriate permissions to the file, to make sure all users can access it.
I'd recommend reading this post Implementing Non-Persisted Memory Mapped Files Exposing IPC Style Communications with Windows Services, which explains the above in a lot more detail and has examples on setting the permissions, etc.
Source code copied from the post linked above:
Mutex, Mutex Security & MMF Security Policy Creation
bool mutexCreated;
Mutex mutex;
MutexSecurity mutexSecurity = new MutexSecurity();
MemoryMappedFileSecurity mmfSecurity = new MemoryMappedFileSecurity();
mutexSecurity.AddAccessRule(new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null),
MutexRights.Synchronize | MutexRights.Modify, AccessControlType.Allow));
mmfSecurity.AddAccessRule(new AccessRule<MemoryMappedFileRights>("everyone", MemoryMappedFileRights.FullControl,
AccessControlType.Allow));
mutex = new Mutex(false, #"Global\MyMutex", out mutexCreated, mutexSecurity);
if (mutexCreated == false) log.DebugFormat("There has been an error creating the mutex");
else log.DebugFormat("mutex created successfully");
Create & Write to the MMF
MemoryMappedFile file = MemoryMappedFile.CreateOrOpen(#"Global\MyMemoryMappedFile", 4096,
MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.DelayAllocatePages, mmfSecurity,
HandleInheritability.Inheritable);
using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor()) {
string xmlData = SerializeToXml(CurrentJobQueue) + "\0"; // \0 terminates the XML to stop badly formed
issues when the next string written is shorter than the current
byte[] buffer = ConvertStringToByteArray(xmlData);
mutex.WaitOne();
accessor.WriteArray<byte>(0, buffer, 0, buffer.Length);
mutex.ReleaseMutex();
}
Reading from the MMF
using (MemoryMappedFile file = MemoryMappedFile.OpenExisting(
#"Global\MyMemoryMappedFile", MemoryMappedFileRights.Read)) {
using (MemoryMappedViewAccessor accessor =
file.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read)) {
byte[] buffer = new byte[accessor.Capacity];
Mutex mutex = Mutex.OpenExisting(#"Global\MyMutex");
mutex.WaitOne();
accessor.ReadArray<byte>(0, buffer, 0, buffer.Length);
mutex.ReleaseMutex();
string xmlData = ConvertByteArrayToString(buffer);
data = DeserializeFromXML(xmlData);
}

Related

Why would reading a certifcate in a server.js started by a process cause the process to terminate?

I have a process which, on start, runs a node.js server. Trying to add HTTPS to the server results in the process which the server is attached to terminating. What could be causing this?
The process is started by a service written in C#. The process is then attached onto by the server.js. I've tried debugging the service to see which path of execution leads to the process being terminated by I couldn't track it down. Removing the lines form server.js that read the certificates results in the process not being terminated. However, I can't work out why that would be the case.
To read the certificates:
var privatekey = fs.readFileSync("xxx.pem", "utf8");
var certificate = fs.readFileSync("xxx.cert", "utf8");
var credentials = { key: privatekey, cert: certificate };
The setup of the Process:
var nodePath = GetNodePath();
// Get the path to the web files
var serverPath = ".\\server\\server.js";
// Create the process arguments
ProcessStartInfo info = new ProcessStartInfo();
info.WorkingDirectory = Path.GetDirectoryName( Assembly.GetExecutingAssembly().Location );
info.CreateNoWindow = true;
info.UseShellExecute = false;
info.FileName = nodePath;
info.EnvironmentVariables.Add( "NODE_ENV", "production" );
info.Arguments = $"{serverPath} --max-old-space=200";
I have an event handler for the exit of the process which is mostly below:
private static void webProcess_Exited( object sender, EventArgs e )
{
// Make sure we haven't restarted more than 10 times
if ( restartCount > 5 )
{
var message = "The xxx web service is being shut down due to the nodejs process expectantly terminating too many times.";
// Log to the event log
ShutdownService();
}
else
{
var message = "The nodejs process was expectantly terminated, restarting the process in 5 seconds.";
// Log to the event log
Thread.Sleep( 5000 );
//Starts the process/node.js server again
Start();
}
}
Starting server.js reading the certificates works fine. Removing the lines reading the certificate and running the service also works fine. But, for some reason, when reading the certificates and starting the node.js server through the service, it terminates/starts before terminating for good, as above.
Would anyone what could be causing this? Or suggest ways to troubleshoot this further?
Investigating the comment by #FrankerZ , I found that moving the certificate file from the directory the server.js is held to where the process assembly is fixed my issue. The reason the process terminated is, I assume, the working directory was set in the c# service to be where the assembly is (one level above the server.js). However, the certificate files were stored in the level below (alongside server.js). Thus, when I read the certificates, it is attempting to read them from the directory above where they actually are, resulting in the process terminating.

iisreset over a list of servers programmatically

I want to perform iisreset programmatically from C# code over a list of servers with account having privilege to do that.
It's easy to do that for local machine for example that's a sample code:
// using ...
using System.Diagnostics;
public class YourForm : Form
{
// ...
private void yourButton_Click(object sender, EventArgs e)
{
Process.Start(#"C:\WINDOWS\system32\iisreset.exe", "/noforce");
}
// ...
}
Also:
using System.ServiceProcess;
using (ServiceController controller = new ServiceController())
{
controller.MachineName = “My local or remote computer name”;
controller.ServiceName = “IIS Service Name”; // i.e “w3svc”
if (controller.Status != ServiceControllerStatus.Running)
{
// Start the service
controller.Start();
Log.Debug(“IIS has been started successfully, now checking again for webservice availability”);
}
else
{
// Stop the service
controller.Stop();
// Start the service
controller.Start();
Log.Debug(“IIS has been restarted successfully”);
}
}
but how to perform this for more than one server.
Your first code snippet should work perfectly taking in considerations that there is no need to provide the full path of iisreset command.
Actually, you don't need that full path while calling IISRESET from CMD or Run tool. So, it is the same call.
Regarding user privilege, there are 2 approaches
You can pass desired user as an argument to Process.Start
Process.Start("iisreset", "server1", "admin", "admin password", "domain");
You can just call Process.Start as you did in your code, then make sure to run your application with the suitable user
I tried below and it worked perfectly
static void Main(string[] args)
{
string[] servers = LoadServersFromFile();
foreach (string server in servers)
{
Process.Start("iisreset", server.Trim());
}
}
private static string[] LoadServersFromFile()
{
//just listed all servers comma separated in a text file, change this to any other approach fits for your case
TextReader reader = new StreamReader("Servers.txt");
return reader.ReadToEnd().Split(',');
}
You probably need an impersonator to execute the above code.
I think the username and password used in the impersonator should have admin rights for that server (which you do).
You probably also need to remotely access the machine and then execute your code.
The post here, here and here might be of help to you.
Will update this post if something more useful comes to my mind.
EDIT:
You can try out the following steps:
Create a windows service with code for restarting the IIS
Deploy this service on all the servers for which you need to reset the IIS
Keep this service turned off
Remotely access this service (code to access services remotely is given in one of the posts above)
Start and stop the service. This will execute the code for resetting the IIS. Code for this is given here
Hope this helps.

How to run a command prompt on a shared server? [duplicate]

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).

WCF service Process.Start under network service account impersonating as a different user

I have Wcf Service hosted in IIS, Windows Server 2008 R2, using an AppPool .NET 4.0 with NETWORK SERVICE Identity.
My Wcf Service has a method that calls an command EXE using Process.Start.
I need use an different user as credentials for execute command EXE, an domain user account.
I try execute it but it doesn't works for me: it seems not execute the command EXE.
update: process exited, but not execute code
I get errors like:
exit code -1073741502
and eventvwr:
Process Information:
Process ID: 0xc50
Process Name: C:\DeployTools\DeployTools.Commands.Ejecutar.exe
Exit Status: 0xc0000142
The appplication was unable to start correctly (0xC0000142). Click OK
to close the application
Any suggestions?
Code:
StreamReader sr = null;
StreamReader serr = null;
try
{
var psi = new ProcessStartInfo(MY_COMMAND_EXE);
psi.WorkingDirectory = Path.GetDirectoryName(MY_COMMAND_EXE);
psi.Arguments = arguments;
psi.Domain = DOMAIN;
psi.UserName = USER_IN_DOMAIN;
psi.Password = SecureStringHelper.ToSecureString(pwd);
psi.LoadUserProfile = true;
psi.UseShellExecute = false;
psi.ErrorDialog = false;
psi.RedirectStandardOutput = true;
psi.RedirectStandardInput = true;
psi.RedirectStandardError = true;
psi.CreateNoWindow = true;
psi.WindowStyle = ProcessWindowStyle.Minimized;
using (Process pr = Process.Start(psi))
{
sr = pr.StandardOutput;
serr = pr.StandardError;
if (!pr.HasExited)
{
pr.WaitForExit(300000);
}
output = pr.StandardOutput.ReadToEnd();
errors = pr.StandardError.ReadToEnd();
exitCode = pr.ExitCode;
return output;
}
}
catch (Exception exc)
{
return "EXCEPCIÓN: " + exc.Message;
}
finally
{
if (sr != null)
{
sr.Close();
sr.Dispose();
sr = null;
}
if (serr != null)
{
serr.Close();
serr.Dispose();
serr = null;
}
}
I had to add a reference to AsproLock.dll and associated code in order to permit the user account to have access to the running resource.
//The following security adjustments are necessary to give the new
//process sufficient permission to run in the service's window station
//and desktop. This uses classes from the AsproLock library also from
//Asprosys.
IntPtr hWinSta = NativeMethods.GetProcessWindowStation();
WindowStationSecurity ws = new WindowStationSecurity(hWinSta,
System.Security.AccessControl.AccessControlSections.Access);
ws.AddAccessRule(new WindowStationAccessRule(userPassDto.Usuario,
WindowStationRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
ws.AcceptChanges();
IntPtr hDesk = NativeMethods.GetThreadDesktop(NativeMethods.GetCurrentThreadId());
DesktopSecurity ds = new DesktopSecurity(hDesk,
System.Security.AccessControl.AccessControlSections.Access);
ds.AddAccessRule(new DesktopAccessRule(userPassDto.Usuario,
DesktopRights.AllAccess, System.Security.AccessControl.AccessControlType.Allow));
ds.AcceptChanges();
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetProcessWindowStation();
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetThreadDesktop(int dwThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern int GetCurrentThreadId();
The Perils and Pitfalls of Launching a Process Under New Credentials by Asprosys
This isn't a common need but it also isn't that rare, so I thought I had better post this step by step guide to troubleshooting the launching of a process under impersonated credentials. This is based on using the Start method of the .Net Process class but it is also applicable to the underlying API calls: CreateProcessWithLogonW and CreateProcessWithTokenW.
Access Denied - The first attempt and an access denied exception right off the bat. This is the most common initial problem and is caused by the fact that the service is running under the LOCAL SYSTEM account. Strangely, the SYSTEM account is the most powerful account on the computer but one of the few things it cannot do is launch a process using CreateProcessWithLogonW which is the API underlying the call to Process.Start. So change your service account to Local Service, it's probably the more appropriate account anyway.
Access Denied Again - Aargh, I thought we solved this. Oops, double check the permissions on the application that you are trying to launch. Remember that the system tries to access the application file as the user account that the process will be running under, not the service account.
Invalid Directory Error - What? All the paths are correct. All the directories are spelled correctly, no invalid characters. This is an incredibly annoying error and is not very consistent. Usually when we run a process we don't bother setting the WorkingDirectory property and just accept the default from the parent process. When starting a process with new credentials you can't do that, you must explicitly set a path for the WorkingDirectory or you'll get a "The directory name is invalid." Win32Exception.
Failure: No Error? - Process.Start handles the creation of the Environment block for the new process for you quite well. So this is a problem only if you are using the underlying API. When calling one of the CreateProcess* APIs it is normal to leave the lpEnvironment parameter as NULL and have the system use the default of copying the block from the parent process. But when launching under new credentials you must create an environment block explicitly, either manually or using CreateEnvironmentBlock. What makes this worse is, if you leave this out the CreateProcess* call will fail but GetLastError will return ERROR_SUCCESS and if you make an error creating your environment block there will be no error but the process may just not run at all.
Application Failed to Initialize Properly - No more exceptions, you've solved all the problems and the process has been launched. Oops again, where is the process? Check the event log (or you may have received an Application Error pop-up). There should be an entry for Application Error that says that your process was the faulting application, either user32.dll or kernel32.dll was the faulting module and the exception was: 0xC0000142. There may be some minor variation in this but basically it is saying that your application could not initialize. The reason for this is that on initialization, before any application code is run, all processes are attached to a Window Station and all threads are attached to a Desktop but the user you are launching under does not have permission to access the Window Station and Desktop in which your process is being launched, ergo it can't initialize. The security descriptors for the Window Station and Desktop must be adjusted to give AllAccess permission to the user the process is being launched under. This is a devil to do directly in .Net, so you might find the security wrapper classes here useful.
No More Errors - Really, no more errors, your process should be running smoothly now. There may be some variations in what you need to do based on who the user is (for instance an administrator will already have the correct permissions in some cases) or what kind of session you are launching in. But following these steps should make your life smooth and easy (well maybe not your whole life).
References:
The Perils and Pitfalls of Launching a Process Under New Credentials
Aspro Lock - Access Control
Code Samples
Creating New Process Under Alternate Credentials (createprocessasuser)
processstart-hangs

How to set "interact with desktop" in windows service installer

I have a windows service which runs under system account and executes some programs from time to time (yeah,yeah, I know that's a bad practice, but that's not my decision). I need to set the "interact with desktop" check, to see the gui of that executed programs, after the service is installed. I've tried several ways, putting the code below in AfterInstall or OnCommited event handlers of my service installer:
ConnectionOptions coOptions = new ConnectionOptions();
coOptions.Impersonation = ImpersonationLevel.Impersonate;
ManagementScope mgmtScope = new System.Management.ManagementScope(#"root\CIMV2", coOptions);
mgmtScope.Connect();
ManagementObject wmiService = new ManagementObject("Win32_Service.Name='" + ServiceMonitorInstaller.ServiceName + "'");
ManagementBaseObject InParam = wmiService.GetMethodParameters("Change");
InParam["DesktopInteract"] = true;
ManagementBaseObject OutParam = wmiService.InvokeMethod("Change", InParam, null);
or
RegistryKey ckey = Registry.LocalMachine.OpenSubKey(
#"SYSTEM\CurrentControlSet\Services\WindowsService1", true);
if(ckey != null)
{
if(ckey.GetValue("Type") != null)
{
ckey.SetValue("Type", ((int)ckey.GetValue("Type") | 256));
}
}
both of these methods "work". They set the check, but after I start the service it launches the exe - and gui isn't shown! So, if I stop the service, recheck and start it again - bingo! everything starts and is shown. The second way to achieve the result is to reboot - after it the gui is also shown.
So the question is: Is there a correct way to set "interact with desktop" check, so it'll start working without rechecks and reboots?
OS: Windows XP (haven't tried Vista and 7 yet...)
private static void SetInterActWithDeskTop()
{
var service = new System.Management.ManagementObject(
String.Format("WIN32_Service.Name='{0}'", "YourServiceName"));
try
{
var paramList = new object[11];
paramList[5] = true;
service.InvokeMethod("Change", paramList);
}
finally
{
service.Dispose();
}
}
And finally after searching the internet for a week - I've found a great working solution:
http://asprosys.blogspot.com/2009/03/allow-service-to-interact-with-desktop.html
Find the desktop to launch into. This
may seem facetious but it isn't as
simple as it seems. With Terminal
Services and Fast User Switching there
can be multiple interactive users
logged on to the computer at the same
time. If you want the user that is
currently sitting at the physical
console then you're in luck, the
Terminal Services API call
WTSGetActiveConsoleSessionId will get
you the session ID you need. If your
needs are more complex (i.e. you need
to interact with a specific user on a
TS server or you need the name of the
window station in a non-interactive
session) you'll need to enumerate the
Terminal Server sessions with
WTSEnumerateSessions and check the
session for the information you need
with WTSGetSessionInformation.
Now you know what session you need to
interact with and you have its ID.
This is the key to the whole process,
using WTSQueryUserToken and the
session ID you can now retrieve the
token of the user logged on to the
target session. This completely
mitigates the security problem of the
'interact with the desktop' setting,
the launched process will not be
running with the LOCAL SYSTEM
credentials but with the same
credentials as the user that is
already logged on to that session! No
privilege elevation.
Using CreateProcessAsUser and the
token we have retrieved we can launch
the process in the normal way and it
will run in the target session with
the target user's credentials. There
are a couple of caveats, both
lpCurrentDirectory and lpEnvironment
must point to valid values - the
normal default resolution methods for
these parameters don't work for
cross-session launching. You can use
CreateEnvironmentBlock to create a
default environment block for the
target user.
There is source code of the working project attached.
Same as Heisa but with WMI. (code is Powershell, but can be easily ported to C#)
if ($svc = gwmi win32_service|?{$_.name -eq $svcname})
{
try {
$null = $svc.change($svc.displayname,$svc.pathname,16,1,`
"Manual",$false,$svc.startname,$null,$null,$null,$null)
write-host "Change made"
catch { throw "Error: $_" }
} else
{ throw "Service $svcname not installed" }
See MSDN: Service Change() method for param description.

Categories