witbitz-code 1.5.0__tar.gz → 1.5.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 (94) hide show
  1. {witbitz_code-1.5.0/src/witbitz_code.egg-info → witbitz_code-1.5.1}/PKG-INFO +1 -1
  2. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/pyproject.toml +1 -1
  3. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/__init__.py +1 -1
  4. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/ui.py +64 -6
  5. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/ui_static/app.js +10 -2
  6. {witbitz_code-1.5.0 → witbitz_code-1.5.1/src/witbitz_code.egg-info}/PKG-INFO +1 -1
  7. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_ui.py +51 -0
  8. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/LICENSE +0 -0
  9. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/README.md +0 -0
  10. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/setup.cfg +0 -0
  11. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/__main__.py +0 -0
  12. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/_js.py +0 -0
  13. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/account.py +0 -0
  14. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/asks.py +0 -0
  15. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/attachments.py +0 -0
  16. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/auto.py +0 -0
  17. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/auto_runner.py +0 -0
  18. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/cli.py +0 -0
  19. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/codekeys.py +0 -0
  20. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/codetools.py +0 -0
  21. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/compress.py +0 -0
  22. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/computers.py +0 -0
  23. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/confidential.py +0 -0
  24. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/confidential_models.json +0 -0
  25. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/connector.py +0 -0
  26. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/devicelink.py +0 -0
  27. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/dirs.py +0 -0
  28. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/folders.py +0 -0
  29. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/gateway.py +0 -0
  30. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/keys.py +0 -0
  31. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/link.py +0 -0
  32. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/mkdir.py +0 -0
  33. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/models.py +0 -0
  34. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/net.py +0 -0
  35. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/offered_models.json +0 -0
  36. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  37. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  38. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  39. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  40. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/outputs.py +0 -0
  41. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/pair.py +0 -0
  42. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/pairings.py +0 -0
  43. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/payers.py +0 -0
  44. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/plugins.py +0 -0
  45. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/policy.py +0 -0
  46. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/ports.py +0 -0
  47. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/qr.py +0 -0
  48. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/receipt.py +0 -0
  49. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/relay.py +0 -0
  50. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/seen.py +0 -0
  51. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/tinfoil_read.py +0 -0
  52. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/tools_catalog.json +0 -0
  53. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/tools_probe.py +0 -0
  54. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/toolsetup.py +0 -0
  55. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/ui_static/app.css +0 -0
  56. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/ui_static/index.html +0 -0
  57. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/uninstall.py +0 -0
  58. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/winagent.py +0 -0
  59. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/winconsole.py +0 -0
  60. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/wizard.py +0 -0
  61. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code/wslstart.py +0 -0
  62. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  63. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  64. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  65. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code.egg-info/requires.txt +0 -0
  66. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/src/witbitz_code.egg-info/top_level.txt +0 -0
  67. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_asks.py +0 -0
  68. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_attachments.py +0 -0
  69. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_auto.py +0 -0
  70. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_cli.py +0 -0
  71. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_codekeys.py +0 -0
  72. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_codetools.py +0 -0
  73. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_confidential.py +0 -0
  74. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_connector_e2e.py +0 -0
  75. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_devicelink.py +0 -0
  76. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_dirs.py +0 -0
  77. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_files_and_compress.py +0 -0
  78. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_folders.py +0 -0
  79. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_outputs.py +0 -0
  80. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_pair_flows.py +0 -0
  81. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_plugins.py +0 -0
  82. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_policy.py +0 -0
  83. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_ports.py +0 -0
  84. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_ports_win.py +0 -0
  85. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_registry.py +0 -0
  86. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_relay_codec.py +0 -0
  87. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_setup.py +0 -0
  88. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_tools_probe.py +0 -0
  89. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_toolsetup.py +0 -0
  90. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_uninstall.py +0 -0
  91. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_winagent.py +0 -0
  92. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_winconsole.py +0 -0
  93. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_windows_files.py +0 -0
  94. {witbitz_code-1.5.0 → witbitz_code-1.5.1}/tests/test_wslstart.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "witbitz-code"
7
- version = "1.5.0"
7
+ version = "1.5.1"
8
8
  description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.5.0"
7
+ __version__ = "1.5.1"
@@ -35,7 +35,7 @@ from importlib import resources
35
35
  from pathlib import Path
36
36
  from typing import Any, Awaitable, Callable
