1
# -*- coding: utf-8 -*-
3
# Author: Alejandro J. Cura <alecu@canonical.com>
5
# Copyright 2011 Canonical Ltd.
7
# This program is free software: you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License version 3, as published
9
# by the Free Software Foundation.
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranties of
13
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14
# PURPOSE. See the GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License along
17
# with this program. If not, see <http://www.gnu.org/licenses/>.
19
"""tcpactivation: start a process if nothing listening in a given port."""
23
from twisted.internet import defer, protocol, reactor
25
LOCALHOST = "127.0.0.1"
26
DELAY_BETWEEN_CHECKS = 0.1
27
NUMBER_OF_CHECKS = 600
29
# twisted uses a different coding convention
30
# pylint: disable=C0103,W0232
33
def async_sleep(delay):
34
"""Fire the returned deferred after some specified delay."""
36
# pylint: disable=E1101
37
reactor.callLater(delay, d.callback, None)
41
class AlreadyStartedError(Exception):
42
"""The instance was already started."""
45
class ActivationTimeoutError(Exception):
46
"""Timeout while trying to start the instance."""
49
class NullProtocol(protocol.Protocol):
50
"""A protocol that drops the connection."""
52
def connectionMade(self):
53
"""Just drop the connection."""
54
self.transport.loseConnection()
57
class PortDetectFactory(protocol.ClientFactory):
58
"""Will detect if something is listening in a given port."""
61
"""Initialize this instance."""
62
self.d = defer.Deferred()
64
def is_listening(self):
65
"""A deferred that will become True if something is listening."""
68
def buildProtocol(self, addr):
74
def clientConnectionLost(self, connector, reason):
75
"""The connection was lost."""
77
self.d.callback(False)
79
def clientConnectionFailed(self, connector, reason):
80
"""The connection failed."""
82
self.d.callback(False)
85
class ActivationConfig(object):
86
"""The configuration for tcp activation."""
88
def __init__(self, service_name, command_line, port):
89
"""Initialize this instance."""
90
self.service_name = service_name
91
self.command_line = command_line
95
class ActivationDetector(object):
96
"""Base class to detect if the service is running."""
98
def __init__(self, config):
99
"""Initialize this instance."""
102
@defer.inlineCallbacks
103
def is_already_running(self):
104
"""Check if the instance is already running."""
105
factory = PortDetectFactory()
106
# pylint: disable=E1101
107
reactor.connectTCP(LOCALHOST, self.config.port, factory)
108
result = yield factory.is_listening()
109
defer.returnValue(result)
112
class ActivationClient(ActivationDetector):
113
"""A client for tcp activation."""
115
# a classwide lock, so the server is started only once
116
lock = defer.DeferredLock()
118
@defer.inlineCallbacks
119
def _wait_server_active(self):
120
"""Wait till the server is active."""
121
for _ in xrange(NUMBER_OF_CHECKS):
122
is_running = yield self.is_already_running()
124
defer.returnValue(None)
125
yield async_sleep(DELAY_BETWEEN_CHECKS)
126
raise ActivationTimeoutError()
128
def _spawn_server(self):
129
"""Start running the server process."""
130
# Without using close_fds=True, strange things happen
131
# with logging on windows. More information at
132
# http://bugs.python.org/issue4749
133
subprocess.Popen(self.config.command_line, close_fds=True)
135
@defer.inlineCallbacks
136
def _do_get_active_port(self):
137
"""Get the port for the running instance, starting it if needed."""
138
is_running = yield self.is_already_running()
141
yield self._wait_server_active()
142
defer.returnValue(self.config.port)
144
@defer.inlineCallbacks
145
def get_active_port(self):
146
"""Serialize the requests to _do_get_active_port."""
147
yield self.lock.acquire()
149
result = yield self._do_get_active_port()
150
defer.returnValue(result)
155
class ActivationInstance(ActivationDetector):
156
"""A tcp activation server instance."""
158
@defer.inlineCallbacks
160
"""Get the port to run this service or fail if already started."""
161
is_running = yield self.is_already_running()
163
raise AlreadyStartedError()
164
defer.returnValue(self.config.port)