Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C++

Environment Variable Compare

Rate me:
Please Sign up or sign in to vote.
4.94/5 (41 votes)
26 Jul 2013CPOL11 min read 45.5K   2.4K   51   27
Compare two system environment variables

Image 1

Introduction

It often happens to me that after installing my project on Windows platform, sometimes some commands will not work in the project. This is because the corresponding environment variables are not properly updated. To fix the problem recently I compared my system environment variables with my team member's system manually. It required a very large effort. Actually this effort is a waste of time :-). Also I understood that many CodeProject members were also facing the same problem. Finally I decided to develop a tool for comparing environment variables. Hope it is a great decision, :-)

What is an Environment Variable?

An environment variable is a dynamic "object" on a computer that stores a value, which in turn can be referenced by one or more software programs in Windows. Environment variables help programs know what directory to install files in, where to store temporary files, where to find user profile settings, and many other things. It can be said that environment variables help to create and shape the environment of where a program runs.

Background

What is Environment Variable Compare?

Environment Variable Compare is a tool which used to compare environment variables of two systems in text format. Comparison results will be displayed in various color formats. Color setting will be done based on the following conditions.

  1. If two lines have different text, then the two lines will be displayed in red color.
  2. If left side file contains a line(s) which is not present in the right side file; then corresponding new line(s) in the left side file will be displayed in blue color.
  3. If right side file contains a line(s) which is not present in the left side file; then the corresponding new line(s) in the right side will displayed in blue color.

Note: To get environment variables as text files you can use the set keyword in the command prompt. [Example: Run this command on the command prompt "set > C:\FirstDemoFile.txt.]

This environment variable compare tool introduces various miscellaneous features to the user. I hope these features will make this a stunning tool. The purpose and implementation details of each and every technique used in this tool will be explained later.

Application Outlook

Mainly the application contains two list controls separated by a splitter. Actually this is not a splitter window application. Here a picture control acts like a splitter. It is very simple to customize a picture control into a splitter. The customized class structure is mentioned in the below lines of code:

C++
class FlatSplitterWnd : public CSplitterWnd
{
    DECLARE_DYNAMIC(FlatSplitterWnd)
public:
    FlatSplitterWnd();
    virtual ~FlatSplitterWnd();
    virtual void OnDrawSplitter( CDC* pDc_i, ESplitType ntype, const CRect& cRect );
protected:
    DECLARE_MESSAGE_MAP()
}; 

The logic of drawing the splitter is explained in the below lines of code.

C++
void FlatSplitterWnd::OnDrawSplitter( CDC* pDc_i, ESplitType ntype, const CRect& cRect )
{
    try
    {
        CRect BorderRect( cRect );
        if( NULL == pDc_i )
        {
            return;
        }
        if(( splitBorder != ntype ))
        {
            CSplitterWnd::OnDrawSplitter(pDc_i, ntype, cRect);
        }
        else
        {
            pDc_i->Draw3dRect( BorderRect, 
              GetSysColor(COLOR_BTNSHADOW), GetSysColor(COLOR_BTNHIGHLIGHT));
            BorderRect.DeflateRect( 1, 1, 1, 1 );
            pDc_i->Draw3dRect( BorderRect, 
              GetSysColor(COLOR_BTNHIGHLIGHT), GetSysColor(COLOR_BTNSHADOW));
            RecalcLayout();
        }
    }
    catch( CMemoryException* pMemoryException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "FlatSplitterWnd::OnDrawSplitter()" ),
                                            pMemoryException, _T( __FILE__ ), __LINE__ );
    }
    catch( CException* pException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "FlatSplitterWnd::OnDrawSplitter()" ),
                                            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( _T( "FlatSplitterWnd::OnDrawSplitter()" ),
                                                _T( __FILE__ ), __LINE__ );
    }
}

Comparison logic

Comparison of environment variables is performed on a line by line basis. After loading the first environment text file it checks if both sides contain files. If both sides contain environment variable text files then it starts to compare. To start the comparison first it parses the files from both sides to identify the different environment variables and the new environment variables in both sides. Based on the line status it makes a “Compare display” for the user. To differentiate the files, internally it sets some status to the lines on both sides. To store environment variable status on both sides it uses a structure called FILE_INFO.

C++
// Structure for file info
typedef struct FILE_INFO_t
{
    CString csEnvironmentVariable;   // To hold Environment variable as Key value
    CString csVaribleValue;          // Corresponding key String
    CString csFullVariable;          // Contain the entire environment variable.
    ROW_TYPE_e eStoreType;
    // Constructor of FILE_INFO_t
    FILE_INFO_t();
    // Destructor of FILE_INFO_t
    ~FILE_INFO_t();
    // Copy Constructor of FILE_INFO_t
    FILE_INFO_t( const FILE_INFO_t& stFileInfo_i );
    // Assignment operator of FILE_INFO_t
    const FILE_INFO_t& operator=( FILE_INFO_t& stFileInfo_i );
    void Clear();
}FILE_INFO_t; 

After getting entire line details from both sides, then it displays lines on both sides based on this compared result. The below section of code shows the comparison:

