//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//
//=============================================================================//
// vmpi_service.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "vmpi.h"
#include "iphelpers.h"
#include "bitbuf.h"
#include "tier1/strtools.h"
#include "interface.h"
#include "ilaunchabledll.h"
#include "resource.h"
#include "consolewnd.h"
#include <io.h>
#include "utllinkedlist.h"
#include "service_helpers.h"
#include "vmpi_filesystem.h"
#include "service_conn_mgr.h"
#include "resource.h"
#include "perf_counters.h"
#include "tier0/icommandline.h"


// If we couldn't get into a job (maybe they weren't accepting more workers at the time),
// then we wait this long and retry the connection.
#define JOB_MEMORY_DURATION	60


char g_VersionString[64]; // From the IDS_VERSION_STRING string.
HKEY g_hVMPIServiceKey = NULL;	// HKML/Software/Valve/VMPI

double g_flLastKillProcessTime = 0;

char *g_pPassword = NULL;	// Set if this service is using a pw.
ISocket *g_pSocket = NULL;
int g_SocketPort = -1;		// Which port we were able to bind the port on.

char g_RunningProcess_ExeName[MAX_PATH] = {0};
char g_RunningProcess_MapName[MAX_PATH] = {0};
HANDLE g_hRunningProcess = NULL;
HANDLE g_hRunningThread = NULL;
DWORD g_dwRunningProcessId = 0;
IPerfTracker *g_pPerfTracker = NULL;	// Tracks CPU usage.

// When this is true, it will launch new processes invisibly.
bool g_bHideNewProcessWindows = true;

HINSTANCE g_hInstance = NULL;
int g_iBoundPort = -1;

bool g_bScreensaverMode = false;	// If this is true, then it'll act like the service is disabled while
									// a screensaver isn't running.

// If this is on, it runs the exes out of c:/hl2/bin instead of the network. If the exes are built in debug,
// this makes it possible to catch nasty crashes.
bool g_bSuperDebugMode = false;


bool g_bScreensaverRunning = false;	// Updated each frame to tell if the screensaver is running.


// GetTickCount() at the time the app was started.
DWORD g_AppStartTime = 0;

// GetTickCount() at the time the service ran a worker app.
DWORD g_CreateProcessTime = 0;


CIPAddr g_CurRespondAddr;
int g_CurJobID[4];
int g_CurJobPriority = -1;	// VMPI priority of the currently-running job.

// The directory we're running in.
char g_BaseAppPath[MAX_PATH];
char g_FileCachePath[MAX_PATH];	// [base app path]\vmpi_service_cache.


// Different modes this app can run in.
#define RUNMODE_INSTALL		0
#define RUNMODE_CONSOLE		1
#define RUNMODE_SERVICE		2

int g_RunMode = RUNMODE_CONSOLE;

bool g_bMinimized = false;				// true if they run with -minimized.
int g_iCurState = VMPI_SERVICE_STATE_IDLE;
char g_CurMasterName[512] = {0};



//////
// This block of variables is setup while we wait for the downloader to finish.
// When the downloading is complete, we launch the app using these variables.
//////

#ifdef _DEBUG
	#define MAX_DOWNLOADER_TIME_ALLOWED	300000	// If the downloader takes longer than this, kill it.
#else
	#define MAX_DOWNLOADER_TIME_ALLOWED	30	// If the downloader takes longer than this, kill it.
#endif

// If this is non-NULL, then there is NOT a VMPI worker app running currently.. the downloader
HANDLE g_Waiting_hProcess = NULL; 
float g_Waiting_StartTime = 0;
CUtlVector<char*> g_Waiting_Argv;
int g_Waiting_Priority = 0;
bool g_Waiting_bShowAppWindow = false;
bool g_Waiting_bPatching = 0;	// If this is nonzero, then we're downloading so we can apply a patch.


// Used to track the services browsers that have been talking to us lately.
#define SERVICES_BROWSER_TIMEOUT	10  // We remove a services browser from the list if we don't hear from it for this long.
class CServicesBrowserInfo
{
public:
	float	m_flLastPingTime;	// Last time they talked to us.
	CIPAddr	m_Addr;				// Their IP address.
};
CUtlVector<CServicesBrowserInfo> g_ServicesBrowsers;


void HandlePacket_KILL_PROCESS( const CIPAddr *ipFrom );
void KillRunningProcess( const char *pReason, bool bGoToIdle );
void LoadStateFromRegistry();
void SaveStateToRegistry();


void SetPassword( const char *pPassword )
{
	delete [] g_pPassword;
	if ( pPassword )
	{
		int len = V_strlen( pPassword ) + 1;
		g_pPassword = new char[len];
		V_strncpy( g_pPassword, pPassword, len );
	}
	else
	{
		g_pPassword = NULL;
	}
}


// ------------------------------------------------------------------------------------------ //
// This handles connection to clients.
// ------------------------------------------------------------------------------------------ //

class CVMPIServiceConnMgr : public CServiceConnMgr
{
public:

	virtual void	OnNewConnection( int id );
	virtual void	HandlePacket( const char *pData, int len );
	void			SendCurStateTo( int id );


public:

	void	AddConsoleOutput( const char *pMsg );

	void	SetAppState( int iState );
};


void CVMPIServiceConnMgr::AddConsoleOutput( const char *pMsg )
{
	// Tell clients of the new text string.
	CUtlVector<char> data;
	data.AddToTail( VMPI_SERVICE_UI_PROTOCOL_VERSION );
	data.AddToTail( VMPI_SERVICE_TO_UI_CONSOLE_TEXT );
	data.AddMultipleToTail( strlen( pMsg ) + 1, pMsg );
	SendPacket( -1, data.Base(), data.Count() );
}


void CVMPIServiceConnMgr::SetAppState( int iState )
{
	// Update our state and send it to the clients.
	g_iCurState = iState;
	SendCurStateTo( -1 );
}


void CVMPIServiceConnMgr::OnNewConnection( int id )
{
	// Send our current state to the new connection.
	Msg( "(debug) Made a new connection!\n" );
	SendCurStateTo( id );
}


void CVMPIServiceConnMgr::SendCurStateTo( int id )
{
	CUtlVector<char> data;
	data.AddToTail( VMPI_SERVICE_UI_PROTOCOL_VERSION );
	data.AddToTail( VMPI_SERVICE_TO_UI_STATE );
	data.AddMultipleToTail( sizeof( g_iCurState ), (char*)&g_iCurState );
	data.AddToTail( (char)g_bScreensaverMode );
	
	if ( g_pPassword )
		data.AddMultipleToTail( strlen( g_pPassword ) + 1, g_pPassword );
	else
		data.AddToTail( 0 );

	SendPacket( -1, data.Base(), data.Count() );
}


