A Python port of the Invisible Internet Project (I2P)
at main 100 lines 3.1 kB view raw
1"""I2PTunnel controller — manages lifecycle of a single tunnel. 2 3Ported from net.i2p.i2ptunnel.TunnelController. 4""" 5 6from __future__ import annotations 7 8import enum 9import logging 10 11from i2p_apps.i2ptunnel.config import TunnelDefinition 12 13logger = logging.getLogger(__name__) 14 15 16class TunnelState(enum.Enum): 17 STOPPED = "stopped" 18 STARTING = "starting" 19 RUNNING = "running" 20 STOPPING = "stopping" 21 DESTROYED = "destroyed" 22 23 24class TunnelController: 25 """Manages the lifecycle of a single I2P tunnel. 26 27 State machine: STOPPED -> STARTING -> RUNNING -> STOPPING -> STOPPED 28 DESTROYED is terminal. 29 """ 30 31 def __init__(self, config: TunnelDefinition, session_factory) -> None: 32 self._config = config 33 self._session_factory = session_factory 34 self._state = TunnelState.STOPPED 35 self._session = None 36 self._tunnel_task = None 37 38 @property 39 def state(self) -> TunnelState: 40 return self._state 41 42 @property 43 def config(self) -> TunnelDefinition: 44 return self._config 45 46 @property 47 def session(self): 48 return self._session 49 50 async def start(self) -> None: 51 """Start the tunnel: create session and tunnel task.""" 52 if self._state == TunnelState.DESTROYED: 53 raise RuntimeError("Cannot start a destroyed tunnel") 54 if self._state == TunnelState.RUNNING: 55 return # already running 56 57 self._state = TunnelState.STARTING 58 logger.info("Starting tunnel %r", self._config.name) 59 60 try: 61 self._session = await self._session_factory.create( 62 self._config, 63 priv_key=self._config.priv_key_file or None, 64 ) 65 self._state = TunnelState.RUNNING 66 logger.info("Tunnel %r is RUNNING", self._config.name) 67 except Exception: 68 self._state = TunnelState.STOPPED 69 logger.exception("Failed to start tunnel %r", self._config.name) 70 raise 71 72 async def stop(self) -> None: 73 """Stop the tunnel: release session and clean up.""" 74 if self._state in (TunnelState.STOPPED, TunnelState.DESTROYED): 75 return # nothing to do 76 77 self._state = TunnelState.STOPPING 78 logger.info("Stopping tunnel %r", self._config.name) 79 80 if self._session is not None: 81 try: 82 await self._session_factory.release(self._session) 83 except Exception: 84 logger.exception("Error releasing session for tunnel %r", 85 self._config.name) 86 self._session = None 87 88 self._tunnel_task = None 89 self._state = TunnelState.STOPPED 90 logger.info("Tunnel %r is STOPPED", self._config.name) 91 92 async def restart(self) -> None: 93 """Stop then start the tunnel.""" 94 await self.stop() 95 await self.start() 96 97 def destroy(self) -> None: 98 """Mark tunnel as destroyed — cannot be restarted.""" 99 self._state = TunnelState.DESTROYED 100 logger.info("Tunnel %r DESTROYED", self._config.name)