Click here to Skip to main content
15,894,955 members
Articles / Programming Languages / Visual Basic 6

Professional System Library: Introduction

Rate me:
Please Sign up or sign in to vote.
4.84/5 (93 votes)
22 Nov 2010CPOL14 min read 194.4K   3.4K   232  
A simplified and unified way for accessing most frequently used information about Process, System, and Environment.
// PSLWMI.cpp : Implementation of CPSLWMI

#include "stdafx.h"
#include "PSLWMI.h"
#include "PSLTable.h"
#include <Wbemidl.h>

/*
WMI is configurable via UI invoked via command wmimgmt.msc
Using wmimgmt.msc one can change default namespace, security for WMI, do backup/restore;
*/

CPSLWMI::CPSLWMI()
{
	m_LastError = 0;

	m_sDefaultNamespace = _T("root\\cimv2");
	m_DefaultImpersonationLevel = impUnknown;
	m_sInstallationDir = _T("");
	m_sVersion = _T("");

	tstring buffer;
	buffer.resize(MAX_FILE_PATH);

	ULONG nChars = MAX_FILE_PATH;
	CRegKey key;
	if(key.Open(HKEY_LOCAL_MACHINE, _T("SOFTWARE\\Microsoft\\WBEM\\Scripting"), KEY_READ) == ERROR_SUCCESS)
	{
		if(key.QueryStringValue(_T("Default Namespace"), (LPTSTR)buffer.c_str(), &nChars) == ERROR_SUCCESS)
			m_sDefaultNamespace = buffer.c_str();

		DWORD dwValue = 0;
		if(key.QueryDWORDValue(_T("Default Impersonation Level"), dwValue) == ERROR_SUCCESS)
			m_DefaultImpersonationLevel = static_cast<PSLImpersonationLevel>(dwValue);

		key.Close();
	}

	if(key.Open(HKEY_LOCAL_MACHINE, _T("SOFTWARE\\Microsoft\\WBEM"), KEY_READ) == ERROR_SUCCESS)
	{
		nChars = MAX_FILE_PATH;
		if(key.QueryStringValue(_T("BUILD"), (LPTSTR)buffer.c_str(), &nChars) == ERROR_SUCCESS)
			m_sVersion = buffer.c_str();

		nChars = MAX_FILE_PATH;
		if(key.QueryStringValue(_T("Installation Directory"), (LPTSTR)buffer.c_str(), &nChars) == ERROR_SUCCESS)
		{
			m_sInstallationDir = buffer.c_str();
			if(!_tcsnicmp(m_sInstallationDir, _T("%SystemRoot%"), 12))
			{
				::GetWindowsDirectory((LPTSTR)buffer.c_str(), MAX_FILE_PATH);
				tstring tmp = m_sInstallationDir;
				m_sInstallationDir = tmp.replace(0, 12, buffer.c_str()).c_str();
			}
		}

		key.Close();
	}

	m_pLocator = NULL;
	m_pServices = NULL;
	::InitializeCriticalSection(&m_csLocator);
	::InitializeCriticalSection(&m_csServices);
}

CPSLWMI::~CPSLWMI()
{
	::DeleteCriticalSection(&m_csServices);
	::DeleteCriticalSection(&m_csLocator);
}

HRESULT CPSLWMI::FinalConstruct()
{
	return S_OK;
}

void CPSLWMI::FinalRelease()
{
	::EnterCriticalSection(&m_csServices);
	if(m_pServices)
	{
		m_pServices.Release();
		m_sServiceName = _T("");
	}
	::LeaveCriticalSection(&m_csServices);

	::EnterCriticalSection(&m_csLocator);
	if(m_pLocator)
	{
		m_pLocator.Release();
		m_pLocator = NULL;
	}
	::LeaveCriticalSection(&m_csLocator);
}

