ominfra 0.0.0.dev127__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.
Files changed (36) hide show
  1. ominfra/scripts/supervisor.py +723 -731
  2. ominfra/supervisor/configs.py +34 -11
  3. ominfra/supervisor/context.py +5 -9
  4. ominfra/supervisor/dispatchers.py +4 -3
  5. ominfra/supervisor/dispatchersimpl.py +10 -9
  6. ominfra/supervisor/groups.py +1 -1
  7. ominfra/supervisor/inject.py +5 -5
  8. ominfra/supervisor/main.py +2 -2
  9. ominfra/supervisor/pipes.py +15 -13
  10. ominfra/supervisor/poller.py +36 -35
  11. ominfra/supervisor/{processes.py → process.py} +2 -1
  12. ominfra/supervisor/{processesimpl.py → processimpl.py} +35 -40
  13. ominfra/supervisor/setup.py +1 -1
  14. ominfra/supervisor/setupimpl.py +4 -3
  15. ominfra/supervisor/spawning.py +2 -1
  16. ominfra/supervisor/spawningimpl.py +15 -12
  17. ominfra/supervisor/supervisor.py +16 -8
  18. ominfra/supervisor/types.py +7 -9
  19. ominfra/supervisor/utils/__init__.py +0 -0
  20. ominfra/supervisor/utils/diag.py +31 -0
  21. ominfra/supervisor/utils/fds.py +46 -0
  22. ominfra/supervisor/utils/fs.py +47 -0
  23. ominfra/supervisor/utils/os.py +45 -0
  24. ominfra/supervisor/utils/ostypes.py +9 -0
  25. ominfra/supervisor/utils/strings.py +105 -0
  26. ominfra/supervisor/{users.py → utils/users.py} +11 -8
  27. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev128.dist-info}/METADATA +3 -3
  28. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev128.dist-info}/RECORD +34 -29
  29. ominfra/supervisor/datatypes.py +0 -113
  30. ominfra/supervisor/utils.py +0 -206
  31. /ominfra/supervisor/{collections.py → utils/collections.py} +0 -0
  32. /ominfra/supervisor/{signals.py → utils/signals.py} +0 -0
  33. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev128.dist-info}/LICENSE +0 -0
  34. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev128.dist-info}/WHEEL +0 -0
  35. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev128.dist-info}/entry_points.txt +0 -0
  36. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev128.dist-info}/top_level.txt +0 -0
@@ -11,14 +11,13 @@ from omlish.lite.logs import log
11
11
  from omlish.lite.typing import Func1
12
12
 
13
13
  from .configs import ProcessConfig
14
- from .datatypes import RestartUnconditionally
14
+ from .configs import RestartUnconditionally
15
15
  from .dispatchers import Dispatchers
16
16
  from .events import PROCESS_STATE_EVENT_MAP
17
17
  from .events import EventCallbacks
18
18
  from .pipes import ProcessPipes
19
19
  from .pipes import close_parent_pipes
20
- from .processes import ProcessStateError
21
- from .signals import sig_name
20
+ from .process import ProcessStateError
22
21
  from .spawning import ProcessSpawnError
23
22
  from .spawning import ProcessSpawning
24
23
  from .states import ProcessState
@@ -27,8 +26,10 @@ from .types import InputDispatcher
27
26
  from .types import Process
28
27
  from .types import ProcessGroup
29
28
  from .types import ServerContext
30
- from .utils import as_string
31
- from .utils import decode_wait_status
29
+ from .utils.os import decode_wait_status
30
+ from .utils.ostypes import Pid
31
+ from .utils.ostypes import Rc
32
+ from .utils.signals import sig_name
32
33
 
33
34
 
34
35
  class ProcessSpawningFactory(Func1[Process, ProcessSpawning]):
@@ -66,7 +67,7 @@ class ProcessImpl(Process):
66
67
  self._pipes = ProcessPipes()
67
68
 
68
69
  self._state = ProcessState.STOPPED
69
- self._pid = 0 # 0 when not running
70
+ self._pid = Pid(0) # 0 when not running
70
71
 
71
72
  self._last_start = 0. # Last time the subprocess was started; 0 if never
72
73
  self._last_stop = 0. # Last time the subprocess was stopped; 0 if never
