rosotacom-dev 2.5.dev72__py3-none-any.whl → 2.5.dev74__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/cli.py CHANGED
@@ -794,6 +794,31 @@ def _new_instance_id() -> str:
794
794
  return hashlib.sha1(seed.encode("utf-8")).hexdigest()[:8]
795
795
 
796
796
 
797
+ #: The timestamp every instance directory carries between its session slug and
798
+ #: its id: `<slug>_YYYY-MM-DD_HH-MM-SS_<id>`. Spelled out digit by digit rather
799
+ #: than left as `*`, because a session's name may be a *prefix* of another
800
+ #: session's name and `f"{slug}_*"` then collects the other session's instances
801
+ #: too. `ccng_remote_assist_*` matches every `ccng_remote_assist_kilted_dds_*`,
802
+ #: and because the matches are ordered by name, `[-1]` answers with the wrong
803
+ #: session's run whenever the longer name sorts after a digit — which `k` does.
804
+ #:
805
+ #: That is not hypothetical. On 2026-08-21 it made `rosotacom status
806
+ #: ccng_remote_assist` report a `ccng_remote_assist_kilted_dds` run that had
807
+ #: been shut down 45 minutes earlier, so three two-host runs concluded their
808
+ #: link never came up while it was in fact carrying traffic for eleven minutes.
809
+ #: Nothing in the reading said it came from another session.
810
+ _INSTANCE_STAMP_GLOB = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9][0-9]-[0-9][0-9]-[0-9][0-9]"
811
+
812
+
813
+ def _instance_glob(session_slug: str, instance_id: str | None = None) -> str:
814
+ """The pattern matching this session's instances, and no other session's.
815
+
816
+ With the stamp pinned, the names sort chronologically, so `sorted(...)[-1]`
817
+ is the newest instance rather than merely the last name.
818
+ """
819
+ return f"*/{session_slug}_{_INSTANCE_STAMP_GLOB}_{instance_id or '*'}"
820
+
821
+
797
822
  def _session_instances_root(runtime: RuntimeConfig) -> Path:
798
823
  return (runtime.session_instances_dir or (Path.cwd() / "session-instances")).resolve()
799
824
 
