/********************************************************************

Copyright (c) 1996-2000 Microsoft Corporation

Module Name:
    weblog.cpp

Abstract:
    Defines a generic class that can be used to log
    info from ISAPIs. This class allows its user to
    create application specific logfiles and
    automatically uses an intermediate file to log info and
    creates permanent log files at predefined intervals
    or after predefined number of records have been
    written to the intermediate file. 

Revision History:
    rsraghav  created   03/25/96
    DerekM    modified  04/06/99
    DerekM    modifued  02/24/00

********************************************************************/

#include "stdafx.h"
#include "weblog.h"
#include "util.h"
#include "wchar.h"

/////////////////////////////////////////////////////////////////////////////
// CWeblog- initialization stuff

// **************************************************************************
CWeblog::CWeblog(void)
{
    InitializeCriticalSection(&m_cs);
    m_fInit              = FALSE;

    m_cMaxRecords        = c_dwMaxRecordsDefault;
    m_dwDumpInterval     = c_dwDumpIntervalDefault;
    
    m_liDumpIntervalAsFT = c_dwDumpIntervalDefault;
    m_liDumpIntervalAsFT *= c_dwMinToMS;
    m_liDumpIntervalAsFT *= c_dwFTtoMS;

    m_szAppName[0]       = L'\0';
    m_szFileName[0]      = L'\0';
    m_szFilePath[0]      = L'\0';

    ZeroMemory(&m_ftLastDump, sizeof(m_ftLastDump));
    m_cRecords           = 0;
    m_hFile              = INVALID_HANDLE_VALUE;
}

// **************************************************************************
CWeblog::~CWeblog()
{
    if (m_hFile != INVALID_HANDLE_VALUE)
        CloseHandle(m_hFile);

    DeleteCriticalSection(&m_cs);
}

/////////////////////////////////////////////////////////////////////////////
// CWeblog- internal stuff

// **************************************************************************
HRESULT CWeblog::InitFromRegistry()
{
    USE_TRACING("CWeblog::InitFromRegistry");

    SAppLogInfoExtra    alie;
    SAppLogInfo         ali;
    HRESULT             hr;

    // read the ALI & ALIE structures 
    TESTHR(hr, ReadALI(m_szAppName, &ali, &alie));
    if (FAILED(hr))
        goto done;

    CopyMemory(&m_ftLastDump, &alie.ftLastDump, sizeof(m_ftLastDump));
    lstrcpyW(m_szFileName, ali.wszLogPath);
    lstrcpyW(m_szFilePath, ali.wszLogPath);
    m_cMaxRecords        = ali.cMaxTempRecs;
    m_cRecords           = alie.cCurTempRecs;
    m_dwDumpInterval     = ali.cDumpMins;
    m_liDumpIntervalAsFT = m_dwDumpInterval;
    m_liDumpIntervalAsFT *= c_dwMinToMS;
    m_liDumpIntervalAsFT *= c_dwFTtoMS;
    
done:
    return hr;
}

// **************************************************************************
BOOL CWeblog::IsDumpRequired() 
{ 
    USE_TRACING("CWeblog::IsDumpRequired");

    SYSTEMTIME  stNow;
    FILETIME    ftNow;

    // check if we've gone over our allotted amount of records  
    if ((m_cMaxRecords > 0) && (m_cRecords >= m_cMaxRecords)) 
        return TRUE;

    // ok, so now check if we've gone over our allotted time range.
    // if we fail to convert the system time, do a dump to be on the safe side...
    GetLocalTime(&stNow);
    if (SystemTimeToFileTime(&stNow, &ftNow) == FALSE)
        return TRUE;

    if (((*(unsigned __int64 *)&ftNow) - 
         (*(unsigned __int64 *)&m_ftLastDump)) > m_liDumpIntervalAsFT)
        return TRUE;

    return FALSE;
}

