create blog

go home go home
  1. about
  2. code
  3. wiki
  4. blog

Running Orbited (or other Twisted Services) as a Windows Service

It took me awhile to figure out how to run Orbited and Dolores as Windows Services. Unfortunately, while I love Mac, almost every other computer at my workplace is Windows, including the servers. When deploying my web application, I can’t really just leave instructions like: “When you restart the server, please run these two scripts.”

They need to be Windows Services.

Thankfully, it isn’t actually too hard. You just need a couple of Python packages (which I actually don’t remember installing, so they may have come with Twisted or Python), and then all you have to do is write a couple of very simple scripts. Here are mine:

Orbited Service

import OrbitedManager # a copy of orbited.start with some very minor modifications, which I'll include after the break
import time
 
# Service Utilities
import win32serviceutil
import win32service
import win32event
 
class WindowsService(win32serviceutil.ServiceFramework):
	_svc_name_ = "Orbited"
	_svc_display_name_ = "Orbited Server"
 
	def __init__(self, args):
		win32serviceutil.ServiceFramework.__init__(self, args)
		self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
 
	def SvcStop(self):
		self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
		win32event.SetEvent(self.hWaitStop)
 
	def SvcDoRun(self):
		import servicemanager
		OrbitedManager.start()
		while True:
			retval = win32event.WaitForSingleObject(self.hWaitStop, 10)
			if not retval == win32event.WAIT_TIMEOUT:
				OrbitedManager.stop()
				break
				time.sleep(5.0)
 
 
if __name__=='__main__':
	win32serviceutil.HandleCommandLine(WindowsService)

Dolores Service

import Dolores
import time
 
# Service Utilities
import win32serviceutil
import win32service
import win32event
 
class WindowsService(win32serviceutil.ServiceFramework):
	_svc_name_ = "Dolores"
	_svc_display_name_ = "Dolores Server"
 
	def __init__(self, args):
		win32serviceutil.ServiceFramework.__init__(self, args)
		self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
 
	def SvcStop(self):
		self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
		win32event.SetEvent(self.hWaitStop)
 
	def SvcDoRun(self):
		import servicemanager
		Dolores.start()
		while True:
			retval = win32event.WaitForSingleObject(self.hWaitStop, 10)
			if not retval == win32event.WAIT_TIMEOUT:
				Dolores.stop()
				break
				time.sleep(5.0)
 
 
if __name__=='__main__':
	win32serviceutil.HandleCommandLine(WindowsService)

That’s the basics, but as you may have noticed, I also changed Orbited’s start script. I did this because the original did not play nicely with threads.

Instead of changing it in its original location in the filesystem, I created a copy of it in the local directory (as you can see, I’m not importing Orbited.OrbitedManager).

Before overwhelming you with a wall of code, I’ll explain what I changed. It is actually quite simple:

  1. First, some imports at the top:
    import orbited
    import threading
    import time
  2. Since the file is no longer “start.py” in the Orbited package directory, the static files path needs changing (just find the line and change it):
    static_files = static.File(os.path.join(os.path.dirname(orbited.__path__[0]), 'orbited/static'))
  3. And, since we’re going to start the reactor manually, we need to remove the auto-start portion:
    # REMOVE THIS PART:
        if options.profile:
            import hotshot
            prof = hotshot.Profile("orbited.profile")
            logger.info("running Orbited in profile mode")
            logger.info("for information on profiler, see http://orbited.org/wiki/Profiler")
            prof.runcall(reactor.run)
            prof.close()
        else:
            reactor.run()
  4. Finally, I added a bit to the bottom, replacing the lines that call “main()”. It arranges to run Orbited (or any Twisted service) in a separate thread:
    def thread_start():
    	main() # initializes the reactor
    	from twisted.internet import reactor
    	reactor.run(installSignalHandlers=0)
    	print "Crashed into Earth's Surface."
     
    orbited_thread = None
    def start():
    	print "Starting Orbited..."
    	orbited_thread = threading.Thread(target=thread_start)
    	orbited_thread.start()
    	print "In Orbit."
     
    def stop_reactor():
    	reactor.stop()
     
    def stop():
    	reactor.callFromThread(stop_reactor)
     
    if __name__ == "__main__":
    	start()
    	run = True
    	while run:
    		try:
    			time.sleep(5)
    		except KeyboardInterrupt:
    			from twisted.internet import reactor
    			stop()
    			run = False

The Raw OrbitedManager.py File

