witbitz-code 1.8.0__tar.gz → 1.9.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.
- {witbitz_code-1.8.0/src/witbitz_code.egg-info → witbitz_code-1.9.1}/PKG-INFO +1 -1
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/pyproject.toml +1 -1
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/browse.py +47 -5
- witbitz_code-1.9.1/src/witbitz_code/browsetree.py +117 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/opencode_plugins/witbitz-browse-publisher.mjs +75 -12
- witbitz_code-1.9.1/src/witbitz_code/opencode_plugins/witbitz-browse-tree.mjs +303 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/ui.py +6 -3
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/ui_static/app.css +1 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/ui_static/app.js +23 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/ui_static/index.html +9 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1/src/witbitz_code.egg-info}/PKG-INFO +1 -1
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code.egg-info/SOURCES.txt +3 -0
- witbitz_code-1.9.1/tests/test_browsetree.py +131 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/LICENSE +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/README.md +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/setup.cfg +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/cli.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/confidential.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/connector.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/images.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/opencode_plugins/witbitz-browse-cdp.mjs +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/opencode_plugins/witbitz-browse-local.mjs +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/opencode_plugins/witbitz-browser.js +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/remind.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/seccheck.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/selfupdate.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/uninstall.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code.egg-info/requires.txt +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_asks.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_attachments.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_auto.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_browse.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_cli.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_codetools.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_confidential.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_dirs.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_folders.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_images.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_outputs.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_plugins.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_policy.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_ports.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_registry.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_remind.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_seccheck.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_selfupdate.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_setup.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_ui.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_uitools.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_uninstall.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_winagent.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_winbg.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_windows_files.py +0 -0
- {witbitz_code-1.8.0 → witbitz_code-1.9.1}/tests/test_wslstart.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.9.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"
|
|
@@ -5,11 +5,16 @@ ON by default; `serve --no-browser` or WB_BROWSE_DISABLE=1 turns it off (and rem
|
|
|
5
5
|
programs are JavaScript (the cloud computer's own: browse-local, the agent's CLI, the publisher), so they run with this
|
|
6
6
|
computer's `node` — as the media tools do; without node the agent works without a browser. Each pairing gets a viewer key
|
|
7
7
|
derived from its secret (relay.browse_key = codeRelay.js deriveBrowseKey), so the app can Watch and Take over.
|
|
8
|
+
|
|
9
|
+
On Linux with the CLOUD COMPUTER'S browser installed (browsetree.py — the page's card, or `witbitz-browse-tree.mjs install`)
|
|
10
|
+
it is that one instead: the image's own Chromium, KasmVNC, PulseAudio and publisher under bwrap, sealed to the FIRST
|
|
11
|
+
pairing's key (the lab build seals to one). WB_BROWSE_TREE=0 keeps the laptop browser.
|
|
8
12
|
"""
|
|
9
13
|
|
|
10
14
|
from __future__ import annotations
|
|
11
15
|
|
|
12
16
|
import asyncio
|
|
17
|
+
import json
|
|
13
18
|
import os
|
|
14
19
|
import re
|
|
15
20
|
import shutil
|
|
@@ -20,8 +25,8 @@ from typing import Any, Callable
|
|
|
20
25
|
|
|
21
26
|
import httpx
|
|
22
27
|
|
|
23
|
-
from . import plugins
|
|
24
|
-
from .relay import browse_key
|
|
28
|
+
from . import browsetree, plugins
|
|
29
|
+
from .relay import RELAY_URL, browse_key
|
|
25
30
|
|
|
26
31
|
BROWSE_DIR = "witbitz-browse"
|
|
27
32
|
PLUGIN_REL = "plugins/witbitz-browser.js"
|
|
@@ -80,13 +85,16 @@ def _free_port() -> int:
|
|
|
80
85
|
|
|
81
86
|
|
|
82
87
|
class LocalBrowser:
|
|
83
|
-
def __init__(self, proc: asyncio.subprocess.Process, ctl: int, env: dict, http: httpx.AsyncClient) -> None:
|
|
84
|
-
self.proc, self.ctl, self.env, self._http = proc, ctl, env, http
|
|
88
|
+
def __init__(self, proc: asyncio.subprocess.Process, ctl: int, env: dict, http: httpx.AsyncClient, tree: str = "") -> None:
|
|
89
|
+
self.proc, self.ctl, self.env, self._http, self.tree = proc, ctl, env, http, tree
|
|
85
90
|
self.last: dict | None = None
|
|
86
91
|
self._task: asyncio.Task | None = None
|
|
87
92
|
|
|
88
93
|
def probe(self) -> dict | None:
|
|
89
|
-
"""The browser's last /status — synchronous: the connector's hello must never wait on it.
|
|
94
|
+
"""The browser's last /status — synchronous: the connector's hello must never wait on it. The cloud computer's
|
|
95
|
+
browser answers as in the enclave; `local`/`cast` tell the app to watch it as this computer's, `tree` which image."""
|
|
96
|
+
if self.last is not None and self.tree:
|
|
97
|
+
return {**self.last, "local": True, "cast": True, "tree": self.tree}
|
|
90
98
|
return self.last
|
|
91
99
|
|
|
92
100
|
async def _poll(self) -> None:
|
|
@@ -124,6 +132,10 @@ async def start(pairings: list[dict], *, config_dir: Path | None = None, log: Ca
|
|
|
124
132
|
log("witbitz-code: no node on this computer — the agent works without a browser (install Node.js 22+ to add one)")
|
|
125
133
|
return None
|
|
126
134
|
paths = await asyncio.to_thread(install, config_dir)
|
|
135
|
+
if browsetree.use_tree():
|
|
136
|
+
b = await _start_tree(node, paths, pairings, config_dir, log)
|
|
137
|
+
if b:
|
|
138
|
+
return b
|
|
127
139
|
watchable = node_major(node) >= 22 # the publisher needs a global WebSocket
|
|
128
140
|
keys = [browse_key(p["secret"]) for p in pairings if p.get("secret")] if watchable else []
|
|
129
141
|
ctl, gate = _free_port(), _free_port()
|
|
@@ -143,3 +155,33 @@ async def start(pairings: list[dict], *, config_dir: Path | None = None, log: Ca
|
|
|
143
155
|
log("witbitz-code: the agent's browser is ready on this computer"
|
|
144
156
|
+ (" — watch it from the Code section" if watchable else " (node is older than 22: the app cannot watch it)") + "; off: serve --no-browser")
|
|
145
157
|
return b
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def _start_tree(node: str, paths: dict, pairings: list[dict], config_dir: Path | None, log: Callable[[str], Any]) -> LocalBrowser | None:
|
|
161
|
+
"""The cloud computer's browser when it is installed here; None → the laptop browser."""
|
|
162
|
+
key = next((browse_key(p["secret"]) for p in pairings if p.get("secret")), "")
|
|
163
|
+
tree = await asyncio.to_thread(browsetree.script, config_dir)
|
|
164
|
+
st = await asyncio.to_thread(browsetree.read_status, node, tree)
|
|
165
|
+
if not key or not st.get("supported") or not st.get("installed"):
|
|
166
|
+
return None
|
|
167
|
+
proc = await asyncio.create_subprocess_exec(node, str(tree), "run", "--relay", RELAY_URL, "--parent", str(os.getpid()),
|
|
168
|
+
env={**os.environ, "WB_BROWSE_KEY": key}, stdin=asyncio.subprocess.DEVNULL,
|
|
169
|
+
stdout=asyncio.subprocess.PIPE)
|
|
170
|
+
try:
|
|
171
|
+
assert proc.stdout
|
|
172
|
+
ready = json.loads(await asyncio.wait_for(proc.stdout.readline(), 40))
|
|
173
|
+
except (ValueError, asyncio.TimeoutError, AssertionError):
|
|
174
|
+
ready = {}
|
|
175
|
+
if not ready.get("ready"):
|
|
176
|
+
log(f"witbitz-code: the cloud computer's browser did not start ({ready.get('error') or 'no answer'}) — using the laptop browser")
|
|
177
|
+
if proc.returncode is None:
|
|
178
|
+
proc.kill()
|
|
179
|
+
return None
|
|
180
|
+
env = {k: str(v) for k, v in ready.items() if k.startswith("WB_BROWSE_")}
|
|
181
|
+
env["WB_BROWSE_CDP_CLI"] = str(paths["browse-cdp.mjs"])
|
|
182
|
+
b = LocalBrowser(proc, int(env["WB_BROWSE_CTL"].rsplit(":", 1)[1]), env, httpx.AsyncClient(), tree=str(ready.get("tag") or ""))
|
|
183
|
+
await b._poll()
|
|
184
|
+
b._task = asyncio.ensure_future(b._every())
|
|
185
|
+
if st.get("current") is False:
|
|
186
|
+
log(f"witbitz-code: this release pins the cloud browser {st['pinned']['tag']} — update it on the witbitz-code page")
|
|
187
|
+
return b
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""THE CLOUD COMPUTER'S BROWSER ON THIS LINUX COMPUTER — the page's card and serve's choice (the owner, 2026-10-19: "why isnt
|
|
2
|
+
it part of the pipx install? and why isnt it installed from the witbitz code webpage?").
|
|
3
|
+
|
|
4
|
+
The program is `witbitz-browse-tree.mjs` (tools/browse-tree/cli.mjs, bundled with this release's pin by tools/browse-embed.mjs)
|
|
5
|
+
run with this computer's node: `status` says what is pinned and installed, `install --json` downloads the pinned image's tree
|
|
6
|
+
(~650 MB, both sha256s checked before anything is unpacked) with progress lines, `run` starts it under bwrap. Nothing is
|
|
7
|
+
downloaded until the person presses Install; a release that pins a newer image shows it as an update.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Callable
|
|
21
|
+
|
|
22
|
+
from . import plugins
|
|
23
|
+
|
|
24
|
+
SCRIPT_REL = "witbitz-browse/browse-tree.mjs"
|
|
25
|
+
PACKAGED = "witbitz-browse-tree.mjs"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def script(config_dir: Path | None = None) -> Path:
|
|
29
|
+
"""The program in OpenCode's config folder, beside the other browser files (an identical file is left alone)."""
|
|
30
|
+
target = (Path(config_dir) if config_dir else plugins.opencode_config_dir()) / SCRIPT_REL
|
|
31
|
+
text = plugins.packaged(PACKAGED)
|
|
32
|
+
try:
|
|
33
|
+
same = target.read_text(encoding="utf-8") == text
|
|
34
|
+
except (OSError, UnicodeDecodeError):
|
|
35
|
+
same = False
|
|
36
|
+
if not same:
|
|
37
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
target.write_text(text, encoding="utf-8", newline="")
|
|
39
|
+
return target
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def read_status(node: str, path: Path) -> dict:
|
|
43
|
+
"""`status` → {linux, needs, supported, pinned, installed, current, why}; {error} when it could not be asked."""
|
|
44
|
+
try:
|
|
45
|
+
r = subprocess.run([node, str(path), "status"], capture_output=True, text=True, timeout=20, check=False) # noqa: S603
|
|
46
|
+
return json.loads(r.stdout.strip().splitlines()[-1])
|
|
47
|
+
except (OSError, ValueError, IndexError, subprocess.SubprocessError) as e:
|
|
48
|
+
return {"error": f"could not check ({str(e)[:80]})"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Tree:
|
|
52
|
+
"""The page's card: {shown, state: idle|installing|done|failed, step, pct, error, ...status}. Outside reaches are injected."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, *, node: Callable[[], str | None] = lambda: shutil.which("node"),
|
|
55
|
+
script_path: Callable[[], Path] = script, read: Callable[[str, Path], dict] = read_status,
|
|
56
|
+
platform: str = sys.platform) -> None:
|
|
57
|
+
self._node, self._script, self._read, self._platform = node, script_path, read, platform
|
|
58
|
+
self.state, self.step, self.pct, self.error, self.tag = "idle", "", 0.0, "", ""
|
|
59
|
+
self.task: asyncio.Task | None = None
|
|
60
|
+
self._cache: tuple[float, dict] | None = None
|
|
61
|
+
|
|
62
|
+
def status(self) -> dict:
|
|
63
|
+
if not self._platform.startswith("linux"):
|
|
64
|
+
return {"shown": False}
|
|
65
|
+
live = {"state": self.state, "step": self.step, "pct": self.pct, "error": self.error, "tag": self.tag}
|
|
66
|
+
if self._cache and time.monotonic() - self._cache[0] < 15 and self.state != "installing":
|
|
67
|
+
return {**self._cache[1], **live}
|
|
68
|
+
node = self._node()
|
|
69
|
+
if not node:
|
|
70
|
+
st = {"shown": True, "supported": False, "needs": ["Node.js 22.15+"]}
|
|
71
|
+
else:
|
|
72
|
+
try:
|
|
73
|
+
st = {"shown": True, **self._read(node, self._script())}
|
|
74
|
+
except OSError as e:
|
|
75
|
+
st = {"shown": True, "error": f"could not check ({str(e)[:80]})"}
|
|
76
|
+
self._cache = (time.monotonic(), st)
|
|
77
|
+
return {**st, **live}
|
|
78
|
+
|
|
79
|
+
async def install(self) -> dict:
|
|
80
|
+
if self.task and not self.task.done():
|
|
81
|
+
return {"ok": True}
|
|
82
|
+
node = self._node()
|
|
83
|
+
if not node or not self._platform.startswith("linux"):
|
|
84
|
+
return {"ok": False, "why": "the cloud computer's browser needs Linux and Node.js"}
|
|
85
|
+
self.state, self.step, self.pct, self.error = "installing", "download", 0.0, ""
|
|
86
|
+
self.task = asyncio.ensure_future(self._install(node))
|
|
87
|
+
return {"ok": True}
|
|
88
|
+
|
|
89
|
+
async def _install(self, node: str) -> None:
|
|
90
|
+
try:
|
|
91
|
+
proc = await asyncio.create_subprocess_exec(node, str(self._script()), "install", "--json", stdin=asyncio.subprocess.DEVNULL,
|
|
92
|
+
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL)
|
|
93
|
+
assert proc.stdout
|
|
94
|
+
async for raw in proc.stdout:
|
|
95
|
+
try:
|
|
96
|
+
m: dict[str, Any] = json.loads(raw)
|
|
97
|
+
except ValueError:
|
|
98
|
+
continue
|
|
99
|
+
if m.get("error"):
|
|
100
|
+
self.error = str(m["error"])[:300]
|
|
101
|
+
elif m.get("ok"):
|
|
102
|
+
self.tag = str(m.get("tag") or "")
|
|
103
|
+
else:
|
|
104
|
+
self.step, self.pct = str(m.get("step") or ""), float(m.get("pct") or 0)
|
|
105
|
+
await proc.wait()
|
|
106
|
+
if proc.returncode and not self.error:
|
|
107
|
+
self.error = f"the install stopped ({proc.returncode})"
|
|
108
|
+
except (OSError, asyncio.CancelledError) as e:
|
|
109
|
+
self.error = self.error or f"could not run it ({str(e)[:120]})"
|
|
110
|
+
finally:
|
|
111
|
+
self._cache = None
|
|
112
|
+
self.state = "failed" if self.error else "done"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def use_tree(env: dict | None = None, platform: str = sys.platform) -> bool:
|
|
116
|
+
env = os.environ if env is None else env
|
|
117
|
+
return platform.startswith("linux") and str(env.get("WB_BROWSE_TREE", "")) != "0"
|
|
@@ -258,16 +258,28 @@ function createCast({ cdpHttp, onFrame, onState = () => {
|
|
|
258
258
|
const sh = c.h + BARS(c.h);
|
|
259
259
|
await onPage("Emulation.setDeviceMetricsOverride", { width: c.w, height: pageFull ? sh : c.h, deviceScaleFactor: c.dpr, mobile, screenWidth: c.w, screenHeight: sh });
|
|
260
260
|
await onPage("Emulation.setTouchEmulationEnabled", mobile ? { enabled: true, maxTouchPoints: 5 } : { enabled: false });
|
|
261
|
-
if (desktopUA) await onPage("Emulation.setUserAgentOverride",
|
|
261
|
+
if (desktopUA) await onPage("Emulation.setUserAgentOverride", uaOverride(desktopUA, await realHints(), mobile));
|
|
262
262
|
} else {
|
|
263
263
|
await onPage("Emulation.clearDeviceMetricsOverride").catch(() => {
|
|
264
264
|
});
|
|
265
265
|
await onPage("Emulation.setTouchEmulationEnabled", { enabled: false }).catch(() => {
|
|
266
266
|
});
|
|
267
|
-
if (desktopUA) await onPage("Emulation.setUserAgentOverride",
|
|
267
|
+
if (desktopUA) await onPage("Emulation.setUserAgentOverride", uaOverride(desktopUA, await realHints(), false)).catch(() => {
|
|
268
268
|
});
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
+
let hintsOf = null;
|
|
272
|
+
async function realHints() {
|
|
273
|
+
if (hintsOf) return hintsOf;
|
|
274
|
+
const r = await onPage("Runtime.evaluate", { expression: HINTS_NOW, returnByValue: true, awaitPromise: true }).catch(() => null);
|
|
275
|
+
const v = r && r.result && r.result.value;
|
|
276
|
+
if (v && Array.isArray(v.brands) && v.brands.length) hintsOf = v;
|
|
277
|
+
if (!hintsOf) {
|
|
278
|
+
const b = await send("Browser.getVersion").catch(() => null);
|
|
279
|
+
hintsOf = hintsFromVersion(b && b.product, desktopUA);
|
|
280
|
+
}
|
|
281
|
+
return hintsOf;
|
|
282
|
+
}
|
|
271
283
|
async function hookFullscreen() {
|
|
272
284
|
await onPage("Runtime.enable");
|
|
273
285
|
await onPage("Runtime.addBinding", { name: FS_BINDING });
|
|
@@ -467,6 +479,20 @@ function norm(v) {
|
|
|
467
479
|
return { w, h, dpr: dpr >= 1 && dpr <= 4 ? dpr : 1, mobile: !!v.mobile };
|
|
468
480
|
}
|
|
469
481
|
var PHONE = { w: 412, h: 915, dpr: 2, mobile: true };
|
|
482
|
+
var HINTS_NOW = `(async () => { const d = navigator.userAgentData; if (!d) return null; let h = {}; try { h = await d.getHighEntropyValues(['platformVersion', 'architecture', 'bitness', 'model', 'fullVersionList', 'wow64']) } catch (e) {} return { brands: d.brands, fullVersionList: h.fullVersionList || d.brands, platform: d.platform, platformVersion: h.platformVersion || '', architecture: h.architecture || '', bitness: h.bitness || '', model: h.model || '', wow64: !!h.wow64 } })()`;
|
|
483
|
+
function hintsFromVersion(product, ua = "") {
|
|
484
|
+
const full = (/\/(\d+(?:\.\d+){1,3})/.exec(String(product || "")) || [])[1];
|
|
485
|
+
if (!full) return null;
|
|
486
|
+
const major = full.split(".")[0];
|
|
487
|
+
const platform = /Windows/.test(ua) ? "Windows" : /Mac OS X/.test(ua) ? "macOS" : /CrOS/.test(ua) ? "Chrome OS" : "Linux";
|
|
488
|
+
return { brands: [{ brand: "Chromium", version: major }, { brand: "Not)A;Brand", version: "99" }], fullVersionList: [{ brand: "Chromium", version: full }, { brand: "Not)A;Brand", version: "99.0.0.0" }], platform, platformVersion: "", architecture: /arm|aarch64/i.test(ua) ? "arm" : "x86", bitness: "64", model: "", wow64: false };
|
|
489
|
+
}
|
|
490
|
+
function uaOverride(desktop, real, mobile) {
|
|
491
|
+
const userAgent = mobile ? phoneUA(desktop) : desktop;
|
|
492
|
+
if (!real || !Array.isArray(real.brands) || !real.brands.length) return { userAgent };
|
|
493
|
+
const meta = { brands: real.brands, fullVersionList: real.fullVersionList || real.brands, platform: real.platform || "", platformVersion: real.platformVersion || "", architecture: real.architecture || "", model: real.model || "", mobile: false, bitness: real.bitness || "", wow64: !!real.wow64 };
|
|
494
|
+
return mobile ? { userAgent, platform: "Linux armv8l", userAgentMetadata: { ...meta, platform: "Android", platformVersion: "14.0.0", architecture: "", bitness: "", model: "Pixel 8", mobile: true, wow64: false } } : { userAgent, userAgentMetadata: meta };
|
|
495
|
+
}
|
|
470
496
|
function phoneUA(desktop) {
|
|
471
497
|
return desktop.replace(/\([^)]*\)/, "(Linux; Android 14; Pixel 8)").replace(/ Safari\//, " Mobile Safari/");
|
|
472
498
|
}
|
|
@@ -476,6 +502,7 @@ var BINDING = "__wbm";
|
|
|
476
502
|
var MAX_BATCH = 8 << 20;
|
|
477
503
|
var MAX_SINCE_SNAPSHOT = 48 << 20;
|
|
478
504
|
var ASSET_CACHE = 96 << 20;
|
|
505
|
+
var MAX_ANSWER = 2900 << 10;
|
|
479
506
|
var MAX_SIDE2 = 1600;
|
|
480
507
|
var STILL_MS = 500;
|
|
481
508
|
var STILL_SIDE = 480;
|
|
@@ -607,12 +634,19 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
|
|
|
607
634
|
for (const h of c.handlers) h(m);
|
|
608
635
|
};
|
|
609
636
|
ws.onclose = () => {
|
|
637
|
+
c.closed = true;
|
|
638
|
+
log2("mirror: the CDP connection to", http, "closed");
|
|
610
639
|
for (const p of c.pending.values()) p.reject(new Error("cdp: closed"));
|
|
611
640
|
c.pending.clear();
|
|
612
641
|
};
|
|
613
642
|
return c;
|
|
614
643
|
}
|
|
615
644
|
async function attachLive() {
|
|
645
|
+
if (live && live.closed) {
|
|
646
|
+
live = null;
|
|
647
|
+
liveSid = null;
|
|
648
|
+
liveTargetId = null;
|
|
649
|
+
}
|
|
616
650
|
const t = liveTarget();
|
|
617
651
|
if (!t || t === liveTargetId) return;
|
|
618
652
|
if (!live) {
|
|
@@ -698,8 +732,14 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
|
|
|
698
732
|
body: body ? Buffer.from(body).toString("base64") : ""
|
|
699
733
|
}, tab.sid).catch(() => {
|
|
700
734
|
});
|
|
735
|
+
const tooBig = (b) => b && Buffer.byteLength(b) > MAX_ANSWER;
|
|
736
|
+
const send = (status, headers, body) => tooBig(body) ? (tab.refused++, log2("mirror: not sent,", Math.round(Buffer.byteLength(body) / 1024), "KB \u2014", url.slice(0, 100)), done(404, {}, "")) : done(status, headers, body);
|
|
701
737
|
if (url === "http://mirror.invalid/") return done(200, { "content-type": "text/html; charset=utf-8" }, shell);
|
|
702
738
|
if (p.request.method !== "GET" || !/^https?:\/\//.test(url)) return done(404, {}, "");
|
|
739
|
+
if (p.resourceType === "Media") {
|
|
740
|
+
tab.refused++;
|
|
741
|
+
return done(404, {}, "");
|
|
742
|
+
}
|
|
703
743
|
if (p.resourceType === "Document") {
|
|
704
744
|
let host = "";
|
|
705
745
|
try {
|
|
@@ -714,14 +754,14 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
|
|
|
714
754
|
try {
|
|
715
755
|
const r = await live.send("Network.getResponseBody", { requestId: known.requestId }, liveSid);
|
|
716
756
|
tab.served++;
|
|
717
|
-
return
|
|
757
|
+
return send(known.status || 200, { "content-type": known.mime || "application/octet-stream" }, r.base64Encoded ? Buffer.from(r.body, "base64") : r.body);
|
|
718
758
|
} catch {
|
|
719
759
|
}
|
|
720
760
|
}
|
|
721
761
|
if (assets.has(url)) {
|
|
722
762
|
tab.served++;
|
|
723
763
|
const a = assets.get(url);
|
|
724
|
-
return
|
|
764
|
+
return send(a.status, a.headers, a.body);
|
|
725
765
|
}
|
|
726
766
|
if (!STATIC.has(p.resourceType) || !liveSid) {
|
|
727
767
|
tab.refused++;
|
|
@@ -730,7 +770,7 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
|
|
|
730
770
|
try {
|
|
731
771
|
const a = await loadCredentialless(url);
|
|
732
772
|
tab.loaded++;
|
|
733
|
-
return
|
|
773
|
+
return send(a.status, a.headers, a.body);
|
|
734
774
|
} catch (e) {
|
|
735
775
|
tab.refused++;
|
|
736
776
|
log2("mirror: could not load", url.slice(0, 100), "\u2014", String(e.message || e).slice(0, 120));
|
|
@@ -816,7 +856,7 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
|
|
|
816
856
|
});
|
|
817
857
|
}
|
|
818
858
|
async function open2(stream, shape) {
|
|
819
|
-
if (!mirror) {
|
|
859
|
+
if (!mirror || mirror.closed) {
|
|
820
860
|
mirror = await connect(mirrorHttp);
|
|
821
861
|
mirror.handlers.push(onMirror);
|
|
822
862
|
}
|
|
@@ -881,6 +921,10 @@ function createMirrors({ liveHttp, mirrorHttp, rrwebUmd, onFrame, log: log2 = ()
|
|
|
881
921
|
/** Make the set of mirrors equal `wanted` (stream → shape). Opens, reshapes and closes as needed. */
|
|
882
922
|
async set(wanted) {
|
|
883
923
|
await attachLive();
|
|
924
|
+
if (mirror && mirror.closed) {
|
|
925
|
+
mirror = null;
|
|
926
|
+
tabs.clear();
|
|
927
|
+
}
|
|
884
928
|
for (const [stream] of tabs) if (!wanted.has(stream)) await close(stream);
|
|
885
929
|
for (const [stream, shape] of wanted) {
|
|
886
930
|
const tab = tabs.get(stream);
|
|
@@ -945,6 +989,7 @@ var tellControl = async (path, body) => {
|
|
|
945
989
|
}
|
|
946
990
|
};
|
|
947
991
|
var said = null;
|
|
992
|
+
var holderVid = null;
|
|
948
993
|
var tellTheViewerWhoHasTheControls = async (why) => {
|
|
949
994
|
let st = null;
|
|
950
995
|
try {
|
|
@@ -954,16 +999,17 @@ var tellTheViewerWhoHasTheControls = async (why) => {
|
|
|
954
999
|
}
|
|
955
1000
|
held = !!st.takeover;
|
|
956
1001
|
if (!held && cast && cast.incognito && cast.incognito()) closeIncognito("the controls lapsed");
|
|
1002
|
+
if (!held) holderVid = null;
|
|
957
1003
|
if (!held && cast && cast.controller()) {
|
|
958
1004
|
for (const v of castViewers.values()) v.driving = false;
|
|
959
1005
|
cast.setController(null).then(assign).catch(() => {
|
|
960
1006
|
});
|
|
961
1007
|
}
|
|
962
|
-
const now = JSON.stringify({ on: !!st.takeover, web: !!st.web });
|
|
1008
|
+
const now = JSON.stringify({ on: !!st.takeover, web: !!st.web, holder: holderVid });
|
|
963
1009
|
if (why === "poll" && now === said) return;
|
|
964
1010
|
said = now;
|
|
965
1011
|
if (relay && relay.readyState === 1) {
|
|
966
|
-
relay.send(await sealCtl(key, { wb: "takeover", on: !!st.takeover, web: !!st.web, audio: !!PULSE, ack: true, cast: !!CDP, site: siteNow(), incognito: !!(cast && cast.incognito && cast.incognito()) }));
|
|
1012
|
+
relay.send(await sealCtl(key, { wb: "takeover", on: !!st.takeover, web: !!st.web, ...st.takeover && holderVid ? { holder: holderVid } : {}, audio: !!PULSE, ack: true, cast: !!CDP, site: siteNow(), incognito: !!(cast && cast.incognito && cast.incognito()) }));
|
|
967
1013
|
}
|
|
968
1014
|
log(
|
|
969
1015
|
why === "poll" ? "the state changed \u2014" : "a viewer arrived \u2014",
|
|
@@ -1223,7 +1269,22 @@ async function assignNow() {
|
|
|
1223
1269
|
} catch {
|
|
1224
1270
|
}
|
|
1225
1271
|
}
|
|
1226
|
-
if (mirrors)
|
|
1272
|
+
if (!mirrors) return;
|
|
1273
|
+
try {
|
|
1274
|
+
await mirrors.set(wanted);
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
const why = "mirror could not open: " + String(e.message || e).slice(0, 80);
|
|
1277
|
+
for (const [vid, s] of assigned) {
|
|
1278
|
+
if (!wanted.has(s)) continue;
|
|
1279
|
+
assigned.set(vid, "live");
|
|
1280
|
+
if (relay && relay.readyState === 1) relay.send(await sealCtl(key, { wb: "stream", vid, stream: "live", why }));
|
|
1281
|
+
}
|
|
1282
|
+
const l = cast && cast.last();
|
|
1283
|
+
if (l) outOrder(async () => {
|
|
1284
|
+
if (relay && relay.readyState === 1) relay.send(await seal(key, KIND_FRAME, packFrame(l.header, l.jpeg)));
|
|
1285
|
+
});
|
|
1286
|
+
throw e;
|
|
1287
|
+
}
|
|
1227
1288
|
}
|
|
1228
1289
|
async function startCast() {
|
|
1229
1290
|
const c = ensureCast();
|
|
@@ -1327,12 +1388,13 @@ function connectRelay() {
|
|
|
1327
1388
|
if (!c.on) await closeIncognito("the controls were given back");
|
|
1328
1389
|
await tellControl("/takeover", { on: !!c.on });
|
|
1329
1390
|
held = !!c.on;
|
|
1391
|
+
holderVid = c.on && typeof c.vid === "string" ? c.vid.slice(0, 64) : null;
|
|
1330
1392
|
log("take-over", c.on ? "ON \u2014 the agent's CDP session is being closed" : "released");
|
|
1331
1393
|
if (cast) await cast.setController(c.on ? norm(c.view) || largestView() : null).catch(() => {
|
|
1332
1394
|
});
|
|
1333
1395
|
for (const [vid, v] of castViewers) v.driving = !!c.on && vid === c.vid;
|
|
1334
1396
|
if (cast) assign();
|
|
1335
|
-
if (relay && relay.readyState === 1) relay.send(await sealCtl(key, { wb: "takeover", on: !!c.on, audio: !!PULSE, ack: true, cast: !!cast, site: siteNow(), incognito: !!(cast && cast.incognito && cast.incognito()), close: true }));
|
|
1397
|
+
if (relay && relay.readyState === 1) relay.send(await sealCtl(key, { wb: "takeover", on: !!c.on, ...holderVid ? { holder: holderVid } : {}, audio: !!PULSE, ack: true, cast: !!cast, site: siteNow(), incognito: !!(cast && cast.incognito && cast.incognito()), close: true }));
|
|
1336
1398
|
} else if (c.wb === "incognito" && cast) {
|
|
1337
1399
|
if (c.on && !held) {
|
|
1338
1400
|
log("incognito: refused \u2014 nobody holds the controls");
|
|
@@ -1357,12 +1419,13 @@ function connectRelay() {
|
|
|
1357
1419
|
if (relay && relay.readyState === 1) relay.send(await sealCtl(key, { wb: "site", mode }));
|
|
1358
1420
|
} else if (c.wb === "hello" && c.cast && typeof c.vid === "string" && CDP) {
|
|
1359
1421
|
const known = castViewers.has(c.vid);
|
|
1360
|
-
|
|
1422
|
+
const mine = held && c.driving && (!holderVid || c.vid.slice(0, 64) === holderVid);
|
|
1423
|
+
castViewers.set(c.vid.slice(0, 64), { view: norm(c.view), at: Date.now(), driving: !!mine });
|
|
1361
1424
|
if (castViewers.size > 8) castViewers.delete(castViewers.keys().next().value);
|
|
1362
1425
|
if (!known) log("cast: a viewer said hello \u2014", JSON.stringify(norm(c.view)));
|
|
1363
1426
|
if (relay && relay.readyState === 1) relay.send(await sealCtl(key, { wb: "cast", on: true, close: true }));
|
|
1364
1427
|
await startCast();
|
|
1365
|
-
if (
|
|
1428
|
+
if (mine && cast) await cast.setController(norm(c.view)).catch(() => {
|
|
1366
1429
|
});
|
|
1367
1430
|
assign();
|
|
1368
1431
|
} else if (c.wb === "in" && cast) {
|