Click here to Skip to main content
15,891,136 members
Articles / Programming Languages / C++

Shell extension to make executables as services

Rate me:
Please Sign up or sign in to vote.
3.22/5 (6 votes)
8 Apr 2000 89.3K   1.9K   41  
A 'not-so-simple' shell extension allowing executable to be run as services
#include "SvcEx.h"
#include "XHdr.h"
#include "SvcUtils.h"
#include "resource.h"
#include "DlgService.h"
#include "DlgConflict.h"
#include "Structs.h"
#include "_Error.h"

#include <winsvc.h>
#include <tchar.h>

extern ULONG		g_cDllRef;
extern HINSTANCE	g_hDll;
extern HMENU		g_hSubMenu;
extern DWORD		g_dwLastError;
extern TCHAR		g_lpcszApplicationName[];

TCHAR pszServiceName[_MAX_PATH + 1];

ISvcExClassFactory::ISvcExClassFactory()
	: m_cRef(0L)
{
	InterlockedIncrement((long *)&g_cDllRef);	
}
																
ISvcExClassFactory::~ISvcExClassFactory()				
{
	InterlockedDecrement((long *)&g_cDllRef);
}

STDMETHODIMP 
ISvcExClassFactory::QueryInterface(REFIID riid, LPVOID FAR *ppvObject)
{
	HRESULT hr = E_NOINTERFACE;
    *ppvObject = NULL;

    if(IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IClassFactory))
    {
        *ppvObject = (LPISVCEXCLASSFACTORY)this;
        AddRef();

        hr = NOERROR;
    }

    return hr;
}	

STDMETHODIMP_(ULONG) 
ISvcExClassFactory::AddRef()
{
	InterlockedIncrement((long *)&m_cRef);
    return m_cRef;
}

STDMETHODIMP_(ULONG) 
ISvcExClassFactory::Release()
{
	InterlockedDecrement((long *)&m_cRef);

    if(m_cRef == 0L)
        delete this;
    return 0L;
}

STDMETHODIMP 
ISvcExClassFactory::CreateInstance(LPUNKNOWN pUnkOuter, REFIID riid, LPVOID *ppvObject)
{
	HRESULT hr = S_OK;
	LPISVCEXCSHELLEXT pShellExt = (LPISVCEXCSHELLEXT)NULL;

    *ppvObject = NULL;

    if(pUnkOuter != (LPUNKNOWN)NULL)
    	hr = CLASS_E_NOAGGREGATION;
	else
	{
		pShellExt = new ISvcExShellExt;
		if(pShellExt == (LPISVCEXCSHELLEXT)NULL)
    		hr = E_OUTOFMEMORY;
	}

	if(hr == S_OK)
		hr = pShellExt->QueryInterface(riid, ppvObject);
	return hr;
}

STDMETHODIMP 
ISvcExClassFactory::LockServer(BOOL fLock)
{
	UNREFERENCED_PARAMETER(fLock);
    return NOERROR;
}

ISvcExShellExt::ISvcExShellExt()
	: 
		m_xFileCount(0),
		m_ppszFileUserClickedOn(0),
		m_cRef(0L),
		m_pDataObj(NULL)
{
    InterlockedIncrement((long *)&g_cDllRef);
}

ISvcExShellExt::~ISvcExShellExt()
{
	if(m_pDataObj)
		m_pDataObj->Release();

	DeleteFileData();

	InterlockedDecrement((long *)&g_cDllRef);
}

void
ISvcExShellExt::DeleteFileData()
{
	if(m_xFileCount > 0)
	{
		for(register UINT x = 0; x < m_xFileCount; x++)
		{
			delete[] m_ppszFileUserClickedOn[x];
		}
		delete m_ppszFileUserClickedOn;
	}
}

STDMETHODIMP 
ISvcExShellExt::QueryInterface(REFIID riid, LPVOID FAR *ppv)
{
    *ppv = NULL;
    if (IsEqualIID(riid, IID_IShellExtInit) || IsEqualIID(riid, IID_IUnknown))
    	*ppv = (LPSHELLEXTINIT)this;
    else if (IsEqualIID(riid, IID_IContextMenu))
        *ppv = (LPCONTEXTMENU)this;

    if(*ppv)
    {
        AddRef();
        return NOERROR;
    }

	return E_NOINTERFACE;
}