HRESULT CPSLWMI::RaiseWMIException(long lErrorCode)
{
	switch(lErrorCode)
	{
	case WBEM_E_ACCESS_DENIED:
		{
			SetException(exAccessDenied);
			break;
		}
	case WBEM_E_INVALID_CLASS:
	case WBEM_E_INVALID_QUERY:
	case WBEM_E_INVALID_PARAMETER:
	case WBEM_E_INVALID_NAMESPACE:
	case WBEM_E_INVALID_OBJECT:
	case WBEM_E_INVALID_QUERY_TYPE:
	case WBEM_E_NOT_FOUND:
	case E_INVALIDARG:
		{
			SetException(exInvalidParameter);
			break;
		}
	case WBEM_E_OUT_OF_MEMORY:
	case E_OUTOFMEMORY:
		{
			SetException(exLowMemory);
			break;
		}
	case WBEM_E_FAILED:
	case WBEM_E_PROVIDER_FAILURE:
	case WBEM_E_TYPE_MISMATCH:
	case WBEM_E_INVALID_CONTEXT:
	case WBEM_E_NOT_AVAILABLE:
	case WBEM_E_CRITICAL_ERROR:
	case WBEM_E_NOT_SUPPORTED:
	case WBEM_E_INVALID_STREAM:
	case WBEM_E_INVALID_SUPERCLASS:
	case WBEM_E_PROVIDER_NOT_FOUND:
	case WBEM_E_INVALID_PROVIDER_REGISTRATION:
	case WBEM_E_PROVIDER_LOAD_FAILURE:
	case WBEM_E_INITIALIZATION_FAILURE:
	case WBEM_E_TRANSPORT_FAILURE:
	case WBEM_E_INVALID_OPERATION:
	case WBEM_E_ALREADY_EXISTS:
	case WBEM_E_UNEXPECTED:
	case WBEM_S_TIMEDOUT:
	case WBEM_S_FALSE:
		{
			SetException(exWMIError);
			break;
		}
	case WBEM_E_SHUTTING_DOWN:
		{
			// special case, need to re-create interfaces again!
			SetException(exWMIError);
			break;
		}
	case DISP_E_BADINDEX:
		{
			SetException(exIndexOutOfRange);
			break;
		}
	default:
		{
			SetException(exGeneric);
			break;
		}
	}
	m_LastError = lErrorCode;
	return GetExitCode();
}

/*
Function GetWQLColumnsFromCSV;
Can parse WQL type of CSV;
Example1: "Col1, Col2"
Example2: "Col1 Col2"
Example3: "Col1;Col2"

Returns true, if at least one column name has been found.
*/
bool CPSLWMI::GetWQLColumnsFromCSV(LPCTSTR sText, vector<tstring> & Columns)
{
	tstring src = sText;
	LPTSTR start = (LPTSTR)src.c_str();
	size_t l = src.length();
	LPTSTR end = start + l;
	for(size_t i = 0;i < l;i ++)
		if(start[i] == '\t' || start[i] == ',' || start[i] == ';' || start[i] == '\n' || start[i] == '\r' || start[i] == '*')
			start[i] = ' ';

	LPTSTR cols = start;
	long nCols = 0; // number of columns found;
	while(cols < end)
	{
		while(cols[0] == ' ' && cols < end)
			cols ++;
		if(cols == end)
			break;
		LPCTSTR colStart = cols;
		while(cols[0] != ' ' && cols < end)
			cols ++;
		tstring colName;
		colName.resize(cols - colStart);
		::_tcsncpy_s((LPTSTR)colName.c_str(), cols - colStart + 1, sText + (colStart - start), cols - colStart);
		Columns.push_back(colName);
		nCols ++;
	}

	return (nCols > 0);
}

