add a fairly basic file locking mechanism with src/LockFile.py

closes #96
This commit is contained in:
jesopo 2019-10-10 12:10:45 +01:00
parent 0331b763ff
commit 2c19bdb949
4 changed files with 57 additions and 6 deletions

View file

@ -36,7 +36,7 @@ class ListLambdaPollHook(PollHook.PollHook):
class Bot(object): class Bot(object):
def __init__(self, directory, args, cache, config, database, events, def __init__(self, directory, args, cache, config, database, events,
exports, log, modules, timers): exports, log, modules, timers, lock_file):
self.directory = directory self.directory = directory
self.args = args self.args = args
self.cache = cache self.cache = cache
@ -71,6 +71,7 @@ class Bot(object):
self._poll_timeouts = [] # typing.List[PollHook] self._poll_timeouts = [] # typing.List[PollHook]
self._poll_timeouts.append(self._timers) self._poll_timeouts.append(self._timers)
self._poll_timeouts.append(self.cache) self._poll_timeouts.append(self.cache)
self._poll_timeouts.append(lock_file)
self._poll_timeouts.append(ListLambdaPollHook( self._poll_timeouts.append(ListLambdaPollHook(
lambda: self.servers.values(), lambda: self.servers.values(),

40
src/LockFile.py Normal file
View file

@ -0,0 +1,40 @@
import datetime, os
from src import PollHook, utils
EXPIRATION = 60 # 1 minute
class LockFile(PollHook.PollHook):
def __init__(self, database_location: str):
self._database_location = database_location
self._lock_location = "%s.lock" % database_location
self._next_lock = None
def available(self):
now = utils.datetime_utcnow()
if os.path.exists(self._lock_location):
with open(self._lock_location, "r") as lock_file:
timestamp_str = lock_file.read().strip().split(" ", 1)[0]
timestamp = utils.iso8601_parse(timestamp_str)
if (now-timestamp).total_seconds() < EXPIRATION:
return False
return True
def lock(self):
with open(self._lock_location, "w") as lock_file:
last_lock = utils.datetime_utcnow()
lock_file.write("%s" % utils.iso8601_format(last_lock))
self._next_lock = last_lock+datetime.timedelta(
seconds=EXPIRATION/2)
def next(self):
return max(0, (self._next_lock-utils.datetime_utcnow()).total_seconds())
def call(self):
if self.next() == 0:
self.lock()
def unlock(self):
if os.path.isfile(self._lock_location):
os.remove(self._lock_location)

View file

@ -15,6 +15,9 @@ ISO8601_FORMAT_TZ = "%z"
DATETIME_HUMAN = "%Y/%m/%d %H:%M:%S" DATETIME_HUMAN = "%Y/%m/%d %H:%M:%S"
def datetime_utcnow() -> datetime.datetime:
return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
def iso8601_format(dt: datetime.datetime, milliseconds: bool=False) -> str: def iso8601_format(dt: datetime.datetime, milliseconds: bool=False) -> str:
dt_format = dt.strftime(ISO8601_FORMAT_DT) dt_format = dt.strftime(ISO8601_FORMAT_DT)
tz_format = dt.strftime(ISO8601_FORMAT_TZ) tz_format = dt.strftime(ISO8601_FORMAT_TZ)
@ -25,8 +28,7 @@ def iso8601_format(dt: datetime.datetime, milliseconds: bool=False) -> str:
return "%s%s%s" % (dt_format, ms_format, tz_format) return "%s%s%s" % (dt_format, ms_format, tz_format)
def iso8601_format_now(milliseconds: bool=False) -> str: def iso8601_format_now(milliseconds: bool=False) -> str:
now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) return iso8601_format(datetime_utcnow(), milliseconds=milliseconds)
return iso8601_format(now, milliseconds=milliseconds)
def iso8601_parse(s: str, microseconds: bool=False) -> datetime.datetime: def iso8601_parse(s: str, microseconds: bool=False) -> datetime.datetime:
fmt = ISO8601_PARSE_MICROSECONDS if microseconds else ISO8601_PARSE fmt = ISO8601_PARSE_MICROSECONDS if microseconds else ISO8601_PARSE
return datetime.datetime.strptime(s, fmt) return datetime.datetime.strptime(s, fmt)

View file

@ -6,9 +6,9 @@ if sys.version_info < (3, 6):
sys.stderr.write("BitBot requires python 3.6.0 or later\n") sys.stderr.write("BitBot requires python 3.6.0 or later\n")
sys.exit(1) sys.exit(1)
import argparse, faulthandler, os, platform, time import atexit, argparse, faulthandler, os, platform, time
from src import Cache, Config, Database, EventManager, Exports, IRCBot from src import Cache, Config, Database, EventManager, Exports, IRCBot
from src import Logging, ModuleManager, Timers, utils from src import LockFile, Logging, ModuleManager, Timers, utils
faulthandler.enable() faulthandler.enable()
@ -65,6 +65,14 @@ log = Logging.Log(not args.no_logging, log_level, args.log_dir)
log.info("Starting BitBot %s (Python v%s)", log.info("Starting BitBot %s (Python v%s)",
[IRCBot.VERSION, platform.python_version()]) [IRCBot.VERSION, platform.python_version()])
lock_file = LockFile.LockFile(args.database)
if not lock_file.available():
log.critical("Database is locked. Is BitBot already running?")
sys.exit(1)
atexit.register(lock_file.unlock)
lock_file.lock()
database = Database.Database(log, args.database) database = Database.Database(log, args.database)
if args.remove_server: if args.remove_server:
@ -98,7 +106,7 @@ modules = ModuleManager.ModuleManager(events, exports, timers, config, log,
module_directories) module_directories)
bot = IRCBot.Bot(directory, args, cache, config, database, events, bot = IRCBot.Bot(directory, args, cache, config, database, events,
exports, log, modules, timers) exports, log, modules, timers, lock_file)
if args.module: if args.module:
definition = modules.find_module(args.module) definition = modules.find_module(args.module)