STDMETHODIMP_(ULONG) 
ISvcExShellExt::AddRef()
{
	InterlockedIncrement((long *)&m_cRef);
    return m_cRef;
}

STDMETHODIMP_(ULONG) 
ISvcExShellExt::Release()
{
	InterlockedDecrement((long *)&m_cRef);
    if(m_cRef == 0L)
	    delete this;
	return m_cRef;
}

STDMETHODIMP 
ISvcExShellExt::Initialize(LPCITEMIDLIST pIDFolder, LPDATAOBJECT pDataObj, HKEY hRegKey)
{
	UNREFERENCED_PARAMETER(hRegKey);
	UNREFERENCED_PARAMETER(pIDFolder);

	if(m_pDataObj)
	{
		m_pDataObj->Release();
		m_pDataObj = 0;
	}
    if(pDataObj)
    {
    	m_pDataObj = pDataObj;
    	pDataObj->AddRef();
    }
    return NOERROR;
}

STDMETHODIMP 
ISvcExShellExt::QueryContextMenu(HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
	UNREFERENCED_PARAMETER(idCmdLast);
    UINT idCmd = idCmdFirst;

	pszServiceName[0] = _T('\0');

	GetFileNames();
	if(m_xFileCount != 1)
		return NOERROR;

	char *szMenuText_Popup		= "&Service...";
    char *szMenuText_Install	= "&Install";
	char *szMenuText_Open		= "&Open";
	char *szMenuText_Uninstall	= "&Uninstall";
	char *szMenuText_About		= "&About...";

	BOOL bAppendItems = TRUE;

	if((uFlags & 0x000F) == CMF_NORMAL)
		bAppendItems = TRUE;
	else if (uFlags & CMF_VERBSONLY)
		bAppendItems = TRUE;
	else if (uFlags & CMF_EXPLORE)
		bAppendItems = TRUE;
	else
		bAppendItems = FALSE;

    if(bAppendItems)
    {
        InsertMenu(hMenu, indexMenu, MF_SEPARATOR | MF_BYPOSITION, 0, NULL);  
		indexMenu++;

		g_hSubMenu = CreateMenu();
		if(g_hSubMenu)
		{
			//	this exe is a service?
			LPTSTR ppSvcNames	= NULL;
			int		 nIndex		= -1;
			BOOL fIsSvc = GetServicesByPath(&ppSvcNames, &nIndex);
			if(nIndex >= 0)
			{
				delete ppSvcNames;
			}

			InsertMenu(g_hSubMenu, 0, MF_STRING		| MF_BYPOSITION, idCmd++, szMenuText_Install);
			InsertMenu(g_hSubMenu, 1, MF_STRING		| MF_BYPOSITION, idCmd++, szMenuText_Open);
			InsertMenu(g_hSubMenu, 2, MF_STRING		| MF_BYPOSITION, idCmd++, szMenuText_Uninstall);
			InsertMenu(g_hSubMenu, 3, MF_SEPARATOR	| MF_BYPOSITION, 0		, NULL);
			InsertMenu(g_hSubMenu, 4, MF_STRING		| MF_BYPOSITION, idCmd++, szMenuText_About);
			
			EnableMenuItem(g_hSubMenu, 0, MF_BYPOSITION | (fIsSvc ? MF_GRAYED : MF_ENABLED));
			EnableMenuItem(g_hSubMenu, 1, MF_BYPOSITION | (fIsSvc ? MF_ENABLED : MF_GRAYED));
			EnableMenuItem(g_hSubMenu, 2, MF_BYPOSITION | (fIsSvc ? MF_ENABLED : MF_GRAYED));
		}
		
		InsertMenu(hMenu, indexMenu, MF_STRING | MF_POPUP | MF_BYPOSITION, (UINT_PTR)g_hSubMenu, szMenuText_Popup);
		indexMenu++;

        InsertMenu(hMenu, indexMenu, MF_SEPARATOR | MF_BYPOSITION, 0, NULL);
		indexMenu++;

        return ResultFromShort(idCmd - idCmdFirst);
   }

   return NOERROR;
}