C++
bool CEnvironmentVarCompareView::CompareFilesBasedOnLeftAsSourse()
{
    try
    {
        m_LeftListCtrl.DeleteAllItems();
        m_RightListCtrl.DeleteAllItems();
        int nLeftListSize = m_csLeftListCtrlList.GetSize();
        int nRightListSize = m_csRightListCtrlList.GetSize();
        // This for easy traversing purpose.
        if( nLeftListSize >= nRightListSize )
        {
            FILE_INFO_t stFileInfo;
            FILE_INFO_t stLeftFileMapLookUpInfo;
            FILE_INFO_t stRightFileMapLookUpInfo;
            CStringArray csRightListCtrlItem;
            CString csEnvVar;
            int nItem = 0;
            // This section code identify the new item in the left side file
            // and insert corresponding blank space in right side list control.
            POSITION Pos = m_csLeftListCtrlList.GetHeadPosition();
            while( NULL != Pos )
            {
                stFileInfo = m_csLeftListCtrlList.GetNext( Pos );
                // Lookup in LeftlistCtrl map for corresponding environment variable.
                if( NULL != m_LeftListCtrl.m_FileInfoMap.Lookup( stFileInfo.csEnvironmentVariable, 
                        stLeftFileMapLookUpInfo ))
                {
                    // If it is not a new row then set corresponding value in to corresponding
                    // Location in list control.
                    if( NEW_ROW != stLeftFileMapLookUpInfo.eStoreType )
                    {
                        m_LeftListCtrl.InsertItem( nItem, stLeftFileMapLookUpInfo.csFullVariab;
                        if( NULL != m_RightListCtrl.m_FileInfoMap.Lookup( 
                          stLeftFileMapLookUpIefo.csEnvironmentVariable, stRightFileMapLookUpInfo ))
                        {
                            m_RightListCtrl.InsertItem( nItem, stRightFileMapLookUpInfo.csFullVariable );
                        }
                    }
                    // If new row type is found, which means that right list control needs
                    // to insert a blank at corresponding index.
                    else
                    {
                        m_LeftListCtrl.InsertItem( nItem, stLeftFileMapLookUpInfo.csFullVariable );
                        m_RightListCtrl.InsertItem( nItem, SPACE_CHAR );
                    }
                }
                ++nItem;
            }
            // After inserting blank at right side list control we need
            // to check any new item present at right side list control?.
            FILE_INFO_t stLookUpinfo;
            FILE_INFO_t stMainLookUpInfo;
            POSITION NewPos = m_csRightListCtrlList.GetHeadPosition();
            int nItemPos = 0;
            while( NULL != NewPos )
            {
                stMainLookUpInfo = m_csRightListCtrlList.GetNext( NewPos );
                if( NULL != m_RightListCtrl.m_FileInfoMap.Lookup( 
                    stMainLookUpInfo.csEnvironmentVariable, stLookUpinfo ))
                {
                    if( NEW_ROW == stLookUpinfo.eStoreType )
                    {
                        m_RightListCtrl.InsertItem( nItemPos, stLookUpinfo.csFullVariable );
                        m_LeftListCtrl.InsertItem( nItemPos, SPACE_CHAR );
                    }
                }
                ++nItemPos;
            }
        }
        else
        {
            FILE_INFO_t stFileInfo;
            FILE_INFO_t stLeftFileMapLookUpInfo;
            FILE_INFO_t stRightFileMapLookUpInfo;
            CStringArray csLeftListCtrlItemArray;
            CString csEnvVar;
            int nItem = 0;
            POSITION Pos = m_csRightListCtrlList.GetHeadPosition();
            while( NULL != Pos )
            {
                stFileInfo = m_csRightListCtrlList.GetNext( Pos );
                // Lookup in LeftlistCtrl map for corresponding environment variable.
                if( NULL != m_RightListCtrl.m_FileInfoMap.Lookup( 
                    stFileInfo.csEnvironmentVariable, stRightFileMapLookUpInfo ))
                {
                    // If it is not a new row then set corresponding value in to corresponding
                    // Location in list control.
                    if( NEW_ROW != stRightFileMapLookUpInfo.eStoreType )
                    {
                        m_RightListCtrl.InsertItem( nItem, stRightFileMapLookUpInfo.csFullVariable );
                        if( NULL != m_LeftListCtrl.m_FileInfoMap.Lookup( 
                          stFileInfo.csEnvironmentVariable, stLeftFileMapLookUpInfo ))
                        {
                            m_LeftListCtrl.InsertItem( nItem, stLeftFileMapLookUpInfo.csFullVariable );
                            csLeftListCtrlItemArray.Add( stLeftFileMapLookUpInfo.csEnvironmentVariable );
                        }
                    }
                    // If new row type is found, which means that right list control needs
                    // to insert a blank at corresponding index.
                    else
                    {
                        m_RightListCtrl.InsertItem( nItem, stRightFileMapLookUpInfo.csFullVariable );
                        m_LeftListCtrl.InsertItem( nItem, SPACE_CHAR );
                        csLeftListCtrlItemArray.Add( SPACE_CHAR );
                    }
                }
                ++nItem;
            }
            // After inserting blank at left side list control we need
            // to check any new item present at left side list control?.
            FILE_INFO_t stLookUpinfo;
            FILE_INFO_t stMainLookUpInfo;
            POSITION NewPos = m_csLeftListCtrlList.GetHeadPosition();
            int nItemPos = 0;
            while( NULL != NewPos )
            {
                stMainLookUpInfo = m_csLeftListCtrlList.GetNext( NewPos );
                if( NULL != m_LeftListCtrl.m_FileInfoMap.Lookup( 
                  stMainLookUpInfo.csEnvironmentVariable, stLookUpinfo ))
                {
                    if( NEW_ROW == stLookUpinfo.eStoreType )
                    {
                        m_LeftListCtrl.InsertItem( nItemPos, stLookUpinfo.csFullVariable );
                        m_RightListCtrl.InsertItem( nItemPos, SPACE_CHAR );
                    }
                }
                ++nItemPos;
            }
        }
        SettingItemData();
        return true;
    }
    catch( CMemoryException* pMemoryException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( 
            _T( "CEnvironmentVarCompareView::CompareFilesBasedOnLeftAsSourse()" ),
            pMemoryException, _T( __FILE__ ), __LINE__ );
    }
    catch( CException* pException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( 
            _T( "CEnvironmentVarCompareView::CompareFilesBasedOnLeftAsSourse()" ),
            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( 
          _T( "CEnvironmentVarCompareView::CompareFilesBasedOnLeftAsSourse()" ),
            _T( __FILE__ ), __LINE__ );
    }
    return false;
} 

Miscellaneous features

The environment variable compare tool provides lots of stunning features to the user. They are:

1. Exception tracker

Environment variable compare uses a very powerful exception handling technique. It handles the following types of exceptions. They are:

  1. CResourceException
  2. CMemoryException
  3. CException

Application handles unknown exceptions also. If any exception occurs during program execution it will show the corresponding exception details to the user and produce an exception details text file in the current working directory. Refer to the figure below:

Image 2

To identify the file, function, and line number it uses the following built-in macros. They are:

  1. __FILE__
  2. __LINE__

Each function should use this set exception handling code to get the exception details.

C++
catch( CMemoryException* pMemoryException )
{
    // dump the error.
    ExceptionHandler::DumpMFCException( 
      _T( "CEnvironmentVarCompareView::CompareFilesBasedOnLeftAsSourse()" ),
        pMemoryException, _T( __FILE__ ), __LINE__ );
}
catch( CException* pException )
{
    // dump the error.
    ExceptionHandler::DumpMFCException( 
      _T( "CEnvironmentVarCompareView::CompareFilesBasedOnLeftAsSourse()" ),
        pException, _T( __FILE__ ), __LINE__ );
}
catch( ... )
{
    // dump the error and throws exception.
    ExceptionHandler::DumpUnknownException( 
      _T(   "CEnvironmentVarCompareView::CompareFilesBasedOnLeftAsSourse()" ),
        _T( __FILE__ ), __LINE__ );
} 

To show a dialog and produce an exception report to the user it uses the following classes:

  1. ExceptionHandler
  2. ExceptionWatcher

Refer to the attached code to check how these classes are used. To use this type of exception handling in your code just add the above mentioned code in your application.

The Exception Handler class is used to get the exception details. After getting the exception details from the appropriate handler, it passes this information to the ExceptionWatcher class for displaying it in a dialog.

C++
class ExceptionHandler
{
public:
    ExceptionHandler();
    ~ExceptionHandler();
    // dump the details of MFC Exceptions.
    static void DumpMFCException( const CString& csFunctionName_i, CException* pException_i,
                                  const CString& csFileName_i = _T( __FILE__ ),
                                  int nLineNumber_i = __LINE__,
                                  const CString& csMoreInfo_i = _T(" "));
    // dump the details of Unknown Exceptions.
    static void DumpUnknownException( const CString& csFunctionName_i,
                                      const CString& csFileName_i = _T( __FILE__ ),
                                      int nLineNumber_i = __LINE__,
                                      const CString& csMoreInfo_i = _T(" "));
    // Catch the unknown exception and exit the process.
    static void HandleUnknownException( const CString& csFunctionName_i,
                                        const CString& csFileName_i = _T( __FILE__ ),
                                        int nLineNumber_i = __LINE__,
                                        const CString& csMoreInfo_i = _T(" "));
    // This function will dump the com exceptions details.
    static void DumpCOMException( const CString& csFunctionName_i, _com_error& ExceptionObject_i,
                                  const CString& csFileName_i = _T( __FILE__ ),
                                  int nLineNumber_i = __LINE__,
                                  const CString& csMoreInfo_i = _T(" "));
    static void DumpToFile( CString& csDumpDetails_i, const CString& csFileName_i, int& nlineNum_i,
                            const CString& csFunctionName_i );
    static bool CreateAndDumpInfo( CString& csDumpInfo_i );
}  

2. Error Trace

If an error happens during program execution, that will be logged into an application error trace. To log an error into Error Trace, Environment Variable Compare uses a DLL called EnvLogInfo.dll. The DLL contains an export function called WriteAppLog.

C++
extern "C" __declspec( dllexport ) void WriteAppLog( const CString csMsg_i )
{   
    try
    {
        // Getting current executable path
        TCHAR szDirectorPath[MAX_PATH];
        GetModuleFileName( NULL, szDirectorPath, MAX_PATH );
        CString csDirectoryPath = szDirectorPath;
        int nPos = csDirectoryPath.ReverseFind( _T( '\\' ));
        if( -1 != nPos )
        {
            CString csDir = csDirectoryPath.Left( nPos );
            csDir += g_lpctszLogFile;
            SYSTEMTIME stTime;
            CString csDate;
            CString csTime;
            CString csDateTimeBind;
            CStdioFile cFileOperator;
            // Get time and date.
            GetLocalTime( &stTime );
            // If the file is already exist, then write the information in to the 
            // same file.
            /* API Info : Determines whether a path to a file 
               system object such as a file or folder is valid.*/
            if( PathFileExists( csDir ))
            {
                if( !cFileOperator.Open( csDir,
                    CFile::modeWrite | CFile::shareDenyWrite ))
                {
                    return;
                }
                cFileOperator.SeekToEnd();
                csDate.Format( _T( "%d/%d/%d " ), stTime.wYear, stTime.wMonth, stTime.wDay );
                csTime.Format( _T( "%d:%d:%d " ), stTime.wHour, stTime.wMinute, stTime.wSecond );
                csDateTimeBind = csDate + _T( " " ) + csTime + _T( " " );
                cFileOperator.WriteString( csDateTimeBind );
                cFileOperator.WriteString( csMsg_i );
                cFileOperator.WriteString( _T( "\n" ));
                cFileOperator.Close();
            }
            else
            {
                if( !cFileOperator.Open( csDir, CFile::modeCreate |
                    CFile::modeWrite | CFile::shareDenyWrite ))
                {
                    return;
                }
                csDate.Format( _T( "%d/%d/%d " ), stTime.wYear, stTime.wMonth, stTime.wDay );
                csTime.Format( _T( "%d:%d:%d " ), stTime.wHour, stTime.wMinute, stTime.wSecond );
                csDateTimeBind = csDate + _T( " " ) + csTime + _T( " " );
                cFileOperator.WriteString( csDateTimeBind );
                cFileOperator.WriteString( csMsg_i );
                cFileOperator.WriteString( _T( "\n" ));
                cFileOperator.Close();
            }
        }
    }
    catch( ... )
    {
    }
}  

The figure below shows the error log.

Image 3

3. Safety Mode

Image 4

Don’t – Attackers can often control the value of important environment variables. You need to make sure that an attacker does not set environment variables to malicious values. In most cases, the information contained in the environment variables set by the shell can be determined by much more reliable means. Safety mode settings in Environment Variable Compare provides three options to the user. They are:

a. Show warning before updating system Registry

This option is enabled when the user updates the system Registry. Please refer to the figure below:

Image 5

b. Create an automatic system environment variable back up

To improve environment variable security, each time an application is run it automatically takes a system environment variable back up and saves it in the current execution directory. For this, Environment Variable Compare uses a thread called RegistryBackupThreadProc().

c. Perform a system restart after updating system Registry

This is for ensuring proper update of environment variables in the system and all the processes running in that system.

C++
bool SaftyMode::RestartSystem()
{
    try
    {
        HANDLE hToken; 
        TOKEN_PRIVILEGES tkp; 
        // Get a token for this process. 
        if (!OpenProcessToken(GetCurrentProcess(), 
            TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) 
        {
            return( FALSE ); 
        }
        // Get the LUID for the shutdown privilege. 
        LookupPrivilegeValue( NULL, SE_SHUTDOWN_NAME, 
            &tkp.Privileges[0].Luid ); 
        tkp.PrivilegeCount = 1;  // one privilege to set    
        tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 
        // Get the shutdown privilege for this process. 
        AdjustTokenPrivileges( hToken, FALSE, &tkp, 0, 
            (PTOKEN_PRIVILEGES)NULL, 0); 
        if ( GetLastError() != ERROR_SUCCESS )
        {
            return FALSE; 
        }
        if( !ExitWindowsEx( EWX_REBOOT, SHTDN_REASON_MINOR_ENVIRONMENT ))
        {
            AfxMessageBox( _T( "Failed to restart windows" ));
            CString csAppMsg;
            csAppMsg.Format(
              _T( "Failed to restart windows %s, Line num - %d" ), 
              _T( __FILE__ ), __LINE__);
            WriteAppLog( csAppMsg );
        }
        return true;
    }
    catch( CMemoryException* pMemoryException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "SaftyMode::RestartSystem()" ),
            pMemoryException, _T( __FILE__ ), __LINE__ );
    }
    catch( CException* pException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "SaftyMode::RestartSystem()" ),
            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( _T( "SaftyMode::RestartSystem()" ),
            _T( __FILE__ ), __LINE__ );
    }
    return false;
}

