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 Bridges
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
, Bridges
, emailDistributor
, httpsDistributor
)
41 class MissingState(Exception):
42 """Raised when the file or class storing global state is missing."""
46 """Retrieve the global state instance.
48 :rtype: :class:`~bridgedb.persistent.State`
49 :returns: An unpickled de-sexp'ed state object, which may contain just
50 about anything, but should contain things like options, loaded config
56 """Set the global state.
58 :type state: :class:`~bridgedb.persistent.State`
59 :param state: The state instance to save.
64 def load(stateCls
=None):
65 """Given a :class:`State`, try to unpickle it's ``statefile``.
67 :param string stateCls: An instance of :class:`~bridgedb.persistent.State`. If
68 not given, try loading from ``_statefile`` if that file exists.
69 :rtype: None or :class:`State`
72 if stateCls
and isinstance(stateCls
, State
):
78 raise MissingState("Could not find a state instance to load.")
84 class State(jelly
.Jellyable
):
85 """Pickled, jellied storage container for persistent state."""
87 def __init__(self
, config
=None, **kwargs
):
88 """Create a persistent state storage mechanism.
90 Serialisation of certain classes in BridgeDB doesn't work. Classes and
91 modules which are known to be unjelliable/unpicklable so far are:
94 - bridgedb.Bridges, and all "splitter" and "ring" classes contained
97 :property statefile: The filename to retrieve a pickled, jellied
98 :class:`~bridgedb.persistent.State` instance from. (default:
99 :attr:`bridgedb.persistent.State._statefile`)
101 self
._statefile
= os
.path
.abspath(str(__package__
) + '.state')
102 self
.proxyList
= None
106 if 'STATEFILE' in kwargs
:
107 self
.statefile
= kwargs
['STATEFILE']
109 for key
, value
in kwargs
.items():
110 self
.__dict
__[key
] = value
112 if config
is not None:
113 for key
, value
in config
.__dict
__.items():
114 self
.__dict
__[key
] = value
118 def _get_statefile(self
):
119 """Retrieve the filename of the global statefile.
122 :returns: The filename of the statefile.
124 return self
._statefile
126 def _set_statefile(self
, filename
):
127 """Set the global statefile.
129 :param string statefile: The filename of the statefile.
133 self
._statefile
= None
136 filename
= os
.path
.abspath(os
.path
.expanduser(filename
))
137 logging
.debug("Setting statefile to '%s'" % filename
)
138 self
._statefile
= filename
140 # Create the parent directory if it doesn't exist:
141 dirname
= os
.path
.dirname(filename
)
142 if not os
.path
.isdir(dirname
):
145 # Create the statefile if it doesn't exist:
146 if not os
.path
.exists(filename
):
147 open(filename
, 'w').close()
149 def _del_statefile(self
):
150 """Delete the file containing previously saved state."""
152 with
open(self
._statefile
, 'w') as fh
:
154 os
.unlink(self
._statefile
)
155 self
._statefile
= None
156 except (IOError, OSError) as error
: # pragma: no cover
157 logging
.error("There was an error deleting the statefile: '%s'"
160 statefile
= property(_get_statefile
, _set_statefile
, _del_statefile
,
161 """Filename property of a persisent.State.""")
163 def load(self
, statefile
=None):
164 """Load a previously saved statefile.
166 :raises MissingState: If there was any error loading the **statefile**.
167 :rtype: :class:`State` or None
168 :returns: The state, loaded from :attr:`State.STATEFILE`, or None if
172 if not self
.statefile
:
173 raise MissingState("Could not find a state file to load.")
174 statefile
= self
.statefile
175 logging
.debug("Retrieving state from: \t'%s'" % statefile
)
181 if isinstance(statefile
, (str, bytes
)):
182 fh
= open(statefile
, 'rb')
183 elif not statefile
.closed
:
185 except (IOError, OSError) as error
: # pragma: no cover
186 err
+= "There was an error reading statefile "
187 err
+= "'{0}':\n{1}".format(statefile
, error
)
188 except (AttributeError, TypeError) as error
:
189 err
+= "Failed statefile.open() and statefile.closed:"
190 err
+= "\n\t{0}\nstatefile type = '{1}'".format(
191 error
, type(statefile
))
194 status
= pickle
.load(fh
)
196 err
+= "The statefile %s was empty." % fh
.name
198 quo
= jelly
.unjelly(status
)
205 raise MissingState(err
)
207 def save(self
, statefile
=None):
208 """Save state as a pickled jelly to a file on disk."""
210 if not self
._statefile
:
211 raise MissingState("Could not find a state file to load.")
212 statefile
= self
._statefile
213 logging
.debug("Saving state to: \t'%s'" % statefile
)
217 fh
= open(statefile
, 'wb')
218 except (IOError, OSError) as error
: # pragma: no cover
219 logging
.warn("Error writing state file to '%s': %s"
220 % (statefile
, error
))
223 pickle
.dump(jelly
.jelly(self
), fh
)
224 except AttributeError as error
:
225 logging
.debug("Tried jellying an unjelliable object: %s"
232 def useChangedSettings(self
, config
):
233 """Take a new config, compare it to the last one, and update settings.
235 Given a ``config`` object created from the configuration file, compare
236 it to the last :class:`~bridgedb.configure.Conf` that was stored, and apply
237 any settings which were changed to be attributes of the :class:`State`
243 for key
, value
in config
.__dict
__.items():
245 # If state.config doesn't have the same value as the new
246 # config, then update the state setting.
248 # Be sure, when updating settings while parsing the config
249 # file, to assign the new settings as attributes of the
250 # :class:`bridgedb.configure.Conf` instance.
251 if value
!= self
.config
.__dict
__[key
]:
252 setattr(self
, key
, value
)
254 logging
.debug("Updated %s setting: %r → %r" %
256 self
.config
.__dict
__[key
],
258 except (KeyError, AttributeError):
259 setattr(self
, key
, value
)
261 logging
.debug("New setting: %s = %r" %
265 logging
.info("Updated setting(s): %s" % ' '.join([x
for x
in updated
]))
266 logging
.info("New setting(s): %s" % ' '.join([x
for x
in new
]))
268 "Saving newer config as `state.config` for later comparison")