@@ -818,7 +843,7 @@ def _resolve_session_instance(
818
843
  resolved_instance_id = _safe_path_token(instance_id or _new_instance_id())
819
844
 
820
845
  if instance_id:
821
- matches = sorted(root.glob(f"*/{session_slug}_*_{resolved_instance_id}"))
846
+ matches = sorted(root.glob(_instance_glob(session_slug, resolved_instance_id)))
822
847
  if matches:
823
848
  host_dir = matches[-1].resolve()
824
849
  else:
@@ -8781,10 +8806,7 @@ def smoke(args: argparse.Namespace) -> int:
8781
8806
  def _find_latest_instance_dir(runtime: RuntimeConfig, session: ResolvedSession, instance_id: str | None) -> Path:
8782
8807
  root = _session_instances_root(runtime)
8783
8808
  session_slug = _session_instance_slug(session, runtime)
8784
- if instance_id:
8785
- pattern = f"*/{session_slug}_*_{_safe_path_token(instance_id)}"
8786
- else:
8787
- pattern = f"*/{session_slug}_*"
8809
+ pattern = _instance_glob(session_slug, _safe_path_token(instance_id) if instance_id else None)
8788
8810
  matches = sorted(p for p in root.glob(pattern) if p.is_dir())
8789
8811
  if not matches:
8790
8812
  raise RuntimeError(
@@ -5614,22 +5614,40 @@ def _make_live_run_point(args: argparse.Namespace, session_name: str) -> RunPoin
5614
5614
  dist_installs = required_dist_installs(profile_obj)
5615
5615
  if dist_installs:
5616
5616
  # Custom delay-distribution tables must exist inside the
5617
- # shaping netns before netem references them by name.
5617
+ # shaping netns before netem references them by name, and
5618
+ # in the directory this image's `tc` was compiled to read
5619
+ # -- see tc_lib_dir_probe for why that is not a constant.
5620
+ from .network_profiles import tc_lib_dir_probe
5621
+
5618
5622
  for dist_container in (a_container, b_container):
5619
5623
  if not dist_container:
5620
5624
  continue
5625
+ probe = subprocess.run(
5626
+ ["docker", "exec", "-u", "root", dist_container, "sh", "-c", tc_lib_dir_probe()],
5627
+ capture_output=True,
5628
+ text=True,
5629
+ check=False,
5630
+ )
5631
+ tc_lib_dir = probe.stdout.strip()
5632
+ if probe.returncode != 0 or not tc_lib_dir.startswith("/"):
5633
+ raise RuntimeError(
5634
+ f"could not locate the tc library directory in {dist_container}: "
5635
+ f"{probe.stderr or probe.stdout}"
5636
+ )
5621
5637
  for table_name, file_path in dist_installs:
5622
- dist_target = f"/usr/lib/tc/{table_name}.dist"
5623
- for install_cmd in (
5624
- ["docker", "exec", "-u", "root", dist_container, "mkdir", "-p", "/usr/lib/tc"],
5638
+ dist_target = f"{tc_lib_dir}/{table_name}.dist"
5639
+ res = subprocess.run(
5625
5640
  ["docker", "cp", file_path, f"{dist_container}:{dist_target}"],
5626
- ):
5627
- res = subprocess.run(install_cmd, capture_output=True, text=True, check=False)
5628
- if res.returncode != 0:
5629
- raise RuntimeError(
5630
- f"failed to install netem distribution table {table_name!r} in "
5631
- f"{dist_container}: {' '.join(install_cmd)} -> {res.stderr or res.stdout}"
5632
- )
5641
+ capture_output=True,
5642
+ text=True,
5643
+ check=False,
5644
+ )
5645
+ if res.returncode != 0:
5646
+ raise RuntimeError(
5647
+ f"failed to install netem distribution table {table_name!r} in "
5648
+ f"{dist_container}: {file_path} -> {dist_target}: "
5649
+ f"{res.stderr or res.stdout}"
5650
+ )
5633
5651
  print(
5634
5652
  f" netem distribution {table_name!r} installed in {dist_container}: {dist_target}"
5635
5653
  )
@@ -440,9 +440,46 @@ def _resolve_distribution_files(profile: Profile, base: Path) -> Profile:
440
440
  return replace(profile, uplink=fix(profile.uplink), downlink=fix(profile.downlink))
441
441
 
442
442
 
443
+ #: Where `tc` looks for a delay-distribution table, in the order to try.
444
+ #: iproute2 compiles the directory in (``LIBDIR "/tc"``), so on a multiarch
445
+ #: Debian or Ubuntu build it is ``/usr/lib/<triplet>/tc`` and ``/usr/lib/tc``
446
+ #: does not exist at all. A table written to the latter is then invisible with
447
+ #: no error of its own: netem answers ``No distribution data for <name>`` and
448
+ #: the run dies at arming time, which is what every profile carrying a fitted
449
+ #: table did until this was found. The directory holding iproute2's own tables
450
+ #: is the one it reads, so that is the evidence to look for rather than a
451
+ #: guessed triplet.
452
+ TC_LIB_DIR_CANDIDATES = ("/usr/lib/*-linux-gnu*/tc", "/usr/lib/tc")
453
+
454
+ #: A table iproute2 ships, used as the marker for "this is the directory".
455
+ TC_LIB_DIR_MARKERS = ("normal.dist", "pareto.dist")
456
+
457
+
458
+ def tc_lib_dir_probe(root: str = "") -> str:
459
+ """A ``sh -c`` program that prints the directory ``tc`` reads tables from.
460
+
461
+ Three rungs, in order: a candidate that already holds one of iproute2's own
462
+ tables (proof, not inference); a candidate that merely exists; and finally
463
+ ``/usr/lib/tc``, created, for an image that has neither. `root` exists so
464
+ the rungs are testable without a container.
465
+ """
466
+ cands = " ".join(f"{root}{c}" for c in TC_LIB_DIR_CANDIDATES)
467
+ marker = " || ".join(f'[ -f "$d/{m}" ]' for m in TC_LIB_DIR_MARKERS)
468
+ fallback = f"{root}/usr/lib/tc"
469
+ return (
470
+ f"for d in {cands}; do "
471
+ f' if {marker}; then printf %s "$d"; exit 0; fi; '
472
+ f"done; "
473
+ f"for d in {cands}; do "
474
+ f' if [ -d "$d" ]; then printf %s "$d"; exit 0; fi; '
475
+ f"done; "
476
+ f"mkdir -p {fallback} && printf %s {fallback}"
477
+ )
478
+
479
+
443
480
  def required_dist_installs(profile: Profile) -> list[tuple[str, str]]:
444
481
  """``(table_name, file_path)`` pairs a runner must install as
445
- ``/usr/lib/tc/<table_name>.dist`` inside the shaping netns before arming.
482
+ ``<tc lib dir>/<table_name>.dist`` inside the shaping netns before arming.
446
483
 
447
484
  Builtin distributions need nothing; duplicates collapse; conflicting files
448
485
  for one name are an error (two segments must mean the same table).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rosotacom-dev
3
- Version: 2.5.dev72
3
+ Version: 2.5.dev74
4
4
  Summary: Host CLI for ROS OTA communication sessions.
5
5
  Author: develNor
6
6
  License-Expression: BSD-3-Clause
@@ -6,8 +6,8 @@ rosotacom/benched_set.py,sha256=lBA1Rj04dbr2e-lIvYTWoL55_LdUpCAC-y7eIvJZ8lc,2721
6
6
  rosotacom/benchmark.py,sha256=3QoXJtWUGQPwvDWyQT_fOjx6K9G2pmvtnVurA21G2QY,76612
7
7
  rosotacom/benchmark_delta.py,sha256=qXzriBkbXOeJdlLKM4ziov1F-JfwlseYHE48S9hjFsQ,10005
8
8
  rosotacom/bundle_check.py,sha256=l_3BSuYk6juXQmsNU6jkxRbH8ayH2GkiW9ONKMQ1oJY,18270
9
- rosotacom/cli.py,sha256=RAe1VAlQorfvV9nQCeK_YUBr4CFrEacRzgNi3MmaL4s,436666
10
- rosotacom/cli_benchmark.py,sha256=yL5izp7_Y_ngGsx5R8yN2lSUe_Y5qxBYiMArDx4ms5I,355085
9
+ rosotacom/cli.py,sha256=WOl0PLl-tEu28ahHh9HIpu-hmNlD4WcH2b45HzoqMJg,438036
10
+ rosotacom/cli_benchmark.py,sha256=IhxiKMl3C7k7zXJAselrjTjcjKAsWGlc16Z_36AVuc0,356021
11
11
  rosotacom/deployment.py,sha256=XVhB8JjxhAsglSEEPtiVSeV6fCG0rgGmDRulh14oIYQ,7318
12
12
  rosotacom/ffmpeg_packet.py,sha256=zB1BHgY1kexr5sETVMkm4bkVnRytj9k2t0uOYiHUZ6M,4267
13
13
  rosotacom/forensics.py,sha256=fxpjuRq3pA1wCf2ERai4SlVnNhJBqZbDh6ETRKODNd0,53576
@@ -15,7 +15,7 @@ rosotacom/geomap.py,sha256=_85kWI-twzS3rf9KxamnFuhlw8Rp_fRk7n7xI-rUMFc,30292
15
15
  rosotacom/image_cache.py,sha256=71wC291j8rRMgF1D5pt2gTQdplmiIwnUDXk0BZoQNNQ,7185
16
16
  rosotacom/netem_dist.py,sha256=OWtc8asx-ow1wCxDoQuuJwJsFLl7F_lthLWsp-nlK-M,4642
17
17
  rosotacom/network_preflight.py,sha256=mQsjVnHP3CPDfHyLsOdobtymcYhcvJbmJRbXm9EgGDs,5532
18
- rosotacom/network_profiles.py,sha256=GAyVL794zQ3HnGlr1d6GoG5_qDaF5BsXwN9WRGF7kLE,31530
18
+ rosotacom/network_profiles.py,sha256=lUGAAXkJSIwpw7CWGoznKme7b6bC7b73lB56PxgMRAA,33258
19
19
  rosotacom/network_shaper.py,sha256=kjn9ZtiO1SMm8UHKIjmn92JBu_hE5TJF6vaNF1XKtNU,5463
20
20
  rosotacom/plots.py,sha256=YRGRxuGHSHDZmtL0sv1JS7cPVSL9VzjGYuvMxeHAj54,22674
21
21
  rosotacom/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -205,9 +205,9 @@ rosotacom/resources/ws/session/creation/generate_session_files.py,sha256=3Sx4xTS
205
205
  rosotacom/resources/ws/session/creation/run_session.py,sha256=G2hJhrmLp11gm3JZ6_C-zZcqu_reXlE_wmobB7BiCfU,16849
206
206
  rosotacom/resources/ws/session/creation/run_session_in_external_terminal.py,sha256=Ub_5f-ttUUc-rAPrZmg_Op34s8Z4C91hTyEn8fgyt-E,2633
207
207
  rosotacom/resources/ws/session/creation/strip_ansi.py,sha256=vNjpemcq4xHr0VG4x0bZqvgwEBEfOgN-b4PzbrM5nfM,851
208
- rosotacom_dev-2.5.dev72.dist-info/licenses/LICENSE.txt,sha256=FDB3-fao3lqZ94wsJHKAH7U88kAnfvZhZC8Hm5rN__g,1459
209
- rosotacom_dev-2.5.dev72.dist-info/METADATA,sha256=efB62nSlWgH7pSZVKXwAKdBh_dDziHTrPXJtjm8a9uU,58884
210
- rosotacom_dev-2.5.dev72.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
211
- rosotacom_dev-2.5.dev72.dist-info/entry_points.txt,sha256=im11ZWqBbgjQUBz2Ik2RNfMu54wlMwgiVpsBKeI7dCI,49
212
- rosotacom_dev-2.5.dev72.dist-info/top_level.txt,sha256=BZrtQcIVDWn_pQZR3Mlssqy5SaRXHjOa6B-1D9Fy4Ao,10
213
- rosotacom_dev-2.5.dev72.dist-info/RECORD,,
208
+ rosotacom_dev-2.5.dev74.dist-info/licenses/LICENSE.txt,sha256=FDB3-fao3lqZ94wsJHKAH7U88kAnfvZhZC8Hm5rN__g,1459
209
+ rosotacom_dev-2.5.dev74.dist-info/METADATA,sha256=nI3Vclf3YkUxefYLlVuE65wu80J6WBxdf_FStvBe-kw,58884
210
+ rosotacom_dev-2.5.dev74.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
211
+ rosotacom_dev-2.5.dev74.dist-info/entry_points.txt,sha256=im11ZWqBbgjQUBz2Ik2RNfMu54wlMwgiVpsBKeI7dCI,49
212
+ rosotacom_dev-2.5.dev74.dist-info/top_level.txt,sha256=BZrtQcIVDWn_pQZR3Mlssqy5SaRXHjOa6B-1D9Fy4Ao,10
213
+ rosotacom_dev-2.5.dev74.dist-info/RECORD,,