ominfra 0.0.0.dev125__py3-none-any.whl → 0.0.0.dev127__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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}')
|