diff --git a/pelican/__init__.py b/pelican/__init__.py index 1af14897..6a5c3e83 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -5,6 +5,7 @@ import six import os import re import sys +import threading import time import logging import argparse @@ -15,6 +16,7 @@ import collections # because logging.setLoggerClass has to be called before logging.getLogger from pelican.log import init +from pelican import server from pelican import signals from pelican.generators import (ArticlesGenerator, PagesGenerator, @@ -152,7 +154,7 @@ class Pelican(object): context = self.settings.copy() # Share these among all the generators and content objects: context['filenames'] = {} # maps source path to Content object or None - context['localsiteurl'] = self.settings['SITEURL'] + context['localsiteurl'] = self.settings['SITEURL'] generators = [ cls( @@ -254,7 +256,7 @@ class Pelican(object): logger.debug('Found writer: %s', writer) else: logger.warning( - '%s writers found, using only first one: %s', + '%s writers found, using only first one: %s', writers_found, writer) return writer(self.output_path, settings=self.settings) @@ -309,6 +311,14 @@ def parse_arguments(): help='Relaunch pelican each time a modification occurs' ' on the content files.') + parser.add_argument('-S', '--serve', dest='serve', + action='store_true', + help='Serve on localhost') + + parser.add_argument('-p', '--port', dest='port', type=int, default=8000, + help='Port to serve on. ' + 'Only used with the -S/--serve option') + parser.add_argument('--relative-urls', dest='relative_paths', action='store_true', help='Use relative urls in output, ' @@ -380,106 +390,112 @@ def get_instance(args): return cls(settings), settings +class Generator(threading.Thread): + + def __init__(self, args): + super(Generator, self).__init__() + self.args = args + self.settings = None + self.pelican = None + self.watchers = None + self.setup() + + def setup(self): + config_file = self.args.settings + if config_file is None and os.path.isfile(DEFAULT_CONFIG_NAME): + config_file = DEFAULT_CONFIG_NAME + + self.settings = read_settings(config_file, + override=get_config(self.args)) + + cls = self.settings['PELICAN_CLASS'] + if isinstance(cls, six.string_types): + module, cls_name = cls.rsplit('.', 1) + module = __import__(module) + cls = getattr(module, cls_name) + + self.pelican = cls(self.settings) + readers = Readers(self.settings) + self.watchers = {'content': folder_watcher(self.pelican.path, + readers.extensions, + self.pelican.ignore_files), + 'theme': folder_watcher(self.pelican.theme, + [''], + self.pelican.ignore_files), + 'settings': file_watcher(self.args.settings)} + + for static_path in self.settings.get("STATIC_PATHS", []): + # use a prefix to avoid overriding standard watchers above + self.watchers['[static]%s' % static_path] = folder_watcher( + os.path.join(self.pelican.path, static_path), + [''], + self.pelican.ignore_files) + + if next(self.watchers['content']) is None: + logger.warning('No valid files found in content.') + + if next(self.watchers['theme']) is None: + logger.warning('Empty theme folder. Using `basic` theme.') + + def generate(self): + list(map(next, self.watchers.values())) + self.pelican.run() + + def run(self): + logger.info(' --- AutoReload Mode: Monitoring `content`, `theme` and' + ' `settings` for changes. ---') + while True: + try: + # Check source dir for changed files ending with the given + # extension in the settings. In the theme dir is no such + # restriction; all files are recursively checked if they + # have changed, no matter what extension the filenames + # have. + modified = {k: next(v) for k, v in self.watchers.items()} + + if modified['settings']: + self.setup() + + if any(modified.values()): + print('\n-> Modified: {}. re-generating...'.format( + ', '.join(k for k, v in modified.items() if v))) + self.generate() + + except Exception as e: + logger.warning('Caught exception "%s". Reloading.', e) + + finally: + time.sleep(.5) # sleep to avoid cpu load + + def main(): args = parse_arguments() init(args.verbosity) - pelican, settings = get_instance(args) - readers = Readers(settings) - - watchers = {'content': folder_watcher(pelican.path, - readers.extensions, - pelican.ignore_files), - 'theme': folder_watcher(pelican.theme, - [''], - pelican.ignore_files), - 'settings': file_watcher(args.settings)} - - old_static = settings.get("STATIC_PATHS", []) - for static_path in old_static: - # use a prefix to avoid possible overriding of standard watchers above - watchers['[static]%s' % static_path] = folder_watcher( - os.path.join(pelican.path, static_path), - [''], - pelican.ignore_files) try: + generator = Generator(args) + generator.generate() + + if args.serve: + serve = server.ServeThread( + generator.settings.get('OUTPUT_PATH', '.'), + 'localhost', + args.port) + serve.daemon = True + serve.start() + if args.autoreload: - print(' --- AutoReload Mode: Monitoring `content`, `theme` and' - ' `settings` for changes. ---') + generator.daemon = True + generator.start() + generator.join() + elif args.serve: + serve.join() - while True: - try: - # Check source dir for changed files ending with the given - # extension in the settings. In the theme dir is no such - # restriction; all files are recursively checked if they - # have changed, no matter what extension the filenames - # have. - modified = {k: next(v) for k, v in watchers.items()} - - if modified['settings']: - pelican, settings = get_instance(args) - - # Adjust static watchers if there are any changes - new_static = settings.get("STATIC_PATHS", []) - - # Added static paths - # Add new watchers and set them as modified - for static_path in set(new_static).difference(old_static): - static_key = '[static]%s' % static_path - watchers[static_key] = folder_watcher( - os.path.join(pelican.path, static_path), - [''], - pelican.ignore_files) - modified[static_key] = next(watchers[static_key]) - - # Removed static paths - # Remove watchers and modified values - for static_path in set(old_static).difference(new_static): - static_key = '[static]%s' % static_path - watchers.pop(static_key) - modified.pop(static_key) - - # Replace old_static with the new one - old_static = new_static - - if any(modified.values()): - print('\n-> Modified: {}. re-generating...'.format( - ', '.join(k for k, v in modified.items() if v))) - - if modified['content'] is None: - logger.warning('No valid files found in content.') - - if modified['theme'] is None: - logger.warning('Empty theme folder. Using `basic` ' - 'theme.') - - pelican.run() - - except KeyboardInterrupt: - logger.warning("Keyboard interrupt, quitting.") - break - - except Exception as e: - if (args.verbosity == logging.DEBUG): - raise - logger.warning( - 'Caught exception "%s". Reloading.', e) - - finally: - time.sleep(.5) # sleep to avoid cpu load - - else: - if next(watchers['content']) is None: - logger.warning('No valid files found in content.') - - if next(watchers['theme']) is None: - logger.warning('Empty theme folder. Using `basic` theme.') - - pelican.run() + except KeyboardInterrupt: + logger.info("Keyboard interrupt, quitting.") except Exception as e: logger.critical('%s', e) - if args.verbosity == logging.DEBUG: raise else: diff --git a/pelican/server.py b/pelican/server.py index f58ac085..5a4b552c 100644 --- a/pelican/server.py +++ b/pelican/server.py @@ -1,8 +1,14 @@ from __future__ import print_function import os +import os.path +import posixpath import sys import logging + +import threading +from six.moves.urllib import parse as url_parse + from six.moves import SimpleHTTPServer as srvmod from six.moves import socketserver @@ -14,18 +20,23 @@ except ImportError: class ComplexHTTPRequestHandler(srvmod.SimpleHTTPRequestHandler): SUFFIXES = ['', '.html', '/index.html'] + SERVPATH = '' + + def translate_path(self, path): + """Add the ability to serve a file from a given directory (not cwd) + """ + path = super(ComplexHTTPRequestHandler, self).translate_path(path) + return os.path.join(self.SERVPATH, os.path.relpath(path, os.getcwd())) def do_GET(self): + self.original_path = self.path # Try to detect file by applying various suffixes for suffix in self.SUFFIXES: - if not hasattr(self, 'original_path'): - self.original_path = self.path - self.path = self.original_path + suffix path = self.translate_path(self.path) if os.path.exists(path): - srvmod.SimpleHTTPRequestHandler.do_GET(self) + super(ComplexHTTPRequestHandler, self).do_GET() logging.info("Found `%s`." % self.path) break @@ -48,21 +59,36 @@ class ComplexHTTPRequestHandler(srvmod.SimpleHTTPRequestHandler): return mimetype -if __name__ == '__main__': - PORT = len(sys.argv) in (2, 3) and int(sys.argv[1]) or 8000 - SERVER = len(sys.argv) == 3 and sys.argv[2] or "" - - socketserver.TCPServer.allow_reuse_address = True +def serve(path, server, port): + ComplexHTTPRequestHandler.SERVPATH = path try: - httpd = socketserver.TCPServer((SERVER, PORT), ComplexHTTPRequestHandler) + httpd = socketserver.TCPServer((server, port), ComplexHTTPRequestHandler) + httpd.allow_reuse_address = True except OSError as e: - logging.error("Could not listen on port %s, server %s.", PORT, SERVER) + logging.error("Could not listen on port %s, server %s.", port, server) sys.exit(getattr(e, 'exitcode', 1)) - - logging.info("Serving at port %s, server %s.", PORT, SERVER) + logging.info("Serving %s at port %s, server %s.", path, port, server) try: httpd.serve_forever() except KeyboardInterrupt as e: logging.info("Shutting down server.") httpd.socket.close() + raise + + +class ServeThread(threading.Thread): + def __init__(self, path, server, port): + super(ServeThread, self).__init__() + self.path = path + self.server = server + self.port = port + + def run(self): + serve(self.path, self.server, self.port) + + +if __name__ == '__main__': + PORT = len(sys.argv) in (2, 3) and int(sys.argv[1]) or 8000 + SERVER = len(sys.argv) == 3 and sys.argv[2] or "" + serve('', SERVER, PORT) diff --git a/pelican/tools/templates/Makefile.in b/pelican/tools/templates/Makefile.in index b97fbe43..66df4f5f 100644 --- a/pelican/tools/templates/Makefile.in +++ b/pelican/tools/templates/Makefile.in @@ -46,8 +46,7 @@ help: @echo ' make publish generate using production settings ' @echo ' make serve [PORT=8000] serve site at http://localhost:8000' @echo ' make serve-global [SERVER=0.0.0.0] serve (as root) to $(SERVER):80 ' - @echo ' make devserver [PORT=8000] start/restart develop_server.sh ' - @echo ' make stopserver stop local server ' + @echo ' make devserver [PORT=8000] start serve with regenerate ' @echo ' make ssh_upload upload the web site via SSH ' @echo ' make rsync_upload upload the web site via rsync+ssh ' @echo ' make dropbox_upload upload the web site via Dropbox ' @@ -86,17 +85,11 @@ endif devserver: ifdef PORT - $$(BASEDIR)/develop_server.sh restart $$(PORT) + $$(PELICAN) -S -o $$(OUTPUTDIR) -s $$(PUBLISHCONF) $$(PELICANOPTS) -p $$(PORT) $$(INPUTDIR) else - $$(BASEDIR)/develop_server.sh restart + $$(PELICAN) -S -o $$(OUTPUTDIR) -s $$(PUBLISHCONF) $$(PELICANOPTS) $$(INPUTDIR) endif -stopserver: - $(BASEDIR)/develop_server.sh stop - @echo 'Stopped Pelican and SimpleHTTPServer processes running in background.' - -publish: - $$(PELICAN) $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(PUBLISHCONF) $$(PELICANOPTS) ssh_upload: publish scp -P $$(SSH_PORT) -r $$(OUTPUTDIR)/* $$(SSH_USER)@$$(SSH_HOST):$$(SSH_TARGET_DIR) diff --git a/pelican/tools/templates/develop_server.sh.in b/pelican/tools/templates/develop_server.sh.in deleted file mode 100755 index 732069c2..00000000 --- a/pelican/tools/templates/develop_server.sh.in +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env bash -## -# This section should match your Makefile -## -PELICAN=$${PELICAN:-$pelican} -PELICANOPTS=$pelicanopts - -BASEDIR=$$(pwd) -INPUTDIR=$$BASEDIR/content -OUTPUTDIR=$$BASEDIR/output -CONFFILE=$$BASEDIR/pelicanconf.py - -### -# Don't change stuff below here unless you are sure -### - -SRV_PID=$$BASEDIR/srv.pid -PELICAN_PID=$$BASEDIR/pelican.pid - -function usage(){ - echo "usage: $$0 (stop) (start) (restart) [port]" - echo "This starts Pelican in debug and reload mode and then launches" - echo "an HTTP server to help site development. It doesn't read" - echo "your Pelican settings, so if you edit any paths in your Makefile" - echo "you will need to edit your settings as well." - exit 3 -} - -function alive() { - kill -0 $$1 >/dev/null 2>&1 -} - -function shut_down(){ - PID=$$(cat $$SRV_PID) - if [[ $$? -eq 0 ]]; then - if alive $PID; then - echo "Stopping HTTP server" - kill $$PID - else - echo "Stale PID, deleting" - fi - rm $$SRV_PID - else - echo "HTTP server PIDFile not found" - fi - - PID=$$(cat $$PELICAN_PID) - if [[ $$? -eq 0 ]]; then - if alive $$PID; then - echo "Killing Pelican" - kill $$PID - else - echo "Stale PID, deleting" - fi - rm $$PELICAN_PID - else - echo "Pelican PIDFile not found" - fi -} - -function start_up(){ - local port=$$1 - echo "Starting up Pelican and HTTP server" - shift - $$PELICAN --debug --autoreload -r $$INPUTDIR -o $$OUTPUTDIR -s $$CONFFILE $$PELICANOPTS & - pelican_pid=$$! - echo $$pelican_pid > $$PELICAN_PID - cd $$OUTPUTDIR - $PY -m pelican.server $$port & - srv_pid=$$! - echo $$srv_pid > $$SRV_PID - cd $$BASEDIR - sleep 1 - if ! alive $$pelican_pid ; then - echo "Pelican didn't start. Is the Pelican package installed?" - return 1 - elif ! alive $$srv_pid ; then - echo "The HTTP server didn't start. Is there another service using port" $$port "?" - return 1 - fi - echo 'Pelican and HTTP server processes now running in background.' -} - -### -# MAIN -### -[[ ($$# -eq 0) || ($$# -gt 2) ]] && usage -port='' -[[ $$# -eq 2 ]] && port=$$2 - -if [[ $$1 == "stop" ]]; then - shut_down -elif [[ $$1 == "restart" ]]; then - shut_down - start_up $$port -elif [[ $$1 == "start" ]]; then - if ! start_up $$port; then - shut_down - fi -else - usage -fi