@@ -80,7 +81,7 @@ class ProcessImpl(Process):
80
81
 
81
82
  self._backoff = 0 # backoff counter (to startretries)
82
83
 
83
- self._exitstatus: ta.Optional[int] = None # status attached to dead process by finish()
84
+ self._exitstatus: ta.Optional[Rc] = None # status attached to dead process by finish()
84
85
  self._spawn_err: ta.Optional[str] = None # error message attached by spawn() if any
85
86
 
86
87
  #
@@ -103,7 +104,7 @@ class ProcessImpl(Process):
103
104
  return self._group
104
105
 
105
106
  @property
106
- def pid(self) -> int:
107
+ def pid(self) -> Pid:
107
108
  return self._pid
108
109
 
109
110
  #
@@ -122,11 +123,9 @@ class ProcessImpl(Process):
122
123
 
123
124
  #
124
125
 
125
- def spawn(self) -> ta.Optional[int]:
126
- process_name = as_string(self._config.name)
127
-
126
+ def spawn(self) -> ta.Optional[Pid]:
128
127
  if self.pid:
129
- log.warning('process \'%s\' already running', process_name)
128
+ log.warning('process \'%s\' already running', self.name)
130
129
  return None
131
130
 
132
131
  self.check_in_state(
@@ -249,7 +248,7 @@ class ProcessImpl(Process):
249
248
  self._check_and_adjust_for_system_clock_rollback(now)
250
249
 
251
250
  if now > (self._last_stop_report + 2): # every 2 seconds
252
- log.info('waiting for %s to stop', as_string(self._config.name))
251
+ log.info('waiting for %s to stop', self.name)
253
252
  self._last_stop_report = now
254
253
 
255
254
  def give_up(self) -> None:
@@ -269,18 +268,17 @@ class ProcessImpl(Process):
269
268
  """
270
269
  now = time.time()
271
270
 
272
- process_name = as_string(self._config.name)
273
271
  # If the process is in BACKOFF and we want to stop or kill it, then BACKOFF -> STOPPED. This is needed because
274
272
  # if startretries is a large number and the process isn't starting successfully, the stop request would be
275
273
  # blocked for a long time waiting for the retries.
276
274
  if self._state == ProcessState.BACKOFF:
277
- log.debug('Attempted to kill %s, which is in BACKOFF state.', process_name)
275
+ log.debug('Attempted to kill %s, which is in BACKOFF state.', self.name)
278
276
  self.change_state(ProcessState.STOPPED)
279
277
  return None
280
278
 
281
279
  args: tuple
282
280
  if not self.pid:
283
- fmt, args = "attempted to kill %s with sig %s but it wasn't running", (process_name, sig_name(sig))
281
+ fmt, args = "attempted to kill %s with sig %s but it wasn't running", (self.name, sig_name(sig))
284
282
  log.debug(fmt, *args)
285
283
  return fmt % args
286
284
 
@@ -294,7 +292,7 @@ class ProcessImpl(Process):
294
292
  if killasgroup:
295
293
  as_group = 'process group '
296
294
 
297
- log.debug('killing %s (pid %s) %s with signal %s', process_name, self.pid, as_group, sig_name(sig))
295
+ log.debug('killing %s (pid %s) %s with signal %s', self.name, self.pid, as_group, sig_name(sig))
298
296
 
299
297
  # RUNNING/STARTING/STOPPING -> STOPPING
300
298
  self._killing = True
@@ -303,24 +301,24 @@ class ProcessImpl(Process):
303
301
  self.check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
304
302
  self.change_state(ProcessState.STOPPING)
305
303
 
306
- pid = self.pid
304
+ kpid = int(self.pid)
307
305
  if killasgroup:
308
306
  # send to the whole process group instead
309
- pid = -self.pid
307
+ kpid = -kpid
310
308
 
311
309
  try:
312
310
  try:
313
- os.kill(pid, sig)
311
+ os.kill(kpid, sig)
314
312
  except OSError as exc:
315
313
  if exc.errno == errno.ESRCH:
316
- log.debug('unable to signal %s (pid %s), it probably just exited on its own: %s', process_name, self.pid, str(exc)) # noqa
314
+ log.debug('unable to signal %s (pid %s), it probably just exited on its own: %s', self.name, self.pid, str(exc)) # noqa
317
315
  # we could change the state here but we intentionally do not. we will do it during normal SIGCHLD
318
316
  # processing.
319
317
  return None
320
318
  raise
321
319
  except Exception: # noqa
322
320
  tb = traceback.format_exc()
323
- fmt, args = 'unknown problem killing %s (%s):%s', (process_name, self.pid, tb)
321
+ fmt, args = 'unknown problem killing %s (%s):%s', (self.name, self.pid, tb)
324
322
  log.critical(fmt, *args)
325
323
  self.change_state(ProcessState.UNKNOWN)
326
324
  self._killing = False
@@ -336,14 +334,13 @@ class ProcessImpl(Process):
336
334
  Return None if the signal was sent, or an error message string if an error occurred or if the subprocess is not
337
335
  running.
338
336
  """
339
- process_name = as_string(self._config.name)
340
337
  args: tuple
341
338
  if not self.pid:
342
- fmt, args = "attempted to send %s sig %s but it wasn't running", (process_name, sig_name(sig))
339
+ fmt, args = "Attempted to send %s sig %s but it wasn't running", (self.name, sig_name(sig))
343
340
  log.debug(fmt, *args)
344
341
  return fmt % args
345
342
 
346
- log.debug('sending %s (pid %s) sig %s', process_name, self.pid, sig_name(sig))
343
+ log.debug('sending %s (pid %s) sig %s', self.name, self.pid, sig_name(sig))
347
344
 
348
345
  self.check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
349
346
 
@@ -354,7 +351,7 @@ class ProcessImpl(Process):
354
351
  if exc.errno == errno.ESRCH:
355
352
  log.debug(
356
353
  'unable to signal %s (pid %s), it probably just now exited on its own: %s',
357
- process_name,
354
+ self.name,
358
355
  self.pid,
359
356
  str(exc),
360
357
  )
@@ -364,14 +361,14 @@ class ProcessImpl(Process):
364
361
  raise
365
362
  except Exception: # noqa
366
363
  tb = traceback.format_exc()
367
- fmt, args = 'unknown problem sending sig %s (%s):%s', (process_name, self.pid, tb)
364
+ fmt, args = 'unknown problem sending sig %s (%s):%s', (self.name, self.pid, tb)
368
365
  log.critical(fmt, *args)
369
366
  self.change_state(ProcessState.UNKNOWN)
370
367
  return fmt % args
371
368
 
372
369
  return None
373
370
 
374
- def finish(self, sts: int) -> None:
371
+ def finish(self, sts: Rc) -> None:
375
372
  """The process was reaped and we need to report and manage its state."""
376
373
 
377
374
  self._dispatchers.drain()
@@ -383,7 +380,6 @@ class ProcessImpl(Process):
383
380
  self._check_and_adjust_for_system_clock_rollback(now)
384
381
 
385
382
  self._last_stop = now
386
- process_name = as_string(self._config.name)
387
383
 
388
384
  if now > self._last_start:
389
385
  too_quickly = now - self._last_start < self._config.startsecs
@@ -392,7 +388,7 @@ class ProcessImpl(Process):
392
388
  log.warning(
393
389
  "process '%s' (%s) last_start time is in the future, don't know how long process was running so "
394
390
  "assuming it did not exit too quickly",
395
- process_name,
391
+ self.name,
396
392
  self.pid,
397
393
  )
398
394
 
@@ -402,9 +398,9 @@ class ProcessImpl(Process):
402
398
  # likely the result of a stop request implies STOPPING -> STOPPED
403
399
  self._killing = False
404
400
  self._delay = 0
405
- self._exitstatus = es
401
+ self._exitstatus = Rc(es)
406
402
 
407
- fmt, args = 'stopped: %s (%s)', (process_name, msg)
403
+ fmt, args = 'stopped: %s (%s)', (self.name, msg)
408
404
  self.check_in_state(ProcessState.STOPPING)
409
405
  self.change_state(ProcessState.STOPPED)
410
406
  if exit_expected:
@@ -418,7 +414,7 @@ class ProcessImpl(Process):
418
414
  self._spawn_err = 'Exited too quickly (process log may have details)'
419
415
  self.check_in_state(ProcessState.STARTING)
420
416
  self.change_state(ProcessState.BACKOFF)
421
- log.warning('exited: %s (%s)', process_name, msg + '; not expected')
417
+ log.warning('exited: %s (%s)', self.name, msg + '; not expected')
422
418
 
423
419
  else:
424
420
  # this finish was not the result of a stop request, the program was in the RUNNING state but exited implies
@@ -437,14 +433,14 @@ class ProcessImpl(Process):
437
433
  if exit_expected:
438
434
  # expected exit code
439
435
  self.change_state(ProcessState.EXITED, expected=True)
440
- log.info('exited: %s (%s)', process_name, msg + '; expected')
436
+ log.info('exited: %s (%s)', self.name, msg + '; expected')
441
437
  else:
442
438
  # unexpected exit code
443
439
  self._spawn_err = f'Bad exit code {es}'
444
440
  self.change_state(ProcessState.EXITED, expected=False)
445
- log.warning('exited: %s (%s)', process_name, msg + '; not expected')
441
+ log.warning('exited: %s (%s)', self.name, msg + '; not expected')
446
442
 
447
- self._pid = 0
443
+ self._pid = Pid(0)
448
444
  close_parent_pipes(self._pipes)
449
445
  self._pipes = ProcessPipes()
450
446
  self._dispatchers = Dispatchers([])
@@ -482,7 +478,6 @@ class ProcessImpl(Process):
482
478
  # BACKOFF -> STARTING
483
479
  self.spawn()
484
480
 
485
- process_name = as_string(self._config.name)
486
481
  if state == ProcessState.STARTING:
487
482
  if now - self._last_start > self._config.startsecs:
488
483
  # STARTING -> RUNNING if the proc has started successfully and it has stayed up for at least
@@ -492,21 +487,21 @@ class ProcessImpl(Process):
492
487
  self.check_in_state(ProcessState.STARTING)
493
488
  self.change_state(ProcessState.RUNNING)
494
489
  msg = ('entered RUNNING state, process has stayed up for > than %s seconds (startsecs)' % self._config.startsecs) # noqa
495
- logger.info('success: %s %s', process_name, msg)
490
+ logger.info('success: %s %s', self.name, msg)
496
491
 
497
492
  if state == ProcessState.BACKOFF:
498
493
  if self._backoff > self._config.startretries:
499
494
  # BACKOFF -> FATAL if the proc has exceeded its number of retries
500
495
  self.give_up()
501
496
  msg = ('entered FATAL state, too many start retries too quickly')
502
- logger.info('gave up: %s %s', process_name, msg)
497
+ logger.info('gave up: %s %s', self.name, msg)
503
498
 
504
499
  elif state == ProcessState.STOPPING:
505
500
  time_left = self._delay - now
506
501
  if time_left <= 0:
507
502
  # kill processes which are taking too long to stop with a final sigkill. if this doesn't kill it, the
508
503
  # process will be stuck in the STOPPING state forever.
509
- log.warning('killing \'%s\' (%s) with SIGKILL', process_name, self.pid)
504
+ log.warning('killing \'%s\' (%s) with SIGKILL', self.name, self.pid)
510
505
  self.kill(signal.SIGKILL)
511
506
 
512
507
  def after_setuid(self) -> None:
@@ -2,7 +2,7 @@
2
2
  import abc
3
3
  import typing as ta
4
4
 
5
- from .users import User
5
+ from .utils.users import User
6
6
 
7
7
 
8
8
  ##
@@ -14,8 +14,9 @@ from .setup import DaemonizeListeners
14
14
  from .setup import SupervisorSetup
15
15
  from .setup import SupervisorUser
16
16
  from .types import ServerEpoch
17
- from .utils import real_exit
18
- from .utils import try_unlink
17
+ from .utils.fs import try_unlink
18
+ from .utils.os import real_exit
19
+ from .utils.ostypes import Rc
19
20
 
20
21
 
21
22
  ##
@@ -238,7 +239,7 @@ class SupervisorSetupImpl(SupervisorSetup):
238
239
  if pid != 0:
239
240
  # Parent
240
241
  log.debug('supervisord forked; parent exiting')
241
- real_exit(0)
242
+ real_exit(Rc(0))
242
243
 
243
244
  # Child
244
245
  log.info('daemonizing the supervisord process')
@@ -5,11 +5,12 @@ import dataclasses as dc
5
5
  from .dispatchers import Dispatchers
6
6
  from .pipes import ProcessPipes
7
7
  from .types import Process
8
+ from .utils.ostypes import Pid
8
9
 
9
10
 
10
11
  @dc.dataclass(frozen=True)
11
12
  class SpawnedProcess:
12
- pid: int
13
+ pid: Pid
13
14
  pipes: ProcessPipes
14
15
  dispatchers: Dispatchers
15
16
 
@@ -25,7 +25,7 @@ from .pipes import close_child_pipes
25
25
  from .pipes import close_pipes
26
26
  from .pipes import make_process_pipes
27
27
  from .privileges import drop_privileges
28
- from .processes import PidHistory
28
+ from .process import PidHistory
29
29
  from .spawning import ProcessSpawnError
30
30
  from .spawning import ProcessSpawning
31
31
  from .spawning import SpawnedProcess
@@ -34,22 +34,25 @@ from .types import InputDispatcher
34
34
  from .types import OutputDispatcher
35
35
  from .types import Process
36
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
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
42
45
 
43
46
 
44
- class OutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], int, OutputDispatcher]):
47
+ class OutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], Fd, OutputDispatcher]):
45
48
  pass
