1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
4
5
6import errno
7import json
8import os
9from contextlib import suppress
10from threading import Thread
11
12from calibre import as_unicode
13from calibre.constants import cache_dir, config_dir, is_running_from_develop
14from calibre.srv.bonjour import BonJour
15from calibre.srv.handler import Handler
16from calibre.srv.http_response import create_http_handler
17from calibre.srv.loop import ServerLoop
18from calibre.srv.opts import server_config
19from calibre.srv.utils import RotatingLog
20
21
22def log_paths():
23    return os.path.join(cache_dir(), 'server-log.txt'), os.path.join(
24        cache_dir(), 'server-access-log.txt'
25    )
26
27
28def read_json(path):
29    try:
30        with lopen(path, 'rb') as f:
31            raw = f.read()
32    except OSError as err:
33        if err.errno != errno.ENOENT:
34            raise
35        return
36    with suppress(json.JSONDecodeError):
37        return json.loads(raw)
38
39
40def custom_list_template():
41    return read_json(custom_list_template.path)
42
43
44def search_the_net_urls():
45    return read_json(search_the_net_urls.path)
46
47
48custom_list_template.path = os.path.join(config_dir, 'server-custom-list-template.json')
49search_the_net_urls.path = os.path.join(config_dir, 'server-search-the-net.json')
50
51
52class Server:
53
54    loop = current_thread = exception = None
55    state_callback = start_failure_callback = None
56
57    def __init__(self, library_broker, notify_changes):
58        opts = server_config()
59        lp, lap = log_paths()
60        try:
61            os.makedirs(cache_dir())
62        except OSError as err:
63            if err.errno != errno.EEXIST:
64                raise
65        log_size = opts.max_log_size * 1024 * 1024
66        log = RotatingLog(lp, max_size=log_size)
67        access_log = RotatingLog(lap, max_size=log_size)
68        self.handler = Handler(library_broker, opts, notify_changes=notify_changes)
69        plugins = self.plugins = []
70        if opts.use_bonjour:
71            plugins.append(BonJour(wait_for_stop=max(0, opts.shutdown_timeout - 0.2)))
72        self.opts = opts
73        self.log, self.access_log = log, access_log
74        self.handler.set_log(self.log)
75        self.handler.router.ctx.custom_list_template = custom_list_template()
76        self.handler.router.ctx.search_the_net_urls = search_the_net_urls()
77
78    @property
79    def ctx(self):
80        return self.handler.router.ctx
81
82    @property
83    def user_manager(self):
84        return self.handler.router.ctx.user_manager
85
86    def start(self):
87        if self.current_thread is None:
88            try:
89                self.loop = ServerLoop(
90                    create_http_handler(self.handler.dispatch),
91                    opts=self.opts,
92                    log=self.log,
93                    access_log=self.access_log,
94                    plugins=self.plugins
95                )
96                self.loop.initialize_socket()
97            except Exception as e:
98                self.loop = None
99                self.exception = e
100                if self.start_failure_callback is not None:
101                    try:
102                        self.start_failure_callback(as_unicode(e))
103                    except Exception:
104                        pass
105                return
106            self.handler.set_jobs_manager(self.loop.jobs_manager)
107            self.current_thread = t = Thread(
108                name='EmbeddedServer', target=self.serve_forever
109            )
110            t.daemon = True
111            t.start()
112
113    def serve_forever(self):
114        self.exception = None
115        from calibre.srv.content import reset_caches
116        try:
117            if is_running_from_develop:
118                from calibre.utils.rapydscript import compile_srv
119                compile_srv()
120        except BaseException as e:
121            self.exception = e
122            if self.start_failure_callback is not None:
123                try:
124                    self.start_failure_callback(as_unicode(e))
125                except Exception:
126                    pass
127            return
128        if self.state_callback is not None:
129            try:
130                self.state_callback(True)
131            except Exception:
132                pass
133        reset_caches()  # we reset the cache as the server tdir has changed
134        try:
135            self.loop.serve_forever()
136        except BaseException as e:
137            self.exception = e
138        if self.state_callback is not None:
139            try:
140                self.state_callback(False)
141            except Exception:
142                pass
143
144    def stop(self):
145        if self.loop is not None:
146            self.loop.stop()
147            self.loop = None
148
149    def exit(self):
150        if self.current_thread is not None:
151            self.stop()
152            self.current_thread.join()
153            self.current_thread = None
154
155    @property
156    def is_running(self):
157        return self.current_thread is not None and self.current_thread.is_alive()
158