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