comparison foodtruck/sites.py @ 602:c6bc0ef03f82

admin: Better UI for publishing websites. * Support multiple publish targets. * Dedicated UI for publishing. * Some UI polish.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 27 Jan 2016 18:02:25 -0800
parents effbc78b5528
children efc1dc916e7c
comparison
equal deleted inserted replaced
601:effbc78b5528 602:c6bc0ef03f82
4 import shlex 4 import shlex
5 import logging 5 import logging
6 import threading 6 import threading
7 import subprocess 7 import subprocess
8 from piecrust.app import PieCrust 8 from piecrust.app import PieCrust
9 from piecrust.configuration import merge_dicts 9 from piecrust.configuration import merge_dicts, Configuration
10 10
11 11
12 logger = logging.getLogger(__name__) 12 logger = logging.getLogger(__name__)
13 13
14 14
22 22
23 class Site(object): 23 class Site(object):
24 def __init__(self, name, root_dir, config): 24 def __init__(self, name, root_dir, config):
25 self.name = name 25 self.name = name
26 self.root_dir = root_dir 26 self.root_dir = root_dir
27 self.config = config 27 self.config = Configuration(values=config.get('sites/%s' % name, {}))
28 self._global_config = config
28 self._piecrust_app = None 29 self._piecrust_app = None
29 self._scm = None 30 self._scm = None
30 self._bake_thread = None 31 self._publish_thread = None
31 logger.debug("Creating site object for %s" % self.name) 32 logger.debug("Creating site object for %s" % self.name)
32 33
33 @property 34 @property
34 def piecrust_app(self): 35 def piecrust_app(self):
35 if self._piecrust_app is None: 36 if self._piecrust_app is None:
39 return self._piecrust_app 40 return self._piecrust_app
40 41
41 @property 42 @property
42 def scm(self): 43 def scm(self):
43 if self._scm is None: 44 if self._scm is None:
44 cfg = None 45 cfg = copy.deepcopy(self._global_config.get('scm', {}))
45 scm_cfg = self.config.get('sites/%s/scm' % self.name) 46 merge_dicts(cfg, self.config.get('scm', {}))
46 global_scm_cfg = self.config.get('scm')
47 if scm_cfg:
48 if global_scm_cfg:
49 cfg = copy.deepcopy(global_scm_cfg)
50 merge_dicts(cfg, scm_cfg)
51 else:
52 cfg = copy.deepcopy(scm_cfg)
53 elif global_scm_cfg:
54 cfg = copy.deepcopy(global_scm_cfg)
55 47
56 if not cfg or 'type' not in cfg: 48 if os.path.isdir(os.path.join(self.root_dir, '.hg')):
57 raise Exception("No SCM available for site: %s" % self.name)
58
59 if cfg['type'] == 'hg':
60 from .scm.mercurial import MercurialSourceControl 49 from .scm.mercurial import MercurialSourceControl
61 self._scm = MercurialSourceControl(self.root_dir, cfg) 50 self._scm = MercurialSourceControl(self.root_dir, cfg)
51 elif os.path.isdir(os.path.join(self.root_dir, '.git')):
52 from .scm.git import GitSourceControl
53 self._scm = GitSourceControl(self.root_dir, cfg)
62 else: 54 else:
63 raise NotImplementedError() 55 self._scm = False
64 56
65 return self._scm 57 return self._scm
66 58
67 @property 59 @property
68 def is_bake_running(self): 60 def is_publish_running(self):
69 return self._bake_thread is not None and self._bake_thread.is_alive() 61 return (self._publish_thread is not None and
62 self._publish_thread.is_alive())
70 63
71 @property 64 @property
72 def bake_thread(self): 65 def publish_thread(self):
73 return self._bake_thread 66 return self._publish_thread
74 67
75 def bake(self): 68 def publish(self, target):
76 bake_cmd = self.config.get('triggers/bake') 69 target_cfg = self.config.get('publish/%s' % target)
77 bake_args = shlex.split(bake_cmd) 70 if not target_cfg:
71 raise Exception("No such publish target: %s" % target)
78 72
79 logger.debug("Running bake: %s" % bake_args) 73 target_cmd = target_cfg.get('cmd')
80 proc = subprocess.Popen(bake_args, cwd=self.root_dir, 74 if not target_cmd:
75 raise Exception("No command specified for publish target: %s" %
76 target)
77 publish_args = shlex.split(target_cmd)
78
79 logger.debug(
80 "Executing publish target '%s': %s" % (target, publish_args))
81 proc = subprocess.Popen(publish_args, cwd=self.root_dir,
81 stdout=subprocess.PIPE, 82 stdout=subprocess.PIPE,
82 stderr=subprocess.PIPE) 83 stderr=subprocess.PIPE)
83 84
84 pid_file_path = os.path.join(self.root_dir, 'foodtruck_bake.pid') 85 pid_file_path = os.path.join(self.root_dir, '.ft_pub.pid')
85 with open(pid_file_path, 'w') as fp: 86 with open(pid_file_path, 'w') as fp:
86 fp.write(str(proc.pid)) 87 fp.write(str(proc.pid))
87 88
88 logger.debug("Running bake monitor for PID %d" % proc.pid) 89 logger.debug("Running publishing monitor for PID %d" % proc.pid)
89 self._bake_thread = _BakeThread(self.name, self.root_dir, proc, 90 self._publish_thread = _PublishThread(
90 self._onBakeEnd) 91 self.name, self.root_dir, proc, self._onPublishEnd)
91 self._bake_thread.start() 92 self._publish_thread.start()
92 93
93 def _onBakeEnd(self): 94 def _onPublishEnd(self):
94 os.unlink(os.path.join(self.root_dir, 'foodtruck_bake.pid')) 95 os.unlink(os.path.join(self.root_dir, '.ft_pub.pid'))
95 self._bake_thread = None 96 self._publish_thread = None
96 97
97 98
98 class _BakeThread(threading.Thread): 99 class _PublishThread(threading.Thread):
99 def __init__(self, sitename, siteroot, proc, callback): 100 def __init__(self, sitename, siteroot, proc, callback):
100 super(_BakeThread, self).__init__( 101 super(_PublishThread, self).__init__(
101 name='%s_bake' % sitename, daemon=True) 102 name='%s_publish' % sitename, daemon=True)
102 self.sitename = sitename 103 self.sitename = sitename
103 self.siteroot = siteroot 104 self.siteroot = siteroot
104 self.proc = proc 105 self.proc = proc
105 self.callback = callback 106 self.callback = callback
106 107
107 log_file_path = os.path.join(self.siteroot, 'foodtruck_bake.log') 108 log_file_path = os.path.join(self.siteroot, '.ft_pub.log')
108 self.log_fp = open(log_file_path, 'w', encoding='utf8') 109 self.log_fp = open(log_file_path, 'w', encoding='utf8')
109 110
110 def run(self): 111 def run(self):
111 for line in self.proc.stdout: 112 for line in self.proc.stdout:
112 self.log_fp.write(line.decode('utf8')) 113 self.log_fp.write(line.decode('utf8'))
113 for line in self.proc.stderr: 114 for line in self.proc.stderr:
114 self.log_fp.write(line.decode('utf8')) 115 self.log_fp.write(line.decode('utf8'))
115 self.proc.communicate() 116 self.proc.communicate()
116 if self.proc.returncode != 0: 117 if self.proc.returncode != 0:
117 self.log_fp.write("Error, bake process returned code %d" % 118 self.log_fp.write("Error, publish process returned code %d" %
118 self.proc.returncode) 119 self.proc.returncode)
119 self.log_fp.close() 120 self.log_fp.close()
120 121
121 logger.debug("Bake ended for %s." % self.sitename) 122 logger.debug("Publish ended for %s." % self.sitename)
122 self.callback() 123 self.callback()
123 124
124 125
125 class FoodTruckSites(): 126 class FoodTruckSites():
126 def __init__(self, config, current_site): 127 def __init__(self, config, current_site):