BFLogCollector.cppΒΆ

/* 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, &timestampSent);
					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;
}