ominfra 0.0.0.dev121__py3-none-any.whl → 0.0.0.dev123__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 EVENT_CALLBACKS
35
- from .events import EventRejectedEvent
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 .states import STOPPED_STATES
30
+ from .signals import sig_name
51
31
  from .states import ProcessState
52
- from .states import ProcessStates
53
- from .states import SupervisorStates
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: 'ProcessGroup',
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.group = group
67
+ self._group = group
96
68
  self._context = context
97
- self._dispatchers: dict = {}
98
- self._pipes: dict = {}
99
- self.state = ProcessStates.STOPPED
100
- self._pid = 0
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.killing:
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.input_buffer += chars
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.config.command)
161
+ commandargs = shlex.split(self._config.command)
156
162
  except ValueError as e:
157
- raise BadCommandError(f"can't parse command {self.config.command!r}: {e}") # noqa
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.state
201
+ old_state = self._state
207
202
  if new_state is old_state:
208
203
  return False
209
204
 
210
- self.state = new_state
211
- if new_state == ProcessStates.BACKOFF:
205
+ self._state = new_state
206
+ if new_state == ProcessState.BACKOFF:
212
207
  now = time.time()
213
- self.backoff += 1
214
- self.delay = now + self.backoff
208
+ self._backoff += 1
209
+ self._delay = now + self._backoff
215
210
 
216
- event_class = self.event_map.get(new_state)
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
- EVENT_CALLBACKS.notify(event)
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.state not in states:
225
- current_state = get_process_state_description(self.state)
226
- allowable_states = ' '.join(map(get_process_state_description, states))
227
- processname = as_string(self.config.name)
228
- raise AssertionError('Assertion failed for %s: %s not in %s' % (processname, current_state, allowable_states)) # noqa
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.spawn_err = msg
232
- log.info('spawn_err: %s', msg)
226
+ self._spawn_err = msg
227
+ log.info('_spawn_err: %s', msg)
233
228
 
234
229
  def spawn(self) -> ta.Optional[int]:
235
- processname = as_string(self.config.name)
230
+ process_name = as_string(self._config.name)
236
231
 
237
232
  if self.pid:
238
- log.warning('process \'%s\' already running', processname)
233
+ log.warning('process \'%s\' already running', process_name)
239
234
  return None
240
235
 
241
- self.killing = False
242
- self.spawn_err = None
243
- self.exitstatus = None
244
- self.system_stop = False
245
- self.administrative_stop = False
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.laststart = time.time()
242
+ self._last_start = time.time()
248
243
 
249
244
  self._check_in_state(
250
- ProcessStates.EXITED,
251
- ProcessStates.FATAL,
252
- ProcessStates.BACKOFF,
253
- ProcessStates.STOPPED,
245
+ ProcessState.EXITED,
246
+ ProcessState.FATAL,
247
+ ProcessState.BACKOFF,
248
+ ProcessState.STOPPED,
254
249
  )
255
250
 
256
- self.change_state(ProcessStates.STARTING)
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(ProcessStates.STARTING)
263
- self.change_state(ProcessStates.BACKOFF)
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 '{processname}'"
267
+ msg = f"too many open files to spawn '{process_name}'"
273
268
  else:
274
- msg = f"unknown error making dispatchers for '{processname}': {errno.errorcode.get(code, code)}"
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(ProcessStates.STARTING)
277
- self.change_state(ProcessStates.BACKOFF)
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 \'{processname}\''
281
+ msg = f'Too many processes in process table to spawn \'{process_name}\''
287
282
  else:
288
- msg = f'unknown error during fork for \'{processname}\': {errno.errorcode.get(code, code)}'
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(ProcessStates.STARTING)
291
- self.change_state(ProcessStates.BACKOFF)
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.config.redirect_stderr
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(self, etype, stdout_fd)
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(self, etype, stderr_fd)
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(self, 'stdin', stdin_fd)
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.config.name), pid)
324
- self.spawn_err = None
325
- self.delay = time.time() + self.config.startsecs
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.config.redirect_stderr:
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.context.inherited_fds:
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.config.uid
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.config.name
365
- if self.group:
366
- env['SUPERVISOR_GROUP_NAME'] = self.group.config.name
367
- if self.config.environment is not None:
368
- env.update(self.config.environment)
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.config.directory
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.config.umask is not None:
384
- os.umask(self.config.umask)
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
- elif self.state == ProcessStates.RUNNING:
412
- if test_time > self.laststart and test_time < (self.laststart + self.config.startsecs):
413
- self.laststart = test_time - self.config.startsecs
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.state == ProcessStates.STOPPING:
416
- self.last_stop_report = min(test_time, self.last_stop_report)
417
- if self.delay > 0 and test_time < (self.delay - self.config.stopwaitsecs):
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.state == ProcessStates.BACKOFF:
421
- if self.delay > 0 and test_time < (self.delay - self.backoff):
422
- self.delay = test_time + self.backoff
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.administrative_stop = True
426
- self.last_stop_report = 0
427
- return self.kill(self.config.stopsignal)
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
- if self.state == ProcessStates.STOPPING:
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.last_stop_report + 2): # every 2 seconds
437
- log.info('waiting for %s to stop', as_string(self.config.name))
438
- self.last_stop_report = now
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.delay = 0
442
- self.backoff = 0
443
- self.system_stop = True
444
- self._check_in_state(ProcessStates.BACKOFF)
445
- self.change_state(ProcessStates.FATAL)
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
- processname = as_string(self.config.name)
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.state == ProcessStates.BACKOFF:
462
- log.debug('Attempted to kill %s, which is in BACKOFF state.', processname)
463
- self.change_state(ProcessStates.STOPPED)
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", (processname, signame(sig))
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.state == ProcessStates.STOPPING:
474
- killasgroup = self.config.killasgroup
495
+ if self._state == ProcessState.STOPPING:
496
+ killasgroup = self._config.killasgroup
475
497
  else:
476
- killasgroup = self.config.stopasgroup
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) %swith signal %s', processname, self.pid, as_group, signame(sig))
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.killing = True
486
- self.delay = now + self.config.stopwaitsecs
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(ProcessStates.RUNNING, ProcessStates.STARTING, ProcessStates.STOPPING)
489
- self.change_state(ProcessStates.STOPPING)
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', processname, self.pid, str(exc)) # noqa
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', (processname, self.pid, tb)
530
+ fmt, args = 'unknown problem killing %s (%s):%s', (process_name, self.pid, tb)
509
531
  log.critical(fmt, *args)
510
- self.change_state(ProcessStates.UNKNOWN)
511
- self.killing = False
512
- self.delay = 0
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
- processname = as_string(self.config.name)
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", (processname, signame(sig))
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', processname, self.pid, signame(sig))
553
+ log.debug('sending %s (pid %s) sig %s', process_name, self.pid, sig_name(sig))
532
554
 
533
- self._check_in_state(ProcessStates.RUNNING, ProcessStates.STARTING, ProcessStates.STOPPING)
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
- processname,
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', (processname, self.pid, tb)
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(ProcessStates.UNKNOWN)
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.laststop = now
571
- processname = as_string(self.config.name)
592
+ self._last_stop = now
593
+ process_name = as_string(self._config.name)
572
594
 
573
- if now > self.laststart:
574
- too_quickly = now - self.laststart < self.config.startsecs
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) laststart time is in the future, don't know how long process was running so "
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
- processname,
602
+ process_name,
581
603
  self.pid,
582
604
  )
583
605
 
584
- exit_expected = es in self.config.exitcodes
606
+ exit_expected = es in self._config.exitcodes
585
607
 
586
- if self.killing:
608
+ if self._killing:
587
609
  # likely the result of a stop request implies STOPPING -> STOPPED
588
- self.killing = False
589
- self.delay = 0
590
- self.exitstatus = es
610
+ self._killing = False
611
+ self._delay = 0
612
+ self._exitstatus = es
591
613
 
592
- fmt, args = 'stopped: %s (%s)', (processname, msg)
593
- self._check_in_state(ProcessStates.STOPPING)
594
- self.change_state(ProcessStates.STOPPED)
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.exitstatus = None
603
- self.spawn_err = 'Exited too quickly (process log may have details)'
604
- self._check_in_state(ProcessStates.STARTING)
605
- self.change_state(ProcessStates.BACKOFF)
606
- log.warning('exited: %s (%s)', processname, msg + '; not expected')
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.delay = 0
612
- self.backoff = 0
613
- self.exitstatus = es
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.laststart to be in the future, the normal
616
- # STARTING->RUNNING transition can be subverted so we perform the transition here.
617
- if self.state == ProcessStates.STARTING:
618
- self.change_state(ProcessStates.RUNNING)
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(ProcessStates.RUNNING)
642
+ self._check_in_state(ProcessState.RUNNING)
621
643
 
622
644
  if exit_expected:
623
645
  # expected exit code
624
- self.change_state(ProcessStates.EXITED, expected=True)
625
- log.info('exited: %s (%s)', processname, msg + '; expected')
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.spawn_err = f'Bad exit code {es}'
629
- self.change_state(ProcessStates.EXITED, expected=False)
630
- log.warning('exited: %s (%s)', processname, msg + '; not expected')
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.config.uid is None:
660
+ if self._config.uid is None:
646
661
  return None
647
- msg = drop_privileges(self.config.uid)
662
+ msg = drop_privileges(self._config.uid)
648
663
  return msg
649
664
 
650
- def __lt__(self, other):
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.config.name
659
- return f'<Subprocess at {id(self)} with name {name} in state {get_process_state_description(self.get_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.state
671
+ return self._state
663
672
 
664
- def transition(self):
673
+ def transition(self) -> None:
665
674
  now = time.time()
666
- state = self.state
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 > SupervisorStates.RESTARTING:
681
+ if self.context.state > SupervisorState.RESTARTING:
673
682
  # dont start any processes if supervisor is shutting down
674
- if state == ProcessStates.EXITED:
675
- if self.config.autorestart:
676
- if self.config.autorestart is RestartUnconditionally:
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.exitstatus not in self.config.exitcodes:
688
+ elif self._exitstatus not in self._config.exitcodes:
680
689
  # EXITED -> STARTING
681
690
  self.spawn()
682
691
 
683
- elif state == ProcessStates.STOPPED and not self.laststart:
684
- if self.config.autostart:
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 == ProcessStates.BACKOFF:
689
- if self.backoff <= self.config.startretries:
690
- if now > self.delay:
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
- processname = as_string(self.config.name)
695
- if state == ProcessStates.STARTING:
696
- if now - self.laststart > self.config.startsecs:
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.delay = 0
700
- self.backoff = 0
701
- self._check_in_state(ProcessStates.STARTING)
702
- self.change_state(ProcessStates.RUNNING)
703
- msg = ('entered RUNNING state, process has stayed up for > than %s seconds (startsecs)' % self.config.startsecs) # noqa
704
- logger.info('success: %s %s', processname, msg)
705
-
706
- if state == ProcessStates.BACKOFF:
707
- if self.backoff > self.config.startretries:
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', processname, msg)
720
+ logger.info('gave up: %s %s', process_name, msg)
712
721
 
713
- elif state == ProcessStates.STOPPING:
714
- time_left = self.delay - now
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', processname, self.pid)
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.config.name # noqa
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()