import os
import sys
import urlparse
from orbited import __version__ as version
from orbited import config
from orbited import logging
import orbited
import threading
import time
 
# NB: this is set after we load the configuration at "main".
logger = None
 
def _import(name):
    module_import = name.rsplit('.', 1)[0]
    return reduce(getattr, name.split('.')[1:], __import__(module_import))
 
def _setup_protocols(root):
    from twisted.internet import reactor
    protocols = [
        #child_path config_key  port_class_import,              factory_class_import
        ('tcp',     'proxy',    'orbited.cometsession.Port',    'orbited.proxy.ProxyFactory'),
    ]
    for child_path, config_key, port_class_import, factory_class_import in protocols:
        if config.map['[global]'].get('%s.enabled' % config_key) == '1':
            port_class = _import(port_class_import)
            factory_class = _import(factory_class_import)
            reactor.listenWith(port_class, factory=factory_class(), resource=root, childName=child_path)
            logger.info('%s protocol active' % config_key)
 
def _setup_static(root, config):
    from twisted.web import static
    for key, val in config['[static]'].items():
        if key == 'INDEX':
            key = ''
        if root.getStaticEntity(key):
            logger.error("cannot mount static directory with reserved name %s" % key)
            sys.exit(1)
        root.putChild(key, static.File(val))
 
def main():
    try:
        import twisted
    except ImportError:
        print "Orbited requires Twisted, which is not installed. See http://twistedmatrix.com/trac/ for installation instructions."
        sys.exit(1)
 
    #################
    # This corrects a bug in Twisted 8.2.0 for certain Python 2.6 builds on Windows
    #   Twisted ticket: http://twistedmatrix.com/trac/ticket/3868
    #     -mario
    try:
        from twisted.python import lockfile
    except ImportError:
        from orbited import __path__ as orbited_path
        sys.path.append(os.path.join(orbited_path[0],"hotfixes","win32api"))
        from twisted.python import lockfile
        lockfile.kill = None
    #################
 
    from optparse import OptionParser
    parser = OptionParser()
    parser.add_option(
        "-c",
        "--config",
        dest="config",
        default=None,
        help="path to configuration file"
    )
    parser.add_option(
        "-v",
        "--version",
        dest="version",
        action="store_true",
        default=False,
        help="print Orbited version"
    )
    parser.add_option(
        "-p",
        "--profile",
        dest="profile",
        action="store_true",
        default=False,
        help="run Orbited with a profiler"
    )
    parser.add_option(
        "-q",
        "--quickstart",
        dest="quickstart",
        action="store_true",
        default=False,
        help="run Orbited on port 8000 and MorbidQ on port 61613"
    )
 
    (options, args) = parser.parse_args()
 
    if args:
        print 'the "orbited" command does not accept positional arguments. type "orbited -h" for options.'
        sys.exit(1)
 
    if options.version:
        print "Orbited version: %s" % (version,)
        sys.exit(0)
 
    if options.quickstart:
        config.map['[listen]'].append('http://:8000')
        config.map['[listen]'].append('stomp://:61613')
        config.map['[access]'][('localhost',61613)] = ['*']
        print "Quickstarting Orbited"
    else:
        # load configuration from configuration
        # file and from command line arguments.
        config.setup(options=options)
 
    logging.setup(config.map)
 
    # we can now safely get loggers.
    global logger; logger = logging.get_logger('orbited.start')
 
 
    # NB: we need to install the reactor before using twisted.
    reactor_name = config.map['[global]'].get('reactor')
    if reactor_name:
        install = _import('twisted.internet.%sreactor.install' % reactor_name)
        install()
        logger.info('using %s reactor' % reactor_name)
 
    ############
    # This crude garbage corrects a bug in twisted
    #   Orbited ticket: http://orbited.org/ticket/111
    #   Twisted ticket: http://twistedmatrix.com/trac/ticket/2447
    import twisted.web.http
    twisted.web.http.HTTPChannel.setTimeout = lambda self, arg: None
    twisted.web.http.HTTPChannel.resetTimeout = lambda self: None
    ############
 
    from twisted.internet import reactor
    from twisted.web import resource
    from twisted.web import server
    from twisted.web import static
    import orbited.system
 
    root = resource.Resource()
    static_files = static.File(os.path.join(os.path.dirname(orbited.__path__[0]), 'orbited/static'))
    root.putChild('static', static_files)
    root.putChild('system', orbited.system.SystemResource())
 
    if config.map['[test]']['stompdispatcher.enabled'] == '1':
        logger.info('stompdispatcher enabled')
 
    #static_files.putChild('orbited.swf', static.File(os.path.join(os.path.dirname(__file__), 'flash', 'orbited.swf')))
    site = server.Site(root)
 
    _setup_protocols(root)
    _setup_static(root, config.map)
    start_listening(site, config.map, logger)
 
    # switch uid and gid to configured user and group.
    if os.name == 'posix' and os.getuid() == 0:
        user = config.map['[global]'].get('user')
        group = config.map['[global]'].get('group')
        if user:
            import pwd
            import grp
            try:
                pw = pwd.getpwnam(user)
                uid = pw.pw_uid
                if group:
                    gr = grp.getgrnam(group)
                    gid = gr.gr_gid
                else:
                    gid = pw.pw_gid
                    gr = grp.getgrgid(gid)
                    group = gr.gr_name
            except Exception, e:
                logger.error('Aborting; Unknown user or group: %s' % e)
                sys.exit(1)
            logger.info('switching to user %s (uid=%d) and group %s (gid=%d)' % (user, uid, group, gid))
            os.setgid(gid)
            os.setuid(uid)
        else:
            logger.error('Aborting; You must define a user (and optionally a group) in the configuration file.')
            sys.exit(1)
 
 