46
49
 
47
50
 
48
- class InputDispatcherFactory(Func3[Process, str, int, InputDispatcher]):
51
+ class InputDispatcherFactory(Func3[Process, str, Fd, InputDispatcher]):
49
52
  pass
50
53
 
51
54
 
52
- InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
55
+ InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[Fd])
53
56
 
54
57
 
55
58
  ##
@@ -120,7 +123,7 @@ class ProcessSpawningImpl(ProcessSpawning):
120
123
  raise ProcessSpawnError(f"Unknown error making dispatchers for '{self.process.name}': {exc}") from exc
121
124
 
122
125
  try:
123
- pid = os.fork()
126
+ pid = Pid(os.fork())
124
127
  except OSError as exc:
125
128
  code = exc.args[0]
126
129
  if code == errno.EAGAIN:
@@ -299,7 +302,7 @@ class ProcessSpawningImpl(ProcessSpawning):
299
302
 
300
303
  finally:
301
304
  os.write(2, as_bytes('supervisor: child process was not spawned\n'))
302
- real_exit(127) # exit process with code for spawn failure
305
+ real_exit(Rc(127)) # exit process with code for spawn failure
303
306
 
304
307
  raise RuntimeError('Unreachable')
305
308
 
@@ -316,7 +319,7 @@ class ProcessSpawningImpl(ProcessSpawning):
316
319
  for i in range(3, self._server_config.minfds):