void CVMPIServiceConnMgr::HandlePacket( const char *pData, int len )
{
	switch( pData[0] )
	{
		case VMPI_KILL_PROCESS:
		{
			HandlePacket_KILL_PROCESS( NULL );
		}
		break;
		
		case VMPI_SERVICE_DISABLE:
		{
			KillRunningProcess( "Got a VMPI_SERVICE_DISABLE packet", true );
			SetAppState( VMPI_SERVICE_STATE_DISABLED );
			SaveStateToRegistry();
		}
		break;

		case VMPI_SERVICE_ENABLE:
		{
			if ( g_iCurState == VMPI_SERVICE_STATE_DISABLED )
			{
				SetAppState( VMPI_SERVICE_STATE_IDLE );
			}
			SaveStateToRegistry();
		}
		break;

		case VMPI_SERVICE_UPDATE_PASSWORD:
		{
			const char *pStr = pData + 1;
			SetPassword( pStr );
			
			// Send out the new state.
			SendCurStateTo( -1 );
		}
		break;

		case VMPI_SERVICE_SCREENSAVER_MODE:
		{
			g_bScreensaverMode = (pData[1] != 0);
			SendCurStateTo( -1 );
			SaveStateToRegistry();
		}
		break;

		case VMPI_SERVICE_EXIT:
		{
			Msg( "Got a VMPI_SERVICE_EXIT packet.\n ");
			ServiceHelpers_ExitEarly();
		}
		break;
	}
}

// This is allocated by the service thread and only used in there.
CVMPIServiceConnMgr *g_pConnMgr = NULL;


// ------------------------------------------------------------------------------------------ //
// Persistent state stuff.
// ------------------------------------------------------------------------------------------ //

void LoadStateFromRegistry()
{
	if ( g_hVMPIServiceKey )
	{
		DWORD val = 0;
		DWORD type = REG_DWORD;
		DWORD size = sizeof( val );

		if ( RegQueryValueEx( 
			g_hVMPIServiceKey,
			"ScreensaverMode",
			0,
			&type,
			(unsigned char*)&val,
			&size ) == ERROR_SUCCESS && 
			type == REG_DWORD && 
			size == sizeof( val ) )
		{
			g_bScreensaverMode = (val != 0);
		}

		if ( RegQueryValueEx( 
			g_hVMPIServiceKey,
			"Disabled",
			0,
			&type,
			(unsigned char*)&val,
			&size ) == ERROR_SUCCESS && 
			type == REG_DWORD && 
			size == sizeof( val ) &&
			val != 0 )
		{
			if ( g_pConnMgr )
				g_pConnMgr->SetAppState( VMPI_SERVICE_STATE_DISABLED );
		}
	}
}

void SaveStateToRegistry()
{
	if ( g_hVMPIServiceKey )
	{
		DWORD val;
		 
		val = g_bScreensaverMode;
		RegSetValueEx( 
			g_hVMPIServiceKey,
			"ScreensaverMode",
			0,
			REG_DWORD,
			(unsigned char*)&val,
			sizeof( val ) );

		val = (g_iCurState == VMPI_SERVICE_STATE_DISABLED);
		RegSetValueEx( 
			g_hVMPIServiceKey,
			"Disabled",
			0,
			REG_DWORD,
			(unsigned char*)&val,
			sizeof( val ) );
	}
}
 

// ------------------------------------------------------------------------------------------ //
// Helper functions.
// ------------------------------------------------------------------------------------------ //

char* FindArg( int argc, char **argv, const char *pArgName, char *pDefaultValue="" )
{
	for ( int i=0; i < argc; i++ )
	{
		if ( stricmp( argv[i], pArgName ) == 0 )
		{
			if ( (i+1) >= argc )
				return pDefaultValue;
			else
				return argv[i+1];
		}
	}
	return NULL;
}


SpewRetval_t MySpewOutputFunc( SpewType_t spewType, const char *pMsg )
{
	// Put the message in status.txt.
#ifdef VMPI_SERVICE_LOGS
	static FILE *fp = fopen( "c:\\vmpi_service.log", "wt" );
	if ( fp )
	{
		fprintf( fp, "%s", pMsg );
		fflush( fp );
	}
#endif


	// Print it to the console.
	if ( g_pConnMgr )
		g_pConnMgr->AddConsoleOutput( pMsg );
	
	// Output to the debug console.
	OutputDebugString( pMsg );

	if ( spewType == SPEW_ASSERT )
		return SPEW_DEBUGGER;
	else if( spewType == SPEW_ERROR )
		return SPEW_ABORT;
	else
		return SPEW_CONTINUE;
}


char* CopyString( const char *pStr )
{
	int len = V_strlen( pStr ) + 1;
	char *pRet = new char[len];
	V_strncpy( pRet, pStr, len );
	return pRet;
}

void AppendArg( CUtlVector<char*> &newArgv, const char *pIn )
{
	newArgv.AddToTail( CopyString( pIn ) );
}


void SendStartStatus( bool bStatus )
{	
	for ( int i=0; i < 3; i++ )
	{
		char data[4096];
		bf_write dataBuf( data, sizeof( data ) );
		dataBuf.WriteByte( VMPI_PROTOCOL_VERSION );
		dataBuf.WriteByte( VMPI_NOTIFY_START_STATUS );
		dataBuf.WriteBytes( g_CurJobID, sizeof( g_CurJobID ) );
		dataBuf.WriteByte( bStatus );
		g_pSocket->SendTo( &g_CurRespondAddr, data, dataBuf.GetNumBytesWritten() );
		
		Sleep( 50 );
	}
}


void SendEndStatus()
{
	for ( int i=0; i < 3; i++ )
	{
		char data[4096];
		bf_write dataBuf( data, sizeof( data ) );
		dataBuf.WriteByte( VMPI_PROTOCOL_VERSION );
		dataBuf.WriteByte( VMPI_NOTIFY_END_STATUS );
		dataBuf.WriteBytes( g_CurJobID, sizeof( g_CurJobID ) );
		g_pSocket->SendTo( &g_CurRespondAddr, data, dataBuf.GetNumBytesWritten() );
		
		Sleep( 50 );
	}
}


void KillRunningProcess( const char *pReason, bool bGoToIdle )
{
	// Kill the downloader if it's running.
	if ( g_Waiting_hProcess )
	{
		TerminateProcess( g_Waiting_hProcess, 1 );
		CloseHandle( g_Waiting_hProcess );
		g_Waiting_hProcess = NULL;
	}

	if ( !g_hRunningProcess )
		return;

	if ( pReason )
		Msg( pReason );

	SendEndStatus();
	TerminateProcess( g_hRunningProcess, 1 );
	g_RunningProcess_ExeName[0] = 0;
	g_RunningProcess_MapName[0] = 0;

	// Yep. Now we can start a new one.
	CloseHandle( g_hRunningThread );
	g_hRunningThread = NULL;
	
	CloseHandle( g_hRunningProcess );
	g_hRunningProcess = NULL;

	g_CurJobPriority = -1;

	if ( bGoToIdle )
		if ( g_pConnMgr )
			g_pConnMgr->SetAppState( VMPI_SERVICE_STATE_IDLE );
}


// ------------------------------------------------------------------------------------------ //
// Job memory stuff.
// ------------------------------------------------------------------------------------------ //

