1import os
2import threading
3import time
4import traceback
5
6from werkzeug.serving import run_simple
7from werkzeug.serving import WSGIRequestHandler
8
9from lektor.admin import WebAdmin
10from lektor.builder import Builder
11from lektor.builder import process_extra_flags
12from lektor.db import Database
13from lektor.reporter import CliReporter
14from lektor.utils import portable_popen
15from lektor.watcher import Watcher
16
17
18_os_alt_seps = list(
19    sep for sep in [os.path.sep, os.path.altsep] if sep not in (None, "/")
20)
21
22
23class SilentWSGIRequestHandler(WSGIRequestHandler):
24    def log(self, type, message, *args):
25        pass
26
27
28class BackgroundBuilder(threading.Thread):
29    def __init__(self, env, output_path, prune=True, verbosity=0, extra_flags=None):
30        threading.Thread.__init__(self)
31        watcher = Watcher(env, output_path)
32        watcher.observer.start()
33        self.env = env
34        self.watcher = watcher
35        self.output_path = output_path
36        self.prune = prune
37        self.verbosity = verbosity
38        self.last_build = time.time()
39        self.extra_flags = extra_flags
40
41    def build(self, update_source_info_first=False):
42        try:
43            db = Database(self.env)
44            builder = Builder(
45                db.new_pad(), self.output_path, extra_flags=self.extra_flags
46            )
47            if update_source_info_first:
48                builder.update_all_source_infos()
49            builder.build_all()
50            if self.prune:
51                builder.prune()
52        except Exception:
53            traceback.print_exc()
54        else:
55            self.last_build = time.time()
56
57    def run(self):
58        with CliReporter(self.env, verbosity=self.verbosity):
59            self.build(update_source_info_first=True)
60            for ts, _, _ in self.watcher:
61                if self.last_build is None or ts > self.last_build:
62                    self.build()
63
64
65class DevTools(object):
66    """This provides extra helpers for launching tools such as webpack."""
67
68    def __init__(self, env):
69        self.watcher = None
70        self.env = env
71
72    def start(self):
73        if self.watcher is not None:
74            return
75        from lektor import admin
76
77        admin = os.path.dirname(admin.__file__)
78        portable_popen(["npm", "install", "."], cwd=admin).wait()
79
80        self.watcher = portable_popen(
81            [os.path.join(admin, "node_modules/.bin/webpack"), "--watch"],
82            cwd=os.path.join(admin, "static"),
83        )
84
85    def stop(self):
86        if self.watcher is None:
87            return
88        self.watcher.kill()
89        self.watcher.wait()
90        self.watcher = None
91
92
93def browse_to_address(addr):
94    import webbrowser
95
96    def browse():
97        time.sleep(1)
98        webbrowser.open("http://%s:%s" % addr)
99
100    t = threading.Thread(target=browse)
101    t.setDaemon(True)
102    t.start()
103
104
105def run_server(
106    bindaddr,
107    env,
108    output_path,
109    prune=True,
110    verbosity=0,
111    lektor_dev=False,
112    ui_lang="en",
113    browse=False,
114    extra_flags=None,
115):
116    """This runs a server but also spawns a background process.  It's
117    not safe to call this more than once per python process!
118    """
119    wz_as_main = os.environ.get("WERKZEUG_RUN_MAIN") == "true"
120    in_main_process = not lektor_dev or wz_as_main
121    extra_flags = process_extra_flags(extra_flags)
122
123    if in_main_process:
124        background_builder = BackgroundBuilder(
125            env,
126            output_path=output_path,
127            prune=prune,
128            verbosity=verbosity,
129            extra_flags=extra_flags,
130        )
131        background_builder.setDaemon(True)
132        background_builder.start()
133        env.plugin_controller.emit(
134            "server-spawn", bindaddr=bindaddr, extra_flags=extra_flags
135        )
136
137    app = WebAdmin(
138        env,
139        output_path=output_path,
140        verbosity=verbosity,
141        debug=lektor_dev,
142        ui_lang=ui_lang,
143        extra_flags=extra_flags,
144    )
145
146    dt = None
147    if lektor_dev and not wz_as_main:
148        dt = DevTools(env)
149        dt.start()
150
151    if browse:
152        browse_to_address(bindaddr)
153
154    try:
155        return run_simple(
156            bindaddr[0],
157            bindaddr[1],
158            app,
159            use_debugger=True,
160            threaded=True,
161            use_reloader=lektor_dev,
162            request_handler=not lektor_dev
163            and SilentWSGIRequestHandler
164            or WSGIRequestHandler,
165        )
166    finally:
167        if dt is not None:
168            dt.stop()
169        if in_main_process:
170            env.plugin_controller.emit("server-stop")
171