ominfra 0.0.0.dev121__py3-none-any.whl → 0.0.0.dev123__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/deploy/_executor.py +10 -1
- ominfra/journald/messages.py +1 -1
- ominfra/pyremote/_runcommands.py +10 -1
- ominfra/scripts/journald2aws.py +10 -2
- ominfra/scripts/supervisor.py +2227 -2043
- ominfra/supervisor/context.py +16 -41
- ominfra/supervisor/dispatchers.py +88 -58
- ominfra/supervisor/events.py +59 -70
- ominfra/supervisor/groups.py +162 -0
- ominfra/supervisor/main.py +27 -11
- ominfra/supervisor/poller.py +51 -53
- ominfra/supervisor/process.py +267 -352
- ominfra/supervisor/signals.py +52 -0
- ominfra/supervisor/states.py +26 -47
- ominfra/supervisor/supervisor.py +105 -81
- ominfra/supervisor/types.py +57 -6
- ominfra/supervisor/{compat.py → utils.py} +34 -53
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/RECORD +23 -21
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/top_level.txt +0 -0
ominfra/supervisor/process.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
|
-
import dataclasses as dc
|
3
2
|
import errno
|
4
|
-
import functools
|
5
3
|
import os.path
|
6
4
|
import shlex
|
7
5
|
import signal
|
@@ -9,19 +7,10 @@ import time
|
|
9
7
|
import traceback
|
10
8
|
import typing as ta
|
11
9
|
|
10
|
+
from omlish.lite.check import check_isinstance
|
12
11
|
from omlish.lite.logs import log
|
13
12
|
|
14
|
-
from .compat import as_bytes
|
15
|
-
from .compat import as_string
|
16
|
-
from .compat import close_fd
|
17
|
-
from .compat import compact_traceback
|
18
|
-
from .compat import decode_wait_status
|
19
|
-
from .compat import get_path
|
20
|
-
from .compat import real_exit
|
21
|
-
from .compat import signame
|
22
13
|
from .configs import ProcessConfig
|
23
|
-
from .configs import ProcessGroupConfig
|
24
|
-
from .context import ServerContext
|
25
14
|
from .context import check_execv_args
|
26
15
|
from .context import close_child_pipes
|
27
16
|
from .context import close_parent_pipes
|
@@ -31,78 +20,84 @@ from .datatypes import RestartUnconditionally
|
|
31
20
|
from .dispatchers import Dispatcher
|
32
21
|
from .dispatchers import InputDispatcher
|
33
22
|
from .dispatchers import OutputDispatcher
|
34
|
-
from .events import
|
35
|
-
from .events import
|
23
|
+
from .events import PROCESS_STATE_EVENT_MAP
|
24
|
+
from .events import EventCallbacks
|
36
25
|
from .events import ProcessCommunicationEvent
|
37
26
|
from .events import ProcessCommunicationStderrEvent
|
38
27
|
from .events import ProcessCommunicationStdoutEvent
|
39
|
-
from .events import ProcessStateBackoffEvent
|
40
|
-
from .events import ProcessStateEvent
|
41
|
-
from .events import ProcessStateExitedEvent
|
42
|
-
from .events import ProcessStateFatalEvent
|
43
|
-
from .events import ProcessStateRunningEvent
|
44
|
-
from .events import ProcessStateStartingEvent
|
45
|
-
from .events import ProcessStateStoppedEvent
|
46
|
-
from .events import ProcessStateStoppingEvent
|
47
|
-
from .events import ProcessStateUnknownEvent
|
48
28
|
from .exceptions import BadCommandError
|
49
29
|
from .exceptions import ProcessError
|
50
|
-
from .
|
30
|
+
from .signals import sig_name
|
51
31
|
from .states import ProcessState
|
52
|
-
from .states import
|
53
|
-
from .
|
54
|
-
from .states import get_process_state_description
|
32
|
+
from .states import SupervisorState
|
33
|
+
from .types import AbstractProcessGroup
|
55
34
|
from .types import AbstractServerContext
|
56
35
|
from .types import AbstractSubprocess
|
36
|
+
from .utils import as_bytes
|
37
|
+
from .utils import as_string
|
38
|
+
from .utils import close_fd
|
39
|
+
from .utils import compact_traceback
|
40
|
+
from .utils import decode_wait_status
|
41
|
+
from .utils import get_path
|
42
|
+
from .utils import real_exit
|
43
|
+
|
44
|
+
|
45
|
+
InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
|
57
46
|
|
58
47
|
|
59
48
|
##
|
60
49
|
|
61
50
|
|
62
|
-
@functools.total_ordering
|
63
51
|
class Subprocess(AbstractSubprocess):
|
64
52
|
"""A class to manage a subprocess."""
|
65
53
|
|
66
|
-
# Initial state; overridden by instance variables
|
67
|
-
|
68
|
-
# pid = 0 # Subprocess pid; 0 when not running
|
69
|
-
# config = None # ProcessConfig instance
|
70
|
-
# state = None # process state code
|
71
|
-
listener_state = None # listener state code (if we're an event listener)
|
72
|
-
event = None # event currently being processed (if we're an event listener)
|
73
|
-
laststart = 0. # Last time the subprocess was started; 0 if never
|
74
|
-
laststop = 0. # Last time the subprocess was stopped; 0 if never
|
75
|
-
last_stop_report = 0. # Last time "waiting for x to stop" logged, to throttle
|
76
|
-
delay = 0. # If nonzero, delay starting or killing until this time
|
77
|
-
administrative_stop = False # true if process has been stopped by an admin
|
78
|
-
system_stop = False # true if process has been stopped by the system
|
79
|
-
killing = False # true if we are trying to kill this process
|
80
|
-
backoff = 0 # backoff counter (to startretries)
|
81
|
-
dispatchers = None # asyncore output dispatchers (keyed by fd)
|
82
|
-
pipes = None # map of channel name to file descriptor #
|
83
|
-
exitstatus = None # status attached to dead process by finish()
|
84
|
-
spawn_err = None # error message attached by spawn() if any
|
85
|
-
group = None # ProcessGroup instance if process is in the group
|
86
|
-
|
87
54
|
def __init__(
|
88
55
|
self,
|
89
56
|
config: ProcessConfig,
|
90
|
-
group:
|
57
|
+
group: AbstractProcessGroup,
|
58
|
+
*,
|
91
59
|
context: AbstractServerContext,
|
60
|
+
event_callbacks: EventCallbacks,
|
61
|
+
|
62
|
+
inherited_fds: ta.Optional[InheritedFds] = None,
|
92
63
|
) -> None:
|
93
64
|
super().__init__()
|
65
|
+
|
94
66
|
self._config = config
|
95
|
-
self.
|
67
|
+
self._group = group
|
96
68
|
self._context = context
|
97
|
-
self.
|
98
|
-
self.
|
99
|
-
|
100
|
-
self.
|
69
|
+
self._event_callbacks = event_callbacks
|
70
|
+
self._inherited_fds = InheritedFds(frozenset(inherited_fds or []))
|
71
|
+
|
72
|
+
self._dispatchers: ta.Dict[int, Dispatcher] = {}
|
73
|
+
self._pipes: ta.Dict[str, int] = {}
|
74
|
+
|
75
|
+
self._state = ProcessState.STOPPED
|
76
|
+
self._pid = 0 # 0 when not running
|
77
|
+
|
78
|
+
self._last_start = 0. # Last time the subprocess was started; 0 if never
|
79
|
+
self._last_stop = 0. # Last time the subprocess was stopped; 0 if never
|
80
|
+
self._last_stop_report = 0. # Last time "waiting for x to stop" logged, to throttle
|
81
|
+
self._delay = 0. # If nonzero, delay starting or killing until this time
|
82
|
+
|
83
|
+
self._administrative_stop = False # true if process has been stopped by an admin
|
84
|
+
self._system_stop = False # true if process has been stopped by the system
|
85
|
+
|
86
|
+
self._killing = False # true if we are trying to kill this process
|
87
|
+
|
88
|
+
self._backoff = 0 # backoff counter (to startretries)
|
89
|
+
|
90
|
+
self._exitstatus: ta.Optional[int] = None # status attached to dead process by finish()
|
91
|
+
self._spawn_err: ta.Optional[str] = None # error message attached by spawn() if any
|
101
92
|
|
102
93
|
@property
|
103
94
|
def pid(self) -> int:
|
104
95
|
return self._pid
|
105
96
|
|
97
|
+
@property
|
98
|
+
def group(self) -> AbstractProcessGroup:
|
99
|
+
return self._group
|
100
|
+
|
106
101
|
@property
|
107
102
|
def config(self) -> ProcessConfig:
|
108
103
|
return self._config
|
@@ -111,6 +106,17 @@ class Subprocess(AbstractSubprocess):
|
|
111
106
|
def context(self) -> AbstractServerContext:
|
112
107
|
return self._context
|
113
108
|
|
109
|
+
@property
|
110
|
+
def state(self) -> ProcessState:
|
111
|
+
return self._state
|
112
|
+
|
113
|
+
@property
|
114
|
+
def backoff(self) -> int:
|
115
|
+
return self._backoff
|
116
|
+
|
117
|
+
def get_dispatchers(self) -> ta.Mapping[int, Dispatcher]:
|
118
|
+
return self._dispatchers
|
119
|
+
|
114
120
|
def remove_logs(self) -> None:
|
115
121
|
for dispatcher in self._dispatchers.values():
|
116
122
|
if hasattr(dispatcher, 'remove_logs'):
|
@@ -131,18 +137,18 @@ class Subprocess(AbstractSubprocess):
|
|
131
137
|
dispatcher.handle_write_event()
|
132
138
|
|
133
139
|
def write(self, chars: ta.Union[bytes, str]) -> None:
|
134
|
-
if not self.pid or self.
|
140
|
+
if not self.pid or self._killing:
|
135
141
|
raise OSError(errno.EPIPE, 'Process already closed')
|
136
142
|
|
137
143
|
stdin_fd = self._pipes['stdin']
|
138
144
|
if stdin_fd is None:
|
139
145
|
raise OSError(errno.EPIPE, 'Process has no stdin channel')
|
140
146
|
|
141
|
-
dispatcher = self._dispatchers[stdin_fd]
|
147
|
+
dispatcher = check_isinstance(self._dispatchers[stdin_fd], InputDispatcher)
|
142
148
|
if dispatcher.closed:
|
143
149
|
raise OSError(errno.EPIPE, "Process' stdin channel is closed")
|
144
150
|
|
145
|
-
dispatcher.
|
151
|
+
dispatcher.write(chars)
|
146
152
|
dispatcher.flush() # this must raise EPIPE if the pipe is closed
|
147
153
|
|
148
154
|
def _get_execv_args(self) -> ta.Tuple[str, ta.Sequence[str]]:
|
@@ -152,9 +158,9 @@ class Subprocess(AbstractSubprocess):
|
|
152
158
|
"""
|
153
159
|
|
154
160
|
try:
|
155
|
-
commandargs = shlex.split(self.
|
161
|
+
commandargs = shlex.split(self._config.command)
|
156
162
|
except ValueError as e:
|
157
|
-
raise BadCommandError(f"can't parse command {self.
|
163
|
+
raise BadCommandError(f"can't parse command {self._config.command!r}: {e}") # noqa
|
158
164
|
|
159
165
|
if commandargs:
|
160
166
|
program = commandargs[0]
|
@@ -191,76 +197,65 @@ class Subprocess(AbstractSubprocess):
|
|
191
197
|
|
192
198
|
return filename, commandargs
|
193
199
|
|
194
|
-
event_map: ta.ClassVar[ta.Mapping[int, ta.Type[ProcessStateEvent]]] = {
|
195
|
-
ProcessStates.BACKOFF: ProcessStateBackoffEvent,
|
196
|
-
ProcessStates.FATAL: ProcessStateFatalEvent,
|
197
|
-
ProcessStates.UNKNOWN: ProcessStateUnknownEvent,
|
198
|
-
ProcessStates.STOPPED: ProcessStateStoppedEvent,
|
199
|
-
ProcessStates.EXITED: ProcessStateExitedEvent,
|
200
|
-
ProcessStates.RUNNING: ProcessStateRunningEvent,
|
201
|
-
ProcessStates.STARTING: ProcessStateStartingEvent,
|
202
|
-
ProcessStates.STOPPING: ProcessStateStoppingEvent,
|
203
|
-
}
|
204
|
-
|
205
200
|
def change_state(self, new_state: ProcessState, expected: bool = True) -> bool:
|
206
|
-
old_state = self.
|
201
|
+
old_state = self._state
|
207
202
|
if new_state is old_state:
|
208
203
|
return False
|
209
204
|
|
210
|
-
self.
|
211
|
-
if new_state ==
|
205
|
+
self._state = new_state
|
206
|
+
if new_state == ProcessState.BACKOFF:
|
212
207
|
now = time.time()
|
213
|
-
self.
|
214
|
-
self.
|
208
|
+
self._backoff += 1
|
209
|
+
self._delay = now + self._backoff
|
215
210
|
|
216
|
-
event_class =
|
211
|
+
event_class = PROCESS_STATE_EVENT_MAP.get(new_state)
|
217
212
|
if event_class is not None:
|
218
213
|
event = event_class(self, old_state, expected)
|
219
|
-
|
214
|
+
self._event_callbacks.notify(event)
|
220
215
|
|
221
216
|
return True
|
222
217
|
|
223
218
|
def _check_in_state(self, *states: ProcessState) -> None:
|
224
|
-
if self.
|
225
|
-
current_state =
|
226
|
-
allowable_states = ' '.join(
|
227
|
-
|
228
|
-
raise
|
219
|
+
if self._state not in states:
|
220
|
+
current_state = self._state.name
|
221
|
+
allowable_states = ' '.join(s.name for s in states)
|
222
|
+
process_name = as_string(self._config.name)
|
223
|
+
raise RuntimeError('Assertion failed for %s: %s not in %s' % (process_name, current_state, allowable_states)) # noqa
|
229
224
|
|
230
225
|
def _record_spawn_err(self, msg: str) -> None:
|
231
|
-
self.
|
232
|
-
log.info('
|
226
|
+
self._spawn_err = msg
|
227
|
+
log.info('_spawn_err: %s', msg)
|
233
228
|
|
234
229
|
def spawn(self) -> ta.Optional[int]:
|
235
|
-
|
230
|
+
process_name = as_string(self._config.name)
|
236
231
|
|
237
232
|
if self.pid:
|
238
|
-
log.warning('process \'%s\' already running',
|
233
|
+
log.warning('process \'%s\' already running', process_name)
|
239
234
|
return None
|
240
235
|
|
241
|
-
self.
|
242
|
-
self.
|
243
|
-
self.
|
244
|
-
self.
|
245
|
-
self.
|
236
|
+
self._killing = False
|
237
|
+
self._spawn_err = None
|
238
|
+
self._exitstatus = None
|
239
|
+
self._system_stop = False
|
240
|
+
self._administrative_stop = False
|
246
241
|
|
247
|
-
self.
|
242
|
+
self._last_start = time.time()
|
248
243
|
|
249
244
|
self._check_in_state(
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
245
|
+
ProcessState.EXITED,
|
246
|
+
ProcessState.FATAL,
|
247
|
+
ProcessState.BACKOFF,
|
248
|
+
ProcessState.STOPPED,
|
254
249
|
)
|
255
250
|
|
256
|
-
self.change_state(
|
251
|
+
self.change_state(ProcessState.STARTING)
|
257
252
|
|
258
253
|
try:
|
259
254
|
filename, argv = self._get_execv_args()
|
260
255
|
except ProcessError as what:
|
261
256
|
self._record_spawn_err(what.args[0])
|
262
|
-
self._check_in_state(
|
263
|
-
self.change_state(
|
257
|
+
self._check_in_state(ProcessState.STARTING)
|
258
|
+
self.change_state(ProcessState.BACKOFF)
|
264
259
|
return None
|
265
260
|
|
266
261
|
try:
|
@@ -269,12 +264,12 @@ class Subprocess(AbstractSubprocess):
|
|
269
264
|
code = why.args[0]
|
270
265
|
if code == errno.EMFILE:
|
271
266
|
# too many file descriptors open
|
272
|
-
msg = f"too many open files to spawn '{
|
267
|
+
msg = f"too many open files to spawn '{process_name}'"
|
273
268
|
else:
|
274
|
-
msg = f"unknown error making dispatchers for '{
|
269
|
+
msg = f"unknown error making dispatchers for '{process_name}': {errno.errorcode.get(code, code)}"
|
275
270
|
self._record_spawn_err(msg)
|
276
|
-
self._check_in_state(
|
277
|
-
self.change_state(
|
271
|
+
self._check_in_state(ProcessState.STARTING)
|
272
|
+
self.change_state(ProcessState.BACKOFF)
|
278
273
|
return None
|
279
274
|
|
280
275
|
try:
|
@@ -283,12 +278,12 @@ class Subprocess(AbstractSubprocess):
|
|
283
278
|
code = why.args[0]
|
284
279
|
if code == errno.EAGAIN:
|
285
280
|
# process table full
|
286
|
-
msg = f'Too many processes in process table to spawn \'{
|
281
|
+
msg = f'Too many processes in process table to spawn \'{process_name}\''
|
287
282
|
else:
|
288
|
-
msg = f'unknown error during fork for \'{
|
283
|
+
msg = f'unknown error during fork for \'{process_name}\': {errno.errorcode.get(code, code)}'
|
289
284
|
self._record_spawn_err(msg)
|
290
|
-
self._check_in_state(
|
291
|
-
self.change_state(
|
285
|
+
self._check_in_state(ProcessState.STARTING)
|
286
|
+
self.change_state(ProcessState.BACKOFF)
|
292
287
|
close_parent_pipes(self._pipes)
|
293
288
|
close_child_pipes(self._pipes)
|
294
289
|
return None
|
@@ -301,41 +296,66 @@ class Subprocess(AbstractSubprocess):
|
|
301
296
|
return None
|
302
297
|
|
303
298
|
def _make_dispatchers(self) -> ta.Tuple[ta.Mapping[int, Dispatcher], ta.Mapping[str, int]]:
|
304
|
-
use_stderr = not self.
|
299
|
+
use_stderr = not self._config.redirect_stderr
|
300
|
+
|
305
301
|
p = make_pipes(use_stderr)
|
306
302
|
stdout_fd, stderr_fd, stdin_fd = p['stdout'], p['stderr'], p['stdin']
|
303
|
+
|
307
304
|
dispatchers: ta.Dict[int, Dispatcher] = {}
|
305
|
+
|
306
|
+
dispatcher_kw = dict(
|
307
|
+
event_callbacks=self._event_callbacks,
|
308
|
+
)
|
309
|
+
|
308
310
|
etype: ta.Type[ProcessCommunicationEvent]
|
309
311
|
if stdout_fd is not None:
|
310
312
|
etype = ProcessCommunicationStdoutEvent
|
311
|
-
dispatchers[stdout_fd] = OutputDispatcher(
|
313
|
+
dispatchers[stdout_fd] = OutputDispatcher(
|
314
|
+
self,
|
315
|
+
etype,
|
316
|
+
stdout_fd,
|
317
|
+
**dispatcher_kw,
|
318
|
+
)
|
319
|
+
|
312
320
|
if stderr_fd is not None:
|
313
321
|
etype = ProcessCommunicationStderrEvent
|
314
|
-
dispatchers[stderr_fd] = OutputDispatcher(
|
322
|
+
dispatchers[stderr_fd] = OutputDispatcher(
|
323
|
+
self,
|
324
|
+
etype,
|
325
|
+
stderr_fd,
|
326
|
+
**dispatcher_kw,
|
327
|
+
)
|
328
|
+
|
315
329
|
if stdin_fd is not None:
|
316
|
-
dispatchers[stdin_fd] = InputDispatcher(
|
330
|
+
dispatchers[stdin_fd] = InputDispatcher(
|
331
|
+
self,
|
332
|
+
'stdin',
|
333
|
+
stdin_fd,
|
334
|
+
**dispatcher_kw,
|
335
|
+
)
|
336
|
+
|
317
337
|
return dispatchers, p
|
318
338
|
|
319
339
|
def _spawn_as_parent(self, pid: int) -> int:
|
320
340
|
# Parent
|
321
341
|
self._pid = pid
|
322
342
|
close_child_pipes(self._pipes)
|
323
|
-
log.info('spawned: \'%s\' with pid %s', as_string(self.
|
324
|
-
self.
|
325
|
-
self.
|
343
|
+
log.info('spawned: \'%s\' with pid %s', as_string(self._config.name), pid)
|
344
|
+
self._spawn_err = None
|
345
|
+
self._delay = time.time() + self._config.startsecs
|
326
346
|
self.context.pid_history[pid] = self
|
327
347
|
return pid
|
328
348
|
|
329
349
|
def _prepare_child_fds(self) -> None:
|
330
350
|
os.dup2(self._pipes['child_stdin'], 0)
|
331
351
|
os.dup2(self._pipes['child_stdout'], 1)
|
332
|
-
if self.
|
352
|
+
if self._config.redirect_stderr:
|
333
353
|
os.dup2(self._pipes['child_stdout'], 2)
|
334
354
|
else:
|
335
355
|
os.dup2(self._pipes['child_stderr'], 2)
|
336
356
|
|
337
357
|
for i in range(3, self.context.config.minfds):
|
338
|
-
if i in self.
|
358
|
+
if i in self._inherited_fds:
|
339
359
|
continue
|
340
360
|
close_fd(i)
|
341
361
|
|
@@ -353,7 +373,7 @@ class Subprocess(AbstractSubprocess):
|
|
353
373
|
# set user
|
354
374
|
setuid_msg = self.set_uid()
|
355
375
|
if setuid_msg:
|
356
|
-
uid = self.
|
376
|
+
uid = self._config.uid
|
357
377
|
msg = f"couldn't setuid to {uid}: {setuid_msg}\n"
|
358
378
|
os.write(2, as_bytes('supervisor: ' + msg))
|
359
379
|
return # finally clause will exit the child process
|
@@ -361,14 +381,14 @@ class Subprocess(AbstractSubprocess):
|
|
361
381
|
# set environment
|
362
382
|
env = os.environ.copy()
|
363
383
|
env['SUPERVISOR_ENABLED'] = '1'
|
364
|
-
env['SUPERVISOR_PROCESS_NAME'] = self.
|
365
|
-
if self.
|
366
|
-
env['SUPERVISOR_GROUP_NAME'] = self.
|
367
|
-
if self.
|
368
|
-
env.update(self.
|
384
|
+
env['SUPERVISOR_PROCESS_NAME'] = self._config.name
|
385
|
+
if self._group:
|
386
|
+
env['SUPERVISOR_GROUP_NAME'] = self._group.config.name
|
387
|
+
if self._config.environment is not None:
|
388
|
+
env.update(self._config.environment)
|
369
389
|
|
370
390
|
# change directory
|
371
|
-
cwd = self.
|
391
|
+
cwd = self._config.directory
|
372
392
|
try:
|
373
393
|
if cwd is not None:
|
374
394
|
os.chdir(os.path.expanduser(cwd))
|
@@ -380,8 +400,8 @@ class Subprocess(AbstractSubprocess):
|
|
380
400
|
|
381
401
|
# set umask, then execve
|
382
402
|
try:
|
383
|
-
if self.
|
384
|
-
os.umask(self.
|
403
|
+
if self._config.umask is not None:
|
404
|
+
os.umask(self._config.umask)
|
385
405
|
os.execve(filename, list(argv), env)
|
386
406
|
except OSError as why:
|
387
407
|
code = errno.errorcode.get(why.args[0], why.args[0])
|
@@ -403,46 +423,48 @@ class Subprocess(AbstractSubprocess):
|
|
403
423
|
"""
|
404
424
|
Check if system clock has rolled backward beyond test_time. If so, set affected timestamps to test_time.
|
405
425
|
"""
|
406
|
-
if self.state == ProcessStates.STARTING:
|
407
|
-
self.laststart = min(test_time, self.laststart)
|
408
|
-
if self.delay > 0 and test_time < (self.delay - self.config.startsecs):
|
409
|
-
self.delay = test_time + self.config.startsecs
|
410
426
|
|
411
|
-
|
412
|
-
|
413
|
-
|
427
|
+
if self._state == ProcessState.STARTING:
|
428
|
+
self._last_start = min(test_time, self._last_start)
|
429
|
+
if self._delay > 0 and test_time < (self._delay - self._config.startsecs):
|
430
|
+
self._delay = test_time + self._config.startsecs
|
414
431
|
|
415
|
-
elif self.
|
416
|
-
self.
|
417
|
-
|
418
|
-
self.delay = test_time + self.config.stopwaitsecs
|
432
|
+
elif self._state == ProcessState.RUNNING:
|
433
|
+
if test_time > self._last_start and test_time < (self._last_start + self._config.startsecs):
|
434
|
+
self._last_start = test_time - self._config.startsecs
|
419
435
|
|
420
|
-
elif self.
|
421
|
-
|
422
|
-
|
436
|
+
elif self._state == ProcessState.STOPPING:
|
437
|
+
self._last_stop_report = min(test_time, self._last_stop_report)
|
438
|
+
if self._delay > 0 and test_time < (self._delay - self._config.stopwaitsecs):
|
439
|
+
self._delay = test_time + self._config.stopwaitsecs
|
440
|
+
|
441
|
+
elif self._state == ProcessState.BACKOFF:
|
442
|
+
if self._delay > 0 and test_time < (self._delay - self._backoff):
|
443
|
+
self._delay = test_time + self._backoff
|
423
444
|
|
424
445
|
def stop(self) -> ta.Optional[str]:
|
425
|
-
self.
|
426
|
-
self.
|
427
|
-
return self.kill(self.
|
446
|
+
self._administrative_stop = True
|
447
|
+
self._last_stop_report = 0
|
448
|
+
return self.kill(self._config.stopsignal)
|
428
449
|
|
429
450
|
def stop_report(self) -> None:
|
430
451
|
"""Log a 'waiting for x to stop' message with throttling."""
|
431
|
-
|
452
|
+
|
453
|
+
if self._state == ProcessState.STOPPING:
|
432
454
|
now = time.time()
|
433
455
|
|
434
456
|
self._check_and_adjust_for_system_clock_rollback(now)
|
435
457
|
|
436
|
-
if now > (self.
|
437
|
-
log.info('waiting for %s to stop', as_string(self.
|
438
|
-
self.
|
458
|
+
if now > (self._last_stop_report + 2): # every 2 seconds
|
459
|
+
log.info('waiting for %s to stop', as_string(self._config.name))
|
460
|
+
self._last_stop_report = now
|
439
461
|
|
440
462
|
def give_up(self) -> None:
|
441
|
-
self.
|
442
|
-
self.
|
443
|
-
self.
|
444
|
-
self._check_in_state(
|
445
|
-
self.change_state(
|
463
|
+
self._delay = 0
|
464
|
+
self._backoff = 0
|
465
|
+
self._system_stop = True
|
466
|
+
self._check_in_state(ProcessState.BACKOFF)
|
467
|
+
self.change_state(ProcessState.FATAL)
|
446
468
|
|
447
469
|
def kill(self, sig: int) -> ta.Optional[str]:
|
448
470
|
"""
|
@@ -454,39 +476,39 @@ class Subprocess(AbstractSubprocess):
|
|
454
476
|
"""
|
455
477
|
now = time.time()
|
456
478
|
|
457
|
-
|
479
|
+
process_name = as_string(self._config.name)
|
458
480
|
# If the process is in BACKOFF and we want to stop or kill it, then BACKOFF -> STOPPED. This is needed because
|
459
481
|
# if startretries is a large number and the process isn't starting successfully, the stop request would be
|
460
482
|
# blocked for a long time waiting for the retries.
|
461
|
-
if self.
|
462
|
-
log.debug('Attempted to kill %s, which is in BACKOFF state.',
|
463
|
-
self.change_state(
|
483
|
+
if self._state == ProcessState.BACKOFF:
|
484
|
+
log.debug('Attempted to kill %s, which is in BACKOFF state.', process_name)
|
485
|
+
self.change_state(ProcessState.STOPPED)
|
464
486
|
return None
|
465
487
|
|
466
488
|
args: tuple
|
467
489
|
if not self.pid:
|
468
|
-
fmt, args = "attempted to kill %s with sig %s but it wasn't running", (
|
490
|
+
fmt, args = "attempted to kill %s with sig %s but it wasn't running", (process_name, sig_name(sig))
|
469
491
|
log.debug(fmt, *args)
|
470
492
|
return fmt % args
|
471
493
|
|
472
494
|
# If we're in the stopping state, then we've already sent the stop signal and this is the kill signal
|
473
|
-
if self.
|
474
|
-
killasgroup = self.
|
495
|
+
if self._state == ProcessState.STOPPING:
|
496
|
+
killasgroup = self._config.killasgroup
|
475
497
|
else:
|
476
|
-
killasgroup = self.
|
498
|
+
killasgroup = self._config.stopasgroup
|
477
499
|
|
478
500
|
as_group = ''
|
479
501
|
if killasgroup:
|
480
502
|
as_group = 'process group '
|
481
503
|
|
482
|
-
log.debug('killing %s (pid %s) %
|
504
|
+
log.debug('killing %s (pid %s) %s with signal %s', process_name, self.pid, as_group, sig_name(sig))
|
483
505
|
|
484
506
|
# RUNNING/STARTING/STOPPING -> STOPPING
|
485
|
-
self.
|
486
|
-
self.
|
507
|
+
self._killing = True
|
508
|
+
self._delay = now + self._config.stopwaitsecs
|
487
509
|
# we will already be in the STOPPING state if we're doing a SIGKILL as a result of overrunning stopwaitsecs
|
488
|
-
self._check_in_state(
|
489
|
-
self.change_state(
|
510
|
+
self._check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
|
511
|
+
self.change_state(ProcessState.STOPPING)
|
490
512
|
|
491
513
|
pid = self.pid
|
492
514
|
if killasgroup:
|
@@ -498,18 +520,18 @@ class Subprocess(AbstractSubprocess):
|
|
498
520
|
os.kill(pid, sig)
|
499
521
|
except OSError as exc:
|
500
522
|
if exc.errno == errno.ESRCH:
|
501
|
-
log.debug('unable to signal %s (pid %s), it probably just exited on its own: %s',
|
523
|
+
log.debug('unable to signal %s (pid %s), it probably just exited on its own: %s', process_name, self.pid, str(exc)) # noqa
|
502
524
|
# we could change the state here but we intentionally do not. we will do it during normal SIGCHLD
|
503
525
|
# processing.
|
504
526
|
return None
|
505
527
|
raise
|
506
528
|
except Exception: # noqa
|
507
529
|
tb = traceback.format_exc()
|
508
|
-
fmt, args = 'unknown problem killing %s (%s):%s', (
|
530
|
+
fmt, args = 'unknown problem killing %s (%s):%s', (process_name, self.pid, tb)
|
509
531
|
log.critical(fmt, *args)
|
510
|
-
self.change_state(
|
511
|
-
self.
|
512
|
-
self.
|
532
|
+
self.change_state(ProcessState.UNKNOWN)
|
533
|
+
self._killing = False
|
534
|
+
self._delay = 0
|
513
535
|
return fmt % args
|
514
536
|
|
515
537
|
return None
|
@@ -521,16 +543,16 @@ class Subprocess(AbstractSubprocess):
|
|
521
543
|
Return None if the signal was sent, or an error message string if an error occurred or if the subprocess is not
|
522
544
|
running.
|
523
545
|
"""
|
524
|
-
|
546
|
+
process_name = as_string(self._config.name)
|
525
547
|
args: tuple
|
526
548
|
if not self.pid:
|
527
|
-
fmt, args = "attempted to send %s sig %s but it wasn't running", (
|
549
|
+
fmt, args = "attempted to send %s sig %s but it wasn't running", (process_name, sig_name(sig))
|
528
550
|
log.debug(fmt, *args)
|
529
551
|
return fmt % args
|
530
552
|
|
531
|
-
log.debug('sending %s (pid %s) sig %s',
|
553
|
+
log.debug('sending %s (pid %s) sig %s', process_name, self.pid, sig_name(sig))
|
532
554
|
|
533
|
-
self._check_in_state(
|
555
|
+
self._check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
|
534
556
|
|
535
557
|
try:
|
536
558
|
try:
|
@@ -539,7 +561,7 @@ class Subprocess(AbstractSubprocess):
|
|
539
561
|
if exc.errno == errno.ESRCH:
|
540
562
|
log.debug(
|
541
563
|
'unable to signal %s (pid %s), it probably just now exited on its own: %s',
|
542
|
-
|
564
|
+
process_name,
|
543
565
|
self.pid,
|
544
566
|
str(exc),
|
545
567
|
)
|
@@ -549,9 +571,9 @@ class Subprocess(AbstractSubprocess):
|
|
549
571
|
raise
|
550
572
|
except Exception: # noqa
|
551
573
|
tb = traceback.format_exc()
|
552
|
-
fmt, args = 'unknown problem sending sig %s (%s):%s', (
|
574
|
+
fmt, args = 'unknown problem sending sig %s (%s):%s', (process_name, self.pid, tb)
|
553
575
|
log.critical(fmt, *args)
|
554
|
-
self.change_state(
|
576
|
+
self.change_state(ProcessState.UNKNOWN)
|
555
577
|
return fmt % args
|
556
578
|
|
557
579
|
return None
|
@@ -567,31 +589,31 @@ class Subprocess(AbstractSubprocess):
|
|
567
589
|
|
568
590
|
self._check_and_adjust_for_system_clock_rollback(now)
|
569
591
|
|
570
|
-
self.
|
571
|
-
|
592
|
+
self._last_stop = now
|
593
|
+
process_name = as_string(self._config.name)
|
572
594
|
|
573
|
-
if now > self.
|
574
|
-
too_quickly = now - self.
|
595
|
+
if now > self._last_start:
|
596
|
+
too_quickly = now - self._last_start < self._config.startsecs
|
575
597
|
else:
|
576
598
|
too_quickly = False
|
577
599
|
log.warning(
|
578
|
-
"process '%s' (%s)
|
600
|
+
"process '%s' (%s) last_start time is in the future, don't know how long process was running so "
|
579
601
|
"assuming it did not exit too quickly",
|
580
|
-
|
602
|
+
process_name,
|
581
603
|
self.pid,
|
582
604
|
)
|
583
605
|
|
584
|
-
exit_expected = es in self.
|
606
|
+
exit_expected = es in self._config.exitcodes
|
585
607
|
|
586
|
-
if self.
|
608
|
+
if self._killing:
|
587
609
|
# likely the result of a stop request implies STOPPING -> STOPPED
|
588
|
-
self.
|
589
|
-
self.
|
590
|
-
self.
|
610
|
+
self._killing = False
|
611
|
+
self._delay = 0
|
612
|
+
self._exitstatus = es
|
591
613
|
|
592
|
-
fmt, args = 'stopped: %s (%s)', (
|
593
|
-
self._check_in_state(
|
594
|
-
self.change_state(
|
614
|
+
fmt, args = 'stopped: %s (%s)', (process_name, msg)
|
615
|
+
self._check_in_state(ProcessState.STOPPING)
|
616
|
+
self.change_state(ProcessState.STOPPED)
|
595
617
|
if exit_expected:
|
596
618
|
log.info(fmt, *args)
|
597
619
|
else:
|
@@ -599,226 +621,119 @@ class Subprocess(AbstractSubprocess):
|
|
599
621
|
|
600
622
|
elif too_quickly:
|
601
623
|
# the program did not stay up long enough to make it to RUNNING implies STARTING -> BACKOFF
|
602
|
-
self.
|
603
|
-
self.
|
604
|
-
self._check_in_state(
|
605
|
-
self.change_state(
|
606
|
-
log.warning('exited: %s (%s)',
|
624
|
+
self._exitstatus = None
|
625
|
+
self._spawn_err = 'Exited too quickly (process log may have details)'
|
626
|
+
self._check_in_state(ProcessState.STARTING)
|
627
|
+
self.change_state(ProcessState.BACKOFF)
|
628
|
+
log.warning('exited: %s (%s)', process_name, msg + '; not expected')
|
607
629
|
|
608
630
|
else:
|
609
631
|
# this finish was not the result of a stop request, the program was in the RUNNING state but exited implies
|
610
632
|
# RUNNING -> EXITED normally but see next comment
|
611
|
-
self.
|
612
|
-
self.
|
613
|
-
self.
|
633
|
+
self._delay = 0
|
634
|
+
self._backoff = 0
|
635
|
+
self._exitstatus = es
|
614
636
|
|
615
|
-
# if the process was STARTING but a system time change causes self.
|
616
|
-
# STARTING->RUNNING transition can be subverted so we perform the transition here.
|
617
|
-
if self.
|
618
|
-
self.change_state(
|
637
|
+
# if the process was STARTING but a system time change causes self.last_start to be in the future, the
|
638
|
+
# normal STARTING->RUNNING transition can be subverted so we perform the transition here.
|
639
|
+
if self._state == ProcessState.STARTING:
|
640
|
+
self.change_state(ProcessState.RUNNING)
|
619
641
|
|
620
|
-
self._check_in_state(
|
642
|
+
self._check_in_state(ProcessState.RUNNING)
|
621
643
|
|
622
644
|
if exit_expected:
|
623
645
|
# expected exit code
|
624
|
-
self.change_state(
|
625
|
-
log.info('exited: %s (%s)',
|
646
|
+
self.change_state(ProcessState.EXITED, expected=True)
|
647
|
+
log.info('exited: %s (%s)', process_name, msg + '; expected')
|
626
648
|
else:
|
627
649
|
# unexpected exit code
|
628
|
-
self.
|
629
|
-
self.change_state(
|
630
|
-
log.warning('exited: %s (%s)',
|
650
|
+
self._spawn_err = f'Bad exit code {es}'
|
651
|
+
self.change_state(ProcessState.EXITED, expected=False)
|
652
|
+
log.warning('exited: %s (%s)', process_name, msg + '; not expected')
|
631
653
|
|
632
654
|
self._pid = 0
|
633
655
|
close_parent_pipes(self._pipes)
|
634
656
|
self._pipes = {}
|
635
657
|
self._dispatchers = {}
|
636
658
|
|
637
|
-
# if we died before we processed the current event (only happens if we're an event listener), notify the event
|
638
|
-
# system that this event was rejected so it can be processed again.
|
639
|
-
if self.event is not None:
|
640
|
-
# Note: this should only be true if we were in the BUSY state when finish() was called.
|
641
|
-
EVENT_CALLBACKS.notify(EventRejectedEvent(self, self.event)) # type: ignore
|
642
|
-
self.event = None
|
643
|
-
|
644
659
|
def set_uid(self) -> ta.Optional[str]:
|
645
|
-
if self.
|
660
|
+
if self._config.uid is None:
|
646
661
|
return None
|
647
|
-
msg = drop_privileges(self.
|
662
|
+
msg = drop_privileges(self._config.uid)
|
648
663
|
return msg
|
649
664
|
|
650
|
-
def
|
651
|
-
return self.config.priority < other.config.priority
|
652
|
-
|
653
|
-
def __eq__(self, other):
|
654
|
-
return self.config.priority == other.config.priority
|
655
|
-
|
656
|
-
def __repr__(self):
|
665
|
+
def __repr__(self) -> str:
|
657
666
|
# repr can't return anything other than a native string, but the name might be unicode - a problem on Python 2.
|
658
|
-
name = self.
|
659
|
-
return f'<Subprocess at {id(self)} with name {name} in state {
|
667
|
+
name = self._config.name
|
668
|
+
return f'<Subprocess at {id(self)} with name {name} in state {self.get_state().name}>'
|
660
669
|
|
661
670
|
def get_state(self) -> ProcessState:
|
662
|
-
return self.
|
671
|
+
return self._state
|
663
672
|
|
664
|
-
def transition(self):
|
673
|
+
def transition(self) -> None:
|
665
674
|
now = time.time()
|
666
|
-
state = self.
|
675
|
+
state = self._state
|
667
676
|
|
668
677
|
self._check_and_adjust_for_system_clock_rollback(now)
|
669
678
|
|
670
679
|
logger = log
|
671
680
|
|
672
|
-
if self.context.state >
|
681
|
+
if self.context.state > SupervisorState.RESTARTING:
|
673
682
|
# dont start any processes if supervisor is shutting down
|
674
|
-
if state ==
|
675
|
-
if self.
|
676
|
-
if self.
|
683
|
+
if state == ProcessState.EXITED:
|
684
|
+
if self._config.autorestart:
|
685
|
+
if self._config.autorestart is RestartUnconditionally:
|
677
686
|
# EXITED -> STARTING
|
678
687
|
self.spawn()
|
679
|
-
elif self.
|
688
|
+
elif self._exitstatus not in self._config.exitcodes:
|
680
689
|
# EXITED -> STARTING
|
681
690
|
self.spawn()
|
682
691
|
|
683
|
-
elif state ==
|
684
|
-
if self.
|
692
|
+
elif state == ProcessState.STOPPED and not self._last_start:
|
693
|
+
if self._config.autostart:
|
685
694
|
# STOPPED -> STARTING
|
686
695
|
self.spawn()
|
687
696
|
|
688
|
-
elif state ==
|
689
|
-
if self.
|
690
|
-
if now > self.
|
697
|
+
elif state == ProcessState.BACKOFF:
|
698
|
+
if self._backoff <= self._config.startretries:
|
699
|
+
if now > self._delay:
|
691
700
|
# BACKOFF -> STARTING
|
692
701
|
self.spawn()
|
693
702
|
|
694
|
-
|
695
|
-
if state ==
|
696
|
-
if now - self.
|
703
|
+
process_name = as_string(self._config.name)
|
704
|
+
if state == ProcessState.STARTING:
|
705
|
+
if now - self._last_start > self._config.startsecs:
|
697
706
|
# STARTING -> RUNNING if the proc has started successfully and it has stayed up for at least
|
698
707
|
# proc.config.startsecs,
|
699
|
-
self.
|
700
|
-
self.
|
701
|
-
self._check_in_state(
|
702
|
-
self.change_state(
|
703
|
-
msg = ('entered RUNNING state, process has stayed up for > than %s seconds (startsecs)' % self.
|
704
|
-
logger.info('success: %s %s',
|
705
|
-
|
706
|
-
if state ==
|
707
|
-
if self.
|
708
|
+
self._delay = 0
|
709
|
+
self._backoff = 0
|
710
|
+
self._check_in_state(ProcessState.STARTING)
|
711
|
+
self.change_state(ProcessState.RUNNING)
|
712
|
+
msg = ('entered RUNNING state, process has stayed up for > than %s seconds (startsecs)' % self._config.startsecs) # noqa
|
713
|
+
logger.info('success: %s %s', process_name, msg)
|
714
|
+
|
715
|
+
if state == ProcessState.BACKOFF:
|
716
|
+
if self._backoff > self._config.startretries:
|
708
717
|
# BACKOFF -> FATAL if the proc has exceeded its number of retries
|
709
718
|
self.give_up()
|
710
719
|
msg = ('entered FATAL state, too many start retries too quickly')
|
711
|
-
logger.info('gave up: %s %s',
|
720
|
+
logger.info('gave up: %s %s', process_name, msg)
|
712
721
|
|
713
|
-
elif state ==
|
714
|
-
time_left = self.
|
722
|
+
elif state == ProcessState.STOPPING:
|
723
|
+
time_left = self._delay - now
|
715
724
|
if time_left <= 0:
|
716
725
|
# kill processes which are taking too long to stop with a final sigkill. if this doesn't kill it, the
|
717
726
|
# process will be stuck in the STOPPING state forever.
|
718
|
-
log.warning('killing \'%s\' (%s) with SIGKILL',
|
727
|
+
log.warning('killing \'%s\' (%s) with SIGKILL', process_name, self.pid)
|
719
728
|
self.kill(signal.SIGKILL)
|
720
729
|
|
721
|
-
def create_auto_child_logs(self):
|
730
|
+
def create_auto_child_logs(self) -> None:
|
722
731
|
# temporary logfiles which are erased at start time
|
723
732
|
# get_autoname = self.context.get_auto_child_log_name # noqa
|
724
733
|
# sid = self.context.config.identifier # noqa
|
725
|
-
# name = self.
|
734
|
+
# name = self._config.name # noqa
|
726
735
|
# if self.stdout_logfile is Automatic:
|
727
736
|
# self.stdout_logfile = get_autoname(name, sid, 'stdout')
|
728
737
|
# if self.stderr_logfile is Automatic:
|
729
738
|
# self.stderr_logfile = get_autoname(name, sid, 'stderr')
|
730
739
|
pass
|
731
|
-
|
732
|
-
|
733
|
-
##
|
734
|
-
|
735
|
-
|
736
|
-
@dc.dataclass(frozen=True)
|
737
|
-
class SubprocessFactory:
|
738
|
-
fn: ta.Callable[[ProcessConfig, 'ProcessGroup'], Subprocess]
|
739
|
-
|
740
|
-
def __call__(self, config: ProcessConfig, group: 'ProcessGroup') -> Subprocess:
|
741
|
-
return self.fn(config, group)
|
742
|
-
|
743
|
-
|
744
|
-
@functools.total_ordering
|
745
|
-
class ProcessGroup:
|
746
|
-
def __init__(
|
747
|
-
self,
|
748
|
-
config: ProcessGroupConfig,
|
749
|
-
context: ServerContext,
|
750
|
-
*,
|
751
|
-
subprocess_factory: ta.Optional[SubprocessFactory] = None,
|
752
|
-
):
|
753
|
-
super().__init__()
|
754
|
-
self.config = config
|
755
|
-
self.context = context
|
756
|
-
|
757
|
-
if subprocess_factory is None:
|
758
|
-
def make_subprocess(config: ProcessConfig, group: ProcessGroup) -> Subprocess:
|
759
|
-
return Subprocess(config, group, self.context)
|
760
|
-
subprocess_factory = SubprocessFactory(make_subprocess)
|
761
|
-
self._subprocess_factory = subprocess_factory
|
762
|
-
|
763
|
-
self.processes = {}
|
764
|
-
for pconfig in self.config.processes or []:
|
765
|
-
process = self._subprocess_factory(pconfig, self)
|
766
|
-
self.processes[pconfig.name] = process
|
767
|
-
|
768
|
-
def __lt__(self, other):
|
769
|
-
return self.config.priority < other.config.priority
|
770
|
-
|
771
|
-
def __eq__(self, other):
|
772
|
-
return self.config.priority == other.config.priority
|
773
|
-
|
774
|
-
def __repr__(self):
|
775
|
-
# repr can't return anything other than a native string, but the name might be unicode - a problem on Python 2.
|
776
|
-
name = self.config.name
|
777
|
-
return f'<{self.__class__.__name__} instance at {id(self)} named {name}>'
|
778
|
-
|
779
|
-
def remove_logs(self) -> None:
|
780
|
-
for process in self.processes.values():
|
781
|
-
process.remove_logs()
|
782
|
-
|
783
|
-
def reopen_logs(self) -> None:
|
784
|
-
for process in self.processes.values():
|
785
|
-
process.reopen_logs()
|
786
|
-
|
787
|
-
def stop_all(self) -> None:
|
788
|
-
processes = list(self.processes.values())
|
789
|
-
processes.sort()
|
790
|
-
processes.reverse() # stop in desc priority order
|
791
|
-
|
792
|
-
for proc in processes:
|
793
|
-
state = proc.get_state()
|
794
|
-
if state == ProcessStates.RUNNING:
|
795
|
-
# RUNNING -> STOPPING
|
796
|
-
proc.stop()
|
797
|
-
|
798
|
-
elif state == ProcessStates.STARTING:
|
799
|
-
# STARTING -> STOPPING
|
800
|
-
proc.stop()
|
801
|
-
|
802
|
-
elif state == ProcessStates.BACKOFF:
|
803
|
-
# BACKOFF -> FATAL
|
804
|
-
proc.give_up()
|
805
|
-
|
806
|
-
def get_unstopped_processes(self) -> ta.List[Subprocess]:
|
807
|
-
return [x for x in self.processes.values() if x.get_state() not in STOPPED_STATES]
|
808
|
-
|
809
|
-
def get_dispatchers(self) -> ta.Dict[int, Dispatcher]:
|
810
|
-
dispatchers = {}
|
811
|
-
for process in self.processes.values():
|
812
|
-
dispatchers.update(process._dispatchers) # noqa
|
813
|
-
return dispatchers
|
814
|
-
|
815
|
-
def before_remove(self) -> None:
|
816
|
-
pass
|
817
|
-
|
818
|
-
def transition(self) -> None:
|
819
|
-
for proc in self.processes.values():
|
820
|
-
proc.transition()
|
821
|
-
|
822
|
-
def after_setuid(self) -> None:
|
823
|
-
for proc in self.processes.values():
|
824
|
-
proc.create_auto_child_logs()
|