37
37
 
38
- from . import __version__, devicelink, link, net, ports, qr, wizard, wslstart
38
+ from . import __version__, devicelink, link, net, ports, qr, uninstall, wizard, wslstart
39
39
  from . import pair as pair_cmd
40
40
  from . import pairings as pp
41
41
 
@@ -91,7 +91,7 @@ class _Tee(io.TextIOBase):
91
91
  while "\n" in self._part:
92
92
  line, self._part = self._part.split("\n", 1)
93
93
  if line.strip() and "\x1b[" not in line:
94
- self.sink.append(line.rstrip()[:300])
94
+ self.sink.append(line.rstrip()[:600])
95
95
  return len(s)
96
96
 
97
97
  def flush(self) -> None:
@@ -122,9 +122,13 @@ class Manager:
122
122
  login_state: Callable[[dict], str] = wslstart.startup_state,
123
123
  login_enable: Callable[[dict], tuple] = wslstart.startup_enable,
124
124
  login_disable: Callable[[dict], bool] = wslstart.startup_disable,
125
+ port_owners: Callable[[int], list] | None = None, stop_pid: Callable[[int], bool] | None = None,
125
126
  label: dict | None = None, log: deque | None = None) -> None:
126
127
  self._serve, self._is_listening, self._pair, self._link = serve, is_listening, pair, link_node
127
128
  self._load, self._keys_line = load_pairings, keys_line
129
+ self._owners = port_owners or ports.port_owners
130
+ self._stop_pid = stop_pid or uninstall.stop_process
131
+ self._elsewhere_cache: tuple[float, dict | None] | None = None
128
132
  self._login_spec, self._login_state, self._login_enable, self._login_disable = login_spec, login_state, login_enable, login_disable
129
133
  self.label = label or computer_label()
130
134
  self.log: deque = log if log is not None else deque(maxlen=60)
@@ -196,15 +200,66 @@ class Manager:
196
200
  for p in ps:
197
201
  paired.append({"name": p.get("name") or "", "account": p.get("account") or "", "port": wizard.port_of(p), "keys": await self.keys(p)})
198
202
  pair = {k: v for k, v in self.pair_state.items() if k != "log"}
