OSCAR-code/oscar/SleepLib/xmlreplay.h
sawinglogz 7316ac676c Add a menu item to create a zip of all diagnostic logs.
Also fix a buffering issue in XmlRecorder that left devices.xml empty
in the zip.
2020-07-21 13:25:17 -04:00

325 lines
14 KiB
C++

/* XML event recording/replay
*
* Copyright (c) 2020 The OSCAR Team
*
* This file is subject to the terms and conditions of the GNU General Public
* License. See the file COPYING in the main directory of the source code
* for more details. */
#ifndef XMLREPLAY_H
#define XMLREPLAY_H
#include <QString>
#include <QDateTime>
#include <QMutex>
#include <QXmlStreamWriter>
#include <QXmlStreamReader>
#include <QObject>
/*
* XML recording base class
*
* While this can be used on its own via the public constructors, it is
* typically used as a base class for a subclasses that handle specific
* events.
*
* A single instance of this class can write a linear sequence of events to
* XML, either to a string (for testing) or to a file (for production use).
*
* Sometimes, however, there is need for certain sequences to be treated as
* separate, either due to multithreading (such recording as multiple
* simultaneous connections), or in order to treat a certain excerpt (such
* as data download that we might wish to archive) separately.
*
* These sequences are handled as "substreams" of the parent stream. The
* parent stream will typically record a substream's open/close or start/
* stop along with its ID. The substream will be written to a separate XML
* stream identified by that ID. Substreams are implemented as subclasses of
* this base class.
*
* TODO: At the moment, only file-based substreams are supported. In theory
* it should be possible to cache string-based substreams and then insert
* them inline into the parent after the substream-close event is recorded.
*/
class XmlRecorder
{
public:
static const QString TAG; // default tag if no subclass
XmlRecorder(class QFile * file, const QString & tag = XmlRecorder::TAG); // record XML to the given file
XmlRecorder(QString & string, const QString & tag = XmlRecorder::TAG); // record XML to the given string
virtual ~XmlRecorder(); // write the epilogue and close the recorder
XmlRecorder* closeSubstream(); // convenience function to close out a substream and return its parent
inline QXmlStreamWriter & xml() { return *m_xml; }
inline void lock() { m_mutex.lock(); }
inline void unlock() { m_mutex.unlock(); }
void flush();
protected:
XmlRecorder(XmlRecorder* parent, const QString & id, const QString & tag); // constructor used by substreams
QXmlStreamWriter* addSubstream(XmlRecorder* child, const QString & id); // initialize a child substream, used by above constructor
const QString m_tag; // opening/closing tag for this instance
QFile* m_file; // nullptr for non-file recordings
QXmlStreamWriter* m_xml; // XML output stream
QMutex m_mutex; // force one thread at a time to write to m_xml
XmlRecorder* m_parent; // parent instance of a substream
void prologue();
void epilogue();
};
/*
* XML replay base class
*
* A single instance of this class caches events from a previously recorded
* XML stream, either from a string (for testing) or from a file (for
* production use).
*
* Unlike recording, the replay need not be strictly linear. In fact, the
* implementation is designed to allow for limited reordering during replay,
* so that minor changes to code should result in sensible replay until a
* new recording can be made.
*
* There are two aspects to this reordering:
*
* First, events can be retrieved (and consumed) in any order, being
* retrieved by type and ID (and then in order within that type and ID).
*
* Second, events that are flagged as random-access (see randomAccess below)
* will cause the above retrieval to subsequently begin searching on or
* after the random-access event's timestamp (except for other random-access
* events, which are always searched from the beginning.)
*
* This allow non-stateful events to be replayed arbitrarily, and for
* stateful events (such as commands sent to a device) to be approximated
* (where subsequent data received matches the command sent).
*
* Furthermore, when events are triggered in the same order as they were
* during recordering, the above reordering will have no effect, and the
* original ordering will be replayed identically.
*
* See XmlRecorder above for a discussion of substreams.
*/
class XmlReplay
{
public:
XmlReplay(class QFile * file, const QString & tag = XmlRecorder::TAG); // replay XML from the given file
XmlReplay(QXmlStreamReader & xml, const QString & tag = XmlRecorder::TAG); // replay XML from the given stream
virtual ~XmlReplay();
XmlReplay* closeSubstream(); // convenience function to close out a substream and return its parent
//! \brief Retrieve next matching event of the given XmlReplayEvent subclass.
template<class T> inline T* getNextEvent(const QString & id = "")
{
T* event = dynamic_cast<T*>(getNextEvent(T::TAG, id));
return event;
}
protected:
XmlReplay(XmlReplay* parent, const QString & id, const QString & tag = XmlRecorder::TAG); // constructor used by substreams
QXmlStreamReader* findSubstream(XmlReplay* child, const QString & id); // initialize a child substream, used by above constructor
void deserialize(QXmlStreamReader & xml);
void deserializeEvents(QXmlStreamReader & xml);
class XmlReplayEvent* getNextEvent(const QString & type, const QString & id = "");
void seekToTime(const QDateTime & time);
const QString m_tag; // opening/closing tag for this instance
QFile* m_file; // nullptr for non-file replay
QHash<QString,QHash<QString,QList<XmlReplayEvent*>>> m_eventIndex; // type and ID-based index into the events, see discussion of reordering above
QHash<QString,QHash<QString,int>> m_indexPosition; // positions at which to begin searching the index, updated by random-access events
QList<XmlReplayEvent*> m_events; // linear list of all events in their original order
XmlReplayEvent* m_pendingSignal; // the signal (if any) that should be replayed as soon as the current event has been processed
QMutex m_lock; // prevent signals from being dispatched while an event is being processed, see XmlReplayLock below
XmlReplay* m_parent; // parent instance of a substream
inline void lock() { m_lock.lock(); }
inline void unlock() { m_lock.unlock(); }
void processPendingSignals(const QObject* target);
friend class XmlReplayLock;
};
/*
* XML replay event base class
*
* This class is used to represent a replayable event. An event is created
* when performing any replayable action, and then recorded (via record())
* when appropriate. During replay, an event is retrieved from the XmlReplay
* instance and its previously recorded result should be returned instead of
* performing the original action.
*
* Subclasses are created as subclasses of the XmlReplayBase template (see
* below), which handles their factory method and tag registration.
*
* Subclasses that should be retrieved by ID as well as type will need to
* override id() to return the ID to use for indexing.
*
* Subclasses that represent signal events (rather than API calls) will need
* to set their m_signal string to the name of the signal to be emitted, and
* additionally override signal() if they need to pass parameters with the
* signal.
*
* Subclasses that represent random-access events (see discussion above)
* will need to override randomAccess() to return true.
*
* Subclasses whose XML contains raw hexadecimal data will need to override
* usesData() to return true. Subclasses whose XML contains other data
* (such as complex data types) will instead need to override read() and
* write().
*/
class XmlReplayEvent
{
public:
XmlReplayEvent();
virtual ~XmlReplayEvent() = default;
//! \brief Add the given key/value to the event. This will be written as an XML attribute in the order it added.
void set(const QString & name, const QString & value);
//! \brief Add the given key/integer to the event. This will be written as an XML attribute in the order it added.
void set(const QString & name, qint64 value);
//! \brief Add the raw data to the event. This will be written in hexadecimal as content of the event's XML tag.
void setData(const char* data, qint64 length);
//! \brief Get the value for the given key.
QString get(const QString & name) const;
//! \brief Get the raw data for this event.
QByteArray getData() const;
//! \brief True if there are no errors in this event, or false if the "error" attribute is set.
inline bool ok() const { return m_values.contains("error") == false; }
//! \brief Copy the result from the retrieved replay event (if any) into the current event.
void copyIf(const XmlReplayEvent* other);
//! \brief Record this event to the given XML recorder, doing nothing if the recorder is null.
void record(XmlRecorder* xml) const;
// Serialize this event to an XML stream.
friend QXmlStreamWriter & operator<<(QXmlStreamWriter & xml, const XmlReplayEvent & event);
// Deserialize this event's contents from an XML stream. The instance is first created via createInstance() based on the tag.
friend QXmlStreamReader & operator>>(QXmlStreamReader & xml, XmlReplayEvent & event);
// Write the tag's attributes and contents.
void writeTag(QXmlStreamWriter & xml) const;
//! \brief Return a string of this event as an XML tag.
operator QString() const;
// Subclassing support
//! \brief Return the XML tag used for this event. Automatically generated for subclasses by template.
virtual const QString & tag() const = 0;
//! \brief Return the ID for this event, if applicable. Subclasses should override this if their events should be retrieved by ID.
virtual const QString id() const { static const QString none(""); return none; }
//! \brief True if this event represents a "random-access" event that should cause subsequent event searches to start after this event's timestamp. Subclasses that represent such a state change should override this method.
virtual bool randomAccess() const { return false; }
// Event subclass registration and instance creation
typedef XmlReplayEvent* (*FactoryMethod)();
static bool registerClass(const QString & tag, FactoryMethod factory);
static XmlReplayEvent* createInstance(const QString & tag);
protected:
static QHash<QString,FactoryMethod> & factories(); // registered subclass factory methods, arranged by XML tag
//! \brief True this event contains raw data. Defaults to false, so subclasses that use raw data must override this.
virtual bool usesData() const { return false; }
//! \brief True if this event represents a signal event. Subclasses representing such events must set m_signal.
inline bool isSignal() const { return m_signal != nullptr; }
//! \brief Send a signal to the target object. Subclasses may override this to send signal arguments.
virtual void signal(QObject* target);
//! \brief Write any attributes or content needed specific to event. Subclasses may override this to support complex data types.
virtual void write(QXmlStreamWriter & xml) const;
//! \brief Read any attributes or content specific to this event. Subclasses may override this to support complex data types.
virtual void read(QXmlStreamReader & xml);
QDateTime m_time; // timestamp of event
XmlReplayEvent* m_next; // next recorded event, used during replay to trigger signals that automatically fire after an event is processed
const char* m_signal; // name of the signal to be emitted for this event, if any
QHash<QString,QString> m_values; // hash of key/value pairs for this event, written as attributes of the XML tag
QList<QString> m_keys; // list of keys so that attributes will be written in the order they were set
QString m_data; // hexademical string representing this event's raw data, written as contents of the XML tag
// Copy the timestamp as well as the attributes. Used when creating substreams.
void copy(const XmlReplayEvent & other);
friend class XmlReplay;
};
// Convenience template for serializing QLists to XML
template<typename T> QXmlStreamWriter & operator<<(QXmlStreamWriter & xml, const QList<T> & list)
{
for (auto & item : list) {
xml << item;
}
return xml;
}
// Convenience template for deserializing QLists from XML
template<typename T> QXmlStreamReader & operator>>(QXmlStreamReader & xml, QList<T> & list)
{
list.clear();
while (xml.readNextStartElement()) {
T item;
xml >> item;
list.append(item);
}
return xml;
}
/*
* Intermediate parent class of concrete event subclasses.
*
* We use this extra CRTP templating so that concrete event subclasses
* require as little code as possible:
*
* The subclass's tag and factory method are automatically generated by this
* template.
*/
template <typename Derived>
class XmlReplayBase : public XmlReplayEvent
{
public:
static const QString TAG;
static const bool registered;
virtual const QString & tag() const { return TAG; };
static XmlReplayEvent* createInstance()
{
Derived* instance = new Derived();
return static_cast<XmlReplayEvent*>(instance);
}
};
/*
* Macro to define an XmlReplayEvent subclass's tag and automatically
* register the subclass at global-initialization time, before main()
*/
#define REGISTER_XMLREPLAYEVENT(tag, type) \
template<> const QString XmlReplayBase<type>::TAG = tag; \
template<> const bool XmlReplayBase<type>::registered = XmlReplayEvent::registerClass(XmlReplayBase<type>::TAG, XmlReplayBase<type>::createInstance);
/*
* XML replay lock class
*
* An instance of this class should be created on the stack during any replayable
* event. Exiting scope will release the lock, at which point any signals that
* need to be replayed will be queued.
*
* Has no effect if events are not being replayed.
*/
class XmlReplayLock
{
public:
//! \brief Temporarily lock the XML replay (if any) until exiting scope, at which point any pending signals will be sent to the specified object.
XmlReplayLock(const QObject* obj, XmlReplay* replay);
~XmlReplayLock();
protected:
const QObject* m_target; // target object to receive any pending signals
XmlReplay* m_replay; // replay instance, or nullptr if not replaying
};
#endif // XMLREPLAY_H