7
7
import socket
8
8
import struct
9
9
import time
10
+ from contextlib import contextmanager
10
11
from typing import Any , TYPE_CHECKING
11
12
12
13
import dateutil .parser
22
23
from epicbox import config , exceptions
23
24
24
25
if TYPE_CHECKING :
26
+ from collections .abc import Iterator
27
+
25
28
from docker .models .containers import Container
26
29
27
30
logger = structlog .get_logger ()
@@ -157,6 +160,54 @@ def _socket_write(sock: socket.SocketIO, data: bytes) -> int:
157
160
raise
158
161
159
162
163
+ def process_sock (
164
+ sock : socket .SocketIO , stdin : bytes | None , log : structlog .BoundLogger
165
+ ) -> tuple [bytes , int ]:
166
+ """Process the socket IO.
167
+
168
+ Read data from the socket if it is ready for reading.
169
+ Write data to the socket if it is ready for writing.
170
+ :returns: A tuple containing the received data and the number of bytes written.
171
+ """
172
+ ready_to_read , ready_to_write , _ = select .select ([sock ], [sock ], [], 1 )
173
+ received_data : bytes = b""
174
+ bytes_written = 0
175
+ if ready_to_read :
176
+ data = _socket_read (sock )
177
+ if data is None :
178
+ msg = "Received EOF from the container"
179
+ raise EOFError (msg )
180
+ received_data = data
181
+
182
+ if ready_to_write and stdin :
183
+ bytes_written = _socket_write (sock , stdin )
184
+ if bytes_written >= len (stdin ):
185
+ log .debug (
186
+ "All input data has been sent. "
187
+ "Shut down the write half of the socket." ,
188
+ )
189
+ sock ._sock .shutdown (socket .SHUT_WR ) # type: ignore[attr-defined]
190
+
191
+ if not ready_to_read and (not ready_to_write or not stdin ):
192
+ # Save CPU time by sleeping when there is no IO activity.
193
+ time .sleep (0.05 )
194
+
195
+ return received_data , bytes_written
196
+
197
+
198
+ @contextmanager
199
+ def attach_socket (
200
+ docker_client : DockerClient ,
201
+ container : Container ,
202
+ params : dict [str , Any ],
203
+ ) -> Iterator [socket .SocketIO ]:
204
+ sock = docker_client .api .attach_socket (container .id , params = params )
205
+
206
+ yield sock
207
+
208
+ sock .close ()
209
+
210
+
160
211
def docker_communicate (
161
212
container : Container ,
162
213
stdin : bytes | None = None ,
@@ -196,44 +247,33 @@ def docker_communicate(
196
247
"stream" : 1 ,
197
248
"logs" : 0 ,
198
249
}
199
- sock = docker_client .api .attach_socket (container .id , params = params )
200
- sock ._sock .setblocking (False ) # Make socket non-blocking
201
- log .info (
202
- "Attached to the container" ,
203
- params = params ,
204
- fd = sock .fileno (),
205
- timeout = timeout ,
206
- )
207
- if not stdin :
208
- log .debug ("There is no input data. Shut down the write half of the socket." )
209
- sock ._sock .shutdown (socket .SHUT_WR )
210
- if start_container :
211
- container .start ()
212
- log .info ("Container started" )
213
-
214
- stream_data = b""
215
- start_time = time .monotonic ()
216
- while timeout is None or time .monotonic () - start_time < timeout :
217
- read_ready , write_ready , _ = select .select ([sock ], [sock ], [], 1 )
218
- is_io_active = bool (read_ready or (write_ready and stdin ))
219
-
220
- if read_ready :
250
+
251
+ with attach_socket (docker_client , container , params ) as sock :
252
+ sock ._sock .setblocking (False ) # type: ignore[attr-defined] # Make socket non-blocking
253
+ log .info (
254
+ "Attached to the container" ,
255
+ params = params ,
256
+ fd = sock .fileno (),
257
+ timeout = timeout ,
258
+ )
259
+ if not stdin :
260
+ log .debug ("There is no input data. Shut down the write half of the socket." )
261
+ sock ._sock .shutdown (socket .SHUT_WR ) # type: ignore[attr-defined]
262
+ if start_container :
263
+ container .start ()
264
+ log .info ("Container started" )
265
+
266
+ stream_data = b""
267
+ start_time = time .monotonic ()
268
+ while timeout is None or time .monotonic () - start_time < timeout :
221
269
try :
222
- data = _socket_read (sock )
270
+ received_data , bytes_written = process_sock (sock , stdin , log )
223
271
except ConnectionResetError :
224
272
log .warning (
225
273
"Connection reset caught on reading the container "
226
274
"output stream. Break communication" ,
227
275
)
228
276
break
229
- if data is None :
230
- log .debug ("Container output reached EOF. Closing the socket" )
231
- break
232
- stream_data += data
233
-
234
- if write_ready and stdin :
235
- try :
236
- written = _socket_write (sock , stdin )
237
277
except BrokenPipeError :
238
278
# Broken pipe may happen when a container terminates quickly
239
279
# (e.g. OOM Killer) and docker manages to close the socket
@@ -242,22 +282,18 @@ def docker_communicate(
242
282
"Broken pipe caught on writing to stdin. Break communication" ,
243
283
)
244
284
break
245
- stdin = stdin [written :]
246
- if not stdin :
247
- log .debug (
248
- "All input data has been sent. Shut down the write "
249
- "half of the socket." ,
250
- )
251
- sock ._sock .shutdown (socket .SHUT_WR )
252
-
253
- if not is_io_active :
254
- # Save CPU time
255
- time .sleep (0.05 )
256
- else :
257
- sock .close ()
258
- msg = "Container didn't terminate after timeout seconds"
259
- raise TimeoutError (msg )
260
- sock .close ()
285
+ except EOFError :
286
+ log .debug ("Container output reached EOF. Closing the socket" )
287
+ break
288
+
289
+ if received_data :
290
+ stream_data += received_data
291
+ if stdin and bytes_written > 0 :
292
+ stdin = stdin [bytes_written :]
293
+ else :
294
+ msg = "Container didn't terminate after timeout seconds"
295
+ raise TimeoutError (msg )
296
+
261
297
return demultiplex_docker_stream (stream_data )
262
298
263
299
0 commit comments