fleetmesh-node 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
node/app/core.py
ADDED
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
"""NodeApp: what `meshnode run` starts. Construction, the compartment
|
|
2
|
+
assembly in start(), the run loop, the inbound door and status.json.
|
|
3
|
+
|
|
4
|
+
Separate from control.py so the file that registers the compartments - the
|
|
5
|
+
one scripts/check-cli.py reads for tick order - holds nothing else.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import collections
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any, Callable, Mapping, MutableMapping, Sequence, cast
|
|
15
|
+
|
|
16
|
+
from ..config.node import NodeConfigDict
|
|
17
|
+
|
|
18
|
+
from fleetmesh_core.accounting import standing
|
|
19
|
+
from fleetmesh_core.identity import identity
|
|
20
|
+
from fleetmesh_core.peering import peering
|
|
21
|
+
from .. import (channels as channels_mod, compartment as compartment_mod,
|
|
22
|
+
keystore, metrics as metrics_mod, transport as transport_mod)
|
|
23
|
+
from ..channels import journal
|
|
24
|
+
from fleetmesh_core.wire import validate
|
|
25
|
+
from ..protocols.nostr import blobstore as blobstore_mod, eventstore, peers as peers_mod
|
|
26
|
+
from ..storage import accounts as accounts_mod, grants as grants_mod
|
|
27
|
+
from ..protocols import ProtocolManager
|
|
28
|
+
from ..transport.host import ProtocolHost
|
|
29
|
+
from . import router as router_mod
|
|
30
|
+
from ..compartment import context as context_mod
|
|
31
|
+
from ..metrics import adapters as metrics_adapters
|
|
32
|
+
from ..metrics.face import face_metrics
|
|
33
|
+
from ..protocols.nostr import client as relay, descriptor
|
|
34
|
+
from ..planes.identity.announce import compartment as announce_mod
|
|
35
|
+
from ..planes.identity.discovery import compartment as discovery_wire
|
|
36
|
+
from ..planes.identity.presence import compartment as presence_mod
|
|
37
|
+
from ..planes.identity.linked import compartment as linked_mod
|
|
38
|
+
from ..planes.store.sync import core as sync_wire
|
|
39
|
+
from ..planes.store.retention import compartment as retention_mod
|
|
40
|
+
from ..planes.store.blob_wire import core as blob_wire_mod
|
|
41
|
+
from ..planes.store.blob_replication import compartment as blob_repl_mod
|
|
42
|
+
from ..planes.peering.trust import compartment as trust_mod
|
|
43
|
+
from ..planes.peering.strain import compartment as strain_mod
|
|
44
|
+
from ..planes.peering.peering import core as peering_wire
|
|
45
|
+
from ..planes.peering.budget import core as budget_mod
|
|
46
|
+
from ..planes.peering.capacity import compartment as capacity_mod
|
|
47
|
+
from ..planes.peering.reporter import core as reporter_mod
|
|
48
|
+
from ..planes.transport.link import compartment as link_mod
|
|
49
|
+
from ..planes.transport.lan import compartment as lan_mod
|
|
50
|
+
from ..planes.transport.p2p import compartment as p2p_wire
|
|
51
|
+
from ..planes.transport.mediation import compartment as mediation_mod
|
|
52
|
+
from ..planes.transport.bridge import compartment as bridge_mod
|
|
53
|
+
from ..planes.services.gossip import core as gossip_wire
|
|
54
|
+
from ..planes.services.compute import compartment as compute_mod
|
|
55
|
+
from ..planes.services.compute_wire import compartment as compute_wire_mod
|
|
56
|
+
from ..planes.services.settle import compartment as settlement_mod
|
|
57
|
+
from ..planes.services.paidfetch import compartment as fetch_pricing_mod
|
|
58
|
+
from ..planes.services.sidecars import compartment as sidecars_mod
|
|
59
|
+
from ..planes.services.dvm import gateway as dvm_mod
|
|
60
|
+
from ..planes.services.nsite import compartment as nsite_mod
|
|
61
|
+
from ..planes.services.video import compartment as video_mod
|
|
62
|
+
from ..planes.operations.scheduler import compartment as scheduler_mod
|
|
63
|
+
from ..planes.operations.updates import core as updates_mod
|
|
64
|
+
from ..compartment import lifecycle as lifecycle_mod
|
|
65
|
+
from . import socket as control_mod
|
|
66
|
+
from ..config import NodeConfig
|
|
67
|
+
from ..interfaces import NostrEvent
|
|
68
|
+
from .config import _blob_storage_cap, load_config, typed_config
|
|
69
|
+
from .control import ControlMixin
|
|
70
|
+
from .plugins import load_plugins, plugin_sidecars
|
|
71
|
+
from .systemd import sd_notify
|
|
72
|
+
from .views import PLANES as _VIEW_PLANES, node_health, node_status
|
|
73
|
+
|
|
74
|
+
# How anything in the node says something an operator will read, and how a
|
|
75
|
+
# trust entry is understood. Both live where a compartment can reach them
|
|
76
|
+
# without importing the node that imports the compartment.
|
|
77
|
+
_line, _short = journal.line, journal.short
|
|
78
|
+
trust_decision, trust_tiers = trust_mod.decision, trust_mod.tiers
|
|
79
|
+
|
|
80
|
+
# The node's compartments in tick order. This is the one list scripts/check-cli.py,
|
|
81
|
+
# the `subsystems` command and the docs agree on. The order is a decision, not
|
|
82
|
+
# a derivation: the relay journal first, so a subscription that dropped is said
|
|
83
|
+
# before anything that depends on it; then the operator's decisions, which
|
|
84
|
+
# outrank the ladder; then the mechanisms; then what the node publishes about
|
|
85
|
+
# itself; then the pulls, which are the only load it chooses; then the paid and
|
|
86
|
+
# fronted services, which are inert until configured. Optional compartments sit
|
|
87
|
+
# where they tick and are built only when their config section is present.
|
|
88
|
+
MANIFEST: tuple[type[compartment_mod.Compartment], ...] = (
|
|
89
|
+
link_mod.Link,
|
|
90
|
+
trust_mod.Trust,
|
|
91
|
+
strain_mod.StrainLedger,
|
|
92
|
+
peering_wire.Peering,
|
|
93
|
+
budget_mod.Budget,
|
|
94
|
+
capacity_mod.Capacity,
|
|
95
|
+
sync_wire.SyncPeer,
|
|
96
|
+
reporter_mod.Reporter,
|
|
97
|
+
discovery_wire.Discovery,
|
|
98
|
+
announce_mod.Announce,
|
|
99
|
+
lan_mod.Lan,
|
|
100
|
+
p2p_wire.P2PTransport, # optional: cfg['p2p']
|
|
101
|
+
gossip_wire.GossipMesh, # optional: cfg['gossip']
|
|
102
|
+
presence_mod.Presence,
|
|
103
|
+
linked_mod.LinkedFace, # the schema.org face; reads presence's 10050 and budget's ledger
|
|
104
|
+
retention_mod.Retention,
|
|
105
|
+
scheduler_mod.Scheduler,
|
|
106
|
+
compute_mod.Compute, # optional: cfg['compute']
|
|
107
|
+
compute_wire_mod.ComputeWire, # optional: cfg['compute']
|
|
108
|
+
bridge_mod.RelayBridge, # optional: cfg['bridge']
|
|
109
|
+
settlement_mod.Settlement,
|
|
110
|
+
mediation_mod.Mediation,
|
|
111
|
+
blob_wire_mod.BlobWire,
|
|
112
|
+
blob_repl_mod.Replication,
|
|
113
|
+
fetch_pricing_mod.PaidFetch,
|
|
114
|
+
sidecars_mod.Sidecars,
|
|
115
|
+
dvm_mod.DVMGateway, # optional: cfg['dvm']
|
|
116
|
+
nsite_mod.NsiteGateway, # optional: cfg['nsite']; after bridge, which it uses for manifests
|
|
117
|
+
video_mod.VideoTransitCompartment, # optional: cfg['video']; after nsite, the other HTTP face
|
|
118
|
+
updates_mod.Updates, # optional: cfg['updates']; last - it reports on all the above
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class NodeApp(ControlMixin):
|
|
123
|
+
# The compartments, set on the node by Registry.assemble() under each
|
|
124
|
+
# class's `attr` (or `name`). Declared so the node's surface is stated,
|
|
125
|
+
# and so the checker knows it: setattr is invisible to mypy. The six (6)
|
|
126
|
+
# optional ones - p2p, gossip, compute, bridge, dvm, updates - are absent
|
|
127
|
+
# on a node whose config does not enable them.
|
|
128
|
+
manifest: tuple[type[compartment_mod.Compartment], ...] # MANIFEST plus any configured plugins
|
|
129
|
+
link: link_mod.Link
|
|
130
|
+
trust: trust_mod.Trust
|
|
131
|
+
strain: strain_mod.StrainLedger
|
|
132
|
+
peering: peering_wire.Peering
|
|
133
|
+
budget: budget_mod.Budget
|
|
134
|
+
capacity: capacity_mod.Capacity
|
|
135
|
+
sync: sync_wire.SyncPeer
|
|
136
|
+
reporter: reporter_mod.Reporter
|
|
137
|
+
discovery: discovery_wire.Discovery
|
|
138
|
+
announce: announce_mod.Announce
|
|
139
|
+
lan: lan_mod.Lan
|
|
140
|
+
p2p: p2p_wire.P2PTransport
|
|
141
|
+
gossip: gossip_wire.GossipMesh
|
|
142
|
+
presence: presence_mod.Presence
|
|
143
|
+
retention: retention_mod.Retention
|
|
144
|
+
scheduler: scheduler_mod.Scheduler
|
|
145
|
+
compute: compute_mod.Compute
|
|
146
|
+
compute_wire: compute_wire_mod.ComputeWire
|
|
147
|
+
bridge: bridge_mod.RelayBridge
|
|
148
|
+
settlement: settlement_mod.Settlement
|
|
149
|
+
mediation: mediation_mod.Mediation
|
|
150
|
+
blob_wire: blob_wire_mod.BlobWire
|
|
151
|
+
blob_replication: blob_repl_mod.Replication
|
|
152
|
+
paid_fetch: fetch_pricing_mod.PaidFetch
|
|
153
|
+
sidecars: sidecars_mod.Sidecars
|
|
154
|
+
dvm: dvm_mod.DVMGateway
|
|
155
|
+
video: video_mod.VideoTransitCompartment
|
|
156
|
+
|
|
157
|
+
def __init__(self, directory: str, cfg: Mapping[str, Any] | None = None,
|
|
158
|
+
clock: Callable[[], int] | None = None,
|
|
159
|
+
relays_override: Sequence[str] | None = None) -> None:
|
|
160
|
+
self.dir: str = directory
|
|
161
|
+
self.cfg: MutableMapping[str, Any] = dict(cfg if cfg is not None else load_config(directory))
|
|
162
|
+
# The typed view of the same file. Compartments get their block through
|
|
163
|
+
# NodeContext.block(); the ambient keys the node itself reads are below.
|
|
164
|
+
self.settings: NodeConfig = typed_config(self.cfg)
|
|
165
|
+
self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
|
|
166
|
+
self.relays: list[str] = list(relays_override if relays_override is not None else self.cfg['relays'])
|
|
167
|
+
self.ks: keystore.Keystore = keystore.Keystore.load_or_generate(directory)
|
|
168
|
+
self.store: eventstore.NodeStore = eventstore.NodeStore(os.path.join(directory, 'events.sqlite'))
|
|
169
|
+
self.grants: grants_mod.GrantStore = grants_mod.GrantStore(os.path.join(directory, 'grants.sqlite'))
|
|
170
|
+
self.accounts: accounts_mod.AccountStore = accounts_mod.AccountStore(os.path.join(directory, 'accounts.sqlite'))
|
|
171
|
+
blobs_cfg = self.settings.blobs
|
|
172
|
+
self.blobs: blobstore_mod.BlobStore = blobstore_mod.BlobStore(
|
|
173
|
+
os.path.join(directory, 'blobs'), max_storage_bytes=_blob_storage_cap(self.cfg),
|
|
174
|
+
fanout=blobs_cfg.fanout, data_dir=blobs_cfg.data_dir)
|
|
175
|
+
# What this node measures, declared by the compartments rather than
|
|
176
|
+
# written out here. Built before the face so the face's
|
|
177
|
+
# `metrics_provider` can close over it; the declarations arrive later,
|
|
178
|
+
# at assemble(), and `read()` walks whatever is registered when a
|
|
179
|
+
# scrape happens.
|
|
180
|
+
self.monitors: metrics_mod.Monitors = metrics_mod.Monitors()
|
|
181
|
+
|
|
182
|
+
# The unified protocol host multiplexer
|
|
183
|
+
host_cfg = self.settings.protocol_suites.host
|
|
184
|
+
host_ip = host_cfg.listen if (host_cfg.listen != '127.0.0.1' or not self.settings.face.host) else self.settings.face.host
|
|
185
|
+
host_port = host_cfg.port if (host_cfg.port != 0 or not self.settings.face.port) else self.settings.face.port
|
|
186
|
+
self.host: ProtocolHost = ProtocolHost(
|
|
187
|
+
host=host_ip,
|
|
188
|
+
port=host_port,
|
|
189
|
+
name=self.settings.name,
|
|
190
|
+
clock=self.clock,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# The protocol manager orchestrating all active suites
|
|
194
|
+
self.protocols: ProtocolManager = ProtocolManager(
|
|
195
|
+
node_dir=directory,
|
|
196
|
+
keystore=self.ks,
|
|
197
|
+
clock=self.clock,
|
|
198
|
+
name=self.settings.name,
|
|
199
|
+
)
|
|
200
|
+
self.protocols.load_suites_config(
|
|
201
|
+
self.settings.protocol_suites,
|
|
202
|
+
backend_store=self.store,
|
|
203
|
+
blob_store=self.blobs,
|
|
204
|
+
grants=self.grants,
|
|
205
|
+
node_instance=self,
|
|
206
|
+
strain_ledger=getattr(self, 'strain', None),
|
|
207
|
+
)
|
|
208
|
+
self.protocols.mount_ingress(self.host)
|
|
209
|
+
|
|
210
|
+
nostr_pkg = self.protocols.get_package('nostr')
|
|
211
|
+
relay_srv = getattr(nostr_pkg, 'relay_server', None) if nostr_pkg is not None else None
|
|
212
|
+
self.face: Any = relay_srv
|
|
213
|
+
if relay_srv is not None:
|
|
214
|
+
relay_srv.metrics_provider = metrics_adapters.provider(self.monitors)
|
|
215
|
+
self.monitors.register(face_metrics(relay_srv), owner='face')
|
|
216
|
+
if relay_srv.blossom is not None:
|
|
217
|
+
apm = self.settings.storage.allow_private_mirror
|
|
218
|
+
relay_srv.blossom.allow_private_mirror = list(apm) if isinstance(apm, tuple) else apm
|
|
219
|
+
|
|
220
|
+
self.descriptor: NostrEvent | None = None
|
|
221
|
+
self.timeout: float = 15.0
|
|
222
|
+
self.peers: peers_mod.Peers = peers_mod.Peers(self.relays, clock=self.clock)
|
|
223
|
+
self.transport: transport_mod.TransportRouter = transport_mod.TransportRouter(
|
|
224
|
+
self.ks, self.peers, self.relays,
|
|
225
|
+
endpoints=self.cfg.get('endpoints', []),
|
|
226
|
+
clock=self.clock,
|
|
227
|
+
seen_store=(self.accounts.seen_wrap, self.accounts.put_seen_wrap),
|
|
228
|
+
ack_store=self.accounts,
|
|
229
|
+
sidecar_dir=self.dir,
|
|
230
|
+
enable_nostr=(nostr_pkg is not None),
|
|
231
|
+
)
|
|
232
|
+
self.protocols.mount_transport_rungs(self.transport)
|
|
233
|
+
pol = self.settings.policy
|
|
234
|
+
self.policy: peering.Policy = peering.Policy(pow_bits=self.settings.pow_bits, open_enrolment=pol.open_enrolment,
|
|
235
|
+
protocols=list(pol.protocols),
|
|
236
|
+
tiers=trust_tiers(self.cfg.get('trust')))
|
|
237
|
+
self.ledger: standing.Ledger = standing.Ledger()
|
|
238
|
+
self.events: channels_mod.NodeEventBus = channels_mod.NodeEventBus()
|
|
239
|
+
self.events.tap = channels_mod.Tap(clock=self.clock)
|
|
240
|
+
# What this node says, fanned out from one declaration per event.
|
|
241
|
+
# After `self.events`, because the bus is one of the places a record
|
|
242
|
+
# goes: anything already subscribed to a topic by name keeps working.
|
|
243
|
+
# Not `self.log`: that name is already a 500-entry deque of tick and
|
|
244
|
+
# handler failures (below), and shadowing it would have silently
|
|
245
|
+
# replaced one with the other. `says` is what this does.
|
|
246
|
+
self.says: channels_mod.Log = channels_mod.Log(
|
|
247
|
+
backends=[channels_mod.ConsoleBackend()], clock=self.clock,
|
|
248
|
+
bus=self.events, monitors=self.monitors)
|
|
249
|
+
self.stores: context_mod.NodeStores = context_mod.NodeStores(
|
|
250
|
+
events=self.store,
|
|
251
|
+
grants=self.grants,
|
|
252
|
+
accounts=self.accounts,
|
|
253
|
+
blobs=self.blobs,
|
|
254
|
+
)
|
|
255
|
+
self.router: router_mod.ProtocolRouter = router_mod.ProtocolRouter()
|
|
256
|
+
self._stop: threading.Event = threading.Event()
|
|
257
|
+
self.context: context_mod.NodeContext = context_mod.NodeContext(
|
|
258
|
+
dir=self.dir, ks=self.ks, cfg=cast(NodeConfigDict, self.cfg), clock=self.clock, events=self.events,
|
|
259
|
+
stores=self.stores, relays=self.relays, transport=self.transport, router=self.router,
|
|
260
|
+
face=self.face, host=self.host, peers=self.peers, policy=self.policy, ledger=self.ledger,
|
|
261
|
+
timeout=self.timeout, stop=self._stop, node=self, persist_cfg=self.save_coverage,
|
|
262
|
+
redescribe=self.redescribe,
|
|
263
|
+
regrant=self.regrant,
|
|
264
|
+
propose=self.propose,
|
|
265
|
+
pull=self.pull,
|
|
266
|
+
amend=self._amend,
|
|
267
|
+
build_descriptor=self.build_descriptor,
|
|
268
|
+
says=self.says,
|
|
269
|
+
)
|
|
270
|
+
# What was a dozen dicts here is now the state of the compartment that
|
|
271
|
+
# keeps it: the pulls belong to the scheduler, the publications to
|
|
272
|
+
# discovery, the descriptor and the heartbeats to presence, the relay
|
|
273
|
+
# journal to link. The node keeps only what the node owns.
|
|
274
|
+
self.log: collections.deque[Any] = collections.deque(maxlen=500)
|
|
275
|
+
self.counts: collections.Counter[str] = collections.Counter() # handler failed, tick failed, config rejected
|
|
276
|
+
self.rejected: collections.Counter[str] = collections.Counter() # messages refused at the door, by reason
|
|
277
|
+
self._last_said: dict[str, float] = {} # rate limit on repeated log lines
|
|
278
|
+
self._last_tick: Sequence[tuple[Any, ...]] = () # what the last tick did, for status.json
|
|
279
|
+
self._last_tick_error: str | None = None
|
|
280
|
+
self._config_error: str | None = None
|
|
281
|
+
self._tick_lock: threading.RLock = threading.RLock() # the run loop and the control socket both tick
|
|
282
|
+
self._built: bool = False
|
|
283
|
+
self._stopped: bool = False
|
|
284
|
+
# Config blocks edited on disk that wait for a node restart, as last
|
|
285
|
+
# said: their owner does not declare restart, so the edit is reported
|
|
286
|
+
# once per change rather than once a tick.
|
|
287
|
+
self._waiting: MutableMapping[str, Any] = {}
|
|
288
|
+
|
|
289
|
+
# ---- assembly ----------------------------------------------------------------
|
|
290
|
+
DESCRIPTOR_FILE: str = 'descriptor.json'
|
|
291
|
+
# Present while descriptor.json holds a descriptor no relay has accepted
|
|
292
|
+
# yet. start() mines before it assembles (peering reads the descriptor at
|
|
293
|
+
# assembly) but publishes only once the compartments have started, so a
|
|
294
|
+
# refused start leaves a mined, unpublished descriptor behind. Reusing it
|
|
295
|
+
# keeps the proof of work; this marker is what makes the next start that
|
|
296
|
+
# does succeed publish it, rather than assume the relays already have it.
|
|
297
|
+
PENDING_FILE: str = 'descriptor.pending'
|
|
298
|
+
|
|
299
|
+
def _endpoints(self) -> list[tuple[str, str, int]]:
|
|
300
|
+
"""What the descriptor advertises, which is what the operator configured.
|
|
301
|
+
|
|
302
|
+
It used to fall back to this node's own relay face, and the face binds
|
|
303
|
+
loopback on an ephemeral port by default: the descriptor then advertised
|
|
304
|
+
an address no peer could reach, and one that changed on every restart,
|
|
305
|
+
so the proof of work could never be reused either. `endpoint` is
|
|
306
|
+
optional on a kind 11801. A node with nothing reachable to advertise
|
|
307
|
+
says nothing, and is still reachable over the nostr transport, which is
|
|
308
|
+
the binding the specification says always works.
|
|
309
|
+
"""
|
|
310
|
+
return [(e['transport'], e['address'], e.get('priority', 50)) for e in self.cfg['endpoints']]
|
|
311
|
+
|
|
312
|
+
def load_descriptor(self) -> NostrEvent | None:
|
|
313
|
+
"""The descriptor this node published last time, if it is still the one
|
|
314
|
+
it would publish now.
|
|
315
|
+
|
|
316
|
+
A descriptor costs a NIP-13 proof of work to mine - twenty bits, which
|
|
317
|
+
took fourteen seconds on a small droplet - and it is a replaceable
|
|
318
|
+
event, so republishing a fresh one on every start burns that work and
|
|
319
|
+
churns the relay for nothing. The first two-machine run re-mined on
|
|
320
|
+
every service restart because nothing here read the file back. It is
|
|
321
|
+
reused only when it would still be published unchanged: valid under the
|
|
322
|
+
digest this node speaks, not yet inside its renewal window, and
|
|
323
|
+
describing the same class and endpoints the config now asks for.
|
|
324
|
+
"""
|
|
325
|
+
path = os.path.join(self.dir, self.DESCRIPTOR_FILE)
|
|
326
|
+
if not os.path.exists(path):
|
|
327
|
+
return None
|
|
328
|
+
try:
|
|
329
|
+
with open(path, encoding='utf-8') as f:
|
|
330
|
+
d = json.load(f)
|
|
331
|
+
except (ValueError, OSError):
|
|
332
|
+
return None
|
|
333
|
+
now = self.clock()
|
|
334
|
+
if identity.validate_descriptor(d, now=now, digest=descriptor.DIGEST):
|
|
335
|
+
return None
|
|
336
|
+
exp = int([t for t in d['tags'] if t[0] == 'expiration'][0][1])
|
|
337
|
+
if now >= exp - (exp - d['created_at']) // 10:
|
|
338
|
+
return None
|
|
339
|
+
want = [['endpoint', s, a, str(p)] for s, a, p in sorted(self._endpoints(), key=lambda e: (int(e[2]), e[0]))]
|
|
340
|
+
if [t for t in d['tags'] if t[0] == 'endpoint'] != want:
|
|
341
|
+
return None
|
|
342
|
+
cls = [t[1] for t in d['tags'] if t[0] == 'class'][0]
|
|
343
|
+
# Optional protocols (the sidecar capabilities) are declared by the
|
|
344
|
+
# compartment that verifies them, after start, and are not config:
|
|
345
|
+
# they neither qualify nor disqualify a reuse here. `sidecars` brings
|
|
346
|
+
# the descriptor into agreement with the live self-tests itself.
|
|
347
|
+
optional = set(identity.CONSTANTS['descriptor'].get('optional_protocols', ()))
|
|
348
|
+
if cls != self.cfg['class'] or identity.protocols_of(d) - optional != set(self.cfg['protocols']) | set(identity.REQUIRED_PROTOCOLS):
|
|
349
|
+
return None
|
|
350
|
+
# The operator is the declaration the independence model reads, and
|
|
351
|
+
# the plain capabilities are what a leaf searches for. Both are
|
|
352
|
+
# config; a change to either is a new descriptor, not a reused one.
|
|
353
|
+
operator = [t[1] for t in d['tags'] if t[0] == 'operator']
|
|
354
|
+
if operator != [self.cfg.get('operator')]:
|
|
355
|
+
return None
|
|
356
|
+
hints = {t[1] for t in d['tags'] if t[0] == 'c' and len(t) > 1 and not t[1].startswith('https://')}
|
|
357
|
+
if hints != set(self.cfg.get('capabilities') or ()):
|
|
358
|
+
return None
|
|
359
|
+
self.descriptor = d
|
|
360
|
+
return d
|
|
361
|
+
|
|
362
|
+
def _descriptor(self) -> NostrEvent:
|
|
363
|
+
"""The descriptor once start() has loaded or mined it. Reaching for it
|
|
364
|
+
earlier is a programming error, not a state the node should paper over."""
|
|
365
|
+
if self.descriptor is None:
|
|
366
|
+
raise RuntimeError('the descriptor is not built yet; start() mines or loads it')
|
|
367
|
+
return self.descriptor
|
|
368
|
+
|
|
369
|
+
def build_descriptor(self) -> NostrEvent:
|
|
370
|
+
# Any built compartment may add capability identifiers (`c` tags) and
|
|
371
|
+
# descriptor tags: sidecars declare the protocols they front, paid_fetch
|
|
372
|
+
# its prices, a plugin the dialect it speaks. Before start() assembles
|
|
373
|
+
# the compartments none have anything to add, which is also right.
|
|
374
|
+
protocols = list(self.cfg['protocols'])
|
|
375
|
+
extra_tags: list[list[str]] = []
|
|
376
|
+
subs = getattr(self, 'subsystems', None)
|
|
377
|
+
for sub in (subs or ()):
|
|
378
|
+
add = getattr(sub, 'capability_protocols', None)
|
|
379
|
+
if callable(add):
|
|
380
|
+
protocols += [p for p in add() if p not in protocols]
|
|
381
|
+
tags = getattr(sub, 'descriptor_tags', None)
|
|
382
|
+
if callable(tags):
|
|
383
|
+
extra_tags += list(tags())
|
|
384
|
+
capabilities = list(self.cfg.get('capabilities', ()))
|
|
385
|
+
if getattr(self, 'settings', None) is not None and not self.settings.protocol_suites.nostr.enabled:
|
|
386
|
+
capabilities = [c for c in capabilities if c not in ('relay', 'negentropy', 'blossom')]
|
|
387
|
+
if getattr(self, 'protocols', None) is not None:
|
|
388
|
+
for tag in self.protocols.descriptor_capabilities():
|
|
389
|
+
tag_list = list(tag)
|
|
390
|
+
if len(tag_list) == 2 and tag_list[0] == 'c' and tag_list[1] in capabilities:
|
|
391
|
+
continue
|
|
392
|
+
if tag_list not in extra_tags:
|
|
393
|
+
extra_tags.append(tag_list)
|
|
394
|
+
self.descriptor = descriptor.build(self.ks, self.cfg['class'], self._endpoints(), self.cfg['operator'],
|
|
395
|
+
protocols, now=self.clock(),
|
|
396
|
+
capabilities=capabilities, bits=self.cfg.get('pow_bits'),
|
|
397
|
+
extra_tags=extra_tags)
|
|
398
|
+
path = os.path.join(self.dir, self.DESCRIPTOR_FILE)
|
|
399
|
+
tmp = path + '.tmp'
|
|
400
|
+
with open(tmp, 'w', encoding='utf-8') as f:
|
|
401
|
+
json.dump(self.descriptor, f, indent=2)
|
|
402
|
+
os.replace(tmp, path)
|
|
403
|
+
return self.descriptor
|
|
404
|
+
|
|
405
|
+
def redescribe(self) -> NostrEvent:
|
|
406
|
+
"""Rebuild the descriptor from what the compartments say NOW and
|
|
407
|
+
publish it. For a compartment whose declaration changed after start
|
|
408
|
+
(a sidecar's self-tests began passing, or stopped): the kind 11801
|
|
409
|
+
must say what is true, and a replaceable event costs one proof of
|
|
410
|
+
work to correct - cheaper than a leaf dialling a protocol nothing
|
|
411
|
+
here answers."""
|
|
412
|
+
d = self.build_descriptor()
|
|
413
|
+
if self.relays:
|
|
414
|
+
relay.publish_all(self.relays, d, self.timeout)
|
|
415
|
+
return d
|
|
416
|
+
|
|
417
|
+
def start(self) -> NodeApp:
|
|
418
|
+
"""Bring the node up. All or nothing: a start that raises leaves no
|
|
419
|
+
bound face, no running transport and no started compartment behind.
|
|
420
|
+
|
|
421
|
+
Audit L1, option c - both halves:
|
|
422
|
+
|
|
423
|
+
a. `_start` puts every point that can refuse - assemble, wire, the
|
|
424
|
+
plugin-origin check, route, and each compartment's start() -
|
|
425
|
+
before anything another machine can see. The face starts serving
|
|
426
|
+
only after the router exists, and a freshly mined descriptor is
|
|
427
|
+
published only after Registry.start returns. (The face's socket
|
|
428
|
+
is bound when the RelayServer is constructed, which is earlier;
|
|
429
|
+
the `from_context` readers hold that object and read its port
|
|
430
|
+
lazily, so none of them needs it serving.)
|
|
431
|
+
b. This wrapper is the backstop. With Registry.start's own unwinding,
|
|
432
|
+
a refused start releases its port at once rather than at process
|
|
433
|
+
exit, and no compartment that started is left running.
|
|
434
|
+
|
|
435
|
+
A refused start therefore publishes nothing. What it mined stays on
|
|
436
|
+
disk under PENDING_FILE and is published by the next start that
|
|
437
|
+
succeeds.
|
|
438
|
+
"""
|
|
439
|
+
try:
|
|
440
|
+
return self._start()
|
|
441
|
+
except BaseException:
|
|
442
|
+
self._abandon_start()
|
|
443
|
+
raise
|
|
444
|
+
|
|
445
|
+
def _abandon_start(self) -> None:
|
|
446
|
+
"""Release whatever a failed start had taken. Every step is guarded:
|
|
447
|
+
the raise that brought us here is the one the operator must see."""
|
|
448
|
+
self._stop.set()
|
|
449
|
+
steps: list[Callable[[], Any]] = []
|
|
450
|
+
if getattr(self, 'control', None) is not None:
|
|
451
|
+
steps.append(self.control.stop)
|
|
452
|
+
if self._built:
|
|
453
|
+
steps.append(self.subsystems.stop)
|
|
454
|
+
steps += [self.transport.stop]
|
|
455
|
+
if self.face is not None:
|
|
456
|
+
steps.append(self.face.stop)
|
|
457
|
+
if getattr(self, 'protocols', None) is not None:
|
|
458
|
+
steps.append(self.protocols.stop_all)
|
|
459
|
+
if getattr(self, 'host', None) is not None:
|
|
460
|
+
steps.append(self.host.stop)
|
|
461
|
+
for step in steps:
|
|
462
|
+
try:
|
|
463
|
+
step()
|
|
464
|
+
except Exception as e:
|
|
465
|
+
_line(' while abandoning the start:', '%s: %s' % (type(e).__name__, e))
|
|
466
|
+
self._stopped = True
|
|
467
|
+
|
|
468
|
+
def _start(self) -> NodeApp:
|
|
469
|
+
_line('starting', self.ks.did[:22] + '…', 'class', self.cfg['class'])
|
|
470
|
+
pending = os.path.join(self.dir, self.PENDING_FILE)
|
|
471
|
+
if self.descriptor is None and self.load_descriptor() is None:
|
|
472
|
+
mined = self.build_descriptor()
|
|
473
|
+
with open(pending, 'w', encoding='utf-8') as f:
|
|
474
|
+
f.write(mined['id'] + '\n')
|
|
475
|
+
_line(' mined a descriptor', mined['id'][:12] + '…',
|
|
476
|
+
'(%d bits)' % identity.pow_bits(mined['id']))
|
|
477
|
+
elif os.path.exists(pending):
|
|
478
|
+
_line(' reusing a descriptor no relay has accepted yet',
|
|
479
|
+
self._descriptor()['id'][:12] + '…')
|
|
480
|
+
else:
|
|
481
|
+
_line(' reusing the descriptor it already published',
|
|
482
|
+
self._descriptor()['id'][:12] + '…')
|
|
483
|
+
# The compartments, built from one context in the declared tick order.
|
|
484
|
+
# Each class says what it needs (`requires`) and the assembler refuses
|
|
485
|
+
# a manifest that contradicts it; what each one takes from the node is
|
|
486
|
+
# in its own `from_context`, not here. See MANIFEST for the order and
|
|
487
|
+
# the reasons behind it.
|
|
488
|
+
self.subsystems = compartment_mod.Registry(say=self.says.say)
|
|
489
|
+
self.context.descriptor = self._descriptor
|
|
490
|
+
self.context.digest = descriptor.DIGEST
|
|
491
|
+
self.manifest = load_plugins(self.settings.plugins.specs, MANIFEST)
|
|
492
|
+
self.subsystems.assemble(self.context, self.manifest)
|
|
493
|
+
# Every edge that runs against tick order (link records into strain,
|
|
494
|
+
# peering refuses through capacity, budget reports through the
|
|
495
|
+
# reporter, the face asks paid_fetch for a price ...) is named by the
|
|
496
|
+
# compartment that owns it, in its `wire()`, and reached here once
|
|
497
|
+
# the whole manifest exists. The node no longer holds that list.
|
|
498
|
+
self.subsystems.wire(self.context)
|
|
499
|
+
# Every compartment's declared channels and metrics, in one place. A
|
|
500
|
+
# rejected metric is kept in the registry's state rather than raised:
|
|
501
|
+
# one malformed declaration must not stop a node booting, and it is
|
|
502
|
+
# reported by `meshnode health` instead of vanishing.
|
|
503
|
+
self.says.register_from(list(self.subsystems))
|
|
504
|
+
self.monitors.register_from(list(self.subsystems))
|
|
505
|
+
# And the lifecycle's own: every compartment moves through the same
|
|
506
|
+
# machine, so its transitions are the registry's to say and count.
|
|
507
|
+
self.says.register(lifecycle_mod.CHANNELS)
|
|
508
|
+
self.monitors.register(self.subsystems.lifecycle_metrics(), owner='lifecycle')
|
|
509
|
+
# And every declared store's size, bound and counts, labelled by
|
|
510
|
+
# compartment: memory metered without a compartment writing a metric.
|
|
511
|
+
self.monitors.register(self.subsystems.store_metrics(), owner='stores')
|
|
512
|
+
self.settle_engine: settlement_mod.SettlementEngine = self.settlement.engine
|
|
513
|
+
self.mediator: mediation_mod.MediatorService = self.mediation.service
|
|
514
|
+
|
|
515
|
+
# The router is a dumb ladder: each compartment declares the
|
|
516
|
+
# protocols it speaks (`piuris`, `exact_types`) and the registry
|
|
517
|
+
# puts its `handle` behind them. A plugin speaks its own origin; one
|
|
518
|
+
# that claims fleetmesh.org's is refused before it is routed. One
|
|
519
|
+
# that fronts a sidecar declares its manifest(s) as `sidecars`
|
|
520
|
+
# (dicts, or '@/path.json'); they join the registry before the
|
|
521
|
+
# sidecars compartment runs its first self-test below.
|
|
522
|
+
for cls in self.manifest:
|
|
523
|
+
if cls in MANIFEST:
|
|
524
|
+
continue
|
|
525
|
+
for piuri in getattr(cls, 'piuris', ()) or ():
|
|
526
|
+
if piuri.startswith(identity.FLEETMESH):
|
|
527
|
+
raise ValueError('plugin %s claims fleetmesh.org protocol %s; plugins speak their own origin' % (cls.name, piuri))
|
|
528
|
+
for sc in plugin_sidecars(cls):
|
|
529
|
+
self.sidecars.declare(sc)
|
|
530
|
+
self.router = router_mod.ProtocolRouter()
|
|
531
|
+
self.subsystems.route(self.router)
|
|
532
|
+
# What the last process left for the compartments that declare
|
|
533
|
+
# `snapshot`, handed back before any of them starts. Never raises; a
|
|
534
|
+
# refusal is recorded on the compartment's lifecycle and it starts
|
|
535
|
+
# fresh. The file is discarded only once the start succeeds, so a
|
|
536
|
+
# refused start does not cost the next one its continuity.
|
|
537
|
+
saved, saved_at, why_not = lifecycle_mod.read_snapshots(self.dir, self.clock())
|
|
538
|
+
if why_not:
|
|
539
|
+
_line(' ' + why_not)
|
|
540
|
+
for name, what in self.subsystems.restore(saved, saved_at).items():
|
|
541
|
+
_line(' %s: %s' % (name, what))
|
|
542
|
+
# Everything above can refuse and none of it is visible off this
|
|
543
|
+
# machine. From here on the node starts being seen: the face serves
|
|
544
|
+
# (before the compartments start, because some of them dial it), the
|
|
545
|
+
# transport subscribes, and - once they have started - the
|
|
546
|
+
# descriptor is published.
|
|
547
|
+
self.host.start()
|
|
548
|
+
self.protocols.start_all()
|
|
549
|
+
if self.face is not None:
|
|
550
|
+
self.face.start()
|
|
551
|
+
self.strain.record('restart')
|
|
552
|
+
self.transport.start()
|
|
553
|
+
wait_listen_timeout = float(self.cfg.get('wait_listening_timeout', 5.0))
|
|
554
|
+
if self.relays and wait_listen_timeout > 0 and not self.transport.wait_listening(wait_listen_timeout):
|
|
555
|
+
# Said, not waited on for ever. Until a relay acknowledges the
|
|
556
|
+
# subscription this node cannot receive a live wrap and would not
|
|
557
|
+
# know it had missed one, because kind 21059 is ephemeral and no
|
|
558
|
+
# relay keeps a copy. Starting anyway is right - the stored kind
|
|
559
|
+
# replays, the compartments have work that needs no inbox, and
|
|
560
|
+
# `link` will keep saying so - but starting silently is not.
|
|
561
|
+
_line(' no relay has acknowledged the inbox subscription yet;',
|
|
562
|
+
'live messages sent before it does are lost')
|
|
563
|
+
if not self.settings.face.port and not self.settings.protocol_suites.host.port:
|
|
564
|
+
# Port 0 asks the kernel for any free port, which is right for a test
|
|
565
|
+
# and wrong for a node an operator runs: the relay face moved on every
|
|
566
|
+
# restart, so every client holding its address lost it. Record what we
|
|
567
|
+
# were given, once.
|
|
568
|
+
port_assigned = self.host.port if self.host is not None else (self.face.server.port if self.face and self.face.server else 0)
|
|
569
|
+
self.cfg['face'] = dict(self.cfg.get('face') or {}, port=port_assigned)
|
|
570
|
+
self.save_coverage()
|
|
571
|
+
_line(' recorded the edge face port', port_assigned, 'so it survives a restart')
|
|
572
|
+
edge_url = self.face.url if self.face is not None else self.host.url
|
|
573
|
+
_line(' edge face', edge_url, '| relays', ', '.join(self.relays) or '(none)',
|
|
574
|
+
'| descriptor', self._descriptor()['id'][:12] + '…')
|
|
575
|
+
if not self.cfg['endpoints']:
|
|
576
|
+
_line(' no endpoint configured: reachable over the nostr transport only')
|
|
577
|
+
_line(' peers configured:', len(self.cfg.get('peers', [])),
|
|
578
|
+
'| grants held:', len(self.grants.list()))
|
|
579
|
+
# The registry's own lifecycle verb, which nothing called: presence
|
|
580
|
+
# holds the heartbeat subscription open and had no way to say so.
|
|
581
|
+
# It unwinds itself when a required compartment cannot start, so
|
|
582
|
+
# `_built` - which tells stop() to stop the compartments - is set only
|
|
583
|
+
# once they have (audit L1: it used to be set before, and a failed
|
|
584
|
+
# start then sent the budget's farewell for a node that never served).
|
|
585
|
+
self.subsystems.start()
|
|
586
|
+
self._built = True
|
|
587
|
+
lifecycle_mod.discard_snapshots(self.dir)
|
|
588
|
+
self._publish_pending(pending)
|
|
589
|
+
self.control = control_mod.Server(self.dir, self.control_request).start()
|
|
590
|
+
threading.Thread(target=self._serve, daemon=True).start()
|
|
591
|
+
sd_notify('READY=1\nSTATUS=Fleetmesh node active')
|
|
592
|
+
self._start_watchdog()
|
|
593
|
+
return self
|
|
594
|
+
|
|
595
|
+
def _publish_pending(self, pending: str) -> None:
|
|
596
|
+
"""Publish a descriptor no relay has accepted yet, now that the node
|
|
597
|
+
has really started. The marker goes only when a relay accepted it: a
|
|
598
|
+
node with no relays configured, or none reachable, publishes on the
|
|
599
|
+
next start instead of believing it already had."""
|
|
600
|
+
if not os.path.exists(pending) or not self.relays:
|
|
601
|
+
return
|
|
602
|
+
out = relay.publish_all(self.relays, self._descriptor())
|
|
603
|
+
if any(ok for ok, _ in out.values()):
|
|
604
|
+
os.unlink(pending)
|
|
605
|
+
else:
|
|
606
|
+
_line(' no relay accepted the descriptor yet; the next start publishes it')
|
|
607
|
+
|
|
608
|
+
def _start_watchdog(self) -> None:
|
|
609
|
+
watchdog_usec = os.environ.get('WATCHDOG_USEC')
|
|
610
|
+
if watchdog_usec:
|
|
611
|
+
try:
|
|
612
|
+
usec = int(watchdog_usec)
|
|
613
|
+
if usec > 0:
|
|
614
|
+
interval = (usec / 1_000_000.0) / 2.0
|
|
615
|
+
threading.Thread(target=self._watchdog_loop, args=(interval,), daemon=True).start()
|
|
616
|
+
except (ValueError, TypeError):
|
|
617
|
+
pass
|
|
618
|
+
|
|
619
|
+
def _watchdog_loop(self, interval: float) -> None:
|
|
620
|
+
while not self._stop.is_set():
|
|
621
|
+
if self._stop.wait(interval):
|
|
622
|
+
break
|
|
623
|
+
sd_notify('WATCHDOG=1')
|
|
624
|
+
|
|
625
|
+
def run(self) -> None:
|
|
626
|
+
"""Tick on the configured cadence until stop()."""
|
|
627
|
+
while not self._stop.is_set():
|
|
628
|
+
try:
|
|
629
|
+
for d in self.tick():
|
|
630
|
+
_line(' ', ' '.join(_short(x) for x in d))
|
|
631
|
+
except Exception as e: # one bad tick must not stop the node
|
|
632
|
+
self.log.append(('tick failed', str(e)))
|
|
633
|
+
self.counts['tick failed'] += 1
|
|
634
|
+
self._last_tick_error = '%s: %s' % (type(e).__name__, e)
|
|
635
|
+
_line(' tick failed:', e)
|
|
636
|
+
self._stop.wait(self.cfg.get('tick_seconds', 30))
|
|
637
|
+
|
|
638
|
+
def stop(self) -> None:
|
|
639
|
+
# Once. `meshnode run` stops from the SIGTERM handler and again from
|
|
640
|
+
# its finally:, and each pass said goodbye: every peer got the same
|
|
641
|
+
# window's farewell at two sequence numbers a few seconds apart, and
|
|
642
|
+
# whichever end saw the second first read the first as a receipt the
|
|
643
|
+
# wire had dropped. The three-continent mesh reported that gap on two
|
|
644
|
+
# of three nodes after its first restart.
|
|
645
|
+
if self._stopped:
|
|
646
|
+
return
|
|
647
|
+
self._stopped = True
|
|
648
|
+
sd_notify('STOPPING=1\nSTATUS=Fleetmesh node shutting down')
|
|
649
|
+
self._stop.set()
|
|
650
|
+
# Before the transport goes, tell every peer what this node used. A node
|
|
651
|
+
# that simply vanishes leaves its issuer with traffic measured and no
|
|
652
|
+
# receipt, which is active silence and an S2 - and an S2 drops the tier
|
|
653
|
+
# to probation, which only a person can raise again. That made the
|
|
654
|
+
# fleet-wide restart a digest change requires cost every peering its
|
|
655
|
+
# tier, every time.
|
|
656
|
+
if getattr(self, '_built', False):
|
|
657
|
+
# The registry stops every compartment, and the budget's stop is the
|
|
658
|
+
# farewell. Calling it here as well sent every peer two receipts for
|
|
659
|
+
# the same window at two sequence numbers - valid, and pointless.
|
|
660
|
+
for name, did in self.subsystems.stop():
|
|
661
|
+
for d in (did or []):
|
|
662
|
+
_line(' ' + ' '.join(str(x) for x in d))
|
|
663
|
+
# After stop(), so the snapshot is what each compartment ended
|
|
664
|
+
# with. Only on a clean stop: a process that died wrote nothing,
|
|
665
|
+
# and the next one starts its compartments fresh.
|
|
666
|
+
snaps = self.subsystems.snapshot()
|
|
667
|
+
if snaps:
|
|
668
|
+
try:
|
|
669
|
+
lifecycle_mod.write_snapshots(self.dir, self.clock(), snaps)
|
|
670
|
+
_line(' kept the state of', ', '.join(snaps), 'for the next start')
|
|
671
|
+
except OSError as e:
|
|
672
|
+
_line(' could not keep compartment state:', '%s: %s' % (type(e).__name__, e))
|
|
673
|
+
if getattr(self, 'control', None) is not None:
|
|
674
|
+
self.control.stop()
|
|
675
|
+
self.transport.stop()
|
|
676
|
+
if self.face is not None:
|
|
677
|
+
self.face.stop()
|
|
678
|
+
if getattr(self, 'protocols', None) is not None:
|
|
679
|
+
self.protocols.stop_all()
|
|
680
|
+
if getattr(self, 'host', None) is not None:
|
|
681
|
+
self.host.stop()
|
|
682
|
+
|
|
683
|
+
def close(self) -> None:
|
|
684
|
+
"""Safely stop node workers and close all persistent database connections.
|
|
685
|
+
|
|
686
|
+
A close that fails is counted and said. The four stores here are shut
|
|
687
|
+
in turn and a failure in one must not leave the other three open, so
|
|
688
|
+
the exception is still caught - but it is no longer discarded: a store
|
|
689
|
+
that cannot close (a locked database, a disk full at commit) is
|
|
690
|
+
exactly the one an operator needs told about at restart, and
|
|
691
|
+
`except Exception: pass` told nobody (audit 2026-10-07, L5)."""
|
|
692
|
+
self.stop()
|
|
693
|
+
for name in ('store', 'accounts', 'grants', 'blobs', 'video'):
|
|
694
|
+
st = getattr(self, name, None)
|
|
695
|
+
if st is not None and hasattr(st, 'close'):
|
|
696
|
+
try:
|
|
697
|
+
st.close()
|
|
698
|
+
except Exception as e:
|
|
699
|
+
self.log.append(('close failed', '%s: %s' % (name, e)))
|
|
700
|
+
self.counts['close failed'] += 1
|
|
701
|
+
_line(' closing %s failed:' % name, '%s: %s' % (type(e).__name__, e))
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# ---- inbound -------------------------------------------------------------------
|
|
705
|
+
def _say_once(self, key: str, *parts: Any, every: float = 60) -> None:
|
|
706
|
+
"""One log line per key per minute. A peer that sends the same
|
|
707
|
+
malformed message forever must not own the journal."""
|
|
708
|
+
now = time.time()
|
|
709
|
+
if now - self._last_said.get(key, 0) < every:
|
|
710
|
+
return
|
|
711
|
+
self._last_said[key] = now
|
|
712
|
+
if len(self._last_said) > 500:
|
|
713
|
+
self._last_said = {k: v for k, v in self._last_said.items() if now - v < every}
|
|
714
|
+
_line(*parts)
|
|
715
|
+
|
|
716
|
+
def _serve(self) -> None:
|
|
717
|
+
while not self._stop.is_set():
|
|
718
|
+
inbound = self.transport.receive(timeout=0.5)
|
|
719
|
+
if inbound is None:
|
|
720
|
+
continue
|
|
721
|
+
# The door. A message without the shape of its type is dropped here
|
|
722
|
+
# with a reason, counted, and never reaches a handler; what a handler
|
|
723
|
+
# had done before it raised on a bad field used to stay done.
|
|
724
|
+
why = validate.check(inbound.message)
|
|
725
|
+
if why is not None:
|
|
726
|
+
self.rejected[why] += 1
|
|
727
|
+
self._say_once('rejected:' + why, '! rejected', _short(inbound.sender),
|
|
728
|
+
str(inbound.message.get('type'))[-40:], '-', why)
|
|
729
|
+
continue
|
|
730
|
+
try:
|
|
731
|
+
done = self.router.dispatch(inbound)
|
|
732
|
+
except Exception as e:
|
|
733
|
+
self.log.append(('handler failed', inbound.message.get('type'), str(e)))
|
|
734
|
+
self.counts['handler failed'] += 1
|
|
735
|
+
self._say_once('handler:' + str(inbound.message.get('type')),
|
|
736
|
+
'! handler failed on', inbound.message.get('type'), '-', e)
|
|
737
|
+
done = ('error',)
|
|
738
|
+
if done is not None:
|
|
739
|
+
self.log.append(done)
|
|
740
|
+
if done[0] not in ('ignored',):
|
|
741
|
+
_line('<', ' '.join(_short(x) for x in done[:3]))
|
|
742
|
+
|
|
743
|
+
# What an operator may change while the node runs. The rest - class, keys,
|
|
744
|
+
# relays - is what the node is. Every live key belongs to the compartment
|
|
745
|
+
# that reads it, and this is what is left over: nothing.
|
|
746
|
+
LIVE_KEYS: tuple[str, ...] = ()
|
|
747
|
+
|
|
748
|
+
def reload_config(self) -> None:
|
|
749
|
+
"""Take up whatever the operator has written since the last tick.
|
|
750
|
+
|
|
751
|
+
Every command writes the config and nothing else, because a running node
|
|
752
|
+
owns its directory and its relay face port and a second process cannot
|
|
753
|
+
stand one up beside it. So the file is the interface, and it is read in
|
|
754
|
+
one place: `trust` was being re-read and `coverage` was not, which meant
|
|
755
|
+
a shard added to the file was never claimed until the node restarted.
|
|
756
|
+
Neither is named here now. A compartment declares the keys it re-reads
|
|
757
|
+
and the registry collects them, so the list cannot fall behind the
|
|
758
|
+
mechanism that depends on it.
|
|
759
|
+
"""
|
|
760
|
+
try:
|
|
761
|
+
fresh = load_config(self.dir) or {}
|
|
762
|
+
except (OSError, ValueError) as e:
|
|
763
|
+
# The last good config stays in force. Said once a minute, not once
|
|
764
|
+
# a tick, and shown in status so an operator finds it without the log.
|
|
765
|
+
self._config_error = str(e)
|
|
766
|
+
self.counts['config rejected'] += 1
|
|
767
|
+
self._say_once('config', '! config.json is not usable, keeping the last good one:', e)
|
|
768
|
+
return
|
|
769
|
+
self._config_error = None
|
|
770
|
+
for key in tuple(self.LIVE_KEYS) + self.subsystems.live_keys():
|
|
771
|
+
if key in fresh:
|
|
772
|
+
self.cfg[key] = fresh[key]
|
|
773
|
+
if self._built and self.capacity.set_total(self.cfg.get('capacity')):
|
|
774
|
+
_line(' aggregate ceiling is now',
|
|
775
|
+
', '.join('%s %s' % kv for kv in sorted(self.capacity.total.items()))
|
|
776
|
+
or 'undeclared - this node will accept load until it fails')
|
|
777
|
+
if self._built:
|
|
778
|
+
self._reconfigure(fresh)
|
|
779
|
+
|
|
780
|
+
def _reconfigure(self, fresh: Mapping[str, Any]) -> list[str]:
|
|
781
|
+
"""Take up a changed config block that is not live, by restarting its owner.
|
|
782
|
+
|
|
783
|
+
A live key is copied above and its compartment re-reads it. Any other
|
|
784
|
+
block used to wait silently for the next node restart. Now, when the
|
|
785
|
+
compartment that owns it declares `Lifecycle(restart=True)`, the new
|
|
786
|
+
block is parsed first - the running settings stay if it does not
|
|
787
|
+
parse - then adopted, and the compartment restarted in place. One that
|
|
788
|
+
does not declare restart keeps the running settings and the operator
|
|
789
|
+
is told, once a minute, that the edit waits for the node.
|
|
790
|
+
|
|
791
|
+
Returns the names restarted.
|
|
792
|
+
"""
|
|
793
|
+
done: list[str] = []
|
|
794
|
+
for s in self.subsystems:
|
|
795
|
+
blk = getattr(s, 'config_cls', None)
|
|
796
|
+
own = getattr(blk, 'key', '') if blk is not None else ''
|
|
797
|
+
if blk is None or not own or own in s.live or fresh.get(own) == self.cfg.get(own):
|
|
798
|
+
continue
|
|
799
|
+
name = s.name or ''
|
|
800
|
+
if not self.subsystems.restartable(name):
|
|
801
|
+
# Once per edit: the running settings are right until the
|
|
802
|
+
# node restarts, so this is news when it changes, not weather.
|
|
803
|
+
if self._waiting.get(own) != fresh.get(own):
|
|
804
|
+
self._waiting[own] = fresh.get(own)
|
|
805
|
+
_line(' %s changed in config.json;' % own, name, 'takes it up when the node restarts')
|
|
806
|
+
continue
|
|
807
|
+
try:
|
|
808
|
+
blk.from_dict(fresh.get(own) or {})
|
|
809
|
+
except Exception as e:
|
|
810
|
+
self._say_once('config:' + own, '! %s in config.json is not usable, keeping the running one:' % own, e)
|
|
811
|
+
continue
|
|
812
|
+
if own in fresh:
|
|
813
|
+
self.cfg[own] = fresh[own]
|
|
814
|
+
else:
|
|
815
|
+
self.cfg.pop(own, None)
|
|
816
|
+
state = self.subsystems.restart(name, 'its %s block changed' % own)
|
|
817
|
+
_line(' %s restarted for its new %s settings:' % (name, own), state)
|
|
818
|
+
done.append(name)
|
|
819
|
+
return done
|
|
820
|
+
|
|
821
|
+
def tick(self, now: int | None = None) -> list[tuple[Any, ...]]:
|
|
822
|
+
with self._tick_lock:
|
|
823
|
+
return self._tick(now)
|
|
824
|
+
|
|
825
|
+
def _tick(self, now: int | None = None) -> list[tuple[Any, ...]]:
|
|
826
|
+
"""One tick: take up the config, run every compartment, write the status.
|
|
827
|
+
|
|
828
|
+
This was a hundred and thirty lines with five mechanisms in it. What is
|
|
829
|
+
left is the three things the node itself owns - the config file, the
|
|
830
|
+
registry, and status.json - and one line about clock drift, which is
|
|
831
|
+
read here because it is the operator's to act on at both ends and not
|
|
832
|
+
one node's to fix.
|
|
833
|
+
"""
|
|
834
|
+
now = now if now is not None else self.clock()
|
|
835
|
+
seen_drift = len(self.budget.clock_drift)
|
|
836
|
+
self.reload_config()
|
|
837
|
+
did = self.subsystems.tick(now)
|
|
838
|
+
self._last_tick = did
|
|
839
|
+
for peer, epoch, drift, _at in self.budget.clock_drift[seen_drift:]:
|
|
840
|
+
_line(' a receipt from', _short(peer), 'is timed %ds from this clock' % drift,
|
|
841
|
+
'- check NTP on both ends before reading any divergence')
|
|
842
|
+
self.write_status(did)
|
|
843
|
+
sd_notify('WATCHDOG=1')
|
|
844
|
+
return did
|
|
845
|
+
|
|
846
|
+
# ---- status --------------------------------------------------------------------
|
|
847
|
+
PLANES: tuple[tuple[str, str, tuple[str, ...]], ...] = _VIEW_PLANES
|
|
848
|
+
|
|
849
|
+
def status(self) -> Mapping[str, Any]:
|
|
850
|
+
return node_status(self)
|
|
851
|
+
|
|
852
|
+
def health(self) -> Mapping[str, Any]:
|
|
853
|
+
"""What is failing, and how often: the operator's view of every error
|
|
854
|
+
plane the node has. Nothing here is a finding about a peer."""
|
|
855
|
+
return node_health(self)
|
|
856
|
+
|
|
857
|
+
def write_status(self, did: Sequence[tuple[Any, ...]] | None = None) -> Mapping[str, Any]:
|
|
858
|
+
if did:
|
|
859
|
+
self._last_tick = did
|
|
860
|
+
st = self.status()
|
|
861
|
+
path = os.path.join(self.dir, 'status.json')
|
|
862
|
+
tmp = path + '.tmp'
|
|
863
|
+
with open(tmp, 'w', encoding='utf-8') as f:
|
|
864
|
+
json.dump(st, f, indent=2)
|
|
865
|
+
os.replace(tmp, path)
|
|
866
|
+
return st
|
|
867
|
+
|