ominfra 0.0.0.dev125__py3-none-any.whl → 0.0.0.dev127__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ominfra/clouds/aws/auth.py +1 -1
- ominfra/deploy/_executor.py +1 -1
- ominfra/deploy/poly/_main.py +1 -1
- ominfra/pyremote/_runcommands.py +1 -1
- ominfra/scripts/journald2aws.py +2 -2
- ominfra/scripts/supervisor.py +1825 -1217
- ominfra/supervisor/collections.py +52 -0
- ominfra/supervisor/context.py +2 -336
- ominfra/supervisor/datatypes.py +1 -63
- ominfra/supervisor/dispatchers.py +22 -338
- ominfra/supervisor/dispatchersimpl.py +342 -0
- ominfra/supervisor/groups.py +33 -110
- ominfra/supervisor/groupsimpl.py +86 -0
- ominfra/supervisor/inject.py +45 -13
- ominfra/supervisor/main.py +1 -1
- ominfra/supervisor/pipes.py +83 -0
- ominfra/supervisor/poller.py +6 -3
- ominfra/supervisor/privileges.py +65 -0
- ominfra/supervisor/processes.py +18 -0
- ominfra/supervisor/{process.py → processesimpl.py} +99 -317
- ominfra/supervisor/setup.py +38 -0
- ominfra/supervisor/setupimpl.py +261 -0
- ominfra/supervisor/signals.py +24 -16
- ominfra/supervisor/spawning.py +31 -0
- ominfra/supervisor/spawningimpl.py +347 -0
- ominfra/supervisor/supervisor.py +54 -78
- ominfra/supervisor/types.py +122 -39
- ominfra/supervisor/users.py +64 -0
- {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/RECORD +34 -23
- {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,261 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import os
|
3
|
+
import re
|
4
|
+
import resource
|
5
|
+
import typing as ta
|
6
|
+
import warnings
|
7
|
+
|
8
|
+
from omlish.lite.cached import cached_nullary
|
9
|
+
from omlish.lite.logs import log
|
10
|
+
|
11
|
+
from .configs import ServerConfig
|
12
|
+
from .privileges import drop_privileges
|
13
|
+
from .setup import DaemonizeListeners
|
14
|
+
from .setup import SupervisorSetup
|
15
|
+
from .setup import SupervisorUser
|
16
|
+
from .types import ServerEpoch
|
17
|
+
from .utils import real_exit
|
18
|
+
from .utils import try_unlink
|
19
|
+
|
20
|
+
|
21
|
+
##
|
22
|
+
|
23
|
+
|
24
|
+
class SupervisorSetupImpl(SupervisorSetup):
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
*,
|
28
|
+
config: ServerConfig,
|
29
|
+
user: ta.Optional[SupervisorUser] = None,
|
30
|
+
epoch: ServerEpoch = ServerEpoch(0),
|
31
|
+
daemonize_listeners: DaemonizeListeners = DaemonizeListeners([]),
|
32
|
+
) -> None:
|
33
|
+
super().__init__()
|
34
|
+
|
35
|
+
self._config = config
|
36
|
+
self._user = user
|
37
|
+
self._epoch = epoch
|
38
|
+
self._daemonize_listeners = daemonize_listeners
|
39
|
+
|
40
|
+
#
|
41
|
+
|
42
|
+
@property
|
43
|
+
def first(self) -> bool:
|
44
|
+
return not self._epoch
|
45
|
+
|
46
|
+
#
|
47
|
+
|
48
|
+
@cached_nullary
|
49
|
+
def setup(self) -> None:
|
50
|
+
if not self.first:
|
51
|
+
# prevent crash on libdispatch-based systems, at least for the first request
|
52
|
+
self._cleanup_fds()
|
53
|
+
|
54
|
+
self._set_uid_or_exit()
|
55
|
+
|
56
|
+
if self.first:
|
57
|
+
self._set_rlimits_or_exit()
|
58
|
+
|
59
|
+
# this sets the options.logger object delay logger instantiation until after setuid
|
60
|
+
if not self._config.nocleanup:
|
61
|
+
# clean up old automatic logs
|
62
|
+
self._clear_auto_child_logdir()
|
63
|
+
|
64
|
+
if not self._config.nodaemon and self.first:
|
65
|
+
self._daemonize()
|
66
|
+
|
67
|
+
# writing pid file needs to come *after* daemonizing or pid will be wrong
|
68
|
+
self._write_pidfile()
|
69
|
+
|
70
|
+
@cached_nullary
|
71
|
+
def cleanup(self) -> None:
|
72
|
+
self._cleanup_pidfile()
|
73
|
+
|
74
|
+
#
|
75
|
+
|
76
|
+
def _cleanup_fds(self) -> None:
|
77
|
+
# try to close any leaked file descriptors (for reload)
|
78
|
+
start = 5
|
79
|
+
os.closerange(start, self._config.minfds)
|
80
|
+
|
81
|
+
#
|
82
|
+
|
83
|
+
def _set_uid_or_exit(self) -> None:
|
84
|
+
"""
|
85
|
+
Set the uid of the supervisord process. Called during supervisord startup only. No return value. Exits the
|
86
|
+
process via usage() if privileges could not be dropped.
|
87
|
+
"""
|
88
|
+
|
89
|
+
if self._user is None:
|
90
|
+
if os.getuid() == 0:
|
91
|
+
warnings.warn(
|
92
|
+
'Supervisor is running as root. Privileges were not dropped because no user is specified in the '
|
93
|
+
'config file. If you intend to run as root, you can set user=root in the config file to avoid '
|
94
|
+
'this message.',
|
95
|
+
)
|
96
|
+
else:
|
97
|
+
msg = drop_privileges(self._user.uid)
|
98
|
+
if msg is None:
|
99
|
+
log.info('Set uid to user %s succeeded', self._user.uid)
|
100
|
+
else: # failed to drop privileges
|
101
|
+
raise RuntimeError(msg)
|
102
|
+
|
103
|
+
#
|
104
|
+
|
105
|
+
def _set_rlimits_or_exit(self) -> None:
|
106
|
+
"""
|
107
|
+
Set the rlimits of the supervisord process. Called during supervisord startup only. No return value. Exits
|
108
|
+
the process via usage() if any rlimits could not be set.
|
109
|
+
"""
|
110
|
+
|
111
|
+
limits = []
|
112
|
+
|
113
|
+
if hasattr(resource, 'RLIMIT_NOFILE'):
|
114
|
+
limits.append({
|
115
|
+
'msg': (
|
116
|
+
'The minimum number of file descriptors required to run this process is %(min_limit)s as per the '
|
117
|
+
'"minfds" command-line argument or config file setting. The current environment will only allow '
|
118
|
+
'you to open %(hard)s file descriptors. Either raise the number of usable file descriptors in '
|
119
|
+
'your environment (see README.rst) or lower the minfds setting in the config file to allow the '
|
120
|
+
'process to start.'
|
121
|
+
),
|
122
|
+
'min': self._config.minfds,
|
123
|
+
'resource': resource.RLIMIT_NOFILE,
|
124
|
+
'name': 'RLIMIT_NOFILE',
|
125
|
+
})
|
126
|
+
|
127
|
+
if hasattr(resource, 'RLIMIT_NPROC'):
|
128
|
+
limits.append({
|
129
|
+
'msg': (
|
130
|
+
'The minimum number of available processes required to run this program is %(min_limit)s as per '
|
131
|
+
'the "minprocs" command-line argument or config file setting. The current environment will only '
|
132
|
+
'allow you to open %(hard)s processes. Either raise the number of usable processes in your '
|
133
|
+
'environment (see README.rst) or lower the minprocs setting in the config file to allow the '
|
134
|
+
'program to start.'
|
135
|
+
),
|
136
|
+
'min': self._config.minprocs,
|
137
|
+
'resource': resource.RLIMIT_NPROC,
|
138
|
+
'name': 'RLIMIT_NPROC',
|
139
|
+
})
|
140
|
+
|
141
|
+
for limit in limits:
|
142
|
+
min_limit = limit['min']
|
143
|
+
res = limit['resource']
|
144
|
+
msg = limit['msg']
|
145
|
+
name = limit['name']
|
146
|
+
|
147
|
+
soft, hard = resource.getrlimit(res) # type: ignore
|
148
|
+
|
149
|
+
# -1 means unlimited
|
150
|
+
if soft < min_limit and soft != -1: # type: ignore
|
151
|
+
if hard < min_limit and hard != -1: # type: ignore
|
152
|
+
# setrlimit should increase the hard limit if we are root, if not then setrlimit raises and we print
|
153
|
+
# usage
|
154
|
+
hard = min_limit # type: ignore
|
155
|
+
|
156
|
+
try:
|
157
|
+
resource.setrlimit(res, (min_limit, hard)) # type: ignore
|
158
|
+
log.info('Increased %s limit to %s', name, min_limit)
|
159
|
+
except (resource.error, ValueError):
|
160
|
+
raise RuntimeError(msg % dict( # type: ignore # noqa
|
161
|
+
min_limit=min_limit,
|
162
|
+
res=res,
|
163
|
+
name=name,
|
164
|
+
soft=soft,
|
165
|
+
hard=hard,
|
166
|
+
))
|
167
|
+
|
168
|
+
#
|
169
|
+
|
170
|
+
_unlink_pidfile = False
|
171
|
+
|
172
|
+
def _write_pidfile(self) -> None:
|
173
|
+
pid = os.getpid()
|
174
|
+
try:
|
175
|
+
with open(self._config.pidfile, 'w') as f:
|
176
|
+
f.write(f'{pid}\n')
|
177
|
+
except OSError:
|
178
|
+
log.critical('could not write pidfile %s', self._config.pidfile)
|
179
|
+
else:
|
180
|
+
self._unlink_pidfile = True
|
181
|
+
log.info('supervisord started with pid %s', pid)
|
182
|
+
|
183
|
+
def _cleanup_pidfile(self) -> None:
|
184
|
+
if self._unlink_pidfile:
|
185
|
+
try_unlink(self._config.pidfile)
|
186
|
+
|
187
|
+
#
|
188
|
+
|
189
|
+
def _clear_auto_child_logdir(self) -> None:
|
190
|
+
# must be called after realize()
|
191
|
+
child_logdir = self._config.child_logdir
|
192
|
+
if child_logdir == '/dev/null':
|
193
|
+
return
|
194
|
+
|
195
|
+
fnre = re.compile(rf'.+?---{self._config.identifier}-\S+\.log\.?\d{{0,4}}')
|
196
|
+
try:
|
197
|
+
filenames = os.listdir(child_logdir)
|
198
|
+
except OSError:
|
199
|
+
log.warning('Could not clear child_log dir')
|
200
|
+
return
|
201
|
+
|
202
|
+
for filename in filenames:
|
203
|
+
if fnre.match(filename):
|
204
|
+
pathname = os.path.join(child_logdir, filename)
|
205
|
+
try:
|
206
|
+
os.remove(pathname)
|
207
|
+
except OSError:
|
208
|
+
log.warning('Failed to clean up %r', pathname)
|
209
|
+
|
210
|
+
#
|
211
|
+
|
212
|
+
def _daemonize(self) -> None:
|
213
|
+
for dl in self._daemonize_listeners:
|
214
|
+
dl.before_daemonize()
|
215
|
+
|
216
|
+
self._do_daemonize()
|
217
|
+
|
218
|
+
for dl in self._daemonize_listeners:
|
219
|
+
dl.after_daemonize()
|
220
|
+
|
221
|
+
def _do_daemonize(self) -> None:
|
222
|
+
# To daemonize, we need to become the leader of our own session (process) group. If we do not, signals sent to
|
223
|
+
# our parent process will also be sent to us. This might be bad because signals such as SIGINT can be sent to
|
224
|
+
# our parent process during normal (uninteresting) operations such as when we press Ctrl-C in the parent
|
225
|
+
# terminal window to escape from a logtail command. To disassociate ourselves from our parent's session group we
|
226
|
+
# use os.setsid. It means "set session id", which has the effect of disassociating a process from is current
|
227
|
+
# session and process group and setting itself up as a new session leader.
|
228
|
+
#
|
229
|
+
# Unfortunately we cannot call setsid if we're already a session group leader, so we use "fork" to make a copy
|
230
|
+
# of ourselves that is guaranteed to not be a session group leader.
|
231
|
+
#
|
232
|
+
# We also change directories, set stderr and stdout to null, and change our umask.
|
233
|
+
#
|
234
|
+
# This explanation was (gratefully) garnered from
|
235
|
+
# http://www.cems.uwe.ac.uk/~irjohnso/coursenotes/lrc/system/daemons/d3.htm
|
236
|
+
|
237
|
+
pid = os.fork()
|
238
|
+
if pid != 0:
|
239
|
+
# Parent
|
240
|
+
log.debug('supervisord forked; parent exiting')
|
241
|
+
real_exit(0)
|
242
|
+
|
243
|
+
# Child
|
244
|
+
log.info('daemonizing the supervisord process')
|
245
|
+
if self._config.directory:
|
246
|
+
try:
|
247
|
+
os.chdir(self._config.directory)
|
248
|
+
except OSError as err:
|
249
|
+
log.critical("can't chdir into %r: %s", self._config.directory, err)
|
250
|
+
else:
|
251
|
+
log.info('set current directory: %r', self._config.directory)
|
252
|
+
|
253
|
+
os.dup2(0, os.open('/dev/null', os.O_RDONLY))
|
254
|
+
os.dup2(1, os.open('/dev/null', os.O_WRONLY))
|
255
|
+
os.dup2(2, os.open('/dev/null', os.O_WRONLY))
|
256
|
+
|
257
|
+
# XXX Stevens, in his Advanced Unix book, section 13.3 (page 417) recommends calling umask(0) and closing unused
|
258
|
+
# file descriptors. In his Network Programming book, he additionally recommends ignoring SIGHUP and forking
|
259
|
+
# again after the setsid() call, for obscure SVR4 reasons.
|
260
|
+
os.setsid()
|
261
|
+
os.umask(self._config.umask)
|
ominfra/supervisor/signals.py
CHANGED
@@ -6,25 +6,33 @@ import typing as ta
|
|
6
6
|
##
|
7
7
|
|
8
8
|
|
9
|
-
|
9
|
+
_SIGS_BY_NUM: ta.Mapping[int, signal.Signals] = {s.value: s for s in signal.Signals}
|
10
|
+
_SIGS_BY_NAME: ta.Mapping[str, signal.Signals] = {s.name: s for s in signal.Signals}
|
10
11
|
|
11
12
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
_SIG_NAMES = _init_sig_names()
|
16
|
-
return _SIG_NAMES.get(sig) or 'signal %d' % sig
|
13
|
+
def sig_num(value: ta.Union[int, str]) -> int:
|
14
|
+
try:
|
15
|
+
num = int(value)
|
17
16
|
|
17
|
+
except (ValueError, TypeError):
|
18
|
+
name = value.strip().upper() # type: ignore
|
19
|
+
if not name.startswith('SIG'):
|
20
|
+
name = f'SIG{name}'
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
if (sn := _SIGS_BY_NAME.get(name)) is None:
|
23
|
+
raise ValueError(f'value {value!r} is not a valid signal name') # noqa
|
24
|
+
num = sn
|
25
|
+
|
26
|
+
if num not in _SIGS_BY_NUM:
|
27
|
+
raise ValueError(f'value {value!r} is not a valid signal number')
|
28
|
+
|
29
|
+
return num
|
30
|
+
|
31
|
+
|
32
|
+
def sig_name(num: int) -> str:
|
33
|
+
if (sig := _SIGS_BY_NUM.get(num)) is not None:
|
34
|
+
return sig.name
|
35
|
+
return f'signal {sig}'
|
28
36
|
|
29
37
|
|
30
38
|
##
|
@@ -36,7 +44,7 @@ class SignalReceiver:
|
|
36
44
|
|
37
45
|
self._signals_recvd: ta.List[int] = []
|
38
46
|
|
39
|
-
def receive(self, sig: int, frame: ta.Any) -> None:
|
47
|
+
def receive(self, sig: int, frame: ta.Any = None) -> None:
|
40
48
|
if sig not in self._signals_recvd:
|
41
49
|
self._signals_recvd.append(sig)
|
42
50
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import dataclasses as dc
|
4
|
+
|
5
|
+
from .dispatchers import Dispatchers
|
6
|
+
from .pipes import ProcessPipes
|
7
|
+
from .types import Process
|
8
|
+
|
9
|
+
|
10
|
+
@dc.dataclass(frozen=True)
|
11
|
+
class SpawnedProcess:
|
12
|
+
pid: int
|
13
|
+
pipes: ProcessPipes
|
14
|
+
dispatchers: Dispatchers
|
15
|
+
|
16
|
+
|
17
|
+
class ProcessSpawnError(RuntimeError):
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
class ProcessSpawning:
|
22
|
+
@property
|
23
|
+
@abc.abstractmethod
|
24
|
+
def process(self) -> Process:
|
25
|
+
raise NotImplementedError
|
26
|
+
|
27
|
+
#
|
28
|
+
|
29
|
+
@abc.abstractmethod
|
30
|
+
def spawn(self) -> SpawnedProcess: # Raises[ProcessSpawnError]
|
31
|
+
raise NotImplementedError
|
@@ -0,0 +1,347 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import errno
|
3
|
+
import os.path
|
4
|
+
import shlex
|
5
|
+
import stat
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from omlish.lite.check import check_isinstance
|
9
|
+
from omlish.lite.check import check_not_none
|
10
|
+
from omlish.lite.typing import Func3
|
11
|
+
|
12
|
+
from .configs import ProcessConfig
|
13
|
+
from .configs import ServerConfig
|
14
|
+
from .dispatchers import Dispatchers
|
15
|
+
from .events import ProcessCommunicationEvent
|
16
|
+
from .events import ProcessCommunicationStderrEvent
|
17
|
+
from .events import ProcessCommunicationStdoutEvent
|
18
|
+
from .exceptions import BadCommandError
|
19
|
+
from .exceptions import NoPermissionError
|
20
|
+
from .exceptions import NotExecutableError
|
21
|
+
from .exceptions import NotFoundError
|
22
|
+
from .exceptions import ProcessError
|
23
|
+
from .pipes import ProcessPipes
|
24
|
+
from .pipes import close_child_pipes
|
25
|
+
from .pipes import close_pipes
|
26
|
+
from .pipes import make_process_pipes
|
27
|
+
from .privileges import drop_privileges
|
28
|
+
from .processes import PidHistory
|
29
|
+
from .spawning import ProcessSpawnError
|
30
|
+
from .spawning import ProcessSpawning
|
31
|
+
from .spawning import SpawnedProcess
|
32
|
+
from .types import Dispatcher
|
33
|
+
from .types import InputDispatcher
|
34
|
+
from .types import OutputDispatcher
|
35
|
+
from .types import Process
|
36
|
+
from .types import ProcessGroup
|
37
|
+
from .utils import as_bytes
|
38
|
+
from .utils import close_fd
|
39
|
+
from .utils import compact_traceback
|
40
|
+
from .utils import get_path
|
41
|
+
from .utils import real_exit
|
42
|
+
|
43
|
+
|
44
|
+
class OutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], int, OutputDispatcher]):
|
45
|
+
pass
|
46
|
+
|
47
|
+
|
48
|
+
class InputDispatcherFactory(Func3[Process, str, int, InputDispatcher]):
|
49
|
+
pass
|
50
|
+
|
51
|
+
|
52
|
+
InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
|
53
|
+
|
54
|
+
|
55
|
+
##
|
56
|
+
|
57
|
+
|
58
|
+
class ProcessSpawningImpl(ProcessSpawning):
|
59
|
+
def __init__(
|
60
|
+
self,
|
61
|
+
process: Process,
|
62
|
+
*,
|
63
|
+
server_config: ServerConfig,
|
64
|
+
pid_history: PidHistory,
|
65
|
+
|
66
|
+
output_dispatcher_factory: OutputDispatcherFactory,
|
67
|
+
input_dispatcher_factory: InputDispatcherFactory,
|
68
|
+
|
69
|
+
inherited_fds: ta.Optional[InheritedFds] = None,
|
70
|
+
) -> None:
|
71
|
+
super().__init__()
|
72
|
+
|
73
|
+
self._process = process
|
74
|
+
|
75
|
+
self._server_config = server_config
|
76
|
+
self._pid_history = pid_history
|
77
|
+
|
78
|
+
self._output_dispatcher_factory = output_dispatcher_factory
|
79
|
+
self._input_dispatcher_factory = input_dispatcher_factory
|
80
|
+
|
81
|
+
self._inherited_fds = InheritedFds(frozenset(inherited_fds or []))
|
82
|
+
|
83
|
+
#
|
84
|
+
|
85
|
+
@property
|
86
|
+
def process(self) -> Process:
|
87
|
+
return self._process
|
88
|
+
|
89
|
+
@property
|
90
|
+
def config(self) -> ProcessConfig:
|
91
|
+
return self._process.config
|
92
|
+
|
93
|
+
@property
|
94
|
+
def group(self) -> ProcessGroup:
|
95
|
+
return self._process.group
|
96
|
+
|
97
|
+
#
|
98
|
+
|
99
|
+
def spawn(self) -> SpawnedProcess: # Raises[ProcessSpawnError]
|
100
|
+
try:
|
101
|
+
exe, argv = self._get_execv_args()
|
102
|
+
except ProcessError as exc:
|
103
|
+
raise ProcessSpawnError(exc.args[0]) from exc
|
104
|
+
|
105
|
+
try:
|
106
|
+
pipes = make_process_pipes(not self.config.redirect_stderr)
|
107
|
+
except OSError as exc:
|
108
|
+
code = exc.args[0]
|
109
|
+
if code == errno.EMFILE:
|
110
|
+
# too many file descriptors open
|
111
|
+
msg = f"Too many open files to spawn '{self.process.name}'"
|
112
|
+
else:
|
113
|
+
msg = f"Unknown error making pipes for '{self.process.name}': {errno.errorcode.get(code, code)}"
|
114
|
+
raise ProcessSpawnError(msg) from exc
|
115
|
+
|
116
|
+
try:
|
117
|
+
dispatchers = self._make_dispatchers(pipes)
|
118
|
+
except Exception as exc: # noqa
|
119
|
+
close_pipes(pipes)
|
120
|
+
raise ProcessSpawnError(f"Unknown error making dispatchers for '{self.process.name}': {exc}") from exc
|
121
|
+
|
122
|
+
try:
|
123
|
+
pid = os.fork()
|
124
|
+
except OSError as exc:
|
125
|
+
code = exc.args[0]
|
126
|
+
if code == errno.EAGAIN:
|
127
|
+
# process table full
|
128
|
+
msg = f"Too many processes in process table to spawn '{self.process.name}'"
|
129
|
+
else:
|
130
|
+
msg = f"Unknown error during fork for '{self.process.name}': {errno.errorcode.get(code, code)}"
|
131
|
+
err = ProcessSpawnError(msg)
|
132
|
+
close_pipes(pipes)
|
133
|
+
raise err from exc
|
134
|
+
|
135
|
+
if pid != 0:
|
136
|
+
sp = SpawnedProcess(
|
137
|
+
pid,
|
138
|
+
pipes,
|
139
|
+
dispatchers,
|
140
|
+
)
|
141
|
+
self._spawn_as_parent(sp)
|
142
|
+
return sp
|
143
|
+
|
144
|
+
else:
|
145
|
+
self._spawn_as_child(
|
146
|
+
exe,
|
147
|
+
argv,
|
148
|
+
pipes,
|
149
|
+
)
|
150
|
+
raise RuntimeError('Unreachable') # noqa
|
151
|
+
|
152
|
+
def _get_execv_args(self) -> ta.Tuple[str, ta.Sequence[str]]:
|
153
|
+
"""
|
154
|
+
Internal: turn a program name into a file name, using $PATH, make sure it exists / is executable, raising a
|
155
|
+
ProcessError if not
|
156
|
+
"""
|
157
|
+
|
158
|
+
try:
|
159
|
+
args = shlex.split(self.config.command)
|
160
|
+
except ValueError as e:
|
161
|
+
raise BadCommandError(f"Can't parse command {self.config.command!r}: {e}") # noqa
|
162
|
+
|
163
|
+
if args:
|
164
|
+
program = args[0]
|
165
|
+
else:
|
166
|
+
raise BadCommandError('Command is empty')
|
167
|
+
|
168
|
+
if '/' in program:
|
169
|
+
exe = program
|
170
|
+
try:
|
171
|
+
st = os.stat(exe)
|
172
|
+
except OSError:
|
173
|
+
st = None
|
174
|
+
|
175
|
+
else:
|
176
|
+
path = get_path()
|
177
|
+
found = None
|
178
|
+
st = None
|
179
|
+
for dir in path: # noqa
|
180
|
+
found = os.path.join(dir, program)
|
181
|
+
try:
|
182
|
+
st = os.stat(found)
|
183
|
+
except OSError:
|
184
|
+
pass
|
185
|
+
else:
|
186
|
+
break
|
187
|
+
|
188
|
+
if st is None:
|
189
|
+
exe = program
|
190
|
+
else:
|
191
|
+
exe = found # type: ignore
|
192
|
+
|
193
|
+
# check_execv_args will raise a ProcessError if the execv args are bogus, we break it out into a separate
|
194
|
+
# options method call here only to service unit tests
|
195
|
+
check_execv_args(exe, args, st)
|
196
|
+
|
197
|
+
return exe, args
|
198
|
+
|
199
|
+
def _make_dispatchers(self, pipes: ProcessPipes) -> Dispatchers:
|
200
|
+
dispatchers: ta.List[Dispatcher] = []
|
201
|
+
|
202
|
+
if pipes.stdout is not None:
|
203
|
+
dispatchers.append(check_isinstance(self._output_dispatcher_factory(
|
204
|
+
self.process,
|
205
|
+
ProcessCommunicationStdoutEvent,
|
206
|
+
pipes.stdout,
|
207
|
+
), OutputDispatcher))
|
208
|
+
|
209
|
+
if pipes.stderr is not None:
|
210
|
+
dispatchers.append(check_isinstance(self._output_dispatcher_factory(
|
211
|
+
self.process,
|
212
|
+
ProcessCommunicationStderrEvent,
|
213
|
+
pipes.stderr,
|
214
|
+
), OutputDispatcher))
|
215
|
+
|
216
|
+
if pipes.stdin is not None:
|
217
|
+
dispatchers.append(check_isinstance(self._input_dispatcher_factory(
|
218
|
+
self.process,
|
219
|
+
'stdin',
|
220
|
+
pipes.stdin,
|
221
|
+
), InputDispatcher))
|
222
|
+
|
223
|
+
return Dispatchers(dispatchers)
|
224
|
+
|
225
|
+
#
|
226
|
+
|
227
|
+
def _spawn_as_parent(self, sp: SpawnedProcess) -> None:
|
228
|
+
close_child_pipes(sp.pipes)
|
229
|
+
|
230
|
+
self._pid_history[sp.pid] = self.process
|
231
|
+
|
232
|
+
#
|
233
|
+
|
234
|
+
def _spawn_as_child(
|
235
|
+
self,
|
236
|
+
exe: str,
|
237
|
+
argv: ta.Sequence[str],
|
238
|
+
pipes: ProcessPipes,
|
239
|
+
) -> ta.NoReturn:
|
240
|
+
try:
|
241
|
+
# Prevent child from receiving signals sent to the parent by calling os.setpgrp to create a new process
|
242
|
+
# group for the child. This prevents, for instance, the case of child processes being sent a SIGINT when
|
243
|
+
# running supervisor in foreground mode and Ctrl-C in the terminal window running supervisord is pressed.
|
244
|
+
# Presumably it also prevents HUP, etc. received by supervisord from being sent to children.
|
245
|
+
os.setpgrp()
|
246
|
+
|
247
|
+
#
|
248
|
+
|
249
|
+
# After preparation sending to fd 2 will put this output in the stderr log.
|
250
|
+
self._prepare_child_fds(pipes)
|
251
|
+
|
252
|
+
#
|
253
|
+
|
254
|
+
setuid_msg = self._set_uid()
|
255
|
+
if setuid_msg:
|
256
|
+
uid = self.config.uid
|
257
|
+
msg = f"Couldn't setuid to {uid}: {setuid_msg}\n"
|
258
|
+
os.write(2, as_bytes('supervisor: ' + msg))
|
259
|
+
raise RuntimeError(msg)
|
260
|
+
|
261
|
+
#
|
262
|
+
|
263
|
+
env = os.environ.copy()
|
264
|
+
env['SUPERVISOR_ENABLED'] = '1'
|
265
|
+
env['SUPERVISOR_PROCESS_NAME'] = self.process.name
|
266
|
+
if self.group:
|
267
|
+
env['SUPERVISOR_GROUP_NAME'] = self.group.name
|
268
|
+
if self.config.environment is not None:
|
269
|
+
env.update(self.config.environment)
|
270
|
+
|
271
|
+
#
|
272
|
+
|
273
|
+
cwd = self.config.directory
|
274
|
+
try:
|
275
|
+
if cwd is not None:
|
276
|
+
os.chdir(os.path.expanduser(cwd))
|
277
|
+
except OSError as exc:
|
278
|
+
code = errno.errorcode.get(exc.args[0], exc.args[0])
|
279
|
+
msg = f"Couldn't chdir to {cwd}: {code}\n"
|
280
|
+
os.write(2, as_bytes('supervisor: ' + msg))
|
281
|
+
raise RuntimeError(msg) from exc
|
282
|
+
|
283
|
+
#
|
284
|
+
|
285
|
+
try:
|
286
|
+
if self.config.umask is not None:
|
287
|
+
os.umask(self.config.umask)
|
288
|
+
os.execve(exe, list(argv), env)
|
289
|
+
|
290
|
+
except OSError as exc:
|
291
|
+
code = errno.errorcode.get(exc.args[0], exc.args[0])
|
292
|
+
msg = f"Couldn't exec {argv[0]}: {code}\n"
|
293
|
+
os.write(2, as_bytes('supervisor: ' + msg))
|
294
|
+
|
295
|
+
except Exception: # noqa
|
296
|
+
(file, fun, line), t, v, tb = compact_traceback()
|
297
|
+
msg = f"Couldn't exec {exe}: {t}, {v}: file: {file} line: {line}\n"
|
298
|
+
os.write(2, as_bytes('supervisor: ' + msg))
|
299
|
+
|
300
|
+
finally:
|
301
|
+
os.write(2, as_bytes('supervisor: child process was not spawned\n'))
|
302
|
+
real_exit(127) # exit process with code for spawn failure
|
303
|
+
|
304
|
+
raise RuntimeError('Unreachable')
|
305
|
+
|
306
|
+
def _prepare_child_fds(self, pipes: ProcessPipes) -> None:
|
307
|
+
os.dup2(check_not_none(pipes.child_stdin), 0)
|
308
|
+
|
309
|
+
os.dup2(check_not_none(pipes.child_stdout), 1)
|
310
|
+
|
311
|
+
if self.config.redirect_stderr:
|
312
|
+
os.dup2(check_not_none(pipes.child_stdout), 2)
|
313
|
+
else:
|
314
|
+
os.dup2(check_not_none(pipes.child_stderr), 2)
|
315
|
+
|
316
|
+
for i in range(3, self._server_config.minfds):
|
317
|
+
if i in self._inherited_fds:
|
318
|
+
continue
|
319
|
+
close_fd(i)
|
320
|
+
|
321
|
+
def _set_uid(self) -> ta.Optional[str]:
|
322
|
+
if self.config.uid is None:
|
323
|
+
return None
|
324
|
+
|
325
|
+
msg = drop_privileges(self.config.uid)
|
326
|
+
return msg
|
327
|
+
|
328
|
+
|
329
|
+
##
|
330
|
+
|
331
|
+
|
332
|
+
def check_execv_args(
|
333
|
+
exe: str,
|
334
|
+
argv: ta.Sequence[str],
|
335
|
+
st: ta.Optional[os.stat_result],
|
336
|
+
) -> None:
|
337
|
+
if st is None:
|
338
|
+
raise NotFoundError(f"Can't find command {exe!r}")
|
339
|
+
|
340
|
+
elif stat.S_ISDIR(st[stat.ST_MODE]):
|
341
|
+
raise NotExecutableError(f'Command at {exe!r} is a directory')
|
342
|
+
|
343
|
+
elif not (stat.S_IMODE(st[stat.ST_MODE]) & 0o111):
|
344
|
+
raise NotExecutableError(f'Command at {exe!r} is not executable')
|
345
|
+
|
346
|
+
elif not os.access(exe, os.X_OK):
|
347
|
+
raise NoPermissionError(f'No permission to run command {exe!r}')
|