caffeinated-whale-cli 3.2.0__tar.gz → 3.2.1__tar.gz

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.
Files changed (100) hide show
  1. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/PKG-INFO +1 -1
  2. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/pyproject.toml +1 -1
  3. caffeinated_whale_cli-3.2.1/src/caffeinated_whale_cli/__init__.py +1 -0
  4. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/supervision.py +110 -9
  5. caffeinated_whale_cli-3.2.0/src/caffeinated_whale_cli/__init__.py +0 -1
  6. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/LICENSE +0 -0
  7. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/README.md +0 -0
  8. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/setup.cfg +0 -0
  9. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/__main__.py +0 -0
  10. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/__init__.py +0 -0
  11. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/apps.py +0 -0
  12. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/axi.py +0 -0
  13. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/backup.py +0 -0
  14. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/config.py +0 -0
  15. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/console.html +0 -0
  16. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/doctor.py +0 -0
  17. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/init.py +0 -0
  18. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/inspect.py +0 -0
  19. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/label.py +0 -0
  20. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/list.py +0 -0
  21. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/logs.py +0 -0
  22. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/open.py +0 -0
  23. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/restart.py +0 -0
  24. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/restore.py +0 -0
  25. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/rm.py +0 -0
  26. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/rm_bench.py +0 -0
  27. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/rm_site.py +0 -0
  28. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/run.py +0 -0
  29. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/scale.py +0 -0
  30. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/self_update.py +0 -0
  31. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/serve.py +0 -0
  32. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/setup.py +0 -0
  33. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/start.py +0 -0
  34. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/status.py +0 -0
  35. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/stop.py +0 -0
  36. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/unlock.py +0 -0
  37. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/update.py +0 -0
  38. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/utils.py +0 -0
  39. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/commands/where.py +0 -0
  40. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/__init__.py +0 -0
  41. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/apps.py +0 -0
  42. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/auto_inspect.py +0 -0
  43. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/backup.py +0 -0
  44. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/bench_ops.py +0 -0
  45. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/config.py +0 -0
  46. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/cred_bridge.py +0 -0
  47. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/credbridge.py +0 -0
  48. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/docker.py +0 -0
  49. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/doctor.py +0 -0
  50. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/envelope.py +0 -0
  51. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/errors.py +0 -0
  52. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/exec_stream.py +0 -0
  53. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/fleet.py +0 -0
  54. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/init.py +0 -0
  55. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/inspect.py +0 -0
  56. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/label.py +0 -0
  57. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/list.py +0 -0
  58. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/logs.py +0 -0
  59. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/open.py +0 -0
  60. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/resolvers.py +0 -0
  61. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/restart.py +0 -0
  62. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/restore.py +0 -0
  63. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/rm.py +0 -0
  64. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/rm_bench.py +0 -0
  65. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/rm_site.py +0 -0
  66. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/run.py +0 -0
  67. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/scale.py +0 -0
  68. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/setup.py +0 -0
  69. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/start.py +0 -0
  70. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/status.py +0 -0
  71. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/stop.py +0 -0
  72. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/unlock.py +0 -0
  73. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/update.py +0 -0
  74. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/url.py +0 -0
  75. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/version.py +0 -0
  76. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/core/where.py +0 -0
  77. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/main.py +0 -0
  78. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/update_notice.py +0 -0
  79. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/__init__.py +0 -0
  80. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/agent_hooks.py +0 -0
  81. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/auto_inspect.py +0 -0
  82. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/bench_labels.py +0 -0
  83. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/bench_sites.py +0 -0
  84. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/cache.py +0 -0
  85. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/completion_utils.py +0 -0
  86. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/config_utils.py +0 -0
  87. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/console.py +0 -0
  88. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/cred_daemon.py +0 -0
  89. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/daemon_identity.py +0 -0
  90. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/db_utils.py +0 -0
  91. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/docker_utils.py +0 -0
  92. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/port_utils.py +0 -0
  93. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/sendme_utils.py +0 -0
  94. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/shared_home.py +0 -0
  95. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/signals.py +0 -0
  96. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/startup.py +0 -0
  97. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/tips.py +0 -0
  98. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/toon.py +0 -0
  99. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli/utils/vscode_utils.py +0 -0
  100. {caffeinated_whale_cli-3.2.0 → caffeinated_whale_cli-3.2.1}/src/caffeinated_whale_cli.egg-info/SOURCES.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: caffeinated-whale-cli
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: A CLI tool to help manage Frappe Docker instances.
5
5
  Author-email: Christopher McKay <mckay.christopher73@outlook.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "caffeinated-whale-cli"
