#include "TelegrafControl.h"

#include "Logger.h"
#include "BaseSocket.h"


#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/types.h>
#include <signal.h>


#include <string>


#define TELEGRAF_PROCESS_NAME				"telegraf"
#define TELEGRAF_BINARY_FULL_PATH			"/usr/local/bin/telegraf"

#define TELEGRAF_PROMETHEUS_CLIENT_IP		"127.0.0.1"
#define TELEGRAF_PROMETHEUS_CLIENT_PORT		9200	



CTelegrafControl::CTelegrafControl()
{
}

CTelegrafControl::~CTelegrafControl()
{
}


// telegraf process 의 PID 정보 반환...
// - 정보 추출시 오류 또는 미동작 상황인 경우.. 0 반환
int CTelegrafControl::GetProcessId()
{
	char tempBuffer[64];
	FILE * fd = NULL;
	int iProcessId = 0;


	snprintf( tempBuffer, sizeof( tempBuffer ), "pgrep -x %s", TELEGRAF_PROCESS_NAME );

	fd = popen( tempBuffer, "r" );
	if( fd == NULL )
	{
		// popen 수행 실패시......
		int errorNum = errno;
		LOG( LERR, "[TelegrafControl] popen failed. [%d][%s]", errorNum, strerror( errorNum ) );

		// 오류 발생시 0 반환
		iProcessId = 0;
	}
	else
	{
		memset( tempBuffer, 0x00, sizeof( tempBuffer ) );

		// 참고사항
		// https://jarvis.solbox.com/redmine/issues/32973 에서 언급된 popen - fgets() 호출시 지연 발생 가능성 관련
		// 서비스 자체에는 영향이 없고...  호출 빈도를 최소화하도록 조정하여....
		// 사용하기로 결정함.

		if( fgets( tempBuffer, sizeof( tempBuffer ) - 1, fd ) != NULL )
		{
			// 프로세스 ID 가 존재하는 경우
			std::string tempPid( tempBuffer );
			Trim( tempPid );

			iProcessId = atoi( tempPid.c_str() );

			_LOG( LDBG, "[TelegrafControl] %s process id %d.", TELEGRAF_PROCESS_NAME, iProcessId );
		}

		pclose( fd );
	}

	return iProcessId;
}

// telegraf service start
// - system() 호출 관련 service 명령어 수행시.. -1 이 반환되므로....
// - telegraf 가 정상 기동 되었는지 판단 불가하여.. 반환값은 void 로 처리한다.
void CTelegrafControl::ServiceStart()
{
	// 우선 binary 가 정상적으로 존재하는지 check
	// - FreeBSD 6.x, 10.0 버전에서는 telegraf 동작시키지 않으므로... binary 가 없는 경우.. 기동시키지 않는다.
	if( access( TELEGRAF_BINARY_FULL_PATH, X_OK ) != 0 )
	{
		_LOG( LWAR, "[TelegrafControl] binary[%s] not exist.", TELEGRAF_BINARY_FULL_PATH );
		return;
	}

	// service 명령을 이용하여... telegraf 를 기동 처리한다.
	// - service 명령어로 기동시... system() 호출 결과는 -1, errno=10 (No child processes) 가 반환된다.
	// - 따라서 system() 호출 결과 -1 을 반환으로 인해.. telegraf 가 정상 기동되었는지 판단 불가하므로...
	// - 본 함수의 반환값은 void 로 처리한다.
	system( "service telegraf start" );
		
	_LOG( LINF, "[TelegrafControl] command \'service telegraf start\' executed.");
	return;
}


