rosotacom-dev 2.2__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.
- rosotacom/__init__.py +32 -0
- rosotacom/__main__.py +8 -0
- rosotacom/anonymize.py +360 -0
- rosotacom/bag_ground_truth.py +395 -0
- rosotacom/benched_set.py +492 -0
- rosotacom/benchmark.py +1842 -0
- rosotacom/bundle_check.py +517 -0
- rosotacom/cli.py +8637 -0
- rosotacom/cli_benchmark.py +8211 -0
- rosotacom/deployment.py +196 -0
- rosotacom/ffmpeg_packet.py +123 -0
- rosotacom/forensics.py +1172 -0
- rosotacom/geomap.py +848 -0
- rosotacom/network_profiles.py +563 -0
- rosotacom/network_shaper.py +127 -0
- rosotacom/plots.py +609 -0
- rosotacom/py.typed +0 -0
- rosotacom/resources/benched-set.yaml +317 -0
- rosotacom/resources/examples/.gitignore +1 -0
- rosotacom/resources/examples/README.md +163 -0
- rosotacom/resources/examples/__init__.py +1 -0
- rosotacom/resources/examples/bags/8_drop_reference/metadata.yaml +17 -0
- rosotacom/resources/examples/deployment.example.yaml +14 -0
- rosotacom/resources/examples/profiles.yaml +85 -0
- rosotacom/resources/examples/ros2docker.json +33 -0
- rosotacom/resources/examples/rosotacom.yaml +12 -0
- rosotacom/resources/examples/scenarios/2_native_chatter/scenario-definition.yaml +10 -0
- rosotacom/resources/examples/scripts/1_heartbeat/run_machine_a.sh +9 -0
- rosotacom/resources/examples/scripts/1_heartbeat/run_machine_b.sh +9 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/external.ros2docker.json +21 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/wait_for_echo_topic.sh +10 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_b/external.ros2docker.json +19 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external.ros2docker.json +12 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external_session.yaml +9 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/foxglove.json +89 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/metadata.yaml +44 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/external.ros2docker.json +13 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external.ros2docker.json +12 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external_session.yaml +9 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/foxglove.json +89 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/metadata.yaml +44 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/external.ros2docker.json +13 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external.ros2docker.json +20 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external.ros2docker.json +20 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external.ros2docker.json +18 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external.ros2docker.json +18 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/sessions/10_restamp/session-definition.yaml +39 -0
- rosotacom/resources/examples/sessions/11_trickle/session-definition.yaml +42 -0
- rosotacom/resources/examples/sessions/12_content_integrity/session-definition.yaml +33 -0
- rosotacom/resources/examples/sessions/13_link_latency/session-definition.yaml +42 -0
- rosotacom/resources/examples/sessions/14_remote_assist_anonymized/session-definition.yaml +211 -0
- rosotacom/resources/examples/sessions/15_remote_assist_anonymized_costmap/session-definition.yaml +59 -0
- rosotacom/resources/examples/sessions/16_remote_assist_anonymized_camera/session-definition.yaml +61 -0
- rosotacom/resources/examples/sessions/17_synthetic_camera_quality/session-definition.yaml +57 -0
- rosotacom/resources/examples/sessions/1_heartbeat/session-definition.yaml +26 -0
- rosotacom/resources/examples/sessions/1_heartbeat_status/session-definition.yaml +37 -0
- rosotacom/resources/examples/sessions/2_native_chatter/session-definition.yaml +34 -0
- rosotacom/resources/examples/sessions/3_comp_occ_grid/session-definition.yaml +43 -0
- rosotacom/resources/examples/sessions/4_comp_occ_grid_zen/session-definition.yaml +43 -0
- rosotacom/resources/examples/sessions/5_sized_payload/session-definition.yaml +39 -0
- rosotacom/resources/examples/sessions/6_sized_payload_zen/session-definition.yaml +50 -0
- rosotacom/resources/examples/sessions/7_latched_static/session-definition.yaml +49 -0
- rosotacom/resources/examples/sessions/8_drop/session-definition.yaml +37 -0
- rosotacom/resources/examples/sessions/9_throttle/session-definition.yaml +37 -0
- rosotacom/resources/examples/sessions/bench_1_1_capacity/session-definition.yaml +42 -0
- rosotacom/resources/examples/sessions/bench_1_2_load_sweep/session-definition.yaml +51 -0
- rosotacom/resources/examples/sessions/bench_1_3_ramp/session-definition.yaml +41 -0
- rosotacom/resources/examples/sessions/bench_1_4_recovery/session-definition.yaml +47 -0
- rosotacom/resources/examples/sessions/bench_1_5_camera/session-definition.yaml +45 -0
- rosotacom/resources/ros2docker.json.example +33 -0
- rosotacom/resources/ws/.vscode/c_cpp_properties.json +20 -0
- rosotacom/resources/ws/.vscode/settings.json +8 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_easy_mode.xml.template +32 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_initial_peer_list.xml.template +21 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v2.xml.template +17 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v3.xml.template +48 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v4.xml.template +95 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v5.xml.template +64 -0
- rosotacom/resources/ws/ota_configs/create_zenoh_json5.py +56 -0
- rosotacom/resources/ws/ota_configs/cyclonedds_minimal.xml.template +9 -0
- rosotacom/resources/ws/ota_configs/cyclonedds_tuned.xml.template +33 -0
- rosotacom/resources/ws/ota_configs/fastdds_unicast.xml.template +71 -0
- rosotacom/resources/ws/ota_configs/get_ota_xml.py +214 -0
- rosotacom/resources/ws/ota_configs/utils.py +74 -0
- rosotacom/resources/ws/ota_configs/zenoh.json5.template +32 -0
- rosotacom/resources/ws/ros2src/.gitkeep +0 -0
- rosotacom/resources/ws/ros2src/com_cpp/CMakeLists.txt +67 -0
- rosotacom/resources/ws/ros2src/com_cpp/COLCON_IGNORE +0 -0
- rosotacom/resources/ws/ros2src/com_cpp/LICENSE.txt +25 -0
- rosotacom/resources/ws/ros2src/com_cpp/package.xml +37 -0
- rosotacom/resources/ws/ros2src/com_cpp/src/hz_monitor.cpp +192 -0
- rosotacom/resources/ws/ros2src/com_cpp/src/universal_compressor.cpp +283 -0
- rosotacom/resources/ws/ros2src/com_cpp/src/universal_decompressor.cpp +306 -0
- rosotacom/resources/ws/ros2src/com_msgs/CMakeLists.txt +22 -0
- rosotacom/resources/ws/ros2src/com_msgs/LICENSE.txt +25 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/CompressedData.msg +3 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/EchoHeartbeat.msg +10 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/OtaStamped.msg +4 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/SizedPayload.msg +4 -0
- rosotacom/resources/ws/ros2src/com_msgs/package.xml +25 -0
- rosotacom/resources/ws/ros2src/com_py/LICENSE.txt +25 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/__init__.py +0 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/base_bridge_relay.py +106 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/bridge_in.py +76 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/bridge_out.py +70 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/heartbeat_echo.py +221 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/hz_monitor.py +178 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/image_bandwidth_reducer.py +218 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/image_pixel_capper.py +370 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/lanelet_saver.py +80 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/latch_relay.py +205 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/costmap_remapper.py +73 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/marker_remapper.py +93 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/speed_test.py +107 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/tf_remapper.py +156 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/traffic_monitor.py +91 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/wifi_network_analysis_1.py +213 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/link_bytes.py +177 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/link_trace.py +186 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/local_global_frame_bridge.py +465 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/localization_map_split_reduce_saver.py +258 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/msg_transform.py +277 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/pair_management.py +87 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/pub_sub_pair.py +145 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/qos.py +254 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/relay_in.py +78 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/relay_out.py +75 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/restamp.py +191 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/sized_publisher.py +244 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency.py +64 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency_core.py +51 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/status_overview.py +476 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/status_overview_core.py +989 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/tf_map_to_cart_saver.py +99 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/topic_monitor.py +327 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/topic_resolution.py +171 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/trickle.py +183 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_compressor.py +653 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_decompressor.py +372 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_unwrapper.py +191 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_wrapper.py +154 -0
- rosotacom/resources/ws/ros2src/com_py/package.xml +34 -0
- rosotacom/resources/ws/ros2src/com_py/resource/com_py +0 -0
- rosotacom/resources/ws/ros2src/com_py/setup.cfg +6 -0
- rosotacom/resources/ws/ros2src/com_py/setup.py +55 -0
- rosotacom/resources/ws/ros2src/com_py/test/test_copyright.py +25 -0
- rosotacom/resources/ws/ros2src/com_py/test/test_flake8.py +25 -0
- rosotacom/resources/ws/ros2src/com_py/test/test_pep257.py +23 -0
- rosotacom/resources/ws/session/content/base/session_plugin_base.yaml +633 -0
- rosotacom/resources/ws/session/content/network/up_connectivity.py +364 -0
- rosotacom/resources/ws/session/content/network/wifi_network_analysis_2.py +215 -0
- rosotacom/resources/ws/session/creation/anonymize_bag.py +156 -0
- rosotacom/resources/ws/session/creation/catmux_log_setup.sh +65 -0
- rosotacom/resources/ws/session/creation/create_session_yaml.py +145 -0
- rosotacom/resources/ws/session/creation/generate_session_files.py +3217 -0
- rosotacom/resources/ws/session/creation/run_session.py +447 -0
- rosotacom/resources/ws/session/creation/run_session_in_external_terminal.py +54 -0
- rosotacom/resources/ws/session/creation/strip_ansi.py +42 -0
- rosotacom/status_eval.py +475 -0
- rosotacom/stream_stats.py +650 -0
- rosotacom/trace_profiles.py +561 -0
- rosotacom/transit.py +249 -0
- rosotacom/video_quality.py +906 -0
- rosotacom_dev-2.2.dist-info/METADATA +874 -0
- rosotacom_dev-2.2.dist-info/RECORD +195 -0
- rosotacom_dev-2.2.dist-info/WHEEL +5 -0
- rosotacom_dev-2.2.dist-info/entry_points.txt +4 -0
- rosotacom_dev-2.2.dist-info/licenses/LICENSE.txt +25 -0
- rosotacom_dev-2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# -- BEGIN LICENSE BLOCK ----------------------------------------------
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
|
6
|
+
#
|
|
7
|
+
# * Redistributions of source code must retain the above copyright
|
|
8
|
+
# notice, this list of conditions and the following disclaimer.
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
11
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
12
|
+
# documentation and/or other materials provided with the distribution.
|
|
13
|
+
#
|
|
14
|
+
# * Neither the name of the {copyright_holder} nor the names of its
|
|
15
|
+
# contributors may be used to endorse or promote products derived from
|
|
16
|
+
# this software without specific prior written permission.
|
|
17
|
+
#
|
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
21
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
22
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
23
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
24
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
25
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
26
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
27
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
28
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
29
|
+
# -- END LICENSE BLOCK ------------------------------------------------
|
|
30
|
+
#
|
|
31
|
+
# ---------------------------------------------------------------------
|
|
32
|
+
# !\file
|
|
33
|
+
#
|
|
34
|
+
# \author Martin Gontscharow <gontscharow@fzi.de>
|
|
35
|
+
# \date 2024-11-13
|
|
36
|
+
#
|
|
37
|
+
#
|
|
38
|
+
# ---------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import copy
|
|
42
|
+
import os
|
|
43
|
+
import re
|
|
44
|
+
import subprocess
|
|
45
|
+
import sys
|
|
46
|
+
from typing import Dict, Optional
|
|
47
|
+
|
|
48
|
+
import yaml
|
|
49
|
+
|
|
50
|
+
ws_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
|
|
51
|
+
sys.path.append(ws_path)
|
|
52
|
+
|
|
53
|
+
from session.creation.create_session_yaml import main as create_session_yaml
|
|
54
|
+
from session.creation import generate_session_files as session_gen
|
|
55
|
+
|
|
56
|
+
_CATMUX_NO_SERVER_RE = re.compile(
|
|
57
|
+
r"^error connecting to /tmp/tmux-\d+/catmux \(No such file or directory\)$"
|
|
58
|
+
)
|
|
59
|
+
# catmux's tmux wrapper swallows failed tmux calls: it prints this marker to
|
|
60
|
+
# stdout and keeps going (exit 0) instead of raising. Treat it as a launch
|
|
61
|
+
# failure so a half-built session surfaces loudly instead of silently.
|
|
62
|
+
_CATMUX_SWALLOWED_ERROR = "Error while calling"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _report_catmux_failure(full_command, result, *, swallowed: bool, failure_log_path) -> None:
|
|
66
|
+
reason = (
|
|
67
|
+
"catmux swallowed a tmux error and left the session incomplete"
|
|
68
|
+
if swallowed and result.returncode == 0
|
|
69
|
+
else f"catmux_create_session exited with status {result.returncode}"
|
|
70
|
+
)
|
|
71
|
+
# Print to stdout: the stderr traceback can be captured/dropped by the layers
|
|
72
|
+
# above (the smoke harness), so keep the cause next to the failure on stdout.
|
|
73
|
+
detail = (result.stderr or "").strip() or (result.stdout or "").strip()
|
|
74
|
+
print("=" * 72)
|
|
75
|
+
print(f"catmux session launch FAILED: {reason}.")
|
|
76
|
+
if detail:
|
|
77
|
+
print("--- catmux output ---")
|
|
78
|
+
print(detail)
|
|
79
|
+
print("--- end catmux output ---")
|
|
80
|
+
if failure_log_path:
|
|
81
|
+
try:
|
|
82
|
+
os.makedirs(os.path.dirname(failure_log_path) or ".", exist_ok=True)
|
|
83
|
+
with open(failure_log_path, "w") as handle:
|
|
84
|
+
handle.write("command: " + " ".join(str(part) for part in full_command) + "\n")
|
|
85
|
+
handle.write(f"returncode: {result.returncode}\n\n")
|
|
86
|
+
handle.write("===== stdout =====\n")
|
|
87
|
+
handle.write(result.stdout or "")
|
|
88
|
+
handle.write("\n===== stderr =====\n")
|
|
89
|
+
handle.write(result.stderr or "")
|
|
90
|
+
print(f"Full catmux launch log: {failure_log_path}")
|
|
91
|
+
except OSError as exc:
|
|
92
|
+
print(f"(could not write catmux launch log to {failure_log_path}: {exc})")
|
|
93
|
+
print("=" * 72)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_catmux(full_command, attach: bool, failure_log_path=None) -> None:
|
|
97
|
+
if attach:
|
|
98
|
+
subprocess.run(full_command, check=True)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
result = subprocess.run(full_command, text=True, capture_output=True)
|
|
102
|
+
if result.stdout:
|
|
103
|
+
print(result.stdout, end="")
|
|
104
|
+
|
|
105
|
+
stderr_lines = [
|
|
106
|
+
line
|
|
107
|
+
for line in (result.stderr or "").splitlines()
|
|
108
|
+
if not _CATMUX_NO_SERVER_RE.match(line.strip())
|
|
109
|
+
]
|
|
110
|
+
if stderr_lines:
|
|
111
|
+
print("\n".join(stderr_lines), file=sys.stderr)
|
|
112
|
+
|
|
113
|
+
swallowed = _CATMUX_SWALLOWED_ERROR in (result.stdout or "")
|
|
114
|
+
if result.returncode != 0 or swallowed:
|
|
115
|
+
_report_catmux_failure(
|
|
116
|
+
full_command, result, swallowed=swallowed, failure_log_path=failure_log_path
|
|
117
|
+
)
|
|
118
|
+
raise subprocess.CalledProcessError(
|
|
119
|
+
result.returncode or 1, full_command, output=result.stdout, stderr=result.stderr
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _parse_peer_address_overrides(overrides) -> Dict[str, str]:
|
|
124
|
+
result: Dict[str, str] = {}
|
|
125
|
+
for override in overrides or []:
|
|
126
|
+
peer_key, sep, address_value = (override or "").partition("=")
|
|
127
|
+
peer_key = peer_key.strip()
|
|
128
|
+
address_value = address_value.strip()
|
|
129
|
+
if sep != "=" or not peer_key or not address_value:
|
|
130
|
+
raise RuntimeError(
|
|
131
|
+
"--peer-address must use '<peer_key>=<address>', "
|
|
132
|
+
"for example 'a=192.168.1.10'."
|
|
133
|
+
)
|
|
134
|
+
if peer_key in result:
|
|
135
|
+
raise RuntimeError(f"Duplicate --peer-address override for peer '{peer_key}'.")
|
|
136
|
+
result[peer_key] = address_value
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _load_session_config_input(session_config_yaml: str) -> Dict:
|
|
141
|
+
param_dir = os.path.dirname(session_config_yaml)
|
|
142
|
+
param = session_gen._load_yaml(session_config_yaml)
|
|
143
|
+
if not isinstance(param, dict):
|
|
144
|
+
raise RuntimeError(
|
|
145
|
+
f"Session config input YAML must be a mapping, got {type(param)} for '{session_config_yaml}'"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if "load_template" in param:
|
|
149
|
+
session_template_fs, provided_params = session_gen._parse_session_config_template_spec(param, param_dir)
|
|
150
|
+
cfg_raw = session_gen._load_yaml(session_template_fs)
|
|
151
|
+
vars_map = session_gen._build_vars_map_from_template(cfg_raw, provided_params)
|
|
152
|
+
cfg = session_gen._substitute(cfg_raw, vars_map) or {}
|
|
153
|
+
else:
|
|
154
|
+
cfg = dict(param)
|
|
155
|
+
|
|
156
|
+
session_gen._validate_session_template_cfg(cfg)
|
|
157
|
+
return cfg
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _apply_link_trace_overrides(
|
|
161
|
+
cfg: Dict,
|
|
162
|
+
*,
|
|
163
|
+
link_trace: Optional[bool] = None,
|
|
164
|
+
link_trace_interval_s: Optional[float] = None,
|
|
165
|
+
link_trace_modem_command: Optional[str] = None,
|
|
166
|
+
) -> Dict:
|
|
167
|
+
if link_trace is None and link_trace_interval_s is None and link_trace_modem_command is None:
|
|
168
|
+
return cfg
|
|
169
|
+
updated = copy.deepcopy(cfg)
|
|
170
|
+
shared = updated.setdefault("shared", {})
|
|
171
|
+
if not isinstance(shared, dict):
|
|
172
|
+
raise RuntimeError("shared must be a mapping before applying link trace overrides.")
|
|
173
|
+
trace_cfg = shared.setdefault("link_trace", {})
|
|
174
|
+
if not isinstance(trace_cfg, dict):
|
|
175
|
+
raise RuntimeError("shared.link_trace must be a mapping before applying link trace overrides.")
|
|
176
|
+
shared["use_status_overview"] = True
|
|
177
|
+
trace_cfg["enabled"] = True if link_trace is None else bool(link_trace)
|
|
178
|
+
if link_trace_interval_s is not None:
|
|
179
|
+
trace_cfg["interval_s"] = float(link_trace_interval_s)
|
|
180
|
+
if link_trace_modem_command is not None:
|
|
181
|
+
trace_cfg["modem_metrics_command"] = link_trace_modem_command
|
|
182
|
+
session_gen._validate_session_template_cfg(updated)
|
|
183
|
+
return updated
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _resolve_peer_dir(
|
|
187
|
+
session_dir: str,
|
|
188
|
+
output_dir: Optional[str],
|
|
189
|
+
identity: Optional[str],
|
|
190
|
+
force: bool,
|
|
191
|
+
rewrite_formatting: bool,
|
|
192
|
+
peer_address=None,
|
|
193
|
+
link_trace: Optional[bool] = None,
|
|
194
|
+
link_trace_interval_s: Optional[float] = None,
|
|
195
|
+
link_trace_modem_command: Optional[str] = None,
|
|
196
|
+
) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Resolve a runnable session directory (the directory containing session_specification.yaml).
|
|
199
|
+
|
|
200
|
+
Supported input:
|
|
201
|
+
- session directory that contains one of:
|
|
202
|
+
- session-definition.yaml (self-contained)
|
|
203
|
+
- session-parametrization.yaml (template + parameters)
|
|
204
|
+
plus: --identity <peer_key>
|
|
205
|
+
"""
|
|
206
|
+
if not os.path.isabs(session_dir):
|
|
207
|
+
for base in (os.environ.get("SESSION_DEFINITIONS_DIR"), os.environ.get("SESSION_CONFIGS_DIR")):
|
|
208
|
+
if not base:
|
|
209
|
+
continue
|
|
210
|
+
candidate = os.path.join(base, session_dir)
|
|
211
|
+
if os.path.isdir(candidate):
|
|
212
|
+
session_dir = candidate
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
p = os.path.abspath(session_dir)
|
|
216
|
+
if not os.path.isdir(p):
|
|
217
|
+
raise RuntimeError(f"--session-dir must be a directory, got: {p}")
|
|
218
|
+
out = os.path.abspath(output_dir) if output_dir else p
|
|
219
|
+
os.makedirs(out, exist_ok=True)
|
|
220
|
+
|
|
221
|
+
if not identity:
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
"Missing --identity. Provide --identity <name of the peer>."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Resolve which session config input file to use.
|
|
227
|
+
#
|
|
228
|
+
# Precedence:
|
|
229
|
+
# - session-parametrization.yaml (if present): treated as the "primary" input even if a generated
|
|
230
|
+
# session-definition.yaml also exists.
|
|
231
|
+
# - session-definition.yaml
|
|
232
|
+
candidates = [
|
|
233
|
+
"session-parametrization.yaml",
|
|
234
|
+
"session-definition.yaml",
|
|
235
|
+
]
|
|
236
|
+
param_yaml = None
|
|
237
|
+
for name in candidates:
|
|
238
|
+
fp = os.path.join(p, name)
|
|
239
|
+
if os.path.exists(fp):
|
|
240
|
+
param_yaml = fp
|
|
241
|
+
break
|
|
242
|
+
if not param_yaml:
|
|
243
|
+
raise RuntimeError(
|
|
244
|
+
"Missing session config input file in session dir. Expected one of: "
|
|
245
|
+
f"{candidates}. Got dir: {p}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
cfg_for_generation = _apply_link_trace_overrides(
|
|
249
|
+
_load_session_config_input(param_yaml),
|
|
250
|
+
link_trace=link_trace,
|
|
251
|
+
link_trace_interval_s=link_trace_interval_s,
|
|
252
|
+
link_trace_modem_command=link_trace_modem_command,
|
|
253
|
+
)
|
|
254
|
+
peer_addresses = _parse_peer_address_overrides(peer_address)
|
|
255
|
+
known_peers = set((cfg_for_generation.get("peers") or {}).keys())
|
|
256
|
+
unknown = sorted(set(peer_addresses) - known_peers)
|
|
257
|
+
if unknown:
|
|
258
|
+
raise RuntimeError(
|
|
259
|
+
f"--peer-address references unknown peer(s) {unknown}. Known peers: {sorted(known_peers)}"
|
|
260
|
+
)
|
|
261
|
+
session_gen.func(
|
|
262
|
+
session_config_yaml=param_yaml,
|
|
263
|
+
force=force,
|
|
264
|
+
rewrite_formatting=rewrite_formatting,
|
|
265
|
+
session_config_obj=cfg_for_generation,
|
|
266
|
+
output_dir=out,
|
|
267
|
+
write_resolved_definition=True,
|
|
268
|
+
peer_addresses=peer_addresses,
|
|
269
|
+
)
|
|
270
|
+
peer_dir = os.path.join(out, identity)
|
|
271
|
+
spec_file = os.path.join(peer_dir, "session_specification.yaml")
|
|
272
|
+
if not os.path.exists(spec_file):
|
|
273
|
+
# Try to help with a quick "what identities exist" hint.
|
|
274
|
+
try:
|
|
275
|
+
subdirs = sorted(d for d in os.listdir(out) if os.path.isdir(os.path.join(out, d)) and not d.startswith("."))
|
|
276
|
+
except Exception:
|
|
277
|
+
subdirs = []
|
|
278
|
+
hint = f" Available subdirs: {subdirs}" if subdirs else ""
|
|
279
|
+
raise RuntimeError(f"Identity '{identity}' did not resolve to a runnable peer dir: missing {spec_file}.{hint}")
|
|
280
|
+
return peer_dir
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main(
|
|
284
|
+
session_dir: str,
|
|
285
|
+
identity: Optional[str] = None,
|
|
286
|
+
output_dir: Optional[str] = None,
|
|
287
|
+
instance_dir: Optional[str] = None,
|
|
288
|
+
catmux_log_dir: Optional[str] = None,
|
|
289
|
+
rosbag_dir: Optional[str] = None,
|
|
290
|
+
force: bool = False,
|
|
291
|
+
rewrite_formatting: bool = False,
|
|
292
|
+
peer_address=None,
|
|
293
|
+
link_trace: Optional[bool] = None,
|
|
294
|
+
link_trace_interval_s: Optional[float] = None,
|
|
295
|
+
link_trace_modem_command: Optional[str] = None,
|
|
296
|
+
attach: Optional[bool] = None,
|
|
297
|
+
):
|
|
298
|
+
|
|
299
|
+
peer_dir = _resolve_peer_dir(
|
|
300
|
+
session_dir,
|
|
301
|
+
output_dir,
|
|
302
|
+
identity,
|
|
303
|
+
force,
|
|
304
|
+
rewrite_formatting,
|
|
305
|
+
peer_address=peer_address,
|
|
306
|
+
link_trace=link_trace,
|
|
307
|
+
link_trace_interval_s=link_trace_interval_s,
|
|
308
|
+
link_trace_modem_command=link_trace_modem_command,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Ensure merged .session_readonly.yaml exists for catmux
|
|
312
|
+
config_dir = output_dir or os.path.dirname(peer_dir)
|
|
313
|
+
create_session_yaml(
|
|
314
|
+
peer_dir,
|
|
315
|
+
instance_dir=instance_dir or os.environ.get("ROSOTACOM_INSTANCE_DIR"),
|
|
316
|
+
config_dir=config_dir,
|
|
317
|
+
catmux_log_dir=catmux_log_dir or os.environ.get("ROSOTACOM_CATMUX_LOG_DIR"),
|
|
318
|
+
rosbag_dir=rosbag_dir or os.environ.get("ROSOTACOM_ROSBAG_DIR"),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Define the command and arguments
|
|
322
|
+
command = "catmux_create_session"
|
|
323
|
+
yaml_file_path = f"{peer_dir}/.session_readonly.yaml"
|
|
324
|
+
session_name_arg = "--session_name"
|
|
325
|
+
|
|
326
|
+
# get name
|
|
327
|
+
spec_file = os.path.join(peer_dir, "session_specification.yaml")
|
|
328
|
+
with open(spec_file, "r") as f:
|
|
329
|
+
spec = yaml.safe_load(f)
|
|
330
|
+
session_name = (spec or {}).get("name", "ros_communication")
|
|
331
|
+
|
|
332
|
+
# Combine them into a single command
|
|
333
|
+
full_command = [
|
|
334
|
+
command,
|
|
335
|
+
yaml_file_path,
|
|
336
|
+
session_name_arg,
|
|
337
|
+
session_name,
|
|
338
|
+
"--overwrite",
|
|
339
|
+
f"dir_path={peer_dir}",
|
|
340
|
+
]
|
|
341
|
+
if attach is None:
|
|
342
|
+
attach = sys.stdin.isatty() and sys.stdout.isatty()
|
|
343
|
+
if not attach:
|
|
344
|
+
full_command.append("--detach")
|
|
345
|
+
|
|
346
|
+
resolved_catmux_log_dir = catmux_log_dir or os.environ.get("ROSOTACOM_CATMUX_LOG_DIR")
|
|
347
|
+
failure_log_path = os.path.join(
|
|
348
|
+
resolved_catmux_log_dir or peer_dir, "catmux-launch-failure.log"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Execute the command
|
|
352
|
+
try:
|
|
353
|
+
_run_catmux(full_command, attach, failure_log_path=failure_log_path)
|
|
354
|
+
print("Command executed successfully.")
|
|
355
|
+
except subprocess.CalledProcessError as e:
|
|
356
|
+
print(
|
|
357
|
+
f"catmux_create_session failed (exit {e.returncode}); see the catmux "
|
|
358
|
+
f"output above and {failure_log_path}.",
|
|
359
|
+
file=sys.stderr,
|
|
360
|
+
)
|
|
361
|
+
raise SystemExit(e.returncode)
|
|
362
|
+
|
|
363
|
+
if __name__ == "__main__":
|
|
364
|
+
parser = argparse.ArgumentParser(
|
|
365
|
+
description="Generate peer session files from a session definition/parametrization and launch catmux for one identity."
|
|
366
|
+
)
|
|
367
|
+
parser.add_argument(
|
|
368
|
+
"-s",
|
|
369
|
+
"--session-dir",
|
|
370
|
+
required=True,
|
|
371
|
+
help="Directory containing session-definition.yaml or session-parametrization.yaml.",
|
|
372
|
+
)
|
|
373
|
+
parser.add_argument(
|
|
374
|
+
"--output-dir",
|
|
375
|
+
help="Directory where generated runtime session files are written.",
|
|
376
|
+
)
|
|
377
|
+
parser.add_argument(
|
|
378
|
+
"--instance-dir",
|
|
379
|
+
help="Runtime session instance directory.",
|
|
380
|
+
)
|
|
381
|
+
parser.add_argument(
|
|
382
|
+
"--catmux-log-dir",
|
|
383
|
+
help="Directory where catmux pane logs for this peer are written.",
|
|
384
|
+
)
|
|
385
|
+
parser.add_argument(
|
|
386
|
+
"--rosbag-dir",
|
|
387
|
+
help="Directory where rosbags for this peer should be written.",
|
|
388
|
+
)
|
|
389
|
+
parser.add_argument(
|
|
390
|
+
"--identity",
|
|
391
|
+
required=True,
|
|
392
|
+
type=str,
|
|
393
|
+
help="Which peer to launch.",
|
|
394
|
+
)
|
|
395
|
+
parser.add_argument(
|
|
396
|
+
"-f",
|
|
397
|
+
"--force",
|
|
398
|
+
action="store_true",
|
|
399
|
+
help="If generating session files: overwrite existing files even if they differ semantically.",
|
|
400
|
+
)
|
|
401
|
+
parser.add_argument(
|
|
402
|
+
"--rewrite-formatting",
|
|
403
|
+
action="store_true",
|
|
404
|
+
help="If generating session files: rewrite files even when semantically equal (format-only differences).",
|
|
405
|
+
)
|
|
406
|
+
parser.add_argument(
|
|
407
|
+
"--peer-address",
|
|
408
|
+
action="append",
|
|
409
|
+
default=[],
|
|
410
|
+
metavar="PEER=ADDRESS",
|
|
411
|
+
help="Resolved peer address. Pass once for every logical peer.",
|
|
412
|
+
)
|
|
413
|
+
parser.add_argument(
|
|
414
|
+
"--link-trace",
|
|
415
|
+
action="store_true",
|
|
416
|
+
default=None,
|
|
417
|
+
help="Enable link_trace.jsonl recording for this generated session instance.",
|
|
418
|
+
)
|
|
419
|
+
parser.add_argument(
|
|
420
|
+
"--link-trace-interval",
|
|
421
|
+
dest="link_trace_interval_s",
|
|
422
|
+
type=float,
|
|
423
|
+
help="Link trace sample interval in seconds; enables link tracing.",
|
|
424
|
+
)
|
|
425
|
+
parser.add_argument(
|
|
426
|
+
"--link-trace-modem-command",
|
|
427
|
+
dest="link_trace_modem_command",
|
|
428
|
+
help="Shell command returning a JSON object to merge under each trace row's modem block.",
|
|
429
|
+
)
|
|
430
|
+
attach_group = parser.add_mutually_exclusive_group()
|
|
431
|
+
attach_group.add_argument(
|
|
432
|
+
"--attach",
|
|
433
|
+
dest="attach",
|
|
434
|
+
action="store_true",
|
|
435
|
+
help="Attach to the catmux/tmux session after creating it.",
|
|
436
|
+
)
|
|
437
|
+
attach_group.add_argument(
|
|
438
|
+
"--detach",
|
|
439
|
+
dest="attach",
|
|
440
|
+
action="store_false",
|
|
441
|
+
help="Create the catmux/tmux session and return without attaching.",
|
|
442
|
+
)
|
|
443
|
+
parser.set_defaults(attach=None)
|
|
444
|
+
args = parser.parse_args()
|
|
445
|
+
|
|
446
|
+
# Use **vars(args) to convert argparse.Namespace to a dict, filtering out None values
|
|
447
|
+
main(**{k: v for k, v in vars(args).items() if v is not None})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# -- BEGIN LICENSE BLOCK ----------------------------------------------
|
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
|
6
|
+
#
|
|
7
|
+
# * Redistributions of source code must retain the above copyright
|
|
8
|
+
# notice, this list of conditions and the following disclaimer.
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
11
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
12
|
+
# documentation and/or other materials provided with the distribution.
|
|
13
|
+
#
|
|
14
|
+
# * Neither the name of the {copyright_holder} nor the names of its
|
|
15
|
+
# contributors may be used to endorse or promote products derived from
|
|
16
|
+
# this software without specific prior written permission.
|
|
17
|
+
#
|
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
21
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
22
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
23
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
24
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
25
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
26
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
27
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
28
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
29
|
+
# -- END LICENSE BLOCK ------------------------------------------------
|
|
30
|
+
#
|
|
31
|
+
# ---------------------------------------------------------------------
|
|
32
|
+
# !\file
|
|
33
|
+
#
|
|
34
|
+
# \author Martin Gontscharow <gontscharow@fzi.de>
|
|
35
|
+
# \date 2024-11-13
|
|
36
|
+
#
|
|
37
|
+
#
|
|
38
|
+
# ---------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import subprocess
|
|
42
|
+
|
|
43
|
+
def main(session_dir):
|
|
44
|
+
print("External Terminal requested. Restarting script inside new gnome-terminal...")
|
|
45
|
+
gnome_terminal_command = f'gnome-terminal -- bash -c "/ws/session_creation/run_session.py --session-dir {session_dir}"'
|
|
46
|
+
print(f"Executing in gnome-terminal with command: {gnome_terminal_command}")
|
|
47
|
+
subprocess.run(gnome_terminal_command, shell=True, check=True)
|
|
48
|
+
print("Script execution in external gnome-terminal initiated.")
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
parser = argparse.ArgumentParser(description=main.__doc__)
|
|
52
|
+
parser.add_argument("-s", "--session-dir")
|
|
53
|
+
args = parser.parse_args()
|
|
54
|
+
main(**{k: v for k, v in vars(args).items() if v is not None})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Strip ANSI/VT100 control sequences from stdin to stdout."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_ANSI_PATTERN = re.compile(
|
|
11
|
+
rb"\x1b\[[0-9;?]*[ -/]*[@-~]"
|
|
12
|
+
rb"|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)"
|
|
13
|
+
rb"|\x1b[@-Z\\-_]"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _strip(chunk: bytes) -> bytes:
|
|
18
|
+
return _ANSI_PATTERN.sub(b"", chunk)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
src = sys.stdin.buffer
|
|
23
|
+
dst = sys.stdout.buffer
|
|
24
|
+
pending = b""
|
|
25
|
+
while True:
|
|
26
|
+
chunk = src.read1(4096)
|
|
27
|
+
if not chunk:
|
|
28
|
+
break
|
|
29
|
+
pending += chunk
|
|
30
|
+
cut = pending.rfind(b"\n")
|
|
31
|
+
if cut < 0:
|
|
32
|
+
continue
|
|
33
|
+
dst.write(_strip(pending[: cut + 1]))
|
|
34
|
+
dst.flush()
|
|
35
|
+
pending = pending[cut + 1:]
|
|
36
|
+
if pending:
|
|
37
|
+
dst.write(_strip(pending))
|
|
38
|
+
dst.flush()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|