7
- version = "3.2.0"
7
+ version = "3.2.1"
8
8
  authors = [
9
9
  { name = "Christopher McKay", email = "mckay.christopher73@outlook.com" },
10
10
  ]
@@ -0,0 +1 @@
1
+ __version__ = "3.2.1"
@@ -51,6 +51,7 @@ never returned across a boundary; every return is plain serializable data.
51
51
  from __future__ import annotations
52
52
 
53
53
  import json
54
+ import re
54
55
  import shlex
55
56
  import time
56
57
  from collections.abc import Callable
@@ -1230,6 +1231,89 @@ def _resync_failed(text: str, restarted: list[str]) -> ResyncOutcome:
1230
1231
  return ResyncOutcome(attempted=True, restarted=list(restarted), unserved_sites=[], error=text)
1231
1232
 
1232
1233
 
1234
+ # A bench's ``common_site_config.json`` MAY legitimately omit ``webserver_port`` -
1235
+ # a DNS-multitenant bench (many domain-named sites) removes it deliberately so
1236
+ # Frappe routes purely by Host header, not by a port cwcli would otherwise read as
1237
+ # a fixed default. `resolve_assigned_ports(fill_defaults=False)` reports that the
1238
+ # same way it reports a genuinely unreadable config: absent. Below, "the config has
1239
+ # no port" is answered with EVIDENCE instead - the live web process's own bound
1240
+ # port, from its argv - never a bare guess.
1241
+ _PORT_FLAG_RE = re.compile(r"--port[=\s]+(\d+)")
1242
+ _BIND_FLAG_RE = re.compile(r"(?:--bind|-b)[=\s]+\S*:(\d+)")
1243
+ _LISTENER_OWNERSHIP_SCRIPT = r"""
1244
+ import os
1245
+ import sys
1246
+
1247
+ pid = int(sys.argv[1])
1248
+ port = int(sys.argv[2])
1249
+ bench = os.path.realpath(sys.argv[3])
1250
+ try:
1251
+ cwd = os.path.realpath(f"/proc/{pid}/cwd")
1252
+ if os.path.commonpath((cwd, bench)) != bench:
1253
+ raise OSError
1254
+ inodes = set()
1255
+ for fd in os.listdir(f"/proc/{pid}/fd"):
1256
+ try:
1257
+ target = os.readlink(f"/proc/{pid}/fd/{fd}")
1258
+ except OSError:
1259
+ continue
1260
+ if target.startswith("socket:[") and target.endswith("]"):
1261
+ inodes.add(target[8:-1])
1262
+ for table in ("/proc/net/tcp", "/proc/net/tcp6"):
1263
+ with open(table, encoding="ascii") as rows:
1264
+ next(rows)
1265
+ for row in rows:
1266
+ fields = row.split()
1267
+ local_port = int(fields[1].rsplit(":", 1)[1], 16)
1268
+ if fields[3] == "0A" and local_port == port and fields[9] in inodes:
1269
+ raise SystemExit(0)
1270
+ except (OSError, ValueError):
1271
+ raise SystemExit(2)
1272
+ raise SystemExit(1)
1273
+ """.strip()
1274
+
1275
+
1276
+ def _port_from_process(container, pid: int) -> int | None:
1277
+ """The bound port of ``pid``, read from its own argv - evidence, not a guess.
1278
+
1279
+ bench bakes the bench's real assigned port into the Procfile's ``web:`` line at
1280
+ init (``bench serve --port <N>``), so once that line is running its cmdline IS
1281
+ the truth; a gunicorn-fronted web instead carries ``--bind``/``-b <host>:<N>``.
1282
+ Returns None when the process is gone or its argv names no port at all (a bare
1283
+ ``bench serve`` with no ``--port``, which defers to Frappe's own default).
1284
+ """
1285
+ for row in _ps_rows(container):
1286
+ if row.pid != pid:
1287
+ continue
1288
+ match = _PORT_FLAG_RE.search(row.args) or _BIND_FLAG_RE.search(row.args)
1289
+ return int(match.group(1)) if match else None
1290
+ return None
1291
+
1292
+
1293
+ def _process_owns_listener(container, pid: int, port: int, bench_path: str) -> bool:
1294
+ """Return whether this bench's process owns the listener on ``port``."""
1295
+ exit_code, _output = container.exec_run(
1296
+ ["python3", "-c", _LISTENER_OWNERSHIP_SCRIPT, str(pid), str(port), bench_path]
1297
+ )
1298
+ return bool(exit_code == 0)
1299
+
1300
+
1301
+ def _resync_web_port(container, bench_path: str, web_pid: int | None) -> int | None:
1302
+ ports = resolvers.resolve_assigned_ports(container, [bench_path], fill_defaults=False).get(
1303
+ bench_path
1304
+ )
1305
+ if ports is not None:
1306
+ return ports[0]
1307
+ process_port = _port_from_process(container, web_pid) if web_pid is not None else None
1308
+ if process_port is not None:
1309
+ return process_port
1310
+ if web_pid is not None and _process_owns_listener(
1311
+ container, web_pid, resolvers.WEB_CONTAINER_BASE, bench_path
1312
+ ):
1313
+ return resolvers.WEB_CONTAINER_BASE
1314
+ return None
1315
+
1316
+
1233
1317
  def resync_after_code_change(
1234
1318
  container,
1235
1319
  bench_path: str,
@@ -1280,6 +1364,18 @@ def resync_after_code_change(
1280
1364
  bound-port-shaped claim, and serving stale code is exactly the class of defect a
1281
1365
  claim like that misses. Every site the caller names must answer a real
1282
1366
  site-routed request before the outcome is ``ok``.
1367
+ - **An absent ``webserver_port`` is not a read failure.** A DNS-multitenant
1368
+ bench (many domain-named sites) deliberately removes it from
1369
+ ``common_site_config.json`` so Frappe routes purely by Host header; a bench
1370
+ like that used to fail here with "the bench's assigned port could not be
1371
+ read", even though the update itself had already succeeded. The probe port
1372
+ now falls back to EVIDENCE - the live web process's own bound port (its argv
1373
+ carries bench's real assignment, baked into the Procfile's ``web:`` line at
1374
+ init), then Frappe's own unconfigured default only when that listener is
1375
+ proven to belong to this bench's web process - before the sites are probed
1376
+ for real. Missing evidence fails honestly without probing a sibling bench or
1377
+ blaming a config key that was never supposed to exist.
1378
+ See :func:`_port_from_process`.
1283
1379
  """
1284
1380
  announce = on_restart or (lambda _program: None)
1285
1381
  unique_sites = list(dict.fromkeys(sites))
@@ -1290,6 +1386,7 @@ def resync_after_code_change(
1290
1386
 
1291
1387
  restarted: list[str] = []
1292
1388
  supervised = False
1389
+ web_port: int | None
1293
1390
  try:
1294
1391
  supervised = discover_stack(container, bench_path, required=True).supervisor_up
1295
1392
  if supervised:
@@ -1304,6 +1401,7 @@ def resync_after_code_change(
1304
1401
  # is no site to verify against, and starting one is not this verb's job.
1305
1402
  if states[web][0] not in _LIVE_STATES:
1306
1403
  return quiet
1404
+ web_port = _resync_web_port(container, bench_path, states[web][1])
1307
1405
 
1308
1406
  for program in code_bearing_programs(list(states)):
1309
1407
  if states[program][0] not in _LIVE_STATES:
@@ -1320,21 +1418,24 @@ def resync_after_code_change(
1320
1418
  restarted,
1321
1419
  )
1322
1420
  restarted.append(program)
1323
- elif not discover_unsupervised_stack(container, bench_path, required=True).manager_up:
1324
- return quiet
1421
+ else:
1422
+ unsupervised = discover_unsupervised_stack(container, bench_path, required=True)
1423
+ if not unsupervised.manager_up:
1424
+ return quiet
1425
+ web_process = next((p for p in unsupervised.processes if p.label == "web"), None)
1426
+ web_port = _resync_web_port(
1427
+ container, bench_path, web_process.pid if web_process is not None else None
1428
+ )
1325
1429
 
1326
- ports = resolvers.resolve_assigned_ports(container, [bench_path], fill_defaults=False).get(
1327
- bench_path
1328
- )
1329
- if ports is None:
1430
+ if web_port is None:
1330
1431
  return _resync_failed(
1331
- "the bench's assigned port could not be read, so cwcli could not confirm "
1332
- "the affected sites serve",
1432
+ "the bench's serving port could not be determined safely, so cwcli could not "
1433
+ "confirm the affected sites serve",
1333
1434
  restarted,
1334
1435
  )
1335
1436
 
1336
1437
  pending, codes = _wait_for_serving_sites(
1337
- container, port=ports[0], sites=unique_sites, timeout=timeout
1438
+ container, port=web_port, sites=unique_sites, timeout=timeout
1338
1439
  )
1339
1440
  if pending:
1340
1441
  observed = ", ".join(f"{site}={codes[site] or 'unreachable'}" for site in pending)
@@ -1 +0,0 @@
1
- __version__ = "3.2.0"