BOOL					
ISvcExShellExt::GetServicesByPath(LPTSTR *ppSvcNames, int *pnIndex)
{
	SC_HANDLE				schSCM				= 0;
	SC_HANDLE				schService			= 0;
	DWORD					 dwResumeHandle		= 0;
	BOOL					  bEnumRetVal		= FALSE;
	LPENUM_SERVICE_STATUS    lpSvc				= 0;
	DWORD					 dwBytesNeeded		= 0;
	DWORD					 dwBufferSize		= 0;
	DWORD					 dwServicesReturned	= 0;
	TCHAR				   lpszServiceName[_MAX_PATH + 1];

	BOOL					  fIsSvc			= FALSE;

	//	Open SCM of remote (or local) machine. All access or die.
	schSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if(schSCM)
	{
		dwResumeHandle = 0;

#pragma warning(disable:4127)
		do
		{
		_retry_:
			bEnumRetVal = 
				EnumServicesStatus
					(
						schSCM, 
						SERVICE_WIN32, 
						SERVICE_STATE_ALL, 
						lpSvc, 
						dwBufferSize, 
						&dwBytesNeeded, 
						&dwServicesReturned, 
						&dwResumeHandle
					);
			if(!bEnumRetVal)
			{
				g_dwLastError = GetLastError();
				if(g_dwLastError == ERROR_MORE_DATA)
				{
					#pragma warning(disable:4127)
					if(lpSvc != 0)
					{
						HeapFree(GetProcessHeap(), 0, lpSvc);
						lpSvc = 0;
					}

					lpSvc = (LPENUM_SERVICE_STATUS)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytesNeeded);
					if(lpSvc)
					{
						dwBufferSize = dwBytesNeeded;
						lpSvc->lpServiceName = lpszServiceName;

						goto _retry_;
					}
					else
						goto _cleanup_;
				}
				else if(g_dwLastError == ERROR_NO_MORE_ITEMS)
					break;
				else
					goto _cleanup_;
			}
			else
			{
				// insert all entries in list
				register DWORD x = 0;
				for(x = 0; x < dwServicesReturned; x++)
				{
					schService = OpenService(schSCM, lpSvc[x].lpServiceName, SERVICE_ALL_ACCESS);
					if(schService)
					{
						QUERY_SERVICE_CONFIG *pqsc;
						DWORD _dwFalseSize = 0;
						DWORD _dwBytesNeeded = 0;
						char lpszBPN[_MAX_PATH + 1];
						char lpszDep[_MAX_PATH + 1];
						char lpszDN[_MAX_PATH + 1];
						char lpszLOG[_MAX_PATH + 1];
						char lpszSSN[_MAX_PATH + 1];

						_dwFalseSize  = sizeof(QUERY_SERVICE_CONFIG);
						_dwFalseSize += 5 * (_MAX_PATH + 1);

						pqsc = (LPQUERY_SERVICE_CONFIG)HeapAlloc(GetProcessHeap(), 
							HEAP_ZERO_MEMORY, _dwFalseSize);

						pqsc->lpBinaryPathName		= lpszBPN;
						pqsc->lpDependencies		= lpszDep;
						pqsc->lpDisplayName			= lpszDN;
						pqsc->lpLoadOrderGroup		= lpszLOG;
						pqsc->lpServiceStartName	= lpszSSN;

						BOOL bQSC = QueryServiceConfig(schService, pqsc, _dwFalseSize, &_dwBytesNeeded);

						#pragma warning(disable:4127)
						HeapFree(GetProcessHeap(), 0, pqsc);

						BOOL _fIsSvc = bQSC ?
							(_tcsicmp(m_ppszFileUserClickedOn[0], pqsc->lpBinaryPathName) == 0 ? TRUE : FALSE) : FALSE;
						
						if(!fIsSvc)
						{
							if(_fIsSvc)
								fIsSvc = TRUE;
						}

						CloseServiceHandle(schService);
						schService = 0;

						if(_fIsSvc)
						{
							if(*ppSvcNames == (LPTSTR)NULL)
								*ppSvcNames = (TCHAR *)malloc((2 + _tcslen(lpSvc[x].lpServiceName)) * sizeof(TCHAR));
							else
							{
								int nOldLen = _tcslen(*ppSvcNames);
								TCHAR *sz = (TCHAR *)malloc((1 + nOldLen) * sizeof(TCHAR));
								_tcscpy(sz, *ppSvcNames);

								*ppSvcNames = (TCHAR *)realloc(*ppSvcNames,
									(2 + _tcslen(lpSvc[x].lpServiceName) + nOldLen) * sizeof(TCHAR));
								_tcscpy(*ppSvcNames, sz);
								free(sz);
							}

							(*pnIndex)++;

							if(*pnIndex == 0)
								_tcscpy(*ppSvcNames, lpSvc[x].lpServiceName);
							else
								_tcscat(*ppSvcNames, lpSvc[x].lpServiceName);
							_tcscat(*ppSvcNames, _T("#"));
						}
					}
				}
			}

			if(dwResumeHandle == 0)
				break;

		} while(TRUE);

_cleanup_:
		#pragma warning(disable:4127)
		if(lpSvc != 0)
			HeapFree(GetProcessHeap(), 0, lpSvc);

		CloseServiceHandle(schSCM);
	}

	return fIsSvc;
}

