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