// CJobMemory is used to track which jobs we ran (or tried to run).
// We remember which jobs we did because Winsock likes to queue up the job packets on
// our socket, so if we don't remember which jobs we ran, we'd run the job a bunch of times.
class CJobMemory
{
public:
	int		m_ID[4];		// Random ID that comes from the server.
	float	m_Time;
};
															  
CUtlLinkedList<CJobMemory, int> g_JobMemories;


bool FindJobMemory( int id[4] )
{
	int iNext;
	for ( int i=g_JobMemories.Head(); i != g_JobMemories.InvalidIndex(); i=iNext )
	{
		iNext = g_JobMemories.Next( i );
		
		CJobMemory *pJob = &g_JobMemories[i];
		if ( memcmp( pJob->m_ID, id, sizeof( pJob->m_ID ) ) == 0 )
			return true;
	}
	return false;
}


void TimeoutJobIDs()
{
	double flCurTime = Plat_FloatTime();
	
	int iNext;
	for ( int i=g_JobMemories.Head(); i != g_JobMemories.InvalidIndex(); i=iNext )
	{
		iNext = g_JobMemories.Next( i );
		
		if ( (flCurTime - g_JobMemories[i].m_Time) > JOB_MEMORY_DURATION )
			g_JobMemories.Remove( i );
	}
}


void AddJobMemory( int id[4] )
{
	TimeoutJobIDs();
	
	CJobMemory job;
	memcpy( job.m_ID, id, sizeof( job.m_ID ) );
	job.m_Time = Plat_FloatTime();
	g_JobMemories.AddToTail( job );
}


bool CheckJobID( bf_read &buf, int jobID[4] )
{
	TimeoutJobIDs();
	
	jobID[0] = buf.ReadLong();
	jobID[1] = buf.ReadLong();
	jobID[2] = buf.ReadLong();
	jobID[3] = buf.ReadLong();
	if ( FindJobMemory( jobID ) || buf.IsOverflowed() )
	{
		return false;
	}
	
	return true;
}




// ------------------------------------------------------------------------------------------ //
// The main VMPI code.
// ------------------------------------------------------------------------------------------ //

void VMPI_Waiter_Term()
{
	KillRunningProcess( NULL, false );
	if ( g_pConnMgr )
	{
		g_pConnMgr->Term();
		delete g_pConnMgr;
		g_pConnMgr = NULL;
	}

	if ( g_pSocket )
	{
		g_pSocket->Release();
		g_pSocket = NULL;
	}
	
	g_pPerfTracker->Release();
	g_pPerfTracker = NULL;
}


bool VMPI_Waiter_Init()
{
	// Run as idle priority.
	HKEY hKey = NULL;
	RegCreateKey( HKEY_LOCAL_MACHINE, VMPI_SERVICE_KEY, &hKey );
	DWORD dwVal = 0;
	DWORD dummyType = REG_DWORD;
	DWORD dwValLen = sizeof( dwVal );
	if ( RegQueryValueEx( hKey, "LowPriority", NULL, &dummyType, (LPBYTE)&dwVal, &dwValLen ) == ERROR_SUCCESS )
	{
		if ( dwVal )
		{
			SetPriorityClass( GetCurrentProcess(), IDLE_PRIORITY_CLASS );
		}
	}
	else
	{
		RegSetValueEx( hKey, "LowPriority", 0, REG_DWORD, (unsigned char*)&dwVal, sizeof( dwVal ) );
	}

	g_pConnMgr = new CVMPIServiceConnMgr;

	if ( !g_pConnMgr->InitServer() )
		Msg( "ERROR INITIALIZING CONNMGR\n" );

	g_pSocket = CreateIPSocket();
	if ( !g_pSocket )
	{
		Msg( "Error creating a socket.\n" );
		return false;
	}
	
	// Bind to the first port we find in the range [VMPI_SERVICE_PORT, VMPI_LAST_SERVICE_PORT].
	int iTest;
	for ( iTest=VMPI_SERVICE_PORT; iTest <= VMPI_LAST_SERVICE_PORT; iTest++ )
	{
		g_SocketPort = iTest;
		if ( g_pSocket->BindToAny( iTest ) )
			break;
	}
	if ( iTest == VMPI_LAST_SERVICE_PORT )
	{	
		Msg( "Error binding a socket to port %d.\n", VMPI_SERVICE_PORT );
		VMPI_Waiter_Term();
		return false;
	}
	
	g_iBoundPort = iTest;
	g_pPerfTracker = CreatePerfTracker();
	return true;
}


void RunInDLL( const char *pFilename, CUtlVector<char*> &newArgv )
{
	if ( g_pConnMgr )						   
		g_pConnMgr->SetAppState( VMPI_SERVICE_STATE_BUSY );

	bool bSuccess = false;
	CSysModule *pModule = Sys_LoadModule( pFilename );
	if ( pModule )
	{
		CreateInterfaceFn fn = Sys_GetFactory( pModule );
		if ( fn )
		{
			ILaunchableDLL *pDLL = (ILaunchableDLL*)fn( LAUNCHABLE_DLL_INTERFACE_VERSION, NULL );
			if( pDLL )
			{
				// Do this here because the executables we would have launched usually would do it.
				CommandLine()->CreateCmdLine( newArgv.Count(), newArgv.Base() );
				pDLL->main( newArgv.Count(), newArgv.Base() );
				bSuccess = true;
				SpewOutputFunc( MySpewOutputFunc );
			}
		}

		Sys_UnloadModule( pModule );
	}
	
	if ( !bSuccess )
	{
		Msg( "Error running VRAD (or VVIS) out of DLL '%s'\n", pFilename );
	}

	if ( g_pConnMgr )
		g_pConnMgr->SetAppState( VMPI_SERVICE_STATE_IDLE );
}


void GetArgsFromBuffer( 
	bf_read &buf, 
	CUtlVector<char*> &newArgv, 
	bool *bShowAppWindow )
{
	int nArgs = buf.ReadWord();

	bool bSpewArgs = false;
	
	for ( int iArg=0; iArg < nArgs; iArg++ )
	{
		char argStr[512];
		buf.ReadString( argStr, sizeof( argStr ) );

		AppendArg( newArgv, argStr );
		if ( stricmp( argStr, "-mpi_verbose" ) == 0 )
			bSpewArgs = true;

		if ( stricmp( argStr, "-mpi_ShowAppWindow" ) == 0 )
			*bShowAppWindow = true;
	}

	if ( bSpewArgs )
	{
		Msg( "nArgs: %d\n", newArgv.Count() );
		for ( int i=0; i < newArgv.Count(); i++ )
			Msg( "Arg %d: %s\n", i, newArgv[i] );
	}
}


