Show More
@@ -16,7 +16,6 b' except ImportError:' | |||||
16 | from pysqlite2 import dbapi2 as sqlite3 |
|
16 | from pysqlite2 import dbapi2 as sqlite3 | |
17 | except ImportError: |
|
17 | except ImportError: | |
18 | sqlite3 = None |
|
18 | sqlite3 = None | |
19 | import sys |
|
|||
20 | import threading |
|
19 | import threading | |
21 |
|
20 | |||
22 | from traitlets.config.configurable import LoggingConfigurable |
|
21 | from traitlets.config.configurable import LoggingConfigurable | |
@@ -72,27 +71,52 b' else:' | |||||
72 | class OperationalError(Exception): |
|
71 | class OperationalError(Exception): | |
73 | "Dummy exception when sqlite could not be imported. Should never occur." |
|
72 | "Dummy exception when sqlite could not be imported. Should never occur." | |
74 |
|
73 | |||
|
74 | # use 16kB as threshold for whether a corrupt history db should be saved | |||
|
75 | # that should be at least 100 entries or so | |||
|
76 | _SAVE_DB_SIZE = 16384 | |||
|
77 | ||||
75 | @decorator |
|
78 | @decorator | |
76 | def catch_corrupt_db(f, self, *a, **kw): |
|
79 | def catch_corrupt_db(f, self, *a, **kw): | |
77 | """A decorator which wraps HistoryAccessor method calls to catch errors from |
|
80 | """A decorator which wraps HistoryAccessor method calls to catch errors from | |
78 | a corrupt SQLite database, move the old database out of the way, and create |
|
81 | a corrupt SQLite database, move the old database out of the way, and create | |
79 | a new one. |
|
82 | a new one. | |
|
83 | ||||
|
84 | We avoid clobbering larger databases because this may be triggered due to filesystem issues, | |||
|
85 | not just a corrupt file. | |||
80 | """ |
|
86 | """ | |
81 | try: |
|
87 | try: | |
82 | return f(self, *a, **kw) |
|
88 | return f(self, *a, **kw) | |
83 | except (DatabaseError, OperationalError) as e: |
|
89 | except (DatabaseError, OperationalError) as e: | |
84 | if os.path.isfile(self.hist_file): |
|
90 | self._corrupt_db_counter += 1 | |
85 | # Try to move the file out of the way |
|
91 | self.log.error("Failed to open SQLite history %s (%s).", self.hist_file, e) | |
|
92 | if self.hist_file != ':memory:': | |||
|
93 | if self._corrupt_db_counter > self._corrupt_db_limit: | |||
|
94 | self.hist_file = ':memory:' | |||
|
95 | self.log.error("Failed to load history too many times, history will not be saved.") | |||
|
96 | elif os.path.isfile(self.hist_file): | |||
|
97 | # move the file out of the way | |||
86 | base,ext = os.path.splitext(self.hist_file) |
|
98 | base, ext = os.path.splitext(self.hist_file) | |
|
99 | size = os.stat(self.hist_file).st_size | |||
|
100 | if size >= _SAVE_DB_SIZE: | |||
|
101 | # if there's significant content, avoid clobbering | |||
|
102 | now = datetime.datetime.now().isoformat().replace(':', '.') | |||
|
103 | newpath = base + '-corrupt-' + now + ext | |||
|
104 | # don't clobber previous corrupt backups | |||
|
105 | for i in range(100): | |||
|
106 | if not os.path.isfile(newpath): | |||
|
107 | break | |||
|
108 | else: | |||
|
109 | newpath = base + '-corrupt-' + now + (u'-%i' % i) + ext | |||
|
110 | else: | |||
|
111 | # not much content, possibly empty; don't worry about clobbering | |||
|
112 | # maybe we should just delete it? | |||
87 | newpath = base + '-corrupt' + ext |
|
113 | newpath = base + '-corrupt' + ext | |
88 | os.rename(self.hist_file, newpath) |
|
114 | os.rename(self.hist_file, newpath) | |
89 | print("ERROR! History file wasn't a valid SQLite database (%s)." % e, |
|
115 | self.log.error("History file was moved to %s and a new file created.", newpath) | |
90 | "It was moved to %s" % newpath, "and a new file created.", file=sys.stderr) |
|
|||
91 | self.init_db() |
|
116 | self.init_db() | |
92 | return [] |
|
117 | return [] | |
93 |
|
||||
94 | else: |
|
118 | else: | |
95 |
# |
|
119 | # Failed with :memory:, something serious is wrong | |
96 | raise |
|
120 | raise | |
97 |
|
121 | |||
98 | class HistoryAccessorBase(LoggingConfigurable): |
|
122 | class HistoryAccessorBase(LoggingConfigurable): | |
@@ -118,6 +142,11 b' class HistoryAccessor(HistoryAccessorBase):' | |||||
118 | This is intended for use by standalone history tools. IPython shells use |
|
142 | This is intended for use by standalone history tools. IPython shells use | |
119 | HistoryManager, below, which is a subclass of this.""" |
|
143 | HistoryManager, below, which is a subclass of this.""" | |
120 |
|
144 | |||
|
145 | # counter for init_db retries, so we don't keep trying over and over | |||
|
146 | _corrupt_db_counter = 0 | |||
|
147 | # after two failures, fallback on :memory: | |||
|
148 | _corrupt_db_limit = 2 | |||
|
149 | ||||
121 | # String holding the path to the history file |
|
150 | # String holding the path to the history file | |
122 | hist_file = Unicode(config=True, |
|
151 | hist_file = Unicode(config=True, | |
123 | help="""Path to file to use for SQLite history database. |
|
152 | help="""Path to file to use for SQLite history database. | |
@@ -231,6 +260,8 b' class HistoryAccessor(HistoryAccessorBase):' | |||||
231 | (session integer, line integer, output text, |
|
260 | (session integer, line integer, output text, | |
232 | PRIMARY KEY (session, line))""") |
|
261 | PRIMARY KEY (session, line))""") | |
233 | self.db.commit() |
|
262 | self.db.commit() | |
|
263 | # success! reset corrupt db count | |||
|
264 | self._corrupt_db_counter = 0 | |||
234 |
|
265 | |||
235 | def writeout_cache(self): |
|
266 | def writeout_cache(self): | |
236 | """Overridden by HistoryManager to dump the cache before certain |
|
267 | """Overridden by HistoryManager to dump the cache before certain |
General Comments 0
You need to be logged in to leave comments.
Login now