317
320
  if i in self._inherited_fds:
318
321
  continue
319
- close_fd(i)
322
+ close_fd(Fd(i))
320
323
 
321
324
  def _set_uid(self) -> ta.Optional[str]:
322
325
  if self.config.uid is None:
@@ -18,17 +18,25 @@ from .events import SupervisorStoppingEvent
18
18
  from .groups import ProcessGroup
19
19
  from .groups import ProcessGroupManager
20
20
  from .poller import Poller
21
- from .processes import PidHistory
21
+ from .process import PidHistory
22
22
  from .setup import SupervisorSetup
23
- from .signals import SignalReceiver
24
- from .signals import sig_name
25
23
  from .states import SupervisorState
26
24
  from .types import OutputDispatcher
27
25
  from .types import Process
28
- from .utils import ExitNow
29
- from .utils import as_string
30
- from .utils import decode_wait_status
31
- from .utils import timeslice
26
+ from .utils.os import decode_wait_status
27
+ from .utils.signals import SignalReceiver
28
+ from .utils.signals import sig_name
29
+
30
+
31
+ ##
32
+
33
+
34
+ class ExitNow(Exception): # noqa
35
+ pass
36
+
37
+
38
+ def timeslice(period: int, when: float) -> int:
39
+ return int(when - (when % period))
32
40
 
