//********************************************************* // // Copyright (c) Microsoft. All rights reserved. // THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY // IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR // PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. // //********************************************************* #include "pch.h" #include "MeshManager.h" #include "Utils.h" using namespace Concurrency; using namespace Platform; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Xbox::Networking; using namespace Microsoft::Xbox::Samples::NetworkMesh; namespace Microsoft { namespace Xbox { namespace Samples { namespace NetworkMesh { MeshManager::MeshManager( uint8 localConsoleId, Platform::String^ secureDeviceAssociationTemplateName, Platform::String^ localConsoleName, bool dropOutOfOrderPackets ) : m_localConsoleName(localConsoleName), m_dropOutOfOrderPackets(dropOutOfOrderPackets) { // load template for Secure Device Association. m_associationTemplate = Windows::Xbox::Networking::SecureDeviceAssociationTemplate::GetTemplateByName( secureDeviceAssociationTemplateName ); Initialize(localConsoleId); RegisterMeshPacketEventHandlers(); } void MeshManager::Initialize(uint8 localConsoleId) { m_connections.clear(); if(m_associationTemplate != nullptr) { DestroyAllTemplateAssociations(); // Listen to AssociationIncoming event TypedEventHandler^ associationIncomingEvent = ref new TypedEventHandler( [this] (Windows::Xbox::Networking::SecureDeviceAssociationTemplate^ associationTemplate, Windows::Xbox::Networking::SecureDeviceAssociationIncomingEventArgs^ args) { OnAssociationIncoming( associationTemplate, args ); }); m_associationIncomingToken = m_associationTemplate->AssociationIncoming += associationIncomingEvent; } else { LogComment("Association template is NULL!"); return; } unsigned short sin6_port = htons(m_associationTemplate->AcceptorSocketDescription->BoundPortRangeLower); m_meshPacketManager = ref new MeshPacketManager(localConsoleId, sin6_port, this, m_dropOutOfOrderPackets); // Try to connect to anything we need to every second int32 threadAffinityMask = ~0x04; // Means to this thread can run all everything except core 3 (which is reserved for graphics for example). m_autoConnectThread = ref new MeshThread(1000, threadAffinityMask, NORMAL_PRIORITY_CLASS); m_autoConnectThread->OnDoWork += ref new Windows::Foundation::EventHandler( [this]( Platform::Object^, ProcessThreadsEventArgs^ args ) { OnAutoConnectWorkerThreadDoWork(args); }); // Send heartbeats and hellos every 2 seconds threadAffinityMask = ~0x04; // Means to this thread can run all everything except core 3 (which is reserved for graphics for example). m_heartbeatThread = ref new MeshThread(DEFAULT_HEARTBEAT_PERIOD_MILLISECONDS, threadAffinityMask, NORMAL_PRIORITY_CLASS); m_heartbeatThread->OnDoWork += ref new Windows::Foundation::EventHandler( [this]( Platform::Object^, ProcessThreadsEventArgs^ args ) { OnHeartbeatWorkerThreadDoWork(args); }); } SecureDeviceAssociationTemplate^ MeshManager::GetSecureDeviceAssociationTemplate() { return m_associationTemplate; } // This should only be called from ConnectTo or OnIncomingAssociation. MeshConnection^ MeshManager::AddConnection( Windows::Xbox::Networking::SecureDeviceAddress^ secureDeviceAddress, Windows::Xbox::Networking::SecureDeviceAssociation^ secureDeviceAssociation, bool inComingAssociation, ConnectionStatus connectionStatus ) { if(secureDeviceAddress == nullptr) { return nullptr; } if(GetConnectionFromSecureDeviceAddress(secureDeviceAddress) != nullptr) { // The secure device address already exists. return nullptr; } MeshConnection^ meshConnection = ref new MeshConnection(secureDeviceAddress, this); meshConnection->SetInComingAssociation(inComingAssociation); meshConnection->SetConnectionStatus(connectionStatus); meshConnection->SetAssociation(secureDeviceAssociation); // Add him to the list of mesh connections. { Concurrency::critical_section::scoped_lock lock(m_connectionsLock); m_connections.push_back(meshConnection); } return meshConnection; } void MeshManager::ConnectToAddress( Windows::Xbox::Networking::SecureDeviceAddress^ secureDeviceAddress, Platform::String^ debugName ) { if (secureDeviceAddress == nullptr) { LogComment(L"Cannot pass a nullptr for the address to MeshManager::ConnectTo"); throw ref new InvalidArgumentException(L"Cannot pass a nullptr for the address to MeshManager::ConnectTo"); } if(AreSecureDeviceAddressesEqual(SecureDeviceAddress::GetLocal(), secureDeviceAddress)) { // Don't try to connect to local console. return; } LogComment( "ConnectToAddress: Attempting to connect to remote console: " + debugName); MeshConnection^ newMeshConnected = AddConnection(secureDeviceAddress, nullptr, false, ConnectionStatus::Disconnected); if( newMeshConnected != nullptr ) { // If this connection was new, then set the console name to be debug name. It will change once the handshake is done newMeshConnected->SetConsoleName(debugName); } } void MeshManager::OnAssociationIncoming( Windows::Xbox::Networking::SecureDeviceAssociationTemplate^ associationTemplate, Windows::Xbox::Networking::SecureDeviceAssociationIncomingEventArgs^ args ) { SecureDeviceAssociation^ association = args->Association; if(association != nullptr) { LogCommentFormat( L"OnAssociationIncoming: Incoming connection %s", Utils::PrintSecureDeviceAssociation(association, false, true)->Data() ); MeshConnection^ newMeshConnected = AddConnection(association->RemoteSecureDeviceAddress, association, true, ConnectionStatus::Connected); if(newMeshConnected != nullptr) { RefreshConnections(); } } } void MeshManager::OnAutoConnectWorkerThreadDoWork( Microsoft::Xbox::Samples::NetworkMesh::ProcessThreadsEventArgs^ args ) { // Try to connect to every outgoing connection that we are disconnected from. SecureDeviceAssociationTemplate^ secureDeviceAssociationTemplate = GetSecureDeviceAssociationTemplate(); Windows::Foundation::Collections::IVectorView^ allDisconnectedConnections = GetConnectionsByType(ConnectionStatus::Disconnected); for each (MeshConnection^ meshConnection in allDisconnectedConnections) { if(meshConnection->IsInComingAssociation()) { // Don't bother connecting to someone who connected to you // because that would create bi-directional connection which cause failures. continue; } // Ignore connections that have a CreateAssociationAsync in progress on them if(meshConnection->IsConnectionInProgress()) { continue; } // Ignore anyone we have now a connection with. Just in case anything changed since we got the list of disconnected connections above if( meshConnection->GetAssociation() != nullptr ) { continue; } LogComment( Utils::GetThreadDescription(L"THREAD: Calling CreateAssociationAsync") ); meshConnection->SetConnectionInProgress(true); auto asyncOp = secureDeviceAssociationTemplate->CreateAssociationAsync( meshConnection->GetSecureDeviceAddress(), Windows::Xbox::Networking::CreateSecureDeviceAssociationBehavior::Default ); create_task( asyncOp ) .then([this, meshConnection](task t) { LogComment( Utils::GetThreadDescription(L"THREAD: CreateAssociationAsync result") ); try { SecureDeviceAssociation^ association = t.get(); LogCommentFormat( L"CreateAssociationAsync success: %s %s", meshConnection->GetConsoleName()->Data(), Utils::PrintSecureDeviceAssociation(association, false, true)->Data() ); MeshConnection^ newMeshConnection = GetConnectionFromSecureDeviceAddress(association->RemoteSecureDeviceAddress); if(newMeshConnection != nullptr) { LogCommentFormat( L"Created new connection for %s", meshConnection->GetConsoleName()->Data() ); newMeshConnection->SetAssociation(association); newMeshConnection->SetConnectionStatus(ConnectionStatus::Connected); } } catch(Platform::Exception^ ex) { //We will auto-retry this again on next update. LogCommentFormat(L"Connecting to remote machine failed %s. %s", meshConnection->GetConsoleName()->Data(), Utils::GetErrorString(ex->HResult)->Data() ); } meshConnection->SetConnectionInProgress(false); }); // This thread is waiting for CreateAssociationAsync to complete since this is happening // inside a worker thread that does nothing but try to connect to other consoles } } void MeshManager::OnHeartbeatWorkerThreadDoWork( Microsoft::Xbox::Samples::NetworkMesh::ProcessThreadsEventArgs^ args ) { // This function is called every so often (eg. every 2 sec) by the MeshThread class // First, refresh the connection list RefreshConnections(); // Send hello to all connections that need it Windows::Foundation::Collections::IVectorView^ allconnected = GetConnectionsByType(ConnectionStatus::Connected); for each (MeshConnection^ meshConnection in allconnected) { //Send hello to retry handshake. Windows::Xbox::Networking::SecureDeviceAssociation^ association = meshConnection->GetAssociation(); if(association != nullptr) { bool isRespondingToHello = false; Platform::String^ remoteName = Utils::PrintSecureDeviceAssociation(association, false, true); LogCommentFormat( L"Sending hello to remote console: %s", remoteName->Data()); m_meshPacketManager->SendHelloMessage(association, GetLocalConsoleDisplayName(), isRespondingToHello); } } // Send heartbeat to all connections that we have completed handshake with Windows::Foundation::Collections::IVectorView^ initializedConnections = GetConnectionsByType(ConnectionStatus::PostHandshake); for each (MeshConnection^ meshConnection in initializedConnections) { Windows::Xbox::Networking::SecureDeviceAssociation^ association = meshConnection->GetAssociation(); if( association != nullptr ) { m_meshPacketManager->SendHeartbeatMessageAsync( association, meshConnection->GetConsoleId() ); } } } Platform::String^ MeshManager::GetLocalConsoleName() { Platform::String^ localConsoleName = L"Console"; { Concurrency::critical_section::scoped_lock lock(m_connectionsLock); localConsoleName = m_localConsoleName; } return localConsoleName; } void MeshManager::SetLocalConsoleName(Platform::String^ localConsoleName) { { Concurrency::critical_section::scoped_lock lock(m_connectionsLock); m_localConsoleName = localConsoleName; } } Platform::String^ MeshManager::GetLocalConsoleDisplayName() { return Utils::FormatString(L"%s [%d]", GetLocalConsoleName()->Data(), m_meshPacketManager->GetLocalConsoleId() ); } /// /// Windows::Foundation::Collections::IVectorView^ MeshManager::GetConnections() { Concurrency::critical_section::scoped_lock lock(m_connectionsLock); auto v = ref new Platform::Collections::Vector(m_connections); return v->GetView(); } Windows::Foundation::Collections::IVectorView^ MeshManager::GetConnectionsByType(ConnectionStatus type) { auto associationsByType = ref new Platform::Collections::Vector(); Windows::Foundation::Collections::IVectorView^ allConnections = GetConnections(); for each (MeshConnection^ meshConnection in allConnections) { if( meshConnection->GetConnectionStatus() == type ) { associationsByType->Append(meshConnection); } } return associationsByType->GetView(); } void MeshManager::RefreshConnections() { // First, loop through all connections and mark all connections as not found in the template Windows::Foundation::Collections::IVectorView^ allConnections = GetConnections(); for each (MeshConnection^ meshConnection in allConnections) { meshConnection->SetAssocationFoundInTemplate(false); } // Now, Loop thought template and find the each connection and mark the ones that we found // And take note of anything in the template that is not yet in our internal list of connections. Windows::Foundation::Collections::IVectorView^ associations = GetSecureDeviceAssociationTemplate()->Associations; for each (Windows::Xbox::Networking::SecureDeviceAssociation^ associationInTemplate in associations) { MeshConnection^ meshConnection = GetConnectionFromSecureDeviceAddress(associationInTemplate->RemoteSecureDeviceAddress); if(meshConnection != nullptr) { meshConnection->SetAssocationFoundInTemplate(true); ConnectionStatus status = meshConnection->GetConnectionStatus(); // If the status is incorrect, then fix it log the error and fix it if( status != ConnectionStatus::Connected && status != ConnectionStatus::PostHandshake ) { LogCommentFormat( L"ERROR: Found association in template that we didn't CreateAssociationAsync or get an OnAssociationIncoming event for" ); LogCommentFormat( L"ERROR: Address: %s", Utils::PrintSecureDeviceAssociation(associationInTemplate, false, true)->Data() ); meshConnection->SetConnectionStatus(ConnectionStatus::Connected); meshConnection->SetAssociation(associationInTemplate); } } else { LogCommentFormat( L"ERROR: Mismatch occurred: Association in template not found in our internal list of connections" ); } } // First, loop through all connections again and for anything not found in the template, disconnect from it and tell the game. // Get the latest list again as connections could have been deleted. allConnections = GetConnections(); for each (MeshConnection^ meshConnection in allConnections) { if ( meshConnection != nullptr && false == meshConnection->GetAssocationFoundInTemplate() && meshConnection->GetConnectionStatus() != ConnectionStatus::Pending && meshConnection->GetConnectionStatus() != ConnectionStatus::Disconnected ) { // Do not delete him. We want the raise an event to the game and have the game call DestroyConnection explicitly. LogCommentFormat( L"Disconnecting remote console %s who was not in template", meshConnection->GetConsoleName()->Data() ); LogCommentFormat( L"Address: %s", Utils::PrintSecureDeviceAssociation(meshConnection->GetAssociation(), false, true)->Data() ); meshConnection->SetConnectionStatus(ConnectionStatus::Disconnected); OnDisconnected(this, meshConnection); } } } bool MeshManager::DoesConnectionExistInList(Windows::Foundation::Collections::IVectorView^ list, MeshConnection^ connection) { if(connection == nullptr) { return false; } bool bfound = false; for (unsigned int i = 0; i < list->Size; i++ ) { if (list->GetAt(i) == connection) { bfound = true; break; } } return bfound; } void MeshManager::OnAssociationChange( Windows::Xbox::Networking::SecureDeviceAssociationStateChangedEventArgs^ args, Windows::Xbox::Networking::SecureDeviceAssociation^ association ) { // Update this mesh member with the latest information switch(args->NewState) { case Windows::Xbox::Networking::SecureDeviceAssociationState::DestroyingLocal: case Windows::Xbox::Networking::SecureDeviceAssociationState::DestroyingRemote: case Windows::Xbox::Networking::SecureDeviceAssociationState::Invalid: { bool bFound = false; { MeshConnection^ meshConnection = GetConnectionFromAssociation(association); if(meshConnection != nullptr) { bFound = true; // let the user know that this connection is being disconnected. LogCommentFormat( L"Remote console is disconnecting %s", meshConnection->GetConsoleName()->Data() ); LogComment( Utils::GetThreadDescription(L"THREAD: Remote console disconnecting") ); meshConnection->SetConnectionDestroying(true); meshConnection->SetConnectionStatus(ConnectionStatus::Disconnected); OnDisconnected(this, meshConnection); } } if (!bFound) { LogComment(L"Association Change event fired for the wrong association."); } } default: break; } } MeshConnection^ MeshManager::GetConnectionFromAssociation(Windows::Xbox::Networking::SecureDeviceAssociation^ association) { if(association == nullptr) { return nullptr; } SecureDeviceAddress^ remoteSecureDeviceAddress = association->RemoteSecureDeviceAddress; return GetConnectionFromSecureDeviceAddress(remoteSecureDeviceAddress); } MeshConnection^ MeshManager::GetConnectionFromSecureDeviceAddress(Windows::Xbox::Networking::SecureDeviceAddress^ address) { if(address == nullptr) { return nullptr; } Windows::Foundation::Collections::IVectorView^ allConnections = GetConnections(); for each (MeshConnection^ meshConnection in allConnections) { SecureDeviceAddress^ remoteSecureDeviceAddress = meshConnection->GetSecureDeviceAddress(); if(AreSecureDeviceAddressesEqual(remoteSecureDeviceAddress, address)) { return meshConnection; } } return nullptr; } MeshConnection^ MeshManager::GetConnectionFromConsoleId(uint8 consoleId) { Windows::Foundation::Collections::IVectorView^ allConnections = GetConnections(); for each (MeshConnection^ meshConnection in allConnections) { uint8 meshConsoleId = meshConnection->GetConsoleId(); if( meshConsoleId == consoleId ) { return meshConnection; } } return nullptr; } void MeshManager::DeleteConnection(MeshConnection^ connection) { if (connection == nullptr) { LogComment(L"Cannot pass a nullptr for the member to DeleteConnection"); throw ref new InvalidArgumentException(L"Cannot pass a nullptr for the member to DeleteConnection"); } bool found = false; { Concurrency::critical_section::scoped_lock lock(m_connectionsLock); auto iter = m_connections.begin(); for( ; iter != m_connections.end(); iter++ ) { if ((*iter) == connection) { found = true; break; } } if (found) { m_connections.erase(iter); } } } void MeshManager::DestroyConnection( MeshConnection^ connection ) { if(connection == nullptr) { return; } SecureDeviceAssociation^ meshAssociation = connection->GetAssociation(); if(meshAssociation != nullptr) { if(false == connection->IsConnectionDestroying()) { connection->SetConnectionDestroying(true); IAsyncAction^ asyncOp = meshAssociation->DestroyAsync(); create_task(asyncOp) .then([this] (task t) { try { t.get(); // if t.get fails, it will throw. } catch (Platform::COMException^ ex) { LogCommentFormat( L"MeshManager::DestroyConnection - DestroyAsync failed %s", Utils::GetErrorString(ex->HResult)->Data()); } }).wait(); } connection->SetAssociation(nullptr); } DeleteConnection(connection); } void MeshManager::DisconectFromAddress( Windows::Xbox::Networking::SecureDeviceAddress^ address ) { MeshConnection^ connection = GetConnectionFromSecureDeviceAddress(address); DestroyConnection(connection); } /// /// void MeshManager::DestroyAndDisconnectAll() { Windows::Foundation::Collections::IVectorView^ allConnections = GetConnections(); for each (MeshConnection^ meshConnection in allConnections) { DestroyConnection(meshConnection); } { Concurrency::critical_section::scoped_lock lock(m_connectionsLock); m_connections.clear(); } GetMeshPacketManager()->DeleteAllPendingAckMeshPackets(); } void MeshManager::DestroyAllTemplateAssociations() { if(GetSecureDeviceAssociationTemplate() != nullptr) { Windows::Foundation::Collections::IVectorView^ associations = GetSecureDeviceAssociationTemplate()->Associations; if(associations == nullptr || associations->Size == 0) { return; } LogCommentFormat(L"We have %d associations on MeshManager::Initialize", associations->Size ); for each (Windows::Xbox::Networking::SecureDeviceAssociation^ associationInTemplate in associations) { IAsyncAction^ asyncOp = associationInTemplate->DestroyAsync(); create_task(asyncOp) .then([this] (task t) { try { t.get(); // if t.get fails, it will throw. } catch (Platform::COMException^ ex) { LogCommentFormat( L"MeshManager::DestroyAllTemplateAssociations - DestroyAsync failed %s", Utils::GetErrorString(ex->HResult)->Data()); } }).wait(); } } } void MeshManager::Shutdown() { LogComment( L"MeshManager::Shutdown"); DestroyAndDisconnectAll(); if (m_autoConnectThread != nullptr) { m_autoConnectThread->Shutdown(); m_autoConnectThread = nullptr; } if (m_heartbeatThread != nullptr) { m_heartbeatThread->Shutdown(); m_heartbeatThread = nullptr; } if( m_associationTemplate != nullptr ) { m_associationTemplate->AssociationIncoming -= m_associationIncomingToken; m_associationTemplate = nullptr; } if( m_meshPacketManager != nullptr ) { m_meshPacketManager->OnHelloReceived -= m_onHelloReceivedToken; m_meshPacketManager->OnHeartbeatReceived -= m_onHeartbeatReceivedToken; m_meshPacketManager->OnDebugMessage -= m_onDebugMessageReceivedToken; m_meshPacketManager->Shutdown(); m_meshPacketManager = nullptr; } } bool MeshManager::AreSecureDeviceAddressesEqual( Windows::Xbox::Networking::SecureDeviceAddress^ secureDeviceAddress1, Windows::Xbox::Networking::SecureDeviceAddress^ secureDeviceAddress2 ) { if( secureDeviceAddress1 != nullptr && secureDeviceAddress2 != nullptr && secureDeviceAddress1->Compare(secureDeviceAddress2) == 0 ) { return true; } return false; } Microsoft::Xbox::Samples::NetworkMesh::MeshPacketManager^ MeshManager::GetMeshPacketManager() { return m_meshPacketManager; } UINT MeshManager::GetHeartbeatPeriod() { if (m_heartbeatThread == nullptr) return 0; return m_heartbeatThread->GetSendPeriod(); } void MeshManager::SetHeartbeatPeriod(UINT periodInMilliseconds) { if (m_heartbeatThread != nullptr) m_heartbeatThread->SetSendPeriod(periodInMilliseconds); } void MeshManager::OnHeartbeatReceived( Microsoft::Xbox::Samples::NetworkMesh::MeshHeartbeatReceivedEvent^ args ) { if(args != nullptr) { MeshConnection^ meshConnection = args->Sender; if(meshConnection != nullptr) { OnHeartbeat( this, args->Sender ); } } } void MeshManager::OnHelloReceived( Microsoft::Xbox::Samples::NetworkMesh::MeshHelloReceivedEvent^ args ) { if(args != nullptr) { LogCommentFormat( L"OnHelloReceived: Remote console: %s [%d]. RespondingToHello: %d", args->ConsoleName->Data(), args->ConsoleId, (int)args->RespondingToHello ); MeshConnection^ meshConnection = args->Sender; if(meshConnection != nullptr ) { Windows::Xbox::Networking::SecureDeviceAssociation^ secureDeviceAssociation = meshConnection->GetAssociation(); if( secureDeviceAssociation == nullptr ) { return; } if (args->ConsoleId != 0) { meshConnection->SetConsoleId(args->ConsoleId); } if (!args->ConsoleName->IsEmpty()) { meshConnection->SetConsoleName(args->ConsoleName); } // When the remote console sends us a Hello that wasn't a response from us, respond back with Hello if( args->RespondingToHello == false ) { LogCommentFormat( L"OnHelloReceived: Responding to hello to %s", Utils::PrintSecureDeviceAssociation(secureDeviceAssociation, false, true)->Data() ); m_meshPacketManager->SendHelloMessage( secureDeviceAssociation, GetLocalConsoleDisplayName(), true ); } // If the connection to them was not Initialized, mark it and fire OnPostHandshake ConnectionStatus connectionStatus = meshConnection->GetConnectionStatus(); if( connectionStatus != ConnectionStatus::PostHandshake ) { meshConnection->SetConnectionStatus(ConnectionStatus::PostHandshake); LogCommentFormat( L"Remote console %s", Utils::PrintSecureDeviceAssociation(secureDeviceAssociation, false, true)->Data() ); LogCommentFormat( L"is now known as %s", meshConnection->GetConsoleName()->Data() ); OnPostHandshake( this, meshConnection ); } } } } void MeshManager::OnDebugMessageReceived( Microsoft::Xbox::Samples::NetworkMesh::DebugMessageEventArgs^ args ) { DebugMessageEventArgs^ eventArgs = ref new DebugMessageEventArgs( args->Message, args->HResult); OnDebugMessage(this,eventArgs); } void MeshManager::RegisterMeshPacketEventHandlers() { if(m_meshPacketManager == nullptr) { LogComment(L"Could not register for events as MeshPacketManager may have failed initializing."); return; } EventHandler^ onHelloReceivedEvent = ref new EventHandler( [this] (Platform::Object^, MeshHelloReceivedEvent^ eventArgs) { OnHelloReceived(eventArgs); }); m_onHelloReceivedToken = m_meshPacketManager->OnHelloReceived += onHelloReceivedEvent; EventHandler^ onHeartbeatReceivedEvent = ref new EventHandler( [this] (Platform::Object^, MeshHeartbeatReceivedEvent^ eventArgs) { OnHeartbeatReceived(eventArgs); }); m_onHeartbeatReceivedToken = m_meshPacketManager->OnHeartbeatReceived += onHeartbeatReceivedEvent; EventHandler^ onDebugMessageEvent = ref new EventHandler( [this] (Platform::Object^, DebugMessageEventArgs^ eventArgs) { OnDebugMessageReceived(eventArgs); }); m_onDebugMessageReceivedToken = m_meshPacketManager->OnDebugMessage += onDebugMessageEvent; } void MeshManager::LogCommentFormat( LPCWSTR strMsg, ... ) { WCHAR strBuffer[2048]; va_list args; va_start(args, strMsg); _vsnwprintf_s( strBuffer, 2048, _TRUNCATE, strMsg, args ); strBuffer[2047] = L'\0'; va_end(args); LogComment(ref new Platform::String(strBuffer)); } void MeshManager::LogComment( Platform::String^ strText ) { DebugMessageEventArgs^ eventArgs = ref new DebugMessageEventArgs( strText, S_OK); OnDebugMessageReceived(eventArgs); } }}}}