private void WorkerThreadMain(uint rootProcessID, IDebugEventListener rootListener)
		{
			if (!DebugActiveProcess(rootProcessID))
			{
				rootListener.OnAttachFailed(GetLastWin32Exception());
				return;
			}

			var processAttachments = new Dictionary<uint, ProcessAttachment>();
			var rootProcessAttachment = new ProcessAttachment(rootListener);
			processAttachments.Add(rootProcessID, rootProcessAttachment);

			while (processAttachments.Count > 0)
			{
				DEBUG_EVENT debugEvent;
				CheckWin32(WaitForDebugEventEx(out debugEvent, INFINITE));

				ProcessAttachment processAttachment;
				if (!processAttachments.TryGetValue(debugEvent.dwProcessId, out processAttachment))
				{
					// Unexpected, we should have been asked to attach to the process
					throw new InvalidOperationException();
				}

				// If we've just attached to a new process, complete the attach event.
				var listener = processAttachment.Listener;
				int threadID = unchecked((int)debugEvent.dwThreadId);
				DebugEventResponse response;
				switch (debugEvent.dwDebugEventCode)
				{
					case EXCEPTION_DEBUG_EVENT:
						{
							var record = IntPtr.Size == sizeof(int)
								? ExceptionRecord.FromStruct(ref debugEvent.Exception32.ExceptionRecord)
								: ExceptionRecord.FromStruct(ref debugEvent.Exception64.ExceptionRecord);
							response = listener.OnException(threadID, record);
						}
						break;

					case CREATE_PROCESS_DEBUG_EVENT:
						if (debugEvent.dwProcessId != rootProcessID)
							throw new NotImplementedException("Attaching to child processes.");
						response = processAttachment.OnAttachSucceeded(threadID, debugEvent.CreateProcessInfo);
						break;

					case CREATE_THREAD_DEBUG_EVENT:
						response = processAttachment.OnThreadCreated(threadID, debugEvent.CreateThread);
						break;

					case EXIT_THREAD_DEBUG_EVENT:
						response = processAttachment.OnThreadExited(threadID, debugEvent.ExitThread);
						break;

					case LOAD_DLL_DEBUG_EVENT:
						{
							var fileHandle = new SafeFileHandle(debugEvent.LoadDll.hFile, ownsHandle: false);
							response = listener.OnModuleLoaded(threadID, new ForeignPtr(debugEvent.LoadDll.lpBaseOfDll), fileHandle);
						}
						break;

					case OUTPUT_DEBUG_STRING_EVENT:
						{
							var str = debugEvent.DebugString.fUnicode == 0
								? Marshal.PtrToStringAnsi(debugEvent.DebugString.lpDebugStringData, debugEvent.DebugString.nDebugStringLength)
								: Marshal.PtrToStringUni(debugEvent.DebugString.lpDebugStringData, debugEvent.DebugString.nDebugStringLength);
							response = listener.OnStringOutputted(threadID, str);
						}
						break;

					case UNLOAD_DLL_DEBUG_EVENT:
						response = listener.OnModuleUnloaded(threadID, new ForeignPtr(debugEvent.UnloadDll.lpBaseOfDll));
						break;

					default:
						Debug.Fail("Unknown debug code.");
						response = DebugEventResponse.ContinueUnhandled;
						break;
				}

				if (response != DebugEventResponse.Break)
				{
					bool unhandledException = response == DebugEventResponse.ContinueUnhandled
						&& debugEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT;
					CheckWin32(ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId,
						unhandledException ? DBG_EXCEPTION_NOT_HANDLED : DBG_CONTINUE));
				}
			}
		}