4. Save Operation

Save option is one of the important features of the Environment Variable Compare tool. It permits the user to do two types of savings. They are:

  1. File Saving
  2. Registry Saving

The figure below shows the user interface for the save operation.

Image 6

File Saving

I already mentioned that Environment Variable Compare uses two text files for comparing environment variables. After the user makes changes in the comparison GUI, the user can update these changes directly in to files. To update a file Environment Variable Compare uses a thread called FileWriteThreadProc.

Please find the below code for file update thread.

C++
UINT _cdecl SaveVariable::FileWriteThreadProc( LPVOID pParam_i )
{
    FILE_THREAD_FILE_INFO_t* pStFileThreadInfo = 
        static_cast<FILE_THREAD_FILE_INFO_t*>( pParam_i );
    try
    {
        if( NULL == pStFileThreadInfo )
        {
            REPORT_OPERATION_INFO( 
              _T( "Unexpected error occured. File updation failed" ));
            CString csAppMsg;
            csAppMsg.Format(
              _T( "Unexpected error occured. File updation " 
              "failed %s, Line num - %d" ), _T( __FILE__ ), __LINE__);
            WriteAppLog( csAppMsg );
            return 0;
        }
        CStdioFile csReadFile;
        CString csWriteLine;
        FILE_INFO_t stFileInfo;
        csReadFile.Remove( pStFileThreadInfo->csPath );
        HANDLE hHandle = CreateFile( pStFileThreadInfo->csPath, GENERIC_WRITE, 
                                     FILE_SHARE_READ, NULL, CREATE_ALWAYS, 
                                     FILE_ATTRIBUTE_NORMAL, NULL );
        if( hHandle == 0 )
        {
            REPORT_OPERATION_INFO( _T( "Failed to create the specified file" ));
            ::SetEvent( pStFileThreadInfo->hFileWriteThreadEvent );
            delete pStFileThreadInfo;
            pStFileThreadInfo = 0;
            CString csAppMsg;
            csAppMsg.Format(
              _T( "Failed to create the specified file %s, Line num - %d" ), 
              _T( __FILE__ ), __LINE__);
            WriteAppLog( csAppMsg );
            return 0;
        }
        CloseHandle( hHandle );
        if( !csReadFile.Open( pStFileThreadInfo->csPath , 
                   CFile::modeWrite | CFile::typeText ))
        { 
            REPORT_OPERATION_INFO(_T( "Failed to open file" ))
            ::SetEvent( pStFileThreadInfo->hFileWriteThreadEvent );
            delete pStFileThreadInfo;
            pStFileThreadInfo = NULL;
            CString csAppMsg;
            csAppMsg.Format(
              _T( "Failed to open file %s, Line num - %d" ), 
              _T( __FILE__ ), __LINE__);
            WriteAppLog( csAppMsg );
            return 0;
        }
        // Write List item to file back.
        int nListSize = pStFileThreadInfo->csFileInfoArray.GetSize();
        for( int nIndex = 0; nIndex < nListSize; ++nIndex )
        {
            CString csItem = pStFileThreadInfo->csFileInfoArray.GetAt( nIndex );
            if( SPACE_CHAR == csItem )
            {
                continue;
            }
            int nItemIdx = csItem.Find( EQUAL_CHAR );
            if( FAIL != nItemIdx )
            {
                CString csKey = csItem.Left( nItemIdx );
                if( NULL != pStFileThreadInfo->FileInfoMap->Lookup( csKey, stFileInfo ))
                {
                    csReadFile.WriteString( stFileInfo.csFullVariable );
                    csReadFile.WriteString( _T( "\n" ));
                }
                else
                {
                    REPORT_OPERATION_INFO( _T( "Unexpected error in file write operation." ));
                    CString csAppMsg;
                    csAppMsg.Format(
                      _T( "Unexpected error in file write operation %s, Line num - %d" ), 
                      _T( __FILE__ ), __LINE__);
                    WriteAppLog( csAppMsg );
                    ::SetEvent( pStFileThreadInfo->hFileWriteThreadEvent );
                    delete pStFileThreadInfo;
                    pStFileThreadInfo = NULL;
                    return false;
                }
            }
        }
        if( NULL != pStFileThreadInfo )
        {
            ::SetEvent( pStFileThreadInfo->hFileWriteThreadEvent );
            delete pStFileThreadInfo;
            pStFileThreadInfo = NULL;
        }
        return 1;
    }
    catch( CMemoryException* pMemoryException )
    {
        if( NULL != pStFileThreadInfo )
        {
            delete pStFileThreadInfo;
            pStFileThreadInfo = NULL;
        }
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "SaveVariable::FileWriteThreadProc()" ),
            pMemoryException, _T( __FILE__ ), __LINE__ );
    }
    catch( CException* pException )
    {
        if( NULL != pStFileThreadInfo )
        {
            delete pStFileThreadInfo;
            pStFileThreadInfo = NULL;
        }
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "SaveVariable::FileWriteThreadProc()" ),
            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        if( NULL != pStFileThreadInfo )
        {
            delete pStFileThreadInfo;
            pStFileThreadInfo = NULL;
        }
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( _T( "SaveVariable::FileWriteThreadProc()" ),
            _T( __FILE__ ), __LINE__ );
    }
    return 0;
} 

