//
//	InterlockedExecute.c
//
//	Execute code with exclusive access to system, with 
//  	all CPUs locked down. 
//
//	** Experimental code not fully tested **
//
//	Code assumes that KeQueryPerformanceCounter returns a
//  	true tick-count per CPU rather than a system-wide value
//
//	Make sure your callback function is prototyped as:
//
//		ULONG UserCallback(PVOID Param);
//
//	Call InterlockedExecute as follows:
//
//		InterlockedExecute(HIGH_LEVEL, UserCallback, userParam, &retVal);
//
#include <ntddk.h>

typedef ULONG (* USER_ROUTINE)(PVOID Param);

#define TIMEOUT (3000)	// 3 second timeout!!

typedef struct
{
	LONG				counter;
	LONG				exitLock;
	KIRQL				irqlLevel;

	LONG				funcStarted;
	LONG				funcTimeout;

	USER_ROUTINE		userFunc;
	PVOID				userParam;
	ULONG				userReturn;

	KDPC				dpcList   [MAXIMUM_PROCESSORS];
	KEVENT				eventList [MAXIMUM_PROCESSORS];

} SYNC_LIST, *PSYNC_LIST;

//
//	Return if specified time (in milliseconds) has passed
//
static BOOLEAN TimeExceeded(LARGE_INTEGER *ticks1, ULONG timeout)
{
	LARGE_INTEGER ticks2, freq;

	ticks2 = KeQueryPerformanceCounter(&freq); 

	if((ticks2.QuadPart - ticks1->QuadPart) * 1000 / freq.QuadPart >= timeout)
	{
		return TRUE;
	}
	else
	{
		return FALSE;
	}
}

//
//	Deferred Procedure, executes at DISPATCH_LEVEL therefore
//  cannot be interrupted by the dispatcher
//
static void SyncDpcProc
	(
		PKDPC		Dpc, 
		PSYNC_LIST	SyncList, 
		PKEVENT		ExitEvent, 
		PVOID		Unused
	)
{
	KIRQL oldIrql;
	
	KeRaiseIrql(SyncList->irqlLevel, &oldIrql);

	// The last DPC to be scheduled gets to execute the user-code
	if(InterlockedDecrement(&SyncList->counter) == 0)
	{
		// Signal that we have started executing, to prevent DPCs 
		// from exiting too early due to time-outs
		InterlockedExchange(&SyncList->funcStarted, 1);

		// if a DPC has timed-out then we must assume it has exited, therefore
		// the system is no longer locked and we must also exit
		if(SyncList->funcTimeout == 0)
		{
			// All the DPCs must be executing - so call the user function!
			SyncList->userReturn = SyncList->userFunc(SyncList->userParam);
		}
		
		InterlockedExchange(&SyncList->exitLock, 0);
	}
	// we weren't the last DPC so just wait until we are told to exit
	else
	{
		LARGE_INTEGER startTime;

		// record current cpu-tick-count
		startTime = KeQueryPerformanceCounter(NULL);

		// Try to acquire the exit lock
		while(InterlockedExchange(&SyncList->exitLock, 1))
		{
			// if we exceed our timeout period and the 
			// user-function hasn't yet started, break out and exit
			if(TimeExceeded(&startTime, TIMEOUT) && !SyncList->funcStarted)
			{
				InterlockedExchange(&SyncList->funcTimeout, 1);
				break;
			}
		}

		// Release the lock for the next thread so it can exit as well
		InterlockedExchange(&SyncList->exitLock, 0);
	}

	KeLowerIrql(oldIrql);

	// signal the main thread that we are exiting!
	KeSetEvent(ExitEvent, 0, FALSE);
}

//
//	InterlockedExecute
//
//	Execute the specified user function at minimum of DISPATCH_LEVEL, with 
//  exclusive access to the current processor (all other CPUs are locked)
//
NTSTATUS InterlockedExecute
	(
		IN	KIRQL				IrqlLevel, 
		IN	USER_ROUTINE		UserFunction, 
		IN	PVOID				UserParam, 
		OUT	PULONG				UserReturn	 OPTIONAL
	)
{
	PVOID		waitList[MAXIMUM_PROCESSORS];
	KIRQL		oldIrql;
	ULONG		numProcessors = KeNumberProcessors;
	ULONG		i;
	
	SYNC_LIST	syncList = 
	{ 
		numProcessors, 
		1,					// acquire the exit-lock 
		IrqlLevel,
		0,
		0,
		UserFunction, 
		UserParam, 
		0 
	};

	//
	// Paranoid parameter validation
	//
	if(UserFunction == 0 || numProcessors >= MAXIMUM_PROCESSORS)
		return STATUS_INVALID_PARAMETER;

	if(KeGetCurrentIrql() != PASSIVE_LEVEL || IrqlLevel < DISPATCH_LEVEL)
		return STATUS_INVALID_PARAMETER;

	//
	// Setup at PASSIVE_LEVEL
	//
	for(i = 0; i < numProcessors; i++)
	{
		// Setup an Event for each DPC to signal when it exits
		KeInitializeEvent(&syncList.eventList[i], SynchronizationEvent, FALSE);
		waitList[i]     = &syncList.eventList[i];

		// Initialize a DPC for each processor
		KeInitializeDpc			(&syncList.dpcList[i], SyncDpcProc, &syncList);
		KeSetImportanceDpc		(&syncList.dpcList[i], HighImportance);
		KeSetTargetProcessorDpc	(&syncList.dpcList[i], (CHAR)i);
	}

	//
	// Scheduling at DISPATCH_LEVEL, so that no DPCs get executed until
	// we drop back to passive - this prevents the current CPU from executing
	// any DPCs and protects us against deadlocking ourselves until all DPCs
	// have been set up
	//
	oldIrql = KeRaiseIrqlToDpcLevel();

	for(i = 0; i < numProcessors; i++)
	{
		KeInsertQueueDpc(&syncList.dpcList[i], &syncList.eventList[i], 0);	
	}

	KeLowerIrql(oldIrql);

	//
	// wait for all DPC routines to exit. KeFlushQueuedDpcs would also work
	// but doesn't seem to exist on NT->XP
	//
	KeWaitForMultipleObjects(	numProcessors, 
								waitList, 
								WaitAll, 
								Executive, 
								KernelMode, 
								FALSE, 0, 0
							);

	//
	// store return value if requested and return to caller
	//
	if(UserReturn != NULL)
		*UserReturn = syncList.userReturn;

	return syncList.funcTimeout ? STATUS_TIMEOUT : STATUS_SUCCESS;
}
