Using memory maps with a service
Asked Answered
B

1

11

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.

enter image description here

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?

Burden answered 30/1, 2020 at 0:38 Comment(11)
@Amy I would think that the version of Visual Studio is important because I cannot use anything higher than VS2017. For example, if an answer was able to work with VS2019 but not 2017 then the answer would not be accepted. Hope that makes sense.Burden
"I can't use anything higher than VS2017" isn't a reason to suspect VS is responsible. What version of C# and the .Net Framework are you using? Add those to your question. The VS tags should only be applied when the question is about VS.Lustral
What account is the service registered to run under? If its the System account, the problem is likely that the handles and names are protected from a user account to access them.Delve
@JoelLucsy as my note says in the question Note: The service is running as the same account as I run Visual Studio in.. In this case .. MYPCNAME\arvobowen. It's an admin account. I have tried running everything as administrator and not. It seems to be something directly related to a true service being ran.Burden
I can't see the value of m_sMapName. It must be prefixed with @"Global\" to be visible across sessions.Banderilla
Have you prefixed the name with Global like "Global\\MyName"? you can see this from the example at learn.microsoft.com/en-us/windows/win32/memory/…Delve
@JoelLucsy I also updated the question with some screen shots of various things to show the current account being used. Hope that helps. As for the Global questions, I'm confused. That's just a string value (and it's correct at runtime). I could put anything in there and type the memory map name manually if that would help. The value at runtime on both the service and client gui is myappsvr_mm_toggles. I'm not sure why it would need to be "visible across sessions". It's really a hard coded value in the class that both instances get.Burden
Also, something to keep in mind as I was saying in my question... This works PERFECTLY if I run the service from a command prompt. It does not work when running as a true service. I feel like it's some type of access in the services. I just can't figure out what it is.Burden
Well, that's the problem then, its name must be @"Global\myappsvr_mm_toggles" to make it work when you run it as a service. Services run in a different session from the logged-in user session, Windows keeps namespaces for kernel objects isolated for sessions so they can't interfere with each other. Unless you opt in to the global namespace. It worked when you tested it because both programs ran in the same session.Banderilla
As soon as I wrote my comment I visited that link @JoelLucsy posted and said what the hell and tried it... Sure enough it worked! So that's the answer. Can one of you guys add an answer for the bounty? Is it possible to split it?Burden
If you use the MMF as a cross process communication channel, there are tons of others possibilities (MMF is low level, file oriented). On plain Windows, you can use COM for example, or RPC (in C# that would be Remoting with IPC channel). In C# you can also embark a small HTTP server very easily (search for 'httplistener C#'). Of course there are also IP/sockets but a bit low-level too IMHO (+ timeouts are painful). I like the embedded web server because you can add lots of features and can even use a browser (no specific client app), like apps for routers/box that we all knowMcintire
N
8

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);
       }
Nadia answered 28/2, 2020 at 23:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.