bool CPSLWMI::ExtractSELECTDetails(LPCTSTR sQuery, tstring & sClassName, vector<tstring> & Columns, bool & bAllCols, bool & bHasSpecials)
{
	bAllCols = false;
	tstring src = sQuery;
	LPTSTR start = (LPTSTR)src.c_str();
	::_tcslwr_s(start, src.length() + 1);
	size_t l = src.length();
	for(size_t i = 0;i < l;i ++)
		if(start[i] == '\t' || start[i] == ',')
			start[i] = ' ';

	LPCTSTR select = ::_tcsstr(start, _T("select "));
	LPCTSTR from = ::_tcsstr(start, _T(" from "));
	if(!select || !from || select > from)
		return false;

	from ++;

	LPCTSTR columns = select;
	while(columns[0] != ' ' && columns < from)
		columns ++;

 	if(columns == from) // Got as far as FROM statement but no columns found;
		return false;

	int cols = 0; // number of columns found;
	while(columns < from)
	{
		while(columns[0] == ' ' && columns < from)
			columns ++;
		if(columns == from)
			break;
		LPCTSTR colStart = columns;
		while(columns[0] != ' ' && columns < from)
			columns ++;
		if(columns == from)
			break;
		if(colStart[0] == '*')
		{
			bAllCols = true;
			break;
		}
		if(columns - colStart >= 2 && colStart[0] == '_' && colStart[1] == '_')
			bHasSpecials = true;
		tstring colName;
		colName.resize(columns - colStart);
		::_tcsncpy_s((LPTSTR)colName.c_str(), columns - colStart + 1, sQuery + (colStart - start), columns - colStart);
		Columns.push_back(colName);
		cols ++;
	}
	if(!cols && !bAllCols)
		return false; // no columns found;

	LPCTSTR table = from;
	while(table[0] && table[0] != ' ')
		table ++;
	if(!table[0])
		return false;
	while(table[0] && table[0] == ' ')
		table ++;
	if(!table[0])
		return false;
	LPCTSTR end = table;
	while(end[0] && end[0] != ' ')
		end ++;
	::_tcsncpy_s((LPTSTR)src.c_str(), end - table + 1, sQuery + (table - start), end - table);
	sClassName = src.c_str();

	return true;
}

CComPtr<IWbemLocator> CPSLWMI::GetLocator(long & lErrorCode)
{
	CCritSecLock cs(m_csLocator);
	if(!m_pLocator)
		lErrorCode = m_pLocator.CoCreateInstance(CLSID_WbemAdministrativeLocator, NULL, CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER);
	return m_pLocator;
}

CComPtr<IWbemServices> CPSLWMI::GetServices(BSTR NameSpace, long & lErrorCode)
{
	tstring NS;
	if(NameSpace)
		NS = NameSpace;

	CCritSecLock cs(m_csServices);
	if(NS.length() < 1)
		NS = m_sDefaultNamespace;
	
	if(m_sServiceName.length() > 0 && !_tcsicmp(m_sServiceName, NS.c_str())) // The same namespace as requested the last time;
		return m_pServices;

	// New WMI namespace is being requested;
	if(m_pServices)
	{
		m_sServiceName = _T("");
		m_pServices.Release();
	}

	CComPtr<IWbemLocator> pLocator = GetLocator(lErrorCode);
	if(pLocator)
	{
		lErrorCode = pLocator->ConnectServer(_bstr_t(NS.c_str()), NULL, NULL, NULL, 0, NULL, NULL, &m_pServices);
		if(lErrorCode == WBEM_S_NO_ERROR)
		{
			m_sServiceName = NS.c_str();

			// Authenticate calls into the interface. Unlikely to ever fail.
			::CoSetProxyBlanket(m_pServices, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE);
		}
	}

	return m_pServices;
}

CComPtr<IEnumWbemClassObject> CPSLWMI::ExecuteQuery(BSTR NameSpace, LPCTSTR sWQL, long & lErrorCode)
{
	CComPtr<IEnumWbemClassObject> pEnumObject;
	CComPtr<IWbemServices> pServices = GetServices(NameSpace, lErrorCode);
	if(pServices)
		lErrorCode = pServices->ExecQuery(_bstr_t(_T("WQL")), _bstr_t(sWQL), WBEM_FLAG_RETURN_IMMEDIATELY|WBEM_FLAG_FORWARD_ONLY, NULL, &pEnumObject);
	return pEnumObject;
}

////////////////////////////////////////////////////////////////////////
// Interface Implementation;
////////////////////////////////////////////////////////////////////////

STDMETHODIMP CPSLWMI::get_LastError(long * pValue)
{
	PSL_BEGIN

	*pValue = m_LastError;

	PSL_END
}

STDMETHODIMP CPSLWMI::get_DefaultNamespace(BSTR * pValue)
{
	PSL_BEGIN

	CCritSecLock cs(m_csServices);
	*pValue = m_sDefaultNamespace.copy();

	PSL_END
}

STDMETHODIMP CPSLWMI::put_DefaultNamespace(BSTR newValue)
{
	PSL_BEGIN

	CCritSecLock cs(m_csServices);
	m_sDefaultNamespace = newValue;

	PSL_END
}

