A Python port of the Invisible Internet Project (I2P)
1"""CLI entry point for the I2P router."""
2
3import argparse
4import asyncio
5import logging
6import os
7import signal
8import sys
9
10from i2p_router.bootstrap import RouterBootstrap
11from i2p_router.config import RouterConfig
12
13
14def main() -> None:
15 parser = argparse.ArgumentParser(description="I2P Python Router")
16 parser.add_argument("--host", default="0.0.0.0", help="Listen host (default: 0.0.0.0)")
17 parser.add_argument("--port", type=int, default=9700, help="NTCP2 listen port (default: 9700)")
18 parser.add_argument("--health-port", type=int, default=9701, help="Health endpoint port (default: 9701)")
19 parser.add_argument("--data-dir", default="~/.i2p-python", help="Data directory (default: ~/.i2p-python)")
20 parser.add_argument("--reseed", action="store_true", help="Force reseed on start")
21 parser.add_argument(
22 "--setup",
23 nargs="?",
24 const="auto",
25 metavar="MODE",
26 help="Run first-run setup wizard (modes: auto, paranoid, performance, interactive)",
27 )
28 parser.add_argument(
29 "--log-level",
30 default="INFO",
31 choices=["DEBUG", "INFO", "WARNING", "ERROR"],
32 help="Log level (default: INFO)",
33 )
34 args = parser.parse_args()
35
36 # Console logging
37 log_level = getattr(logging, args.log_level)
38 log_format = "%(asctime)s %(levelname)-8s %(name)s: %(message)s"
39 root_logger = logging.getLogger()
40 root_logger.setLevel(log_level)
41
42 console_handler = logging.StreamHandler(sys.stdout)
43 console_handler.setFormatter(logging.Formatter(log_format))
44 root_logger.addHandler(console_handler)
45
46 # File logging (always DEBUG for post-mortem analysis)
47 data_dir = os.path.expanduser(args.data_dir)
48 os.makedirs(data_dir, exist_ok=True)
49 log_path = os.path.join(data_dir, "router.log")
50 file_handler = logging.FileHandler(log_path)
51 file_handler.setLevel(logging.DEBUG)
52 file_handler.setFormatter(logging.Formatter(log_format))
53 root_logger.addHandler(file_handler)
54
55 log = logging.getLogger(__name__)
56
57 # Run setup wizard if --setup or first run
58 from i2p_apps.setup.wizard import SetupWizard
59 wizard = SetupWizard(data_dir=data_dir)
60 if args.setup:
61 mode = args.setup
62 if mode == "interactive":
63 wizard.run_interactive()
64 else:
65 wizard.run_auto(mode=mode)
66 log.info("Setup complete, config saved to %s", data_dir)
67 elif wizard.is_first_run():
68 log.info("First run detected — running auto setup")
69 wizard.run_auto(mode="normal")
70
71 log.info("I2P Python Router starting")
72 log.info(" NTCP2 port: %d", args.port)
73 log.info(" Health port: %d", args.health_port)
74 log.info(" Data dir: %s", args.data_dir)
75 log.info(" Log file: %s", log_path)
76 log.info(" Log level: %s (file always DEBUG)", args.log_level)
77
78 config = RouterConfig(
79 listen_host=args.host,
80 listen_port=args.port,
81 health_port=args.health_port,
82 data_dir=args.data_dir,
83 )
84 config = RouterConfig.with_env_overrides(config)
85
86 bootstrap = RouterBootstrap(config)
87
88 async def run() -> None:
89 await bootstrap.start()
90 log.info(
91 "Router running — NTCP2 on %s:%d, health on %s:%d",
92 args.host, args.port, args.host, args.health_port,
93 )
94 stop = asyncio.Event()
95 loop = asyncio.get_event_loop()
96 for sig in (signal.SIGTERM, signal.SIGINT):
97 loop.add_signal_handler(sig, stop.set)
98 await stop.wait()
99 log.info("Signal received, shutting down...")
100 await bootstrap.shutdown()
101
102 asyncio.run(run())
103
104
105if __name__ == "__main__":
106 main()