Registry update

“Modifying the registry can cause serious problems that may require you to reinstall your Operating System. We cannot guarantee that problems resulting from modifications to the Registry can be solved. Use the information provided at your own risk.”

Environment Variable Compare provides a powerful feature to the user for saving environment variables directly into the system Registry. I already mentioned that it is a very risky job to update system environment variables. After comparison the user can update the changed environment variables to the system by using the update system registry option in the save confirmation dialog. Environment Variable Compare uses a thread for updating system environment variables. To consider safety, during the save process, Environment Variable Compare skips some of the environment variables permanently. It means any changes in these types of variables during comparison of the two machines will be shown in the comparison tool but they will not be saved. The skipped variable names will be listed in the Error trace. The user can check the skipped environment variables from the Error trace.

The thread which is used to update the system environment variable is RegistryUpdateThreadProc.<code>Refer shown in the code below.

C++
UINT _cdecl SaveVariable::RegistryUpdateThreadProc( LPVOID pParam_i )
{
    REG_THREAD_INFO_t* pstRegThreadInfo = static_cast<REG_THREAD_INFO_t*>( pParam_i );
    try
    {
        if( NULL == pstRegThreadInfo )
        {
            REPORT_OPERATION_INFO( _T( "Unexpected error. Registry updation failed" ));
            CString csAppMsg;
            csAppMsg.Format(
              _T( "Unexpected error. Registry updation failed %s, Line num - %d" ), 
              _T( __FILE__ ), __LINE__);
            WriteAppLog( csAppMsg );
            return 0;
        }
        FILE_INFO_t stFileinfo;
        CString csItem;
        CString csKey;
        int nFind = 0;
        HKEY hEnvVarKey = 0;
        CString csNodeKey;
        HKEY hRootKey;
        csNodeKey = VARIABLE_REGISTER_KEY;
        hRootKey = HKEY_LOCAL_MACHINE;
        DWORD dwValueType = REG_SZ;
        const int MAX_BUFFER_SIZE = 2048;
        DWORD dwBufferSize;
        dwBufferSize = MAX_BUFFER_SIZE;
        CString csEnvironmentVal = _T( "" );
        bool bIsAlreadyPresent = false;
        int nCount = pstRegThreadInfo->csFileInfoArray.GetSize();
        for( int nidx = 0; nidx < nCount; ++nidx )
        {
            csItem = pstRegThreadInfo->csFileInfoArray.GetAt( nidx );
            nFind = csItem.Find( EQUAL_CHAR );
            if( FAIL == nCount )
            {
                continue;
            }
            csKey = csItem.Left( nFind );
            pstRegThreadInfo->FileInfoMap->Lookup( csKey, stFileinfo );
            bool isAvailable = false;
            for( int nIdx = 0; nIdx < DEF_VAR_COUNT; ++nIdx )
            {
                if( DefaultVariables[nIdx] == csKey )
                {
                    isAvailable = true;
                    CString csAppMsg;
                    csAppMsg.Format(
                      _T( "Skipped environment variable is %s : %s, Line num - %d" ),
                      csKey, _T( __FILE__ ), __LINE__);
                    WriteAppLog( csAppMsg );
                    break;
                }
            }
            // If it is not a default variable then add it in system environments.
            if( !isAvailable )
            {
                // Open the registry for saving environment variables.
                if( ERROR_SUCCESS != ::RegOpenKeyEx( hRootKey , csNodeKey , 0 ,
                    KEY_QUERY_VALUE  , &hEnvVarKey ))
                {
                    REPORT_OPERATION_INFO( _T( "Failed to open registry" ));
                    CString csAppMsg;
                    csAppMsg.Format(
                      _T( "Failed to open registry %s, Line num - %d" ), 
                      _T( __FILE__ ), __LINE__);
                    WriteAppLog( csAppMsg );
                    ::SetEvent( pstRegThreadInfo->hRegUpdateThreadEvent );
                    delete pstRegThreadInfo;
                    pstRegThreadInfo = 0;
                    return 0;
                }
                TCHAR* ptszBuffer = 0;
                dwValueType = REG_NONE;
                bIsAlreadyPresent = false;
                ptszBuffer = new TCHAR[MAX_BUFFER_SIZE];
                ZeroMemory( ptszBuffer, dwBufferSize );
                // Function for Retrieving the type and data for the specified
                // value name associated with an open registry key.
                if( ERROR_SUCCESS == ::RegQueryValueEx( hEnvVarKey , csKey ,
                    0 , &dwValueType , reinterpret_cast <LPBYTE>( ptszBuffer ), &dwBufferSize ))
                {
                    OutputDebugString( _T( "Check:" )+ csKey +_T( ":" )+stFileinfo.csVaribleValue );
                    bIsAlreadyPresent = true;
                }
                RegCloseKey( hEnvVarKey ); 
                delete []ptszBuffer;
                ptszBuffer = 0;
                DWORD dwRegType;
                // Function used to open system variable register key for setting value
                if( ERROR_SUCCESS == ::RegOpenKeyEx( hRootKey , csNodeKey , 0 ,
                    KEY_SET_VALUE  , &hEnvVarKey ))
                {
                    if( bIsAlreadyPresent )
                    {
                        if( 0 == dwValueType )
                        {
                            dwRegType = REG_NONE;
                        }
                        else if( UNITY == dwValueType )
                        {
                            dwRegType = REG_SZ;
                        }
                        else if( 2 == dwValueType )
                        {
                            dwRegType = REG_EXPAND_SZ;
                        }
                        else
                        {
                            // nop.
                        }
                    }
                    else
                    {
                        dwRegType = REG_SZ;
                    }
                }
                csEnvironmentVal = stFileinfo.csVaribleValue;
                dwBufferSize = 0;
                dwBufferSize = csEnvironmentVal.GetLength()*sizeof( TCHAR );
                LPSTR lpszBuffer = reinterpret_cast<LPSTR>( csEnvironmentVal.GetBuffer( dwBufferSize ));
                if( ERROR_SUCCESS == ::RegSetValueEx( hEnvVarKey , csKey, 0 , dwRegType ,
                    reinterpret_cast<LPBYTE>( lpszBuffer ) ,dwBufferSize ))
                {
                }
                csEnvironmentVal.ReleaseBuffer();
                RegCloseKey( hEnvVarKey );
            }
            else
            {
            }
        }
        if( NULL != pstRegThreadInfo )
        {
            ::SetEvent( pstRegThreadInfo->hRegUpdateThreadEvent );
            delete pstRegThreadInfo;
            pstRegThreadInfo = NULL;
        }
        return 1;
    }
    catch( CMemoryException* pMemoryException )
    {
        if( NULL != pstRegThreadInfo )
        {
            delete pstRegThreadInfo;
            pstRegThreadInfo = NULL;
        }
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "SaveVariable::RegistryUpdateThreadProc()" ),
            pMemoryException, _T( __FILE__ ), __LINE__ );
    }
    catch( CException* pException )
    {
        if( NULL != pstRegThreadInfo )
        {
            delete pstRegThreadInfo;
            pstRegThreadInfo = NULL;
        }
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "SaveVariable::RegistryUpdateThreadProc()" ),
            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        if( NULL != pstRegThreadInfo )
        {
            delete pstRegThreadInfo;
            pstRegThreadInfo = NULL;
        }
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( _T( "SaveVariable::RegistryUpdateThreadProc()" ),
            _T( __FILE__ ), __LINE__ );
    }
    return 0;
} 

