ominfra 0.0.0.dev126__py3-none-any.whl → 0.0.0.dev128__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ominfra/clouds/aws/auth.py +1 -1
- ominfra/deploy/_executor.py +1 -1
- ominfra/deploy/poly/_main.py +1 -1
- ominfra/pyremote/_runcommands.py +1 -1
- ominfra/scripts/journald2aws.py +2 -2
- ominfra/scripts/supervisor.py +4736 -4166
- ominfra/supervisor/configs.py +34 -11
- ominfra/supervisor/context.py +7 -345
- ominfra/supervisor/dispatchers.py +21 -324
- ominfra/supervisor/dispatchersimpl.py +343 -0
- ominfra/supervisor/groups.py +33 -111
- ominfra/supervisor/groupsimpl.py +86 -0
- ominfra/supervisor/inject.py +45 -20
- ominfra/supervisor/main.py +3 -3
- ominfra/supervisor/pipes.py +85 -0
- ominfra/supervisor/poller.py +42 -38
- ominfra/supervisor/privileges.py +65 -0
- ominfra/supervisor/process.py +6 -742
- ominfra/supervisor/processimpl.py +516 -0
- ominfra/supervisor/setup.py +38 -0
- ominfra/supervisor/setupimpl.py +262 -0
- ominfra/supervisor/spawning.py +32 -0
- ominfra/supervisor/spawningimpl.py +350 -0
- ominfra/supervisor/supervisor.py +67 -84
- ominfra/supervisor/types.py +101 -47
- ominfra/supervisor/utils/__init__.py +0 -0
- ominfra/supervisor/utils/collections.py +52 -0
- ominfra/supervisor/utils/diag.py +31 -0
- ominfra/supervisor/utils/fds.py +46 -0
- ominfra/supervisor/utils/fs.py +47 -0
- ominfra/supervisor/utils/os.py +45 -0
- ominfra/supervisor/utils/ostypes.py +9 -0
- ominfra/supervisor/utils/signals.py +60 -0
- ominfra/supervisor/utils/strings.py +105 -0
- ominfra/supervisor/utils/users.py +67 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/RECORD +41 -25
- ominfra/supervisor/datatypes.py +0 -175
- ominfra/supervisor/signals.py +0 -52
- ominfra/supervisor/utils.py +0 -206
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/top_level.txt +0 -0
@@ -1,336 +1,33 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
|
-
import abc
|
3
|
-
import errno
|
4
|
-
import logging
|
5
|
-
import os
|
6
|
-
import typing as ta
|
7
|
-
|
8
|
-
from omlish.lite.logs import log
|
9
|
-
|
10
|
-
from .configs import ProcessConfig
|
11
|
-
from .events import EventCallbacks
|
12
|
-
from .events import ProcessCommunicationEvent
|
13
|
-
from .events import ProcessLogStderrEvent
|
14
|
-
from .events import ProcessLogStdoutEvent
|
15
2
|
from .types import Dispatcher
|
16
|
-
from .types import InputDispatcher
|
17
3
|
from .types import OutputDispatcher
|
18
|
-
from .
|
19
|
-
from .utils import
|
20
|
-
from .utils import compact_traceback
|
21
|
-
from .utils import find_prefix_at_end
|
22
|
-
from .utils import read_fd
|
23
|
-
from .utils import strip_escapes
|
24
|
-
|
25
|
-
|
26
|
-
class BaseDispatcherImpl(Dispatcher, abc.ABC):
|
27
|
-
def __init__(
|
28
|
-
self,
|
29
|
-
process: Process,
|
30
|
-
channel: str,
|
31
|
-
fd: int,
|
32
|
-
*,
|
33
|
-
event_callbacks: EventCallbacks,
|
34
|
-
) -> None:
|
35
|
-
super().__init__()
|
36
|
-
|
37
|
-
self._process = process # process which "owns" this dispatcher
|
38
|
-
self._channel = channel # 'stderr' or 'stdout'
|
39
|
-
self._fd = fd
|
40
|
-
self._event_callbacks = event_callbacks
|
41
|
-
|
42
|
-
self._closed = False # True if close() has been called
|
43
|
-
|
44
|
-
def __repr__(self) -> str:
|
45
|
-
return f'<{self.__class__.__name__} at {id(self)} for {self._process} ({self._channel})>'
|
46
|
-
|
47
|
-
@property
|
48
|
-
def process(self) -> Process:
|
49
|
-
return self._process
|
50
|
-
|
51
|
-
@property
|
52
|
-
def channel(self) -> str:
|
53
|
-
return self._channel
|
54
|
-
|
55
|
-
@property
|
56
|
-
def fd(self) -> int:
|
57
|
-
return self._fd
|
58
|
-
|
59
|
-
@property
|
60
|
-
def closed(self) -> bool:
|
61
|
-
return self._closed
|
62
|
-
|
63
|
-
def handle_error(self) -> None:
|
64
|
-
nil, t, v, tbinfo = compact_traceback()
|
65
|
-
|
66
|
-
log.critical('uncaptured python exception, closing channel %s (%s:%s %s)', repr(self), t, v, tbinfo)
|
67
|
-
self.close()
|
68
|
-
|
69
|
-
def close(self) -> None:
|
70
|
-
if not self._closed:
|
71
|
-
log.debug('fd %s closed, stopped monitoring %s', self._fd, self)
|
72
|
-
self._closed = True
|
73
|
-
|
74
|
-
|
75
|
-
class OutputDispatcherImpl(BaseDispatcherImpl, OutputDispatcher):
|
76
|
-
"""
|
77
|
-
Dispatcher for one channel (stdout or stderr) of one process. Serves several purposes:
|
78
|
-
|
79
|
-
- capture output sent within <!--XSUPERVISOR:BEGIN--> and <!--XSUPERVISOR:END--> tags and signal a
|
80
|
-
ProcessCommunicationEvent by calling notify_event(event).
|
81
|
-
- route the output to the appropriate log handlers as specified in the config.
|
82
|
-
"""
|
83
|
-
|
84
|
-
def __init__(
|
85
|
-
self,
|
86
|
-
process: Process,
|
87
|
-
event_type: ta.Type[ProcessCommunicationEvent],
|
88
|
-
fd: int,
|
89
|
-
*,
|
90
|
-
event_callbacks: EventCallbacks,
|
91
|
-
) -> None:
|
92
|
-
super().__init__(
|
93
|
-
process,
|
94
|
-
event_type.channel,
|
95
|
-
fd,
|
96
|
-
event_callbacks=event_callbacks,
|
97
|
-
)
|
98
|
-
|
99
|
-
self._event_type = event_type
|
100
|
-
|
101
|
-
self._lc: ProcessConfig.Log = getattr(process.config, self._channel)
|
102
|
-
|
103
|
-
self._init_normal_log()
|
104
|
-
self._init_capture_log()
|
105
|
-
|
106
|
-
self._child_log = self._normal_log
|
107
|
-
|
108
|
-
self._capture_mode = False # are we capturing process event data
|
109
|
-
self._output_buffer = b'' # data waiting to be logged
|
110
|
-
|
111
|
-
# all code below is purely for minor speedups
|
112
|
-
|
113
|
-
begin_token = self._event_type.BEGIN_TOKEN
|
114
|
-
end_token = self._event_type.END_TOKEN
|
115
|
-
self._begin_token_data = (begin_token, len(begin_token))
|
116
|
-
self._end_token_data = (end_token, len(end_token))
|
117
|
-
|
118
|
-
self._main_log_level = logging.DEBUG
|
119
|
-
|
120
|
-
self._log_to_main_log = process.context.config.loglevel <= self._main_log_level
|
121
|
-
|
122
|
-
config = self._process.config
|
123
|
-
self._stdout_events_enabled = config.stdout.events_enabled
|
124
|
-
self._stderr_events_enabled = config.stderr.events_enabled
|
125
|
-
|
126
|
-
_child_log: ta.Optional[logging.Logger] = None # the current logger (normal_log or capture_log)
|
127
|
-
_normal_log: ta.Optional[logging.Logger] = None # the "normal" (non-capture) logger
|
128
|
-
_capture_log: ta.Optional[logging.Logger] = None # the logger used while we're in capture_mode
|
4
|
+
from .utils.collections import KeyedCollection
|
5
|
+
from .utils.ostypes import Fd
|
129
6
|
|
130
|
-
def _init_normal_log(self) -> None:
|
131
|
-
"""
|
132
|
-
Configure the "normal" (non-capture) log for this channel of this process. Sets self.normal_log if logging is
|
133
|
-
enabled.
|
134
|
-
"""
|
135
7
|
|
136
|
-
|
137
|
-
|
8
|
+
class Dispatchers(KeyedCollection[Fd, Dispatcher]):
|
9
|
+
def _key(self, v: Dispatcher) -> Fd:
|
10
|
+
return v.fd
|
138
11
|
|
139
|
-
|
140
|
-
maxbytes = self._lc.maxbytes # noqa
|
141
|
-
backups = self._lc.backups # noqa
|
142
|
-
to_syslog = self._lc.syslog
|
12
|
+
#
|
143
13
|
|
144
|
-
|
145
|
-
|
14
|
+
def drain(self) -> None:
|
15
|
+
for d in self:
|
16
|
+
# note that we *must* call readable() for every dispatcher, as it may have side effects for a given
|
17
|
+
# dispatcher (eg. call handle_listener_state_change for event listener processes)
|
18
|
+
if d.readable():
|
19
|
+
d.handle_read_event()
|
20
|
+
if d.writable():
|
21
|
+
d.handle_write_event()
|
146
22
|
|
147
|
-
|
148
|
-
# loggers.handle_file(
|
149
|
-
# self.normal_log,
|
150
|
-
# filename=logfile,
|
151
|
-
# fmt='%(message)s',
|
152
|
-
# rotating=bool(maxbytes), # optimization
|
153
|
-
# maxbytes=maxbytes,
|
154
|
-
# backups=backups,
|
155
|
-
# )
|
156
|
-
|
157
|
-
# if to_syslog:
|
158
|
-
# loggers.handle_syslog(
|
159
|
-
# self.normal_log,
|
160
|
-
# fmt=config.name + ' %(message)s',
|
161
|
-
# )
|
162
|
-
|
163
|
-
def _init_capture_log(self) -> None:
|
164
|
-
"""
|
165
|
-
Configure the capture log for this process. This log is used to temporarily capture output when special output
|
166
|
-
is detected. Sets self.capture_log if capturing is enabled.
|
167
|
-
"""
|
168
|
-
|
169
|
-
capture_maxbytes = self._lc.capture_maxbytes
|
170
|
-
if capture_maxbytes:
|
171
|
-
self._capture_log = logging.getLogger(__name__)
|
172
|
-
# loggers.handle_boundIO(
|
173
|
-
# self._capture_log,
|
174
|
-
# fmt='%(message)s',
|
175
|
-
# maxbytes=capture_maxbytes,
|
176
|
-
# )
|
23
|
+
#
|
177
24
|
|
178
25
|
def remove_logs(self) -> None:
|
179
|
-
for
|
180
|
-
if
|
181
|
-
|
182
|
-
handler.remove() # type: ignore
|
183
|
-
handler.reopen() # type: ignore
|
26
|
+
for d in self:
|
27
|
+
if isinstance(d, OutputDispatcher):
|
28
|
+
d.remove_logs()
|
184
29
|
|
185
30
|
def reopen_logs(self) -> None:
|
186
|
-
for
|
187
|
-
if
|
188
|
-
|
189
|
-
handler.reopen() # type: ignore
|
190
|
-
|
191
|
-
def _log(self, data: ta.Union[str, bytes, None]) -> None:
|
192
|
-
if not data:
|
193
|
-
return
|
194
|
-
|
195
|
-
if self._process.context.config.strip_ansi:
|
196
|
-
data = strip_escapes(as_bytes(data))
|
197
|
-
|
198
|
-
if self._child_log:
|
199
|
-
self._child_log.info(data)
|
200
|
-
|
201
|
-
if self._log_to_main_log:
|
202
|
-
if not isinstance(data, bytes):
|
203
|
-
text = data
|
204
|
-
else:
|
205
|
-
try:
|
206
|
-
text = data.decode('utf-8')
|
207
|
-
except UnicodeDecodeError:
|
208
|
-
text = f'Undecodable: {data!r}'
|
209
|
-
log.log(self._main_log_level, '%r %s output:\n%s', self._process.config.name, self._channel, text) # noqa
|
210
|
-
|
211
|
-
if self._channel == 'stdout':
|
212
|
-
if self._stdout_events_enabled:
|
213
|
-
self._event_callbacks.notify(ProcessLogStdoutEvent(self._process, self._process.pid, data))
|
214
|
-
|
215
|
-
elif self._stderr_events_enabled:
|
216
|
-
self._event_callbacks.notify(ProcessLogStderrEvent(self._process, self._process.pid, data))
|
217
|
-
|
218
|
-
def record_output(self) -> None:
|
219
|
-
if self._capture_log is None:
|
220
|
-
# shortcut trying to find capture data
|
221
|
-
data = self._output_buffer
|
222
|
-
self._output_buffer = b''
|
223
|
-
self._log(data)
|
224
|
-
return
|
225
|
-
|
226
|
-
if self._capture_mode:
|
227
|
-
token, token_len = self._end_token_data
|
228
|
-
else:
|
229
|
-
token, token_len = self._begin_token_data
|
230
|
-
|
231
|
-
if len(self._output_buffer) <= token_len:
|
232
|
-
return # not enough data
|
233
|
-
|
234
|
-
data = self._output_buffer
|
235
|
-
self._output_buffer = b''
|
236
|
-
|
237
|
-
try:
|
238
|
-
before, after = data.split(token, 1)
|
239
|
-
except ValueError:
|
240
|
-
after = None
|
241
|
-
index = find_prefix_at_end(data, token)
|
242
|
-
if index:
|
243
|
-
self._output_buffer = self._output_buffer + data[-index:]
|
244
|
-
data = data[:-index]
|
245
|
-
self._log(data)
|
246
|
-
else:
|
247
|
-
self._log(before)
|
248
|
-
self.toggle_capture_mode()
|
249
|
-
self._output_buffer = after # type: ignore
|
250
|
-
|
251
|
-
if after:
|
252
|
-
self.record_output()
|
253
|
-
|
254
|
-
def toggle_capture_mode(self) -> None:
|
255
|
-
self._capture_mode = not self._capture_mode
|
256
|
-
|
257
|
-
if self._capture_log is not None:
|
258
|
-
if self._capture_mode:
|
259
|
-
self._child_log = self._capture_log
|
260
|
-
else:
|
261
|
-
for handler in self._capture_log.handlers:
|
262
|
-
handler.flush()
|
263
|
-
data = self._capture_log.getvalue() # type: ignore
|
264
|
-
channel = self._channel
|
265
|
-
procname = self._process.config.name
|
266
|
-
event = self._event_type(self._process, self._process.pid, data)
|
267
|
-
self._event_callbacks.notify(event)
|
268
|
-
|
269
|
-
log.debug('%r %s emitted a comm event', procname, channel)
|
270
|
-
for handler in self._capture_log.handlers:
|
271
|
-
handler.remove() # type: ignore
|
272
|
-
handler.reopen() # type: ignore
|
273
|
-
self._child_log = self._normal_log
|
274
|
-
|
275
|
-
def writable(self) -> bool:
|
276
|
-
return False
|
277
|
-
|
278
|
-
def readable(self) -> bool:
|
279
|
-
if self._closed:
|
280
|
-
return False
|
281
|
-
return True
|
282
|
-
|
283
|
-
def handle_read_event(self) -> None:
|
284
|
-
data = read_fd(self._fd)
|
285
|
-
self._output_buffer += data
|
286
|
-
self.record_output()
|
287
|
-
if not data:
|
288
|
-
# if we get no data back from the pipe, it means that the child process has ended. See
|
289
|
-
# mail.python.org/pipermail/python-dev/2004-August/046850.html
|
290
|
-
self.close()
|
291
|
-
|
292
|
-
|
293
|
-
class InputDispatcherImpl(BaseDispatcherImpl, InputDispatcher):
|
294
|
-
def __init__(
|
295
|
-
self,
|
296
|
-
process: Process,
|
297
|
-
channel: str,
|
298
|
-
fd: int,
|
299
|
-
*,
|
300
|
-
event_callbacks: EventCallbacks,
|
301
|
-
) -> None:
|
302
|
-
super().__init__(
|
303
|
-
process,
|
304
|
-
channel,
|
305
|
-
fd,
|
306
|
-
event_callbacks=event_callbacks,
|
307
|
-
)
|
308
|
-
|
309
|
-
self._input_buffer = b''
|
310
|
-
|
311
|
-
def write(self, chars: ta.Union[bytes, str]) -> None:
|
312
|
-
self._input_buffer += as_bytes(chars)
|
313
|
-
|
314
|
-
def writable(self) -> bool:
|
315
|
-
if self._input_buffer and not self._closed:
|
316
|
-
return True
|
317
|
-
return False
|
318
|
-
|
319
|
-
def readable(self) -> bool:
|
320
|
-
return False
|
321
|
-
|
322
|
-
def flush(self) -> None:
|
323
|
-
# other code depends on this raising EPIPE if the pipe is closed
|
324
|
-
sent = os.write(self._fd, as_bytes(self._input_buffer))
|
325
|
-
self._input_buffer = self._input_buffer[sent:]
|
326
|
-
|
327
|
-
def handle_write_event(self) -> None:
|
328
|
-
if self._input_buffer:
|
329
|
-
try:
|
330
|
-
self.flush()
|
331
|
-
except OSError as why:
|
332
|
-
if why.args[0] == errno.EPIPE:
|
333
|
-
self._input_buffer = b''
|
334
|
-
self.close()
|
335
|
-
else:
|
336
|
-
raise
|
31
|
+
for d in self:
|
32
|
+
if isinstance(d, OutputDispatcher):
|
33
|
+
d.reopen_logs()
|
@@ -0,0 +1,343 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import errno
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from omlish.lite.logs import log
|
9
|
+
|
10
|
+
from .configs import ProcessConfig
|
11
|
+
from .events import EventCallbacks
|
12
|
+
from .events import ProcessCommunicationEvent
|
13
|
+
from .events import ProcessLogStderrEvent
|
14
|
+
from .events import ProcessLogStdoutEvent
|
15
|
+
from .types import Dispatcher
|
16
|
+
from .types import InputDispatcher
|
17
|
+
from .types import OutputDispatcher
|
18
|
+
from .types import Process
|
19
|
+
from .utils.diag import compact_traceback
|
20
|
+
from .utils.fds import read_fd
|
21
|
+
from .utils.ostypes import Fd
|
22
|
+
from .utils.strings import as_bytes
|
23
|
+
from .utils.strings import find_prefix_at_end
|
24
|
+
from .utils.strings import strip_escapes
|
25
|
+
|
26
|
+
|
27
|
+
class BaseDispatcherImpl(Dispatcher, abc.ABC):
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
process: Process,
|
31
|
+
channel: str,
|
32
|
+
fd: Fd,
|
33
|
+
*,
|
34
|
+
event_callbacks: EventCallbacks,
|
35
|
+
) -> None:
|
36
|
+
super().__init__()
|
37
|
+
|
38
|
+
self._process = process # process which "owns" this dispatcher
|
39
|
+
self._channel = channel # 'stderr' or 'stdout'
|
40
|
+
self._fd = fd
|
41
|
+
self._event_callbacks = event_callbacks
|
42
|
+
|
43
|
+
self._closed = False # True if close() has been called
|
44
|
+
|
45
|
+
#
|
46
|
+
|
47
|
+
def __repr__(self) -> str:
|
48
|
+
return f'<{self.__class__.__name__} at {id(self)} for {self._process} ({self._channel})>'
|
49
|
+
|
50
|
+
#
|
51
|
+
|
52
|
+
@property
|
53
|
+
def process(self) -> Process:
|
54
|
+
return self._process
|
55
|
+
|
56
|
+
@property
|
57
|
+
def channel(self) -> str:
|
58
|
+
return self._channel
|
59
|
+
|
60
|
+
@property
|
61
|
+
def fd(self) -> Fd:
|
62
|
+
return self._fd
|
63
|
+
|
64
|
+
@property
|
65
|
+
def closed(self) -> bool:
|
66
|
+
return self._closed
|
67
|
+
|
68
|
+
#
|
69
|
+
|
70
|
+
def close(self) -> None:
|
71
|
+
if not self._closed:
|
72
|
+
log.debug('fd %s closed, stopped monitoring %s', self._fd, self)
|
73
|
+
self._closed = True
|
74
|
+
|
75
|
+
def handle_error(self) -> None:
|
76
|
+
nil, t, v, tbinfo = compact_traceback()
|
77
|
+
|
78
|
+
log.critical('uncaptured python exception, closing channel %s (%s:%s %s)', repr(self), t, v, tbinfo)
|
79
|
+
self.close()
|
80
|
+
|
81
|
+
|
82
|
+
class OutputDispatcherImpl(BaseDispatcherImpl, OutputDispatcher):
|
83
|
+
"""
|
84
|
+
Dispatcher for one channel (stdout or stderr) of one process. Serves several purposes:
|
85
|
+
|
86
|
+
- capture output sent within <!--XSUPERVISOR:BEGIN--> and <!--XSUPERVISOR:END--> tags and signal a
|
87
|
+
ProcessCommunicationEvent by calling notify_event(event).
|
88
|
+
- route the output to the appropriate log handlers as specified in the config.
|
89
|
+
"""
|
90
|
+
|
91
|
+
def __init__(
|
92
|
+
self,
|
93
|
+
process: Process,
|
94
|
+
event_type: ta.Type[ProcessCommunicationEvent],
|
95
|
+
fd: Fd,
|
96
|
+
*,
|
97
|
+
event_callbacks: EventCallbacks,
|
98
|
+
) -> None:
|
99
|
+
super().__init__(
|
100
|
+
process,
|
101
|
+
event_type.channel,
|
102
|
+
fd,
|
103
|
+
event_callbacks=event_callbacks,
|
104
|
+
)
|
105
|
+
|
106
|
+
self._event_type = event_type
|
107
|
+
|
108
|
+
self._lc: ProcessConfig.Log = getattr(process.config, self._channel)
|
109
|
+
|
110
|
+
self._init_normal_log()
|
111
|
+
self._init_capture_log()
|
112
|
+
|
113
|
+
self._child_log = self._normal_log
|
114
|
+
|
115
|
+
self._capture_mode = False # are we capturing process event data
|
116
|
+
self._output_buffer = b'' # data waiting to be logged
|
117
|
+
|
118
|
+
# all code below is purely for minor speedups
|
119
|
+
|
120
|
+
begin_token = self._event_type.BEGIN_TOKEN
|
121
|
+
end_token = self._event_type.END_TOKEN
|
122
|
+
self._begin_token_data = (begin_token, len(begin_token))
|
123
|
+
self._end_token_data = (end_token, len(end_token))
|
124
|
+
|
125
|
+
self._main_log_level = logging.DEBUG
|
126
|
+
|
127
|
+
self._log_to_main_log = process.context.config.loglevel <= self._main_log_level
|
128
|
+
|
129
|
+
config = self._process.config
|
130
|
+
self._stdout_events_enabled = config.stdout.events_enabled
|
131
|
+
self._stderr_events_enabled = config.stderr.events_enabled
|
132
|
+
|
133
|
+
_child_log: ta.Optional[logging.Logger] = None # the current logger (normal_log or capture_log)
|
134
|
+
_normal_log: ta.Optional[logging.Logger] = None # the "normal" (non-capture) logger
|
135
|
+
_capture_log: ta.Optional[logging.Logger] = None # the logger used while we're in capture_mode
|
136
|
+
|
137
|
+
def _init_normal_log(self) -> None:
|
138
|
+
"""
|
139
|
+
Configure the "normal" (non-capture) log for this channel of this process. Sets self.normal_log if logging is
|
140
|
+
enabled.
|
141
|
+
"""
|
142
|
+
|
143
|
+
config = self._process.config # noqa
|
144
|
+
channel = self._channel # noqa
|
145
|
+
|
146
|
+
logfile = self._lc.file
|
147
|
+
maxbytes = self._lc.maxbytes # noqa
|
148
|
+
backups = self._lc.backups # noqa
|
149
|
+
to_syslog = self._lc.syslog
|
150
|
+
|
151
|
+
if logfile or to_syslog:
|
152
|
+
self._normal_log = logging.getLogger(__name__)
|
153
|
+
|
154
|
+
# if logfile:
|
155
|
+
# loggers.handle_file(
|
156
|
+
# self.normal_log,
|
157
|
+
# filename=logfile,
|
158
|
+
# fmt='%(message)s',
|
159
|
+
# rotating=bool(maxbytes), # optimization
|
160
|
+
# maxbytes=maxbytes,
|
161
|
+
# backups=backups,
|
162
|
+
# )
|
163
|
+
|
164
|
+
# if to_syslog:
|
165
|
+
# loggers.handle_syslog(
|
166
|
+
# self.normal_log,
|
167
|
+
# fmt=config.name + ' %(message)s',
|
168
|
+
# )
|
169
|
+
|
170
|
+
def _init_capture_log(self) -> None:
|
171
|
+
"""
|
172
|
+
Configure the capture log for this process. This log is used to temporarily capture output when special output
|
173
|
+
is detected. Sets self.capture_log if capturing is enabled.
|
174
|
+
"""
|
175
|
+
|
176
|
+
capture_maxbytes = self._lc.capture_maxbytes
|
177
|
+
if capture_maxbytes:
|
178
|
+
self._capture_log = logging.getLogger(__name__)
|
179
|
+
# loggers.handle_boundIO(
|
180
|
+
# self._capture_log,
|
181
|
+
# fmt='%(message)s',
|
182
|
+
# maxbytes=capture_maxbytes,
|
183
|
+
# )
|
184
|
+
|
185
|
+
def remove_logs(self) -> None:
|
186
|
+
for l in (self._normal_log, self._capture_log):
|
187
|
+
if l is not None:
|
188
|
+
for handler in l.handlers:
|
189
|
+
handler.remove() # type: ignore
|
190
|
+
handler.reopen() # type: ignore
|
191
|
+
|
192
|
+
def reopen_logs(self) -> None:
|
193
|
+
for l in (self._normal_log, self._capture_log):
|
194
|
+
if l is not None:
|
195
|
+
for handler in l.handlers:
|
196
|
+
handler.reopen() # type: ignore
|
197
|
+
|
198
|
+
def _log(self, data: ta.Union[str, bytes, None]) -> None:
|
199
|
+
if not data:
|
200
|
+
return
|
201
|
+
|
202
|
+
if self._process.context.config.strip_ansi:
|
203
|
+
data = strip_escapes(as_bytes(data))
|
204
|
+
|
205
|
+
if self._child_log:
|
206
|
+
self._child_log.info(data)
|
207
|
+
|
208
|
+
if self._log_to_main_log:
|
209
|
+
if not isinstance(data, bytes):
|
210
|
+
text = data
|
211
|
+
else:
|
212
|
+
try:
|
213
|
+
text = data.decode('utf-8')
|
214
|
+
except UnicodeDecodeError:
|
215
|
+
text = f'Undecodable: {data!r}'
|
216
|
+
log.log(self._main_log_level, '%r %s output:\n%s', self._process.config.name, self._channel, text) # noqa
|
217
|
+
|
218
|
+
if self._channel == 'stdout':
|
219
|
+
if self._stdout_events_enabled:
|
220
|
+
self._event_callbacks.notify(ProcessLogStdoutEvent(self._process, self._process.pid, data))
|
221
|
+
|
222
|
+
elif self._stderr_events_enabled:
|
223
|
+
self._event_callbacks.notify(ProcessLogStderrEvent(self._process, self._process.pid, data))
|
224
|
+
|
225
|
+
def record_output(self) -> None:
|
226
|
+
if self._capture_log is None:
|
227
|
+
# shortcut trying to find capture data
|
228
|
+
data = self._output_buffer
|
229
|
+
self._output_buffer = b''
|
230
|
+
self._log(data)
|
231
|
+
return
|
232
|
+
|
233
|
+
if self._capture_mode:
|
234
|
+
token, token_len = self._end_token_data
|
235
|
+
else:
|
236
|
+
token, token_len = self._begin_token_data
|
237
|
+
|
238
|
+
if len(self._output_buffer) <= token_len:
|
239
|
+
return # not enough data
|
240
|
+
|
241
|
+
data = self._output_buffer
|
242
|
+
self._output_buffer = b''
|
243
|
+
|
244
|
+
try:
|
245
|
+
before, after = data.split(token, 1)
|
246
|
+
except ValueError:
|
247
|
+
after = None
|
248
|
+
index = find_prefix_at_end(data, token)
|
249
|
+
if index:
|
250
|
+
self._output_buffer = self._output_buffer + data[-index:]
|
251
|
+
data = data[:-index]
|
252
|
+
self._log(data)
|
253
|
+
else:
|
254
|
+
self._log(before)
|
255
|
+
self.toggle_capture_mode()
|
256
|
+
self._output_buffer = after # type: ignore
|
257
|
+
|
258
|
+
if after:
|
259
|
+
self.record_output()
|
260
|
+
|
261
|
+
def toggle_capture_mode(self) -> None:
|
262
|
+
self._capture_mode = not self._capture_mode
|
263
|
+
|
264
|
+
if self._capture_log is not None:
|
265
|
+
if self._capture_mode:
|
266
|
+
self._child_log = self._capture_log
|
267
|
+
else:
|
268
|
+
for handler in self._capture_log.handlers:
|
269
|
+
handler.flush()
|
270
|
+
data = self._capture_log.getvalue() # type: ignore
|
271
|
+
channel = self._channel
|
272
|
+
procname = self._process.config.name
|
273
|
+
event = self._event_type(self._process, self._process.pid, data)
|
274
|
+
self._event_callbacks.notify(event)
|
275
|
+
|
276
|
+
log.debug('%r %s emitted a comm event', procname, channel)
|
277
|
+
for handler in self._capture_log.handlers:
|
278
|
+
handler.remove() # type: ignore
|
279
|
+
handler.reopen() # type: ignore
|
280
|
+
self._child_log = self._normal_log
|
281
|
+
|
282
|
+
def writable(self) -> bool:
|
283
|
+
return False
|
284
|
+
|
285
|
+
def readable(self) -> bool:
|
286
|
+
if self._closed:
|
287
|
+
return False
|
288
|
+
return True
|
289
|
+
|
290
|
+
def handle_read_event(self) -> None:
|
291
|
+
data = read_fd(self._fd)
|
292
|
+
self._output_buffer += data
|
293
|
+
self.record_output()
|
294
|
+
if not data:
|
295
|
+
# if we get no data back from the pipe, it means that the child process has ended. See
|
296
|
+
# mail.python.org/pipermail/python-dev/2004-August/046850.html
|
297
|
+
self.close()
|
298
|
+
|
299
|
+
|
300
|
+
class InputDispatcherImpl(BaseDispatcherImpl, InputDispatcher):
|
301
|
+
def __init__(
|
302
|
+
self,
|
303
|
+
process: Process,
|
304
|
+
channel: str,
|
305
|
+
fd: Fd,
|
306
|
+
*,
|
307
|
+
event_callbacks: EventCallbacks,
|
308
|
+
) -> None:
|
309
|
+
super().__init__(
|
310
|
+
process,
|
311
|
+
channel,
|
312
|
+
fd,
|
313
|
+
event_callbacks=event_callbacks,
|
314
|
+
)
|
315
|
+
|
316
|
+
self._input_buffer = b''
|
317
|
+
|
318
|
+
def write(self, chars: ta.Union[bytes, str]) -> None:
|
319
|
+
self._input_buffer += as_bytes(chars)
|
320
|
+
|
321
|
+
def writable(self) -> bool:
|
322
|
+
if self._input_buffer and not self._closed:
|
323
|
+
return True
|
324
|
+
return False
|
325
|
+
|
326
|
+
def readable(self) -> bool:
|
327
|
+
return False
|
328
|
+
|
329
|
+
def flush(self) -> None:
|
330
|
+
# other code depends on this raising EPIPE if the pipe is closed
|
331
|
+
sent = os.write(self._fd, as_bytes(self._input_buffer))
|
332
|
+
self._input_buffer = self._input_buffer[sent:]
|
333
|
+
|
334
|
+
def handle_write_event(self) -> None:
|
335
|
+
if self._input_buffer:
|
336
|
+
try:
|
337
|
+
self.flush()
|
338
|
+
except OSError as why:
|
339
|
+
if why.args[0] == errno.EPIPE:
|
340
|
+
self._input_buffer = b''
|
341
|
+
self.close()
|
342
|
+
else:
|
343
|
+
raise
|