25
25
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
26
27
27
"""Start test server."""
28
-
28
+ import atexit
29
29
import os
30
30
import sys
31
31
import re
32
32
import signal
33
33
import socket
34
+ import threading
34
35
import webbrowser
35
36
from http .server import HTTPServer
36
37
from http .server import SimpleHTTPRequestHandler
37
38
from io import BytesIO as StringIO
39
+ from threading import Thread , current_thread
40
+ from typing import Callable , Optional
38
41
39
42
from nikola .plugin_categories import Command
40
- from nikola .utils import dns_sd
43
+ from nikola .utils import base_path_from_siteuri , dns_sd
41
44
42
45
43
46
class IPv6Server (HTTPServer ):
@@ -52,7 +55,8 @@ class CommandServe(Command):
52
55
name = "serve"
53
56
doc_usage = "[options]"
54
57
doc_purpose = "start the test webserver"
55
- dns_sd = None
58
+ httpd : Optional [HTTPServer ] = None
59
+ httpd_serving_thread : Optional [Thread ] = None
56
60
57
61
cmd_options = (
58
62
{
@@ -98,13 +102,21 @@ class CommandServe(Command):
98
102
)
99
103
100
104
def shutdown (self , signum = None , _frame = None ):
101
- """Shut down the server that is running detached."""
102
- if self .dns_sd :
103
- self .dns_sd .Reset ()
105
+ """Shut down the server."""
104
106
if os .path .exists (self .serve_pidfile ):
105
107
os .remove (self .serve_pidfile )
106
- if not self .detached :
107
- self .logger .info ("Server is shutting down." )
108
+
109
+ # Deal with the non-detached state:
110
+ if self .httpd is not None and self .httpd_serving_thread is not None and self .httpd_serving_thread != current_thread ():
111
+ shut_me_down = self .httpd
112
+ self .httpd = None
113
+ self .httpd_serving_thread = None
114
+ self .logger .info ("Web server is shutting down." )
115
+ shut_me_down .shutdown ()
116
+ else :
117
+ self .logger .debug ("No need to shut down the web server." )
118
+
119
+ # If this was called as a signal handler, shut down the entire application:
108
120
if signum :
109
121
sys .exit (0 )
110
122
@@ -127,29 +139,33 @@ def _execute(self, options, args):
127
139
ipv6 = False
128
140
OurHTTP = HTTPServer
129
141
130
- httpd = OurHTTP ((options ['address' ], options ['port' ]),
131
- OurHTTPRequestHandler )
132
- sa = httpd .socket .getsockname ()
142
+ base_path = base_path_from_siteuri (self .site .config ['BASE_URL' ])
143
+ if base_path == "" :
144
+ handler_factory = OurHTTPRequestHandler
145
+ else :
146
+ handler_factory = _create_RequestHandler_removing_basepath (base_path )
147
+ self .httpd = OurHTTP ((options ['address' ], options ['port' ]), handler_factory )
148
+
149
+ sa = self .httpd .socket .getsockname ()
133
150
if ipv6 :
134
- server_url = "http://[{0}]:{1}/" .format (* sa )
151
+ server_url = "http://[{0}]:{1}/" .format (* sa ) + base_path
135
152
else :
136
- server_url = "http://{0}:{1}/" .format (* sa )
153
+ server_url = "http://{0}:{1}/" .format (* sa ) + base_path
137
154
self .logger .info ("Serving on {0} ..." .format (server_url ))
138
155
139
156
if options ['browser' ]:
140
157
# Some browsers fail to load 0.0.0.0 (Issue #2755)
141
158
if sa [0 ] == '0.0.0.0' :
142
- server_url = "http://127.0.0.1:{1}/" .format (* sa )
159
+ server_url = "http://127.0.0.1:{1}/" .format (* sa ) + base_path
143
160
self .logger .info ("Opening {0} in the default web browser..." .format (server_url ))
144
161
webbrowser .open (server_url )
145
162
if options ['detach' ]:
146
- self .detached = True
147
163
OurHTTPRequestHandler .quiet = True
148
164
try :
149
165
pid = os .fork ()
150
166
if pid == 0 :
151
167
signal .signal (signal .SIGTERM , self .shutdown )
152
- httpd .serve_forever ()
168
+ self . httpd .serve_forever ()
153
169
else :
154
170
with open (self .serve_pidfile , 'w' ) as fh :
155
171
fh .write ('{0}\n ' .format (pid ))
@@ -160,11 +176,26 @@ def _execute(self, options, args):
160
176
else :
161
177
raise
162
178
else :
163
- self .detached = False
164
179
try :
165
- self .dns_sd = dns_sd (options ['port' ], (options ['ipv6' ] or '::' in options ['address' ]))
166
- signal .signal (signal .SIGTERM , self .shutdown )
167
- httpd .serve_forever ()
180
+ dns_socket_publication = dns_sd (options ['port' ], (options ['ipv6' ] or '::' in options ['address' ]))
181
+ try :
182
+ self .httpd_serving_thread = threading .current_thread ()
183
+ if threading .main_thread () == self .httpd_serving_thread :
184
+ # If we are running as the main thread,
185
+ # likely no other threads are running and nothing else will run after us.
186
+ # In this special case, we take some responsibility for the application whole
187
+ # (not really the job of any single plugin).
188
+ # Clean up the socket publication on exit (if we actually had a socket publication):
189
+ if dns_socket_publication is not None :
190
+ atexit .register (dns_socket_publication .Reset )
191
+ # Enable application shutdown via SIGTERM:
192
+ signal .signal (signal .SIGTERM , self .shutdown )
193
+ self .logger .info ("Starting web server." )
194
+ self .httpd .serve_forever ()
195
+ self .logger .info ("Web server has shut down." )
196
+ finally :
197
+ if dns_socket_publication is not None :
198
+ dns_socket_publication .Reset ()
168
199
except KeyboardInterrupt :
169
200
self .shutdown ()
170
201
return 130
@@ -186,7 +217,7 @@ def log_message(self, *args):
186
217
187
218
# NOTICE: this is a patched version of send_head() to disable all sorts of
188
219
# caching. `nikola serve` is a development server, hence caching should
189
- # not happen to have access to the newest resources.
220
+ # not happen, instead, we should give access to the newest resources.
190
221
#
191
222
# The original code was copy-pasted from Python 2.7. Python 3.3 contains
192
223
# the same code, missing the binary mode comment.
@@ -205,6 +236,7 @@ def send_head(self):
205
236
206
237
"""
207
238
path = self .translate_path (self .path )
239
+
208
240
f = None
209
241
if os .path .isdir (path ):
210
242
path_parts = list (self .path .partition ('?' ))
@@ -277,3 +309,29 @@ def send_head(self):
277
309
# end no-cache patch
278
310
self .end_headers ()
279
311
return f
312
+
313
+
314
+ def _omit_basepath_component (base_path_with_slash : str , path : str ) -> str :
315
+ if path .startswith (base_path_with_slash ):
316
+ return path [len (base_path_with_slash ) - 1 :]
317
+ elif path == base_path_with_slash [:- 1 ]:
318
+ return "/"
319
+ else :
320
+ # Somewhat dubious. We should not really get asked this, normally.
321
+ return path
322
+
323
+
324
+ def _create_RequestHandler_removing_basepath (base_path : str ) -> Callable :
325
+ """Create a new subclass of OurHTTPRequestHandler that removes a trailing base path from the path.
326
+
327
+ Returns that class (used as a factory for objects).
328
+ Better return type should be Callable[[...], OurHTTPRequestHandler], but Python 3.8 doesn't understand that.
329
+ """
330
+ base_path_with_slash = base_path if base_path .endswith ("/" ) else f"{ base_path } /"
331
+
332
+ class OmitBasepathRequestHandler (OurHTTPRequestHandler ):
333
+
334
+ def translate_path (self , path : str ) -> str :
335
+ return super ().translate_path (_omit_basepath_component (base_path_with_slash , path ))
336
+
337
+ return OmitBasepathRequestHandler
0 commit comments