5. Info Listener

This is a very powerful dynamic error reporting technique used in Environment Variable Compare for reporting any runtime execution information to the user. Actually Info Listener is a separate C++ application. This application is run on the system tray when the user turns on the “Execute Info Listener” option from the Environment Variable Compare options. Please refer to the figure below which shows the style of error reporting.

Image 7

To communicate between two C++ applications there is a Windows message called WM_COPYDATA. Please refer to the code below.

C++
void CEnvironmentVarCompareView::OnExecuteListner()
{
    try
    {
        CString csFileName;
        csFileName = _T( "\\" ) + csInfoListner;
        CWnd* hWnd = FindWindow( NULL, csInfoListner );
        if( NULL != hWnd )
        {
            REPORT_OPERATION_INFO( _T( "Hey iam here..." ));
            return;
        }
        TCHAR szDirectorPath[MAX_PATH];
        GetModuleFileName( NULL, szDirectorPath, MAX_PATH );
        CString csDirectoryPath = szDirectorPath;
        int nPos = csDirectoryPath.ReverseFind( _T( '\\' ));
        if( FAIL != nPos )
        {
            CString csDir = csDirectoryPath.Left( nPos );
            // Attach file name to directory path
            csDir += csFileName;
            SHELLEXECUTEINFO ExecuteInfo;
            memset( &ExecuteInfo, 0, sizeof(ExecuteInfo));
            ExecuteInfo.cbSize       = sizeof(ExecuteInfo);
            ExecuteInfo.fMask        = 0;                
            ExecuteInfo.hwnd         = 0;                
            ExecuteInfo.lpVerb       = _T( "open" );  // Operation to perform
            ExecuteInfo.lpFile       = csDir;         // Application name
            ExecuteInfo.lpDirectory  = 0;             // Default directory
            ExecuteInfo.nShow        = SW_SHOW;
            ExecuteInfo.hInstApp     = 0;
            if( ShellExecuteEx( &ExecuteInfo ) == FALSE )
            {
                if( IDYES == AfxMessageBox( 
                  _T( "Do you want browse application?" ), 
                  MB_ICONQUESTION|MB_YESNO ))
                {
                    CFileDialog FilDlgObj( TRUE );
                    CString csFilePath;    // Holding path of the opened file.
                    UpdateData( TRUE );
                    // Open the selected file.
                    if( IDOK == FilDlgObj.DoModal())
                    {
                        csFilePath = FilDlgObj.GetPathName();
                        // We need to ensure whether selected application is
                        // InfoListner or not?
                        int nPosition = csFilePath.ReverseFind('\\');
                        if( FAIL != nPosition )
                        {
                            CString csFileName = 
                              csFilePath.Right( csFilePath.GetLength() - nPosition - UNITY );
                            if( 0 != csFileName.Compare( _T( "InfoListner.exe" ) ))
                            {
                                AfxMessageBox( _T( "Sorry. Selected application is not InfoListner" ));
                                CString csAppMsg;
                                csAppMsg.Format(_T( "Sorry. Selected application is not " 
                                  "InfoListner - %s, Line num - %d" ), _T( __FILE__ ), __LINE__);
                                WriteAppLog( csAppMsg );
                                return;
                            }
                        }
                        SHELLEXECUTEINFO ExecuteInfoEx;
                        memset( &ExecuteInfoEx, 0, sizeof(ExecuteInfoEx));
                        ExecuteInfoEx.cbSize       = sizeof(ExecuteInfoEx);
                        ExecuteInfoEx.fMask        = 0;                
                        ExecuteInfoEx.hwnd         = 0;                
                        ExecuteInfoEx.lpVerb       = _T( "open" );  // Operation to perform
                        ExecuteInfoEx.lpFile       = csFilePath;    // Application name
                        ExecuteInfoEx.lpDirectory  = 0;             // Default directory
                        ExecuteInfoEx.nShow        = SW_SHOW;
                        ExecuteInfoEx.hInstApp     = 0;
                        if( ShellExecuteEx( &ExecuteInfoEx ) == FALSE )
                        {
                        }
                    }
                }
            }
        }
    }
    catch( CException* pException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( 
            _T( "CEnvironmentVarCompareView::OnExecuteListner()" ),
            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( 
            _T( "CEnvironmentVarCompareView::OnExecuteListner()" ),
            _T( __FILE__ ), __LINE__ );
    }
} 

Two receive messages from Environment Variable Compare, the Info Listener application should handle the WM_COPYDATA message.

C++
LRESULT CMainFrame::OnRecieve( WPARAM wParam_i, LPARAM lParam_i )
{
    PCOPYDATASTRUCT pstPageInfo = reinterpret_cast<PCOPYDATASTRUCT>( lParam_i );
    int nDataLength = pstPageInfo->cbData / sizeof( TCHAR );
    TCHAR *ptcInfoMsg = 0;
    ptcInfoMsg = new TCHAR[pstPageInfo->cbData + 1];
    memset( ptcInfoMsg, 0, pstPageInfo->cbData );
    memset( ptcInfoMsg, 0, pstPageInfo->cbData );
    _tcsnccpy( ptcInfoMsg, 
      reinterpret_cast<wchar_t *>( pstPageInfo->lpData ), nDataLength );
    ptcInfoMsg[nDataLength] = 0;
    CView* pView = GetActiveView();
    CString csText;
    if( 0 == pView )
    {
        return 0;
    }
    pView->PostMessage( INFO_MSG, 0, reinterpret_cast<LPARAM>( ptcInfoMsg ));
    return 1;
} 

Suppose the Info Listener application is already running on the system tray, then the user can not run another instance of Info Listener using the Environment Variable Compare application.

6. Path Editor

Image 8

Editing the PATH environment variable in Windows is an unpleasant experience. First, it takes several steps to get to the interface. Second, the interface is impossible: Something you can do to make the task easier is copy the whole field, edit it in a text editor, and paste it back.

But Environment Variable Compare path editor provides a better experience for users. It provides all the features to the user for editing environment variables in better ways.

7.Compare files through Command prompt

Image 9

This is an important feature provided to user for comparing their environment variables through a command prompt. The execution step is explained below.

  1. Run command prompt.
  2. Change directory to your application exe path using “cd” command.
  3. Enter exe name and give a space and enter path of files you want to compare (e.g.: EnvironmentVarCompare.exe C:\FileFirst.txt C:\FileSecond.txt).
  4. Press Enter button from keyboard.

Expected result: EnvironmentVariableCompare will be run and shows the compared results in two sides.

[You can check the code from the attached files.]

To parse the command line at start up, there is a Windows class called CCommandLineInfo.

Remarks: To get the command line parameters we need to create a class from CCommandLineInfo. Please check the code below.

C++
class ReadCommandLineInfo : public CCommandLineInfo
{
public:
    ReadCommandLineInfo(void);
    ~ReadCommandLineInfo(void);
protected:
     virtual void ParseParam( LPCTSTR, BOOL bFlag_i, BOOL bLast_i );
private:
    CStringArray m_csParamArray; 
};  

After getting the first parameter, we need to keep that parameter in our internal data structure. This is because we get the parameters from the command line one by one. So after getting the first parameter, internally a timer will be started. When the timer elapses it starts to compare the variables and shows the result. To perform the command line comparison, Environment Variable Compare uses a class called PerformCmdLineCompare.

C++
#define THREAD_CMD_INFO WM_USER + 1001
// PerformCmdLineCompare
IMPLEMENT_DYNCREATE(PerformCmdLineCompare, CWinThread)
PerformCmdLineCompare* PerformCmdLineCompare::m_pPerfmCmdLineOp = 0;
UINT_PTR PerformCmdLineCompare::m_uPerformCmdOptimerID = 0;
int PerformCmdLineCompare::m_nDelayTime = 0;
PerformCmdLineCompare::PerformCmdLineCompare()
{
}
PerformCmdLineCompare::~PerformCmdLineCompare()
{
}
BOOL PerformCmdLineCompare::InitInstance()
{
    // TODO:  perform and per-thread initialization here
    return TRUE;
}
int PerformCmdLineCompare::ExitInstance()
{
    // TODO:  perform any per-thread cleanup here
    return CWinThread::ExitInstance();
}
BEGIN_MESSAGE_MAP(PerformCmdLineCompare, CWinThread)
ON_THREAD_MESSAGE(WM_TIMER, OnTimer )
ON_THREAD_MESSAGE(THREAD_CMD_INFO, OnReciveCmd )
END_MESSAGE_MAP()

// PerformCmdLineCompare message handlers
PerformCmdLineCompare* PerformCmdLineCompare::GetInstance()
{
    try
    {
        if( NULL == m_pPerfmCmdLineOp )
        {
            m_pPerfmCmdLineOp = dynamic_cast<PerformCmdLineCompare*>( 
              ::AfxBeginThread( RUNTIME_CLASS( PerformCmdLineCompare )));
            if( NULL == m_pPerfmCmdLineOp )
            {
                AfxMessageBox( _T(  "Failed to create object" ));
                REPORT_OPERATION_INFO( _T( "Unable to create PerformCmdLineCompare." ))
            }
            m_nDelayTime = 200;
        }
    }
    catch( CMemoryException* pMemoryException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "PerformCmdLineCompare::GetInstance()" ),
            pMemoryException, _T( __FILE__ ), __LINE__ );
    }
    catch( CException* pException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "PerformCmdLineCompare::GetInstance()" ),
            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( _T( "PerformCmdLineCompare::GetInstance()" ),
            _T( __FILE__ ), __LINE__ );
    }
    return m_pPerfmCmdLineOp;
}   
C++
void PerformCmdLineCompare::OnTimer( UINT nIDEvent_i, LPARAM lparam_i )
{
    try
    {
        // Kill the timer
        ::KillTimer( NULL, m_uPerformCmdOptimerID );
        m_uPerformCmdOptimerID = 0;
        if( 2 < m_csParamInfoArray.GetSize())
        {
            REPORT_OPERATION_INFO( _T( "Invalied parameter format" ));
            return;
        }
        CEnvironmentVarCompareView* pMainView = 
          PooledInstance::GetInstance().GetMainViewPtr();
        if( 0 != pMainView )
        {
            CString csLeftFile = m_csParamInfoArray.GetAt( 0 );
            CString csRightFile = m_csParamInfoArray.GetAt( 1 );
            pMainView->ReadCmdLineFiles( csLeftFile, csRightFile );
        }
    }
    catch( CException* pException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "PerformCmdLineCompare::OnTimer()" ),
            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( _T( "PerformCmdLineCompare::OnTimer()"),
            _T( __FILE__ ), __LINE__ );
    }
}