// **************************************************************************
HRESULT CWeblog::DumpLog()
{
    USE_TRACING("CWeblog::DumpLog");

    SYSTEMTIME  st;
    HRESULT     hr = NOERROR;
    DWORD       dwRet; 
    WCHAR       *pszStart = NULL;
    WCHAR       szNewName[MAX_PATH + 1];
    int         nUsed;
    
    this->Lock();

        // Close handle to the intermediate file
        CloseHandle(m_hFile);
        
        // intialize new internal stuff...
        m_hFile = INVALID_HANDLE_VALUE;
        m_cRecords = 0;     
        
        // Rename the intermediate file to be permanent logfile with 
        //  appropriate name       
        nUsed = wsprintfW(szNewName, L"%ls%ls", m_szFilePath, m_szAppName);
        
        if ((nUsed + c_cMaxTimeSuffixLen) > MAX_PATH)
        {
            // the file name is too long - truncate it
            szNewName[MAX_PATH - c_cMaxTimeSuffixLen - 1] = L'\0';
            nUsed = lstrlenW(szNewName);
        }

        // update last dump time stamp 
        GetLocalTime(&st);
        SystemTimeToFileTime(&st, &m_ftLastDump);

        // create the rest of the filename...       
        pszStart = &szNewName[nUsed];
        wsprintfW(pszStart, L"%04d%02d%02d%02d%02d%02d%ls", st.wYear, st.wMonth, 
                  st.wDay, st.wHour, st.wMinute, st.wSecond, 
                  c_szPermLogfileSuffix);

        // move the file (duh.)
        MoveFileExW(m_szFileName, szNewName, 
                    MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH);

        // recreate the intermediate file and get a handle to it
        m_hFile = CreateFileW(m_szFileName, GENERIC_WRITE, FILE_SHARE_READ, 
                              NULL, OPEN_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, 
                              NULL);
        if (m_hFile == INVALID_HANDLE_VALUE)
            hr = Err2HR(GetLastError());
        else
            SetFilePointer(m_hFile, 0, NULL, FILE_END);
        
    this->Unlock();
        
    return hr;
}

/////////////////////////////////////////////////////////////////////////////
// CWeblog- exposed methods

