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.
Files changed (34) 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 +1825 -1217
  7. ominfra/supervisor/collections.py +52 -0
  8. ominfra/supervisor/context.py +2 -336
  9. ominfra/supervisor/datatypes.py +1 -63
  10. ominfra/supervisor/dispatchers.py +22 -338
  11. ominfra/supervisor/dispatchersimpl.py +342 -0
  12. ominfra/supervisor/groups.py +33 -110
  13. ominfra/supervisor/groupsimpl.py +86 -0
  14. ominfra/supervisor/inject.py +45 -13
  15. ominfra/supervisor/main.py +1 -1
  16. ominfra/supervisor/pipes.py +83 -0
  17. ominfra/supervisor/poller.py +6 -3
  18. ominfra/supervisor/privileges.py +65 -0
  19. ominfra/supervisor/processes.py +18 -0
  20. ominfra/supervisor/{process.py → processesimpl.py} +99 -317
  21. ominfra/supervisor/setup.py +38 -0
  22. ominfra/supervisor/setupimpl.py +261 -0
  23. ominfra/supervisor/signals.py +24 -16
  24. ominfra/supervisor/spawning.py +31 -0
  25. ominfra/supervisor/spawningimpl.py +347 -0
  26. ominfra/supervisor/supervisor.py +54 -78
  27. ominfra/supervisor/types.py +122 -39
  28. ominfra/supervisor/users.py +64 -0
  29. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/METADATA +3 -3
  30. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/RECORD +34 -23
  31. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/LICENSE +0 -0
  32. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/WHEEL +0 -0
  33. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/entry_points.txt +0 -0
  34. {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)
@@ -6,25 +6,33 @@ import typing as ta
6
6
  ##
7
7
 
8
8
 
9
- _SIG_NAMES: ta.Optional[ta.Mapping[int, str]] = None
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 sig_name(sig: int) -> str:
13
- global _SIG_NAMES
14
- if _SIG_NAMES is None:
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
- def _init_sig_names() -> ta.Dict[int, str]:
20
- d = {}
21
- for k, v in signal.__dict__.items(): # noqa
22
- k_startswith = getattr(k, 'startswith', None)
23
- if k_startswith is None:
24
- continue
25
- if k_startswith('SIG') and not k_startswith('SIG_'):
26
- d[v] = k
27
- return d
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}')