|
3 | 3 | # Read more https://github.com/dgtlmoon/changedetection.io/wiki |
4 | 4 |
|
5 | 5 | __version__ = '0.47.06' |
6 | | - |
7 | | -from changedetectionio.strtobool import strtobool |
8 | | -from json.decoder import JSONDecodeError |
9 | | -import os |
10 | | -os.environ['EVENTLET_NO_GREENDNS'] = 'yes' |
11 | | -import eventlet |
12 | | -import eventlet.wsgi |
13 | | -import getopt |
14 | | -import signal |
15 | | -import socket |
16 | | -import sys |
17 | | - |
18 | | -from changedetectionio import store |
19 | | -from changedetectionio.flask_app import changedetection_app |
20 | | -from loguru import logger |
21 | | - |
22 | | - |
23 | | -# Only global so we can access it in the signal handler |
24 | | -app = None |
25 | | -datastore = None |
26 | | - |
27 | | -# Parent wrapper or OS sends us a SIGTERM/SIGINT, do everything required for a clean shutdown |
28 | | -def sigshutdown_handler(_signo, _stack_frame): |
29 | | - global app |
30 | | - global datastore |
31 | | - name = signal.Signals(_signo).name |
32 | | - logger.critical(f'Shutdown: Got Signal - {name} ({_signo}), Saving DB to disk and calling shutdown') |
33 | | - datastore.sync_to_json() |
34 | | - logger.success('Sync JSON to disk complete.') |
35 | | - # This will throw a SystemExit exception, because eventlet.wsgi.server doesn't know how to deal with it. |
36 | | - # Solution: move to gevent or other server in the future (#2014) |
37 | | - datastore.stop_thread = True |
38 | | - app.config.exit.set() |
39 | | - sys.exit() |
40 | | - |
41 | | -def main(): |
42 | | - global datastore |
43 | | - global app |
44 | | - |
45 | | - datastore_path = None |
46 | | - do_cleanup = False |
47 | | - host = '' |
48 | | - ipv6_enabled = False |
49 | | - port = os.environ.get('PORT') or 5000 |
50 | | - ssl_mode = False |
51 | | - |
52 | | - # On Windows, create and use a default path. |
53 | | - if os.name == 'nt': |
54 | | - datastore_path = os.path.expandvars(r'%APPDATA%\changedetection.io') |
55 | | - os.makedirs(datastore_path, exist_ok=True) |
56 | | - else: |
57 | | - # Must be absolute so that send_from_directory doesnt try to make it relative to backend/ |
58 | | - datastore_path = os.path.join(os.getcwd(), "../datastore") |
59 | | - |
60 | | - try: |
61 | | - opts, args = getopt.getopt(sys.argv[1:], "6Ccsd:h:p:l:", "port") |
62 | | - except getopt.GetoptError: |
63 | | - print('backend.py -s SSL enable -h [host] -p [port] -d [datastore path] -l [debug level - TRACE, DEBUG(default), INFO, SUCCESS, WARNING, ERROR, CRITICAL]') |
64 | | - sys.exit(2) |
65 | | - |
66 | | - create_datastore_dir = False |
67 | | - |
68 | | - # Set a default logger level |
69 | | - logger_level = 'DEBUG' |
70 | | - # Set a logger level via shell env variable |
71 | | - # Used: Dockerfile for CICD |
72 | | - # To set logger level for pytest, see the app function in tests/conftest.py |
73 | | - if os.getenv("LOGGER_LEVEL"): |
74 | | - level = os.getenv("LOGGER_LEVEL") |
75 | | - logger_level = int(level) if level.isdigit() else level.upper() |
76 | | - |
77 | | - for opt, arg in opts: |
78 | | - if opt == '-s': |
79 | | - ssl_mode = True |
80 | | - |
81 | | - if opt == '-h': |
82 | | - host = arg |
83 | | - |
84 | | - if opt == '-p': |
85 | | - port = int(arg) |
86 | | - |
87 | | - if opt == '-d': |
88 | | - datastore_path = arg |
89 | | - |
90 | | - if opt == '-6': |
91 | | - logger.success("Enabling IPv6 listen support") |
92 | | - ipv6_enabled = True |
93 | | - |
94 | | - # Cleanup (remove text files that arent in the index) |
95 | | - if opt == '-c': |
96 | | - do_cleanup = True |
97 | | - |
98 | | - # Create the datadir if it doesnt exist |
99 | | - if opt == '-C': |
100 | | - create_datastore_dir = True |
101 | | - |
102 | | - if opt == '-l': |
103 | | - logger_level = int(arg) if arg.isdigit() else arg.upper() |
104 | | - |
105 | | - # Without this, a logger will be duplicated |
106 | | - logger.remove() |
107 | | - try: |
108 | | - log_level_for_stdout = { 'DEBUG', 'SUCCESS' } |
109 | | - logger.configure(handlers=[ |
110 | | - {"sink": sys.stdout, "level": logger_level, |
111 | | - "filter" : lambda record: record['level'].name in log_level_for_stdout}, |
112 | | - {"sink": sys.stderr, "level": logger_level, |
113 | | - "filter": lambda record: record['level'].name not in log_level_for_stdout}, |
114 | | - ]) |
115 | | - # Catch negative number or wrong log level name |
116 | | - except ValueError: |
117 | | - print("Available log level names: TRACE, DEBUG(default), INFO, SUCCESS," |
118 | | - " WARNING, ERROR, CRITICAL") |
119 | | - sys.exit(2) |
120 | | - |
121 | | - # isnt there some @thingy to attach to each route to tell it, that this route needs a datastore |
122 | | - app_config = {'datastore_path': datastore_path} |
123 | | - |
124 | | - if not os.path.isdir(app_config['datastore_path']): |
125 | | - if create_datastore_dir: |
126 | | - os.mkdir(app_config['datastore_path']) |
127 | | - else: |
128 | | - logger.critical( |
129 | | - f"ERROR: Directory path for the datastore '{app_config['datastore_path']}'" |
130 | | - f" does not exist, cannot start, please make sure the" |
131 | | - f" directory exists or specify a directory with the -d option.\n" |
132 | | - f"Or use the -C parameter to create the directory.") |
133 | | - sys.exit(2) |
134 | | - |
135 | | - try: |
136 | | - datastore = store.ChangeDetectionStore(datastore_path=app_config['datastore_path'], version_tag=__version__) |
137 | | - except JSONDecodeError as e: |
138 | | - # Dont' start if the JSON DB looks corrupt |
139 | | - logger.critical(f"ERROR: JSON DB or Proxy List JSON at '{app_config['datastore_path']}' appears to be corrupt, aborting.") |
140 | | - logger.critical(str(e)) |
141 | | - return |
142 | | - |
143 | | - app = changedetection_app(app_config, datastore) |
144 | | - |
145 | | - signal.signal(signal.SIGTERM, sigshutdown_handler) |
146 | | - signal.signal(signal.SIGINT, sigshutdown_handler) |
147 | | - |
148 | | - # Go into cleanup mode |
149 | | - if do_cleanup: |
150 | | - datastore.remove_unused_snapshots() |
151 | | - |
152 | | - app.config['datastore_path'] = datastore_path |
153 | | - |
154 | | - |
155 | | - @app.context_processor |
156 | | - def inject_version(): |
157 | | - return dict(right_sticky="v{}".format(datastore.data['version_tag']), |
158 | | - new_version_available=app.config['NEW_VERSION_AVAILABLE'], |
159 | | - has_password=datastore.data['settings']['application']['password'] != False |
160 | | - ) |
161 | | - |
162 | | - # Monitored websites will not receive a Referer header when a user clicks on an outgoing link. |
163 | | - # @Note: Incompatible with password login (and maybe other features) for now, submit a PR! |
164 | | - @app.after_request |
165 | | - def hide_referrer(response): |
166 | | - if strtobool(os.getenv("HIDE_REFERER", 'false')): |
167 | | - response.headers["Referrer-Policy"] = "no-referrer" |
168 | | - |
169 | | - return response |
170 | | - |
171 | | - # Proxy sub-directory support |
172 | | - # Set environment var USE_X_SETTINGS=1 on this script |
173 | | - # And then in your proxy_pass settings |
174 | | - # |
175 | | - # proxy_set_header Host "localhost"; |
176 | | - # proxy_set_header X-Forwarded-Prefix /app; |
177 | | - |
178 | | - |
179 | | - if os.getenv('USE_X_SETTINGS'): |
180 | | - logger.info("USE_X_SETTINGS is ENABLED") |
181 | | - from werkzeug.middleware.proxy_fix import ProxyFix |
182 | | - app.wsgi_app = ProxyFix(app.wsgi_app, x_prefix=1, x_host=1) |
183 | | - |
184 | | - s_type = socket.AF_INET6 if ipv6_enabled else socket.AF_INET |
185 | | - |
186 | | - if ssl_mode: |
187 | | - # @todo finalise SSL config, but this should get you in the right direction if you need it. |
188 | | - eventlet.wsgi.server(eventlet.wrap_ssl(eventlet.listen((host, port), s_type), |
189 | | - certfile='cert.pem', |
190 | | - keyfile='privkey.pem', |
191 | | - server_side=True), app) |
192 | | - |
193 | | - else: |
194 | | - eventlet.wsgi.server(eventlet.listen((host, int(port)), s_type), app) |
195 | | - |
0 commit comments