/*! \file pibinarylog.h * \ingroup IO * \~\brief * \~english Binary log * \~russian Бинарный лог */ /* PIP - Platform Independent Primitives Class for write binary data to logfile, and read or playback this data Andrey Bychkov work.a.b@yandex.ru This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . */ #ifndef PIBINARYLOG_H #define PIBINARYLOG_H #include "pichunkstream.h" #include "pifile.h" //! \ingroup IO //! \~\brief //! \~english Binary log //! \~russian Бинарный лог class PIP_EXPORT PIBinaryLog: public PIIODevice { PIIODEVICE(PIBinaryLog, "binlog"); public: explicit PIBinaryLog(); virtual ~PIBinaryLog(); //! \brief Play modes for \a PIBinaryLog enum PlayMode { PlayRealTime /*! Play in system realtime, default mode */, PlayVariableSpeed /*! Play in software realtime with speed, set by \a setSpeed */, PlayStaticDelay /*! Play with custom static delay, ignoring timestamp */ }; //! \brief Different split modes for writing \a PIBinaryLog, which can separate files by size, by time or by records count enum SplitMode { SplitNone /*! Without separate, default mode */, SplitTime /*! Separate files by record time */, SplitSize /*! Separate files by size */, SplitCount /*! Separate files by records count */ }; #pragma pack(push, 8) //! \brief Struct contains information about all records with same ID struct PIP_EXPORT BinLogRecordInfo { BinLogRecordInfo() { id = count = 0; minimum_size = maximum_size = 0; } int id; int count; int minimum_size; int maximum_size; PISystemTime start_time; PISystemTime end_time; }; //! \brief Struct contains position, ID and timestamp of record in file struct PIP_EXPORT BinLogIndex { int id; int data_size; llong pos; PISystemTime timestamp; }; #pragma pack(pop) //! \brief Struct contains full information about Binary Log file and about all Records using map of \a BinLogRecordInfo struct PIP_EXPORT BinLogInfo { PIString path; int records_count = 0; llong log_size = 0L; PISystemTime start_time; PISystemTime end_time; PIMap records; PIByteArray user_header; }; //! Current \a PlayMode PlayMode playMode() const { return play_mode; } //! Current \a SplitMode SplitMode splitMode() const { return split_mode; } //! Current directory where billogs wiil be saved PIString logDir() const { return property("logDir").toString(); } //! Returns current file prefix PIString filePrefix() const { return property("filePrefix").toString(); } //! Default ID, used in \a write function int defaultID() const { return default_id; } //! Returns current play speed double playSpeed() const { return play_speed > 0 ? 1. / play_speed : 0.; } //! Returns current play delay PISystemTime playDelay() const { return play_delay; } //! Returns current binlog file split time PISystemTime splitTime() const { return split_time; } //! Returns current binlog file split size llong splitFileSize() const { return split_size; } //! Returns current binlog file split records count int splitRecordCount() const { return split_count; } //! Returns if rapid start enabled bool rapidStart() const { return rapid_start; } //! Returns if index creates while writing bool createIndexOnFly() const { return create_index_on_fly; } //! Create binlog file with Filename = path void createNewFile(const PIString & path); //! Set \a PlayMode void setPlayMode(PlayMode mode) { setProperty("playMode", (int)mode); } //! Set \a SplitMode void setSplitMode(SplitMode mode) { setProperty("splitMode", (int)mode); } //! Set path to directory where binlogs will be saved void setLogDir(const PIString & path) { setProperty("logDir", path); } //! Set file prefix, used to void setFilePrefix(const PIString & prefix) { setProperty("filePrefix", prefix); } //! Set defaultID, used in \a write function void setDefaultID(int id) { setProperty("defaultID", id); } //! If enabled BinLog \a ThreadedRead starts without delay for first record, i.e. first record will be readed immediately void setRapidStart(bool enabled) { setProperty("rapidStart", enabled); } //! Set index creation while writing void setCreateIndexOnFly(bool yes); //! Set play speed to "speed", default value is 1.0x //! Also this function set \a playMode to \a PlayVariableSpeed void setPlaySpeed(double speed) { setPlayMode(PlayVariableSpeed); setProperty("playSpeed", speed); } //! Setting static delay between records, default value is 1 sec //! Also this function set \a playMode to \a PlayStaticDelay void setPlayDelay(const PISystemTime & delay) { setPlayMode(PlayStaticDelay); setProperty("playDelay", delay); } //! Set \a playMode to \a PlayRealTime void setPlayRealTime() { setPlayMode(PlayRealTime); } //! Set binlog file split time //! Also this function set \a splitMode to \a SplitTime void setSplitTime(const PISystemTime & time) { setSplitMode(SplitTime); setProperty("splitTime", time); } //! Set binlog file split size //! Also this function set \a splitMode to \a SplitSize void setSplitFileSize(llong size) { setSplitMode(SplitSize); setProperty("splitFileSize", size); } //! Set binlog file split records count //! Also this function set \a splitMode to \a SplitCount void setSplitRecordCount(int count) { setSplitMode(SplitCount); setProperty("splitRecordCount", count); } //! Set pause while playing via \a threadedRead or writing via write void setPause(bool pause); //! Set function wich returns new binlog file path when using split mode. //! Overrides internal file path generator (logdir() + prefix() + current_time()). //! To restore internal file path generator set this function to "nullptr". void setFuncGetNewFilePath(std::function f) { f_new_path = f; } //! Write one record to BinLog file, with ID = id, id must be greather than 0 int writeBinLog(int id, PIByteArray data) { return writeBinLog(id, data.data(), data.size_s()); } //! Write one record to BinLog file, with ID = id, id must be greather than 0 int writeBinLog(int id, const void * data, int size); //! Write one RAW record to BinLog file, with ID = id, Timestamp = time int writeBinLog_raw(int id, const PISystemTime & time, const PIByteArray & data) { return writeBinLog_raw(id, time, data.data(), data.size_s()); } int writeBinLog_raw(int id, const PISystemTime & time, const void * data, int size); //! Returns count of writed records int writeCount() const { return write_count; } //! Read one record from BinLog file, with ID = id, if id = 0 than any id will be readed PIByteArray readBinLog(int id = 0, PISystemTime * time = 0, int * readed_id = 0); //! Read one record from BinLog file, with ID = id, if id = 0 than any id will be readed int readBinLog(int id, void * read_to, int max_size, PISystemTime * time = 0, int * readed_id = 0); //! Returns binary log file size llong logSize() const { return log_size; } //! Return position in current binlog file llong logPos() const { return file.pos(); } //! Return true, if position at the end of BinLog file bool isEnd() const { if (isClosed()) return true; return file.isEnd(); } //! Returns if BinLog file is empty bool isEmpty() const; //! Returns BinLog pause status bool isPause() const { return is_pause; } //! Returns id of last readed record int lastReadedID() const { return lastrecord.id; } //! Returns timestamp of last readed record PISystemTime lastReadedTimestamp() const { return lastrecord.timestamp; } //! Returns timestamp of log start PISystemTime logStartTimestamp() const { return startlogtime; } //! Set custom file header, you can get it back when read this binlog void setHeader(const PIByteArray & header); //! Get custom file header PIByteArray getHeader() const; #ifdef DOXYGEN //! Read one message from binlog file, with ID contains in "filterID" or any ID, if "filterID" is empty int read(void * read_to, int max_size); //! Write one record to BinLog file, with ID = "defaultID" int write(const void * data, int size); #endif //! Array of ID, that BinLog can read from binlog file, when use \a read function, or in \a ThreadedRead PIVector filterID; //! Go to begin of BinLog file void restart(); //! Get binlog info \a BinLogInfo BinLogInfo logInfo() const { if (is_indexed) return index.info; return getLogInfo(path()); } //! Get binlog index \a BinLogIndex, need \a createIndex before getting index const PIVector & logIndex() const { return index.index; } //! Create index of current binlog file bool createIndex(); //! Return if current binlog file is indexed bool isIndexed() { return is_indexed; } //! Find nearest record of time \"time\". Returns -1 if not indexed or time less than first record int posForTime(const PISystemTime & time); //! Go to record #index void seekTo(int rindex); //! Go to nearest record bool seek(const PISystemTime & time); //! Set position in file to reading/playing bool seek(llong filepos); //! Get current record index (position record in file) int pos() const; PIByteArray saveIndex() const; bool loadIndex(PIByteArray saved); //! \handlers //! \{ //! \fn PIString createNewFile() //! \brief Create new binlog file in \a logDir, if successful returns filename, else returns empty string. //! Filename is like \a filePrefix + "yyyy_MM_dd__hh_mm_ss.binlog" //! \} //! \events //! \{ //! \fn void fileEnd() //! \brief Raise on file end while reading //! \fn void fileError() //! \brief Raise on file creation error //! \fn void newFile(const PIString & filename) //! \brief Raise on new file created //! \} EVENT_HANDLER(PIString, createNewFile); EVENT(fileEnd); EVENT(fileError); EVENT1(newFile, const PIString &, filename); EVENT1(posChanged, int, pos); EVENT3(threadedReadRecord, PIByteArray, data, int, id, PISystemTime, time); //! Get binlog info and statistic static BinLogInfo getLogInfo(const PIString & path); //! Create new binlog from part of "src" with allowed IDs and "from" to "to" file position static bool cutBinLog(const BinLogInfo & src, const PIString & dst, int from, int to); //! Create new binlog from serial splitted binlogs "src" static bool joinBinLogsSerial(const PIStringList & src, const PIString & dst, std::function progress = nullptr); protected: PIString constructFullPathDevice() const override; void configureFromFullPathDevice(const PIString & full_path) override; PIPropertyStorage constructVariantDevice() const override; void configureFromVariantDevice(const PIPropertyStorage & d) override; ssize_t readDevice(void * read_to, ssize_t max_size) override; ssize_t writeDevice(const void * data, ssize_t size) override; bool openDevice() override; bool closeDevice() override; void propertyChanged(const char * s) override; bool threadedRead(const uchar * readed, ssize_t size) override; DeviceInfoFlags deviceInfoFlags() const override { return PIIODevice::Reliable; } private: struct Record { int id; int size; PISystemTime timestamp; PIByteArray data; }; struct CompleteIndex { void clear(); void makeIndexPos(); BinLogInfo info; PIVector index; PIMap index_pos; }; BINARY_STREAM_FRIEND(CompleteIndex) bool writeFileHeader(); bool checkFileHeader(); Record readRecord(); int writeRecord(int id, PISystemTime time, const void * data, int size); static void parseLog(PIFile * f, BinLogInfo * info, PIVector * index); void moveIndex(int i); static PIString getLogfilePath(const PIString & log_dir, const PIString & prefix); CompleteIndex index; PlayMode play_mode; SplitMode split_mode; PIFile file; Record lastrecord; PISystemTime startlogtime, play_delay, split_time, pause_time; mutable PIMutex logmutex, pausemutex; double play_time, play_speed; llong split_size, log_size; int write_count, split_count, default_id, current_index; bool is_started, is_thread_ok, is_indexed, rapid_start, is_pause, create_index_on_fly; PIByteArray user_header; std::function f_new_path; }; BINARY_STREAM_WRITE(PIBinaryLog::BinLogInfo) { PIChunkStream cs; cs.add(1, v.path) .add(2, v.records_count) .add(3, v.log_size) .add(4, v.start_time) .add(5, v.end_time) .add(6, v.records) .add(7, v.user_header); s << cs.data(); return s; } BINARY_STREAM_READ(PIBinaryLog::BinLogInfo) { PIByteArray csba; s >> csba; PIChunkStream cs(csba); while (!cs.atEnd()) { switch (cs.read()) { case 1: cs.get(v.path); break; case 2: cs.get(v.records_count); break; case 3: cs.get(v.log_size); break; case 4: cs.get(v.start_time); break; case 5: cs.get(v.end_time); break; case 6: cs.get(v.records); break; case 7: cs.get(v.user_header); break; } } return s; } BINARY_STREAM_WRITE(PIBinaryLog::CompleteIndex) { s << v.info << v.index; return s; } BINARY_STREAM_READ(PIBinaryLog::CompleteIndex) { s >> v.info >> v.index; return s; } //! \relatesalso PICout \brief Output operator PIBinaryLog::BinLogInfo to PICout inline PICout operator<<(PICout s, const PIBinaryLog::BinLogInfo & bi) { s.space(); s.saveAndSetControls(0); s << "[PIBinaryLog] " << bi.path << "\n"; if (bi.log_size < 0) { s << "invalid file path"; s.restoreControls(); return s; } if (bi.log_size == 0) { s << "Invalid empty file"; s.restoreControls(); return s; } if (bi.records_count < 0 && bi.records_count > -4) { s << "Invalid file or corrupted signature"; s.restoreControls(); return s; } if (bi.records_count < -3) { s << "Invalid binlog version"; s.restoreControls(); return s; } s << "read records " << bi.records_count << " in " << bi.records.size() << " types, log size " << bi.log_size; s << "\nlog start " << bi.start_time << " , log end " << bi.end_time; const auto keys = bi.records.keys(); for (int i: keys) { const auto & bri(bi.records.at(i)); s << "\n record id " << bri.id << " , count " << bri.count; s << "\n record start " << bri.start_time << " , end " << bri.end_time; s << "\n record size " << bri.minimum_size << " - " << bri.maximum_size; } s.restoreControls(); return s; } #endif // PIBINARYLOG_H