Skip to content

Commit 266b879

Browse files
committed
Eliminate globals usage
This will help running tests because the app isn't initialized automatically by touching the "changedetectionio" package. Moving things out of the __init__.py removes the side-effect of "import changedetection" which means tests can control the state without restarting. This is the first step in making the tests run with only calling "pytest". The fixture use and test setup need to be adjusted to not depend on test ordering.
1 parent 6084b0f commit 266b879

File tree

9 files changed

+344
-334
lines changed

9 files changed

+344
-334
lines changed

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
1313
RUN apt-get update && apt-get install -y --no-install-recommends \
1414
g++ \
1515
gcc \
16+
git \
1617
libc-dev \
1718
libffi-dev \
1819
libjpeg-dev \

changedetection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
# Only exists for direct CLI usage
44

5-
import changedetectionio
6-
changedetectionio.main()
5+
from changedetectionio.__main__ import main
6+
main()

changedetectionio/__init__.py

Lines changed: 0 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -3,193 +3,3 @@
33
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
44

55
__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

Comments
 (0)