33
41
 
34
42
  ##
@@ -168,7 +176,7 @@ class Supervisor:
168
176
  # throttle 'waiting for x to die' reports
169
177
  now = time.time()
170
178
  if now > (self._last_shutdown_report + 3): # every 3 secs
171
- names = [as_string(p.config.name) for p in unstopped]
179
+ names = [p.config.name for p in unstopped]
172
180
  namestr = ', '.join(names)
173
181
  log.info('waiting for %s to die', namestr)
174
182
  self._last_shutdown_report = now
@@ -3,12 +3,15 @@ import abc
3
3
  import functools
4
4
  import typing as ta
5
5
 
6
- from .collections import KeyedCollectionAccessors
7
6
  from .configs import ProcessConfig
8
7
  from .configs import ProcessGroupConfig
9
8
  from .configs import ServerConfig
10
9
  from .states import ProcessState
11
10
  from .states import SupervisorState
11
+ from .utils.collections import KeyedCollectionAccessors
12
+ from .utils.ostypes import Fd
13
+ from .utils.ostypes import Pid
14
+ from .utils.ostypes import Rc
12
15
 
13
16
 
14
17
  if ta.TYPE_CHECKING:
@@ -56,11 +59,6 @@ class ServerContext(abc.ABC):
56
59
  def set_state(self, state: SupervisorState) -> None:
