diff options
-rw-r--r-- | gitautodeploy/cli/config.py | 4 | ||||
-rw-r--r-- | gitautodeploy/gitautodeploy.py | 182 | ||||
-rw-r--r-- | gitautodeploy/httpserver.py | 9 | ||||
-rw-r--r-- | gitautodeploy/wsserver.py | 81 | ||||
-rw-r--r-- | platforms/debian/stdeb/control | 2 |
5 files changed, 167 insertions, 111 deletions
diff --git a/gitautodeploy/cli/config.py b/gitautodeploy/cli/config.py index f87057e..6cd62be 100644 --- a/gitautodeploy/cli/config.py +++ b/gitautodeploy/cli/config.py @@ -28,7 +28,9 @@ def get_config_defaults(): config['web-ui'] = { 'enabled': False, - 'remote-whitelist': ['127.0.0.1'] + 'remote-whitelist': ['127.0.0.1'], + 'ws-host': '0.0.0.0', + 'ws-port': 9000 } return config diff --git a/gitautodeploy/gitautodeploy.py b/gitautodeploy/gitautodeploy.py index c7ebf3a..4d4f838 100644 --- a/gitautodeploy/gitautodeploy.py +++ b/gitautodeploy/gitautodeploy.py @@ -14,17 +14,19 @@ class LogInterface(object): def flush(self): pass +from .wsserver import WebSocketClientHandlerFactory +from .httpserver import WebhookRequestHandlerFactory + class GitAutoDeploy(object): _instance = None _http_server = None - _ws_server = None _config = {} - _port = None _pid = None _event_store = None _default_stdout = None _default_stderr = None _startup_event = None + _ws_clients = [] def __new__(cls, *args, **kwargs): """Overload constructor to enable singleton access""" @@ -40,7 +42,8 @@ class GitAutoDeploy(object): self._event_store = EventStore() self._event_store.register_observer(self) - # Create a startup event that can hold status and any error messages from the startup process + # Create a startup event that can hold status and any error messages + # from the startup process self._startup_event = StartupEvent() self._event_store.register_action(self._startup_event) @@ -53,7 +56,7 @@ class GitAutoDeploy(object): from .wrappers import GitWrapper logger = logging.getLogger() - if not 'repositories' in self._config: + if 'repositories' not in self._config: return # Iterate over all configured repositories @@ -82,8 +85,8 @@ class GitAutoDeploy(object): logger = logging.getLogger() for repository in self._config['repositories']: - - if not 'url' in repository: + + if 'url' not in repository: continue logger.info("Scanning repository: %s" % repository['url']) @@ -116,7 +119,8 @@ class GitAutoDeploy(object): try: os.remove(self._config['pidfilepath']) except OSError as e: - if e.errno != errno.ENOENT: # errno.ENOENT = no such file or directory + # errno.ENOENT = no such file or directory + if e.errno != errno.ENOENT: raise @staticmethod @@ -136,7 +140,7 @@ class GitAutoDeploy(object): try: # Spawn second child pid = os.fork() - + except OSError as e: raise Exception("%s [%d]" % (e.strerror, e.errno)) @@ -152,7 +156,11 @@ class GitAutoDeploy(object): return 0 def update(self, *args, **kwargs): - pass + import json + data = self._event_store.dict_repr() + json_data = json.dumps(data).encode('utf-8') + for client in self._ws_clients: + client.notify_refresh(json_data) def setup(self, config): """Setup an instance of GAD based on the provided config object.""" @@ -161,12 +169,6 @@ class GitAutoDeploy(object): import os import logging from .lock import Lock - from .httpserver import WebhookRequestHandlerFactory - - try: - from BaseHTTPServer import HTTPServer - except ImportError as e: - from http.server import HTTPServer # This solves https://github.com/olipo186/Git-Auto-Deploy/issues/118 try: @@ -231,8 +233,6 @@ class GitAutoDeploy(object): if 'daemon-mode' in self._config and self._config['daemon-mode']: self._startup_event.log_info('Starting Git Auto Deploy in daemon mode') GitAutoDeploy.create_daemon() - else: - self._startup_event.log_info('Git Auto Deploy started') self._pid = os.getpid() self.create_pid_file() @@ -245,6 +245,27 @@ class GitAutoDeploy(object): Lock(os.path.join(repo_config['path'], 'status_running')).clear() Lock(os.path.join(repo_config['path'], 'status_waiting')).clear() + if 'daemon-mode' not in self._config or not self._config['daemon-mode']: + self._startup_event.log_info('Git Auto Deploy started') + + + def serve_http(self): + """Starts a HTTP server that listens for webhook requests and serves the web ui.""" + import sys + import socket + import logging + import os + from .events import SystemEvent, StartupEvent + + try: + from BaseHTTPServer import HTTPServer + except ImportError as e: + from http.server import HTTPServer + + start_http_event = StartupEvent() + self._event_store.register_action(start_http_event) + + # Setup try: # Create web hook request handler class @@ -255,22 +276,6 @@ class GitAutoDeploy(object): self._config['port']), WebhookRequestHandler) - # Start a web socket server if the web UI is enabled - #if self._config['web-ui']['enabled']: - - # try: - # from SimpleWebSocketServer import SimpleWebSocketServer - # from wsserver import WebSocketClientHandler - - # # Create web socket server - # self._ws_server = SimpleWebSocketServer(self._config['ws-host'], self._config['ws-port'], WebSocketClientHandler) - - # sa = self._ws_server.socket.getsockname() - # self._startup_event.log_info("Listening for web socket on %s port %s" % (sa[0], sa[1])) - - # except ImportError as e: - # self._startup_event.log_error("Unable to start web socket server due to a too old version of Python. Version => 2.7.9 is required.") - # Setup SSL for HTTP server if 'ssl' in self._config and self._config['ssl']: import ssl @@ -279,25 +284,16 @@ class GitAutoDeploy(object): certfile=os.path.expanduser(self._config['ssl-pem']), server_side=True) sa = self._http_server.socket.getsockname() - self._startup_event.log_info("Listening for http connections on %s port %s" % (sa[0], sa[1])) - self._startup_event.address = sa[0] - self._startup_event.port = sa[1] - self._startup_event.notify() - - # Actual port bound to (nessecary when OS picks randomly free port) - self._port = sa[1] + start_http_event.log_info("Listening for http connections on %s port %s" % (sa[0], sa[1])) + start_http_event.address = sa[0] + start_http_event.port = sa[1] + start_http_event.notify() except socket.error as e: - self._startup_event.log_critical("Error on socket: %s" % e) + start_http_event.log_critical("Error on socket: %s" % e) sys.exit(1) - def serve_http(self): - import sys - import socket - import logging - import os - from .events import SystemEvent - + # Run forever try: self._http_server.serve_forever() @@ -306,7 +302,7 @@ class GitAutoDeploy(object): self._event_store.register_action(event) event.log_critical("Error on socket: %s" % e) sys.exit(1) - + except KeyboardInterrupt as e: event = SystemEvent() self._event_store.register_action(event) @@ -317,12 +313,45 @@ class GitAutoDeploy(object): pass def serve_ws(self): - if not self._ws_server: + """Start a web socket server, used by the web UI to get notifications about updates.""" + + from .events import SystemEvent, StartupEvent + start_ws_event = StartupEvent() + self._event_store.register_action(start_ws_event) + + # Start a web socket server if the web UI is enabled + if not self._config['web-ui']['enabled']: return - self._ws_server.serveforever() + + try: + import sys + from autobahn.websocket import WebSocketServerProtocol, WebSocketServerFactory + from twisted.internet import reactor + + # Create a WebSocketClientHandler instance + WebSocketClientHandler = WebSocketClientHandlerFactory(self._config, self._ws_clients, self._event_store) + + uri = u"ws://%s:%s" % (self._config['web-ui']['ws-host'], self._config['web-ui']['ws-port']) + factory = WebSocketServerFactory(uri) + factory.protocol = WebSocketClientHandler + # factory.setProtocolOptions(maxConnections=2) + + # note to self: if using putChild, the child must be bytes... + self._ws_server_port = reactor.listenTCP(self._config['web-ui']['ws-port'], factory) + + start_ws_event.log_info("Listening for web socket connections on %s port %s" % (self._config['web-ui']['ws-host'], self._config['web-ui']['ws-port'])) + start_ws_event.address = self._config['web-ui']['ws-host'] + start_ws_event.port = self._config['web-ui']['ws-port'] + start_ws_event.notify() + + # Serve forever (until reactor.stop()) + reactor.run(installSignalHandlers=False) + + except ImportError: + self._startup_event.log_error("Unable to start web socket server due to missing dependency.") def serve_forever(self): - """Start listening for incoming requests.""" + """Start HTTP and web socket servers.""" import sys import socket import logging @@ -337,45 +366,24 @@ class GitAutoDeploy(object): wwwroot = os.path.join(os.path.dirname(os.path.realpath(__file__)), "wwwroot") os.chdir(wwwroot) + # Start HTTP server t1 = threading.Thread(target=self.serve_http) #t1.daemon = True - t1.start() + # Start web socket server + t2 = threading.Thread(target=self.serve_ws) + #t2.daemon = True - #t2 = threading.Thread(target=self.serve_ws) - #t1.daemon = True - #t2.start() + t1.start() + t2.start() # Wait for thread to finish without blocking main thread - while t1.isAlive: + while t1.is_alive(): t1.join(5) # Wait for thread to finish without blocking main thread - #while t2.isAlive: - # t2.join(5) - - - def handle_request(self): - """Start listening for incoming requests.""" - import sys - import socket - from .events import SystemEvent - - try: - self._http_server.handle_request() - - except socket.error as e: - event = SystemEvent() - self._event_store.register_action(event) - event.log_critical("Error on socket: %s" % e) - sys.exit(1) - - except KeyboardInterrupt as e: - event = SystemEvent() - self._event_store.register_action(event) - event.log_info('Requested close by keyboard interrupt signal') - self.stop() - self.exit() + while t2.is_alive(): + t2.join(5) def signal_handler(self, signum, frame): from .events import SystemEvent @@ -407,12 +415,16 @@ class GitAutoDeploy(object): # Shut down the underlying TCP server self._http_server.shutdown() + # Close the socket self._http_server.socket.close() # Stop web socket server if running - if self._ws_server is not None: - self._ws_server.close() + try: + from twisted.internet import reactor + reactor.callFromThread(reactor.stop) + except ImportError: + pass def exit(self): import sys @@ -428,7 +440,7 @@ class GitAutoDeploy(object): sys.stdout = self._default_stdout sys.stderr = self._default_stderr - sys.exit(0) + #sys.exit(0) def main(): import signal diff --git a/gitautodeploy/httpserver.py b/gitautodeploy/httpserver.py index 5a06498..e830fe6 100644 --- a/gitautodeploy/httpserver.py +++ b/gitautodeploy/httpserver.py @@ -1,4 +1,7 @@ -from .parsers import CodingRequestParser, GitLabCIRequestParser, GitLabRequestParser, GitHubRequestParser, BitBucketRequestParser, GenericRequestParser +from .parsers import CodingRequestParser, GitLabCIRequestParser +from .parsers import GitLabRequestParser, GitHubRequestParser +from .parsers import BitBucketRequestParser, GenericRequestParser + class WebbhookRequestProcessor(object): @@ -70,7 +73,7 @@ class WebbhookRequestProcessor(object): # In case there is no path configured for the repository, no pull will # be made. - if not 'path' in repo_config: + if 'path' not in repo_config: res = GitWrapper.deploy(repo_config) repo_result['deploy'] = res result.append(repo_result) @@ -183,7 +186,7 @@ class WebhookRequestFilter(object): continue # If the filter value is set to True. the filter - # will pass regardless of the actual value + # will pass regardless of the actual value if filter_value == True: continue diff --git a/gitautodeploy/wsserver.py b/gitautodeploy/wsserver.py index 3198c18..05e2b61 100644 --- a/gitautodeploy/wsserver.py +++ b/gitautodeploy/wsserver.py @@ -1,21 +1,60 @@ -from SimpleWebSocketServer import WebSocket - -clients = [] -class WebSocketClientHandler(WebSocket): - - def handleMessage(self): - for client in clients: - if client != self: - client.sendMessage(self.address[0] + u' - ' + self.data) - - def handleConnected(self): - print (self.address, 'connected') - for client in clients: - client.sendMessage(self.address[0] + u' - connected') - clients.append(self) - - def handleClose(self): - clients.remove(self) - print (self.address, 'closed') - for client in clients: - client.sendMessage(self.address[0] + u' - disconnected') +try: + from autobahn.websocket import WebSocketServerProtocol +except ImportError: + WebSocketServerProtocol = object + +def WebSocketClientHandlerFactory(config, clients, event_store): + """Factory method for webhook request handler class""" + + class WebSocketClientHandler(WebSocketServerProtocol, object): + from .events import SystemEvent + + def __init__(self, *args, **kwargs): + self._config = config + self.clients = clients + self.event_store = event_store + import logging + self.logger = logging.getLogger() + super(WebSocketClientHandler, self).__init__(*args, **kwargs) + + def onConnect(self, request): + self.logger.info("Client connecting: {0}".format(request.peer)) + + # Validate the request + if not self._config['web-ui']['enabled'] or not self.peer.host in self._config['web-ui']['remote-whitelist']: + self.sendClose() + logger.info("Unautorized connection attempt from %s" % self.peer.host) + return + + self.clients.append(self) + + def onOpen(self): + self.logger.info("WebSocket connection open.") + + def onMessage(self, payload, isBinary): + self.logger.info("WebSocket connection open.") + if isBinary: + self.logger.info("Binary message received: {0} bytes".format(len(payload))) + else: + self.logger.info("Text message received: {0}".format(payload.decode('utf8'))) + + for client in self.clients: + client.sendMessage(payload, isBinary) + + # echo back message verbatim + self.sendMessage(payload, isBinary) + + def onClose(self, wasClean, code, reason): + self.logger.info("WebSocket connection closed: {0}".format(reason)) + + if self in self.clients: + self.clients.remove(self) + + def notify_refresh(self, payload): + import json + self.sendMessage(json.dumps({ + "event": "refresh", + "payload": payload + })) + + return WebSocketClientHandler diff --git a/platforms/debian/stdeb/control b/platforms/debian/stdeb/control index 2f70137..c7b8e16 100644 --- a/platforms/debian/stdeb/control +++ b/platforms/debian/stdeb/control @@ -2,7 +2,7 @@ Source: git-auto-deploy Maintainer: Oliver Poignant <oliver@poignant.se> Section: python Priority: optional -Build-Depends: python-all (>= 2.6.6-3), debhelper (>= 7), python-setuptools (>= 0.6), git (>= 2), python-lockfile (>= 0.8) +Build-Depends: python-all (>= 2.6.6-3), debhelper (>= 7), python-setuptools (>= 0.6), git (>= 2), python-lockfile (>= 0.8), python-twisted (>= 13.2.0), python-autobahn (>= 0.5.14) Standards-Version: 3.9.1 |