Fawkes API  Fawkes Development Version
bblogfile.cpp
00001 
00002 /***************************************************************************
00003  *  bblogfile.cpp - BlackBoard log file access convenience class
00004  *
00005  *  Created: Sun Feb 21 11:27:41 2010
00006  *  Copyright  2006-2010  Tim Niemueller [www.niemueller.de]
00007  *
00008  ****************************************************************************/
00009 
00010 /*  This program is free software; you can redistribute it and/or modify
00011  *  it under the terms of the GNU General Public License as published by
00012  *  the Free Software Foundation; either version 2 of the License, or
00013  *  (at your option) any later version.
00014  *
00015  *  This program is distributed in the hope that it will be useful,
00016  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00017  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00018  *  GNU Library General Public License for more details.
00019  *
00020  *  Read the full text in the LICENSE.GPL file in the doc directory.
00021  */
00022 
00023 #include "bblogfile.h"
00024 
00025 #include <core/exceptions/system.h>
00026 #include <utils/misc/strndup.h>
00027 #include <blackboard/internal/instance_factory.h>
00028 
00029 #include <cstdlib>
00030 #include <cerrno>
00031 #include <cstring>
00032 #ifdef __FreeBSD__
00033 #  include <sys/endian.h>
00034 #elif defined(__MACH__) && defined(__APPLE__)
00035 #  include <sys/_endian.h>
00036 #else
00037 #  include <endian.h>
00038 #endif
00039 #include <arpa/inet.h>
00040 #include <sys/types.h>
00041 #include <sys/stat.h>
00042 #include <unistd.h>
00043 #include <sys/mman.h>
00044 
00045 using namespace fawkes;
00046 
00047 /** @class BBLogFile "bblogfile.h"
00048  * Class to easily access bblogger log files.
00049  * This class provides an easy way to interact with bblogger log files.
00050  * @author Tim Niemueller
00051  */
00052 
00053 /** Constructor.
00054  * Opens the given file and performs basic sanity checks.
00055  * @param filename log file to open
00056  * @param interface optional interface instance which must match the data
00057  * from the log file. Read methods will store read data in this interface
00058  * instance. If no interface is given an instance is created that is not
00059  * tied to a blackboard.
00060  * @param do_sanity_check true to perform a sanity check on the file on
00061  * opening. Turn this off only if you know what you are doing.
00062  * @exception CouldNotOpenFileException thrown if file cannot be opened
00063  * @exception FileReadException some error occured while reading data from
00064  */
00065 BBLogFile::BBLogFile(const char *filename, fawkes::Interface *interface,
00066                      bool do_sanity_check)
00067 {
00068   ctor(filename, do_sanity_check);
00069 
00070   if (interface) {
00071     __instance_factory = NULL;
00072     __interface = interface;
00073     if ((strcmp(__interface->type(), __interface_type) != 0) ||
00074         (strcmp(__interface->id(), __interface_id) != 0)) {
00075       fclose(__f);
00076       free(__filename);
00077       free(__scenario);
00078       free(__interface_type);
00079       free(__interface_id);
00080       throw Exception("Interface UID %s does not match expected %s:%s",
00081                       __interface->uid(), __interface_type, __interface_id);
00082     }
00083   } else {
00084     __instance_factory = new BlackBoardInstanceFactory();
00085     __interface = __instance_factory->new_interface_instance(__interface_type,
00086                                                              __interface_id);
00087   }
00088 }
00089 
00090 
00091 /** Constructor.
00092  * Opens the given file and performs basic sanity checks.
00093  * No internal interface is created. You must take care to set it using
00094  * set_interface() before any reading is done.
00095  * @param filename log file to open
00096  * @param do_sanity_check true to perform a sanity check on the file on
00097  * opening. Turn this off only if you know what you are doing.
00098  * @exception CouldNotOpenFileException thrown if file cannot be opened
00099  * @exception FileReadException some error occured while reading data from
00100  */
00101 BBLogFile::BBLogFile(const char *filename, bool do_sanity_check)
00102 {
00103   ctor(filename, do_sanity_check);
00104 
00105   __instance_factory = NULL;
00106   __interface        = NULL;
00107 }
00108 
00109 
00110 void
00111 BBLogFile::ctor(const char *filename, bool do_sanity_check)
00112 {
00113   __f = fopen(filename, "r");
00114   if (!__f) {
00115     throw CouldNotOpenFileException(filename, errno);
00116   }
00117 
00118   __filename = strdup(filename);
00119   __header   = (bblog_file_header *)malloc(sizeof(bblog_file_header));
00120 
00121   try {
00122     read_file_header();
00123     if (do_sanity_check)  sanity_check();
00124   } catch (Exception &e) {
00125     free(__filename);
00126     free(__scenario);
00127     free(__interface_type);
00128     free(__interface_id);
00129     fclose(__f);
00130     throw;
00131   }
00132 
00133   __ifdata = malloc(__header->data_size);
00134 }
00135 
00136 /** Destructor. */
00137 BBLogFile::~BBLogFile()
00138 {
00139   if (__instance_factory) {
00140     __instance_factory->delete_interface_instance(__interface);
00141     delete __instance_factory;
00142   }
00143 
00144   fclose(__f);
00145 
00146   free(__filename);
00147   free(__scenario);
00148   free(__interface_type);
00149   free(__interface_id);
00150 
00151   free(__header);
00152   free(__ifdata);
00153 }
00154 
00155 
00156 /** Read file header. */
00157 void
00158 BBLogFile::read_file_header()
00159 {
00160   uint32_t magic;
00161   uint32_t version;
00162   if ((fread(&magic, sizeof(uint32_t), 1, __f) == 1) &&
00163       (fread(&version, sizeof(uint32_t), 1, __f) == 1) ) {
00164     if ( (ntohl(magic) == BBLOGGER_FILE_MAGIC) &&
00165          (ntohl(version) == BBLOGGER_FILE_VERSION) ) {
00166       ::rewind(__f);
00167       if (fread(__header, sizeof(bblog_file_header), 1, __f) != 1) {
00168         throw FileReadException(__filename, errno, "Failed to read file header");
00169       }
00170     } else {
00171       throw Exception("File magic/version %X/%u does not match (expected %X/%u)",
00172                       ntohl(magic), ntohl(version),
00173                       BBLOGGER_FILE_VERSION, BBLOGGER_FILE_MAGIC);
00174     }
00175   } else {
00176     throw Exception(__filename, errno, "Failed to read magic/version from file");
00177   }
00178 
00179   __scenario = strndup(__header->scenario, BBLOG_SCENARIO_SIZE);
00180   __interface_type = strndup(__header->interface_type, BBLOG_INTERFACE_TYPE_SIZE);
00181   __interface_id = strndup(__header->interface_id, BBLOG_INTERFACE_ID_SIZE);
00182 
00183   __start_time.set_time(__header->start_time_sec, __header->start_time_usec);
00184 }
00185 
00186 
00187 /** Perform sanity checks.
00188  * This methods performs some sanity checks like:
00189  * - check if number of items is 0
00190  * - check if number of items and file size match
00191  * - check endianess of file and system
00192  * - check if file seems to be truncated
00193  */
00194 void
00195 BBLogFile::sanity_check()
00196 {
00197   if (__header->num_data_items == 0) {
00198     Exception e("File %s does not specify number of data items", __filename);
00199     e.set_type_id("bblogfile-num-items-zero");
00200     throw e;
00201   }
00202 
00203   struct stat fs;
00204   if (fstat(fileno(__f), &fs) != 0) {
00205     Exception e(errno, "Failed to stat file %s", __filename);
00206     e.set_type_id("bblogfile-stat-failed");
00207     throw e;
00208   }
00209 
00210   long int expected_size = sizeof(bblog_file_header)
00211     + __header->num_data_items * __header->data_size
00212     + __header->num_data_items * sizeof(bblog_entry_header);
00213   if (expected_size != fs.st_size) {
00214     Exception e("Size of file %s does not match expectation "
00215                 "(actual: %li, actual: %li)",
00216                 __filename, expected_size, (long int)fs.st_size);
00217     e.set_type_id("bblogfile-file-size-mismatch");
00218     throw e;
00219   }
00220 
00221 #if __BYTE_ORDER == __LITTLE_ENDIAN
00222   if (__header->endianess == 1)
00223 #else
00224   if (__header->endianess == 0)
00225 #endif
00226   {
00227     Exception e("File %s has incompatible endianess", __filename);
00228     e.set_type_id("bblogfile-endianess-mismatch");
00229     throw e;
00230   }
00231 }
00232 
00233 /** Read entry at particular index.
00234  * @param index index of entry, 0-based
00235  */
00236 void
00237 BBLogFile::read_index(unsigned int index)
00238 {
00239   long offset = sizeof(bblog_file_header)
00240     + (sizeof(bblog_entry_header) + __header->data_size) * index;
00241 
00242   if (fseek(__f, offset, SEEK_SET) != 0) {
00243     throw Exception(errno, "Cannot seek to index %u", index);
00244   }
00245 
00246   read_next();
00247 }
00248 
00249 
00250 /** Rewind file to start.
00251  * This moves the file cursor immediately before the first entry.
00252  */
00253 void
00254 BBLogFile::rewind()
00255 {
00256   if (fseek(__f, sizeof(bblog_file_header), SEEK_SET) != 0) {
00257     throw Exception(errno, "Cannot reset file");
00258   }
00259   __entry_offset.set_time(0, 0);
00260 }
00261 
00262 
00263 /** Check if another entry is available.
00264  * @return true if a consecutive read_next() will succeed, false otherwise
00265  */
00266 bool
00267 BBLogFile::has_next()
00268 {
00269   // we always re-test to support continuous file watching
00270   clearerr(__f);
00271   if (getc(__f) == EOF) {
00272     return false;
00273   } else {
00274     fseek(__f, -1, SEEK_CUR);
00275     return true;
00276   }
00277 }
00278 
00279 /** Read next entry.
00280  * @exception Exception thrown if reading fails, for example because no more
00281  * entries are left.
00282  */
00283 void
00284 BBLogFile::read_next()
00285 {
00286   bblog_entry_header entryh;
00287 
00288   if ( (fread(&entryh, sizeof(bblog_entry_header), 1, __f) == 1) &&
00289        (fread(__ifdata, __header->data_size, 1, __f) == 1) ) {
00290     __entry_offset.set_time(entryh.rel_time_sec, entryh.rel_time_usec);
00291     __interface->set_from_chunk(__ifdata);
00292   } else {
00293     throw Exception("Cannot read interface data");
00294   }
00295 }
00296 
00297 
00298 /** Set number of entries.
00299  * Set the number of entries in the file. Attention, this is only to be used
00300  * by the repair() method.
00301  * @param num_entries number of entries
00302  */
00303 void
00304 BBLogFile::set_num_entries(size_t num_entries)
00305 {
00306 #if _POSIX_MAPPED_FILES
00307   void *h = mmap(NULL, sizeof(bblog_file_header), PROT_WRITE, MAP_SHARED,
00308                    fileno(__f), 0);
00309   if (h == MAP_FAILED) {
00310     throw Exception(errno, "Failed to mmap log, not updating number of data items");
00311   } else {
00312     bblog_file_header *header = (bblog_file_header *)h;
00313     header->num_data_items = num_entries;
00314     munmap(h, sizeof(bblog_file_header));
00315   }
00316 #else
00317   throw Exception("Cannot set number of entries, mmap not available.");
00318 #endif    
00319 }
00320 
00321 
00322 /** Repair file.
00323  * @param filename file to repair
00324  * @see repair()
00325  */
00326 void
00327 BBLogFile::repair_file(const char *filename)
00328 {
00329   BBLogFile file(filename, NULL, false);
00330   file.repair();
00331 }
00332 
00333 
00334 /** This tries to fix files which are in an inconsistent state.
00335  * On success, an exception is thrown with a type_id of "repair-success", which
00336  * will have an entry for each successful operation.
00337  */
00338 void
00339 BBLogFile::repair()
00340 {
00341   FILE *f = freopen(__filename, "r+", __f);
00342   if (! f) {
00343     throw Exception("Reopening file %s with new mode failed", __filename);
00344   }
00345   __f = f;
00346 
00347   bool repair_done = false;
00348 
00349   Exception success("Successfully repaired file");
00350   success.set_type_id("repair-success");
00351 
00352 #if __BYTE_ORDER == __LITTLE_ENDIAN
00353   if (__header->endianess == 1)
00354 #else
00355   if (__header->endianess == 0)
00356 #endif
00357   {
00358     throw Exception("File %s has incompatible endianess. Cannot repair.",
00359                     __filename);
00360   }
00361 
00362   struct stat fs;
00363   if (fstat(fileno(__f), &fs) != 0) {
00364     throw Exception(errno, "Failed to stat file %s", __filename);
00365   }
00366 
00367   size_t entry_size = sizeof(bblog_entry_header) + __header->data_size;
00368   size_t all_entries_size = fs.st_size - sizeof(bblog_file_header);
00369   size_t num_entries = all_entries_size / entry_size;
00370   size_t extra_bytes = all_entries_size % entry_size;
00371 
00372   if (extra_bytes != 0) {
00373     success.append("FIXING: errorneous bytes at end of file, "
00374                    "truncating by %zu b", extra_bytes);
00375     if (ftruncate(fileno(__f), fs.st_size - extra_bytes) == -1) {
00376       throw Exception(errno, "Failed to truncate file %s", __filename);
00377     }
00378     all_entries_size -= extra_bytes;
00379     extra_bytes = 0;
00380     if (fstat(fileno(__f), &fs) != 0) {
00381       throw Exception(errno, "Failed to update information of file %s "
00382                       "after truncate", __filename);
00383     }
00384     repair_done = true;
00385   }
00386   if (__header->num_data_items == 0) {
00387     success.append("FIXING: header of file %s has 0 data items, setting to %zu.",
00388                    __filename, num_entries);
00389     set_num_entries(num_entries);
00390     repair_done = true;
00391   } else if (__header->num_data_items != num_entries) {
00392     success.append("FIXING: header has %u data items, but expecting %zu, setting",
00393                    __header->num_data_items, num_entries);
00394     set_num_entries(num_entries);
00395     repair_done = true;
00396   }
00397 
00398   f = freopen(__filename, "r", __f);
00399   if (! f) {
00400     throw Exception("Reopening file %s with read-only mode failed", __filename);
00401   }
00402   __f = f;
00403 
00404   if (repair_done) {
00405     throw success;
00406   }
00407 }
00408 
00409 
00410 /** Print file meta info.
00411  * @param line_prefix a prefix printed before each line
00412  * @param outf file handle to print to
00413  */
00414 void
00415 BBLogFile::print_info(const char *line_prefix, FILE *outf)
00416 {
00417   char interface_hash[BBLOG_INTERFACE_HASH_SIZE * 2 + 1];
00418 
00419   for (unsigned int i = 0; i < BBLOG_INTERFACE_HASH_SIZE; ++i) {
00420     snprintf(&interface_hash[i*2], 3, "%02X", __header->interface_hash[i]);
00421   }
00422 
00423   struct stat fs;
00424   if (fstat(fileno(__f), &fs) != 0) {
00425     throw Exception(errno, "Failed to get stat file");
00426   }
00427 
00428   fprintf(outf,
00429           "%sFile version: %-10u  Endianess: %s Endian\n"
00430           "%s# data items: %-10u  Data size: %u bytes\n"
00431           "%sHeader size:  %zu bytes   File size: %li bytes\n"
00432           "%s\n"
00433           "%sScenario:   %s\n"
00434           "%sInterface:  %s::%s (%s)\n"
00435           "%sStart time: %s\n",
00436           line_prefix, ntohl(__header->file_version),
00437           (__header->endianess == 1) ? "Big" : "Little",
00438           line_prefix, __header->num_data_items, __header->data_size,
00439           line_prefix, sizeof(bblog_file_header), (long int)fs.st_size,
00440           line_prefix,
00441           line_prefix, __scenario,
00442           line_prefix, __interface_type, __interface_id, interface_hash,
00443           line_prefix, __start_time.str());
00444 
00445 }
00446 
00447 
00448 /** Print an entry.
00449  * Verbose print of a single entry.
00450  * @param outf file handle to print to
00451  */
00452 void
00453 BBLogFile::print_entry(FILE *outf)
00454 {
00455   fprintf(outf, "Time Offset: %f\n", __entry_offset.in_sec());
00456 
00457   InterfaceFieldIterator i;
00458   for (i = __interface->fields(); i != __interface->fields_end(); ++i) {
00459     char *typesize;
00460     if (i.get_length() > 1) {
00461       if (asprintf(&typesize, "%s[%zu]", i.get_typename(), i.get_length()) == -1) {
00462         throw Exception("Out of memory");
00463       }
00464     } else {
00465       if (asprintf(&typesize, "%s", i.get_typename()) == -1) {
00466         throw Exception("Out of memory");
00467       }
00468     }
00469     fprintf(outf, "%-16s %-18s: %s\n",
00470             i.get_name(), typesize, i.get_value_string());
00471     free(typesize);
00472   }
00473 }
00474 
00475 
00476 /** Get interface instance.
00477  * @return internally used interface
00478  */
00479 fawkes::Interface *
00480 BBLogFile::interface()
00481 {
00482   return __interface;
00483 }
00484 
00485 
00486 /** Set the internal interface.
00487  * @param interface an interface matching the type and ID given in the
00488  * log file.
00489  */
00490 void
00491 BBLogFile::set_interface(fawkes::Interface *interface)
00492 {
00493   if ( (strcmp(interface->type(), __interface_type) == 0) &&
00494        (strcmp(interface->id(), __interface_id) == 0) &&
00495        (memcmp(interface->hash(), __header->interface_hash,
00496                __INTERFACE_HASH_SIZE) == 0) ) {
00497     if (__instance_factory) {
00498       __instance_factory->delete_interface_instance(__interface);
00499       delete __instance_factory;
00500       __instance_factory = NULL;
00501     }
00502     __interface = interface;
00503   } else {
00504     throw TypeMismatchException("Interfaces incompatible");
00505   }
00506 }
00507 
00508 
00509 /** Get current entry offset.
00510  * @return offset from start time of current entry (may be 0 if no entry has
00511  * been read, yet, or after rewind()).
00512  */
00513 const fawkes::Time &
00514 BBLogFile::entry_offset() const
00515 {
00516   return __entry_offset;
00517 }
00518 
00519 
00520 /** Get file version.
00521  * @return file version
00522  */
00523 uint32_t
00524 BBLogFile::file_version() const
00525 {
00526   return ntohl(__header->file_version);
00527 }
00528 
00529 
00530 /** Check if file is big endian.
00531  * @return true if file is big endian, false otherwise
00532  */
00533 bool
00534 BBLogFile::is_big_endian() const
00535 {
00536   return (__header->endianess == 1);
00537 }
00538 
00539 /** Get number of data items in file.
00540  * @return number of data items
00541  */
00542 uint32_t
00543 BBLogFile::num_data_items() const
00544 {
00545   return __header->num_data_items;
00546 }
00547 
00548 
00549 /** Get scenario identifier.
00550  * @return scenario identifier
00551  */
00552 const char *
00553 BBLogFile::scenario() const
00554 {
00555   return __scenario;
00556 }
00557 
00558 
00559 /** Get interface type.
00560  * @return type of logged interface
00561  */
00562 const char *
00563 BBLogFile::interface_type() const
00564 {
00565   return __interface_type;
00566 }
00567 
00568 
00569 /** Get interface ID.
00570  * @return ID of logged interface
00571  */
00572 const char *
00573 BBLogFile::interface_id() const
00574 {
00575   return __interface_id;
00576 }
00577 
00578 
00579 /** Get interface hash.
00580  * Hash of logged interface.
00581  * @return interface hash
00582  */
00583 unsigned char *
00584 BBLogFile::interface_hash() const
00585 {
00586   return __header->interface_hash;
00587 }
00588 
00589 
00590 /** Get data size.
00591  * @return size of the pure data part of the log entries
00592  */
00593 uint32_t
00594 BBLogFile::data_size()
00595 {
00596   return __header->data_size;
00597 }
00598 
00599 
00600 /** Get start time.
00601  * @return starting time of log
00602  */
00603 fawkes::Time &
00604 BBLogFile::start_time()
00605 {
00606   return __start_time;
00607 }
00608 
00609 
00610 /** Get number of remaining entries.
00611  * @return number of remaining entries
00612  */
00613 unsigned int
00614 BBLogFile::remaining_entries()
00615 {
00616   // we make this so "complicated" to be able to use it from a FAM handler
00617   size_t entry_size = sizeof(bblog_entry_header) + __header->data_size;
00618   long   curpos     = ftell(__f);
00619   size_t fsize      = file_size();
00620   size_t sizediff   = fsize - curpos;
00621 
00622   if (sizediff < 0) {
00623     throw Exception("File %s shrank while reading it", __filename);
00624   }
00625 
00626   return sizediff / entry_size;
00627 }
00628 
00629 /** Get file size.
00630  * @return total size of log file including all headers
00631  */
00632 size_t
00633 BBLogFile::file_size() const
00634 {
00635   struct stat fs;
00636   if (fstat(fileno(__f), &fs) != 0) {
00637     Exception e(errno, "Failed to stat file %s", __filename);
00638     e.set_type_id("bblogfile-stat-failed");
00639     throw e;
00640   }
00641   return fs.st_size;
00642 }