void PerformCmdLineCompare::ReciveParam( CStringArray& csParamInfoArray_i )
{
    try
    {
        if( 0 != m_uPerformCmdOptimerID )
        {
            REPORT_OPERATION_INFO( _T( "Another comparison operation in progress."));
            CString csAppMsg;
            csAppMsg.Format(_T( "Another comparison operation in" 
              " progress. - %s, Line num - %d" ), _T( __FILE__ ), __LINE__);
            WriteAppLog( csAppMsg );
            return;
        }
        m_csParamInfoArray.RemoveAll();
        m_csParamInfoArray.Copy( csParamInfoArray_i );
        m_pPerfmCmdLineOp->PostThreadMessage( THREAD_CMD_INFO, NULL, NULL );
    }
    catch( CException* pException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "PerformCmdLineCompare::ReciveParam()"),
            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( _T( "PerformCmdLineCompare::ReciveParam()" ),
            _T( __FILE__ ), __LINE__ );
    }
}

void PerformCmdLineCompare::OnReciveCmd( WPARAM wParam_i, LPARAM lParam_i )
{
    try
    {
        m_uPerformCmdOptimerID = ::SetTimer( NULL, 0, 1000, NULL );
        if( 0 == m_uPerformCmdOptimerID )
        {
            REPORT_OPERATION_INFO( _T( "Timer not started." ));
        }
    }
    catch( CException* pException )
    {
        // dump the error.
        ExceptionHandler::DumpMFCException( _T( "PerformCmdLineCompare::OnReciveCmd()" ),
            pException, _T( __FILE__ ), __LINE__ );
    }
    catch( ... )
    {
        // dump the error and throws exception.
        ExceptionHandler::DumpUnknownException( _T( "PerformCmdLineCompare::OnReciveCmd()" ),
            _T( __FILE__ ), __LINE__ );
    }
} 

