// Copyright Epic Games, Inc. All Rights Reserved. #include "DirectLinkManager.h" #include "DirectLinkAssetObserver.h" #include "DirectLinkExtensionSettings.h" #include "DirectLinkExternalSource.h" #include "DirectLinkUriResolver.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/IAssetRegistry.h" #include "Async/Async.h" #include "Misc/CoreDelegates.h" #include "UObject/UObjectGlobals.h" #define LOCTEXT_NAMESPACE "DirectLinkManager" DEFINE_LOG_CATEGORY(LogDirectLinkManager); namespace UE::DatasmithImporter { TUniquePtr FDirectLinkManager::Instance = nullptr; FDirectLinkAutoReconnectManager::FDirectLinkAutoReconnectManager(FDirectLinkManager& InManager) : Manager(InManager) , bShouldRun(true) { if (const UDirectLinkExtensionSettings* DefaultSettings = GetDefault()) { bAutoReconnectEnabled = DefaultSettings->bAutoReconnect; ReconnectionDelayInSeconds = DefaultSettings->ReconnectionDelayInSeconds; } FCoreDelegates::OnPreExit.AddRaw(this, &FDirectLinkAutoReconnectManager::Stop); } FDirectLinkAutoReconnectManager::~FDirectLinkAutoReconnectManager() { FCoreDelegates::OnPreExit.RemoveAll(this); if (CompletedFuture.IsValid()) { Stop(); CompletedFuture.Wait(); } } bool FDirectLinkAutoReconnectManager::Start() { if (bAutoReconnectEnabled && (!CompletedFuture.IsValid() || CompletedFuture.IsReady())) { bShouldRun = true; CompletedFuture = Async(EAsyncExecution::ThreadPool, [this]() { Run(); }); return true; } return false; } void FDirectLinkAutoReconnectManager::Stop() { bShouldRun = false; } void FDirectLinkAutoReconnectManager::Run() { const float CurrentTime = FPlatformTime::Seconds(); const float TimeSinceLastTry = CurrentTime - LastTryTime; if (TimeSinceLastTry < ReconnectionDelayInSeconds) { FPlatformProcess::Sleep(ReconnectionDelayInSeconds - TimeSinceLastTry); } bool bHasSourceToReconnect = false; { FWriteScopeLock ReconnectionScopeLock(Manager.ReconnectionListLock); for (int32 Index = Manager.ExternalSourcesToReconnect.Num() - 1; Index >= 0; --Index) { if (Manager.ExternalSourcesToReconnect[Index]->OpenStream()) { Manager.ExternalSourcesToReconnect.RemoveAtSwap(Index); } } bHasSourceToReconnect = Manager.ExternalSourcesToReconnect.Num() > 0; } LastTryTime = FPlatformTime::Seconds(); if (bShouldRun && bHasSourceToReconnect) { // Could not reconnect, go back to the ThreadPool and try again later. CompletedFuture = Async(EAsyncExecution::ThreadPool, [this]() { Run(); }); } } FDirectLinkManager::FDirectLinkManager() : Endpoint(MakeUnique(TEXT("UE5-Editor"))) , AssetObserver(MakeUnique(*this)) , ReconnectionManager(MakeUnique(*this)) , AutoReimportManger(MakeShared()) { Endpoint->AddEndpointObserver(this); ensureMsgf(!Instance, TEXT("There can only be one instance of FDirectLinkManager.")); } FDirectLinkManager::~FDirectLinkManager() = default; void FDirectLinkManager::Clear() { Endpoint->RemoveEndpointObserver(this); // Make sure all DirectLink external source become stales and their delegates stripped. for (const TPair>& UriExternalSourcePair : DirectLinkSourceToExternalSourceMap) { UriExternalSourcePair.Value->Invalidate(); } } TSharedPtr FDirectLinkManager::GetOrCreateExternalSource(const DirectLink::FSourceHandle& SourceHandle) { if (TSharedRef* ExternalSourceEntry = DirectLinkSourceToExternalSourceMap.Find(SourceHandle)) { // A DirectLinkExternalSource already exists for this SourceHandle. return *ExternalSourceEntry; } else if (RegisteredExternalSourcesInfo.Num() > 0) { DirectLink::FRawInfo::FDataPointInfo* SourceDataPointInfo; { FRWScopeLock ScopeLock(RawInfoLock, FRWScopeLockType::SLT_ReadOnly); SourceDataPointInfo = RawInfoCache.DataPointsInfo.Find(SourceHandle); } FSourceUri ExternalSourceUri(GetUriFromSourceHandle(SourceHandle)); if (SourceDataPointInfo && ExternalSourceUri.IsValid()) { const FString& SourceName = SourceDataPointInfo->Name; const FString ExternalSourceName = FString::Printf(TEXT("%s_%s_ExternalSource"), *SourceName, *SourceHandle.ToString()); const DirectLink::IConnectionRequestHandler::FSourceInformation SourceInfo{ SourceHandle }; for (const FDirectLinkExternalSourceRegisterInformation& RegisteredInfo : RegisteredExternalSourcesInfo) { TSharedRef SpawnedExternalSource = RegisteredInfo.SpawnFunction(ExternalSourceUri); if (SpawnedExternalSource->CanOpenNewConnection(SourceInfo)) { const FGuid DestinationHandle = Endpoint->AddDestination(ExternalSourceName, DirectLink::EVisibility::Private, SpawnedExternalSource); SpawnedExternalSource->Initialize(SourceName, SourceHandle, DestinationHandle); DirectLinkSourceToExternalSourceMap.Add(SourceHandle, SpawnedExternalSource); return SpawnedExternalSource; } } } } return nullptr; } TSharedPtr FDirectLinkManager::GetOrCreateExternalSource(const FSourceUri& Uri) { DirectLink::FSourceHandle SourceHandle; TSharedPtr ExternalSource; if (TOptional SourceDescription = FDirectLinkUriResolver::TryParseDirectLinkUri(Uri)) { // Try getting the external source with the explicit id first. if (SourceDescription->SourceId) { SourceHandle = SourceDescription->SourceId.GetValue(); if (SourceHandle.IsValid()) { ExternalSource = GetOrCreateExternalSource(SourceHandle); } } // Could not retrieve the external source from the id, fall back on the first source matching the source description. if (!ExternalSource) { SourceHandle = ResolveSourceHandleFromDescription(SourceDescription.GetValue()); if (SourceHandle.IsValid()) { ExternalSource = GetOrCreateExternalSource(SourceHandle); } } } return ExternalSource; } DirectLink::FSourceHandle FDirectLinkManager::ResolveSourceHandleFromDescription(const FDirectLinkSourceDescription& SourceDescription) const { FRWScopeLock ScopeLock(RawInfoLock, FRWScopeLockType::SLT_ReadOnly); for (const auto& EndpointKeyValue : RawInfoCache.EndpointsInfo) { // Try to find a matching DirectLink source. if (EndpointKeyValue.Value.ComputerName == SourceDescription.ComputerName && EndpointKeyValue.Value.ExecutableName == SourceDescription.ExecutableName && EndpointKeyValue.Value.Name == SourceDescription.EndpointName) { for (const DirectLink::FRawInfo::FDataPointId& SourceInfo : EndpointKeyValue.Value.Sources) { if (SourceInfo.Name == SourceDescription.SourceName) { // Source found, returning the handle. return SourceInfo.Id; } } } } //Returning default invalid handle. return DirectLink::FSourceHandle(); } void FDirectLinkManager::OnStateChanged(const DirectLink::FRawInfo& RawInfo) { { FRWScopeLock ScopeLock(RawInfoLock, FRWScopeLockType::SLT_Write); RawInfoCache = RawInfo; } UpdateSourceCache(); CancelEmptySourcesLoading(); } void FDirectLinkManager::UpdateSourceCache() { FRWScopeLock ScopeLock(RawInfoLock, FRWScopeLockType::SLT_ReadOnly); // List the source Id of all current external source. This is used to determine which ones are no longer valid. TSet InvalidExternalSourceIds; DirectLinkSourceToExternalSourceMap.GetKeys(InvalidExternalSourceIds); for (const TPair& EndpointInfoPair : RawInfoCache.EndpointsInfo) { if (!EndpointInfoPair.Value.bIsLocal) { continue; } for (const DirectLink::FRawInfo::FDataPointId& DataPointId : EndpointInfoPair.Value.Sources) { if (DataPointId.bIsPublic) { if (GetOrCreateExternalSource(DataPointId.Id)) { // This source is still valid. InvalidExternalSourceIds.Remove(DataPointId.Id); } } } } TSet ActiveStreamsSources; for (const DirectLink::FRawInfo::FStreamInfo& StreamInfo : RawInfoCache.StreamsInfo) { if (!(StreamInfo.ConnectionState == DirectLink::EStreamConnectionState::Active || StreamInfo.ConnectionState == DirectLink::EStreamConnectionState::RequestSent)) { continue; } if (TSharedRef* ExternalSource = DirectLinkSourceToExternalSourceMap.Find(StreamInfo.Source)) { ActiveStreamsSources.Add(StreamInfo.Source); } } { FWriteScopeLock ReconnectionScopeLock(ReconnectionListLock); for (int32 SourceIndex = ExternalSourcesToReconnect.Num() - 1; SourceIndex >= 0; --SourceIndex) { // If the source no longer exists, then there is no point in trying to reconnect. if (InvalidExternalSourceIds.Contains(ExternalSourcesToReconnect[SourceIndex]->GetSourceHandle())) { ExternalSourcesToReconnect.RemoveAtSwap(SourceIndex); } } for (const TPair>& ExternalSourceKeyValue : DirectLinkSourceToExternalSourceMap) { const TSharedRef& ExternalSource = ExternalSourceKeyValue.Value; if (ExternalSource->IsStreamOpen() && !ActiveStreamsSources.Contains(ExternalSourceKeyValue.Key)) { // Lost connection, update the external source state and try to reconnect. ExternalSource->CloseStream(); if (!ExternalSource->OpenStream()) { // Could not reopen the stream, retry later. ExternalSourcesToReconnect.Add(ExternalSource); ReconnectionManager->Start(); } } } } // Remove all external sources that are no longer valid. for (const DirectLink::FSourceHandle& SourceHandle : InvalidExternalSourceIds) { InvalidateSource(SourceHandle); } } void FDirectLinkManager::CancelEmptySourcesLoading() const { FRWScopeLock ScopeLock(RawInfoLock, FRWScopeLockType::SLT_ReadOnly); for (const DirectLink::FRawInfo::FStreamInfo& StreamInfo : RawInfoCache.StreamsInfo) { if (const TSharedRef* ExternalSourcePtr = DirectLinkSourceToExternalSourceMap.Find(StreamInfo.Source)) { const TSharedRef& ExternalSource = *ExternalSourcePtr; // We can infer that a DirectLink source is empty (no scene synced) by looking at if its stream is planning to send any data. // #ueent_todo: Ideally it would be better to not allow an AsyncLoad to take place in the first time, but we can't know a source is empty // before actually connecting to it, so this is the best we can do at the current time. const bool bStreamIsEmpty = StreamInfo.ConnectionState == DirectLink::EStreamConnectionState::Active && !StreamInfo.CommunicationStatus.IsTransmitting() && StreamInfo.CommunicationStatus.TaskTotal == 0; if (bStreamIsEmpty && ExternalSource->IsAsyncLoading() && !ExternalSource->GetDatasmithScene().IsValid()) { ExternalSource->CancelAsyncLoad(); UE_LOG(LogDirectLinkManager, Warning, TEXT("The DirectLink source \"%s\" could not be loaded: Nothing to synchronize. Make sure to do a DirectLink sync in your exporter."), *ExternalSource->GetSourceName()); } } } } void FDirectLinkManager::RegisterDirectLinkExternalSource(FDirectLinkExternalSourceRegisterInformation&& RegisterInformation) { RegisteredExternalSourcesInfo.Add(MoveTemp(RegisterInformation)); } void FDirectLinkManager::UnregisterDirectLinkExternalSource(FName InName) { for (int32 InfoIndex = RegisteredExternalSourcesInfo.Num() - 1; InfoIndex >= 0; --InfoIndex) { if (RegisteredExternalSourcesInfo[InfoIndex].Name == InName) { RegisteredExternalSourcesInfo.RemoveAtSwap(InfoIndex); break; } } } DirectLink::FEndpoint& FDirectLinkManager::GetEndpoint() { return *Endpoint.Get(); } void FDirectLinkManager::InvalidateSource(const DirectLink::FSourceHandle& InvalidSourceHandle) { TSharedRef DirectLinkExternalSource = DirectLinkSourceToExternalSourceMap.FindAndRemoveChecked(InvalidSourceHandle); AutoReimportManger->OnExternalSourceInvalidated(DirectLinkExternalSource); DirectLinkExternalSource->Invalidate(); } TArray> FDirectLinkManager::GetExternalSourceList() const { TArray> ExternalSources; DirectLinkSourceToExternalSourceMap.GenerateValueArray(ExternalSources); return ExternalSources; } FSourceUri FDirectLinkManager::GetUriFromSourceHandle(const DirectLink::FSourceHandle& SourceHandle) { FRWScopeLock ScopeLock(RawInfoLock, FRWScopeLockType::SLT_ReadOnly); if (const DirectLink::FRawInfo::FDataPointInfo* SourceInfo = RawInfoCache.DataPointsInfo.Find(SourceHandle)) { const FString SourceName(SourceInfo->Name); if (DirectLink::FRawInfo::FEndpointInfo* EndpointInfo = RawInfoCache.EndpointsInfo.Find(SourceInfo->EndpointAddress)) { const FString EndpointName(EndpointInfo->Name); const FString UriPath(EndpointInfo->ComputerName / EndpointInfo->ExecutableName / EndpointInfo->Name / SourceName); TMap UriQuery = { {FDirectLinkUriResolver::GetSourceIdPropertyName(), LexToString(SourceHandle)} }; return FSourceUri(FDirectLinkUriResolver::GetDirectLinkScheme(), UriPath, UriQuery); } } return FSourceUri(); } } #undef LOCTEXT_NAMESPACE