bool GetDLLFilename( CUtlVector<char*> &newArgv, char pDLLFilename[MAX_PATH] )
{
	char *argStr = newArgv[0];
	int argLen = strlen( argStr );
	if ( argLen <= 4 )
		return false;

	if ( Q_stricmp( &argStr[argLen-4], ".exe" ) != 0 )
		return false;
		
	char baseFilename[MAX_PATH];
	Q_strncpy( baseFilename, argStr, MAX_PATH );
	baseFilename[ min( MAX_PATH-1, argLen-4 ) ] = 0;
	
	// First try _dll.dll (src_main), then try .dll (rel).
	V_snprintf( pDLLFilename, MAX_PATH, "%s_dll.dll", baseFilename );
	if ( _access( pDLLFilename, 0 ) != 0 )
	{
		V_snprintf( pDLLFilename, MAX_PATH, "%s.dll", baseFilename );
	}

	return true;
}

void BuildCommandLineFromArgs( CUtlVector<char*> &newArgv, char *pOut, int outLen )
{
	pOut[0] = 0;

	for ( int i=0; i < newArgv.Count(); i++ )
	{
		char argStr[512];
		if ( strlen( newArgv[i] ) > 0 && newArgv[i][strlen(newArgv[i])-1] == '\\' )
			Q_snprintf( argStr, sizeof( argStr ), "\"%s\\\" ", newArgv[i] );
		else
			Q_snprintf( argStr, sizeof( argStr ), "\"%s\" ", newArgv[i] );

		Q_strncat( pOut, argStr, outLen, COPY_ALL_CHARACTERS );
	}
}

bool RunProcessFromArgs( CUtlVector<char*> &newArgv, bool bShowAppWindow, bool bCreateSuspended, const char *pWorkingDir, PROCESS_INFORMATION *pOut )
{
	char commandLine[2048];
	BuildCommandLineFromArgs( newArgv, commandLine, sizeof( commandLine ) );

	Msg( "Running '%s'\n", commandLine );

	STARTUPINFO si;
	memset( &si, 0, sizeof( si ) );
	si.cb = sizeof( si );

	memset( pOut, 0, sizeof( *pOut ) );

	DWORD dwFlags = 0;//IDLE_PRIORITY_CLASS;
	if ( bShowAppWindow )
		dwFlags |= CREATE_NEW_CONSOLE;
	else
		dwFlags |= CREATE_NO_WINDOW;

	if ( bCreateSuspended )
		dwFlags |= CREATE_SUSPENDED;

	UINT oldMode = SetErrorMode( SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS );

	BOOL bRet = CreateProcess( 
		NULL, 
		commandLine, 
		NULL,							// security
		NULL,
		TRUE,
		dwFlags | IDLE_PRIORITY_CLASS,	// flags
		NULL,							// environment
		pWorkingDir,	
		&si,
		pOut );
	
	SetErrorMode( oldMode );
	return (bRet != FALSE);
}


void RunProcessAtCommandLine( 
	CUtlVector<char*> &newArgv, 
	bool bShowAppWindow,
	bool bCreateSuspended,
	int iPriority )
{
	// current directory (use c:\\ because we don't want it to accidentally share
	// DLLs like vstdlib with us).	PROCESS_INFORMATION pi;
	PROCESS_INFORMATION pi;
	if ( RunProcessFromArgs( newArgv, bShowAppWindow, bCreateSuspended, g_FileCachePath, &pi ) )
	{
		if ( g_pConnMgr )
			g_pConnMgr->SetAppState( VMPI_SERVICE_STATE_BUSY );

		if ( newArgv.Count() > 0 && newArgv[0] )
		{
			V_FileBase( newArgv[0], g_RunningProcess_ExeName, sizeof( g_RunningProcess_ExeName ) );
			
			if ( V_stricmp( g_RunningProcess_ExeName, "vrad" ) == 0 || V_stricmp( g_RunningProcess_ExeName, "vvis" ) == 0 )
				V_FileBase( newArgv[newArgv.Count()-1], g_RunningProcess_MapName, sizeof( g_RunningProcess_MapName ) );
		}

		g_hRunningProcess = pi.hProcess;
		g_hRunningThread = pi.hThread;
		g_dwRunningProcessId = pi.dwProcessId;
		g_pPerfTracker->Init( g_dwRunningProcessId );
		g_CurJobPriority = iPriority;
		g_CreateProcessTime = GetTickCount();
		
		SendStartStatus( true );
	}
	else
	{
		Msg( " - ERROR in CreateProcess (%s)!\n", GetLastErrorString() );
		SendStartStatus( false );
		g_CurJobPriority = -1;
		g_RunningProcess_ExeName[0] = 0;
		g_RunningProcess_MapName[0] = 0;
	}
}


bool WaitForProcessToExit()
{
	if ( g_hRunningProcess )
	{
		// Did the process complete yet?
		if ( WaitForSingleObject( g_hRunningProcess, 0 ) == WAIT_TIMEOUT )
		{
			// Nope.. keep waiting.
			return true;
		}
		else
		{
			Msg( "Finished!\n ");
			
			SendEndStatus();

			// Change back to the 'waiting' icon.
			if ( g_pConnMgr )
				g_pConnMgr->SetAppState( VMPI_SERVICE_STATE_IDLE );
			g_CurJobPriority = -1;

			// Yep. Now we can start a new one.
			CloseHandle( g_hRunningThread );
			CloseHandle( g_hRunningProcess );
			g_hRunningProcess = g_hRunningThread = NULL;
			g_RunningProcess_ExeName[0] = g_RunningProcess_MapName[0] = 0;
		}
	}

	return false;
}


void HandleWindowMessages()
{
	MSG msg;
	while ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
	{
		TranslateMessage( &msg );
		DispatchMessage( &msg );
	}
}

void GetRunningProcessStats( int &processorPercentage, int &memoryUsageMegabytes )
{
	static int lastProcessorPercentage = 0;
	static int lastMemory = 0;
	if ( g_hRunningProcess )
	{
		// Only update this every couple seconds. It's not too expensive (about 800 microseconds), but we don't
		// need to do it a whole lot.
		static DWORD lastReturnTime = GetTickCount();
		DWORD curTime = GetTickCount();
		if ( (curTime - lastReturnTime) >= 1000 )
		{
			lastReturnTime = curTime;
			g_pPerfTracker->GetPerfData( lastProcessorPercentage, lastMemory );
		}
	}
	else
	{
		lastProcessorPercentage = lastMemory = 0;
	}

	processorPercentage = lastProcessorPercentage;
	memoryUsageMegabytes = lastMemory;
}


