Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.8.25
1.8.26
19 changes: 13 additions & 6 deletions src/torchlight/Commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from torchlight.AccessManager import AccessManager
from torchlight.AudioManager import AudioManager
from torchlight.Config import Config
from torchlight.GeoIP import get_city_reader
from torchlight.MyInstants import myinstants_get_random_sound
from torchlight.Player import Player
from torchlight.PlayerManager import PlayerManager
Expand Down Expand Up @@ -422,12 +423,15 @@ def __init__(
)
self.config_folder = self.torchlight.config["GeoIP"]["Path"]
self.city_filename = self.torchlight.config["GeoIP"]["CityFilename"]
self.geo_ip = geoip2.database.Reader(f"{self.config_folder}/{self.city_filename}")

def close(self) -> None:
# CommandHandler.Setup() rebuilds every command on each reload; without this the
# previous Reader's mmap of the GeoIP database is only released on GC (issue #55).
self.geo_ip.close()
self.geo_ip: geoip2.database.Reader | None = None
database_path = f"{self.config_folder}/{self.city_filename}"
try:
self.geo_ip = get_city_reader(database_path)
except Exception:
self.logger.error(
f"Failed to open GeoIP database ({database_path}); "
f"!ow without an explicit location will be unavailable\n{traceback.format_exc()}"
)

def degreeToCardinal(self, degree: int) -> str:
directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
Expand All @@ -444,6 +448,9 @@ async def _func(self, message: list[str], player: Player) -> int:

if not message[1]:
# Use GeoIP location
if self.geo_ip is None:
self.torchlight.SayPrivate(player, "[OW] Location lookup is unavailable, please specify a city.")
return 1
info = self.geo_ip.city(player.address.split(":")[0])
search = f"lat={info.location.latitude}&lon={info.location.longitude}"
else:
Expand Down
58 changes: 58 additions & 0 deletions src/torchlight/GeoIP.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import logging
import os

import geoip2.database

logger = logging.getLogger(__name__)

# Cached per database path as (mtime, reader). Kept at module level - and in this
# module rather than in Commands.py - so it survives importlib.reload(Commands)
# when the bot processes a !reload.
_readers: dict[str, tuple[float, geoip2.database.Reader]] = {}


def _close_quietly(reader: geoip2.database.Reader) -> None:
try:
reader.close()
except Exception:
logger.debug("Failed to close previous GeoIP reader", exc_info=True)


def get_city_reader(database_path: str) -> geoip2.database.Reader:
"""Open the GeoIP city database once and reuse it across reloads.

``CommandHandler.Setup`` rebuilds every command whenever the configuration is
reloaded. Opening a fresh :class:`geoip2.database.Reader` each time leaked the
previous memory map of the database until garbage collection (issue #55) and
could then fail to map the file at all, breaking command setup (issue #174).

The database is read-only, so a single reader is kept per path and only
reopened when the file on disk changes (for instance after a GeoIP update).
"""
cached = _readers.get(database_path)

try:
mtime = os.path.getmtime(database_path)
except OSError:
if cached is not None:
return cached[1]
raise

if cached is not None and cached[0] == mtime:
return cached[1]

try:
reader = geoip2.database.Reader(database_path)
except Exception:
if cached is not None:
# A refreshed database file is momentarily unreadable; keep serving
# the reader we already have instead of losing GeoIP entirely.
logger.warning(f"Could not reopen GeoIP database {database_path}, keeping the previous reader")
return cached[1]
raise

if cached is not None:
_close_quietly(cached[1])
_readers[database_path] = (mtime, reader)
logger.info(f"Opened GeoIP database {database_path}")
return reader
Loading