// telegraf 가 hang 상태인지 check
// - hang 상황인 경우 true 반환
// - 그 외의 경우에는 false 반환
bool CTelegrafControl::IsHang()
{
	// telegraf 의 prometheus_client 와 통신을 통하여.. telegraf 가 hang 상태인지 check 한다.
	// [[outputs.prometheus_client]]
	// listen = ":9200"

	// telegraf 가
	// TCP 9200 port 접속 후 전달한 잘못된 요청에 대해.. HTTP 400 Bad Request 등과 같은 어떠한 응답이라도 하면 정상..
	// 그 외에는 hang 상황으로 판단한다.
	// - 접속은 되나... 지정된 시간동안 응답을 수신하지 못할 경우...
	// - 아예 접속이 안되는 경우.. ( Listen queue overflow 상황)

	std::string strTragetIp = TELEGRAF_PROMETHEUS_CLIENT_IP;

	// socket 관련 변수.
	int sock = -1;
	struct sockaddr_in  stTarget;
	int flags;

	int result = 0;
	int errorNum = 0;

	// socket 생성.	
	sock = socket( AF_INET, SOCK_STREAM, 0 );
	if( sock == -1 )
	{
		errorNum = errno;
		_LOG( LERR, "[TelegrafControl] socket create error. [%d][%s]", errorNum, strerror( errorNum ) );

		// hang check 수행하지 못했으므로.. 우선 정상 상태로 판단.
		return false;
	}

	// Connnection 종료시 많은 TIME_WAIT 상태 발생 방지를 위해 SO_LINGER 옵션을 설정처리한다.
	struct linger opt_linger;
	opt_linger.l_onoff = 1;		/* LINGER ON */

	// 0 : No Wait for abortive disconnect	
	// - 0 으로 설정할 경우. close 처리시.. 상대편에서 정상 종료를 제대로 인지 못할 수 있음
	// - 0 이외 정수값으로 설정할 경우.. TCP TIME_WAIT 이 발생하며... 60 sec 후 정리됨. 
	// - 우선 check 주기가 1 min 정도 이므로.. 많은 세션이 발생하지 않아... 1 로 설정한다.
	opt_linger.l_linger = 3;

	if( setsockopt( sock, SOL_SOCKET, SO_LINGER, &opt_linger, sizeof( opt_linger ) ) != 0 )
	{
		// 오류가 발생한 경우..
		// - 관련 내역 로깅만 처리하고 check 작업은 그대로 진행한다.
		errorNum = errno;
		_LOG( LWAR, "[TelegrafControl] setsockopt SO_LINGER set error. [%d][%s]", errorNum, strerror( errorNum ) );
	}

	memset( &stTarget, 0x00, sizeof( struct sockaddr_in ) );
	stTarget.sin_family = AF_INET;
	stTarget.sin_port = htons( TELEGRAF_PROMETHEUS_CLIENT_PORT );
	stTarget.sin_addr.s_addr = inet_addr( strTragetIp.c_str() );

	// connection timeout 관련 처리
	// - 접속 대상 장비가 부하 상황인 경우...  connect() 함수의 block 발생
	// - 이를 해결하기 위해 connection timeout 를 구현
	// - non block socket 으로 만든 후.. connect 호출. select 함수를 호출하여 정상 처리 여부 확인.

	/* Non Block Mode Set */
	if( ( flags = fcntl( sock, F_GETFL, 0 ) ) >= 0 )
	{
		if( fcntl( sock, F_SETFL, flags | O_NONBLOCK ) < 0 )
		{
			errorNum = errno;
			_LOG( LWAR, "[TelegrafControl] socket NONBLOCK set error [%d][%s]", errorNum, strerror( errorNum ) );

			close( sock );

			// hang check 수행하지 못했으므로.. 우선 정상 상태로 판단.
			return false;
		}
	}
	else
	{
		// fcntl 호출 오류시
		errorNum = errno;
		_LOG( LWAR, "[TelegrafControl] socket fcntl get error [%d][%s]", errorNum, strerror( errorNum ) );

		close( sock );

		// hang check 수행하지 못했으므로.. 우선 정상 상태로 판단.
		return false;
	}


	// 접속 시도	
	result = connect( sock, (struct sockaddr *)&stTarget, sizeof( stTarget ) );
	if( result < 0 )
	{
		// 중요 코드
		// - connect 시도 후 결과를 바로 확인하지 않고... select 함수에서 결과 확인시..
		// - 오류 코드 clear 발생 등의 문제로 인해...
		// - 실제 접속이 되지 않았는데.. 접속된 것으로 처리 될 수 있음.
		// - 이를 방지하기 위해 본 코드가 중요함.

		errorNum = errno;

		// non-block 모드이므로.. 처리 중인 경우 제외하고 오류로 처리.
		if( errorNum != EINPROGRESS )
		{
			_LOG( LINF, "[TelegrafControl] telegraf connect fail.[%s][%d]->[%d][%s]"
				, strTragetIp.c_str(), TELEGRAF_PROMETHEUS_CLIENT_PORT, errorNum, strerror( errorNum ) );

			close( sock );

			// 접속 실패이므로.. hang 으로 판단.
			return true;
		}
	}

	// 바로 접속이 된 경우...
	if( result == 0 )
	{
		// 관련 내역 로깅
		_LOG( LDBG, "[TelegrafControl] telegraf connect ok. ip[%s][%d]", strTragetIp.c_str(), TELEGRAF_PROMETHEUS_CLIENT_PORT );
	}
	else
	{
		// 바로 접속이 되지 않은 경우....
		// select 함수를 이용하여 timeout 여부 확인.

		// connect timeout 설정 관련 변수
		fd_set selectReadFds, selectWriteFds;
		struct timeval timeOver;

		FD_ZERO( &selectReadFds );
		FD_SET( sock, &selectReadFds );
		selectWriteFds = selectReadFds;

		timeOver.tv_sec  = 2;             /* 2 second timeout */
		timeOver.tv_usec = 0;

		// select 함수를 이용한 접속 결과 관련 처리.
		if( select( sock + 1, &selectReadFds, &selectWriteFds, NULL, &timeOver ) == 1 )
		{
			errorNum = 0;
			socklen_t errorLength = sizeof( errorNum );

			if( getsockopt( sock, SOL_SOCKET, SO_ERROR, &errorNum, &errorLength ) == -1 )
			{
				// 오류 발생시.
				errorNum = errno;
				_LOG( LERR, "[TelegrafControl] telegraf connect fail. getsockopt SO_ERROR get error [%d][%s] ip[%s][%d]"
					, errorNum, strerror( errorNum ), strTragetIp.c_str(), TELEGRAF_PROMETHEUS_CLIENT_PORT );

				close( sock );

				// 접속 실패로 판단하여.. hang 상태로 처리한다.
				return true;
			}
			else
			{
				if( errorNum == 0 )
				{
					// 접속 성공시.			
					_LOG( LDBG, "[TelegrafControl] telegraf connect[wait] ok. ip[%s][%d]", strTragetIp.c_str(), TELEGRAF_PROMETHEUS_CLIENT_PORT );
				}
				else
				{
					// 접속 실패시.
					_LOG( LINF, "[TelegrafControl] telegraf connect[wait] fail.[%s][%d]->[%d][%s]", strTragetIp.c_str(), TELEGRAF_PROMETHEUS_CLIENT_PORT, errorNum, strerror( errorNum ) );

					close( sock );
				
					// 접속 실패로 판단하여.. hang 상태로 처리한다.
					return true;
				}
			}
		}
		else
		{
			// connect timeout or select 함수 오류 발생시
			// - 접속 실패로 처리.				
			_LOG( LINF, "[TelegrafControl] telegraf connect timeout.[%s][%d]", strTragetIp.c_str(), TELEGRAF_PROMETHEUS_CLIENT_PORT );

			close( sock );

			// 접속 실패로 판단하여.. hang 상태로 처리한다.
			return true;
		}
	}

	// request 전송을 위해 socket 을 blocking 모드로 다시 전환
	if( fcntl( sock, F_SETFL, flags ) < 0 )
	{
		errorNum = errno;
		_LOG( LWAR, "[TelegrafControl] socket BLOCK set error [%d][%s]", errorNum, strerror( errorNum ) );

		close( sock );

		// 접속은 정상이지만... hang check 수행하지 못했으므로.. 우선 정상 상태로 판단.
		return false;
	}

	// send time out 설정
	// - request 전송시 timeout 설정 목적.
	struct timeval tv_timeo = { 2, 0 };

	result = setsockopt( sock, SOL_SOCKET, SO_SNDTIMEO, &tv_timeo, sizeof( tv_timeo ) );
	if( result != 0 )
	{
		errorNum = errno;
		_LOG( LERR, "[TelegrafControl] socket SO_SNDTIMEO set fail. [%d][%s]", errorNum, strerror( errorNum ) );
	}


	// data 전송, 수신 처리 단순화를 위해 basesocket 객체 이용
	CBaseSocket basesock( sock );

	// request 전송
	std::string strMsg = "GET /hang_check\r\n";

	long lResult = basesock.WriteN( strMsg.c_str(), strMsg.size() );
	if( lResult <= 0 )
	{
		// 전송 실패시...
		_LOG( LERR, "[TelegrafControl] telegraf request send fail. [%ld]", lResult );

		basesock.Close();

		// 전송 실패가 발생했으므로.. hang 상태로 판단.
		return true;
	}

	// 수신 대기
	char buffer[64];
	memset( buffer, 0x00, 64 );

	// HTTP/1.1 400 Bad Request 응답만 수신 시도, timeout 은 3 sec
	lResult = basesock.ReadNTimeout( buffer, 24, 3 );

	if( lResult <= 0 )
	{
		// 응답 수신 실패시....

		if( lResult == -2 )
		{
			// timeout 발생시 명확하게 로깅 처리.
			_LOG( LERR, "[TelegrafControl] telegraf response receive error. [timeout 3 sec]");
		}
		else
		{
			_LOG( LERR, "[TelegrafControl] telegraf response receive error. [%ld]", lResult );
		}

		basesock.Close();

		// 수신 실패가 발생했으므로.. hang 상태로 판단.
		return true;
	}
	else
	{
		// 정상 응답 수신한 경우..
		_LOG( LDBG, "[TelegrafControl] telegraf response recevied. [%s]", buffer );
	}

	// 사용한 socket 종료 처리.
	basesock.Close();

	// telegraf 정상인 상태로 판단한다.
	return false;
}

