ominfra 0.0.0.dev126__py3-none-any.whl → 0.0.0.dev128__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.
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}')