8. Auto complete in combo box

Image 10

You have probably noticed that when we type some paths in the Run dialog of Windows, it will list the files and folders under that path. Similarly, when you type a path in the combobox of this application, it will list the files and folders under that path. SHAutoComplete takes care of this feature. This function needs a handle of the edit control.

C++
CoInitialize(0);
AfxOleInit();
COMBOBOXINFO info = { sizeof(COMBOBOXINFO) };
// Retrieves information about the specified combo box.
BOOL bLeftComboInfo = GetComboBoxInfo( m_LeftCombo.m_hWnd, &info );
if( bLeftComboInfo )
{
    // Instructs system edit controls to use AutoComplete t/o help complete URLs or file system paths.
    HRESULT hRes = SHAutoComplete( info.hwndItem, SHACF_FILESYSTEM ); 
} 

9. Quick compare info

This feature helps the user to find quick comparison information in very less time, i.e., after comparison we need to know how many different lines are present and how many new lines are present in the compared file. One solution for this problem is to count the lines manually. But it requires much more time to find. So in this tool I introduced a new interface for it. Please find the option “Compare Info” from the File option. It provides three main information for the user. They are:

  1. Number of same lines
  2. Number of different lines
  3. Number of new lines

Please refer to the figure below.

Image 11

Add New variables

Image 12 

Environment variable compare provide an option to user for adding new variables to system. You can add new variables to system or user variables by selecting appropriate option from specified combo box.

System Variables

You must be an administrator to modify a system environment variable. So when you want to add new variables to your system, you should run this application with Administrator privilege. System environment variables are defined by Windows and apply to all computer users. Changes to the system environment are written to the registry, and usually require a restart to become effective.

User Variable for User Name

Any user can add, modify, or remove a user environment variable. Environment variable compare provide an option to add new variable to system user variables. The changes are written to the registry, and are usually effective immediately. However, after a change to user environment variables is made, any open software programs should be restarted to force them to read the new registry values. The common reason to add variables is to provide data that is required for variables that you want to use in scripts. 

Revision History

  • 07-July-2013 :- First post.
  •  26-July-2013 : Given explanation for Adding new variables to system. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
India India
Creative thinker Smile | :)

Comments and Discussions

 
GeneralMy vote of 5 Pin
skyformat99@gmail.com6-Jul-13 13:09
skyformat99@gmail.com6-Jul-13 13:09 
GeneralRe: My vote of 5 Pin
SajeeshCheviry6-Jul-13 17:33
SajeeshCheviry6-Jul-13 17:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.