# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_persistent -*-
#
# This file is part of BridgeDB, a Tor bridge distribution system.
#
# :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis@torproject.org>
# please also see AUTHORS file
# :copyright: (c) 2013-2015 Isis Lovecruft
# (c) 2007-2015, The Tor Project, Inc.
# (c) 2007-2015, all entities within the AUTHORS file
# :license: 3-clause BSD, see included LICENSE for information
"""Module for functionality to persistently store state."""
import copy
import logging
import os.path
try:
import cPickle as pickle
except (ImportError, NameError): # pragma: no cover
import pickle
from twisted.python.reflect import safe_repr
from twisted.spread import jelly
from bridgedb import Bridges
from bridgedb import filters
from bridgedb.email import distributor as emailDistributor
from bridgedb.https import distributor as httpsDistributor
from bridgedb.configure import Conf
#from bridgedb.proxy import ProxySet
_state = None
#: Types and classes which are allowed to be jellied:
_security = jelly.SecurityOptions()
#_security.allowInstancesOf(ProxySet)
_security.allowModules(filters, Bridges, emailDistributor, httpsDistributor)
[docs]class MissingState(Exception):
"""Raised when the file or class storing global state is missing."""
def _getState():
"""Retrieve the global state instance.
:rtype: :class:`~bridgedb.persistent.State`
:returns: An unpickled de-sexp'ed state object, which may contain just
about anything, but should contain things like options, loaded config
settings, etc.
"""
return _state
def _setState(state):
"""Set the global state.
:type state: :class:`~bridgedb.persistent.State`
:param state: The state instance to save.
"""
global _state
_state = state
[docs]def load(stateCls=None):
"""Given a :class:`State`, try to unpickle it's ``statefile``.
:param string stateCls: An instance of :class:`~bridgedb.persistent.State`. If
not given, try loading from ``_statefile`` if that file exists.
:rtype: None or :class:`State`
"""
statefile = None
if stateCls and isinstance(stateCls, State):
cls = stateCls
else:
cls = _getState()
if not cls:
raise MissingState("Could not find a state instance to load.")
else:
loaded = cls.load()
return loaded
[docs]class State(jelly.Jellyable):
"""Pickled, jellied storage container for persistent state."""
def __init__(self, config=None, **kwargs):
"""Create a persistent state storage mechanism.
Serialisation of certain classes in BridgeDB doesn't work. Classes and
modules which are known to be unjelliable/unpicklable so far are:
- bridgedb.Dist
- bridgedb.Bridges, and all "splitter" and "ring" classes contained
within
:property statefile: The filename to retrieve a pickled, jellied
:class:`~bridgedb.persistent.State` instance from. (default:
:attr:`bridgedb.persistent.State._statefile`)
"""
self._statefile = os.path.abspath(str(__package__) + '.state')
self.proxyList = None
self.config = None
self.key = None
if 'STATEFILE' in kwargs:
self.statefile = kwargs['STATEFILE']
for key, value in kwargs.items():
self.__dict__[key] = value
if config is not None:
for key, value in config.__dict__.items():
self.__dict__[key] = value
_setState(self)
def _get_statefile(self):
"""Retrieve the filename of the global statefile.
:rtype: string
:returns: The filename of the statefile.
"""
return self._statefile
def _set_statefile(self, filename):
"""Set the global statefile.
:param string statefile: The filename of the statefile.
"""
filename = os.path.abspath(os.path.expanduser(filename))
logging.debug("Setting statefile to '%s'" % filename)
self._statefile = filename
# Create the parent directory if it doesn't exist:
dirname = os.path.dirname(filename)
if not os.path.isdir(dirname):
os.makedirs(dirname)
# Create the statefile if it doesn't exist:
if not os.path.exists(filename):
open(filename, 'w').close()
def _del_statefile(self):
"""Delete the file containing previously saved state."""
try:
with open(self._statefile, 'w') as fh:
fh.close()
os.unlink(self._statefile)
self._statefile = None
except (IOError, OSError) as error: # pragma: no cover
logging.error("There was an error deleting the statefile: '%s'"
% self._statefile)
statefile = property(_get_statefile, _set_statefile, _del_statefile,
"""Filename property of a persisent.State.""")
[docs] def load(self, statefile=None):
"""Load a previously saved statefile.
:raises MissingState: If there was any error loading the **statefile**.
:rtype: :class:`State` or None
:returns: The state, loaded from :attr:`State.STATEFILE`, or None if
an error occurred.
"""
if not statefile:
if not self.statefile:
raise MissingState("Could not find a state file to load.")
statefile = self.statefile
logging.debug("Retrieving state from: \t'%s'" % statefile)
quo= fh = None
err = ''
try:
if isinstance(statefile, basestring):
fh = open(statefile, 'r')
elif not statefile.closed:
fh = statefile
except (IOError, OSError) as error: # pragma: no cover
err += "There was an error reading statefile "
err += "'{0}':\n{1}".format(statefile, error)
except (AttributeError, TypeError) as error:
err += "Failed statefile.open() and statefile.closed:"
err += "\n\t{0}\nstatefile type = '{1}'".format(
error.message, type(statefile))
else:
try:
status = pickle.load(fh)
except EOFError:
err += "The statefile %s was empty." % fh.name
else:
quo = jelly.unjelly(status)
if fh is not None:
fh.close()
if quo:
return quo
if err:
raise MissingState(err)
[docs] def save(self, statefile=None):
"""Save state as a pickled jelly to a file on disk."""
if not statefile:
if not self._statefile:
raise MissingState("Could not find a state file to load.")
statefile = self._statefile
logging.debug("Saving state to: \t'%s'" % statefile)
fh = None
try:
fh = open(statefile, 'w')
except (IOError, OSError) as error: # pragma: no cover
logging.warn("Error writing state file to '%s': %s"
% (statefile, error))
else:
try:
pickle.dump(jelly.jelly(self), fh)
except AttributeError as error:
logging.debug("Tried jellying an unjelliable object: %s"
% error.message)
if fh is not None:
fh.flush()
fh.close()
[docs] def useChangedSettings(self, config):
"""Take a new config, compare it to the last one, and update settings.
Given a ``config`` object created from the configuration file, compare
it to the last :class:`~bridgedb.configure.Conf` that was stored, and apply
any settings which were changed to be attributes of the :class:`State`
instance.
"""
updated = []
new = []
for key, value in config.__dict__.items():
try:
# If state.config doesn't have the same value as the new
# config, then update the state setting.
#
# Be sure, when updating settings while parsing the config
# file, to assign the new settings as attributes of the
# :class:`bridgedb.configure.Conf` instance.
if value != self.config.__dict__[key]:
setattr(self, key, value)
updated.append(key)
logging.debug("Updated %s setting: %r → %r" %
(safe_repr(key),
self.config.__dict__[key],
safe_repr(value)))
except (KeyError, AttributeError):
setattr(self, key, value)
new.append(key)
logging.debug("New setting: %s = %r" %
(safe_repr(key),
safe_repr(value)))
logging.info("Updated setting(s): %s" % ' '.join([x for x in updated]))
logging.info("New setting(s): %s" % ' '.join([x for x in new]))
logging.debug(
"Saving newer config as `state.config` for later comparison")
self.config = config