/* FILE: BFLogCollector.cpp
* DATE: 9/13/2019
* AUTHOR: Jeremy Greene
* COMPANY: BitFlow, Inc.
* COPYRIGHT: Copyright (C) 2019, BitFlow, Inc.
* DESCRIPTION: BFLogCollector terminal utility. Can be used to "collect" any
* number of BFLogIO messages sent from any number of client
* processes. Message data is printed to stdout.
*/
#include <BFLogIOSyncApi.h>
#include <BFLogIOMessageApi.h>
#include <cstdlib>
#include <cstdio>
#include <ctime>
#include <cctype>
#include <string>
#include <cstring>
#include <iostream>
#include <sstream>
#include <vector>
#include <algorithm>
#include <atomic>
#include <thread>
#if defined(_WIN32)
#define LOCALTIME_S(RESULT,TIME) localtime_s(RESULT,TIME)
#elif defined(__GNUC__)
#define LOCALTIME_S(RESULT,TIME) (!localtime_r(TIME,RESULT))
#else
#error Platform implementation missing.
#endif
// Message delimiter characters.
enum OutputDelimiter
{
PrettyPrint = '\n',
FlatPrint = '\t',
EncodedCSV = ','
};
// Field names, to be printed in the order listed.
#define LOG_NUMBER_FIELD "LogNumber"
#define PID_FIELD "PID"
#define TIMESTAMP_SENT_FIELD "TimestampSent"
#define TYPE_FIELD "Type"
#define SOURCE_FIELD "Source"
#define APPLICATION_FIELD "Application"
#define TITLE_FIELD "Title"
#define TEXT_FIELD "Text"
const char *const FieldArray[] = {
LOG_NUMBER_FIELD,
PID_FIELD,
TIMESTAMP_SENT_FIELD,
TYPE_FIELD,
SOURCE_FIELD,
APPLICATION_FIELD,
TITLE_FIELD,
TEXT_FIELD,
0
};
#define FINAL_FIELD TEXT_FIELD
// Encode a string so that all whitespace and a_extraChars are hex encoded.
std::string hexEncode(std::string const &a_encode, const char a_escapeChar, std::string const &a_extraChars)
{
std::string encoded = a_encode;
char encStr[16];
for (std::string::iterator Iter = encoded.begin(); Iter != encoded.end();
Iter++)
{
if (a_escapeChar == *Iter || !std::isprint(*Iter)
|| a_extraChars.find(*Iter) != std::string::npos)
{
sprintf(encStr, "%02X", (unsigned int)(*Iter) & 0xFF);
*Iter = a_escapeChar;
Iter = encoded.insert(++Iter, encStr[0]);
Iter = encoded.insert(++Iter, encStr[1]);
}
}
return encoded;
}
// Print the message header, determining the encoding from the specified delimiter.
std::ostream & printHeader(std::ostream & a_out, std::vector < std::string > const &pFields, const OutputDelimiter a_delim, const unsigned int a_index, const unsigned int a_count)
{
const char *sep;
switch (a_delim)
{
default:
case PrettyPrint:
if (pFields.empty() || std::find(pFields.begin(), pFields.end(), PID_FIELD) != pFields.end())
{
a_out << "[" << (a_index + 1) << " / ";
if (0 == ~a_count)
a_out << "inf";
else
a_out << a_count;
a_out << "]\n";
}
return a_out;
case FlatPrint:
sep = "\t";
break;
case EncodedCSV:
sep = ",";
break;
}
if (0 == a_index)
{
if (pFields.empty())
{
for (int i = 0; FieldArray[i]; i++)
a_out << FieldArray[i] << (FieldArray[i + 1] ? sep : "\n");
}
else
{
for (int i = 0, cnt = pFields.size(); cnt > i; i++)
a_out << pFields[i] << (cnt > i + 1 ? sep : "\n");
}
}
return a_out;
}
// Print a message field, determining the encoding from the specified delimiter.
std::ostream & printField(std::ostream & a_out, std::vector < std::string > const &pFields, const OutputDelimiter a_delim, std::string const &a_name, std::string const &a_value)
{
const bool isFinal = (pFields.empty()? FINAL_FIELD == a_name : pFields.back() == a_name);
switch (a_delim)
{
default:
case PrettyPrint:
if (LOG_NUMBER_FIELD != a_name && a_value.size() > 0)
a_out << a_name << " = " << a_value << "\n";
break;
case FlatPrint:
a_out << a_value << (isFinal ? "\n" : "\t");
break;
case EncodedCSV:
a_out << hexEncode(a_value, '%', ",;") << (isFinal ? "\n" : ",");
break;
}
return a_out;
}
// Convert the timestamp value into a displayable string, determining the format
// from the specified delimiter.
std::string timestampString(const OutputDelimiter a_delim, const time_t a_timestamp)
{
if (a_timestamp)
{
char timeCStr[256];
tm timeinfo;
if (!LOCALTIME_S(&timeinfo, &a_timestamp))
{
size_t result;
switch (a_delim)
{
default:
case PrettyPrint:
result = strftime(timeCStr, sizeof(timeCStr),
"%B %d, %Y @ %H:%M:%S", &timeinfo);
break;
case FlatPrint:
result = strftime(timeCStr, sizeof(timeCStr), "%b %d %H:%M:%S", &timeinfo);
break;
case EncodedCSV:
result = strftime(timeCStr, sizeof(timeCStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
break;
}
if (result)
return timeCStr;
}
}
return "N/A";
}
// Convert the Message Type value into a displayable string.
std::string typeString(const BFLogIOMsgType a_type)
{
if (BFLogIOTypeMask & a_type)
{
std::string typeStr;
for (unsigned int t = 1; BFLogIOTypeMask & t; t <<= 1)
{
const char *tName = 0;
switch (a_type & t)
{
case BFLogIODebugType:
tName = "Debug";
break;
case BFLogIONotificationType:
tName = "Notification";
break;
case BFLogIOWarningType:
tName = "Warning";
break;
case BFLogIOErrorType:
tName = "Error";
break;
case BFLogIOFatalErrorType:
tName = "Fatal Error";
break;
default:
break;
}
if (tName != 0)
{
if (typeStr.size() > 0)
typeStr.append(1, '|');
typeStr.append(tName);
}
}
return typeStr;
}
else
return "Unknown";
}
// Convert the Message Source value into a displayable string.
std::string sourceString(const BFLogIOMsgSource a_source)
{
switch (a_source)
{
case BFLogIOUserSrc:
return "User";
case BFLogIODriverSrc:
return "Driver";
case BFLogIOCLSerialSrc:
return "Serial";
case BFLogIOCxpRegSrc:
return "CxpReg";
case BFLogIOBufInSrc:
return "BufIn";
case BFLogIOGenTLSrc:
return "GenTL";
default:
break;
}
return "Unknown";
}
// Convert a data value to a string.
template < typename T > std::string toString(T const &a_value)
{
std::stringstream strm;
strm << a_value;
return strm.str();
}
// Global variables.
static std::atomic < BFLogIOSync > g_hSync(NULL);
#if defined(__GNUC__)
#include <signal.h>
// Handle SIGTERM, to allow proper cleanup.
void sig_handler(sigset_t sigset)
{
int signum = 0;
sigwait(&sigset, &signum);
// Take the sync handle.
const BFLogIOSync hSync = g_hSync.exchange(NULL);
if (hSync)
BFLogIOCancelWaitNextMessage(hSync, true);
}
std::thread launch_sig_handler(void)
{
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, SIGINT);
sigaddset(&sigset, SIGTERM);
pthread_sigmask(SIG_BLOCK, &sigset, NULL);
return std::thread(&sig_handler, std::move(sigset));
}
#else
# error Platform implementation missing.
#endif
int main(int argc, char *argv[])
{
unsigned int collectCnt = 0;
unsigned int timeout = BFLOGIO_INFINITE_TIMEOUT;
bool printTimeoutMsg = false;
OutputDelimiter outputDelim = PrettyPrint;
std::vector < std::string > pFields;
bool needsHelp = false;
int rc = EXIT_FAILURE;
// Parse input arguments.
for (int i = 1; i < argc; i++)
{
if (strcmp(argv[i], "-h") == 0)
{
rc = EXIT_SUCCESS;
needsHelp = true;
}
else if (strcmp(argv[i], "-c") == 0)
{
if (i + 1 >= argc)
{
std::cerr << "Invalid argument count for " << argv[i] << "\n";
needsHelp = true;
}
else
{
const char *const cntStr = argv[++i];
if (strcmp(cntStr, "inf") == 0)
collectCnt = 0;
else if (sscanf(cntStr, "%u", &collectCnt) != 1)
{
std::
cerr << "Bad collection count, \"" << argv[i] <<
"\"!\n";
needsHelp = true;
}
else if (0 == collectCnt)
{
std::
cerr << "Collection count \"" << argv[i] <<
"\" is no-op!\n";
needsHelp = true;
}
}
}
else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "-T") == 0)
{
if (i + 1 >= argc)
{
std::cerr << "Invalid argument count for " << argv[i] << "\n";
needsHelp = true;
}
else
{
printTimeoutMsg = (argv[i][1] == 'T');
if (sscanf(argv[++i], "%u", &timeout) != 1)
{
std::cerr << "Bad timeout, \"" << argv[i] << "\"!\n";
needsHelp = true;
}
}
}
else if (argv[i][0] == '+')
{
const char *const pField = argv[i] + 1;
size_t j;
for (j = 0; FieldArray[j] && strcmp(FieldArray[j], pField) != 0;
j++);
if (!FieldArray[j])
{
std::cerr << "Unknown field name, \"" << pField << "\"\n";
needsHelp = true;
}
if (std::find(pFields.begin(), pFields.end(), pField) != pFields.end())
{
std::
cerr << "Duplicate print field detected, \"" << pField <<
"\"\n";
needsHelp = true;
}
pFields.push_back(pField);
}
else if (strcmp(argv[i], "-f") == 0)
outputDelim = FlatPrint;
else if (strcmp(argv[i], "-e") == 0)
outputDelim = EncodedCSV;
else if (strcmp(argv[i], "-F") == 0)
{
printHeader(std::cout, pFields, FlatPrint, 0, 0);
return EXIT_SUCCESS;
}
else if (strcmp(argv[i], "-E") == 0)
{
printHeader(std::cout, pFields, EncodedCSV, 0, 0);
return EXIT_SUCCESS;
}
else
{
std::cerr << "Bad argument \"" << argv[i] << "\"!\n";
needsHelp = true;
}
if (needsHelp)
{
std::cout <<
/* ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** */
"\nBFLogCollector - Command line Sync Server for BFLogIO messages."
"\n"
"\nValid arguments:"
"\n -c <count> Collect <count> messages, then exit. Use \"-c inf\" for"
"\n infinite collection (default if unspecified)."
"\n -t <timeout> Message wait <timeout> in milliseconds (default is infinite)."
"\n -T <timeout> Same as -t, but prints a message to stderr at timeout."
"\n +<field> Enable printing of <field>. Invoke repeatedly, to enable"
"\n multpiple fields. Use -F to query the list of fields. Default"
"\n is all fields enabled, if none are explicit."
"\n -f Enable Flat Print mode, using unencoded tab-separated values."
"\n -e Enable Encoded CSV message output. Files encoded in this"
"\n format can be opened in the BFLog GUI application."
"\n -F Print the Flat Print header for the enabled fields, and exit."
"\n -E Print the Encoded CSV header for the enabled fields, and exit."
"\n -h Display this help message." "\n";
/* ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** */
return rc;
}
}
// First, open the sync server.
BFLogIOSync hSync;
if (BFLOGIO_OK != BFLogIOAcquireSync(&hSync))
{
std::cerr << "Unable to open a BFLogIO sync handle.\n";
return EXIT_FAILURE;
}
g_hSync.store(hSync);
#if defined(__GNUC__)
// Launch the SIGTERM and SIGINT handler.
std::thread sigThread = launch_sig_handler();
#else
# error Platform implementation missing.
#endif
// Enter the Sync loop.
BFLOGIORC err = BFLOGIO_OK;
rc = EXIT_SUCCESS;
// Collect as many messages as requested.
for (unsigned int i = 0; (0 == collectCnt || i < collectCnt); i++)
{
BFLogIOMessage hMsg;
// Wait for the next message.
err = BFLogIOWaitNextMessage(hSync, &hMsg, timeout);
// If we've been canceled or timed out, exit quietly.
if (BFLOGIO_ERR_CANCELED == err)
break;
else if (BFLOGIO_ERR_TIMEOUT == err && !printTimeoutMsg)
break;
// Print error messages.
switch (err)
{
case BFLOGIO_OK:
break;
case BFLOGIO_ERR_SYNC_IS_BAD:
std::
cerr <<
"ERR: Sync is Bad! Is another collector already open?\n";
break;
case BFLOGIO_ERR_SOURCE_CONNECT_FAILED:
std::cerr << "ERR: Source Connect Failed!\n";
break;
case BFLOGIO_ERR_SOURCE_WAIT_FAILED:
std::cerr << "ERR: Source Wait Failed!\n";
break;
case BFLOGIO_ERR_SOURCE_DISCONNECTED:
std::cerr << "ERR: Source Disconnected!\n";
break;
case BFLOGIO_ERR_START_READ_FAILED:
std::cerr << "ERR: Start Read Failed!\n";
break;
case BFLOGIO_ERR_READ_WAIT_FAILED:
std::cerr << "ERR: Read Wait Failed!\n";
break;
case BFLOGIO_ERR_READ_FAILED:
std::cerr << "ERR: Read Failed!\n";
break;
case BFLOGIO_ERR_MESSAGE_DEFORMED:
std::cerr << "ERR: Message Deformed!\n";
break;
case BFLOGIO_ERR_CANCEL_STATE_CHANGE:
std::cerr << "ERR: Cancel State Change!\n";
break;
case BFLOGIO_ERR_TIMEOUT:
std::cerr << "ERR: Timeout!\n";
break;
default:
std::cerr << "ERR: UNKNOWN (code = " << err << ")!\n";
break;
}
if (BFLOGIO_OK != err)
{
// Standard error handling.
rc = EXIT_FAILURE;
break;
}
else
{
// Print the message as specified by our input arguments.
time_t timestampSent;
BFLOGIO_UINT64 pid;
BFLogIOMsgType type;
BFLogIOMsgSource source;
size_t bufSize;
char tmp[1024];
printHeader(std::cout, pFields, outputDelim, i, collectCnt);
for (size_t j = 0;
pFields.empty()? !!FieldArray[j] : pFields.size() > j; j++)
{
const std::string field = pFields.empty()? FieldArray[j] : pFields[j];
if (LOG_NUMBER_FIELD == field)
printField(std::cout, pFields, outputDelim, field, toString(i + 1));
else if (PID_FIELD == field)
{
err = BFLogIOGetMsgPID(hMsg, &pid);
printField(std::cout, pFields, outputDelim, field, BFLOGIO_OK == err ? toString(pid) : "0");
}
else if (TIMESTAMP_SENT_FIELD == field)
{
err = BFLogIOGetMsgTimestampSent(hMsg, ×tampSent);
printField(std::cout, pFields, outputDelim, field, timestampString(outputDelim, BFLOGIO_OK == err ? timestampSent : 0));
}
else if (TYPE_FIELD == field)
{
err = BFLogIOGetMsgType(hMsg, &type);
printField(std::cout, pFields, outputDelim, field, typeString(BFLOGIO_OK == err ? type : BFLogIOUnknownType));
}
else if (SOURCE_FIELD == field)
{
err = BFLogIOGetMsgSource(hMsg, &source);
printField(std::cout, pFields, outputDelim, field, sourceString(BFLOGIO_OK == err ? source : BFLogIOUnknownSrc));
}
else if (APPLICATION_FIELD == field)
{
bufSize = sizeof(tmp);
err = BFLogIOGetMsgApp(hMsg, tmp, &bufSize);
printField(std::cout, pFields, outputDelim, field, BFLOGIO_OK == err ? tmp : "");
}
else if (TITLE_FIELD == field)
{
bufSize = sizeof(tmp);
err = BFLogIOGetMsgTitle(hMsg, tmp, &bufSize);
printField(std::cout, pFields, outputDelim, field, BFLOGIO_OK == err ? tmp : "");
}
else if (TEXT_FIELD == field)
{
bufSize = sizeof(tmp);
err = BFLogIOGetMsgText(hMsg, tmp, &bufSize);
printField(std::cout, pFields, outputDelim, field, BFLOGIO_OK == err ? tmp : "");
}
}
// Cleanup.
std::cout.flush();
BFLogIOFreeMessage(hMsg);
}
}
// If the global handle is NULL, wait for termination.
#if defined(__GNUC__)
if (g_hSync.exchange(NULL) == NULL)
{
// Await signal thread completion.
if (sigThread.joinable())
sigThread.join();
}
else
{
// We can't force sigwait to return. Abandon all responsbility.
sigThread.detach();
}
#else
# error Platform implementation missing.
#endif
// Release the Sync.
BFLogIOReleaseSync(hSync);
return rc;
}