57
60
  raise NotImplementedError
58
61
 
59
- @property
60
- @abc.abstractmethod
61
- def pid_history(self) -> ta.Dict[int, 'Process']:
62
- raise NotImplementedError
63
-
64
62
 
65
63
  ##
66
64
 
@@ -78,7 +76,7 @@ class Dispatcher(abc.ABC):
78
76
 
79
77
  @property
80
78
  @abc.abstractmethod
81
- def fd(self) -> int:
79
+ def fd(self) -> Fd:
82
80
  raise NotImplementedError
83
81
 
84
82
  @property
@@ -156,7 +154,7 @@ class Process(ConfigPriorityOrdered, abc.ABC):
156
154
 
157
155
  @property
158
156
  @abc.abstractmethod
159
- def pid(self) -> int:
157
+ def pid(self) -> Pid:
160
158
  raise NotImplementedError
161
159
 
162
160
  #
@@ -167,7 +165,7 @@ class Process(ConfigPriorityOrdered, abc.ABC):
167
165
  raise NotImplementedError
168
166
 
169
167
  @abc.abstractmethod
170
- def finish(self, sts: int) -> None:
168
+ def finish(self, sts: Rc) -> None:
171
169
  raise NotImplementedError
172
170
 
173
171
  @abc.abstractmethod
