| 
1 | 1 | # -*- coding: utf-8 -*-  | 
2 |  | -# (c) 2016-2021 Andreas Motl <[email protected]>  | 
 | 2 | +# (c) 2016-2023 Andreas Motl <[email protected]>  | 
 | 3 | +import dataclasses  | 
3 | 4 | import re  | 
4 | 5 | import json  | 
5 | 6 | import mimetypes  | 
@@ -48,61 +49,89 @@ def log(self, request):  | 
48 | 49 |         log.debug(line)  | 
49 | 50 | 
 
  | 
50 | 51 | 
 
  | 
 | 52 | +@dataclasses.dataclass  | 
 | 53 | +class HttpServerAddress:  | 
 | 54 | +    """  | 
 | 55 | +    Represent a typical host/port pair for configuring IP server listening addresses.  | 
 | 56 | +    Other than this, provide sensible factory and helper methods.  | 
 | 57 | +    """  | 
 | 58 | +    host: str  | 
 | 59 | +    port: int  | 
 | 60 | + | 
 | 61 | +    @classmethod  | 
 | 62 | +    def from_settings(cls, settings):  | 
 | 63 | +        return cls(host=settings.kotori.http_listen, port=int(settings.kotori.http_port))  | 
 | 64 | + | 
 | 65 | +    @property  | 
 | 66 | +    def combined(self):  | 
 | 67 | +        return f"{self.host}:{self.port}"  | 
 | 68 | + | 
 | 69 | +    @property  | 
 | 70 | +    def slug(self):  | 
 | 71 | +        return f"{self.host}-{self.port}"  | 
 | 72 | + | 
 | 73 | + | 
51 | 74 | class HttpServerService(Service):  | 
52 | 75 |     """  | 
53 |  | -    Singleton instance of a Twisted service wrapping  | 
54 |  | -    the Twisted TCP/HTTP server object "Site", in turn  | 
55 |  | -    obtaining a ``HttpChannelContainer`` as root resource.  | 
 | 76 | +    A Twisted service for managing multiple Twisted TCP/HTTP `Site` server objects,  | 
 | 77 | +    and associating them with corresponding `HttpChannelContainer` root resources.  | 
56 | 78 |     """  | 
57 | 79 | 
 
  | 
58 |  | -    _instance = None  | 
 | 80 | +    _instances = {}  | 
59 | 81 | 
 
  | 
60 | 82 |     def __init__(self, settings):  | 
 | 83 | +        log.info(f"Initializing HttpServerService. settings={settings}")  | 
61 | 84 | 
 
  | 
62 |  | -        # Propagate global settings  | 
 | 85 | +        # Propagate global settings.  | 
63 | 86 |         self.settings = settings  | 
64 | 87 | 
 
  | 
65 |  | -        # Unique name of this service  | 
66 |  | -        self.name = 'http-server-default'  | 
 | 88 | +        # Extract listen address settings.  | 
 | 89 | +        self.address = HttpServerAddress.from_settings(self.settings)  | 
67 | 90 | 
 
  | 
68 |  | -        # Root resource object representing a channel  | 
69 |  | -        # Contains routing machinery  | 
 | 91 | +        # Assign a unique name to the Twisted service object.  | 
 | 92 | +        self.name = f'http-server-{self.address.slug}'  | 
 | 93 | + | 
 | 94 | +        # Assign a root resource object, representing  | 
 | 95 | +        # a channel containing the routing machinery.  | 
70 | 96 |         self.root = HttpChannelContainer(self.settings)  | 
71 | 97 | 
 
  | 
72 |  | -        # Forward route registration method to channel object  | 
 | 98 | +        # Forward route registration method to channel object.  | 
73 | 99 |         self.registerEndpoint = self.root.registerEndpoint  | 
74 | 100 | 
 
  | 
75 | 101 |     def startService(self):  | 
76 | 102 |         """  | 
77 |  | -        Start TCP listener on designated HTTP port,  | 
78 |  | -        serving ``HttpChannelContainer`` as root resource.  | 
 | 103 | +        Start TCP listener on designated HTTP port, serving a  | 
 | 104 | +        `HttpChannelContainer` as root resource.  | 
79 | 105 |         """  | 
80 | 106 | 
 
  | 
81 |  | -        # Don't start service twice  | 
 | 107 | +        # Don't start service twice.  | 
82 | 108 |         if self.running == 1:  | 
83 | 109 |             return  | 
84 | 110 | 
 
  | 
85 | 111 |         self.running = 1  | 
86 | 112 | 
 
  | 
87 |  | -        # Prepare startup  | 
88 |  | -        http_listen = self.settings.kotori.http_listen  | 
89 |  | -        http_port   = int(self.settings.kotori.http_port)  | 
90 |  | -        log.info('Starting HTTP service on {http_listen}:{http_port}', http_listen=http_listen, http_port=http_port)  | 
 | 113 | +        # Prepare startup.  | 
 | 114 | +        log.info(f"Starting HTTP service on {self.address.combined}")  | 
91 | 115 | 
 
  | 
92 | 116 |         # Configure root Site object and start listening to requests.  | 
93 | 117 |         # This must take place only once - can't bind to the same port multiple times!  | 
94 | 118 |         factory = LocalSite(self.root)  | 
95 |  | -        reactor.listenTCP(http_port, factory, interface=http_listen)  | 
 | 119 | +        reactor.listenTCP(self.address.port, factory, interface=self.address.host)  | 
96 | 120 | 
 
  | 
97 | 121 |     @classmethod  | 
98 | 122 |     def create(cls, settings):  | 
99 | 123 |         """  | 
100 |  | -        Singleton factory  | 
 | 124 | +        Factory method for creating `HttpServerService` instances.  | 
 | 125 | +
  | 
 | 126 | +        It makes sure to create only one instance per listening address,  | 
 | 127 | +        in order not to bind to the same port multiple times.  | 
101 | 128 |         """  | 
102 |  | -        if not cls._instance:  | 
103 |  | -            cls._instance = HttpServerService(settings)  | 
104 |  | -            cls._instance.startService()  | 
105 |  | -        return cls._instance  | 
 | 129 | +        key = HttpServerAddress.from_settings(settings).combined  | 
 | 130 | +        if key not in cls._instances:  | 
 | 131 | +            instance = HttpServerService(settings)  | 
 | 132 | +            instance.startService()  | 
 | 133 | +            cls._instances[key] = instance  | 
 | 134 | +        return cls._instances[key]  | 
106 | 135 | 
 
  | 
107 | 136 | 
 
  | 
108 | 137 | class HttpChannelContainer(Resource):  | 
 | 
0 commit comments