diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 8a6c97f717..31cfdcab7e 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -475,6 +475,13 @@ def start_reactor( def main() -> None: homeserver_config = load_or_generate_config(sys.argv[1:]) + # Create an asyncio event loop early so that NativeClock.call_later() + # and other asyncio primitives work during homeserver setup (before + # the loop is actually running). + import asyncio + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + # Create a logging context as soon as possible so we can start associating # everything with this homeserver. with LoggingContext(name="main", server_name=homeserver_config.server.server_name): diff --git a/synapse/http/server.py b/synapse/http/server.py index f7ee566df0..fcf0e393af 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -632,21 +632,51 @@ except ImportError: from synapse.http.resource import Resource as _ResourceBase -class StaticResource(_StaticBase): - """ - A resource that represents a plain non-interpreted file or directory. +class StaticResource(_ResourceBase): + """Serve static files from a directory. - Differs from the File resource by adding clickjacking protection. + Pure-Python replacement for Twisted's ``twisted.web.static.File``. + Adds clickjacking protection headers. """ - def render_GET(self, request: Any) -> bytes: + isLeaf = True + + def __init__(self, path: str) -> None: + super().__init__() + self._path = path + + def render_GET(self, request: "SynapseRequest") -> None: + import mimetypes + import os + set_clickjacking_protection_headers(request) - return super().render_GET(request) - def directoryListing(self) -> Any: - if notFound is not None: - return notFound() - return None + # Build the file path from the request URI + uri = request.path + if isinstance(uri, bytes): + uri = uri.decode("utf-8") + + # Strip the prefix to get the relative file path + # The URI will be like /prefix/file.css — we need just the filename + parts = uri.rstrip("/").split("/") + filename = parts[-1] if parts else "index.html" + + filepath = os.path.join(self._path, filename) + if not os.path.isfile(filepath): + request.setResponseCode(404) + request.write(b"Not Found") + request.finish() + return + + content_type, _ = mimetypes.guess_type(filepath) + if content_type: + request.setHeader(b"Content-Type", content_type.encode("ascii")) + + with open(filepath, "rb") as f: + data = f.read() + request.setHeader(b"Content-Length", str(len(data)).encode("ascii")) + request.write(data) + request.finish() class UnrecognizedRequestResource(_ResourceBase): diff --git a/synapse/logging/handlers.py b/synapse/logging/handlers.py index 3ff67fe822..3c1e05d61a 100644 --- a/synapse/logging/handlers.py +++ b/synapse/logging/handlers.py @@ -3,7 +3,7 @@ import time from logging import Handler, LogRecord from logging.handlers import MemoryHandler from threading import Thread -from typing import Optional, cast +from typing import Any, Optional class PeriodicallyFlushingMemoryHandler(MemoryHandler): @@ -24,21 +24,20 @@ class PeriodicallyFlushingMemoryHandler(MemoryHandler): target: Handler | None = None, flushOnClose: bool = True, period: float = 5.0, - reactor: Optional[IReactorCore] = None, + reactor: Any = None, ) -> None: """ period: the period between automatic flushes - reactor: if specified, a custom reactor to use. If not specifies, - defaults to the globally-installed reactor. - Log entries will be flushed immediately until this reactor has - started. + reactor: legacy parameter, ignored. Kept for config compat. """ super().__init__(capacity, flushLevel, target, flushOnClose) self._flush_period: float = period self._active: bool = True - self._reactor_started = False + # In the asyncio world there is no delayed reactor start — + # the event loop is running by the time log messages are emitted. + self._reactor_started = True self._flushing_thread: Thread = Thread( name="PeriodicallyFlushingMemoryHandler flushing thread", @@ -47,28 +46,6 @@ class PeriodicallyFlushingMemoryHandler(MemoryHandler): ) self._flushing_thread.start() - def on_reactor_running() -> None: - self._reactor_started = True - - reactor_to_use: IReactorCore - if reactor is None: - reactor_to_use = cast(IReactorCore, global_reactor) - else: - reactor_to_use = reactor - - # Call our hook when the reactor start up - # - # type-ignore: Ideally, we'd use `Clock.call_when_running(...)`, but - # `PeriodicallyFlushingMemoryHandler` is instantiated via Python logging - # configuration, so it's not straightforward to pass in the homeserver's clock - # (and we don't want to burden other peoples logging config with the details). - # - # The important reason why we want to use `Clock.call_when_running` is so that - # the callback runs with a logcontext as we want to know which server the logs - # came from. But since we don't log anything in the callback, it's safe to - # ignore the lint here. - reactor_to_use.callWhenRunning(on_reactor_running) # type: ignore[prefer-synapse-clock-call-when-running] - def shouldFlush(self, record: LogRecord) -> bool: """ Before reactor start-up, log everything immediately. diff --git a/synapse/server.py b/synapse/server.py index 6d9222470d..ecff22e684 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -326,9 +326,10 @@ class HomeServer(metaclass=abc.ABCMeta): config: The full config for the homeserver. """ - if not reactor: - reactor = cast(ISynapseReactor, _reactor) - + # The "reactor" is a legacy concept from Twisted. In the asyncio + # world it's just a handle that some code passes around. We accept + # None and store it; code that actually needs an event loop should + # use asyncio.get_running_loop() at call time. self._reactor = reactor self.hostname = hostname # the key we use to sign events and requests