1#
2# sync_server.py <Peter.Bienstman@UGent.be>
3#
4
5import os
6import http.client
7import threading
8import mnemosyne.version
9
10from openSM2sync.ui import UI
11from openSM2sync.server import Server
12from mnemosyne.libmnemosyne.component import Component
13from mnemosyne.libmnemosyne.utils import expand_path, localhost_IP
14
15
16class SyncServer(Component, Server):
17
18    """libmnemosyne-specific parts of the openSM2sync server."""
19
20    program_name = "Mnemosyne"
21    program_version = mnemosyne.version.version
22
23    def __init__(self, **kwds):
24        config = kwds["component_manager"].current("config")
25        if "server_only" in kwds:
26            self.server_only = kwds["server_only"]
27            del kwds["server_only"]
28        else:
29            self.server_only = False
30        super().__init__(machine_id=config.machine_id(),
31            port=config["sync_server_port"], **kwds)
32        self.check_for_edited_local_media_files = \
33            self.config()["check_for_edited_local_media_files"]
34
35    def authorization_set_up(self) -> bool:
36        auth_username = self.config()["remote_access_username"]
37        auth_password = self.config()["remote_access_password"]
38        return auth_password != "" and auth_username != ""
39
40    def authorise(self, username, password):
41        # We should not be running if authorization is not set up,
42        # but check just in case.
43        return self.authorization_set_up() and \
44               username == self.config()["remote_access_username"] and \
45               password == self.config()["remote_access_password"]
46
47    def load_database(self, database_name):
48        if self.server_only:
49            # First see if web server needs to release database.
50            try:
51                con = http.client.HTTPConnection("127.0.0.1",
52                    self.config()["web_server_port"])
53                con.request("GET", "/release_database")
54                response = con.getresponse()
55            except:
56                pass
57            if not os.path.exists(expand_path(database_name,
58                self.config().data_dir)):
59                self.database().new(database_name)
60            else:
61                self.database().load(database_name)
62        else:
63            self.previous_database = self.config()["last_database"]
64            if self.previous_database != database_name:
65                if not os.path.exists(expand_path(database_name,
66                    self.config().data_dir)):
67                    self.database().new(database_name)
68                else:
69                    self.database().load(database_name)
70        return self.database()
71
72    def unload_database(self, database):
73        if self.server_only:
74            self.database().unload()
75        else:
76            self.database().load(self.previous_database)
77            self.log().loaded_database()
78            self.review_controller().reset_but_try_to_keep_current_card()
79            self.review_controller().update_dialog(redraw_all=True)
80
81    def flush(self):
82
83        """If there are still dangling sessions (i.e. those waiting in vain
84        for more client input) in the sync server, we should flush them and
85        make sure they restore from backup before doing anything that could
86        change the database (e.g. adding a card). Otherwise, if these
87        sessions close later during program shutdown, their backup
88        restoration will override the changes.
89
90        """
91
92        self.expire_old_sessions()
93
94
95class SyncServerThread(threading.Thread, SyncServer):
96
97    """Basic threading implementation of the sync server, suitable for text-
98    based UIs. A GUI-based client will want to override several functions
99    in SyncServer and SyncServerThread in view of the interaction between
100    multiple threads and the GUI event loop.
101
102    """
103
104    def __init__(self, component_manager):
105        threading.Thread.__init__(self)
106        SyncServer.__init__(self, component_manager=component_manager,
107                            ui=UI(), server_only=True)
108
109    def run(self):
110        if not self.authorization_set_up():
111            print("""Error: Authorization not set up.
112If on a headless server, you may use the following commands in the sqlite3 console on config.db to configure authorization:
113   update config set value="'<username>'" where key = "remote_access_username";
114   update config set value="'<password>'" where key = "remote_access_password";""")
115            return
116
117        # Start server.
118        print(("Sync server listening on " + localhost_IP() + ":" + \
119            str(self.config()["sync_server_port"])))
120        self.serve_until_stopped()
121        server_hanging = (len(self.sessions) != 0)
122        if server_hanging:
123            self.terminate_all_sessions()
124            self.database().release_connection()
125