weed-cli 1.6.0__tar.gz → 1.6.7__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.
- {weed_cli-1.6.0/weed_cli.egg-info → weed_cli-1.6.7}/PKG-INFO +13 -2
- {weed_cli-1.6.0 → weed_cli-1.6.7}/README.md +12 -1
- {weed_cli-1.6.0 → weed_cli-1.6.7}/node.py +109 -11
- {weed_cli-1.6.0 → weed_cli-1.6.7}/pyproject.toml +1 -1
- {weed_cli-1.6.0 → weed_cli-1.6.7}/web_ui.py +84 -34
- {weed_cli-1.6.0 → weed_cli-1.6.7/weed_cli.egg-info}/PKG-INFO +13 -2
- {weed_cli-1.6.0 → weed_cli-1.6.7}/LICENSE +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/dht.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/discovery_relay.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/lightning_settle.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/poc_reputation.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/setup.cfg +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/shell.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/tunnel_relay.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/weed.py +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/weed_cli.egg-info/SOURCES.txt +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/weed_cli.egg-info/dependency_links.txt +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/weed_cli.egg-info/entry_points.txt +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/weed_cli.egg-info/requires.txt +0 -0
- {weed_cli-1.6.0 → weed_cli-1.6.7}/weed_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.7
|
|
4
4
|
Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
|
|
@@ -174,17 +174,28 @@ blind.
|
|
|
174
174
|
### Docker
|
|
175
175
|
|
|
176
176
|
`Dockerfile.node` + `docker-compose.node.yml` package `web_ui.py` to run
|
|
177
|
-
somewhere other than a laptop
|
|
177
|
+
somewhere other than a laptop — alongside its own local discovery relay
|
|
178
|
+
(`Dockerfile.discovery-relay`), so there's always something real for it
|
|
179
|
+
to announce to and discover from out of the box.
|
|
178
180
|
|
|
179
181
|
```bash
|
|
180
182
|
make node # build + run, http://127.0.0.1:8080
|
|
181
183
|
make node-down # stop it — data persists
|
|
184
|
+
make node-shell # interactive weed shell, same container as the running node
|
|
182
185
|
WEED_SHARE_DIR=~/Movies make node # mount a real directory of .ott archives at /share
|
|
186
|
+
docker compose -f docker-compose.node.yml exec node python3 weed.py discover # or a one-off CLI command
|
|
183
187
|
```
|
|
184
188
|
|
|
185
189
|
- Identity key, reputation store, and library manifest all persist in a
|
|
186
190
|
named volume (`node-data`, mounted at `$HOME=/data`) — one node keeps
|
|
187
191
|
the same pubkey (and everything vouching for it) across restarts.
|
|
192
|
+
- The `relay` service is `node`'s default `WEED_RELAY` (`http://relay:9101`,
|
|
193
|
+
resolved over Compose's own network) — its events persist in their own
|
|
194
|
+
`relay-data` volume, and port 9101 is published to the host too, for
|
|
195
|
+
curling `/events` directly or pointing a non-Docker `weed`/shell session
|
|
196
|
+
at the exact same relay. Override `WEED_RELAY` (shell env or `.env`,
|
|
197
|
+
see `.env.sample`) to point at public infra instead, e.g. for a real
|
|
198
|
+
end-to-end test.
|
|
188
199
|
- `/share` is a separate *bind* mount, not a named volume, so the actual
|
|
189
200
|
`.ott` archives you're hosting are real files on the host you can see
|
|
190
201
|
and manage directly. Point the web UI's Host form at `/share`.
|
|
@@ -144,17 +144,28 @@ blind.
|
|
|
144
144
|
### Docker
|
|
145
145
|
|
|
146
146
|
`Dockerfile.node` + `docker-compose.node.yml` package `web_ui.py` to run
|
|
147
|
-
somewhere other than a laptop
|
|
147
|
+
somewhere other than a laptop — alongside its own local discovery relay
|
|
148
|
+
(`Dockerfile.discovery-relay`), so there's always something real for it
|
|
149
|
+
to announce to and discover from out of the box.
|
|
148
150
|
|
|
149
151
|
```bash
|
|
150
152
|
make node # build + run, http://127.0.0.1:8080
|
|
151
153
|
make node-down # stop it — data persists
|
|
154
|
+
make node-shell # interactive weed shell, same container as the running node
|
|
152
155
|
WEED_SHARE_DIR=~/Movies make node # mount a real directory of .ott archives at /share
|
|
156
|
+
docker compose -f docker-compose.node.yml exec node python3 weed.py discover # or a one-off CLI command
|
|
153
157
|
```
|
|
154
158
|
|
|
155
159
|
- Identity key, reputation store, and library manifest all persist in a
|
|
156
160
|
named volume (`node-data`, mounted at `$HOME=/data`) — one node keeps
|
|
157
161
|
the same pubkey (and everything vouching for it) across restarts.
|
|
162
|
+
- The `relay` service is `node`'s default `WEED_RELAY` (`http://relay:9101`,
|
|
163
|
+
resolved over Compose's own network) — its events persist in their own
|
|
164
|
+
`relay-data` volume, and port 9101 is published to the host too, for
|
|
165
|
+
curling `/events` directly or pointing a non-Docker `weed`/shell session
|
|
166
|
+
at the exact same relay. Override `WEED_RELAY` (shell env or `.env`,
|
|
167
|
+
see `.env.sample`) to point at public infra instead, e.g. for a real
|
|
168
|
+
end-to-end test.
|
|
158
169
|
- `/share` is a separate *bind* mount, not a named volume, so the actual
|
|
159
170
|
`.ott` archives you're hosting are real files on the host you can see
|
|
160
171
|
and manage directly. Point the web UI's Host form at `/share`.
|
|
@@ -19,6 +19,7 @@ every previous script verified chunks locally or over a network, none of
|
|
|
19
19
|
them reassembled a full file from a remote peer onto disk before this.
|
|
20
20
|
"""
|
|
21
21
|
import base64
|
|
22
|
+
import concurrent.futures
|
|
22
23
|
import hashlib
|
|
23
24
|
import json
|
|
24
25
|
import os
|
|
@@ -44,8 +45,12 @@ TAGLINE = 'we do in 4 what others do in 5'
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def weed_version():
|
|
47
|
-
"""Installed package version (pyproject.toml's source of truth), with
|
|
48
|
-
|
|
48
|
+
"""Installed package version (pyproject.toml's source of truth), with
|
|
49
|
+
fallbacks for the two other real ways this runs: straight from a git
|
|
50
|
+
checkout without `pip install -e .` (read pyproject.toml directly —
|
|
51
|
+
Dockerfile.node copies it in for exactly this, since it copies loose
|
|
52
|
+
.py files rather than pip-installing the package), or neither file
|
|
53
|
+
present at all (genuinely no version info available)."""
|
|
49
54
|
try:
|
|
50
55
|
from importlib.metadata import version, PackageNotFoundError
|
|
51
56
|
try:
|
|
@@ -54,6 +59,15 @@ def weed_version():
|
|
|
54
59
|
pass
|
|
55
60
|
except ImportError:
|
|
56
61
|
pass
|
|
62
|
+
pyproject_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pyproject.toml')
|
|
63
|
+
try:
|
|
64
|
+
with open(pyproject_path) as f:
|
|
65
|
+
for line in f:
|
|
66
|
+
line = line.strip()
|
|
67
|
+
if line.startswith('version'):
|
|
68
|
+
return line.split('=', 1)[1].strip().strip('"\'')
|
|
69
|
+
except (OSError, IndexError):
|
|
70
|
+
pass
|
|
57
71
|
return '0.0.0-dev'
|
|
58
72
|
|
|
59
73
|
|
|
@@ -65,9 +79,36 @@ def _git_commit_hash():
|
|
|
65
79
|
behind a real debugging session: two checkouts reporting the same
|
|
66
80
|
version, one of them missing a fix the other had. None for anything
|
|
67
81
|
that isn't a git checkout at all (installed from a built wheel/sdist,
|
|
68
|
-
no .git present) — a version number is all there is to go on there.
|
|
82
|
+
no .git present) — a version number is all there is to go on there.
|
|
83
|
+
|
|
84
|
+
$WEED_GIT_COMMIT checked first — Dockerfile.node has no .git directory
|
|
85
|
+
at all (never copied in, on purpose: bloats the image and ships the
|
|
86
|
+
full history for no reason), so a container build has no way to
|
|
87
|
+
answer this live. docker-compose.node.yml passes the *host's* commit
|
|
88
|
+
hash in as a build arg at image-build time instead, baked in as this
|
|
89
|
+
env var, same intent as the live git lookup below just computed once,
|
|
90
|
+
earlier, somewhere that actually has the repo.
|
|
91
|
+
|
|
92
|
+
`git rev-parse` walks *up* parent directories looking for a .git —
|
|
93
|
+
a real `pip install .` (not `-e`) copies this file into site-packages,
|
|
94
|
+
which usually isn't a checkout at all, but if it happens to be nested
|
|
95
|
+
anywhere under some unrelated git-tracked ancestor directory (a
|
|
96
|
+
dotfiles repo, a pyenv install tracked in git, anything above it),
|
|
97
|
+
this would otherwise silently report *that* repo's own unrelated
|
|
98
|
+
commit — worse than showing nothing, since it looks plausible. Only
|
|
99
|
+
trust the hash once --show-toplevel confirms this file's own
|
|
100
|
+
directory really is that repo's root."""
|
|
101
|
+
env_commit = os.environ.get('WEED_GIT_COMMIT')
|
|
102
|
+
if env_commit:
|
|
103
|
+
return env_commit
|
|
69
104
|
repo_dir = os.path.dirname(os.path.abspath(__file__))
|
|
70
105
|
try:
|
|
106
|
+
toplevel = subprocess.run(['git', 'rev-parse', '--show-toplevel'], cwd=repo_dir,
|
|
107
|
+
capture_output=True, text=True, timeout=2)
|
|
108
|
+
if toplevel.returncode != 0:
|
|
109
|
+
return None
|
|
110
|
+
if os.path.realpath(toplevel.stdout.strip()) != os.path.realpath(repo_dir):
|
|
111
|
+
return None
|
|
71
112
|
result = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'], cwd=repo_dir,
|
|
72
113
|
capture_output=True, text=True, timeout=2)
|
|
73
114
|
if result.returncode == 0:
|
|
@@ -337,8 +378,24 @@ def serve_session(conn, entries_by_hash, default_hash, price, ln_node=None):
|
|
|
337
378
|
_graceful_close(conn)
|
|
338
379
|
|
|
339
380
|
|
|
381
|
+
def bind_host_port(port, bind_host='0.0.0.0'):
|
|
382
|
+
"""Split out of run_host_server so a caller (see web_ui.py's
|
|
383
|
+
_run_host_job) can bind the real, permanent listening socket early —
|
|
384
|
+
right where a port-collision needs to fail fast, before announcing
|
|
385
|
+
anything — and hand that exact socket to run_host_server later
|
|
386
|
+
instead of it binding a second, separate one. A bind-then-close probe
|
|
387
|
+
followed by a *second*, later bind of the same port isn't atomic
|
|
388
|
+
across two hosts starting concurrently: both can pass the probe
|
|
389
|
+
before either does the real bind. Binding once, early, and reusing
|
|
390
|
+
the same socket removes that race entirely instead of narrowing it."""
|
|
391
|
+
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
392
|
+
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
393
|
+
srv.bind((bind_host, port))
|
|
394
|
+
return srv
|
|
395
|
+
|
|
396
|
+
|
|
340
397
|
def run_host_server(archive_dir, file_name, port, bind_host='0.0.0.0', quiet=False, price=0,
|
|
341
|
-
ln_node=None):
|
|
398
|
+
ln_node=None, sock=None):
|
|
342
399
|
archive_dir = os.path.expanduser(archive_dir)
|
|
343
400
|
entries = load_manifest_entries(archive_dir, file_name)
|
|
344
401
|
entries_by_hash = {}
|
|
@@ -350,9 +407,7 @@ def run_host_server(archive_dir, file_name, port, bind_host='0.0.0.0', quiet=Fal
|
|
|
350
407
|
entries_by_hash[entry['sha256']] = (entry, leaves, file_path)
|
|
351
408
|
default_hash = next(iter(entries_by_hash)) if len(entries_by_hash) == 1 else None
|
|
352
409
|
|
|
353
|
-
srv =
|
|
354
|
-
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
355
|
-
srv.bind((bind_host, port))
|
|
410
|
+
srv = sock if sock is not None else bind_host_port(port, bind_host)
|
|
356
411
|
srv.listen(8)
|
|
357
412
|
if not quiet:
|
|
358
413
|
# a background thread's print() races with cmd.Cmd's input()-driven
|
|
@@ -979,19 +1034,61 @@ def _relay_url_hint(relay_url):
|
|
|
979
1034
|
return None
|
|
980
1035
|
|
|
981
1036
|
|
|
1037
|
+
def _normalize_relay_url(relay_url):
|
|
1038
|
+
"""A bare host:port with no scheme (e.g. '127.0.0.1:9101', the most
|
|
1039
|
+
natural way to type a local relay) isn't a URL urllib can parse at
|
|
1040
|
+
all -- everything before the first ':' becomes the *scheme*, so this
|
|
1041
|
+
reads as scheme '127.0.0.1' and raises "unknown url type", not a
|
|
1042
|
+
connection error. That already lands on post_event/fetch_events'
|
|
1043
|
+
existing except clauses (an unreachable relay is routine, not
|
|
1044
|
+
exceptional), which is exactly the trap: hosting against a
|
|
1045
|
+
scheme-less relay used to report status: running with zero actual
|
|
1046
|
+
announcements ever reaching it, and discover against one silently
|
|
1047
|
+
came back empty -- nothing hinted that the URL itself, not the
|
|
1048
|
+
relay, was the problem. Every relay this project actually ships or
|
|
1049
|
+
documents is plain HTTP(S), so defaulting a missing scheme to
|
|
1050
|
+
http:// turns the common typo into the thing the user obviously
|
|
1051
|
+
meant instead of a silent no-op."""
|
|
1052
|
+
if '://' not in relay_url:
|
|
1053
|
+
return 'http://' + relay_url
|
|
1054
|
+
return relay_url
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
_dns_bound_pool = concurrent.futures.ThreadPoolExecutor(max_workers=8, thread_name_prefix='urlopen')
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def _urlopen_bounded(req_or_url, timeout=5):
|
|
1061
|
+
"""urlopen's own timeout= only bounds the connect/read phases — the
|
|
1062
|
+
getaddrinfo() call underneath (DNS resolution) is a blocking libc call
|
|
1063
|
+
with no timeout of its own, so a relay hostname that's slow or
|
|
1064
|
+
unreachable to resolve can hang here indefinitely no matter what
|
|
1065
|
+
timeout= says. That's exactly the situation right after a container
|
|
1066
|
+
boots and its network/DNS isn't fully settled yet: _resume_persisted_hosts
|
|
1067
|
+
(web_ui.py) resuming a host at that moment used to get stuck at
|
|
1068
|
+
"(starting…)" forever, since nothing downstream ever times out either.
|
|
1069
|
+
Running the call in a worker thread and bounding *that* with
|
|
1070
|
+
.result(timeout=) catches DNS hangs the same as connect/read hangs —
|
|
1071
|
+
the stuck worker thread leaks (a blocking C call can't be cancelled),
|
|
1072
|
+
but it's a daemon-pool thread and this process was going to give up and
|
|
1073
|
+
move on regardless."""
|
|
1074
|
+
future = _dns_bound_pool.submit(urllib.request.urlopen, req_or_url, timeout=timeout)
|
|
1075
|
+
return future.result(timeout=timeout + 1)
|
|
1076
|
+
|
|
1077
|
+
|
|
982
1078
|
def post_event(relay_url, event):
|
|
983
1079
|
hint = _relay_url_hint(relay_url)
|
|
984
1080
|
if hint:
|
|
985
1081
|
return {'ok': False, 'error': hint}
|
|
1082
|
+
relay_url = _normalize_relay_url(relay_url)
|
|
986
1083
|
req = urllib.request.Request(
|
|
987
1084
|
f'{relay_url}/event', data=json.dumps(event).encode(),
|
|
988
1085
|
headers={'Content-Type': 'application/json'}, method='POST')
|
|
989
1086
|
try:
|
|
990
|
-
with
|
|
1087
|
+
with _urlopen_bounded(req, timeout=5) as resp:
|
|
991
1088
|
return json.loads(resp.read())
|
|
992
1089
|
except urllib.error.HTTPError as e:
|
|
993
1090
|
return json.loads(e.read())
|
|
994
|
-
except (urllib.error.URLError, ConnectionRefusedError) as e:
|
|
1091
|
+
except (urllib.error.URLError, ConnectionRefusedError, concurrent.futures.TimeoutError) as e:
|
|
995
1092
|
# download_with_auction posts the post-download attestation to every
|
|
996
1093
|
# relay in relay_urls, which by default includes 127.0.0.1:9101
|
|
997
1094
|
# whether or not anything is actually listening there (same default
|
|
@@ -1006,11 +1103,12 @@ def fetch_events(relay_url, event_type=None):
|
|
|
1006
1103
|
if hint:
|
|
1007
1104
|
print(f" {relay_url}: {hint}")
|
|
1008
1105
|
return None
|
|
1106
|
+
relay_url = _normalize_relay_url(relay_url)
|
|
1009
1107
|
url = f'{relay_url}/events' + (f'?type={event_type}' if event_type else '')
|
|
1010
1108
|
try:
|
|
1011
|
-
with
|
|
1109
|
+
with _urlopen_bounded(url, timeout=5) as resp:
|
|
1012
1110
|
return json.loads(resp.read())
|
|
1013
|
-
except (urllib.error.URLError, ConnectionRefusedError):
|
|
1111
|
+
except (urllib.error.URLError, ConnectionRefusedError, concurrent.futures.TimeoutError):
|
|
1014
1112
|
return None
|
|
1015
1113
|
|
|
1016
1114
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weed-cli"
|
|
7
|
-
version = "1.6.
|
|
7
|
+
version = "1.6.7"
|
|
8
8
|
description = "Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -187,9 +187,35 @@ def _ott_status(archive_dir):
|
|
|
187
187
|
|
|
188
188
|
|
|
189
189
|
def _resume_persisted_hosts():
|
|
190
|
+
"""Sequential on purpose, not fire-and-forget-them-all-at-once: two
|
|
191
|
+
persisted configs sharing a port (the default in the Host form is
|
|
192
|
+
always 9201, so this is the common case, not an edge case) could
|
|
193
|
+
both bind successfully if started concurrently, since node.bind_host_port
|
|
194
|
+
for the second one wouldn't even run until the first's already had time
|
|
195
|
+
to occupy the port — but nothing serializes *when* each thread gets
|
|
196
|
+
there. Waiting for each host to reach 'bound' (or 'error', if the bind
|
|
197
|
+
itself failed) before starting the next one closes that window: by the
|
|
198
|
+
time the next one binds, the previous one already has the port or
|
|
199
|
+
already knows it can't.
|
|
200
|
+
|
|
201
|
+
Only waits for the bind, not all the way to 'running' — 'running'
|
|
202
|
+
doesn't happen until after each host's full publish loop (real network
|
|
203
|
+
I/O, up to 5s timeout per relay), which several persisted hosts
|
|
204
|
+
resuming in sequence have no reason to serialize on. That was a real
|
|
205
|
+
regression the first version of this function had: booting with a
|
|
206
|
+
handful of persisted hosts got noticeably slower, for zero extra
|
|
207
|
+
correctness, since the publish delay doesn't affect port ownership at
|
|
208
|
+
all."""
|
|
190
209
|
for cfg in _persisted_hosts.values():
|
|
191
|
-
_start_host_job(cfg['archive_dir'], cfg['file_name'], cfg['port'], cfg['price'],
|
|
192
|
-
|
|
210
|
+
host_id = _start_host_job(cfg['archive_dir'], cfg['file_name'], cfg['port'], cfg['price'],
|
|
211
|
+
cfg['relay'], cfg['advertise_host'], cfg['tunnel'],
|
|
212
|
+
cfg.get('lightning_node'))
|
|
213
|
+
for _ in range(100): # up to ~10s per host before moving on regardless
|
|
214
|
+
with _lock:
|
|
215
|
+
status = _hosts[host_id]['status']
|
|
216
|
+
if status in ('bound', 'running', 'error'):
|
|
217
|
+
break
|
|
218
|
+
time.sleep(0.1)
|
|
193
219
|
|
|
194
220
|
|
|
195
221
|
def _unpublish_all_hosts():
|
|
@@ -284,29 +310,6 @@ def _detect_lan_ip():
|
|
|
284
310
|
s.close()
|
|
285
311
|
|
|
286
312
|
|
|
287
|
-
def _probe_port_free(port, bind_host='0.0.0.0'):
|
|
288
|
-
"""Bind-and-immediately-release, purely to fail fast on a port
|
|
289
|
-
collision *before* announcing anything — see _run_host_job's real
|
|
290
|
-
incident: the web UI's Host form always defaults to port 9201, so
|
|
291
|
-
hosting more than one file (including multiple entries auto-resumed
|
|
292
|
-
at once on startup, see _resume_persisted_hosts) without picking a
|
|
293
|
-
distinct port each time meant every loser published a real, signed
|
|
294
|
-
announcement and only discovered it couldn't actually bind *after*
|
|
295
|
-
— a permanent, unreachable zombie listing nothing could ever clean
|
|
296
|
-
up, since a host that never reaches 'running' never gets files/
|
|
297
|
-
announced_on populated for the graceful-shutdown unpublish to use
|
|
298
|
-
either. Still has a small window (this socket closes before
|
|
299
|
-
run_host_server's own real bind), not a hard guarantee — but it
|
|
300
|
-
catches the common case (concurrent startup, the actual incident)
|
|
301
|
-
instead of the current zero attempt to catch it at all."""
|
|
302
|
-
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
303
|
-
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
304
|
-
try:
|
|
305
|
-
probe.bind((bind_host, port))
|
|
306
|
-
finally:
|
|
307
|
-
probe.close()
|
|
308
|
-
|
|
309
|
-
|
|
310
313
|
def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, advertise_host, tunnel,
|
|
311
314
|
ln_node=None):
|
|
312
315
|
captured = io.StringIO()
|
|
@@ -325,17 +328,44 @@ def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, adve
|
|
|
325
328
|
# and only fail later, deep in a background thread with no way
|
|
326
329
|
# for the UI to ever find out
|
|
327
330
|
all_leaves = {e['sha256']: node.load_leaves(archive_dir, e['sha256']) for e in entries}
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
|
|
331
|
+
# bind the real, permanent listening socket now — before
|
|
332
|
+
# announcing anything — and hand it to run_host_server below
|
|
333
|
+
# instead of it binding a second one later. A separate
|
|
334
|
+
# bind-then-close probe here, followed by a *later* real bind,
|
|
335
|
+
# isn't atomic across two hosts starting concurrently (see
|
|
336
|
+
# _resume_persisted_hosts): both could pass the probe before
|
|
337
|
+
# either did the real bind. One bind, kept alive and reused,
|
|
338
|
+
# has no such window — the port either really is free right
|
|
339
|
+
# now or this raises immediately, before any publish.
|
|
340
|
+
bound_sock = node.bind_host_port(port)
|
|
341
|
+
# a distinct status the instant the bind (the only part
|
|
342
|
+
# _resume_persisted_hosts actually needs to wait for) is done
|
|
343
|
+
# — 'running' doesn't happen until after the publish loop
|
|
344
|
+
# below, which is real network I/O (up to 5s timeout *per*
|
|
345
|
+
# relay, see post_event) that resuming several hosts
|
|
346
|
+
# sequentially has no reason to serialize on
|
|
347
|
+
with _lock:
|
|
348
|
+
_hosts[host_id]['status'] = 'bound'
|
|
332
349
|
ott_status = node.ott_commit_status(archive_dir)
|
|
333
350
|
announced = []
|
|
334
351
|
for entry in entries:
|
|
335
352
|
for relay_url in relay_urls:
|
|
336
353
|
host_addr = f'{advertise_host}:{port}'
|
|
337
|
-
node.publish(identity, relay_url, entry['sha256'], entry['name'], host_addr,
|
|
338
|
-
|
|
354
|
+
result = node.publish(identity, relay_url, entry['sha256'], entry['name'], host_addr,
|
|
355
|
+
tunnel=tunnel, ott_status=ott_status)
|
|
356
|
+
# publish()/post_event() report a failed announce as a
|
|
357
|
+
# normal {'ok': False, ...} return, not an exception (an
|
|
358
|
+
# unreachable or malformed relay is routine, not
|
|
359
|
+
# exceptional -- see post_event's own docstring) -- which
|
|
360
|
+
# this loop used to just ignore entirely, so a host could
|
|
361
|
+
# sit at status: running, "announced" on a relay it never
|
|
362
|
+
# actually reached, with nothing in the UI to say so.
|
|
363
|
+
# do_host (shell.py) already prints every result
|
|
364
|
+
# unconditionally; this only prints the failures; a
|
|
365
|
+
# working host's log staying free of routine "ok: True"
|
|
366
|
+
# noise matters more here than in a CLI's own scrollback.
|
|
367
|
+
if isinstance(result, dict) and not result.get('ok', True):
|
|
368
|
+
print(f' ✗ announce to {relay_url} failed: {result.get("error")}')
|
|
339
369
|
announced = relay_urls
|
|
340
370
|
files = [{'name': e['name'], 'content_hash': e['sha256']} for e in entries]
|
|
341
371
|
with _lock:
|
|
@@ -354,7 +384,8 @@ def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, adve
|
|
|
354
384
|
all_leaves[entry['sha256']], file_path, price),
|
|
355
385
|
kwargs={'use_tls': use_tls, 'quiet': True, 'ln_node': ln_node},
|
|
356
386
|
daemon=True).start()
|
|
357
|
-
node.run_host_server(archive_dir, file_name, port, quiet=True, price=price, ln_node=ln_node
|
|
387
|
+
node.run_host_server(archive_dir, file_name, port, quiet=True, price=price, ln_node=ln_node,
|
|
388
|
+
sock=bound_sock)
|
|
358
389
|
except SystemExit as e:
|
|
359
390
|
with _lock:
|
|
360
391
|
_hosts[host_id].update(status='error', error=_with_captured_detail(str(e), captured))
|
|
@@ -374,6 +405,11 @@ def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightni
|
|
|
374
405
|
try:
|
|
375
406
|
t0 = time.time()
|
|
376
407
|
with _quiet() as captured:
|
|
408
|
+
# registered as soon as it's the real live buffer (not the
|
|
409
|
+
# placeholder above) so /api/download/<job_id> can read
|
|
410
|
+
# node.py's real prints as they happen, not just after the
|
|
411
|
+
# job finishes — see the GET handler's use of _job_logs
|
|
412
|
+
_job_logs[job_id] = captured
|
|
377
413
|
path = node.download_with_auction(content_hash, relay_urls, out_path=out_path, k=k,
|
|
378
414
|
use_lightning=use_lightning, lightning_node=lightning_node,
|
|
379
415
|
on_progress=on_progress)
|
|
@@ -458,9 +494,16 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
458
494
|
'subscriptions': list(_library['subscriptions']),
|
|
459
495
|
})
|
|
460
496
|
if path.startswith('/api/download/'):
|
|
497
|
+
job_id = path[len('/api/download/'):]
|
|
461
498
|
with _lock:
|
|
462
|
-
job = _jobs.get(
|
|
463
|
-
|
|
499
|
+
job = _jobs.get(job_id)
|
|
500
|
+
job = dict(job) if job else None
|
|
501
|
+
if not job:
|
|
502
|
+
return self._json({'error': 'no such job'}, status=404)
|
|
503
|
+
log_buf = _job_logs.get(job_id)
|
|
504
|
+
if log_buf is not None:
|
|
505
|
+
job['log'] = log_buf.getvalue()
|
|
506
|
+
return self._json(job)
|
|
464
507
|
if path.startswith('/api/reputation/'):
|
|
465
508
|
from poc_reputation import ReputationStore
|
|
466
509
|
pubkey = path[len('/api/reputation/'):]
|
|
@@ -755,6 +798,13 @@ def run_web_ui(port=8080, bind_host='127.0.0.1', quiet=False, advertise_host=Non
|
|
|
755
798
|
_lan_url = f'http://{reachable_host}:{port}/'
|
|
756
799
|
|
|
757
800
|
if not quiet:
|
|
801
|
+
# answers "is this container actually running the code I think it
|
|
802
|
+
# is" directly in `docker compose logs`/`make node`'s own output —
|
|
803
|
+
# see node.weed_banner()'s own docstring for exactly the debugging
|
|
804
|
+
# session that motivated adding a commit hash to it in the first
|
|
805
|
+
# place; printing it here means the same question doesn't need a
|
|
806
|
+
# docker exec + grep to answer for the web UI specifically
|
|
807
|
+
print(f"[web:{port}] {node.weed_banner()}", flush=True)
|
|
758
808
|
print(f"[web:{port}] weed control UI at http://{bind_host}:{port}/", flush=True)
|
|
759
809
|
if bind_host == '127.0.0.1':
|
|
760
810
|
print(" bound to localhost only -- pass --bind 0.0.0.0 to reach this from your "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.7
|
|
4
4
|
Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
|
|
@@ -174,17 +174,28 @@ blind.
|
|
|
174
174
|
### Docker
|
|
175
175
|
|
|
176
176
|
`Dockerfile.node` + `docker-compose.node.yml` package `web_ui.py` to run
|
|
177
|
-
somewhere other than a laptop
|
|
177
|
+
somewhere other than a laptop — alongside its own local discovery relay
|
|
178
|
+
(`Dockerfile.discovery-relay`), so there's always something real for it
|
|
179
|
+
to announce to and discover from out of the box.
|
|
178
180
|
|
|
179
181
|
```bash
|
|
180
182
|
make node # build + run, http://127.0.0.1:8080
|
|
181
183
|
make node-down # stop it — data persists
|
|
184
|
+
make node-shell # interactive weed shell, same container as the running node
|
|
182
185
|
WEED_SHARE_DIR=~/Movies make node # mount a real directory of .ott archives at /share
|
|
186
|
+
docker compose -f docker-compose.node.yml exec node python3 weed.py discover # or a one-off CLI command
|
|
183
187
|
```
|
|
184
188
|
|
|
185
189
|
- Identity key, reputation store, and library manifest all persist in a
|
|
186
190
|
named volume (`node-data`, mounted at `$HOME=/data`) — one node keeps
|
|
187
191
|
the same pubkey (and everything vouching for it) across restarts.
|
|
192
|
+
- The `relay` service is `node`'s default `WEED_RELAY` (`http://relay:9101`,
|
|
193
|
+
resolved over Compose's own network) — its events persist in their own
|
|
194
|
+
`relay-data` volume, and port 9101 is published to the host too, for
|
|
195
|
+
curling `/events` directly or pointing a non-Docker `weed`/shell session
|
|
196
|
+
at the exact same relay. Override `WEED_RELAY` (shell env or `.env`,
|
|
197
|
+
see `.env.sample`) to point at public infra instead, e.g. for a real
|
|
198
|
+
end-to-end test.
|
|
188
199
|
- `/share` is a separate *bind* mount, not a named volume, so the actual
|
|
189
200
|
`.ott` archives you're hosting are real files on the host you can see
|
|
190
201
|
and manage directly. Point the web UI's Host form at `/share`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|