1 # -*- coding: utf-8 ; test-case-name: bridgedb.test.test_persistent -*-
3 # This file is part of BridgeDB, a Tor bridge distribution system.
5 # :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis@torproject.org>
6 # please also see AUTHORS file
7 # :copyright: (c) 2013-2017 Isis Lovecruft
8 # (c) 2007-2017, The Tor Project, Inc.
9 # (c) 2007-2017, all entities within the AUTHORS file
10 # :license: 3-clause BSD, see included LICENSE for information
12 """Module for functionality to persistently store state."""
19 import cPickle
as pickle
20 except (ImportError, NameError): # pragma: no cover
23 from twisted
.python
.reflect
import safe_repr
24 from twisted
.spread
import jelly
26 from bridgedb
import bridgerings
27 from bridgedb
import filters
28 from bridgedb
.distributors
.email
import distributor
as emailDistributor
29 from bridgedb
.distributors
.https
import distributor
as httpsDistributor
30 from bridgedb
.configure
import Conf
31 #from bridgedb.proxy import ProxySet
35 #: Types and classes which are allowed to be jellied:
36 _security
= jelly
.SecurityOptions()
37 #_security.allowInstancesOf(ProxySet)
38 _security
.allowModules(filters
,
44 class MissingState(Exception):
45 """Raised when the file or class storing global state is missing."""
49 """Retrieve the global state instance.
51 :rtype: :class:`~bridgedb.persistent.State`
52 :returns: An unpickled de-sexp'ed state object, which may contain just
53 about anything, but should contain things like options, loaded config
59 """Set the global state.
61 :type state: :class:`~bridgedb.persistent.State`
62 :param state: The state instance to save.
67 def load(stateCls
=None):
68 """Given a :class:`State`, try to unpickle it's ``statefile``.
70 :param string stateCls: An instance of :class:`~bridgedb.persistent.State`. If
71 not given, try loading from ``_statefile`` if that file exists.
72 :rtype: None or :class:`State`
75 if stateCls
and isinstance(stateCls
, State
):
81 raise MissingState("Could not find a state instance to load.")
87 class State(jelly
.Jellyable
):
88 """Pickled, jellied storage container for persistent state."""
90 def __init__(self
, config
=None, **kwargs
):
91 """Create a persistent state storage mechanism.
93 Serialisation of certain classes in BridgeDB doesn't work. Classes and
94 modules which are known to be unjelliable/unpicklable so far are:
97 - bridgedb.bridgerings, and all "splitter" and "ring" classes
100 :property statefile: The filename to retrieve a pickled, jellied
101 :class:`~bridgedb.persistent.State` instance from. (default:
102 :attr:`bridgedb.persistent.State._statefile`)
104 self
._statefile
= os
.path
.abspath(str(__package__
) + '.state')
105 self
.proxyList
= None
109 if 'STATEFILE' in kwargs
:
110 self
.statefile
= kwargs
['STATEFILE']
112 for key
, value
in kwargs
.items():
113 self
.__dict
__[key
] = value
115 if config
is not None:
116 for key
, value
in config
.__dict
__.items():
117 self
.__dict
__[key
] = value
121 def _get_statefile(self
):
122 """Retrieve the filename of the global statefile.
125 :returns: The filename of the statefile.
127 return self
._statefile
129 def _set_statefile(self
, filename
):
130 """Set the global statefile.
132 :param string statefile: The filename of the statefile.
136 self
._statefile
= None
139 filename
= os
.path
.abspath(os
.path
.expanduser(filename
))
140 logging
.debug("Setting statefile to '%s'" % filename
)
141 self
._statefile
= filename
143 # Create the parent directory if it doesn't exist:
144 dirname
= os
.path
.dirname(filename
)
145 if not os
.path
.isdir(dirname
):
148 # Create the statefile if it doesn't exist:
149 if not os
.path
.exists(filename
):
150 open(filename
, 'w').close()
152 def _del_statefile(self
):
153 """Delete the file containing previously saved state."""
155 with
open(self
._statefile
, 'w') as fh
:
157 os
.unlink(self
._statefile
)
158 self
._statefile
= None
159 except (IOError, OSError) as error
: # pragma: no cover
160 logging
.error("There was an error deleting the statefile: '%s'"
163 statefile
= property(_get_statefile
, _set_statefile
, _del_statefile
,
164 """Filename property of a persisent.State.""")
166 def load(self
, statefile
=None):
167 """Load a previously saved statefile.
169 :raises MissingState: If there was any error loading the **statefile**.
170 :rtype: :class:`State` or None
171 :returns: The state, loaded from :attr:`State.STATEFILE`, or None if
175 if not self
.statefile
:
176 raise MissingState("Could not find a state file to load.")
177 statefile
= self
.statefile
178 logging
.debug("Retrieving state from: \t'%s'" % statefile
)
184 if isinstance(statefile
, (str, bytes
)):
185 fh
= open(statefile
, 'rb')
186 elif not statefile
.closed
:
188 except (IOError, OSError) as error
: # pragma: no cover
189 err
+= "There was an error reading statefile "
190 err
+= "'{0}':\n{1}".format(statefile
, error
)
191 except (AttributeError, TypeError) as error
:
192 err
+= "Failed statefile.open() and statefile.closed:"
193 err
+= "\n\t{0}\nstatefile type = '{1}'".format(
194 error
, type(statefile
))
197 status
= pickle
.load(fh
)
199 err
+= "The statefile %s was empty." % fh
.name
201 quo
= jelly
.unjelly(status
)
208 raise MissingState(err
)
210 def save(self
, statefile
=None):
211 """Save state as a pickled jelly to a file on disk."""
213 if not self
._statefile
:
214 raise MissingState("Could not find a state file to load.")
215 statefile
= self
._statefile
216 logging
.debug("Saving state to: \t'%s'" % statefile
)
220 fh
= open(statefile
, 'wb')
221 except (IOError, OSError) as error
: # pragma: no cover
222 logging
.warn("Error writing state file to '%s': %s"
223 % (statefile
, error
))
226 pickle
.dump(jelly
.jelly(self
), fh
)
227 except AttributeError as error
:
228 logging
.debug("Tried jellying an unjelliable object: %s"
235 def useChangedSettings(self
, config
):
236 """Take a new config, compare it to the last one, and update settings.
238 Given a ``config`` object created from the configuration file, compare
239 it to the last :class:`~bridgedb.configure.Conf` that was stored, and apply
240 any settings which were changed to be attributes of the :class:`State`
246 for key
, value
in config
.__dict
__.items():
248 # If state.config doesn't have the same value as the new
249 # config, then update the state setting.
251 # Be sure, when updating settings while parsing the config
252 # file, to assign the new settings as attributes of the
253 # :class:`bridgedb.configure.Conf` instance.
254 if value
!= self
.config
.__dict
__[key
]:
255 setattr(self
, key
, value
)
257 logging
.debug("Updated %s setting: %r → %r" %
259 self
.config
.__dict
__[key
],
261 except (KeyError, AttributeError):
262 setattr(self
, key
, value
)
264 logging
.debug("New setting: %s = %r" %
268 logging
.info("Updated setting(s): %s" % ' '.join([x
for x
in updated
]))
269 logging
.info("New setting(s): %s" % ' '.join([x
for x
in new
]))
271 "Saving newer config as `state.config` for later comparison")