-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathprodigy_control.py
154 lines (125 loc) · 3.96 KB
/
prodigy_control.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
import json
import os
import shutil
import signal
import subprocess
import sys
import threading
import psutil
from psutil import NoSuchProcess, ZombieProcess
from prodigy_constants import *
from prodigy_model import prodigy_sys_fn, port_used, iter_prodigy_services, get_work_dir_or_none
def get_next_available_port(start: int = 8080) -> int:
"""
Get the next available port in system.
:param start: The starting port to check.
:return: Next available port that can be listened on.
"""
port = start
while True:
if port_used(port):
port += 1
else:
break
return port
def start_prodigy(working_dir, arguments=None):
"""Start a prodigy service at a new port"""
port = get_next_available_port()
work_dir = os.path.realpath(working_dir)
# Write config
with open(prodigy_sys_fn(working_dir), 'w') as f:
json.dump({
"port": port,
"host": "127.0.0.1",
}, f)
new_env = os.environ.copy()
new_env['PRODIGY_HOME'] = work_dir
process = subprocess.Popen(
['python', PRODIGY_ENTRY_POINT, work_dir],
shell=False,
cwd=working_dir,
env=new_env,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
with open(os.path.join(work_dir, 'prodigy.pid'), 'w') as f:
f.write(f'{process.pid}')
return {
'pid': process.pid,
'process': process,
'port': port,
'work_dir': work_dir,
}
def kill_pid_and_children(
pid: int,
sig=signal.SIGINT if sys.platform != 'win32' else signal.SIGTERM):
"""
Send signal to a process and its children to kill them all.
Does not necessarily clean up zombie processes.
:param pid:
:param sig:
:return:
"""
try:
parent = psutil.Process(pid)
except psutil.NoSuchProcess:
return
children_pid = []
for process in parent.children():
children_pid.append(process.pid)
kill_pid_and_children(process.pid)
parent.send_signal(sig)
def stop_prodigy(pid: int) -> None:
"""
Stop a Prodigy instance by killing it and its child processes.
:param pid:
:return:
"""
kill_pid_and_children(pid)
def get_prodigy_pid(work_dir_or_prodigy_id: str) -> [None, int]:
"""Return the PID of current prodigy instance."""
if len(work_dir_or_prodigy_id.split(os.sep)) > 1:
work_dir = work_dir_or_prodigy_id
else:
work_dir = get_work_dir_or_none(work_dir_or_prodigy_id)
pid_fn = os.path.join(work_dir, PRODIGY_PID_FILE)
if os.path.exists(pid_fn):
if not os.path.isfile(pid_fn):
shutil.rmtree(pid_fn)
return None
with open(pid_fn) as f:
try:
# Already started
pid = int(f.read())
psutil.Process(pid)
return pid
except NoSuchProcess:
pass
return None
def stop_all_prodigy(*_, **__):
for prodigy_id, _, alive, listening, pid in iter_prodigy_services():
if alive:
print('Stopping prodigy PID', pid)
stop_prodigy(pid)
# Stop all services at exit
if threading.current_thread() is threading.main_thread():
signal.signal(signal.SIGTERM, stop_all_prodigy)
signal.signal(signal.SIGABRT, stop_all_prodigy)
signal.signal(signal.SIGINT, stop_all_prodigy)
def register_zombie_cleaner(app):
"""
Register a function that runs after every flask request
to clean up zombie processes.
:param app: Flask app.
:return: None
"""
def cleanup_zombie(_):
self = psutil.Process()
ppid_map = psutil._ppid_map()
for pid, ppid in ppid_map.items():
if ppid == self.pid:
try:
psutil.Process(pid)
except ZombieProcess:
os.waitpid(pid, 0)
except NoSuchProcess:
pass
app.teardown_appcontext(cleanup_zombie)