// **************************************************************************
HRESULT CWeblog::InitLogging(LPCWSTR szAppName)
{
    USE_TRACING("CWeblog::InitLogging");

    HRESULT hr = NOERROR;
    int     nUsed;
    
    // only allow one init per lifetime of the object...
    VALIDATEEXPR(hr, (m_fInit), E_FAIL);
    if (FAILED(hr))
        goto done;

    // validate params
    VALIDATEPARM(hr, (szAppName == NULL));
    if (FAILED(hr))
        goto done;

    // copy in the app name, zero terminating in case app name is longer than we allow... 
    wcsncpy(m_szAppName, szAppName, c_cMaxAppNameLen);
    m_szAppName[c_cMaxAppNameLen] = L'\0';
        
    // Read in settings from Registry
    TESTHR(hr, InitFromRegistry());
    if (FAILED(hr))
        goto done;

    // m_szFileName contains the file path: append appname and suffix
    nUsed = lstrlenW(m_szFileName);
    VALIDATEEXPR(hr, ((nUsed + lstrlenW(m_szAppName) + 6) > MAX_PATH), E_FAIL);
    if (FAILED(hr))
        goto done;

    // Open the intermediate file and keep it ready for appending
    wsprintfW(m_szFileName + nUsed, L"%ls%ls", m_szAppName, c_szTempLogfileSuffix);
    m_hFile = CreateFileW(m_szFileName, GENERIC_WRITE, FILE_SHARE_READ, NULL, 
                          OPEN_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
    TESTBOOL(hr, (m_hFile != INVALID_HANDLE_VALUE))
    if (FAILED(hr))
        goto done;
        
    SetFilePointer(m_hFile, 0, NULL, FILE_END);

    m_fInit = TRUE;

done:
    return hr;
}

// **************************************************************************
HRESULT CWeblog::TerminateLogging(void)
{
    USE_TRACING("CWeblog::TerminateLogging");

    SAppLogInfoExtra    alie;
    HRESULT             hr = NOERROR;

    // bug out if the initialization didn't work smoothly...
    if (m_fInit == FALSE)
        goto done;

    lstrcpyW(alie.wszName, m_szAppName);
    CopyMemory(&alie.ftLastDump, &m_ftLastDump, sizeof(alie.ftLastDump));
    alie.cCurTempRecs = m_cRecords;

    TESTHR(hr, WriteALI(NULL, &alie));
    if (FAILED(hr))
        goto done;

    m_fInit              = FALSE;

    m_cMaxRecords        = c_dwMaxRecordsDefault;
    m_dwDumpInterval     = c_dwDumpIntervalDefault;
    
    m_liDumpIntervalAsFT = c_dwDumpIntervalDefault;
    m_liDumpIntervalAsFT *= c_dwMinToMS;
    m_liDumpIntervalAsFT *= c_dwFTtoMS;

    m_szAppName[0]       = L'\0';
    m_szFileName[0]      = L'\0';
    m_szFilePath[0]      = L'\0';

    ZeroMemory(&m_ftLastDump, sizeof(m_ftLastDump));
    m_cRecords           = 0;

done:
    return hr;
}


// **************************************************************************
HRESULT CWeblog::LogRecord(LPCWSTR szFormat, ... )
{
    USE_TRACING("CWeblog::LogRecord");

    SYSTEMTIME  st;
    va_list     arglist;
    HRESULT     hr = NOERROR;
    DWORD       dwUsed, nWritten, dwUsedA;
    WCHAR       szLogRecordW[c_cMaxRecLen + 1];
	CHAR		szLogRecordA[(2 * c_cMaxRecLen) + 1];
    int         nAdded;

    // only allow 'em in if they've called init...
    VALIDATEEXPR(hr, (m_fInit == FALSE), E_FAIL);
    if (FAILED(hr))
        goto done;

    // validate params
    VALIDATEPARM(hr, (szFormat == NULL));
    if (FAILED(hr))
        goto done;
    
    // prepend the app name and current time
    GetLocalTime(&st);

    dwUsed = (ULONG)wsprintfW(szLogRecordW, L"%ls,%04u/%02u/%02u %02u:%02u:%02u\n", 
                              m_szAppName, st.wYear, st.wMonth, st.wDay, 
                              st.wHour, st.wMinute, st.wSecond);

    va_start(arglist, szFormat);

    nAdded = _vsnwprintf(&szLogRecordW[dwUsed], c_cMaxRecLen - dwUsed, 
                         szFormat, arglist);

    // is the arglist too big for us?
    if (nAdded < 0)
    {
        // if so, just insert a dummy value
        lstrcpyW(&szLogRecordW[dwUsed], 
                 L"Logging Error: Record given for logging is too big!\r\n");
        dwUsed = lstrlenW(szLogRecordW);
    }
    else
    {
        // otherwise, add a CRLF to the end...
        dwUsed += (ULONG) nAdded;
        szLogRecordW[dwUsed++] = L'\r';
        szLogRecordW[dwUsed++] = L'\n';
        szLogRecordW[dwUsed] = L'\0';
    }

    va_end(arglist);

	// translate the record down into ASCII so the lab team can use grep &
	//  other such utilities on these files (which don't work with unicode
	//  text files)
	dwUsedA = WideCharToMultiByte(CP_ACP, 0, szLogRecordW, -1, szLogRecordA, 
                                  2 * c_cMaxRecLen, NULL, NULL);
    if (dwUsedA == 0)
    {
        dwUsedA = (ULONG)wsprintfA(szLogRecordA, "%ls,%04u/%02u/%02u %02u:%02u:%02u\n", 
                                   m_szAppName, st.wYear, st.wMonth, st.wDay, 
                                   st.wHour, st.wMinute, st.wSecond);
        lstrcpyA(&szLogRecordA[dwUsedA], "Logging Error: unable to translate log record to ASCII\r\n");
        dwUsedA = lstrlen(szLogRecordA);
    }
    
    else
    {
        // dwUsedA contains a character for the NULL terminator at the end of
        //  the ASCII string.  Since we don't want to write this to the log
        //  file, reduce the size of the log entry by 1 byte...
        dwUsedA--;
    }

    this->Lock();

        TESTBOOL(hr, (WriteFile(m_hFile, szLogRecordA, dwUsedA, &nWritten, 
                                NULL) == FALSE));
        if (FAILED(hr))
        {
            this->Unlock();
            goto done;
        }

        m_cRecords++;
        
        if (IsDumpRequired())
            TESTHR(hr, DumpLog());
        
    this->Unlock();

done:

    return hr;
}

//////////////////////////////////////////////////////////////////////
// CWeblogConfig initialization

// **************************************************************************
HRESULT ReadALI(LPCWSTR wszName, SAppLogInfo *pali, SAppLogInfoExtra *palie)
{
    USE_TRACING("CWeblog::ReadALI");

    HRESULT     hr = NOERROR;
    WCHAR       wszDef[MAX_PATH];
    DWORD       dw = 0, cb;
    HKEY        hKeyLog = NULL, hKeyRoot = NULL;
    BOOL        fWrite;

    VALIDATEPARM(hr, (wszName == NULL || pali == NULL));
    if (FAILED(hr))
        goto done;
        
    GetWindowsDirectoryW(wszDef, sizeof(wszDef) / sizeof(WCHAR));
    wszDef[3] = L'\0';

    TESTHR(hr, OpenRegKey(HKEY_LOCAL_MACHINE, c_szRPWeblogRootKey, FALSE,
                          &hKeyRoot));
    if (FAILED(hr))
        goto done;

    TESTHR(hr, OpenRegKey(hKeyRoot, wszName, FALSE, &hKeyLog));
    if (FAILED(hr))
        goto done;

    // max allowed temp records
    cb = sizeof(pali->cMaxTempRecs);
    TESTHR(hr, ReadRegEntry(hKeyLog, c_szRVMaxRecords, NULL,
                            (PBYTE)&pali->cMaxTempRecs, &cb,
                            (PBYTE)&c_dwMaxRecordsDefault, sizeof(DWORD))); 
    if (FAILED(hr))
        goto done;

    // dump interval
    cb = sizeof(pali->cDumpMins);
    TESTHR(hr, ReadRegEntry(hKeyLog, c_szRVDumpInterval, NULL,
                            (PBYTE)&pali->cDumpMins, &cb,
                            (PBYTE)&c_dwDumpIntervalDefault, sizeof(DWORD))); 
    if (FAILED(hr))
        goto done;

    // weblog path 
    cb = sizeof(pali->wszLogPath) * sizeof(WCHAR);
    TESTHR(hr, ReadRegEntry(hKeyLog, c_szRVLogFilePath, NULL,
                            (PBYTE)&pali->wszLogPath, &cb, (PBYTE)&wszDef,
                            (lstrlenW(wszDef) + 1) * sizeof(WCHAR)));
    if (FAILED(hr))
        goto done;

    lstrcpyW(pali->wszName, wszName);

    // make sure we have a '\' at the end of the file path...
    dw = lstrlenW(pali->wszLogPath);
    if (pali->wszLogPath[dw - 1] != L'\\')
    {
        pali->wszLogPath[dw] = L'\\';
        pali->wszLogPath[dw + 1] = L'\0';
    }

    // make sure we have valid values here... 
    if (pali->cDumpMins == 0)
        pali->cDumpMins = c_dwDumpIntervalDefault;
    if (pali->cMaxTempRecs == 0)
        pali->cMaxTempRecs = c_dwMaxRecordsDefault;


    // if this is NULL, we don't have to do anything more
    if (palie != NULL)
    {
        FILETIME    ftDef;

        // last dump time
        ZeroMemory(&ftDef, sizeof(FILETIME));
        cb = sizeof(palie->ftLastDump);
        TESTHR(hr, ReadRegEntry(hKeyLog, c_szRVLastDumpTime, NULL,
                                (PBYTE)&palie->ftLastDump, &cb, 
                                (PBYTE)&ftDef, sizeof(FILETIME)));
        if (FAILED(hr)) 
            goto done;

        // current temp records
        dw = 0;
        cb = sizeof(palie->cCurTempRecs);
        TESTHR(hr, ReadRegEntry(hKeyLog, c_szRVCurrentRecs, NULL, 
                                (PBYTE)&palie->cCurTempRecs, &cb, 
                                (PBYTE)&dw, sizeof(DWORD)));
        if (FAILED(hr)) 
            goto done;

        lstrcpyW(palie->wszName, wszName);
    }

done:
    if (hKeyLog != NULL)
        RegCloseKey(hKeyLog);
    if (hKeyRoot != NULL)
        RegCloseKey(hKeyRoot);

    return hr;
}

// **************************************************************************
HRESULT WriteALI(SAppLogInfo *pali, SAppLogInfoExtra *palie)
{
    USE_TRACING("CWeblog::WriteALI");

    HRESULT hr = NOERROR;
    LPWSTR  pwsz = NULL;
    DWORD   dwErr;
    HKEY    hKeyLog = NULL, hKeyRoot = NULL;
    BOOL    fWrite;

    if (pali != NULL)
        pwsz = pali->wszName;
    else if (palie != NULL)
        pwsz = palie->wszName;
    else
        goto done;

    TESTHR(hr, OpenRegKey(HKEY_LOCAL_MACHINE, c_szRPWeblogRootKey, TRUE,
                          &hKeyRoot));
    if (FAILED(hr))
        goto done;

    TESTHR(hr, OpenRegKey(hKeyRoot, pwsz, TRUE, &hKeyLog));
    if (FAILED(hr))
        goto done;

    if (pali != NULL)
    {
        DWORD dw;

        // make sure we have valid values here... 
        if (pali->cDumpMins == 0)
            pali->cDumpMins = c_dwDumpIntervalDefault;
        if (pali->cMaxTempRecs == 0)
            pali->cMaxTempRecs = c_dwMaxRecordsDefault;

        // make sure we have a '\' at the end of the file path...
        dw = lstrlenW(pali->wszLogPath);
        if (pali->wszLogPath[dw - 1] != L'\\')
        {
            pali->wszLogPath[dw] = L'\\';
            pali->wszLogPath[dw + 1] = L'\0';
        }

        // max temp records
        TESTERR(hr, RegSetValueExW(hKeyLog, c_szRVMaxRecords, 0, REG_DWORD, 
                                   (PBYTE)&pali->cMaxTempRecs, sizeof(DWORD)));
        if (FAILED(hr))
            goto done;

        // dump interval
        TESTERR(hr, RegSetValueExW(hKeyLog, c_szRVDumpInterval, 0, REG_DWORD, 
                                   (PBYTE)&pali->cDumpMins, sizeof(DWORD)));
        if (FAILED(hr))
            goto done;

        // weblog path 
        TESTERR(hr, RegSetValueExW(hKeyLog, c_szRVLogFilePath, 0, REG_SZ, 
                                   (PBYTE)&pali->wszLogPath, 
                                   (lstrlenW(pali->wszLogPath) + 1) * sizeof(WCHAR)));
        if (FAILED(hr))
            goto done;
    }

    // if this is NULL, we don't have to do anything more
    if (palie != NULL)
    {
        // last dump time
        TESTERR(hr, RegSetValueExW(hKeyLog, c_szRVLastDumpTime, 0, REG_BINARY, 
                                   (PBYTE)&palie->ftLastDump, sizeof(FILETIME)));
        if (FAILED(hr))
            goto done;

        // current temp records 
        TESTERR(hr, RegSetValueExW(hKeyLog, c_szRVCurrentRecs, 0, REG_DWORD, 
                                   (PBYTE)&palie->cCurTempRecs, sizeof(DWORD)));
        if (FAILED(hr))
            goto done;
    }

done:
    if (hKeyLog != NULL)
        RegCloseKey(hKeyLog);
    if (hKeyRoot != NULL)
        RegCloseKey(hKeyRoot);

    return hr;
}

// **************************************************************************
HRESULT DeleteALI(LPCWSTR wszName)
{
    USE_TRACING("CWeblog::DeleteALI");

    HRESULT hr = NOERROR;
    DWORD   dwErr;
    HKEY    hKeyRoot = NULL;
    BOOL    fWrite;
    
    TESTHR(hr, OpenRegKey(HKEY_LOCAL_MACHINE, c_szRPWeblogRootKey, TRUE,
                          &hKeyRoot));
    if (FAILED(hr))
        goto done;

    dwErr = RegDeleteKeyW(hKeyRoot, wszName);
    if (dwErr != ERROR_SUCCESS && dwErr != ERROR_PATH_NOT_FOUND &&
        dwErr != ERROR_FILE_NOT_FOUND)
    {
        hr = Err2HR(GetLastError());
        goto done;
    }

done:
    if (hKeyRoot != NULL)
        RegCloseKey(hKeyRoot);
    return hr;
}