STDMETHODIMP CPSLWMI::get_DefaultImpersonationLevel(PSLImpersonationLevel * pValue)
{
	PSL_BEGIN

	*pValue = m_DefaultImpersonationLevel;

	PSL_END
}

STDMETHODIMP CPSLWMI::get_Version(BSTR * pValue)
{
	PSL_BEGIN

	*pValue = m_sVersion.copy();

	PSL_END
}

STDMETHODIMP CPSLWMI::get_InstallationDir(BSTR * pValue)
{
	PSL_BEGIN

	*pValue = m_sInstallationDir.copy();

	PSL_END
}

STDMETHODIMP CPSLWMI::GetRowValues(BSTR NameSpace, BSTR ClassName, BSTR ValueName, SAFEARRAY ** ppValue)
{
	PSL_BEGIN

	*ppValue = NULL;

	if(!ClassName || !ValueName)
		return MakeException(exInvalidParameter);

	_bstr_t name(ValueName);
	vector<tstring> Columns;
	if(!GetWQLColumnsFromCSV(name, Columns) || Columns.size() != 1)
		return MakeException(exInvalidParameter);

	tstring sQuery;
	sQuery.resize(64);
	::wsprintf((LPTSTR)sQuery.c_str(), _T("SELECT %s FROM %s"), (LPCTSTR)name, (LPCTSTR)_bstr_t(ClassName));

	long lErrorCode = 0;
	CComPtr<IEnumWbemClassObject> pEnumObject = ExecuteQuery(NameSpace, _bstr_t(sQuery.c_str()), lErrorCode);
	if(!pEnumObject)
		return RaiseWMIException(lErrorCode);

	vector<_variant_t> values;
	do
	{
		ULONG uCount = 1, uReturned;
		CComPtr<IWbemClassObject> pClassObject;
		lErrorCode = pEnumObject->Next(WBEM_INFINITE, uCount, &pClassObject, &uReturned);
		if(lErrorCode != WBEM_S_FALSE && lErrorCode != WBEM_S_NO_ERROR)
			return RaiseWMIException(lErrorCode);
		if(lErrorCode == WBEM_S_NO_ERROR)
		{
			_variant_t v;
			lErrorCode = pClassObject->Get(name, 0, &v, 0, 0);
			if(lErrorCode != WBEM_S_NO_ERROR)
				return RaiseWMIException(lErrorCode);
			values.push_back(v);
			v.Clear();
		}
	}
	while(lErrorCode == WBEM_S_NO_ERROR);

	SAFEARRAYBOUND rgsabound[1];
	rgsabound[0].lLbound = 0;
	rgsabound[0].cElements = (ULONG)values.size();
	*ppValue = ::SafeArrayCreate(VT_VARIANT, 1, rgsabound);	// Create Safe Array;
	if(!*ppValue)
		return MakeException(exLowMemory);

	long Idx = 0;
	for(vector<_variant_t>::iterator i = values.begin();i != values.end();i ++)
	{
		::SafeArrayPutElement(*ppValue, &Idx, &(*i));	// Put string into array;
		Idx ++;
		i->Clear();
	}

	PSL_END
}