void BuildPingHeader( CUtlVector<char> &data, char packetID, int iState )
{
	// Figure out the computer's name.
	char computerName[128];
	DWORD computerNameLen = sizeof( computerName );
	GetComputerName( computerName, &computerNameLen );	

	// Ping back at them.
	data.AddToTail( VMPI_PROTOCOL_VERSION );
	data.AddToTail( packetID );
	data.AddToTail( (char)iState );
	
	DWORD liveTime = GetTickCount() - g_AppStartTime;
	data.AddMultipleToTail( sizeof( liveTime ), (char*)&liveTime );

	data.AddMultipleToTail( sizeof( g_SocketPort ), (char*)&g_SocketPort );
	data.AddMultipleToTail( strlen( computerName ) + 1, computerName );

	if ( g_hRunningProcess )
		data.AddMultipleToTail( strlen( g_CurMasterName ) + 1, g_CurMasterName );
	else
		data.AddMultipleToTail( 1, "" );

	// Write in how long the worker app has been running.
	DWORD appRunTime = 0;
	if ( g_hRunningProcess )
		appRunTime = GetTickCount() - g_CreateProcessTime;

	data.AddMultipleToTail( sizeof( appRunTime ), (char*)&appRunTime );

	// Finally, write the password.
	if ( g_pPassword )
		data.AddMultipleToTail( strlen( g_pPassword ) + 1, g_pPassword );
	else
		data.AddToTail( 0 );

	data.AddMultipleToTail( V_strlen( g_VersionString ) + 1, g_VersionString );

	int processorPercentage, memoryUsageMegabytes;
	GetRunningProcessStats( processorPercentage, memoryUsageMegabytes );

	// Write processor percentage.
	data.AddToTail( (char)processorPercentage );

	// Write the EXE name.
	data.AddMultipleToTail( V_strlen( g_RunningProcess_ExeName ) + 1, g_RunningProcess_ExeName );
							
	// Write memory usage.
	short memUsageShort = (short)memoryUsageMegabytes;
	data.AddMultipleToTail( sizeof( memUsageShort ), (const char*)&memUsageShort );

	// Write the map name.
	data.AddMultipleToTail( V_strlen( g_RunningProcess_MapName ) + 1, g_RunningProcess_MapName );
}


// This tracks a list 
void AddServicesBrowserIP( const CIPAddr &ipFrom )
{
	for ( int i=0; i < g_ServicesBrowsers.Count(); i++ )
	{
		if ( g_ServicesBrowsers[i].m_Addr == ipFrom )
		{
			g_ServicesBrowsers[i].m_flLastPingTime = Plat_FloatTime();
			return;
		}
	}
	CServicesBrowserInfo info;
	info.m_Addr = ipFrom;
	info.m_flLastPingTime = Plat_FloatTime();
	g_ServicesBrowsers.AddToTail( info );
}


void UpdateServicesBrowserIPs()
{
	double curTime = Plat_FloatTime();
	for ( int i=0; i < g_ServicesBrowsers.Count(); i++ )
	{
		if ( (curTime - g_ServicesBrowsers[i].m_flLastPingTime) >= SERVICES_BROWSER_TIMEOUT )
		{
			g_ServicesBrowsers.Remove( i );
			--i;
			break;
		}
	}
}


void SendStateToServicesBrowsers()
{
	int curState;
	if ( g_hRunningProcess )
	{
		if ( g_Waiting_bPatching )
			curState = VMPI_STATE_PATCHING;
		else
			curState = VMPI_STATE_BUSY;
	}
	else if ( g_Waiting_hProcess )
	{
		if ( g_Waiting_bPatching )
			curState = VMPI_STATE_PATCHING;
		else
			curState = VMPI_STATE_DOWNLOADING;
	}
	else if ( g_iCurState == VMPI_SERVICE_STATE_DISABLED )
	{
		curState = VMPI_STATE_DISABLED;
	}
	else if ( g_bScreensaverMode && !g_bScreensaverRunning )
	{
		curState = VMPI_STATE_SCREENSAVER_DISABLED;
	}
	else
	{
		curState = VMPI_STATE_IDLE;
	}

	CUtlVector<char> data;
	BuildPingHeader( data, VMPI_PING_RESPONSE, curState );
	
	for ( int i=0; i < g_ServicesBrowsers.Count(); i++ )
	{
		g_pSocket->SendTo( &g_ServicesBrowsers[i].m_Addr, data.Base(), data.Count() );
	}
}


void StopUI()
{
	char cPacket[2] = {VMPI_SERVICE_UI_PROTOCOL_VERSION, VMPI_SERVICE_TO_UI_EXIT};
	if ( g_pConnMgr )
		g_pConnMgr->SendPacket( -1, &cPacket, sizeof( cPacket ) );

	// Wait for a bit for the connection to go away.
	DWORD startTime = GetTickCount();
	while ( GetTickCount()-startTime < 2000 )
	{
		if ( g_pConnMgr )
		{
			g_pConnMgr->Update();
			if ( !g_pConnMgr->IsConnected() )
				break;
			else
				Sleep( 10 );
		}
	}
}


void CheckScreensaverRunning()
{
	// We want to let patching finish even if we're in screensaver mode.
	if ( g_Waiting_hProcess && g_Waiting_bPatching )
		return;
	
	BOOL bRunning = false;
	SystemParametersInfo( SPI_GETSCREENSAVERRUNNING, 0, &bRunning, 0 );

	g_bScreensaverRunning = (bRunning != 0);
	if ( !g_bScreensaverRunning && g_bScreensaverMode )
	{
		KillRunningProcess( "Screensaver not running", true );
	}
}


void AdjustSuperDebugArgs( CUtlVector<char*> &args )
{
	// Get the directory this exe was run out of.
	char filename[512];
	if ( GetModuleFileName( GetModuleHandle( NULL ), filename, sizeof( filename ) ) == 0 )
		return;
	
	char *pLastSlash = filename;
	char *pCurPos = filename;
	while ( *pCurPos )
	{
		if ( *pCurPos == '/' || *pCurPos == '\\' )
			pLastSlash = pCurPos;
		++pCurPos;
	}
	*pLastSlash = 0;
	
	// In superdebug mode, run it out of c:/hl2/bin.
	const char *pBase = args[0];
	const char *pBaseCur = pBase;
	while ( *pBaseCur )
	{
		if ( *pBaseCur == '/' || *pBaseCur == '\\' || *pBaseCur == ':' )
		{
			pBase = pBaseCur+1;
			pBaseCur = pBase;
		}
		++pBaseCur;
	}
	
	int maxLen = 64 + strlen( pBase ) + 1;
	char *pNewFilename = new char[maxLen];
	_snprintf( pNewFilename, maxLen, "%s\\%s", filename, pBase );
	delete args[0];
	args[0] = pNewFilename;


	// Now insert -allowdebug.
	const char *pAllowDebug = "-allowdebug";
	char *pToInsert = new char[ strlen( pAllowDebug ) + 1 ];
	strcpy( pToInsert, pAllowDebug );
	args.InsertAfter( 0, pToInsert );

}


// -------------------------------------------------------------------------------- //
// Purpose: Launches vmpi_transfer.exe to download the required
// files from the master so we can launch.
//
// If successful, it sets hProcess to the process handle of the downloader.
// When that process terminates, we look for [cache dir]\ReadyToGo.txt and if it's
// there, then we start the job.
// -------------------------------------------------------------------------------- //
bool StartDownloadingAppFiles( 
	CUtlVector<char*> &newArgv, 
	char *cacheDir, 
	int cacheDirLen, 
	bool bShowAppWindow, 
	HANDLE *hProcess, 
	bool bPatching )
{
	*hProcess = NULL;
	
	V_strncpy( cacheDir, g_FileCachePath, cacheDirLen );
	
	// For now, cache dir is always the same. It's [current directory]\cache.
	if ( _access( cacheDir, 0 ) != 0 )
	{
		if ( !CreateDirectory( cacheDir, NULL ) && GetLastError() != ERROR_ALREADY_EXISTS )
		{
			Warning( "Unable to create cache directory: %s.\n", cacheDir );
			return false;
		}
	}

	// Clear all the files in the directory.
	char searchStr[MAX_PATH];
	V_ComposeFileName( cacheDir, "*.*", searchStr, sizeof( searchStr ) );
	_finddata_t findData;
	intptr_t ret = _findfirst( searchStr, &findData );
	if ( ret != -1 )
	{
		do
		{
			if ( findData.name[0] == '.' )
				continue;
			
			char fullFilename[MAX_PATH];
			V_ComposeFileName( cacheDir, findData.name, fullFilename, sizeof( fullFilename ) );
			if ( _unlink( fullFilename ) != 0 )
			{
				Warning( "_unlink( %s ) failed.\n", fullFilename );
				return false;
			}
		} while ( _findnext( ret, &findData ) == 0 );
		
		_findclose( ret );
	}

	// Change the EXE name to an absolute path to exactly where it is in the cache directory.
	int maxExeNameLen = 1024;
	char *pExeName = new char[maxExeNameLen];
	if ( bPatching )
	{
		V_ComposeFileName( cacheDir, "vmpi_service_install.exe", pExeName, maxExeNameLen );
		
		// Add args for the installer.
		newArgv.InsertAfter( 0, CopyString( "-DontTouchUI" ) );

		// When patching, we can't start the UI and the installer can't because we're running in the local system account
		// and the UI is running on the account of whoever logged in. So what we do is send a message to the UI telling it
		// to run <cacheDir>\WaitAndRestart and restart itself in N seconds.
		newArgv.InsertAfter( 0, CopyString( "-Install_Quiet" ) );
	}
	else
	{
		V_ComposeFileName( cacheDir, newArgv[0], pExeName, maxExeNameLen );
	}
		
	delete newArgv[0];
	newArgv[0] = pExeName;

	char fullExeFilename[MAX_PATH];
	V_ComposeFileName( g_BaseAppPath, "vmpi_transfer.exe", fullExeFilename, sizeof( fullExeFilename ) );
	
	CUtlVector<char*> downloaderArgs;
	downloaderArgs.AddToTail( fullExeFilename );
#if defined( _DEBUG )
	downloaderArgs.AddToTail( "-allowdebug" );
#endif
	downloaderArgs.AddToTail( "-CachePath" );		// Tell it where to download the files to.
	downloaderArgs.AddToTail( cacheDir );
	
	// Pass all the -mpi_worker, -mpi_file, -mpi_filebase args into the downloader app.
	for ( int i=1; i < (int)newArgv.Count()-1; i++ )
	{
		if ( V_stricmp( newArgv[i], "-mpi_filebase" ) == 0 || V_stricmp( newArgv[i], "-mpi_file" ) == 0 )
		{
			downloaderArgs.AddToTail( newArgv[i] );
			downloaderArgs.AddToTail( newArgv[i+1] );
			newArgv.Remove( i );
			newArgv.Remove( i );
			--i;
		}
		else if ( V_stricmp( newArgv[i], "-mpi_worker" ) == 0 )
		{
			// We need this arg so it knows what IP to connect to, but we want to leave it in the final launch args too.
			downloaderArgs.AddToTail( newArgv[i] );
			downloaderArgs.AddToTail( newArgv[i+1] );
			++i;
		}
	}	
	
	// Transfer each file.
	PROCESS_INFORMATION pi;
	if ( !RunProcessFromArgs( downloaderArgs, bShowAppWindow, false, g_BaseAppPath, &pi ) )
		return false;
	
	*hProcess = pi.hProcess;
	return true;
}


void SendPatchCommandToUIs( DWORD dwInstallerProcessId )
{
	Msg( "SendPatchCommandToUIs\n ");
	
	CUtlVector<char> data;
	data.AddToTail( VMPI_SERVICE_UI_PROTOCOL_VERSION );
	data.AddToTail( VMPI_SERVICE_TO_UI_PATCHING );

	// This arg tells the UI whether to exit after running the command or not.
	data.AddToTail( 1 );
	
	// First argument is the working directory, which is the cache path in this case.
	data.AddMultipleToTail( V_strlen( g_FileCachePath ) + 1, g_FileCachePath );
	
	// Second argument is the command line.
	char waitAndRestartExe[MAX_PATH], serviceUIExe[MAX_PATH], commandLine[1024 * 8];
	V_ComposeFileName( g_FileCachePath, "WaitAndRestart.exe", waitAndRestartExe, sizeof( waitAndRestartExe ) );
	V_ComposeFileName( g_BaseAppPath, "vmpi_service_ui.exe", serviceUIExe, sizeof( serviceUIExe ) ); // We're running the UI from the same directory this exe is in.
	char strSeconds[64];
	V_snprintf( strSeconds, sizeof( strSeconds ), "*%lu", dwInstallerProcessId );

	// IMPORTANT to use BuildCommandLineFromArgs here because it'll handle slashes and quotes correctly.
	// If we don't do that, the command often won't work.
	CUtlVector<char*> args;
	args.AddToTail( waitAndRestartExe );
	args.AddToTail( strSeconds );
	args.AddToTail( g_BaseAppPath );
	args.AddToTail( serviceUIExe );
	BuildCommandLineFromArgs( args, commandLine, sizeof( commandLine ) );
	data.AddMultipleToTail( V_strlen( commandLine ) + 1, commandLine );
	
	if ( g_pConnMgr )
	{
		g_pConnMgr->SendPacket( -1, data.Base(), data.Count() );
		Sleep( 1000 );	// Make sure this packet goes out.
	}
}


// Returns true if the service was just patched and should exit.
bool CheckDownloaderFinished()
{
	if ( !g_Waiting_hProcess )
		return false;
	
	// Check if the downloader has timed out and kill it if necessary.
	if ( Plat_FloatTime() - g_Waiting_StartTime > MAX_DOWNLOADER_TIME_ALLOWED )
	{
		TerminateProcess( g_Waiting_hProcess, 1 );
		CloseHandle( g_Waiting_hProcess );
		g_Waiting_hProcess = NULL;
		return false;
	}

	// Check if it's done.
	if ( WaitForSingleObject( g_Waiting_hProcess, 0 ) != WAIT_OBJECT_0 )
		return false;

	CloseHandle( g_Waiting_hProcess );
	g_Waiting_hProcess = NULL;

	// Ok, it's done. Did it finish successfully?
	char testFilename[MAX_PATH];
	V_ComposeFileName( g_FileCachePath, "ReadyToGo.txt", testFilename, sizeof( testFilename ) );
	if ( _access( testFilename, 0 ) != 0 )
		return false;
		
	// Ok, the downloader finished successfully. Run the worker app.
	if ( g_bSuperDebugMode )
		AdjustSuperDebugArgs( g_Waiting_Argv );

	// Figure out the name of the master machine.
	V_strncpy( g_CurMasterName, "<unknown>", sizeof( g_CurMasterName ) );
	for ( int iArg=1; iArg < g_Waiting_Argv.Count()-1; iArg++ )
	{
		if ( stricmp( g_Waiting_Argv[iArg], "-mpi_MasterName" ) == 0 )
		{
			Q_strncpy( g_CurMasterName, g_Waiting_Argv[iArg+1], sizeof( g_CurMasterName ) );
		}
	}

	char DLLFilename[MAX_PATH];
	if ( FindArg( __argc, __argv, "-TryDLLMode" ) && 
		g_RunMode == RUNMODE_CONSOLE && 
		GetDLLFilename( g_Waiting_Argv, DLLFilename ) &&
		!g_Waiting_bPatching )
	{
		// This is just a helper for debugging. If it's VRAD, we can run it
		// in-process as a DLL instead of running it as a separate EXE.
		RunInDLL( DLLFilename, g_Waiting_Argv );
	}
	else
	{
		// Run the (hopefully!) MPI app they specified.
		RunProcessAtCommandLine( g_Waiting_Argv, g_Waiting_bShowAppWindow, g_Waiting_bPatching, g_Waiting_Priority );
		
		if ( g_Waiting_bPatching )
		{														
			// Tell any currently-running UI apps to patch themselves and quit ASAP so the installer can finish.
			SendPatchCommandToUIs( g_dwRunningProcessId );

			ResumeThread( g_hRunningThread ); // We started the installer suspended so we could make sure we'd send out the patch command.
			
			// We just ran the installer, but let's forget about it, otherwise we'll kill its process when we exit here.
			CloseHandle( g_hRunningProcess );
			CloseHandle( g_hRunningThread ) ;
			g_hRunningProcess = g_hRunningThread = NULL;
			g_RunningProcess_ExeName[0] = 0;
			g_RunningProcess_MapName[0] = 0;

			ServiceHelpers_ExitEarly();
			return true;
		}
	}
	
	g_Waiting_Argv.PurgeAndDeleteElements();
	return false;
}


void HandlePacket_LOOKING_FOR_WORKERS( bf_read &buf, const CIPAddr &ipFrom )
{
	// If we're downloading files for a job request, don't process any more "looking for workers" packets.
	if ( g_Waiting_hProcess )
		return;
	
	// This will be a nonzero-length string if patching.
	char versionString[512];
	buf.ReadString( versionString, sizeof( versionString ) );

	int iPort = buf.ReadShort();
	int iPriority = buf.ReadShort();

	// Make sure we don't run the same job more than once.
	if ( !CheckJobID( buf, g_CurJobID ) )
		return;

	CUtlVector<char*> newArgv;
	GetArgsFromBuffer( buf, newArgv, &g_Waiting_bShowAppWindow );

	bool bForcePatch = false;
	if ( buf.GetNumBytesLeft() >= 1 )
		bForcePatch = (buf.ReadByte() != 0);

	int iDownloaderPort = iPort;
	if  ( buf.GetNumBytesLeft() >= 2 )
		iDownloaderPort = buf.ReadShort();

	// Add these arguments after the executable filename to tell the program
	// that it's an MPI worker and who to connect to. 
	char strDownloaderIP[128], strMainIP[128];
	V_snprintf( strDownloaderIP, sizeof( strDownloaderIP ), "%d.%d.%d.%d:%d", ipFrom.ip[0], ipFrom.ip[1], ipFrom.ip[2], ipFrom.ip[3], iDownloaderPort );
	V_snprintf( strMainIP, sizeof( strMainIP ), "%d.%d.%d.%d:%d", ipFrom.ip[0], ipFrom.ip[1], ipFrom.ip[2], ipFrom.ip[3], iPort );

	// (-mpi is already on the command line of whoever ran the app).
	// AppendArg( commandLine, sizeof( commandLine ), "-mpi" );
	newArgv.InsertAfter( 0, CopyString( "-mpi_worker" ) );
	newArgv.InsertAfter( 1, CopyString( strDownloaderIP ) );


	// If the version string is set, then this is a patch.
	bool bPatching = false;
	if ( versionString[0] != 0 )
	{
		bPatching = true;
		
		// Check that we haven't applied this patch version yet. This case usually happens right after we've applied a patch
		// and we're restarting. The vmpi_transfer master is still pinging us telling us to patch, but we don't want to
		// reapply this patch.
		if ( atof( versionString ) <= atof( g_VersionString ) && !bForcePatch )
		{
			newArgv.PurgeAndDeleteElements();
			return;
		}
		
		// Ok, it's a new version. Get rid of whatever was running before.
		KillRunningProcess( "Starting a patch..", true );
	}
								 
	// If there's already a job running, only interrupt it if this new one has a higher priority.
	if ( WaitForProcessToExit() )
	{
		if ( iPriority > g_CurJobPriority )
		{
			KillRunningProcess( "Interrupted by a higher priority process", true );
		}
		else
		{
			// This means we're already running a job with equal to or greater priority than
			// the one that has been requested. We're going to ignore this request.
			newArgv.PurgeAndDeleteElements();
			return;
		}
	}

	// Responses go here.
	g_CurRespondAddr = ipFrom;
	
	// Also look for -mpi_ShowAppWindow in the args to the service.
	if ( !g_Waiting_bShowAppWindow && FindArg( __argc, __argv, "-mpi_ShowAppWindow" ) )
		g_Waiting_bShowAppWindow = true;

	// Copy all the files from the master and put them in our cache dir to run with.
	char cacheDir[MAX_PATH];
	if ( StartDownloadingAppFiles( newArgv, cacheDir, sizeof( cacheDir ), g_Waiting_bShowAppWindow, &g_Waiting_hProcess, bPatching ) )
	{
		// After it's downloaded, we want it to switch to the main connection port.
		if ( newArgv.Count() >= 3 && V_stricmp( newArgv[2], strDownloaderIP ) == 0 )
		{
			delete newArgv[2];
			newArgv[2] = CopyString( strMainIP );
		}
		
		g_Waiting_StartTime = Plat_FloatTime();
		g_Waiting_Argv.PurgeAndDeleteElements();
		g_Waiting_Argv = newArgv;
		g_Waiting_Priority = iPriority;
		g_Waiting_bPatching = bPatching;
		newArgv.Purge();
	}
	else
	{
		newArgv.PurgeAndDeleteElements();
	}

	// Remember that we tried to run this job so we don't try to run it again.
	AddJobMemory( g_CurJobID );
	
	SendStateToServicesBrowsers();
}


void HandlePacket_STOP_SERVICE( bf_read &buf, const CIPAddr &ipFrom )
{
	Msg( "Got a STOP_SERVICE packet. Shutting down...\n" );

	CWaitTimer timer( 1 );
	while ( 1 )
	{
		AddServicesBrowserIP( ipFrom );
		SendStateToServicesBrowsers();

		if ( timer.ShouldKeepWaiting() )
			Sleep( 200 );
		else
			break;
	}

	StopUI();
	ServiceHelpers_ExitEarly();
}


void HandlePacket_KILL_PROCESS( const CIPAddr *ipFrom )
{
	if ( Plat_FloatTime() - g_flLastKillProcessTime > 5 )
	{
		KillRunningProcess( "Got a KILL_PROCESS packet. Stopping the worker executable.\n", true );
		
		if ( ipFrom )
		{
			AddServicesBrowserIP( *ipFrom );
			SendStateToServicesBrowsers();
		}
		
		g_flLastKillProcessTime = Plat_FloatTime();
	}
}


void HandlePacket_FORCE_PASSWORD_CHANGE( bf_read &buf, const CIPAddr &ipFrom )
{
	char newPassword[512];
	buf.ReadString( newPassword, sizeof( newPassword ) );
	
	Msg( "Got a FORCE_PASSWORD_CHANGE (%s) packet.\n", newPassword );
	
	SetPassword( newPassword );
	if ( g_pConnMgr )
		g_pConnMgr->SendCurStateTo( -1 );
}


void VMPI_Waiter_Update()
{
	CheckScreensaverRunning();
	HandleWindowMessages();
	UpdateServicesBrowserIPs();
	
	while ( 1 )
	{
		WaitForProcessToExit();
		if ( CheckDownloaderFinished() )
			return;

		// Recv off the socket first so it clears the queue while we're waiting for the process to exit.
		char data[4096];
		CIPAddr ipFrom;
		int len = g_pSocket->RecvFrom( data, sizeof( data ), &ipFrom );

		// Any incoming packets?
		if ( len <= 0 )
			break;

		bf_read buf( data, len );
		if ( buf.ReadByte() != VMPI_PROTOCOL_VERSION )
			continue;

		// Only handle packets with the right password.
		char pwString[256];
		buf.ReadString( pwString, sizeof( pwString ) );

		int packetID = buf.ReadByte();
		
		if ( pwString[0] == VMPI_PASSWORD_OVERRIDE )
		{
			// Always process these packets regardless of the password (these usually come from
			// the installer when it is trying to stop a previously-running instance).
		}
		else if ( packetID == VMPI_LOOKING_FOR_WORKERS )
		{
			if ( pwString[0] == 0 )
			{
				if ( g_pPassword && g_pPassword[0] != 0 )
					continue;
			}
			else
			{
				if ( !g_pPassword || stricmp( g_pPassword, pwString ) != 0 )
					continue;
			}
		}

		// VMPI_KILL_PROCESS is checked before everything.
		if ( packetID == VMPI_KILL_PROCESS )
		{
			HandlePacket_KILL_PROCESS( &ipFrom );
		}
		else if ( packetID == VMPI_PING_REQUEST )
		{
			AddServicesBrowserIP( ipFrom );
			SendStateToServicesBrowsers();
		}
		else if ( packetID == VMPI_STOP_SERVICE )
		{
			HandlePacket_STOP_SERVICE( buf, ipFrom );
			return;
		}
		else if ( packetID == VMPI_SERVICE_PATCH )
		{
			// The key to doing this here is that we ignore whether we're disabled or in screensaver mode.. we always handle
			// the patch command (unless we've already handled this job ID OR if we've already applied this patch version).
			HandlePacket_LOOKING_FOR_WORKERS( buf, ipFrom );
		}
		else if ( packetID == VMPI_FORCE_PASSWORD_CHANGE )
		{
			HandlePacket_FORCE_PASSWORD_CHANGE( buf, ipFrom );
		}

		// If they've told us not to wait for jobs, then ignore the packet.
		if ( g_iCurState == VMPI_SERVICE_STATE_DISABLED || (g_bScreensaverMode && !g_bScreensaverRunning) )
			continue;

		if ( packetID == VMPI_LOOKING_FOR_WORKERS )
		{
			HandlePacket_LOOKING_FOR_WORKERS( buf, ipFrom );
		}
	}
}


// ------------------------------------------------------------------------------------------------ //
// Startup and service code.
// ------------------------------------------------------------------------------------------------ //

void RunMainLoop()
{
	// This is the service's main loop.
	while ( 1 )
	{
		// If the service has been told to exit, then just exit.
		if ( ServiceHelpers_ShouldExit() )
			break;

		VMPI_Waiter_Update();
		g_pConnMgr->Update();
		
		Sleep( 50 );
	}
}


void InternalRunService()
{
	if ( !VMPI_Waiter_Init() )
		return;

	RunMainLoop();
	VMPI_Waiter_Term();
}


// This function runs us as a console app. Useful for debugging or if you want to run more
// than one instance of VRAD on the same machine.
void RunAsNonServiceApp()
{
	InternalRunService();
}


// This function runs inside the service thread.
void ServiceThreadFn( void *pParam )
{
	InternalRunService();
}


// This function works with the service manager and runs as a system service.
void RunService()
{
	if( !ServiceHelpers_StartService( VMPI_SERVICE_NAME_INTERNAL, ServiceThreadFn, NULL ) )
	{
		Msg( "Service manager not started. Running as console app.\n" );
		g_RunMode = RUNMODE_CONSOLE;
		InternalRunService();
	}
}

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
	// Hook spew output.
	SpewOutputFunc( MySpewOutputFunc );

	// Get access to the registry..
	RegCreateKey( HKEY_LOCAL_MACHINE, VMPI_SERVICE_KEY, &g_hVMPIServiceKey );

	// Setup our version string.
	LoadString( hInstance, VMPI_SERVICE_IDS_VERSION_STRING, g_VersionString, sizeof( g_VersionString ) );

	// Setup the base app path.
	if ( !GetModuleFileName( GetModuleHandle( NULL ), g_BaseAppPath, sizeof( g_BaseAppPath ) ) )
	{
		Warning( "GetModuleFileName failed.\n" );
		return false;
	}
	V_StripLastDir( g_BaseAppPath, sizeof( g_BaseAppPath ) );

	// Setup the cache path.
	V_ComposeFileName( g_BaseAppPath, "vmpi_service_cache", g_FileCachePath, sizeof( g_FileCachePath ) );


	const char *pArg = FindArg( __argc, __argv, "-mpi_pw", NULL );
	SetPassword( pArg );
	
	if ( FindArg( __argc, __argv, "-console" ) )
	{					
		g_RunMode = RUNMODE_CONSOLE;
	}
	else
	{
		g_RunMode = RUNMODE_SERVICE;
	}

	if ( FindArg( __argc, __argv, "-superdebug" ) )
		g_bSuperDebugMode = true;

	g_AppStartTime = GetTickCount();
	g_bMinimized = FindArg( __argc, __argv, "-minimized" ) != NULL;

	ServiceHelpers_Init(); 	
	g_hInstance = hInstance;

	LoadStateFromRegistry();

	// Install the service?
	if ( g_RunMode == RUNMODE_CONSOLE )
	{					
		RunAsNonServiceApp();
	}
	else
	{
		RunService();
	}

	return 0;
}