-
Notifications
You must be signed in to change notification settings - Fork 62
/
4cat-daemon.py
278 lines (235 loc) · 10.4 KB
/
4cat-daemon.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
import subprocess
import argparse
import time
import sys
import os
import re
from pathlib import Path
cli = argparse.ArgumentParser()
cli.add_argument("--interactive", "-i", default=False, help="Run 4CAT in interactive mode (not in the background).",
action="store_true")
cli.add_argument("--log-level", "-l", default="INFO", help="Set log level (\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\", \"FATAL\").")
cli.add_argument("--no-version-check", "-n", default=False,
help="Skip version check that may prompt the user to migrate first.", action="store_true")
cli.add_argument("command")
args = cli.parse_args()
# ---------------------------------------------
# first-run.py ensures everything is set up
# right when running 4CAT for the first time
# ---------------------------------------------
first_run = Path(__file__).parent.joinpath("helper-scripts", "first-run.py")
result = subprocess.run([sys.executable, str(first_run)], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if result.returncode != 0:
print("Unexpected error while preparing 4CAT. You may need to re-install 4CAT.")
print("stdout:\n" + "\n".join([" " + line for line in result.stdout.decode("utf-8").split("\n")]))
print("stderr:\n" + "\n".join([" " + line for line in result.stderr.decode("utf-8").split("\n")]))
exit(1)
# ---------------------------------------------
# Do not start if migration is required
# ---------------------------------------------
if not args.no_version_check:
target_version_file = Path("VERSION")
current_version_file = Path("config/.current-version")
if not current_version_file.exists():
# this is the latest version lacking version files
current_version = "1.9"
else:
with current_version_file.open() as handle:
current_version = re.split(r"\s", handle.read())[0].strip()
if not target_version_file.exists():
target_version = "1.9"
else:
with target_version_file.open() as handle:
target_version = re.split(r"\s", handle.read())[0].strip()
if current_version != target_version:
print("Migrated version: %s" % current_version)
print("Code version: %s" % target_version)
print("Upgrade detected. You should run the following command to update 4CAT before (re)starting:")
print(" %s helper-scripts/migrate.py" % sys.executable)
exit(1)
# we can only import this here, because the version check above needs to be
# done first, as it may detect that the user needs to migrate first before
# the config manager can be run properly
from common.config_manager import config
from common.lib.helpers import call_api
# ---------------------------------------------
# Check validity of configuration file
# (could be expanded to check for other values)
# ---------------------------------------------
if not config.get('ANONYMISATION_SALT') or config.get('ANONYMISATION_SALT') == "REPLACE_THIS":
print(
"You need to set a random value for anonymisation in config.py before you can run 4CAT. Look for the ANONYMISATION_SALT option.")
sys.exit(1)
# ---------------------------------------------
# Running as a daemon is only supported on
# POSIX-compatible systems - run interactive
# on Windows.
# ---------------------------------------------
if os.name not in ("posix",):
# if not, run the backend directly and quit
print("Using '%s' to run the 4CAT backend is only supported on UNIX-like systems." % __file__)
print("Running backend in interactive mode instead.")
import backend.bootstrap as bootstrap
bootstrap.run(as_daemon=False, log_level=args.log_level)
sys.exit(0)
if args.interactive:
print("Running backend in interactive mode.")
import backend.bootstrap as bootstrap
bootstrap.run(as_daemon=False, log_level=args.log_level)
sys.exit(0)
else:
# if so, import necessary modules
import psutil
import daemon
# determine PID file
pidfile = config.get('PATH_ROOT').joinpath(config.get('PATH_LOCKFILE'), "4cat.pid") # pid file location
# ---------------------------------------------
# These functions start and stop the daemon
# ---------------------------------------------
# These are only defined at this point because they require the psutil and
# daemon modules which are not available on Windows.
def start():
"""
Start backend, as a daemon
:return bool: True
"""
# only one instance may be running at a time
if pidfile.is_file():
with pidfile.open() as infile:
pid = int(infile.read().strip())
if pid in psutil.pids():
print("...error: the 4CAT Backend Daemon is already running.")
return False
# start daemon in a separate process, so we can continue doing stuff in
# this one afterwards
new_pid = os.fork()
if new_pid == 0:
# create new daemon context and run bootstrapper inside it
with daemon.DaemonContext(
working_directory=os.path.abspath(os.path.dirname(__file__)),
umask=0x002,
stderr=open(Path(config.get('PATH_ROOT'), config.get('PATH_LOGS'), "4cat.stderr"), "w+"),
detach_process=True
) as context:
import backend.bootstrap as bootstrap
bootstrap.run(as_daemon=True, log_level=args.log_level)
sys.exit(0)
else:
# wait a few seconds and see if PIDfile was created by the bootstrapper
# and refers to a running process
now = time.time()
while time.time() < now + 60:
if pidfile.is_file():
break
else:
time.sleep(0.1)
if not pidfile.is_file():
print("...error while starting 4CAT Backend Daemon (pidfile not found).")
return False
else:
with pidfile.open() as infile:
pid = int(infile.read().strip())
if pid in psutil.pids():
print("...4CAT Backend Daemon started.")
else:
print("...error while starting 4CAT Backend Daemon (PID invalid).")
return True
def stop(force=False):
"""
Stop the backend daemon, if it is running
Sends a SIGTERM signal - this is intercepted by the daemon after which it
shuts down gracefully.
:param bool force: send SIGKILL if process does not quit quickly enough?
:return bool: True if the backend was running (and a shut down signal was
sent, False if not.
"""
killed = False
if pidfile.is_file():
# see if the listed process is actually running right now
with pidfile.open() as infile:
pid = int(infile.read().strip())
if pid not in psutil.pids():
print("...error: 4CAT Backend Daemon is not running, but a PID file exists. Has it crashed?")
return False
# tell the backend to stop
os.system("kill -15 %s" % str(pid))
print("...sending SIGTERM to process %i. Waiting for backend to quit..." % pid)
# periodically check if the process has quit
starttime = time.time()
while pid in psutil.pids():
nowtime = time.time()
if nowtime - starttime > 60:
# give up if it takes too long
if force == True and not killed:
os.system("kill -9 %s" % str(pid))
print("...error: the 4CAT backend daemon did not quit within 60 seconds. Sending SIGKILL...")
killed = True
starttime = time.time()
else:
print(
"...error: the 4CAT backend daemon did not quit within 60 seconds. A worker may not have quit (yet).")
return False
time.sleep(1)
if killed and pidfile.is_file():
# SIGKILL doesn't clean up the pidfile, so we do it here
pidfile.unlink()
print("...4CAT Backend stopped.")
return True
else:
# no pid file, so nothing running
print("...the 4CAT backend daemon is not currently running.")
return True
# ---------------------------------------------
# Show manual, if command does not exists
# ---------------------------------------------
manual = """Usage: python(3) backend.py <start|stop|restart|force-restart|force-stop|status>
Starts, stops or restarts the 4CAT backend daemon.
"""
if args.command not in ("start", "stop", "restart", "status", "force-restart", "force-stop"):
print(manual)
sys.exit(1)
# determine command given and get the current PID (if any)
command = args.command
if pidfile.is_file():
with pidfile.open() as file:
pid = int(file.read().strip())
else:
pid = None
# ---------------------------------------------
# Run code for valid commands
# ---------------------------------------------
if command in ("restart", "force-restart"):
print("Restarting 4CAT Backend Daemon...")
# restart daemon, but only if it's already running and could successfully be stopped
stopped = stop(force=(command == "force-restart"))
if stopped:
print("...starting 4CAT Backend Daemon...")
start()
elif command == "start":
# start...but only if there currently is no running backend process
print("Starting 4CAT Backend Daemon...")
start()
elif command in ("stop", "force-stop"):
# stop
print("Stopping 4CAT Backend Daemon...")
sys.exit(0 if stop(force=(command == "force-stop")) else 1)
elif command == "status":
# show whether the daemon is currently running
if not pid:
print("4CAT Backend Daemon is currently not running.")
elif pid in psutil.pids():
print("4CAT Backend Daemon is currently up and running.")
# fetch more detailed status via internal API
if not config.get('API_PORT'):
sys.exit(0)
print("\n Active workers:\n-------------------------")
active_workers = call_api("workers")["response"]
active_workers = {worker: active_workers[worker] for worker in
sorted(active_workers, key=lambda id: active_workers[id], reverse=True) if
active_workers[worker] > 0}
for worker in active_workers:
print("%s: %i" % (worker, active_workers[worker]))
print("\n")
else:
print("4CAT Backend Daemon is not running, but a PID file exists. Has it crashed?")