comparison piecrust/serving/server.py @ 552:9612cfc6455a

serve: Rewrite of the Server-Sent Event code for build notifications. At the moment the server monitors the asset directories, and notifies the browser when an asset has changed and has been re-processed. * Fix issues around long-running requests/threads which mess up the ability to shutdown the server correctly with `CTRL-C` (see comments in code). * Move the notification queue to each SSE producer, to support having multiple pages open in a browser. * Add JS/CSS for showing quick notifications about re-processed assets. * Add support for hot-reloading CSS and pictures that have been re-processed.
author Ludovic Chabant <ludovic@chabant.com>
date Sat, 08 Aug 2015 16:12:04 -0700
parents f2b875ecc940
children cc6f3dbe3048
comparison
equal deleted inserted replaced
551:f2b875ecc940 552:9612cfc6455a
108 wsgi_wrapper = WsgiServerWrapper(self) 108 wsgi_wrapper = WsgiServerWrapper(self)
109 return wsgi_wrapper 109 return wsgi_wrapper
110 110
111 def _run_request(self, environ, start_response): 111 def _run_request(self, environ, start_response):
112 try: 112 try:
113 return self._try_run_request(environ, start_response) 113 response = self._try_run_request(environ)
114 if isinstance(response, tuple):
115 response, close_func = response
116 return ClosingIterator(response(environ, start_response),
117 [close_func])
118 else:
119 return response(environ, start_response)
114 except Exception as ex: 120 except Exception as ex:
115 if self.debug: 121 if self.debug:
116 raise 122 raise
117 return self._handle_error(ex, environ, start_response) 123 return self._handle_error(ex, environ, start_response)
118 124
119 def _try_run_request(self, environ, start_response): 125 def _try_run_request(self, environ):
120 request = Request(environ) 126 request = Request(environ)
121 127
122 # We don't support anything else than GET requests since we're 128 # We don't support anything else than GET requests since we're
123 # previewing something that will be static later. 129 # previewing something that will be static later.
124 if self.static_preview and request.method != 'GET': 130 if self.static_preview and request.method != 'GET':
127 raise MethodNotAllowed() 133 raise MethodNotAllowed()
128 134
129 # Handle special requests right away. 135 # Handle special requests right away.
130 response = self._try_special_request(environ, request) 136 response = self._try_special_request(environ, request)
131 if response is not None: 137 if response is not None:
132 return response(environ, start_response) 138 return response
133 139
134 # Also handle requests to a pipeline-built asset right away. 140 # Also handle requests to a pipeline-built asset right away.
135 response = self._try_serve_asset(environ, request) 141 response = self._try_serve_asset(environ, request)
136 if response is not None: 142 if response is not None:
137 return response(environ, start_response) 143 return response
138 144
139 # Create the app for this request. 145 # Create the app for this request.
140 app = PieCrust(root_dir=self.root_dir, debug=self.debug) 146 app = PieCrust(root_dir=self.root_dir, debug=self.debug)
141 if self.sub_cache_dir: 147 if self.sub_cache_dir:
142 app._useSubCacheDir(self.sub_cache_dir) 148 app._useSubCacheDir(self.sub_cache_dir)
151 app.env.base_asset_url_format = '/_asset/%path%' 157 app.env.base_asset_url_format = '/_asset/%path%'
152 158
153 # Let's see if it can be a page asset. 159 # Let's see if it can be a page asset.
154 response = self._try_serve_page_asset(app, environ, request) 160 response = self._try_serve_page_asset(app, environ, request)
155 if response is not None: 161 if response is not None:
156 return response(environ, start_response) 162 return response
157 163
158 # Nope. Let's see if it's an actual page. 164 # Nope. Let's see if it's an actual page.
159 try: 165 try:
160 response = self._try_serve_page(app, environ, request) 166 response = self._try_serve_page(app, environ, request)
161 return response(environ, start_response) 167 return response
162 except (RouteNotFoundError, SourceNotFoundError) as ex: 168 except (RouteNotFoundError, SourceNotFoundError) as ex:
163 raise NotFound() from ex 169 raise NotFound() from ex
164 except HTTPException: 170 except HTTPException:
165 raise 171 raise
166 except Exception as ex: 172 except Exception as ex:
185 pass 191 pass
186 192
187 debug_mount = '/__piecrust_debug/' 193 debug_mount = '/__piecrust_debug/'
188 if request.path.startswith(debug_mount): 194 if request.path.startswith(debug_mount):
189 rel_req_path = request.path[len(debug_mount):] 195 rel_req_path = request.path[len(debug_mount):]
196 if rel_req_path == 'werkzeug_shutdown':
197 shutdown_func = environ.get('werkzeug.server.shutdown')
198 if shutdown_func is None:
199 raise RuntimeError('Not running with the Werkzeug Server')
200 shutdown_func()
201 return Response("Server shutting down...")
202
190 if rel_req_path == 'pipeline_status': 203 if rel_req_path == 'pipeline_status':
191 from piecrust.serving.procloop import ( 204 from piecrust.serving.procloop import (
192 PipelineStatusServerSideEventProducer) 205 PipelineStatusServerSentEventProducer)
193 provider = PipelineStatusServerSideEventProducer( 206 provider = PipelineStatusServerSentEventProducer(
194 self._proc_loop.status_queue) 207 self._proc_loop)
195 it = ClosingIterator(provider.run(), [provider.close]) 208 it = provider.run()
196 response = Response(it) 209 response = Response(it, mimetype='text/event-stream')
197 response.headers['Cache-Control'] = 'no-cache' 210 response.headers['Cache-Control'] = 'no-cache'
198 if 'text/event-stream' in request.accept_mimetypes: 211 response.headers['Last-Event-ID'] = \
199 response.mimetype = 'text/event-stream' 212 self._proc_loop.last_status_id
200 response.direct_passthrough = True 213 return response, provider.close
201 response.implicit_sequence_conversion = False
202 return response
203 214
204 return None 215 return None
205 216
206 def _try_serve_asset(self, environ, request): 217 def _try_serve_asset(self, environ, request):
207 rel_req_path = request.path.lstrip('/').replace('/', os.sep) 218 rel_req_path = request.path.lstrip('/').replace('/', os.sep)