openMSX
FilePool.cc
Go to the documentation of this file.
1 #include "FilePool.hh"
2 #include "File.hh"
3 #include "FileException.hh"
4 #include "FileContext.hh"
5 #include "FileOperations.hh"
6 #include "TclObject.hh"
7 #include "StringSetting.hh"
8 #include "ReadDir.hh"
9 #include "Date.hh"
10 #include "CommandController.hh"
11 #include "CommandException.hh"
12 #include "EventDistributor.hh"
13 #include "CliComm.hh"
14 #include "Timer.hh"
15 #include "StringOp.hh"
16 #include "memory.hh"
17 #include "sha1.hh"
18 #include <fstream>
19 #include <cassert>
20 
21 using std::endl;
22 using std::ifstream;
23 using std::make_pair;
24 using std::ofstream;
25 using std::pair;
26 using std::string;
27 using std::vector;
28 using std::unique_ptr;
29 
30 namespace openmsx {
31 
32 const char* const FILE_CACHE = "/.filecache";
33 
34 static string initialFilePoolSettingValue()
35 {
36  TclObject result;
37 
38  for (auto& p : SystemFileContext().getPaths()) {
39  TclObject entry1;
40  entry1.addListElement("-path");
41  entry1.addListElement(FileOperations::join(p, "systemroms"));
42  entry1.addListElement("-types");
43  entry1.addListElement("system_rom");
44  result.addListElement(entry1);
45 
46  TclObject entry2;
47  entry2.addListElement("-path");
48  entry2.addListElement(FileOperations::join(p, "software"));
49  entry2.addListElement("-types");
50  entry2.addListElement("rom disk tape");
51  result.addListElement(entry2);
52  }
53  return result.getString().str();
54 }
55 
57  : filePoolSetting(make_unique<StringSetting>(
58  controller, "__filepool",
59  "This is an internal setting. Don't change this directly, "
60  "instead use the 'filepool' command.",
61  initialFilePoolSettingValue()))
62  , distributor(distributor_)
63  , cliComm(controller.getCliComm())
64  , quit(false)
65 {
66  filePoolSetting->attach(*this);
67  distributor.registerEventListener(OPENMSX_QUIT_EVENT, *this);
68  readSha1sums();
69  needWrite = false;
70 }
71 
73 {
74  if (needWrite) {
75  writeSha1sums();
76  }
77  distributor.unregisterEventListener(OPENMSX_QUIT_EVENT, *this);
78  filePoolSetting->detach(*this);
79 }
80 
81 void FilePool::insert(const Sha1Sum& sum, time_t time, const string& filename)
82 {
83  auto it = pool.insert(make_pair(sum, make_pair(time, filename)));
84  reversePool.insert(make_pair(it->second.second, it));
85  needWrite = true;
86 }
87 
88 void FilePool::remove(Pool::iterator it)
89 {
90  reversePool.erase(it->second.second);
91  pool.erase(it);
92  needWrite = true;
93 }
94 
95 static bool parse(const string& line, Sha1Sum& sha1, time_t& time, string& filename)
96 {
97  if (line.size() <= 68) return false;
98 
99  try {
100  sha1.parse40(line.data());
101  } catch (MSXException& /*e*/) {
102  return false;
103  }
104 
105  time = Date::fromString(line.data() + 42);
106  if (time == time_t(-1)) return false;
107 
108  filename.assign(line, 68, line.size());
109  return true;
110 }
111 
112 void FilePool::readSha1sums()
113 {
114  string cacheFile = FileOperations::getUserDataDir() + FILE_CACHE;
115  ifstream file(cacheFile.c_str());
116  string line;
117  Sha1Sum sum;
118  string filename;
119  time_t time;
120  while (file.good()) {
121  getline(file, line);
122  if (parse(line, sum, time, filename)) {
123  insert(sum, time, filename);
124  }
125  }
126 }
127 
128 void FilePool::writeSha1sums()
129 {
130  string cacheFile = FileOperations::getUserDataDir() + FILE_CACHE;
131  ofstream file;
132  FileOperations::openofstream(file, cacheFile);
133  if (!file.is_open()) {
134  return;
135  }
136  for (auto& p : pool) {
137  file << p.first.toString() << " " // sum
138  << Date::toString(p.second.first) << " " // date
139  << p.second.second // filename
140  << '\n';
141  }
142 }
143 
144 static int parseTypes(const TclObject& list)
145 {
146  int result = 0;
147  unsigned num = list.getListLength();
148  for (unsigned i = 0; i < num; ++i) {
149  string_ref elem = list.getListIndex(i).getString();
150  if (elem == "system_rom") {
151  result |= FilePool::SYSTEM_ROM;
152  } else if (elem == "rom") {
153  result |= FilePool::ROM;
154  } else if (elem == "disk") {
155  result |= FilePool::DISK;
156  } else if (elem == "tape") {
157  result |= FilePool::TAPE;
158  } else {
159  throw CommandException("Unknown type: " + elem);
160  }
161  }
162  return result;
163 }
164 
165 void FilePool::update(const Setting& setting)
166 {
167  assert(&setting == filePoolSetting.get()); (void)setting;
168  getDirectories(); // check for syntax errors
169 }
170 
171 FilePool::Directories FilePool::getDirectories() const
172 {
173  Directories result;
174  TclObject all(filePoolSetting->getValue());
175  unsigned numLines = all.getListLength();
176  for (unsigned i = 0; i < numLines; ++i) {
177  Entry entry;
178  bool hasPath = false;
179  entry.types = 0;
180  TclObject line = all.getListIndex(i);
181  unsigned numItems = line.getListLength();
182  if (numItems & 1) {
183  throw CommandException(
184  "Expected a list with an even number "
185  "of elements, but got " + line.getString());
186  }
187  for (unsigned j = 0; j < numItems; j += 2) {
188  string_ref name = line.getListIndex(j + 0).getString();
189  TclObject value = line.getListIndex(j + 1);
190  if (name == "-path") {
191  entry.path = value.getString().str();
192  hasPath = true;
193  } else if (name == "-types") {
194  entry.types = parseTypes(value);
195  } else {
196  throw CommandException(
197  "Unknown item: " + name);
198  }
199  }
200  if (!hasPath) {
201  throw CommandException(
202  "Missing -path item: " + line.getString());
203  }
204  if (entry.types == 0) {
205  throw CommandException(
206  "Missing -types item: " + line.getString());
207  }
208  result.push_back(entry);
209  }
210  return result;
211 }
212 
213 unique_ptr<File> FilePool::getFile(FileType fileType, const Sha1Sum& sha1sum)
214 {
215  unique_ptr<File> result;
216  result = getFromPool(sha1sum);
217  if (result.get()) {
218  return result;
219  }
220 
221  // not found in cache, need to scan directories
222  lastTime = Timer::getTime(); // for progress messages
223  amountScanned = 0; // also for progress messages
224  Directories directories;
225  try {
226  directories = getDirectories();
227  } catch (CommandException& e) {
228  cliComm.printWarning("Error while parsing '__filepool' setting" +
229  e.getMessage());
230  }
231  for (auto& d : directories) {
232  if (d.types & fileType) {
233  string path = FileOperations::expandTilde(d.path);
234  result = scanDirectory(sha1sum, path, d.path);
235  if (result.get()) {
236  return result;
237  }
238  }
239  }
240 
241  return result; // not found
242 }
243 
244 static Sha1Sum calcSha1sum(File& file, CliComm& cliComm, EventDistributor& distributor)
245 {
246  size_t size;
247  const byte* data = file.mmap(size);
248  return SHA1::calcWithProgress(data, size, file.getOriginalName(), cliComm, distributor);
249 }
250 
251 unique_ptr<File> FilePool::getFromPool(const Sha1Sum& sha1sum)
252 {
253  auto bound = pool.equal_range(sha1sum);
254  auto it = bound.first;
255  while (it != bound.second) {
256  auto& time = it->second.first;
257  const auto& filename = it->second.second;
258  try {
259  auto file = make_unique<File>(filename);
260  auto newTime = file->getModificationDate();
261  if (time == newTime) {
262  // When modification time is unchanged, assume
263  // sha1sum is also unchanged. So avoid
264  // expensive sha1sum calculation.
265  return file;
266  }
267  auto newSum = calcSha1sum(*file, cliComm, distributor);
268  if (newSum == sha1sum) {
269  // Modification time was changed, but
270  // (recalculated) sha1sum is still the same,
271  // only update timestamp.
272  time = newTime;
273  return file;
274  }
275  // Did not match: update db with new sum and continue
276  // searching.
277  remove(it++);
278  insert(newSum, newTime, filename);
279  } catch (FileException&) {
280  // Error reading file: remove from db and continue
281  // searching.
282  remove(it++);
283  }
284  }
285  return nullptr; // not found
286 }
287 
288 unique_ptr<File> FilePool::scanDirectory(const Sha1Sum& sha1sum, const string& directory, const string& poolPath)
289 {
290  ReadDir dir(directory);
291  while (dirent* d = dir.getEntry()) {
292  if (quit) {
293  // Scanning can take a long time. Allow to exit
294  // openmsx when it takes too long. Stop scanning
295  // by pretending we didn't find the file.
296  return nullptr;
297  }
298  string file = d->d_name;
299  string path = directory + '/' + file;
301  if (FileOperations::getStat(path, st)) {
302  unique_ptr<File> result;
304  result = scanFile(sha1sum, path, st, poolPath);
305  } else if (FileOperations::isDirectory(st)) {
306  if ((file != ".") && (file != "..")) {
307  result = scanDirectory(sha1sum, path, poolPath);
308  }
309  }
310  if (result.get()) {
311  return result;
312  }
313  }
314  }
315  return nullptr; // not found
316 }
317 
318 unique_ptr<File> FilePool::scanFile(const Sha1Sum& sha1sum, const string& filename,
319  const FileOperations::Stat& st, const string& poolPath)
320 {
321  amountScanned++;
322  // Periodically send a progress message with the current filename
323  auto now = Timer::getTime();
324  if (now > (lastTime + 250000)) { // 4Hz
325  lastTime = now;
326  cliComm.printProgress("Searching for file with sha1sum " +
327  sha1sum.toString() + "...\nIndexing filepool " + poolPath +
328  ": [" + StringOp::toString(amountScanned) + "]: " +
329  filename.substr(poolPath.size()));
330  }
331 
332  // deliverEvents() is relatively cheap when there are no events to
333  // deliver, so it's ok to call on each file.
334  distributor.deliverEvents();
335 
336  auto it = findInDatabase(filename);
337  if (it == pool.end()) {
338  // not in pool
339  try {
340  auto file = make_unique<File>(filename);
341  auto sum = calcSha1sum(*file, cliComm, distributor);
342  auto time = FileOperations::getModificationDate(st);
343  insert(sum, time, filename);
344  if (sum == sha1sum) {
345  return file;
346  }
347  } catch (FileException&) {
348  // ignore
349  }
350  } else {
351  // already in pool
352  assert(filename == it->second.second);
353  try {
354  auto time = FileOperations::getModificationDate(st);
355  if (time == it->second.first) {
356  // db is still up to date
357  if (it->first == sha1sum) {
358  return make_unique<File>(filename);
359  }
360  } else {
361  // db outdated
362  auto file = make_unique<File>(filename);
363  auto sum = calcSha1sum(*file, cliComm, distributor);
364  remove(it);
365  insert(sum, time, filename);
366  if (sum == sha1sum) {
367  return file;
368  }
369  }
370  } catch (FileException&) {
371  // error reading file, remove from db
372  remove(it);
373  }
374  }
375  return nullptr; // not found
376 }
377 
378 FilePool::Pool::iterator FilePool::findInDatabase(const string& filename)
379 {
380  auto it = reversePool.find(filename);
381  if (it != reversePool.end()) {
382  return it->second;
383  }
384  return pool.end();
385 }
386 
387 
389 {
390  auto time = file.getModificationDate();
391  const auto& filename = file.getURL();
392 
393  auto it = findInDatabase(filename);
394  if (it != pool.end()) {
395  // in database
396  if (time == it->second.first) {
397  // modification time matches, assume sha1sum also matches
398  return it->first;
399  } else {
400  // mismatch, remove from db and re-calculate
401  remove(it);
402  }
403  }
404  // not in db (or timestamp mismatch)
405  auto sum = calcSha1sum(file, cliComm, distributor);
406  insert(sum, time, filename);
407  return sum;
408 }
409 
411 {
412  auto it = findInDatabase(file.getURL());
413  if (it != pool.end()) {
414  remove(it);
415  }
416 }
417 
418 int FilePool::signalEvent(const std::shared_ptr<const Event>& event)
419 {
420  (void)event; // avoid warning for non-assert compiles
421  assert(event->getType() == OPENMSX_QUIT_EVENT);
422  quit = true;
423  return 0;
424 }
425 
426 } // namespace openmsx