STDMETHODIMP 
ISvcExShellExt::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{
	HRESULT hr = E_INVALIDARG;
	if (!HIWORD(lpcmi->lpVerb))
    {
        UINT idCmd = LOWORD(lpcmi->lpVerb);
        switch (idCmd)
        {
            case 0:
				hr = Install(lpcmi->hwnd, lpcmi->lpDirectory, lpcmi->lpVerb, lpcmi->lpParameters, lpcmi->nShow);
                break;
            case 1:
				hr = Open(lpcmi->hwnd, lpcmi->lpDirectory, lpcmi->lpVerb, lpcmi->lpParameters, lpcmi->nShow);
                break;
            case 2:
				hr = Uninstall(lpcmi->hwnd, lpcmi->lpDirectory, lpcmi->lpVerb, lpcmi->lpParameters, lpcmi->nShow);
                break;
            case 3:
				hr = About(lpcmi->hwnd, lpcmi->lpDirectory, lpcmi->lpVerb, lpcmi->lpParameters, lpcmi->nShow);
                break;
			default:
                break;
        }
    }
    return hr;
}

STDMETHODIMP 
ISvcExShellExt::GetCommandString(UINT idCmd, UINT uFlags, UINT FAR *reserved, LPSTR pszName, UINT cchMax)
{
	UNREFERENCED_PARAMETER(reserved);
	UNREFERENCED_PARAMETER(uFlags);

	*pszName = 0;

	cchMax   = 40;
	char psz[40];

    switch (idCmd)
    {
        case 0:
			LoadString(g_hDll, IDCMD_NEW, psz, cchMax);
            break;
        case 1:
			LoadString(g_hDll, IDCMD_OPEN, psz, cchMax);
            break;
        case 2:
			LoadString(g_hDll, IDCMD_DELETE, psz, cchMax);
            break;
        case 3:
			LoadString(g_hDll, IDCMD_ABOUT, psz, cchMax);
            break;
		default:
			break;
    }

	wcscpy((unsigned short *)pszName, _WCSTR(psz));
    return NOERROR;
}

STDMETHODIMP 
ISvcExShellExt::Install(HWND hParent, LPCSTR pszWorkingDir, LPCSTR pszCmd, LPCSTR pszParam, int iShowCmd)
{
	UNREFERENCED_PARAMETER(pszWorkingDir);
	UNREFERENCED_PARAMETER(pszCmd);
	UNREFERENCED_PARAMETER(pszParam);
	UNREFERENCED_PARAMETER(iShowCmd);

	LPSERVICEDESC lpSD = (LPSERVICEDESC)malloc(sizeof(SERVICEDESC));
	lpSD->pszExeName		= (TCHAR *)malloc((_MAX_PATH + 1) * sizeof(TCHAR));
	lpSD->pszServiceName	= NULL;

	_tcscpy(lpSD->pszExeName, m_ppszFileUserClickedOn[0]);

	DialogBoxParam(g_hDll, MAKEINTRESOURCE(IDD_SERVICE), hParent, ManageService_DlgProc, (LPARAM)lpSD);

	return S_OK;
}

