# HG changeset patch # User Ludovic Chabant # Date 1453946545 28800 # Node ID c6bc0ef03f82bd462469a2ad8c5a9fce3bb0c8e8 # Parent effbc78b5528973114d204b3541f2713a33af78f admin: Better UI for publishing websites. * Support multiple publish targets. * Dedicated UI for publishing. * Some UI polish. diff -r effbc78b5528 -r c6bc0ef03f82 foodtruck/main.py --- a/foodtruck/main.py Sun Jan 24 10:42:33 2016 -0800 +++ b/foodtruck/main.py Wed Jan 27 18:02:25 2016 -0800 @@ -16,8 +16,8 @@ # This is needed for Werkzeug's code reloader to be able to correctly # shutdown the child process in order to restart it (otherwise, SSE # generators will keep it alive). - from .views import baking + from . import pubutil logger.debug("Shutting down SSE generators from main...") - baking.server_shutdown = True + pubutil.server_shutdown = True raise diff -r effbc78b5528 -r c6bc0ef03f82 foodtruck/pubutil.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/foodtruck/pubutil.py Wed Jan 27 18:02:25 2016 -0800 @@ -0,0 +1,93 @@ +import os +import os.path +import time +import signal +import logging +from .web import app + + +logger = logging.getLogger(__name__) + +server_shutdown = False + + +def _shutdown_server_and_raise_sigint(): + if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true': + # This is needed when hitting CTRL+C to shutdown the Werkzeug server, + # otherwise SSE generators will keep it alive. + logger.debug("Shutting down SSE generators...") + global server_shutdown + server_shutdown = True + raise KeyboardInterrupt() + + +if app.config['FOODTRUCK_CMDLINE_MODE']: + # Make sure CTRL+C works correctly. + signal.signal(signal.SIGINT, + lambda *args: _shutdown_server_and_raise_sigint()) + + +class PublishLogReader(object): + _pub_max_time = 10 * 60 # Don't bother about pubs older than 10mins. + _poll_interval = 2 # Check the PID file every 2 seconds. + _ping_interval = 30 # Send a ping message every 30 seconds. + + def __init__(self, pid_path, log_path): + self.pid_path = pid_path + self.log_path = log_path + self._pub_pid_mtime = 0 + self._last_seek = 0 + self._last_ping_time = 0 + + def run(self): + logger.debug("Opening publish log...") + + try: + while not server_shutdown: + # PING! + interval = time.time() - self._last_ping_time + if interval > self._ping_interval: + logger.debug("Sending ping...") + self._last_ping_time = time.time() + yield bytes("event: ping\ndata: 1\n\n", 'utf8') + + # Check pid file. + prev_mtime = self._pub_pid_mtime + try: + self._pub_pid_mtime = os.path.getmtime(self.pid_path) + if time.time() - self._pub_pid_mtime > \ + self._pub_max_time: + self._pub_pid_mtime = 0 + except OSError: + self._pub_pid_mtime = 0 + + # Send data. + new_data = None + if self._pub_pid_mtime > 0 or prev_mtime > 0: + if self._last_seek == 0: + outstr = 'event: message\ndata: Publish started.\n\n' + yield bytes(outstr, 'utf8') + + try: + with open(self.log_path, 'r', encoding='utf8') as fp: + fp.seek(self._last_seek) + new_data = fp.read() + self._last_seek = fp.tell() + except OSError: + pass + if self._pub_pid_mtime == 0: + self._last_seek = 0 + + if new_data: + logger.debug("SSE: %s" % outstr) + for line in new_data.split('\n'): + outstr = 'event: message\ndata: %s\n\n' % line + yield bytes(outstr, 'utf8') + + time.sleep(self._poll_interval) + + except GeneratorExit: + pass + + logger.debug("Closing publish log...") + diff -r effbc78b5528 -r c6bc0ef03f82 foodtruck/sites.py --- a/foodtruck/sites.py Sun Jan 24 10:42:33 2016 -0800 +++ b/foodtruck/sites.py Wed Jan 27 18:02:25 2016 -0800 @@ -6,7 +6,7 @@ import threading import subprocess from piecrust.app import PieCrust -from piecrust.configuration import merge_dicts +from piecrust.configuration import merge_dicts, Configuration logger = logging.getLogger(__name__) @@ -24,10 +24,11 @@ def __init__(self, name, root_dir, config): self.name = name self.root_dir = root_dir - self.config = config + self.config = Configuration(values=config.get('sites/%s' % name, {})) + self._global_config = config self._piecrust_app = None self._scm = None - self._bake_thread = None + self._publish_thread = None logger.debug("Creating site object for %s" % self.name) @property @@ -41,70 +42,70 @@ @property def scm(self): if self._scm is None: - cfg = None - scm_cfg = self.config.get('sites/%s/scm' % self.name) - global_scm_cfg = self.config.get('scm') - if scm_cfg: - if global_scm_cfg: - cfg = copy.deepcopy(global_scm_cfg) - merge_dicts(cfg, scm_cfg) - else: - cfg = copy.deepcopy(scm_cfg) - elif global_scm_cfg: - cfg = copy.deepcopy(global_scm_cfg) + cfg = copy.deepcopy(self._global_config.get('scm', {})) + merge_dicts(cfg, self.config.get('scm', {})) - if not cfg or 'type' not in cfg: - raise Exception("No SCM available for site: %s" % self.name) - - if cfg['type'] == 'hg': + if os.path.isdir(os.path.join(self.root_dir, '.hg')): from .scm.mercurial import MercurialSourceControl self._scm = MercurialSourceControl(self.root_dir, cfg) + elif os.path.isdir(os.path.join(self.root_dir, '.git')): + from .scm.git import GitSourceControl + self._scm = GitSourceControl(self.root_dir, cfg) else: - raise NotImplementedError() + self._scm = False return self._scm @property - def is_bake_running(self): - return self._bake_thread is not None and self._bake_thread.is_alive() + def is_publish_running(self): + return (self._publish_thread is not None and + self._publish_thread.is_alive()) @property - def bake_thread(self): - return self._bake_thread + def publish_thread(self): + return self._publish_thread + + def publish(self, target): + target_cfg = self.config.get('publish/%s' % target) + if not target_cfg: + raise Exception("No such publish target: %s" % target) - def bake(self): - bake_cmd = self.config.get('triggers/bake') - bake_args = shlex.split(bake_cmd) + target_cmd = target_cfg.get('cmd') + if not target_cmd: + raise Exception("No command specified for publish target: %s" % + target) + publish_args = shlex.split(target_cmd) - logger.debug("Running bake: %s" % bake_args) - proc = subprocess.Popen(bake_args, cwd=self.root_dir, + logger.debug( + "Executing publish target '%s': %s" % (target, publish_args)) + proc = subprocess.Popen(publish_args, cwd=self.root_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - pid_file_path = os.path.join(self.root_dir, 'foodtruck_bake.pid') + pid_file_path = os.path.join(self.root_dir, '.ft_pub.pid') with open(pid_file_path, 'w') as fp: fp.write(str(proc.pid)) - logger.debug("Running bake monitor for PID %d" % proc.pid) - self._bake_thread = _BakeThread(self.name, self.root_dir, proc, - self._onBakeEnd) - self._bake_thread.start() + logger.debug("Running publishing monitor for PID %d" % proc.pid) + self._publish_thread = _PublishThread( + self.name, self.root_dir, proc, self._onPublishEnd) + self._publish_thread.start() - def _onBakeEnd(self): - os.unlink(os.path.join(self.root_dir, 'foodtruck_bake.pid')) - self._bake_thread = None + def _onPublishEnd(self): + os.unlink(os.path.join(self.root_dir, '.ft_pub.pid')) + self._publish_thread = None -class _BakeThread(threading.Thread): +class _PublishThread(threading.Thread): def __init__(self, sitename, siteroot, proc, callback): - super(_BakeThread, self).__init__( - name='%s_bake' % sitename, daemon=True) + super(_PublishThread, self).__init__( + name='%s_publish' % sitename, daemon=True) self.sitename = sitename self.siteroot = siteroot self.proc = proc self.callback = callback - log_file_path = os.path.join(self.siteroot, 'foodtruck_bake.log') + log_file_path = os.path.join(self.siteroot, '.ft_pub.log') self.log_fp = open(log_file_path, 'w', encoding='utf8') def run(self): @@ -114,11 +115,11 @@ self.log_fp.write(line.decode('utf8')) self.proc.communicate() if self.proc.returncode != 0: - self.log_fp.write("Error, bake process returned code %d" % + self.log_fp.write("Error, publish process returned code %d" % self.proc.returncode) self.log_fp.close() - logger.debug("Bake ended for %s." % self.sitename) + logger.debug("Publish ended for %s." % self.sitename) self.callback() diff -r effbc78b5528 -r c6bc0ef03f82 foodtruck/templates/dashboard.html --- a/foodtruck/templates/dashboard.html Sun Jan 24 10:42:33 2016 -0800 +++ b/foodtruck/templates/dashboard.html Wed Jan 27 18:02:25 2016 -0800 @@ -8,7 +8,9 @@
{% for site in sites %} {% if site.name != site_name %} - + {% endif %} {% endfor %}
@@ -55,15 +57,6 @@ -
-
-

Publishing

-
- -
-
-
{% endblock %} diff -r effbc78b5528 -r c6bc0ef03f82 foodtruck/templates/error.html --- a/foodtruck/templates/error.html Sun Jan 24 10:42:33 2016 -0800 +++ b/foodtruck/templates/error.html Wed Jan 27 18:02:25 2016 -0800 @@ -4,9 +4,7 @@ {% block content %}
- {% if error == 'invalid_site' %} -

There was an error with your configuration file: {{exception}}

- {% endif %} +

{{error}}

{% endblock %} diff -r effbc78b5528 -r c6bc0ef03f82 foodtruck/templates/layouts/master.html --- a/foodtruck/templates/layouts/master.html Sun Jan 24 10:42:33 2016 -0800 +++ b/foodtruck/templates/layouts/master.html Wed Jan 27 18:02:25 2016 -0800 @@ -21,7 +21,7 @@ {% block content %}{% endblock %} {% block after_content %}{% endblock %} -
+