pyxcp 0.23.4__cp310-cp310-macosx_11_0_arm64.whl → 0.23.7__cp310-cp310-macosx_11_0_arm64.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.
Potentially problematic release.
This version of pyxcp might be problematic. Click here for more details.
- pyxcp/__init__.py +1 -1
- pyxcp/cmdline.py +1 -1
- pyxcp/config/__init__.py +74 -20
- pyxcp/daq_stim/__init__.py +61 -1
- pyxcp/examples/run_daq.py +1 -1
- pyxcp/master/errorhandler.py +116 -11
- pyxcp/master/master.py +18 -1
- pyxcp/recorder/__init__.py +3 -6
- pyxcp/recorder/rekorder.cpython-310-darwin.so +0 -0
- pyxcp/recorder/unfolder.hpp +67 -55
- pyxcp/recorder/wrap.cpp +0 -6
- pyxcp/transport/base.py +144 -6
- pyxcp/transport/can.py +59 -6
- pyxcp/transport/eth.py +23 -5
- pyxcp/utils.py +2 -2
- pyxcp-0.23.7.dist-info/METADATA +339 -0
- {pyxcp-0.23.4.dist-info → pyxcp-0.23.7.dist-info}/RECORD +20 -20
- pyxcp-0.23.4.dist-info/METADATA +0 -219
- {pyxcp-0.23.4.dist-info → pyxcp-0.23.7.dist-info}/LICENSE +0 -0
- {pyxcp-0.23.4.dist-info → pyxcp-0.23.7.dist-info}/WHEEL +0 -0
- {pyxcp-0.23.4.dist-info → pyxcp-0.23.7.dist-info}/entry_points.txt +0 -0
pyxcp/recorder/wrap.cpp
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
namespace py = pybind11;
|
|
12
12
|
using namespace pybind11::literals;
|
|
13
13
|
|
|
14
|
-
PYBIND11_MAKE_OPAQUE(ValueHolder);
|
|
15
14
|
|
|
16
15
|
class PyDaqOnlinePolicy : public DaqOnlinePolicy {
|
|
17
16
|
public:
|
|
@@ -181,9 +180,4 @@ PYBIND11_MODULE(rekorder, m) {
|
|
|
181
180
|
.def("get_header", &XcpLogFileDecoder::get_header)
|
|
182
181
|
.def("initialize", &XcpLogFileDecoder::initialize)
|
|
183
182
|
.def("finalize", &XcpLogFileDecoder::finalize);
|
|
184
|
-
|
|
185
|
-
py::class_<ValueHolder>(m, "ValueHolder")
|
|
186
|
-
//.def(py::init<const ValueHolder&>())
|
|
187
|
-
.def(py::init<const std::any&>())
|
|
188
|
-
.def_property_readonly("value", &ValueHolder::get_value);
|
|
189
183
|
}
|
pyxcp/transport/base.py
CHANGED
|
@@ -172,6 +172,13 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
172
172
|
self.pre_send_timestamp: int = self.timestamp.value
|
|
173
173
|
self.post_send_timestamp: int = self.timestamp.value
|
|
174
174
|
self.recv_timestamp: int = self.timestamp.value
|
|
175
|
+
# Ring buffer for last PDUs to aid diagnostics on failures
|
|
176
|
+
try:
|
|
177
|
+
from collections import deque as _dq
|
|
178
|
+
|
|
179
|
+
self._last_pdus = _dq(maxlen=200)
|
|
180
|
+
except Exception:
|
|
181
|
+
self._last_pdus = []
|
|
175
182
|
|
|
176
183
|
def __del__(self) -> None:
|
|
177
184
|
self.finish_listener()
|
|
@@ -239,6 +246,8 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
239
246
|
self.timing.start()
|
|
240
247
|
with self.policy_lock:
|
|
241
248
|
self.policy.feed(types.FrameCategory.CMD, self.counter_send, self.timestamp.value, frame)
|
|
249
|
+
# Record outgoing CMD for diagnostics
|
|
250
|
+
self._record_pdu("out", types.FrameCategory.CMD, self.counter_send, self.timestamp.value, frame)
|
|
242
251
|
self.send(frame)
|
|
243
252
|
try:
|
|
244
253
|
xcpPDU = self.get()
|
|
@@ -247,7 +256,10 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
247
256
|
MSG = f"Response timed out (timeout={self.timeout / 1_000_000_000}s)"
|
|
248
257
|
with self.policy_lock:
|
|
249
258
|
self.policy.feed(types.FrameCategory.METADATA, self.counter_send, self.timestamp.value, bytes(MSG, "ascii"))
|
|
250
|
-
|
|
259
|
+
# Build diagnostics and include in exception
|
|
260
|
+
diag = self._build_diagnostics_dump() if self._diagnostics_enabled() else ""
|
|
261
|
+
self.logger.debug("XCP request timeout", extra={"event": "timeout", "command": cmd.name})
|
|
262
|
+
raise types.XcpTimeoutError(MSG + ("\n" + diag if diag else "")) from None
|
|
251
263
|
else:
|
|
252
264
|
self.timing.stop()
|
|
253
265
|
return
|
|
@@ -303,16 +315,30 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
303
315
|
self.parent._setService(cmd)
|
|
304
316
|
|
|
305
317
|
cmd_len = cmd.bit_length() // 8 # calculate bytes needed for cmd
|
|
306
|
-
|
|
318
|
+
cmd_bytes = cmd.to_bytes(cmd_len, "big")
|
|
319
|
+
try:
|
|
320
|
+
from pyxcp.cpp_ext import accel as _accel
|
|
321
|
+
except Exception:
|
|
322
|
+
_accel = None # type: ignore
|
|
323
|
+
|
|
324
|
+
# Build payload (command + data) using optional accelerator
|
|
325
|
+
if _accel is not None and hasattr(_accel, "build_packet"):
|
|
326
|
+
packet = _accel.build_packet(cmd_bytes, data)
|
|
327
|
+
else:
|
|
328
|
+
packet = bytes(flatten(cmd_bytes, data))
|
|
307
329
|
|
|
308
330
|
header = self.HEADER.pack(len(packet), self.counter_send)
|
|
309
331
|
self.counter_send = (self.counter_send + 1) & 0xFFFF
|
|
310
332
|
|
|
311
333
|
frame = header + packet
|
|
312
334
|
|
|
313
|
-
|
|
314
|
-
if
|
|
315
|
-
frame
|
|
335
|
+
# Align using optional accelerator
|
|
336
|
+
if _accel is not None and hasattr(_accel, "add_alignment"):
|
|
337
|
+
frame = _accel.add_alignment(frame, self.alignment)
|
|
338
|
+
else:
|
|
339
|
+
remainder = len(frame) % self.alignment
|
|
340
|
+
if remainder:
|
|
341
|
+
frame += b"\0" * (self.alignment - remainder)
|
|
316
342
|
|
|
317
343
|
if self._debug:
|
|
318
344
|
self.logger.debug(f"-> {hexDump(frame)}")
|
|
@@ -345,7 +371,14 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
345
371
|
block_response += partial_response[1:]
|
|
346
372
|
else:
|
|
347
373
|
if self.timestamp.value - start > self.timeout:
|
|
348
|
-
|
|
374
|
+
waited = (self.timestamp.value - start) / 1e9 if hasattr(self.timestamp, "value") else None
|
|
375
|
+
msg = f"Response timed out [block_receive]: received {len(block_response)} of {length_required} bytes"
|
|
376
|
+
if waited is not None:
|
|
377
|
+
msg += f" after {waited:.3f}s"
|
|
378
|
+
# Attach diagnostics
|
|
379
|
+
diag = self._build_diagnostics_dump() if self._diagnostics_enabled() else ""
|
|
380
|
+
self.logger.debug("XCP block_receive timeout", extra={"event": "timeout"})
|
|
381
|
+
raise types.XcpTimeoutError(msg + ("\n" + diag if diag else "")) from None
|
|
349
382
|
short_sleep()
|
|
350
383
|
return block_response
|
|
351
384
|
|
|
@@ -382,6 +415,19 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
382
415
|
if self._debug:
|
|
383
416
|
self.logger.debug(f"<- L{length} C{counter} {hexDump(response)}")
|
|
384
417
|
self.counter_received = counter
|
|
418
|
+
# Record incoming non-DAQ frames for diagnostics
|
|
419
|
+
self._record_pdu(
|
|
420
|
+
"in",
|
|
421
|
+
(
|
|
422
|
+
types.FrameCategory.RESPONSE
|
|
423
|
+
if pid >= 0xFE
|
|
424
|
+
else types.FrameCategory.SERV if pid == 0xFC else types.FrameCategory.EVENT
|
|
425
|
+
),
|
|
426
|
+
counter,
|
|
427
|
+
recv_timestamp,
|
|
428
|
+
response,
|
|
429
|
+
length,
|
|
430
|
+
)
|
|
385
431
|
if pid >= 0xFE:
|
|
386
432
|
self.resQueue.append(response)
|
|
387
433
|
with self.policy_lock:
|
|
@@ -412,12 +458,104 @@ class BaseTransport(metaclass=abc.ABCMeta):
|
|
|
412
458
|
timestamp = recv_timestamp
|
|
413
459
|
else:
|
|
414
460
|
timestamp = 0
|
|
461
|
+
# Record DAQ frame (only keep small prefix in payload string later)
|
|
462
|
+
self._record_pdu("in", types.FrameCategory.DAQ, counter, timestamp, response, length)
|
|
415
463
|
# DAQ activity indicates the slave is alive/busy; keep extending the wait window for any
|
|
416
464
|
# outstanding request, similar to EV_CMD_PENDING behavior on stacks that don't emit it.
|
|
417
465
|
self.timer_restart_event.set()
|
|
418
466
|
with self.policy_lock:
|
|
419
467
|
self.policy.feed(types.FrameCategory.DAQ, self.counter_received, timestamp, response)
|
|
420
468
|
|
|
469
|
+
def _record_pdu(
|
|
470
|
+
self,
|
|
471
|
+
direction: str,
|
|
472
|
+
category: types.FrameCategory,
|
|
473
|
+
counter: int,
|
|
474
|
+
timestamp: int,
|
|
475
|
+
payload: bytes,
|
|
476
|
+
length: Optional[int] = None,
|
|
477
|
+
) -> None:
|
|
478
|
+
try:
|
|
479
|
+
entry = {
|
|
480
|
+
"dir": direction,
|
|
481
|
+
"cat": category.name,
|
|
482
|
+
"ctr": int(counter),
|
|
483
|
+
"ts": int(timestamp),
|
|
484
|
+
"len": int(length if length is not None else len(payload)),
|
|
485
|
+
"data": hexDump(payload if category != types.FrameCategory.DAQ else payload[:8])[:512],
|
|
486
|
+
}
|
|
487
|
+
self._last_pdus.append(entry)
|
|
488
|
+
except Exception:
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
def _build_diagnostics_dump(self) -> str:
|
|
492
|
+
import json as _json
|
|
493
|
+
|
|
494
|
+
# transport params
|
|
495
|
+
tp = {"transport": self.__class__.__name__}
|
|
496
|
+
cfg = getattr(self, "config", None)
|
|
497
|
+
# Extract common Eth/Can fields when available
|
|
498
|
+
for key in (
|
|
499
|
+
"host",
|
|
500
|
+
"port",
|
|
501
|
+
"protocol",
|
|
502
|
+
"ipv6",
|
|
503
|
+
"bind_to_address",
|
|
504
|
+
"bind_to_port",
|
|
505
|
+
"fd",
|
|
506
|
+
"bitrate",
|
|
507
|
+
"data_bitrate",
|
|
508
|
+
"can_id_master",
|
|
509
|
+
"can_id_slave",
|
|
510
|
+
):
|
|
511
|
+
if cfg is not None and hasattr(cfg, key):
|
|
512
|
+
try:
|
|
513
|
+
tp[key] = getattr(cfg, key)
|
|
514
|
+
except Exception:
|
|
515
|
+
pass
|
|
516
|
+
# negotiated properties
|
|
517
|
+
negotiated = None
|
|
518
|
+
try:
|
|
519
|
+
master = getattr(self, "parent", None)
|
|
520
|
+
if master is not None and hasattr(master, "slaveProperties"):
|
|
521
|
+
sp = getattr(master, "slaveProperties")
|
|
522
|
+
negotiated = getattr(sp, "__dict__", None) or str(sp)
|
|
523
|
+
except Exception:
|
|
524
|
+
negotiated = None
|
|
525
|
+
# last PDUs
|
|
526
|
+
general = None
|
|
527
|
+
last_n = 20
|
|
528
|
+
try:
|
|
529
|
+
app = getattr(self.config, "parent", None)
|
|
530
|
+
app = getattr(app, "parent", None)
|
|
531
|
+
if app is not None and hasattr(app, "general") and hasattr(app.general, "diagnostics_last_pdus"):
|
|
532
|
+
last_n = int(app.general.diagnostics_last_pdus or last_n)
|
|
533
|
+
general = app.general
|
|
534
|
+
except Exception:
|
|
535
|
+
pass
|
|
536
|
+
pdus = list(self._last_pdus)[-last_n:]
|
|
537
|
+
payload = {
|
|
538
|
+
"transport_params": tp,
|
|
539
|
+
"last_pdus": pdus,
|
|
540
|
+
}
|
|
541
|
+
try:
|
|
542
|
+
body = _json.dumps(payload, ensure_ascii=False, default=str, indent=2)
|
|
543
|
+
except Exception:
|
|
544
|
+
body = str(payload)
|
|
545
|
+
# Add a small header to explain what follows
|
|
546
|
+
header = "--- Diagnostics (for troubleshooting) ---"
|
|
547
|
+
return f"{header}\n{body}"
|
|
548
|
+
|
|
549
|
+
def _diagnostics_enabled(self) -> bool:
|
|
550
|
+
try:
|
|
551
|
+
app = getattr(self.config, "parent", None)
|
|
552
|
+
app = getattr(app, "parent", None)
|
|
553
|
+
if app is not None and hasattr(app, "general"):
|
|
554
|
+
return bool(getattr(app.general, "diagnostics_on_failure", True))
|
|
555
|
+
except Exception:
|
|
556
|
+
return True
|
|
557
|
+
return True
|
|
558
|
+
|
|
421
559
|
# @abc.abstractproperty
|
|
422
560
|
# @property
|
|
423
561
|
# def transport_layer_interface(self) -> Any:
|
pyxcp/transport/can.py
CHANGED
|
@@ -292,7 +292,13 @@ class PythonCanWrapper:
|
|
|
292
292
|
self.timeout: int = timeout
|
|
293
293
|
self.parameters = parameters
|
|
294
294
|
if not self.parent.has_user_supplied_interface:
|
|
295
|
-
|
|
295
|
+
try:
|
|
296
|
+
self.can_interface_class = _get_class_for_interface(self.interface_name)
|
|
297
|
+
except Exception as ex:
|
|
298
|
+
# Provide clearer message if interface not supported by python-can on this platform
|
|
299
|
+
raise CanInitializationError(
|
|
300
|
+
f"Unsupported or unavailable CAN interface {self.interface_name!r}: {ex.__class__.__name__}: {ex}"
|
|
301
|
+
) from ex
|
|
296
302
|
else:
|
|
297
303
|
self.can_interface_class = None
|
|
298
304
|
self.can_interface: BusABC
|
|
@@ -320,7 +326,15 @@ class PythonCanWrapper:
|
|
|
320
326
|
self.can_interface.set_filters(merged_filters)
|
|
321
327
|
self.software_filter.set_filters(can_filters) # Filter unwanted traffic.
|
|
322
328
|
else:
|
|
323
|
-
|
|
329
|
+
try:
|
|
330
|
+
self.can_interface = self.can_interface_class(
|
|
331
|
+
interface=self.interface_name, can_filters=can_filters, **self.parameters
|
|
332
|
+
)
|
|
333
|
+
except OSError as ex:
|
|
334
|
+
# Typical when selecting socketcan on unsupported OS (e.g., Windows)
|
|
335
|
+
raise CanInitializationError(
|
|
336
|
+
f"OS error while creating CAN interface {self.interface_name!r}: {ex.__class__.__name__}: {ex}"
|
|
337
|
+
) from ex
|
|
324
338
|
self.software_filter.accept_all()
|
|
325
339
|
self.parent.logger.info(f"XCPonCAN - Using Interface: '{self.can_interface!s}'")
|
|
326
340
|
self.parent.logger.info(f"XCPonCAN - Filters used: {self.can_interface.filters}")
|
|
@@ -401,13 +415,33 @@ class Can(BaseTransport):
|
|
|
401
415
|
self.padding_value = self.config.padding_value
|
|
402
416
|
if transport_layer_interface is None:
|
|
403
417
|
self.interface_name = self.config.interface
|
|
404
|
-
|
|
418
|
+
# On platforms that do not support certain backends (e.g., SocketCAN on Windows),
|
|
419
|
+
# python-can may raise OSError deep inside interface initialization. We want to
|
|
420
|
+
# fail fast with a clearer hint and avoid unhandled low-level errors.
|
|
421
|
+
try:
|
|
422
|
+
self.interface_configuration = detect_available_configs(interfaces=[self.interface_name])
|
|
423
|
+
except Exception as ex:
|
|
424
|
+
# Best-effort graceful message; keep original exception context
|
|
425
|
+
self.logger.critical(
|
|
426
|
+
f"XCPonCAN - Failed to query available configs for interface {self.interface_name!r}: {ex.__class__.__name__}: {ex}"
|
|
427
|
+
)
|
|
428
|
+
self.interface_configuration = []
|
|
405
429
|
parameters = self.get_interface_parameters()
|
|
406
430
|
else:
|
|
407
431
|
self.interface_name = "custom"
|
|
408
432
|
# print("TRY GET PARAMs", self.get_interface_parameters())
|
|
409
433
|
parameters = {}
|
|
410
|
-
|
|
434
|
+
try:
|
|
435
|
+
self.can_interface = PythonCanWrapper(self, self.interface_name, config.timeout, **parameters)
|
|
436
|
+
except OSError as ex:
|
|
437
|
+
# Catch platform-specific socket errors early (e.g., SocketCAN on Windows)
|
|
438
|
+
msg = (
|
|
439
|
+
f"XCPonCAN - Failed to initialize CAN interface {self.interface_name!r}: "
|
|
440
|
+
f"{ex.__class__.__name__}: {ex}.\n"
|
|
441
|
+
f"Hint: Interface may be unsupported on this OS or missing drivers."
|
|
442
|
+
)
|
|
443
|
+
self.logger.critical(msg)
|
|
444
|
+
raise CanInitializationError(msg) from ex
|
|
411
445
|
self.logger.info(f"XCPonCAN - Interface-Type: {self.interface_name!r} Parameters: {list(parameters.items())}")
|
|
412
446
|
self.logger.info(
|
|
413
447
|
f"XCPonCAN - Master-ID (Tx): 0x{self.can_id_master.id:08X}{self.can_id_master.type_str} -- "
|
|
@@ -459,15 +493,34 @@ class Can(BaseTransport):
|
|
|
459
493
|
self.data_received(frame.data, frame.timestamp)
|
|
460
494
|
|
|
461
495
|
def connect(self):
|
|
462
|
-
|
|
463
|
-
|
|
496
|
+
# Start listener lazily after a successful interface connection to avoid a dangling
|
|
497
|
+
# thread waiting on a not-yet-connected interface if initialization fails.
|
|
464
498
|
try:
|
|
465
499
|
self.can_interface.connect()
|
|
466
500
|
except CanInitializationError:
|
|
501
|
+
# Ensure any previously-started listener is stopped to prevent hangs.
|
|
502
|
+
self.finish_listener()
|
|
467
503
|
console.print("[red]\nThere may be a problem with the configuration of your CAN-interface.\n")
|
|
468
504
|
console.print(f"[grey]Current configuration of interface {self.interface_name!r}:")
|
|
469
505
|
console.print(self.interface_configuration)
|
|
470
506
|
raise
|
|
507
|
+
except OSError as ex:
|
|
508
|
+
# Ensure any previously-started listener is stopped to prevent hangs.
|
|
509
|
+
self.finish_listener()
|
|
510
|
+
# E.g., attempting to instantiate SocketCAN on Windows raises an OSError from socket layer.
|
|
511
|
+
# Provide a clearer, actionable message and keep the original exception.
|
|
512
|
+
msg = (
|
|
513
|
+
f"XCPonCAN - OS error while initializing interface {self.interface_name!r}: "
|
|
514
|
+
f"{ex.__class__.__name__}: {ex}.\n"
|
|
515
|
+
f"Hint: This interface may not be supported on your platform. "
|
|
516
|
+
f"On Windows, use e.g. 'vector', 'kvaser', 'pcan', or other vendor backends instead of 'socketcan'."
|
|
517
|
+
)
|
|
518
|
+
self.logger.critical(msg)
|
|
519
|
+
raise CanInitializationError(msg) from ex
|
|
520
|
+
else:
|
|
521
|
+
# Only now start the default listener if requested.
|
|
522
|
+
if self.useDefaultListener:
|
|
523
|
+
self.start_listener()
|
|
471
524
|
self.status = 1 # connected
|
|
472
525
|
|
|
473
526
|
def send(self, frame: bytes) -> None:
|
pyxcp/transport/eth.py
CHANGED
|
@@ -69,8 +69,8 @@ class Eth(BaseTransport):
|
|
|
69
69
|
self.sockaddr,
|
|
70
70
|
) = addrinfo[0]
|
|
71
71
|
except BaseException as ex: # noqa: B036
|
|
72
|
-
msg = f"XCPonEth - Failed to resolve address {self.host}:{self.port}"
|
|
73
|
-
self.logger.critical(msg)
|
|
72
|
+
msg = f"XCPonEth - Failed to resolve address {self.host}:{self.port} ({self.protocol}, ipv6={self.ipv6}): {ex.__class__.__name__}: {ex}"
|
|
73
|
+
self.logger.critical(msg, extra={"transport": "eth", "host": self.host, "port": self.port, "protocol": self.protocol})
|
|
74
74
|
raise Exception(msg) from ex
|
|
75
75
|
self.status: int = 0
|
|
76
76
|
self.sock = socket.socket(self.address_family, self.socktype, self.proto)
|
|
@@ -87,8 +87,10 @@ class Eth(BaseTransport):
|
|
|
87
87
|
try:
|
|
88
88
|
self.sock.bind(self._local_address)
|
|
89
89
|
except BaseException as ex: # noqa: B036
|
|
90
|
-
msg = f"XCPonEth - Failed to bind socket to given address {self._local_address}"
|
|
91
|
-
self.logger.critical(
|
|
90
|
+
msg = f"XCPonEth - Failed to bind socket to given address {self._local_address}: {ex.__class__.__name__}: {ex}"
|
|
91
|
+
self.logger.critical(
|
|
92
|
+
msg, extra={"transport": "eth", "host": self.host, "port": self.port, "protocol": self.protocol}
|
|
93
|
+
)
|
|
92
94
|
raise Exception(msg) from ex
|
|
93
95
|
self._packet_listener = threading.Thread(
|
|
94
96
|
target=self._packet_listen,
|
|
@@ -194,7 +196,23 @@ class Eth(BaseTransport):
|
|
|
194
196
|
else:
|
|
195
197
|
if current_size >= length:
|
|
196
198
|
response = data[current_position : current_position + length]
|
|
197
|
-
|
|
199
|
+
try:
|
|
200
|
+
process_response(response, length, counter, timestamp)
|
|
201
|
+
except BaseException as ex: # Guard listener against unhandled exceptions (e.g., disk full in policy)
|
|
202
|
+
try:
|
|
203
|
+
self.logger.critical(
|
|
204
|
+
f"Listener error in process_response: {ex.__class__.__name__}: {ex}. Stopping listener.",
|
|
205
|
+
extra={"event": "listener_error"},
|
|
206
|
+
)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
try:
|
|
210
|
+
# Signal all loops to stop
|
|
211
|
+
if hasattr(self, "closeEvent"):
|
|
212
|
+
self.closeEvent.set()
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
return
|
|
198
216
|
current_size -= length
|
|
199
217
|
current_position += length
|
|
200
218
|
length = None
|