ominfra 0.0.0.dev126__py3-none-any.whl → 0.0.0.dev128__py3-none-any.whl

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