// telegraf SIGQUIT signal 전송
// - log 상에 stack trace 남기고 재기동 처리됨
// - hang 상황시.. trace 처리 후 재기동 처리하기 위한 함수.
void CTelegrafControl::ServiceQuit()
{
	int pid = GetProcessId();

	if( pid != 0 )
	{
		// telegraf 에 SIGQUIT 전송시.. log 상에 statck trace 정보 남기고.. 자동 재기동 됨..
		// - hang 상태에서도 SIGQUIT 전송시.. 위의 내역대로 정상 동작함.
		kill( pid, SIGQUIT );

		_LOG( LINF, "[TelegrafControl] SIGQUIT send to restart telegraf[%d] in hang state.", pid );
	}
	else
	{
		_LOG( LERR, "[TelegrafControl] telegraf pid for restarting not valid. [%d]", pid );
	}

	return;
}

// telegraf 의 기동 후 elapsed time 정보를 추출한다.
// - ps -o etimes= -p `pgrep -x telegraf` 명령을 이용하여 추출
// - 0 인 경우.. 오류로 판단...
long long CTelegrafControl::GetProcessElapsedSec()
{
	int pid = GetProcessId();

	if( pid == 0 )
	{
		LOG( LERR, "[TelegrafControl] telegraf pid for elapsed time not valid. [%d]", pid );

		// process id 정보가 유효하지 않은 경우.. 오류로 처리
		return 0;
	}
	else
	{
		char tempBuffer[64];
		FILE * fd = NULL;
		long long llElapsedSec = 0;

		snprintf( tempBuffer, sizeof( tempBuffer ), "ps -o etimes= -p %d", pid );

		fd = popen( tempBuffer, "r" );
		if( fd == NULL )
		{
			// popen 수행 실패시......
			int errorNum = errno;
			LOG( LERR, "[TelegrafControl] popen failed by ps -o etimes=. [%d][%s]", errorNum, strerror( errorNum ) );

			// 오류 발생시 0 반환
			return 0;
		}
		else
		{
			memset( tempBuffer, 0x00, sizeof( tempBuffer ) );

			// 참고사항
			// https://jarvis.solbox.com/redmine/issues/32973 에서 언급된 popen - fgets() 호출시 지연 발생 가능성 관련
			// 서비스 자체에는 영향이 없고...  호출 빈도를 최소화하도록 조정하여....
			// 사용하기로 결정함.

			if( fgets( tempBuffer, sizeof( tempBuffer ) - 1, fd ) != NULL )
			{
				// result 값이 존재하는 경우
				std::string tempResult( tempBuffer );
				Trim( tempResult );

				llElapsedSec = atoll( tempResult.c_str() );

				_LOG( LDBG, "[TelegrafControl] %s process elapsed time %lld sec.", TELEGRAF_PROCESS_NAME, llElapsedSec );
			}

			pclose( fd );
		}

		return llElapsedSec;
	}

	return 0;
}


