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.
- ominfra/clouds/aws/auth.py +1 -1
- ominfra/deploy/_executor.py +1 -1
- ominfra/deploy/poly/_main.py +1 -1
- ominfra/pyremote/_runcommands.py +1 -1
- ominfra/scripts/journald2aws.py +2 -2
- ominfra/scripts/supervisor.py +4736 -4166
- ominfra/supervisor/configs.py +34 -11
- ominfra/supervisor/context.py +7 -345
- ominfra/supervisor/dispatchers.py +21 -324
- ominfra/supervisor/dispatchersimpl.py +343 -0
- ominfra/supervisor/groups.py +33 -111
- ominfra/supervisor/groupsimpl.py +86 -0
- ominfra/supervisor/inject.py +45 -20
- ominfra/supervisor/main.py +3 -3
- ominfra/supervisor/pipes.py +85 -0
- ominfra/supervisor/poller.py +42 -38
- ominfra/supervisor/privileges.py +65 -0
- ominfra/supervisor/process.py +6 -742
- ominfra/supervisor/processimpl.py +516 -0
- ominfra/supervisor/setup.py +38 -0
- ominfra/supervisor/setupimpl.py +262 -0
- ominfra/supervisor/spawning.py +32 -0
- ominfra/supervisor/spawningimpl.py +350 -0
- ominfra/supervisor/supervisor.py +67 -84
- ominfra/supervisor/types.py +101 -47
- ominfra/supervisor/utils/__init__.py +0 -0
- ominfra/supervisor/utils/collections.py +52 -0
- ominfra/supervisor/utils/diag.py +31 -0
- ominfra/supervisor/utils/fds.py +46 -0
- ominfra/supervisor/utils/fs.py +47 -0
- ominfra/supervisor/utils/os.py +45 -0
- ominfra/supervisor/utils/ostypes.py +9 -0
- ominfra/supervisor/utils/signals.py +60 -0
- ominfra/supervisor/utils/strings.py +105 -0
- ominfra/supervisor/utils/users.py +67 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/RECORD +41 -25
- ominfra/supervisor/datatypes.py +0 -175
- ominfra/supervisor/signals.py +0 -52
- ominfra/supervisor/utils.py +0 -206
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/top_level.txt +0 -0
ominfra/supervisor/configs.py
CHANGED
@@ -7,11 +7,21 @@ import typing as ta
|
|
7
7
|
|
8
8
|
from ..configs import ConfigMapping
|
9
9
|
from ..configs import build_config_named_children
|
10
|
-
from .
|
11
|
-
from .
|
12
|
-
from .
|
13
|
-
from .
|
14
|
-
|
10
|
+
from .utils.fs import check_existing_dir
|
11
|
+
from .utils.fs import check_path_with_existing_dir
|
12
|
+
from .utils.strings import parse_bytes_size
|
13
|
+
from .utils.strings import parse_octal
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
class RestartWhenExitUnexpected:
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
class RestartUnconditionally:
|
24
|
+
pass
|
15
25
|
|
16
26
|
|
17
27
|
##
|
@@ -104,12 +114,12 @@ class ServerConfig:
|
|
104
114
|
**kwargs: ta.Any,
|
105
115
|
) -> 'ServerConfig':
|
106
116
|
return cls(
|
107
|
-
umask=
|
108
|
-
directory=
|
109
|
-
logfile=
|
110
|
-
logfile_maxbytes=
|
111
|
-
loglevel=
|
112
|
-
pidfile=
|
117
|
+
umask=parse_octal(umask),
|
118
|
+
directory=check_existing_dir(directory) if directory is not None else None,
|
119
|
+
logfile=check_path_with_existing_dir(logfile),
|
120
|
+
logfile_maxbytes=parse_bytes_size(logfile_maxbytes),
|
121
|
+
loglevel=parse_logging_level(loglevel),
|
122
|
+
pidfile=check_path_with_existing_dir(pidfile),
|
113
123
|
child_logdir=child_logdir if child_logdir else tempfile.gettempdir(),
|
114
124
|
**kwargs,
|
115
125
|
)
|
@@ -129,3 +139,16 @@ def prepare_server_config(dct: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.An
|
|
129
139
|
group_dcts = build_config_named_children(out.get('groups'))
|
130
140
|
out['groups'] = [prepare_process_group_config(group_dct) for group_dct in group_dcts or []]
|
131
141
|
return out
|
142
|
+
|
143
|
+
|
144
|
+
##
|
145
|
+
|
146
|
+
|
147
|
+
def parse_logging_level(value: ta.Union[str, int]) -> int:
|
148
|
+
if isinstance(value, int):
|
149
|
+
return value
|
150
|
+
s = str(value).lower()
|
151
|
+
level = logging.getLevelNamesMapping().get(s.upper())
|
152
|
+
if level is None:
|
153
|
+
raise ValueError(f'bad logging level name {value!r}')
|
154
|
+
return level
|
ominfra/supervisor/context.py
CHANGED
@@ -1,34 +1,18 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import errno
|
3
|
-
import fcntl
|
4
|
-
import grp
|
5
3
|
import os
|
6
|
-
import pwd
|
7
|
-
import re
|
8
|
-
import resource
|
9
|
-
import stat
|
10
4
|
import typing as ta
|
11
|
-
import warnings
|
12
5
|
|
13
6
|
from omlish.lite.logs import log
|
14
7
|
|
15
8
|
from .configs import ServerConfig
|
16
|
-
from .datatypes import gid_for_uid
|
17
|
-
from .datatypes import name_to_uid
|
18
|
-
from .exceptions import NoPermissionError
|
19
|
-
from .exceptions import NotExecutableError
|
20
|
-
from .exceptions import NotFoundError
|
21
9
|
from .poller import Poller
|
22
10
|
from .states import SupervisorState
|
23
|
-
from .types import Process
|
24
11
|
from .types import ServerContext
|
25
|
-
from .
|
26
|
-
from .utils import mktempfile
|
27
|
-
from .utils import
|
28
|
-
from .utils import
|
29
|
-
|
30
|
-
|
31
|
-
ServerEpoch = ta.NewType('ServerEpoch', int)
|
12
|
+
from .types import ServerEpoch
|
13
|
+
from .utils.fs import mktempfile
|
14
|
+
from .utils.ostypes import Pid
|
15
|
+
from .utils.ostypes import Rc
|
32
16
|
|
33
17
|
|
34
18
|
class ServerContextImpl(ServerContext):
|
@@ -45,19 +29,8 @@ class ServerContextImpl(ServerContext):
|
|
45
29
|
self._poller = poller
|
46
30
|
self._epoch = epoch
|
47
31
|
|
48
|
-
self._pid_history: ta.Dict[int, Process] = {}
|
49
32
|
self._state: SupervisorState = SupervisorState.RUNNING
|
50
33
|
|
51
|
-
if config.user is not None:
|
52
|
-
uid = name_to_uid(config.user)
|
53
|
-
self._uid: ta.Optional[int] = uid
|
54
|
-
self._gid: ta.Optional[int] = gid_for_uid(uid)
|
55
|
-
else:
|
56
|
-
self._uid = None
|
57
|
-
self._gid = None
|
58
|
-
|
59
|
-
self._unlink_pidfile = False
|
60
|
-
|
61
34
|
@property
|
62
35
|
def config(self) -> ServerConfig:
|
63
36
|
return self._config
|
@@ -77,21 +50,9 @@ class ServerContextImpl(ServerContext):
|
|
77
50
|
def set_state(self, state: SupervisorState) -> None:
|
78
51
|
self._state = state
|
79
52
|
|
80
|
-
|
81
|
-
def pid_history(self) -> ta.Dict[int, Process]:
|
82
|
-
return self._pid_history
|
83
|
-
|
84
|
-
@property
|
85
|
-
def uid(self) -> ta.Optional[int]:
|
86
|
-
return self._uid
|
87
|
-
|
88
|
-
@property
|
89
|
-
def gid(self) -> ta.Optional[int]:
|
90
|
-
return self._gid
|
53
|
+
#
|
91
54
|
|
92
|
-
|
93
|
-
|
94
|
-
def waitpid(self) -> ta.Tuple[ta.Optional[int], ta.Optional[int]]:
|
55
|
+
def waitpid(self) -> ta.Tuple[ta.Optional[Pid], ta.Optional[Rc]]:
|
95
56
|
# Need pthread_sigmask here to avoid concurrent sigchld, but Python doesn't offer in Python < 3.4. There is
|
96
57
|
# still a race condition here; we can get a sigchld while we're sitting in the waitpid call. However, AFAICT, if
|
97
58
|
# waitpid is interrupted by SIGCHLD, as long as we call waitpid again (which happens every so often during the
|
@@ -107,167 +68,7 @@ class ServerContextImpl(ServerContext):
|
|
107
68
|
if code == errno.EINTR:
|
108
69
|
log.debug('EINTR during reap')
|
109
70
|
pid, sts = None, None
|
110
|
-
return pid, sts
|
111
|
-
|
112
|
-
def set_uid_or_exit(self) -> None:
|
113
|
-
"""
|
114
|
-
Set the uid of the supervisord process. Called during supervisord startup only. No return value. Exits the
|
115
|
-
process via usage() if privileges could not be dropped.
|
116
|
-
"""
|
117
|
-
|
118
|
-
if self.uid is None:
|
119
|
-
if os.getuid() == 0:
|
120
|
-
warnings.warn(
|
121
|
-
'Supervisor is running as root. Privileges were not dropped because no user is specified in the '
|
122
|
-
'config file. If you intend to run as root, you can set user=root in the config file to avoid '
|
123
|
-
'this message.',
|
124
|
-
)
|
125
|
-
else:
|
126
|
-
msg = drop_privileges(self.uid)
|
127
|
-
if msg is None:
|
128
|
-
log.info('Set uid to user %s succeeded', self.uid)
|
129
|
-
else: # failed to drop privileges
|
130
|
-
raise RuntimeError(msg)
|
131
|
-
|
132
|
-
def set_rlimits_or_exit(self) -> None:
|
133
|
-
"""
|
134
|
-
Set the rlimits of the supervisord process. Called during supervisord startup only. No return value. Exits
|
135
|
-
the process via usage() if any rlimits could not be set.
|
136
|
-
"""
|
137
|
-
|
138
|
-
limits = []
|
139
|
-
|
140
|
-
if hasattr(resource, 'RLIMIT_NOFILE'):
|
141
|
-
limits.append({
|
142
|
-
'msg': (
|
143
|
-
'The minimum number of file descriptors required to run this process is %(min_limit)s as per the '
|
144
|
-
'"minfds" command-line argument or config file setting. The current environment will only allow '
|
145
|
-
'you to open %(hard)s file descriptors. Either raise the number of usable file descriptors in '
|
146
|
-
'your environment (see README.rst) or lower the minfds setting in the config file to allow the '
|
147
|
-
'process to start.'
|
148
|
-
),
|
149
|
-
'min': self.config.minfds,
|
150
|
-
'resource': resource.RLIMIT_NOFILE,
|
151
|
-
'name': 'RLIMIT_NOFILE',
|
152
|
-
})
|
153
|
-
|
154
|
-
if hasattr(resource, 'RLIMIT_NPROC'):
|
155
|
-
limits.append({
|
156
|
-
'msg': (
|
157
|
-
'The minimum number of available processes required to run this program is %(min_limit)s as per '
|
158
|
-
'the "minprocs" command-line argument or config file setting. The current environment will only '
|
159
|
-
'allow you to open %(hard)s processes. Either raise the number of usable processes in your '
|
160
|
-
'environment (see README.rst) or lower the minprocs setting in the config file to allow the '
|
161
|
-
'program to start.'
|
162
|
-
),
|
163
|
-
'min': self.config.minprocs,
|
164
|
-
'resource': resource.RLIMIT_NPROC,
|
165
|
-
'name': 'RLIMIT_NPROC',
|
166
|
-
})
|
167
|
-
|
168
|
-
for limit in limits:
|
169
|
-
min_limit = limit['min']
|
170
|
-
res = limit['resource']
|
171
|
-
msg = limit['msg']
|
172
|
-
name = limit['name']
|
173
|
-
|
174
|
-
soft, hard = resource.getrlimit(res) # type: ignore
|
175
|
-
|
176
|
-
# -1 means unlimited
|
177
|
-
if soft < min_limit and soft != -1: # type: ignore
|
178
|
-
if hard < min_limit and hard != -1: # type: ignore
|
179
|
-
# setrlimit should increase the hard limit if we are root, if not then setrlimit raises and we print
|
180
|
-
# usage
|
181
|
-
hard = min_limit # type: ignore
|
182
|
-
|
183
|
-
try:
|
184
|
-
resource.setrlimit(res, (min_limit, hard)) # type: ignore
|
185
|
-
log.info('Increased %s limit to %s', name, min_limit)
|
186
|
-
except (resource.error, ValueError):
|
187
|
-
raise RuntimeError(msg % dict( # type: ignore # noqa
|
188
|
-
min_limit=min_limit,
|
189
|
-
res=res,
|
190
|
-
name=name,
|
191
|
-
soft=soft,
|
192
|
-
hard=hard,
|
193
|
-
))
|
194
|
-
|
195
|
-
def cleanup(self) -> None:
|
196
|
-
if self._unlink_pidfile:
|
197
|
-
try_unlink(self.config.pidfile)
|
198
|
-
self._poller.close()
|
199
|
-
|
200
|
-
def cleanup_fds(self) -> None:
|
201
|
-
# try to close any leaked file descriptors (for reload)
|
202
|
-
start = 5
|
203
|
-
os.closerange(start, self.config.minfds)
|
204
|
-
|
205
|
-
def clear_auto_child_logdir(self) -> None:
|
206
|
-
# must be called after realize()
|
207
|
-
child_logdir = self.config.child_logdir
|
208
|
-
fnre = re.compile(rf'.+?---{self.config.identifier}-\S+\.log\.?\d{{0,4}}')
|
209
|
-
try:
|
210
|
-
filenames = os.listdir(child_logdir)
|
211
|
-
except OSError:
|
212
|
-
log.warning('Could not clear child_log dir')
|
213
|
-
return
|
214
|
-
|
215
|
-
for filename in filenames:
|
216
|
-
if fnre.match(filename):
|
217
|
-
pathname = os.path.join(child_logdir, filename)
|
218
|
-
try:
|
219
|
-
os.remove(pathname)
|
220
|
-
except OSError:
|
221
|
-
log.warning('Failed to clean up %r', pathname)
|
222
|
-
|
223
|
-
def daemonize(self) -> None:
|
224
|
-
self._poller.before_daemonize()
|
225
|
-
self._daemonize()
|
226
|
-
self._poller.after_daemonize()
|
227
|
-
|
228
|
-
def _daemonize(self) -> None:
|
229
|
-
# To daemonize, we need to become the leader of our own session (process) group. If we do not, signals sent to
|
230
|
-
# our parent process will also be sent to us. This might be bad because signals such as SIGINT can be sent to
|
231
|
-
# our parent process during normal (uninteresting) operations such as when we press Ctrl-C in the parent
|
232
|
-
# terminal window to escape from a logtail command. To disassociate ourselves from our parent's session group we
|
233
|
-
# use os.setsid. It means "set session id", which has the effect of disassociating a process from is current
|
234
|
-
# session and process group and setting itself up as a new session leader.
|
235
|
-
#
|
236
|
-
# Unfortunately we cannot call setsid if we're already a session group leader, so we use "fork" to make a copy
|
237
|
-
# of ourselves that is guaranteed to not be a session group leader.
|
238
|
-
#
|
239
|
-
# We also change directories, set stderr and stdout to null, and change our umask.
|
240
|
-
#
|
241
|
-
# This explanation was (gratefully) garnered from
|
242
|
-
# http://www.cems.uwe.ac.uk/~irjohnso/coursenotes/lrc/system/daemons/d3.htm
|
243
|
-
|
244
|
-
pid = os.fork()
|
245
|
-
if pid != 0:
|
246
|
-
# Parent
|
247
|
-
log.debug('supervisord forked; parent exiting')
|
248
|
-
real_exit(0)
|
249
|
-
|
250
|
-
# Child
|
251
|
-
log.info('daemonizing the supervisord process')
|
252
|
-
if self.config.directory:
|
253
|
-
try:
|
254
|
-
os.chdir(self.config.directory)
|
255
|
-
except OSError as err:
|
256
|
-
log.critical("can't chdir into %r: %s", self.config.directory, err)
|
257
|
-
else:
|
258
|
-
log.info('set current directory: %r', self.config.directory)
|
259
|
-
|
260
|
-
os.dup2(0, os.open('/dev/null', os.O_RDONLY))
|
261
|
-
os.dup2(1, os.open('/dev/null', os.O_WRONLY))
|
262
|
-
os.dup2(2, os.open('/dev/null', os.O_WRONLY))
|
263
|
-
|
264
|
-
os.setsid()
|
265
|
-
|
266
|
-
os.umask(self.config.umask)
|
267
|
-
|
268
|
-
# XXX Stevens, in his Advanced Unix book, section 13.3 (page 417) recommends calling umask(0) and closing unused
|
269
|
-
# file descriptors. In his Network Programming book, he additionally recommends ignoring SIGHUP and forking
|
270
|
-
# again after the setsid() call, for obscure SVR4 reasons.
|
71
|
+
return pid, sts # type: ignore
|
271
72
|
|
272
73
|
def get_auto_child_log_name(self, name: str, identifier: str, channel: str) -> str:
|
273
74
|
prefix = f'{name}-{channel}---{identifier}-'
|
@@ -277,142 +78,3 @@ class ServerContextImpl(ServerContext):
|
|
277
78
|
dir=self.config.child_logdir,
|
278
79
|
)
|
279
80
|
return logfile
|
280
|
-
|
281
|
-
def write_pidfile(self) -> None:
|
282
|
-
pid = os.getpid()
|
283
|
-
try:
|
284
|
-
with open(self.config.pidfile, 'w') as f:
|
285
|
-
f.write(f'{pid}\n')
|
286
|
-
except OSError:
|
287
|
-
log.critical('could not write pidfile %s', self.config.pidfile)
|
288
|
-
else:
|
289
|
-
self._unlink_pidfile = True
|
290
|
-
log.info('supervisord started with pid %s', pid)
|
291
|
-
|
292
|
-
|
293
|
-
def drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
|
294
|
-
"""
|
295
|
-
Drop privileges to become the specified user, which may be a username or uid. Called for supervisord startup
|
296
|
-
and when spawning subprocesses. Returns None on success or a string error message if privileges could not be
|
297
|
-
dropped.
|
298
|
-
"""
|
299
|
-
|
300
|
-
if user is None:
|
301
|
-
return 'No user specified to setuid to!'
|
302
|
-
|
303
|
-
# get uid for user, which can be a number or username
|
304
|
-
try:
|
305
|
-
uid = int(user)
|
306
|
-
except ValueError:
|
307
|
-
try:
|
308
|
-
pwrec = pwd.getpwnam(user) # type: ignore
|
309
|
-
except KeyError:
|
310
|
-
return f"Can't find username {user!r}"
|
311
|
-
uid = pwrec[2]
|
312
|
-
else:
|
313
|
-
try:
|
314
|
-
pwrec = pwd.getpwuid(uid)
|
315
|
-
except KeyError:
|
316
|
-
return f"Can't find uid {uid!r}"
|
317
|
-
|
318
|
-
current_uid = os.getuid()
|
319
|
-
|
320
|
-
if current_uid == uid:
|
321
|
-
# do nothing and return successfully if the uid is already the current one. this allows a supervisord
|
322
|
-
# running as an unprivileged user "foo" to start a process where the config has "user=foo" (same user) in
|
323
|
-
# it.
|
324
|
-
return None
|
325
|
-
|
326
|
-
if current_uid != 0:
|
327
|
-
return "Can't drop privilege as nonroot user"
|
328
|
-
|
329
|
-
gid = pwrec[3]
|
330
|
-
if hasattr(os, 'setgroups'):
|
331
|
-
user = pwrec[0]
|
332
|
-
groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
|
333
|
-
|
334
|
-
# always put our primary gid first in this list, otherwise we can lose group info since sometimes the first
|
335
|
-
# group in the setgroups list gets overwritten on the subsequent setgid call (at least on freebsd 9 with
|
336
|
-
# python 2.7 - this will be safe though for all unix /python version combos)
|
337
|
-
groups.insert(0, gid)
|
338
|
-
try:
|
339
|
-
os.setgroups(groups)
|
340
|
-
except OSError:
|
341
|
-
return 'Could not set groups of effective user'
|
342
|
-
|
343
|
-
try:
|
344
|
-
os.setgid(gid)
|
345
|
-
except OSError:
|
346
|
-
return 'Could not set group id of effective user'
|
347
|
-
|
348
|
-
os.setuid(uid)
|
349
|
-
|
350
|
-
return None
|
351
|
-
|
352
|
-
|
353
|
-
def make_pipes(stderr=True) -> ta.Mapping[str, int]:
|
354
|
-
"""
|
355
|
-
Create pipes for parent to child stdin/stdout/stderr communications. Open fd in non-blocking mode so we can
|
356
|
-
read them in the mainloop without blocking. If stderr is False, don't create a pipe for stderr.
|
357
|
-
"""
|
358
|
-
|
359
|
-
pipes: ta.Dict[str, ta.Optional[int]] = {
|
360
|
-
'child_stdin': None,
|
361
|
-
'stdin': None,
|
362
|
-
'stdout': None,
|
363
|
-
'child_stdout': None,
|
364
|
-
'stderr': None,
|
365
|
-
'child_stderr': None,
|
366
|
-
}
|
367
|
-
|
368
|
-
try:
|
369
|
-
stdin, child_stdin = os.pipe()
|
370
|
-
pipes['child_stdin'], pipes['stdin'] = stdin, child_stdin
|
371
|
-
|
372
|
-
stdout, child_stdout = os.pipe()
|
373
|
-
pipes['stdout'], pipes['child_stdout'] = stdout, child_stdout
|
374
|
-
|
375
|
-
if stderr:
|
376
|
-
stderr, child_stderr = os.pipe()
|
377
|
-
pipes['stderr'], pipes['child_stderr'] = stderr, child_stderr
|
378
|
-
|
379
|
-
for fd in (pipes['stdout'], pipes['stderr'], pipes['stdin']):
|
380
|
-
if fd is not None:
|
381
|
-
flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NDELAY
|
382
|
-
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
|
383
|
-
|
384
|
-
return pipes # type: ignore
|
385
|
-
|
386
|
-
except OSError:
|
387
|
-
for fd in pipes.values():
|
388
|
-
if fd is not None:
|
389
|
-
close_fd(fd)
|
390
|
-
raise
|
391
|
-
|
392
|
-
|
393
|
-
def close_parent_pipes(pipes: ta.Mapping[str, int]) -> None:
|
394
|
-
for fdname in ('stdin', 'stdout', 'stderr'):
|
395
|
-
fd = pipes.get(fdname)
|
396
|
-
if fd is not None:
|
397
|
-
close_fd(fd)
|
398
|
-
|
399
|
-
|
400
|
-
def close_child_pipes(pipes: ta.Mapping[str, int]) -> None:
|
401
|
-
for fdname in ('child_stdin', 'child_stdout', 'child_stderr'):
|
402
|
-
fd = pipes.get(fdname)
|
403
|
-
if fd is not None:
|
404
|
-
close_fd(fd)
|
405
|
-
|
406
|
-
|
407
|
-
def check_execv_args(filename, argv, st) -> None:
|
408
|
-
if st is None:
|
409
|
-
raise NotFoundError(f"can't find command {filename!r}")
|
410
|
-
|
411
|
-
elif stat.S_ISDIR(st[stat.ST_MODE]):
|
412
|
-
raise NotExecutableError(f'command at {filename!r} is a directory')
|
413
|
-
|
414
|
-
elif not (stat.S_IMODE(st[stat.ST_MODE]) & 0o111):
|
415
|
-
raise NotExecutableError(f'command at {filename!r} is not executable')
|
416
|
-
|
417
|
-
elif not os.access(filename, os.X_OK):
|
418
|
-
raise NoPermissionError(f'no permission to run command {filename!r}')
|