STDMETHODIMP 
ISvcExShellExt::Open(HWND hParent, LPCSTR pszWorkingDir, LPCSTR pszCmd, LPCSTR pszParam, int iShowCmd)
{
	UNREFERENCED_PARAMETER(pszWorkingDir);
	UNREFERENCED_PARAMETER(pszCmd);
	UNREFERENCED_PARAMETER(pszParam);
	UNREFERENCED_PARAMETER(iShowCmd);

	LPTSTR ppSvcNames	= NULL;
	int		 nIndex		= -1;
	LPLPTSTR *ppS		= NULL;

	BOOL fIsSvc = GetServicesByPath(&ppSvcNames, &nIndex);
	if(fIsSvc)
	{
		ppS = (LPLPTSTR *)malloc(sizeof(LPLPTSTR));
		ppS->ppStr		= ppSvcNames;
		ppS->nHowMany	= nIndex;
		_tcscpy(ppS->szExeName, m_ppszFileUserClickedOn[0]);
	}
	else
		return S_OK;

	if(nIndex > 0) // more than 1
	{
		int nRetVal = DialogBoxParam(g_hDll, MAKEINTRESOURCE(IDD_CONFLICT), hParent, ConflictDlg_Proc, (LPARAM)ppS);
		if(nRetVal == IDCANCEL)
		{
			free(ppS->ppStr);
			free(ppS);

			return S_OK;
		}
	}
	else
	{
		TCHAR *szSvcName = _tcstok(ppSvcNames, _T("#"));
		_tcscpy(ppS->szServiceName, szSvcName);
	}

	LPSERVICEDESC lpSD = (LPSERVICEDESC)malloc(sizeof(SERVICEDESC));
	lpSD->pszExeName		= (TCHAR *)malloc((_MAX_PATH + 1) * sizeof(TCHAR));
	lpSD->pszServiceName	= (TCHAR *)malloc((_MAX_PATH + 1) * sizeof(TCHAR));

	_tcscpy(lpSD->pszExeName, m_ppszFileUserClickedOn[0]);
	_tcscpy(lpSD->pszServiceName, ppS->szServiceName);

	free(ppS->ppStr);
	free(ppS);

	DialogBoxParam(g_hDll, MAKEINTRESOURCE(IDD_SERVICE), hParent, ManageService_DlgProc, (LPARAM)lpSD);

	return S_OK;
}

STDMETHODIMP 
ISvcExShellExt::Uninstall(HWND hParent, LPCSTR pszWorkingDir, LPCSTR pszCmd, LPCSTR pszParam, int iShowCmd)
{
	UNREFERENCED_PARAMETER(pszWorkingDir);
	UNREFERENCED_PARAMETER(pszCmd);
	UNREFERENCED_PARAMETER(pszParam);
	UNREFERENCED_PARAMETER(iShowCmd);

	LPTSTR ppSvcNames	= NULL;
	int		 nIndex		= -1;
	LPLPTSTR *ppS		= NULL;

	BOOL fIsSvc = GetServicesByPath(&ppSvcNames, &nIndex);
	if(fIsSvc)
	{
		ppS = (LPLPTSTR *)malloc(sizeof(LPLPTSTR));
		ppS->ppStr		= ppSvcNames;
		ppS->nHowMany	= nIndex;
		_tcscpy(ppS->szExeName, m_ppszFileUserClickedOn[0]);
	}
	else
		return S_OK;

	if(nIndex > 0) // more than 1
	{
		int nRetVal = DialogBoxParam(g_hDll, MAKEINTRESOURCE(IDD_CONFLICT), hParent, ConflictDlg_Proc, (LPARAM)ppS);
		if(nRetVal == IDCANCEL)
		{
			free(ppS->ppStr);
			free(ppS);

			return S_OK;
		}
	}
	else
	{
		TCHAR *szSvcName = _tcstok(ppSvcNames, _T("#"));
		_tcscpy(ppS->szServiceName, szSvcName);
	}

	TCHAR pszServiceName[_MAX_PATH + 1];
	_tcscpy(pszServiceName, ppS->szServiceName);

	free(ppS->ppStr);
	free(ppS);

	//	Delete service.
	{
		TCHAR lpszMsg[512], lpszFmt[256];

		LoadString(g_hDll, IDS_DELETESERVICE, lpszFmt, 256);
		_stprintf(lpszMsg, lpszFmt, pszServiceName);
		if(MessageBox(hParent, lpszMsg, g_lpcszApplicationName, MB_YESNO | MB_ICONQUESTION) == IDNO)
			return S_OK;

		SC_HANDLE schSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
		if(schSCM)
		{
			SC_HANDLE schService = OpenService(schSCM, pszServiceName, DELETE);
			if(schService)
			{
				if(DeleteService(schService))
				{
					LoadString(g_hDll, IDS_SERVICEDELETED, lpszFmt, 256);
					_stprintf(lpszMsg, lpszFmt, pszServiceName);

					MessageBox(hParent, lpszMsg, g_lpcszApplicationName, MB_OK | MB_ICONINFORMATION);
				}
				else
					ReportLastError(NULL, g_lpcszApplicationName, TRUE, 0);

				CloseServiceHandle(schService);
			}
			else
				ReportLastError(NULL, NULL, TRUE, 0);

			CloseServiceHandle(schSCM);
		}
		else
			ReportLastError(NULL, NULL, TRUE, 0);
	}

	return S_OK;
}