// telegraf service restart
// - ServiceStart() 함수와 마찬가지로.. service 명령어 호출시. .무조건 -1 이 반환되므로....
// - telegraf 가 정상적으로 restart 되었는지 판단 불가하여.. void 로 처리
void CTelegrafControl::ServiceRestart()
{
	// 우선 binary 가 정상적으로 존재하는지 check
	if( access( TELEGRAF_BINARY_FULL_PATH, X_OK ) != 0 )
	{
		_LOG( LWAR, "[TelegrafControl] binary[%s] not exist.", TELEGRAF_BINARY_FULL_PATH );
		return;
	}

	// service 명령을 이용하여... telegraf 를 기동 처리한다.
	// - service 명령어로 기동시... system() 호출 결과는 -1, errno=10 (No child processes) 가 반환된다.
	// - 따라서 system() 호출 결과 -1 을 반환으로 인해.. telegraf 가 정상 기동되었는지 판단 불가하므로...
	// - 본 함수의 반환값은 void 로 처리한다.
	system( "service telegraf restart" );

	_LOG( LINF, "[TelegrafControl] command \'service telegraf restart\' executed." );
	return;
}


///////////////////////////////////////////////////////////////////////

void CTelegrafControl::Trim( std::string & str )
{
	if( str.length() == 0 )
		return;

	// 문자열 뒤의 공백, TAB, CR 등의 문자 제거처리.
	std::string::size_type pos = str.find_last_not_of( " \a\b\f\n\r\t\v" );
	if( pos != std::string::npos )
		str.erase( pos + 1 );

	// 문자열 앞의 공백, TAB, CR 등의 문자 제거처리.
	pos = str.find_first_not_of( " \a\b\f\n\r\t\v" );
	if( pos != std::string::npos )
		str.erase( 0, pos );

}