STDMETHODIMP CPSLWMI::GetColValues(BSTR NameSpace, BSTR ClassName, BSTR ValueNamesCSV, SAFEARRAY ** ppValue)
{
	PSL_BEGIN

	*ppValue = NULL;

	if(!ClassName || !ValueNamesCSV)
		return MakeException(exInvalidParameter);

	_bstr_t names(ValueNamesCSV);
	vector<tstring> Columns;
	if(!GetWQLColumnsFromCSV(names, Columns))
		return MakeException(exInvalidParameter);

	tstring sQuery;
	long l = 64 + names.length();
	sQuery.resize(l);
	::wsprintf((LPTSTR)sQuery.c_str(), _T("SELECT %s FROM %s"), (LPCTSTR)names, (LPCTSTR)_bstr_t(ClassName));

	long lErrorCode = 0;
	CComPtr<IEnumWbemClassObject> pEnumObject = ExecuteQuery(NameSpace, _bstr_t(sQuery.c_str()), lErrorCode);
	if(!pEnumObject)
		return RaiseWMIException(lErrorCode);

	ULONG uCount = 1, uReturned;
	CComPtr<IWbemClassObject> pClassObject;
	lErrorCode = pEnumObject->Next(WBEM_INFINITE, uCount, &pClassObject, &uReturned);

	if(lErrorCode == WBEM_S_FALSE) // no record found;
	{
		SAFEARRAYBOUND rgsabound[1];	// Safe Array boundaries;
		rgsabound[0].lLbound = 0;		// Low bound of the dimension is 0;
		rgsabound[0].cElements = 0;		// Number of elements in the array;
		*ppValue = ::SafeArrayCreate(VT_VARIANT, 1, rgsabound);	// Create Safe Array;
		if(!*ppValue)
			return MakeException(exLowMemory);
		return S_OK;
	}

	if(lErrorCode != WBEM_S_NO_ERROR)
		return RaiseWMIException(lErrorCode);

	SAFEARRAYBOUND rgsabound[1];
	rgsabound[0].lLbound = 0;
	rgsabound[0].cElements = (ULONG)Columns.size();
	*ppValue = ::SafeArrayCreate(VT_VARIANT, 1, rgsabound);
	if(!*ppValue)
		return MakeException(exLowMemory);

	long Idx = 0;
	for(vector<tstring>::iterator i = Columns.begin();i != Columns.end();i ++)
	{
		_variant_t v;
		lErrorCode = pClassObject->Get(i->c_str(), 0, &v, 0, 0);
		if(lErrorCode != WBEM_S_NO_ERROR)
		{
			::SafeArrayDestroy(*ppValue);
			*ppValue = NULL;
			return RaiseWMIException(lErrorCode);
		}
		::SafeArrayPutElement(*ppValue, &Idx, &v);	// Put string into array;
		Idx ++;
		v.Clear();
	}

	PSL_END
}

STDMETHODIMP CPSLWMI::GetColNames(BSTR NameSpace, BSTR ClassName, SAFEARRAY ** ppValue)
{
	PSL_BEGIN

	*ppValue = NULL;

	if(!ClassName)
		return MakeException(exInvalidParameter);

	long lErrorCode = 0;
	CComPtr<IWbemServices> pServices = GetServices(NameSpace, lErrorCode);
	if(!pServices)
		return RaiseWMIException(lErrorCode);

	CComPtr<IWbemClassObject> pClass;
	lErrorCode = pServices->GetObject(ClassName, WBEM_FLAG_RETURN_WBEM_COMPLETE, NULL, &pClass, NULL);
	if(lErrorCode != WBEM_S_NO_ERROR)
		return RaiseWMIException(lErrorCode);

	lErrorCode = pClass->GetNames(NULL, WBEM_FLAG_ALWAYS|WBEM_FLAG_NONSYSTEM_ONLY, NULL, ppValue);
	if(lErrorCode != WBEM_S_NO_ERROR)
		return RaiseWMIException(lErrorCode);

	PSL_END
}

STDMETHODIMP CPSLWMI::GetValue(BSTR NameSpace, BSTR ClassName, BSTR ValueName, VARIANT * pValue)
{
	PSL_BEGIN

	if(!ClassName || !ValueName)
		return MakeException(exInvalidParameter);

	tstring sQuery;
	sQuery.resize(64);
	::wsprintf((LPTSTR)sQuery.c_str(), _T("SELECT %s FROM %s"), (LPCTSTR)_bstr_t(ValueName), (LPCTSTR)_bstr_t(ClassName));

	long lErrorCode = 0;
	CComPtr<IEnumWbemClassObject> pEnumObject = ExecuteQuery(NameSpace, _bstr_t(sQuery.c_str()), lErrorCode);
	if(!pEnumObject)
		return RaiseWMIException(lErrorCode);

	ULONG uCount = 1, uReturned;
	CComPtr<IWbemClassObject> pClassObject;
	lErrorCode = pEnumObject->Next(WBEM_INFINITE, uCount, &pClassObject, &uReturned);
	if(lErrorCode == WBEM_S_FALSE)
	{
		VARIANT v;
		::VariantInit(&v);
		v.vt = VT_NULL;
		::VariantCopy(pValue, &v);
		lErrorCode = WBEM_S_NO_ERROR;
	}
	else
	{
		if(lErrorCode != WBEM_S_NO_ERROR)
			return RaiseWMIException(lErrorCode);
		lErrorCode = pClassObject->Get(_bstr_t(ValueName), 0, pValue, 0, 0);
	}

	if(lErrorCode != WBEM_S_NO_ERROR)
		return RaiseWMIException(lErrorCode);

	PSL_END
}