STDMETHODIMP 
ISvcExShellExt::About(HWND hParent, LPCSTR pszWorkingDir, LPCSTR pszCmd, LPCSTR pszParam, int iShowCmd)
{
	UNREFERENCED_PARAMETER(iShowCmd);
	UNREFERENCED_PARAMETER(pszParam);
	UNREFERENCED_PARAMETER(pszCmd);
	UNREFERENCED_PARAMETER(pszWorkingDir);

	char szAboutText[1024];

	LoadString(g_hDll, IDS_ABOUT, szAboutText, 1024);
	MessageBox(hParent, szAboutText, TEXT("Windows Explorer"), MB_OK | MB_ICONINFORMATION);
	return NOERROR;
}

STDMETHODIMP 
ISvcExShellExt::GetFileNames()
{
	HRESULT hr = S_FALSE;

	IEnumFORMATETC *pefEtc = 0;
	hr = m_pDataObj->EnumFormatEtc(DATADIR_GET, &pefEtc);
	if(SUCCEEDED(hr))
	{
		hr = pefEtc->Reset();
		if(SUCCEEDED(hr))
		{
			FORMATETC fEtc;
			ULONG ulFetched = 0L;
			while(TRUE)
			{
				hr = pefEtc->Next(1, &fEtc, &ulFetched);
				if(FAILED(hr) || (ulFetched <= 0))
					break;

				fEtc.cfFormat	= CF_HDROP;
				fEtc.dwAspect	= DVASPECT_CONTENT;
				fEtc.lindex		= -1;
				fEtc.ptd		= NULL;
				fEtc.tymed		= TYMED_HGLOBAL;

				STGMEDIUM stgM;
				hr = m_pDataObj->GetData(&fEtc, &stgM);
				if(SUCCEEDED(hr))
				{
					if(stgM.tymed == TYMED_HGLOBAL)
					{
						UINT nFileCount = DragQueryFile((HDROP)stgM.hGlobal, (UINT)INVALID_HANDLE_VALUE, NULL, 0);
						if(nFileCount >= 1)
						{
							if(m_ppszFileUserClickedOn)
							{
								DeleteFileData();
							}

							m_ppszFileUserClickedOn = new LPTSTR[nFileCount];

							for(register UINT x = 0; x < nFileCount; x++)
							{
								m_ppszFileUserClickedOn[x] = new TCHAR[_MAX_PATH + 1];

								DragQueryFile((HDROP)stgM.hGlobal, x, m_ppszFileUserClickedOn[x], _MAX_PATH + 1);
							}

							m_xFileCount = nFileCount;
						}
					}
				}
			}
		}
	}

	if(pefEtc)
		pefEtc->Release();
	return hr;
}

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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Romania Romania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions