Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do Windows services need to ensure that commands can be processed on different threads?

When run via the Service Control Manager do Windows services need to assume that command processing methods (OnStart, OnStop, etc.) can be called on different threads with nothing ensuring that, e.g., assignments to members will be visible between methods?

public class MyService : ServiceBase {

    private Object _foo;    

    protected override void OnStart(string[] args) {
        _foo = new Object();
    }

    protected override void OnStop() {
        if (_foo == null) {
            throw new Exception("Assignment not visible"); // Can this happen?
        }

        _foo = null;
    }

}

I can't find a guarantee that the exception in my example won't be thrown, but all of the examples I've found, including elsewhere on StackOverflow, seems to assume that, e.g., assignments to variables in OnStart() will always be visible in OnStop().

If no such guarantee is made by the SCM I do know how to ensure the assignment is visible (e.g. by adding a lock around all reads/writes in the service). I'm interested in whether or not such measures are necessary.

like image 430
Lawrence Johnston Avatar asked Nov 02 '22 16:11

Lawrence Johnston


1 Answers

In one sense, the SCM cannot guarantee that the exception you outline will not be thrown. It does not control the service's manipulation of a private member of course - e.g. if additional service code affects _foo.

Having said this, consider the following scenario to see why the answer to your specific question is clearly no:

1) Build your service with the following changes to demonstrate:

    public partial class MyService : ServiceBase
    {
        private Object _foo;
        private const string _logName = "MyService Log.txt"; // to support added logging

        public MyService()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            // demonstrative logging
            var threadId = Thread.CurrentThread.ManagedThreadId;
            using (var log = new StreamWriter(AppDomain.CurrentDomain.BaseDirectory + _logName, true))
            {
                log.WriteLine("{0}:  In OnStart(string[]) on thread ID {1}.  Sleeping for 10 seconds...", DateTime.Now, threadId);
            }

            // Sleep before initializing _foo to allow calling OnStop before OnStart completes unless the SCM synchronizes calls to the methods.
            Thread.Sleep(10000);

            _foo = new Object();
        }

        protected override void OnStop()
        {
            // demonstrative logging added
            var threadId = Thread.CurrentThread.ManagedThreadId;
            using (var log = new StreamWriter(AppDomain.CurrentDomain.BaseDirectory + _logName, true))
            {
                log.WriteLine("{0}:  In OnStop() on thread ID {1}.", DateTime.Now, threadId);
            }

            if (_foo == null)
            {
                // demonstrative logging added
                using (var log = new StreamWriter(AppDomain.CurrentDomain.BaseDirectory + _logName, true))
                {
                    log.WriteLine("{0}:  _foo == null", DateTime.Now);
                }

                throw new Exception("Assignment not visible"); // Can this happen?
            }

            _foo = null;
        }
    }

2) Open a command shell.

3) Open another command shell.

4) In the first command shell, install the service (with sc create) if you haven't already, and start it (with net start). You should see:

The MyService service is starting.....

The trailing dots should be added one by one as the SCM waits through the 10 seconds of sleeping to start the service.

5) In the second command shell, try to stop the service (with net stop) before 10 seconds elapse. You should see:

The service is starting or stopping. Please try again later.

So starting a service is clearly a blocking operation that must complete before the service can be stopped.

6) Check the first command shell after 10 seconds have elapsed. You should see:

The MyService service was started successfully.

7) Return to the second command shell and try to stop the service again. You should see:

The MyService service is stopping.

The MyService service was stopped successfully.

8) Review the resulting log - for example:

10/22/2013 7:28:55 AM: In OnStart(string[]) on thread ID 4. Sleeping for 10 seconds...

10/22/2013 7:29:17 AM: In OnStop() on thread ID 5.

Starting and stopping the service quickly is easier with two command shells I think; but the example works similarly with one command shell too.

Lastly, you might find Mitchell Taylor (CoolDadTx)'s answer to a similar question in MSDN forums interesting as I did:

The threading model used by the SCM isn't formally documented AFAIK. What is known is that each service gets called on its own thread. However the SCM might or might not use a thread pool to reuse a thread across services. When a service is called (start, stop, custom commands, etc) it is expected to perform its task and return quickly. There is a strong limit on how long it can take. Anything more than a quick return requires that you push the request to a secondary thread for processing. The SCM itself runs on a separate thread so if a service takes too long to respond then the SCM sees it as hung. This is discussed here: http://msdn.microsoft.com/en-us/library/ms684264(VS.85).aspx

UPDATE:

Particularly note the Service State Transitions MSDN article to which the article that Mitchell Taylor cites links. It contains a state diagram that pretty clearly & authoritatively documents defined service state transitions and aligns with what I outlined above. It also explains in relation to the state diagram how the SCM does not transmit service control requests at times to ensure only defined state transitions.

like image 141
J0e3gan Avatar answered Nov 08 '22 03:11

J0e3gan