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.
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}')