STDMETHODIMP CPSLWMI::GetData(BSTR NameSpace, BSTR WQL, IPSLTable ** ppValue)
{
	PSL_BEGIN

	*ppValue = NULL;

	if(!WQL)
		return MakeException(exInvalidParameter);

	tstring ClassName;
	vector<tstring> Columns;
	vector<tstring> * pColumns = &Columns;
	bool bAllCols = false, bHasSpecials = false;
	_bstr_t sQuery = WQL;
	if(!ExtractSELECTDetails(sQuery, ClassName, Columns, bAllCols, bHasSpecials))
		return MakeException(exInvalidParameter);

	if(bAllCols)
		pColumns = NULL;

	long lErrorCode = 0;
	CComPtr<IEnumWbemClassObject> pEnumObject = ExecuteQuery(NameSpace, sQuery, lErrorCode);
	if(!pEnumObject)
		return RaiseWMIException(lErrorCode);

	ULONG uCount = 1, uReturned;
	CComPtr<IWbemClassObject> pClassObject;
	lErrorCode = pEnumObject->Next(WBEM_INFINITE, uCount, &pClassObject, &uReturned);

	if(lErrorCode == WBEM_S_FALSE) // No records found;
	{
		// We need to get list of columns in a different way now;
		CComPtr<IWbemServices> pServices = GetServices(NameSpace, lErrorCode);
		if(pServices)
		{
			CComPtr<IWbemClassObject> pClass;
			lErrorCode = pServices->GetObject(_bstr_t(ClassName.c_str()), WBEM_FLAG_RETURN_WBEM_COMPLETE, NULL, &pClass, NULL);
			if(lErrorCode != WBEM_S_NO_ERROR)
				return RaiseWMIException(lErrorCode);

			CComObject<CPSLTable> * pTable = NULL;
			CComObject<CPSLTable>::CreateInstance(&pTable);
			pTable->AddRef();

			if(pTable->Initialize(pClass, lErrorCode, pColumns, bHasSpecials))
			{
				long nCols;
				pTable->get_nCols(&nCols);
				if(nCols < 1)
				{
					pTable->Release();
					return MakeException(exInvalidParameter);
				}
				*ppValue = CComPtr<IPSLTable>(pTable);
			}
			else
				pTable->Release();
		}
		else
			return RaiseWMIException(lErrorCode);

		if(lErrorCode != WBEM_S_NO_ERROR)
			return RaiseWMIException(lErrorCode);

		return S_OK;
	}

	if(lErrorCode != WBEM_S_NO_ERROR)
		return RaiseWMIException(lErrorCode);

	CComObject<CPSLTable> * pTable = NULL;
	CComObject<CPSLTable>::CreateInstance(&pTable);
	pTable->AddRef();

	bool bInit = pTable->Initialize(pClassObject, lErrorCode, pColumns, bHasSpecials);
	long lCols = 0;
	if(bInit)
		pTable->get_nCols(&lCols);

	if(!bInit || !lCols)
	{
		pTable->Release();

		if(!bInit)
			return RaiseWMIException(lErrorCode);

		return MakeException(exInvalidParameter);
	}

	while(lErrorCode == WBEM_S_NO_ERROR)
	{
		if(!pTable->AddRow(pClassObject, lErrorCode))
		{
			pTable->Release();
			return RaiseWMIException(lErrorCode);
		}

		pClassObject.Release();
		lErrorCode = pEnumObject->Next(WBEM_INFINITE, uCount, &pClassObject, &uReturned);
	}

	if(lErrorCode == WBEM_S_FALSE) // Finished because no more elements left;
		*ppValue = CComPtr<IPSLTable>(pTable);
	else
		pTable->Release(); // Terminated because of an error;

	if(lErrorCode != WBEM_S_FALSE) // Something went wrong there!
		return RaiseWMIException(lErrorCode);

	PSL_END
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer (Senior) Sibedge IT
Ireland Ireland
My online CV: cv.vitalytomilov.com

Comments and Discussions