def start_listening(site, config, logger):
    from twisted.internet import reactor
    from twisted.internet import protocol as protocol_module
    # allow stomp:// URIs to be parsed by urlparse
    urlparse.uses_netloc.append("stomp")
    # allow test server URIs to be parsed by urlparse
    from orbited.servers import test_servers
    for protocol in test_servers:
        urlparse.uses_netloc.append(protocol)
 
    for addr in config['[listen]']:
        if addr.startswith("stomp"):
            stompConfig = ""
            if " " in addr:
                addr, stompConfig = addr.split(" ",1)
        url = urlparse.urlparse(addr)
        hostname = url.hostname or ''
        if url.scheme == 'stomp':
            logger.info('Listening stomp@%s' % url.port)
            from morbid import get_stomp_factory
            morbid_instance = get_stomp_factory(stompConfig)
            config['morbid_instance'] = morbid_instance
            reactor.listenTCP(url.port, morbid_instance, interface=hostname)
        elif url.scheme == 'http':
            logger.info('Listening http@%s' % url.port)
            reactor.listenTCP(url.port, site, interface=hostname)
        elif url.scheme == 'https':
            from twisted.internet import ssl
            crt = config['[ssl]']['crt']
            key = config['[ssl]']['key']
            try:
                ssl_context = ssl.DefaultOpenSSLContextFactory(key, crt)
            except ImportError:
                raise
            except:
                logger.error("Error opening key or crt file: %s, %s" % (key, crt))
                sys.exit(1)
            logger.info('Listening https@%s (%s, %s)' % (url.port, key, crt))
            reactor.listenSSL(url.port, site, ssl_context, interface=hostname)
        elif url.scheme in test_servers:
            test_factory = protocol_module.ServerFactory()
            test_factory.protocol = test_servers[url.scheme]
            logger.info("Listening %s@%s"%(url.scheme, url.port))
            reactor.listenTCP(url.port, test_factory)
            if url.scheme == 'monitor':
                config['globalVars']['monitoring'] = url.port
        else:
            logger.error("Invalid Listen URI: %s" % addr)
            sys.exit(1)
 
 
def thread_start():
	main()
	from twisted.internet import reactor
	reactor.run(installSignalHandlers=0)
	print "Crashed into Earth's Surface."
 
 
orbited_thread = None
def start():
	print "Starting Orbited..."
	dolores_thread = threading.Thread(target=thread_start)
	dolores_thread.start()
	print "In Orbit."
 
def stop_reactor():
	reactor.stop()
 
def stop():
	reactor.callFromThread(stop_reactor)
 
if __name__ == "__main__":
	start()
	run = True
	while run:
		try:
			time.sleep(5)
		except KeyboardInterrupt:
			from twisted.internet import reactor
			stop()
			run = False

So, What Do I Do?

You should just need to put the windows service script and the OrbitedManager.py script in the same directory, and then run the windows service script from the command line. If you run it with no arguments, I believe it will give you an overview of available options, including the option to install.

I hope someone can find this helpful.

Update: I added some clarification in the middle, discussing high-level about the replacement of Orbited’s start module, and a bit at the end about what to do.

Leave a Reply