199
- return {"version": __version__, "computer": self.label, "paired": paired, "serving": self.serving(), "pair": pair,
203
+ serving = self.serving()
204
+ if serving["state"] == "stopped" and ps:
205
+ el = await self.elsewhere()
206
+ if el:
207
+ serving = {"state": "elsewhere", "port": el["port"], "owners": el["owners"]}
208
+ return {"version": __version__, "computer": self.label, "paired": paired, "serving": serving, "pair": pair,
200
209
  "login": self.login(), "busy": self.busy, "log": list(self.log)[-14:]}
201
210
 
211
+ async def elsewhere(self, fresh: bool = False) -> dict | None:
212
+ """{port, owners} when THIS person's other process holds OpenCode's port or the model proxy's (port + 100) while this
213
+ page serves nothing — `witbitz-code setup` serving in its own window, a login-started copy, an earlier serve (the owner,
214
+ 2026-10-09: the installer's setup was serving and the page said Stopped; its Start then failed "held by python.exe").
215
+ Another user's program, or WSL's copy (Windows cannot name it), is not listed: Start then fails and says why."""
216
+ if self.serve_task and not self.serve_task.done():
217
+ return None
218
+ hit = self._elsewhere_cache
219
+ if hit and not fresh and time.monotonic() - hit[0] < 5:
220
+ return hit[1]
221
+ port, owners = self.port(), []
222
+ for q in (port, port + 100):
223
+ if not await self._is_listening(q):
224
+ continue
225
+ try:
226
+ found = await asyncio.to_thread(self._owners, q)
227
+ except Exception: # noqa: BLE001 — a reading, never a failure of the page
228
+ found = []
229
+ for o in found:
230
+ if all(o["pid"] != x["pid"] for x in owners):
231
+ owners.append({"pid": int(o["pid"]), "cmd": str(o.get("cmd") or "")[:80]})
232
+ out = {"port": port, "owners": owners} if owners else None
233
+ self._elsewhere_cache = (time.monotonic(), out)
234
+ return out
235
+
236
+ async def _stop_elsewhere(self, el: dict) -> list[str]:
237
+ """Stop what elsewhere() named, then wait (≤ 6 s) for both ports to come free. The lines that say what happened."""
238
+ said = []
239
+ for o in el["owners"]:
240
+ ok = await asyncio.to_thread(self._stop_pid, o["pid"])
241
+ said.append(f"{'stopped' if ok else 'could not stop'} {o['cmd'] or 'a program'} (process {o['pid']})")
242
+ self.log.append("witbitz-code: " + said[-1])
243
+ for _ in range(30):
244
+ if not [q for q in (el["port"], el["port"] + 100) if await self._is_listening(q)]:
245
+ break
246
+ await asyncio.sleep(0.2)
247
+ self._elsewhere_cache = None
248
+ return said
249
+
202
250
  # ── serving ────────────────────────────────────────────────────────────────────────────────────────────────────────
203
- async def start_serving(self) -> dict:
251
+ async def start_serving(self, takeover: bool = False) -> dict:
252
+ """Serve from this page. Another copy of witbitz-code already serving here: the page's Start (takeover) stops it
253
+ and runs here instead; the launch's own start leaves it alone (the page then shows where it runs)."""
204
254
  if self.serve_task and not self.serve_task.done():
205
255
  return {"ok": True}
206
256
  if not self.pairings():
207
257
  return {"ok": False, "why": "pair this computer first"}
258
+ el = await self.elsewhere(fresh=True)
259
+ if el and not takeover:
260
+ return {"ok": False, "why": "already running in another window", "elsewhere": el}
261
+ if el:
262
+ await self._stop_elsewhere(el)
208
263
  self.serve_stop, self.serve_exit, self.live = _StopFlag(), None, {}
209
264
  self.serve_port = self.port()
210
265
  self.serve_task = asyncio.ensure_future(self._serve_run(self.serve_port))
@@ -216,7 +271,7 @@ class Manager:
216
271
  except asyncio.CancelledError:
217
272
  raise
218
273
  except Exception as e: # noqa: BLE001
219
- self.serve_exit = {"code": 1, "why": str(e)[:200]}
274
+ self.serve_exit = {"code": 1, "why": str(e)[:500]}
220
275
  return 1
221
276
  last = next((ln for ln in reversed(self.log) if ln.startswith("witbitz-code:")), "")
222
277
  self.serve_exit = {"code": code, "why": "" if code == 0 else last.replace("witbitz-code: ", "", 1)}
@@ -225,6 +280,9 @@ class Manager:
225
280
  async def stop_serving(self) -> dict:
226
281
  t = self.serve_task
227
282
  if t is None or t.done():
283
+ el = await self.elsewhere(fresh=True)
284
+ if el:
285
+ return {"ok": True, "said": await self._stop_elsewhere(el)}
228
286
  return {"ok": True}
229
287
  self.serve_stop.event.set()
230
288
  try:
@@ -455,7 +513,7 @@ class Server:
455
513
  return 400, {"content-type": "application/json"}, b'{"error":"not json"}'
456
514
  data = data if isinstance(data, dict) else {}
457
515
  actions: dict[str, Callable[[], Awaitable[dict]]] = {
458
- "/api/serve": lambda: self.m.start_serving() if data.get("on") else self.m.stop_serving(),
516
+ "/api/serve": lambda: self.m.start_serving(takeover=True) if data.get("on") else self.m.stop_serving(),
459
517
  "/api/pair": self.m.start_pairing,
460
518
  "/api/pair/cancel": self.m.cancel_pairing,
461
519
  "/api/unpair": lambda: self.m.unpair(str(data.get("account") or "")),
@@ -31,6 +31,13 @@ const PILL = {
31
31
  connecting: ['Connecting…', 'connecting', 'reaching the relay'],
32
32
  starting: ['Starting…', 'starting', 'starting OpenCode'],
33
33
  stopped: ['Stopped', '', 'not reachable from Spaces'],
34
+ elsewhere: ['Running in another window', 'online', ''],
35
+ }
36
+
37
+ // Another copy of witbitz-code serves this computer (setup's window, a login-started one): say which, so Start/Stop make sense.
38
+ function elsewhereText(sv) {
39
+ const who = (sv.owners || []).map((o) => `${o.cmd || 'a program'} (process ${o.pid})`).join(', ')
40
+ return `witbitz-code is running in another window${who ? ' — ' + who : ''}. Run it here to manage it from this page, or Stop it.`
34
41
  }
35
42
 
36
43
  function render(s) {
@@ -43,7 +50,7 @@ function render(s) {
43
50
  const pill = $('pill')
44
51
  pill.textContent = label
45
52
  pill.className = 'pill ' + cls
46
- $('stateText').textContent = run === 'stopped' && s.serving.why ? s.serving.why : hint + (s.serving.port && s.serving.port !== 4096 ? ` · port ${s.serving.port}` : '')
53
+ $('stateText').textContent = run === 'elsewhere' ? elsewhereText(s.serving) : run === 'stopped' && s.serving.why ? s.serving.why : hint + (s.serving.port && s.serving.port !== 4096 ? ` · port ${s.serving.port}` : '')
47
54
 
48
55
  const list = $('paired')
49
56
  list.textContent = ''
@@ -58,7 +65,8 @@ function render(s) {
58
65
  const waiting = s.pair.state === 'waiting'
59
66
  const busy = !!s.busy
60
67
  show($('pair'), !paired && !waiting)
61
- show($('start'), paired && run === 'stopped' && !waiting)
68
+ show($('start'), paired && (run === 'stopped' || run === 'elsewhere') && !waiting)
69
+ $('start').textContent = run === 'elsewhere' ? 'Run it here' : 'Start'
62
70
  show($('stop'), paired && run !== 'stopped')
63
71
  show($('pairAgain'), paired && !waiting)
64
72
  show($('unpair'), paired && !waiting)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -192,3 +192,54 @@ def test_the_page_names_which_computer_it_controls(monkeypatch):
192
192
  monkeypatch.setattr(ui.sys, "platform", "linux")
193
193
  monkeypatch.setenv("WSL_DISTRO_NAME", "UbuntuD")
194
194
  assert ui.computer_label() == {"label": "UbuntuD (WSL) on AlonASUS", "kind": "WSL UbuntuD"}
195
+
196
+
197
+ def _other_window_manager(w, held):
198
+ """A manager on a computer where setup's window already serves port 4097 (+ the proxy's 4197), as the owner's laptop did."""
199
+ stopped = []
200
+
201
+ async def is_listening(p):
202
+ return p in held
203
+
204
+ def stop_pid(pid):
205
+ stopped.append(pid)
206
+ held.clear()
207
+ return True
208
+
209
+ m = ui.Manager(serve=w.serve, is_listening=is_listening, pair=w.pair, load_pairings=lambda: list(w.pairings),
210
+ port_owners=lambda q: [{"pid": 19824, "cmd": "python.exe"}] if q in held else [],
211
+ stop_pid=stop_pid, login_spec=lambda: None, label={"label": "AlonASUS", "kind": "Windows"}, log=deque(maxlen=60))
212
+ return m, stopped
213
+
214
+
215
+ def test_a_copy_serving_in_another_window_is_shown_and_run_it_here_takes_over():
216
+ async def go():
217
+ w = World([DESK])
218
+ m, stopped = _other_window_manager(w, {4097, 4197})
219
+ assert (await m.status())["serving"] == {"state": "elsewhere", "port": 4097, "owners": [{"pid": 19824, "cmd": "python.exe"}]}
220
+ r = await m.start_serving() # the launch's own start: leaves setup's window alone, starts nothing
221
+ assert r["ok"] is False and stopped == [] and w.serves == []
222
+ srv, base = await with_server(m)
223
+ try:
224
+ async with httpx.AsyncClient() as c:
225
+ assert (await c.post(f"{base}/api/serve", headers=authed(srv), json={"on": True})).json() == {"ok": True}
226
+ assert stopped == [19824], "Run it here stops the other copy first"
227
+ async def served():
228
+ return w.serves == [["--port", "4097"]]
229
+ await until(served)
230
+ assert (await m.status())["serving"]["state"] == "online"
231
+ finally:
232
+ await m.shutdown()
233
+ await srv.close()
234
+ run(go())
235
+
236
+
237
+ def test_stop_stops_a_copy_running_in_another_window():
238
+ async def go():
239
+ w = World([DESK])
240
+ m, stopped = _other_window_manager(w, {4197})
241
+ r = await m.stop_serving()
242
+ assert r["ok"] is True and stopped == [19824] and "process 19824" in r["said"][0]
243
+ assert (await m.status())["serving"]["state"] == "stopped"
244
+ assert w.serves == []
245
+ run(go())
File without changes
File without changes
File without changes