A Python port of the Invisible Internet Project (I2P)
1"""Main I2P Router — lifecycle management and state machine.
2
3Ported from net.i2p.router.Router.
4"""
5
6from __future__ import annotations
7
8import enum
9import time
10from dataclasses import asdict
11
12from i2p_router.config import RouterConfig
13from i2p_router.core import RouterContext
14
15
16class RouterState(enum.Enum):
17 """Router lifecycle states."""
18
19 STOPPED = "stopped"
20 STARTING = "starting"
21 RUNNING = "running"
22 SHUTTING_DOWN = "shutting_down"
23
24
25class Router:
26 """Main I2P router instance.
27
28 Manages the router lifecycle (start, run, shutdown) and owns
29 the RouterContext which wires together all subsystems.
30 """
31
32 def __init__(self, config: RouterConfig | None = None) -> None:
33 self._config = config or RouterConfig()
34 self._state = RouterState.STOPPED
35 self._context: RouterContext | None = None
36 self._started_at: float | None = None
37
38 @property
39 def state(self) -> RouterState:
40 """Current router state."""
41 return self._state
42
43 @property
44 def config(self) -> RouterConfig:
45 """Router configuration."""
46 return self._config
47
48 @property
49 def uptime_seconds(self) -> float:
50 """Seconds since the router was started, or 0.0 if stopped."""
51 if self._started_at is None:
52 return 0.0
53 return time.monotonic() - self._started_at
54
55 def start(self) -> None:
56 """Start the router.
57
58 1. If already RUNNING, this is a no-op (idempotent).
59 2. Validate config.
60 3. Transition to STARTING.
61 4. Create RouterContext and initialize subsystems.
62 5. Transition to RUNNING.
63
64 Raises
65 ------
66 ValueError
67 If the configuration is invalid. State remains STOPPED.
68 """
69 if self._state == RouterState.RUNNING:
70 return
71
72 # Validate before changing state
73 self._config.validate()
74
75 self._state = RouterState.STARTING
76
77 # Create the router context (wires all subsystems)
78 self._context = RouterContext()
79 self._started_at = time.monotonic()
80
81 self._state = RouterState.RUNNING
82
83 def shutdown(self) -> None:
84 """Gracefully shut down the router.
85
86 If already STOPPED, this is a no-op.
87 Transitions through SHUTTING_DOWN to STOPPED.
88 """
89 if self._state == RouterState.STOPPED:
90 return
91
92 self._state = RouterState.SHUTTING_DOWN
93
94 # Clean up
95 self._context = None
96 self._started_at = None
97
98 self._state = RouterState.STOPPED
99
100 def get_status(self) -> dict:
101 """Return router status including state, uptime, and config.
102
103 Returns
104 -------
105 dict
106 Status dictionary with state, uptime_seconds, and config fields.
107 """
108 return {
109 "state": self._state.value,
110 "uptime_seconds": self.uptime_seconds,
111 "config": asdict(self._config),
112 }