File without changes
@@ -0,0 +1,31 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import sys
3
+ import types
4
+ import typing as ta
5
+
6
+
7
+ def compact_traceback() -> ta.Tuple[
8
+ ta.Tuple[str, str, int],
9
+ ta.Type[BaseException],
10
+ BaseException,
11
+ types.TracebackType,
12
+ ]:
13
+ t, v, tb = sys.exc_info()
14
+ if not tb:
15
+ raise RuntimeError('No traceback')
16
+
17
+ tbinfo = []
18
+ while tb:
19
+ tbinfo.append((
20
+ tb.tb_frame.f_code.co_filename,
21
+ tb.tb_frame.f_code.co_name,
22
+ str(tb.tb_lineno),
23
+ ))
24
+ tb = tb.tb_next
25
+
26
+ # just to be safe
27
+ del tb
28
+
29
+ file, function, line = tbinfo[-1]
30
+ info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo]) # noqa
31
+ return (file, function, line), t, v, info # type: ignore
@@ -0,0 +1,46 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import errno
3
+ import os
4
+ import typing as ta
5
+
6
+ from .ostypes import Fd
7
+
8
+
9
+ class PipeFds(ta.NamedTuple):
10
+ r: Fd
11
+ w: Fd
12
+
13
+
14
+ def make_pipe() -> PipeFds:
15
+ return PipeFds(*os.pipe()) # type: ignore
16
+
17
+
18
+ def read_fd(fd: Fd) -> bytes:
19
+ try:
20
+ data = os.read(fd, 2 << 16) # 128K
21
+ except OSError as why:
22
+ if why.args[0] not in (errno.EWOULDBLOCK, errno.EBADF, errno.EINTR):
23
+ raise
24
+ data = b''
25
+ return data
26
+
27
+
28
+ def close_fd(fd: Fd) -> bool:
29
+ try:
30
+ os.close(fd)
31
+ except OSError:
32
+ return False
33
+ return True
34
+
35
+
36
+ def is_fd_open(fd: Fd) -> bool:
37
+ try:
38
+ n = os.dup(fd)
39
+ except OSError:
40
+ return False
41
+ os.close(n)
42
+ return True
43
+
44
+
45
+ def get_open_fds(limit: int) -> ta.FrozenSet[Fd]:
46
+ return frozenset(fd for i in range(limit) if is_fd_open(fd := Fd(i)))
@@ -0,0 +1,47 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import os
3
+ import tempfile
4
+ import typing as ta
5
+
6
+
7
+ def try_unlink(path: str) -> bool:
8
+ try:
9
+ os.unlink(path)
10
+ except OSError:
11
+ return False
12
+ return True
13
+
14
+
15
+ def mktempfile(suffix: str, prefix: str, dir: str) -> str: # noqa
16
+ fd, filename = tempfile.mkstemp(suffix, prefix, dir)
17
+ os.close(fd)
18
+ return filename
19
+
20
+
21
+ def get_path() -> ta.Sequence[str]:
22
+ """Return a list corresponding to $PATH, or a default."""
23
+
24
+ path = ['/bin', '/usr/bin', '/usr/local/bin']
25
+ if 'PATH' in os.environ:
26
+ p = os.environ['PATH']
27
+ if p:
28
+ path = p.split(os.pathsep)
29
+ return path
30
+
31
+
32
+ def check_existing_dir(v: str) -> str:
33
+ nv = os.path.expanduser(v)
34
+ if os.path.isdir(nv):
35
+ return nv
36
+ raise ValueError(f'{v} is not an existing directory')
37
+
38
+
39
+ def check_path_with_existing_dir(v: str) -> str:
40
+ nv = os.path.expanduser(v)
41
+ dir = os.path.dirname(nv) # noqa
42
+ if not dir:
43
+ # relative pathname with no directory component
44
+ return nv
45
+ if os.path.isdir(dir):
46
+ return nv
47
+ raise ValueError(f'The directory named as part of the path {v} does not exist')
@@ -0,0 +1,45 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import os
3
+ import typing as ta
4
+
5
+ from .ostypes import Rc
6
+ from .signals import sig_name
7
+
8
+
9
+ ##
10
+
11
+
12
+ def real_exit(code: Rc) -> None:
13
+ os._exit(code) # noqa
14
+
15
+
16
+ ##
17
+
18
+
19
+ def decode_wait_status(sts: int) -> ta.Tuple[Rc, str]:
20
+ """
21
+ Decode the status returned by wait() or waitpid().
22
+
23
+ Return a tuple (exitstatus, message) where exitstatus is the exit status, or -1 if the process was killed by a
24
+ signal; and message is a message telling what happened. It is the caller's responsibility to display the message.
25
+ """
26
+
27
+ if os.WIFEXITED(sts):
28
+ es = os.WEXITSTATUS(sts) & 0xffff
29
+ msg = f'exit status {es}'
30
+ return Rc(es), msg
31
+
32
+ elif os.WIFSIGNALED(sts):
33
+ sig = os.WTERMSIG(sts)
34
+ msg = f'terminated by {sig_name(sig)}'
35
+ if hasattr(os, 'WCOREDUMP'):
36
+ iscore = os.WCOREDUMP(sts)
37
+ else:
38
+ iscore = bool(sts & 0x80)
39
+ if iscore:
40
+ msg += ' (core dumped)'
41
+ return Rc(-1), msg
42
+
43
+ else:
44
+ msg = 'unknown termination cause 0x%04x' % sts # noqa
45
+ return Rc(-1), msg
@@ -0,0 +1,9 @@
1
+ import typing as ta
2
+
3
+
4
+ Fd = ta.NewType('Fd', int)
5
+ Pid = ta.NewType('Pid', int)
6
+ Rc = ta.NewType('Rc', int)
7
+
8
+ Uid = ta.NewType('Uid', int)
9
+ Gid = ta.NewType('Gid', int)