// Copyright Epic Games, Inc. All Rights Reserved. #include "Perforce.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Algo/Sort.h" #include "Algo/Transform.h" #include "HAL/PlatformProcess.h" #include "HAL/Event.h" #include "Utility.h" namespace UGSCore { bool TryGetValue(const TMap& Map, const TCHAR* Key, FString& OutValue) { const FString* Value = Map.Find(Key); if(Value == nullptr) { OutValue = FString(); return false; } else { OutValue = *Value; return true; } } TArray Split(const FString& Text, const TCHAR* Delim, bool bRemoveEmptyEntries = false) { TArray Result; Text.ParseIntoArray(Result, Delim, bRemoveEmptyEntries); return Result; } FString GetTempFileName() { return FPaths::EngineIntermediateDir() / FGuid::NewGuid().ToString(); } struct FPerforceExe { bool bValidP4 = false; // Default assumed locations, we will try to find the real path #if PLATFORM_WINDOWS FString ExecutablePath = TEXT("C:\\Program Files (x86)\\Perforce\\p4.exe"); #else FString ExecutablePath = TEXT("/usr/bin/p4"); #endif FString GetPerforceExe() { static bool bExecutablePathCached = false; if (bExecutablePathCached) { return ExecutablePath; } bExecutablePathCached = true; #if PLATFORM_MAC || PLATFORM_LINUX int32 ReturnCode = -1; FString OutResults; FString OutErrors; #if PLATFORM_MAC FPlatformProcess::ExecProcess(TEXT("/usr/bin/mdfind"), TEXT("\"kMDItemFSName = 'p4' && kMDItemContentType = 'public.unix-executable'\""), &ReturnCode, &OutResults, &OutErrors); // in case it returns multiple p4's, pick the first one OutResults = Split(OutResults, LINE_TERMINATOR)[0]; #elif PLATFORM_LINUX FPlatformProcess::ExecProcess(TEXT("which"), TEXT("p4"), &ReturnCode, &OutResults, &OutErrors); #endif // PLATFORM_MAC if (ReturnCode != 0) { // Todo: Log OutErrors somehow (should this take function the output log?) // for now just return our default paths return ExecutablePath; } ExecutablePath = OutResults.TrimEnd(); #endif // PLATFORM_MAC || PLATFORM_LINUX return ExecutablePath; } inline int InnerRunCommand(const FString& CommandLine, TArray& OutLines, FEvent* AbortEvent, const FString WriteChildText = FString()) { void* ParentReadPipe = nullptr; void* ParentWritePipe = nullptr; void* ChildReadPipe = nullptr; void* ChildWritePipe = nullptr; // Create pipe for reading from child stdout FPlatformProcess::CreatePipe(ParentReadPipe, ChildWritePipe); // Create pipe for writing to child stdin const bool bWriting = !WriteChildText.IsEmpty(); if (bWriting) { FPlatformProcess::CreatePipe(ChildReadPipe, ParentWritePipe); } FProcHandle P4Proc = FPlatformProcess::CreateProc(*GetPerforceExe(), *CommandLine, false, true, true, nullptr, 0, nullptr, ChildWritePipe, ChildReadPipe); // Write to child process's stdin if (FPlatformProcess::IsProcRunning(P4Proc) && bWriting) { FPlatformProcess::WritePipe(ParentWritePipe, WriteChildText); } // Done writing FPlatformProcess::ClosePipe(ChildReadPipe, ParentWritePipe); // Read from child process's stdout FString P4Output; FString LatestOutput = FPlatformProcess::ReadPipe(ParentReadPipe); while (FPlatformProcess::IsProcRunning(P4Proc) || !LatestOutput.IsEmpty()) { P4Output += LatestOutput; LatestOutput = FPlatformProcess::ReadPipe(ParentReadPipe); if (AbortEvent->Wait(FTimespan::Zero())) { FPlatformProcess::TerminateProc(P4Proc); return -1; } } // Done reading FPlatformProcess::ClosePipe(ParentReadPipe, ChildWritePipe); OutLines = Split(P4Output, LINE_TERMINATOR); int ExitCode = -1; const bool bGotReturnCode = FPlatformProcess::GetProcReturnCode(P4Proc, &ExitCode); if (bGotReturnCode) { return ExitCode; } return -1; } int RunCommand(const FString& CommandLine, TArray& OutLines, FEvent* AbortEvent, const FString& WritePipeText = FString()) { if (bValidP4) { return InnerRunCommand(CommandLine, OutLines, AbortEvent, WritePipeText); } return -1; } void VerifyPerforcePath() { #if PLATFORM_WINDOWS bValidP4 = FPaths::FileExists(ExecutablePath); if (!bValidP4) { ExecutablePath.ReplaceInline(TEXT(" (x86)"), TEXT("")); bValidP4 = FPaths::FileExists(ExecutablePath); } else #endif { bValidP4 = true; } } FPerforceExe() { VerifyPerforcePath(); } }; static FPerforceExe GPerforceExe; //// FPerforceChangeSummary //// FPerforceChangeSummary::FPerforceChangeSummary() : Number(0) , Date(0) { } //// FPerforceFileChangeSummary //// FPerforceFileChangeSummary::FPerforceFileChangeSummary() : Revision(0) , ChangeNumber(0) , Date(0) { } //// FPerforceClientRecord //// FPerforceClientRecord::FPerforceClientRecord(const TMap& Tags) { TryGetValue(Tags, TEXT("client"), Name); TryGetValue(Tags, TEXT("Owner"), Owner); TryGetValue(Tags, TEXT("Host"), Host); TryGetValue(Tags, TEXT("Root"), Root); } //// FPerforceDescribeFileRecord //// FPerforceDescribeFileRecord::FPerforceDescribeFileRecord() : Revision(0) , FileSize(0) { } //// FPerforceDescribeRecord //// FPerforceDescribeRecord::FPerforceDescribeRecord(const TMap& Tags) { FString ChangeString; if(!TryGetValue(Tags, TEXT("change"), ChangeString) || !FUtility::TryParse(*ChangeString, ChangeNumber)) { ChangeNumber = -1; } TryGetValue(Tags, TEXT("user"), User); TryGetValue(Tags, TEXT("client"), Client); FString TimeString; if(!TryGetValue(Tags, TEXT("time"), TimeString) || !FUtility::TryParse(*TimeString, Time)) { Time = -1; } TryGetValue(Tags, TEXT("desc"), Description); TryGetValue(Tags, TEXT("status"), Status); TryGetValue(Tags, TEXT("changeType"), ChangeType); TryGetValue(Tags, TEXT("path"), Path); for(int Idx = 0;;Idx++) { FString Suffix = FString::Printf(TEXT("%d"), Idx); FPerforceDescribeFileRecord File; if(!TryGetValue(Tags, *(FString(TEXT("depotFile")) + Suffix), File.DepotFile)) { break; } TryGetValue(Tags, *(FString(TEXT("action")) + Suffix), File.Action); TryGetValue(Tags, *(FString(TEXT("type")) + Suffix), File.Type); FString RevisionString; if(!TryGetValue(Tags, *(FString(TEXT("rev")) + Suffix), RevisionString) || !FUtility::TryParse(*RevisionString, File.Revision)) { File.Revision = -1; } FString FileSizeString; if(!TryGetValue(Tags, *(FString(TEXT("fileSize")) + Suffix), FileSizeString) || !FUtility::TryParse(*FileSizeString, File.FileSize)) { File.FileSize = -1; } TryGetValue(Tags, *(FString(TEXT("digest")) + Suffix), File.Digest); Files.Add(File); } } //// FPerforceInfoRecord //// FPerforceInfoRecord::FPerforceInfoRecord(const TMap& Tags) : ServerTimeZone(0, 0, 0) { TryGetValue(Tags, TEXT("userName"), UserName); TryGetValue(Tags, TEXT("clientHost"), HostName); TryGetValue(Tags, TEXT("clientAddress"), ClientAddress); TryGetValue(Tags, TEXT("serverAddress"), ServerAddress); FString ServerDateTime; if(TryGetValue(Tags, TEXT("serverDate"), ServerDateTime)) { TArray Fields; ServerDateTime.ParseIntoArray(Fields, TEXT(" ")); if(Fields.Num() >= 3) { int32 Offset; if(FUtility::TryParse(*Fields[2], Offset)) { ServerTimeZone = (Offset < 0)? -FTimespan(-Offset / 100, -Offset % 100, 0) : FTimespan(Offset / 100, Offset % 100, 0); } } } } //// FPerforceFileRecord //// FPerforceFileRecord::FPerforceFileRecord(const TMap& Tags) : Revision(0) { TryGetValue(Tags, TEXT("depotFile"), DepotPath); TryGetValue(Tags, TEXT("clientFile"), ClientPath); TryGetValue(Tags, TEXT("path"), Path); if(!TryGetValue(Tags, TEXT("action"), Action)) { TryGetValue(Tags, TEXT("headAction"), Action); } IsMapped = Tags.Contains(TEXT("isMapped")); Unmap = Tags.Contains(TEXT("unmap")); FString RevisionString; if(TryGetValue(Tags, TEXT("rev"), RevisionString)) { FUtility::TryParse(*RevisionString, Revision); } } //// FPerforceTagRecordParser //// FPerforceTagRecordParser::FPerforceTagRecordParser(TFunction&)> InOutputRecord) : OutputRecord(InOutputRecord) { } FPerforceTagRecordParser::~FPerforceTagRecordParser() { Flush(); } void FPerforceTagRecordParser::OutputLine(const FString& Line) { int SpaceIdx; if(!Line.FindChar(TEXT(' '), SpaceIdx)) { SpaceIdx = INDEX_NONE; } FString Key = (SpaceIdx > 0)? Line.Left(SpaceIdx) : Line; if(Tags.Contains(Key)) { OutputRecord(Tags); Tags.Empty(); } Tags.FindOrAdd(Key) = (SpaceIdx > 0)? Line.Mid(SpaceIdx + 1) : TEXT(""); } void FPerforceTagRecordParser::Flush() { if(Tags.Num() > 0) { OutputRecord(Tags); Tags.Empty(); } } //// FPerforceSyncOptions //// FPerforceSyncOptions::FPerforceSyncOptions() : NumRetries(0) , NumThreads(0) , TcpBufferSize(0) { } //// FPerforceSpec //// const TCHAR* FPerforceSpec::GetField(const TCHAR* Name) const { for(const TTuple& Section : Sections) { if(Section.Key == Name) { return *Section.Value; } } return nullptr; } void FPerforceSpec::SetField(const TCHAR* Name, const TCHAR* Value) { for(TTuple& Section : Sections) { if(Section.Key == Name) { Section.Value = Value; return; } } Sections.Add(TTuple(Name, Value)); } bool FPerforceSpec::TryParse(const TArray& Lines, TSharedPtr& OutSpec, FOutputDevice& Log) { TSharedPtr Spec = MakeShared(); for(int LineIdx = 0; LineIdx < Lines.Num(); LineIdx++) { FString Line = Lines[LineIdx].TrimStartAndEnd(); if(Line.Len() > 0 && Lines[LineIdx][0] != '#') { // Read the section name int SeparatorIdx; if(!Lines[LineIdx].FindChar(':', SeparatorIdx)) { SeparatorIdx = -1; } if(SeparatorIdx == -1 || !FChar::IsAlpha(Lines[LineIdx][0])) { Log.Logf(TEXT("Invalid spec format at line %d: \"%s\""), LineIdx, *Lines[LineIdx]); return false; } // Get the section name FString SectionName = Lines[LineIdx].Mid(0, SeparatorIdx); // Parse the section value FString Value = Lines[LineIdx].Mid(SeparatorIdx + 1).TrimStartAndEnd(); for(; LineIdx + 1 < Lines.Num(); LineIdx++) { if(Lines[LineIdx + 1].Len() == 0) { Value += TEXT("\n"); } else if(Lines[LineIdx + 1][0] == '\t') { Value += Lines[LineIdx + 1].Mid(1) + TEXT("\n"); } else { break; } } Value.TrimEndInline(); Spec->Sections.Add(TTuple(SectionName, Value)); } } OutSpec = Spec; return true; } FString FPerforceSpec::ToString() const { FString Result; for(const TTuple& Section : Sections) { if(Section.Value.Contains(TEXT("\n"))) { Result += Section.Key + TEXT(":\n\t") + Section.Value.Replace(TEXT("\n"), TEXT("\n\t")) + LINE_TERMINATOR; } else { Result += Section.Key + TEXT(":\t") + Section.Value + LINE_TERMINATOR; } Result += LINE_TERMINATOR; } return Result; } //// FPerforceOutputLine //// FPerforceOutputLine::FPerforceOutputLine() : Channel(EPerforceOutputChannel::Unknown) { } FPerforceOutputLine::FPerforceOutputLine(EPerforceOutputChannel InChannel, const FString& InText) : Channel(InChannel) , Text(InText) { } //// FPerforceConnection //// FPerforceConnection::FPerforceConnection(const TCHAR* InUserName, const TCHAR* InClientName, const TCHAR* InServerAndPort) : ServerAndPort((InServerAndPort != nullptr)? InServerAndPort : TEXT("")) , UserName((InUserName != nullptr)? InUserName : TEXT("")) , ClientName((InClientName != nullptr)? InClientName : TEXT("")) { } TSharedRef FPerforceConnection::OpenClient(const FString& NewClientName) const { return MakeShared(*UserName, *NewClientName, *ServerAndPort); } bool FPerforceConnection::Info(TSharedPtr& OutInfo, FEvent* AbortEvent, FOutputDevice& Log) const { TArray> TagRecords; if(!RunCommand(TEXT("info -s"), TagRecords, ECommandOptions::NoClient, AbortEvent, Log) || TagRecords.Num() != 1) { OutInfo = TSharedPtr(); return false; } else { OutInfo = MakeShared(TagRecords[0]); return true; } } bool FPerforceConnection::GetSetting(const FString& Name, FString& Value, FEvent* AbortEvent, FOutputDevice& Log) const { TArray Lines; if(!RunCommand(FString::Printf(TEXT("set %s"), *Name), Lines, ECommandOptions::NoChannels, AbortEvent, Log) || Lines.Num() != 1) { Value = FString(); return false; } if(Lines[0].Len() <= Name.Len() || !Lines[0].StartsWith(Name) || Lines[0][Name.Len()] != '=') { Value = FString(); return false; } Value = Lines[0].Mid(Name.Len() + 1); int EndIdx = Value.Find(TEXT(" (")); if(EndIdx != INDEX_NONE) { Value = Value.Mid(0, EndIdx).TrimStartAndEnd(); } return true; } bool FPerforceConnection::FindClients(TArray& Clients, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(TEXT("clients"), Clients, ECommandOptions::NoClient, AbortEvent, Log); } bool FPerforceConnection::FindClients(TArray& Clients, const FString& ForUserName, FEvent* AbortEvent, FOutputDevice& Log) const { TArray Lines; if(!RunCommand(FString::Printf(TEXT("clients -u%s"), *ForUserName), Clients, ECommandOptions::NoClient, AbortEvent, Log)) { return false; } for(const FString& Line : Lines) { TArray Tokens = Split(Line, TEXT(" "), true); if(Tokens.Num() < 5 || Tokens[0] != TEXT("Client") || Tokens[3] != TEXT("root")) { Log.Logf(TEXT("Couldn't parse client from line '%s'"), *Line); } } return true; } bool FPerforceConnection::TryGetClientSpec(const FString& InClientName, TSharedPtr& OutSpec, FEvent* AbortEvent, FOutputDevice& Log) const { TArray Lines; if(!RunCommand(FString::Printf(TEXT("client -o %s"), *InClientName), Lines, ECommandOptions::None, AbortEvent, Log)) { OutSpec = nullptr; return false; } if(!FPerforceSpec::TryParse(Lines, OutSpec, Log)) { OutSpec = nullptr; return false; } return true; } bool FPerforceConnection::CreateClient(const FPerforceClientRecord& Client, const FString& Stream, FEvent* AbortEvent, FOutputDevice& Log) const { const FString PipeInput = FString::Printf( TEXT("Client: %s\nStream: %s\nRoot: %s\nOwner: %s\nHost: %s\nDescription: Created by %s in SlateUGS\n"), *Client.Name, *Stream, *Client.Root, *Client.Owner, *Client.Host, *Client.Owner); return RunCommand(TEXT("client -i"), ECommandOptions::None, AbortEvent, Log, PipeInput); } bool FPerforceConnection::TryGetStreamSpec(const FString& StreamName, TSharedPtr& OutSpec, FEvent* AbortEvent, FOutputDevice& Log) const { TArray Lines; if(!RunCommand(FString::Printf(TEXT("stream -o %s"), *StreamName), Lines, ECommandOptions::None, AbortEvent, Log)) { OutSpec = nullptr; return false; } if(!FPerforceSpec::TryParse(Lines, OutSpec, Log)) { OutSpec = nullptr; return false; } return true; } bool FPerforceConnection::FindFiles(const FString& Filter, TArray& OutFileRecords, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("fstat \"%s\""), *Filter), OutFileRecords, ECommandOptions::None, AbortEvent, Log); } bool FPerforceConnection::Print(const FString& DepotPath, TArray& OutLines, FEvent* AbortEvent, FOutputDevice& Log) const { FString TempFileName = GetTempFileName(); if(!PrintToFile(DepotPath, TempFileName, AbortEvent, Log)) { return false; } else { return FFileHelper::LoadFileToStringArray(OutLines, *TempFileName); } } bool FPerforceConnection::PrintToFile(const FString& DepotPath, const FString& OutputFileName, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("print -q -o \"%s\" \"%s\""), *OutputFileName, *DepotPath), ECommandOptions::None, AbortEvent, Log); } bool FPerforceConnection::FileExists(const FString& Filter, bool& bExists, FEvent* AbortEvent, FOutputDevice& Log) const { TArray FileRecords; if(RunCommand(FString::Printf(TEXT("fstat \"%s\""), *Filter), FileRecords, ECommandOptions::IgnoreNoSuchFilesError | ECommandOptions::IgnoreFilesNotInClientViewError, AbortEvent, Log)) { bExists = (FileRecords.Num() > 0); return true; } else { bExists = false; return false; } } bool FPerforceConnection::FindChanges(const FString& Filter, int MaxResults, TArray& OutChanges, FEvent* AbortEvent, FOutputDevice& Log) const { TArray Filters; Filters.Add(Filter); return FindChanges(Filters, MaxResults, OutChanges, AbortEvent, Log); } bool FPerforceConnection::FindChanges(const TArray& Filters, int MaxResults, TArray& OutChanges, FEvent* AbortEvent, FOutputDevice& Log) const { OutChanges.Empty(); FString Arguments = TEXT("changes -s submitted -t -L"); if(MaxResults > 0) { Arguments += FString::Printf(TEXT(" -m %d"), MaxResults); } for(const FString& Filter : Filters) { Arguments += FString::Printf(TEXT(" \"%s\""), *Filter); } TArray Lines; if(!RunCommand(Arguments, Lines, ECommandOptions::None, AbortEvent, Log)) { return false; } for(int Idx = 0; Idx < Lines.Num(); Idx++) { FPerforceChangeSummary Change; if(!TryParseChangeSummary(Lines[Idx], Change)) { Log.Logf(TEXT("Couldn't parse description from '%s'"), *Lines[Idx]); } else { for(; Idx + 1 < Lines.Num(); Idx++) { if(Lines[Idx + 1].Len() == 0) { Change.Description += TEXT("\n"); } else if(Lines[Idx + 1].StartsWith("\t")) { Change.Description += Lines[Idx + 1].Mid(1) + TEXT("\n"); } else { break; } } Change.Description = Change.Description.TrimStartAndEnd(); OutChanges.Add(Change); } } Algo::Sort(OutChanges, [](const FPerforceChangeSummary& A, const FPerforceChangeSummary& B){ return A.Number > B.Number; }); for(int Idx = OutChanges.Num() - 1; Idx > 0; Idx--) { if(OutChanges[Idx - 1].Number == OutChanges[Idx].Number) { OutChanges.RemoveAt(Idx); } } if(MaxResults >= 0 && MaxResults < OutChanges.Num()) { OutChanges.RemoveAt(MaxResults, OutChanges.Num() - MaxResults); } return true; } bool FPerforceConnection::TryParseChangeSummary(const FString& Line, FPerforceChangeSummary& Change) const { TArray Tokens = Split(Line, TEXT(" "), true); if(Tokens.Num() == 7 && Tokens[0] == "Change" && Tokens[2] == "on" && Tokens[5] == "by") { if(FUtility::TryParse(*Tokens[1], Change.Number) && FPerforceUtils::TryParseDateTime(Tokens[3], Tokens[4], Change.Date)) { int UserClientIdx; if(Tokens[6].FindChar(TEXT('@'), UserClientIdx)) { Change.User = Tokens[6].Mid(0, UserClientIdx); Change.Client = Tokens[6].Mid(UserClientIdx + 1); return true; } } } return false; } bool FPerforceConnection::FindFileChanges(const FString& FilePath, int MaxResults, TArray& Changes, FEvent* AbortEvent, FOutputDevice& Log) const { FString Arguments = TEXT("filelog -L -t"); if(MaxResults > 0) { Arguments += FString::Printf(TEXT(" -m %d"), MaxResults); } Arguments += FString::Printf(TEXT(" \"%s\""), *FilePath); TArray Lines; if(!RunCommand(Arguments, EPerforceOutputChannel::TaggedInfo, Lines, ECommandOptions::None, AbortEvent, Log)) { return false; } for(int Idx = 0; Idx < Lines.Num(); Idx++) { FPerforceFileChangeSummary Change; if(!TryParseFileChangeSummary(Lines, Idx, Change)) { Log.Logf(TEXT("Couldn't parse description from '%s'"), *Lines[Idx]); } else { Changes.Add(Change); } } return true; } bool FPerforceConnection::TryParseFileChangeSummary(const TArray& Lines, int& LineIdx, FPerforceFileChangeSummary& OutChange) const { TArray Tokens = Split(Lines[LineIdx].TrimStartAndEnd(), TEXT(" "), true); if(Tokens.Num() != 10 || !Tokens[0].StartsWith(TEXT("#")) || Tokens[1] != TEXT("change") || Tokens[4] != TEXT("on") || Tokens[7] != TEXT("by")) { return false; } // Replace [5] date and [6] time with . as FDateTime expects format to be (yyyy.mm.dd-hh.mm.ss) Tokens[5].ReplaceCharInline('/', '.'); Tokens[6].ReplaceCharInline(':', '.'); if(!FUtility::TryParse(*Tokens[0].Mid(1), OutChange.Revision) || !FUtility::TryParse(*Tokens[2], OutChange.ChangeNumber) || !FDateTime::Parse(Tokens[5] + TEXT("-") + Tokens[6], OutChange.Date)) { return false; } int UserClientIdx; if(!Tokens[8].FindChar('@', UserClientIdx)) { return false; } OutChange.Action = Tokens[3]; OutChange.Type = Tokens[9]; if(OutChange.Type.StartsWith(TEXT("("))) { OutChange.Type = OutChange.Type.Mid(1); } if(OutChange.Type.EndsWith(TEXT(")"))) { OutChange.Type = OutChange.Type.Left(OutChange.Type.Len() - 1); } OutChange.User = Tokens[8].Mid(0, UserClientIdx); OutChange.Client = Tokens[8].Mid(UserClientIdx + 1); FString Description; for(; LineIdx + 1 < Lines.Num(); LineIdx++) { if(Lines[LineIdx + 1].Len() == 0) { Description += TEXT("\n"); } else if(Lines[LineIdx + 1].StartsWith("\t")) { Description += Lines[LineIdx + 1].Mid(1) + TEXT("\n"); } else { break; } } Description.TrimStartAndEndInline(); OutChange.Description = Description; return true; } bool FPerforceConnection::ConvertToClientPath(const FString& FileName, FString& OutClientFileName, FEvent* AbortEvent, FOutputDevice& Log) const { TSharedPtr WhereRecord; if(Where(FileName, WhereRecord, AbortEvent, Log)) { OutClientFileName = WhereRecord->ClientPath; return true; } else { OutClientFileName = TEXT(""); return false; } } bool FPerforceConnection::ConvertToDepotPath(const FString& FileName, FString& OutDepotFileName, FEvent* AbortEvent, FOutputDevice& Log) const { TSharedPtr WhereRecord; if(Where(FileName, WhereRecord, AbortEvent, Log)) { OutDepotFileName = WhereRecord->DepotPath; return true; } else { OutDepotFileName = TEXT(""); return false; } } bool FPerforceConnection::ConvertToLocalPath(const FString& FileName, FString& OutLocalFileName, FEvent* AbortEvent, FOutputDevice& Log) const { TSharedPtr WhereRecord; if(Where(FileName, WhereRecord, AbortEvent, Log)) { OutLocalFileName = FUtility::GetPathWithCorrectCase(*WhereRecord->LocalPath); return true; } else { OutLocalFileName = TEXT(""); return false; } } bool FPerforceConnection::FindStreams(const FString& Filter, TArray& OutStreamNames, FEvent* AbortEvent, FOutputDevice& Log) const { TArray Lines; if(!RunCommand(FString::Printf(TEXT("streams -F \"Stream=%s\""), *Filter), Lines, ECommandOptions::None, AbortEvent, Log)) { OutStreamNames.Empty(); return false; } TArray StreamNames; for(const FString& Line : Lines) { TArray Tokens = Split(Line, TEXT(" "), true); if(Tokens.Num() < 2 || Tokens[0] != TEXT("Stream") || !Tokens[1].StartsWith(TEXT("//"))) { Log.Logf(TEXT("Unexpected output from stream query: %s"), *Line); OutStreamNames.Empty(); return false; } StreamNames.Add(Tokens[1]); } OutStreamNames = StreamNames; return true; } bool FPerforceConnection::HasOpenFiles(FEvent* AbortEvent, FOutputDevice& Log) const { TArray Records; bool bResult = RunCommand(TEXT("opened -m 1"), Records, ECommandOptions::None, AbortEvent, Log); return bResult && Records.Num() > 0; } bool FPerforceConnection::SwitchStream(const FString& NewStream, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("client -f -s -S \"%s\" \"%s\""), *NewStream, *ClientName), ECommandOptions::None, AbortEvent, Log); } bool FPerforceConnection::Describe(int ChangeNumber, TSharedPtr& OutRecord, FEvent* AbortEvent, FOutputDevice& Log) const { FString CommandLine = FString::Printf(TEXT("describe -s %d"), ChangeNumber); TArray> Records; if(!RunCommandWithBinaryOutput(CommandLine, Records, ECommandOptions::None, AbortEvent, Log)) { OutRecord = nullptr; return false; } if(Records.Num() != 1) { Log.Logf(TEXT("Expected 1 record from p4 %s, got %d"), *CommandLine, Records.Num()); OutRecord = nullptr; return false; } FString Code; if(!TryGetValue(Records[0], TEXT("code"), Code) || Code != "stat") { Log.Logf(TEXT("Unexpected response from p4 %s (code=%s)"), *CommandLine, *Code); OutRecord = nullptr; return false; } OutRecord = MakeShared(Records[0]); return true; } bool FPerforceConnection::Where(const FString& Filter, TSharedPtr& OutWhereRecord, FEvent* AbortEvent, FOutputDevice& Log) const { TArray FileRecords; if(!RunCommand(FString::Printf(TEXT("where \"%s\""), *Filter), FileRecords, ECommandOptions::None, AbortEvent, Log)) { OutWhereRecord = nullptr; return false; } FileRecords.RemoveAll([](const FPerforceFileRecord& Record){ return Record.Unmap; }); if(FileRecords.Num() == 0) { Log.Logf(TEXT("'%s' is not mapped to workspace."), *Filter); OutWhereRecord = nullptr; return false; } else if(FileRecords.Num() > 1) { Log.Logf(TEXT("File is mapped to %d locations:"), FileRecords.Num()); for(const FPerforceFileRecord& FileRecord : FileRecords) { Log.Logf(TEXT(" %s"), *FileRecord.Path); } OutWhereRecord = nullptr; return false; } OutWhereRecord = MakeShared(); OutWhereRecord->LocalPath = FileRecords[0].Path; OutWhereRecord->DepotPath = FileRecords[0].DepotPath; OutWhereRecord->ClientPath = FileRecords[0].ClientPath; return true; } bool FPerforceConnection::Have(const FString& Filter, TArray& OutFileRecords, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("have \"%s\""), *Filter), OutFileRecords, ECommandOptions::None, AbortEvent, Log); } bool FPerforceConnection::Stat(const FString& Filter, TArray& OutFileRecords, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("fstat \"%s\""), *Filter), OutFileRecords, ECommandOptions::None, AbortEvent, Log); } bool FPerforceConnection::Sync(const FString& Filter, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString(TEXT("sync ")) + Filter, ECommandOptions::IgnoreFilesUpToDateError, AbortEvent, Log); } bool FPerforceConnection::Sync(const TArray& DepotPaths, int ChangeNumber, TFunction SyncOutput, TArray& OutTamperedFiles, const FPerforceSyncOptions* Options, FEvent* AbortEvent, FOutputDevice& Log) const { // Write all the files we want to sync to a temp file TArray SyncFiles; for(const FString& DepotPath : DepotPaths) { SyncFiles.Add(FString::Printf(TEXT("%s@%d"), *DepotPath, ChangeNumber)); } FString TempFileName = GetTempFileName(); FFileHelper::SaveStringArrayToFile(SyncFiles, *TempFileName); // Create a filter to strip all the sync records FPerforceTagRecordParser Parser([SyncOutput](const TMap& Tags){ SyncOutput(FPerforceFileRecord(Tags)); }); FString CommandLine; CommandLine += FString::Printf(TEXT("-x \"%s\" -z tag"), *TempFileName); if(Options != nullptr && Options->NumRetries > 0) { CommandLine += FString::Printf(TEXT(" -r %d"), Options->NumRetries); } if(Options != nullptr && Options->TcpBufferSize > 0) { CommandLine += FString::Printf(TEXT(" -v net.tcpsize=%d"), Options->TcpBufferSize); } CommandLine += TEXT(" sync"); if(Options != nullptr && Options->NumThreads > 1) { CommandLine += FString::Printf(TEXT(" --parallel=threads=%d"), Options->NumThreads); } return RunCommand(CommandLine, nullptr, [&Parser, &OutTamperedFiles, &Log](const FPerforceOutputLine& Line){ return FilterSyncOutput(Line, Parser, OutTamperedFiles, Log); }, ECommandOptions::NoFailOnErrors | ECommandOptions::IgnoreFilesUpToDateError | ECommandOptions::IgnoreExitCode, AbortEvent, Log); } bool FPerforceConnection::LatestChangeList(int& OutChangeList, FEvent* AbortEvent, FOutputDevice& Log) const { TArray> OutLatestChange; RunCommand(TEXT("changes -m 1"), OutLatestChange, ECommandOptions::None, AbortEvent, Log); if (OutLatestChange.Num() == 1) { FString StringChangeList; if (TryGetValue(OutLatestChange[0], TEXT("change"), StringChangeList)) { return FUtility::TryParse(*StringChangeList, OutChangeList); } } return false; } bool FPerforceConnection::FilterSyncOutput(const FPerforceOutputLine& Line, FPerforceTagRecordParser& Parser, TArray& OutTamperedFiles, FOutputDevice& Log) { if(Line.Channel == EPerforceOutputChannel::TaggedInfo) { Parser.OutputLine(Line.Text); return true; } Log.Logf(TEXT("%s"), *Line.Text); static const FString Prefix = TEXT("Can't clobber writable file "); if(Line.Channel == EPerforceOutputChannel::Error && Line.Text.StartsWith(Prefix)) { OutTamperedFiles.Add(Line.Text.Mid(Prefix.Len()).TrimStartAndEnd()); return true; } return Line.Channel != EPerforceOutputChannel::Error; } void FPerforceConnection::ParseTamperedFile(const FString& Line, TArray& OutTamperedFiles) { static const FString Prefix = TEXT("Can't clobber writable file "); if(Line.StartsWith(Prefix)) { OutTamperedFiles.Add(Line.Mid(Prefix.Len()).TrimStartAndEnd()); } } bool FPerforceConnection::SyncPreview(const FString& Filter, int ChangeNumber, bool bOnlyFilesInThisChange, TArray& OutFileRecords, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("sync -n %s@%s%d"), *Filter, bOnlyFilesInThisChange? TEXT("=") : TEXT(""), ChangeNumber), OutFileRecords, ECommandOptions::IgnoreFilesUpToDateError | ECommandOptions::IgnoreNoSuchFilesError, AbortEvent, Log); } bool FPerforceConnection::ForceSync(const FString& Filter, int ChangeNumber, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("sync -f \"%s\"@%d"), *Filter, ChangeNumber), ECommandOptions::IgnoreFilesUpToDateError, AbortEvent, Log); } bool FPerforceConnection::GetOpenFiles(const FString& Filter, TArray& OutFileRecords, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("opened \"%s\""), *Filter), OutFileRecords, ECommandOptions::None, AbortEvent, Log); } bool FPerforceConnection::GetUnresolvedFiles(const FString& Filter, TArray& OutFileRecords, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("fstat -Ru \"%s\""), *Filter), OutFileRecords, ECommandOptions::IgnoreNoSuchFilesError | ECommandOptions::IgnoreFilesNotOpenedOnThisClientError, AbortEvent, Log); } bool FPerforceConnection::AutoResolveFile(const FString& File, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommand(FString::Printf(TEXT("resolve -am %s"), *File), ECommandOptions::None, AbortEvent, Log); } bool FPerforceConnection::GetActiveStream(FString& OutStreamName, FEvent* AbortEvent, FOutputDevice& Log) const { TSharedPtr ClientSpec; if(TryGetClientSpec(ClientName, ClientSpec, AbortEvent, Log)) { OutStreamName = ClientSpec->GetField(TEXT("Stream")); return OutStreamName.Len() > 0; } else { OutStreamName.Empty(); return false; } } bool FPerforceConnection::RunCommand(const FString& CommandLine, TArray& OutFileRecords, ECommandOptions Options, FEvent* AbortEvent, FOutputDevice& Log) const { TArray> TagRecords; if(!RunCommand(CommandLine, TagRecords, Options, AbortEvent, Log)) { OutFileRecords.Empty(); return false; } else { Algo::Transform(TagRecords, OutFileRecords, [](const TMap& Tags) -> FPerforceFileRecord { return FPerforceFileRecord(Tags); }); return true; } } bool FPerforceConnection::RunCommand(const FString& CommandLine, TArray& OutClientRecords, ECommandOptions Options, FEvent* AbortEvent, FOutputDevice& Log) const { TArray> TagRecords; if(!RunCommand(CommandLine, TagRecords, Options, AbortEvent, Log)) { UE_LOG(LogUGSCore, Warning, TEXT("RunCommand1 returned false")); return false; } else { Algo::Transform(TagRecords, OutClientRecords, [](const TMap& Tags) -> FPerforceClientRecord { return FPerforceClientRecord(Tags); }); return true; } } bool FPerforceConnection::RunCommand(const FString& CommandLine, TArray>& OutTagRecords, ECommandOptions Options, FEvent* AbortEvent, FOutputDevice& Log) const { TArray Lines; if(!RunCommand(FString(TEXT("-ztag ")) + CommandLine, EPerforceOutputChannel::TaggedInfo, Lines, Options, AbortEvent, Log)) //if(!RunCommand(FString(CommandLine, EPerforceOutputChannel::TaggedInfo, Lines, Options, AbortEvent, Log)) { return false; } FPerforceTagRecordParser Parser([&OutTagRecords](const TMap& Tags){ OutTagRecords.Add(Tags); }); for(const FString& Line : Lines) { Parser.OutputLine(Line); } return true; } bool FPerforceConnection::RunCommand(const FString& CommandLine, ECommandOptions Options, FEvent* AbortEvent, FOutputDevice& Log, const FString& WritePipeText) const { TArray Lines; return RunCommand(CommandLine, Lines, Options, AbortEvent, Log, WritePipeText); } bool FPerforceConnection::RunCommand(const FString& CommandLine, TArray& OutLines, ECommandOptions Options, FEvent* AbortEvent, FOutputDevice& Log, const FString& WritePipeText) const { return RunCommand(CommandLine, EPerforceOutputChannel::Info, OutLines, Options, AbortEvent, Log, WritePipeText); } bool FPerforceConnection::RunCommand(const FString& CommandLine, EPerforceOutputChannel Channel, TArray& OutLines, ECommandOptions Options, FEvent* AbortEvent, FOutputDevice& Log, const FString& WritePipeText) const { FString FullCommandLine = GetFullCommandLine(CommandLine, Options); Log.Logf(TEXT("p4> %s %s"), *FPaths::GetCleanFilename(GPerforceExe.GetPerforceExe()), *FullCommandLine); TArray RawOutputLines; int ExitCode = GPerforceExe.RunCommand(FullCommandLine, RawOutputLines, AbortEvent, WritePipeText); if (ExitCode != 0 && !EnumHasAnyFlags(Options, ECommandOptions::IgnoreExitCode)) { return false; } bool bResult = true; if(EnumHasAnyFlags(Options, ECommandOptions::NoChannels)) { OutLines = RawOutputLines; } else { TArray LocalLines; for(const FString& RawOutputLine : RawOutputLines) { bResult &= ParseCommandOutput(RawOutputLine, [&LocalLines, Channel, &Log](const FPerforceOutputLine& Line){ if(Line.Channel == Channel){ LocalLines.Add(Line.Text); return true; } else { Log.Logf(TEXT("%s"), *Line.Text); return Line.Channel != EPerforceOutputChannel::Error; } }, Options); } OutLines = LocalLines; } return bResult; } bool FPerforceConnection::RunCommand(const FString& CommandLine, const TCHAR* Input, TFunction HandleOutput, ECommandOptions Options, FEvent* AbortEvent, FOutputDevice& Log) const { FString FullCommandLine = GetFullCommandLine(CommandLine, Options); Log.Logf(TEXT("p4> %s %s"), *FPaths::GetCleanFilename(GPerforceExe.GetPerforceExe()), *FullCommandLine); TArray RawOutputLines; int ExitCode = GPerforceExe.RunCommand(FullCommandLine, RawOutputLines, AbortEvent); // TODO check this vs the old way as things *may* have changed. Before they were handling each line as it was coming // back from the process. here we just spin + collect all the output. Then try to process all the output, then if the // exit code was not good return false still. bool bResult = true; FString Line; for(const FString& RawOutputLine : RawOutputLines) { bResult &= ParseCommandOutput(Line, HandleOutput, Options); } if (ExitCode != 0 && !EnumHasAnyFlags(Options, ECommandOptions::IgnoreExitCode)) { bResult = false; } return bResult; } FString FPerforceConnection::GetFullCommandLine(const FString& CommandLine, ECommandOptions Options) const { FString FullCommandLine; if(ServerAndPort.Len() > 0) { FullCommandLine += FString::Printf(TEXT("-p%s "), *ServerAndPort); } if(UserName.Len() > 0) { FullCommandLine += FString::Printf(TEXT("-u%s "), *UserName); } if((Options & ECommandOptions::NoClient) == ECommandOptions::None && ClientName.Len() > 0) { FullCommandLine += FString::Printf(TEXT("-c%s "), *ClientName); } if((Options & ECommandOptions::NoChannels) == ECommandOptions::None) { FullCommandLine += TEXT("-s "); } FullCommandLine += CommandLine; return FullCommandLine; } bool FPerforceConnection::ParseCommandOutput(const FString& Text, TFunction HandleOutput, ECommandOptions Options) const { if(EnumHasAnyFlags(Options, ECommandOptions::NoChannels)) { FPerforceOutputLine Line(EPerforceOutputChannel::Unknown, Text); return HandleOutput(Line); } else if(!IgnoreCommandOutput(Text, Options)) { FPerforceOutputLine Line; if(Text.StartsWith(TEXT("text: "))) { Line = FPerforceOutputLine(EPerforceOutputChannel::Text, Text.Mid(6)); } else if(Text.StartsWith("info: ")) { Line = FPerforceOutputLine(EPerforceOutputChannel::Info, Text.Mid(6)); } else if(Text.StartsWith("info1: ")) { Line = FPerforceOutputLine(IsValidTag(Text, 7)? EPerforceOutputChannel::TaggedInfo : EPerforceOutputChannel::Info, Text.Mid(7)); } else if(Text.StartsWith("warning: ")) { Line = FPerforceOutputLine(EPerforceOutputChannel::Warning, Text.Mid(9)); } else if(Text.StartsWith("error: ")) { Line = FPerforceOutputLine(EPerforceOutputChannel::Error, Text.Mid(7)); } else { Line = FPerforceOutputLine(EPerforceOutputChannel::Unknown, Text); } return HandleOutput(Line) && (Line.Channel != EPerforceOutputChannel::Error || EnumHasAnyFlags(Options, ECommandOptions::NoFailOnErrors)) && Line.Channel != EPerforceOutputChannel::Unknown; } return true; } bool FPerforceConnection::IsValidTag(const FString& Line, int StartIndex) { // Annoyingly, we sometimes get commentary with an info1: prefix. Since it typically starts with a depot or file path, we can pick it out. for(int Idx = StartIndex; Idx < Line.Len() && Line[Idx] != ' '; Idx++) { if(Line[Idx] == '/' || Line[Idx] == '\\') { return false; } } return true; } bool FPerforceConnection::IgnoreCommandOutput(const FString& Text, ECommandOptions Options) { if(Text.StartsWith(TEXT("exit: ")) || Text.StartsWith(TEXT("info2: ")) || Text.Len() == 0) { return true; } else if(EnumHasAnyFlags(Options, ECommandOptions::IgnoreFilesUpToDateError) && Text.StartsWith(TEXT("error: ")) && Text.EndsWith(TEXT("- file(s) up-to-date."))) { return true; } else if(EnumHasAnyFlags(Options, ECommandOptions::IgnoreNoSuchFilesError) && Text.StartsWith(TEXT("error: ")) && Text.EndsWith(TEXT(" - no such file(s)."))) { return true; } else if(EnumHasAnyFlags(Options, ECommandOptions::IgnoreFilesNotInClientViewError) && Text.StartsWith(TEXT("error: ")) && Text.EndsWith(TEXT("- file(s) not in client view."))) { return true; } else if(EnumHasAnyFlags(Options, ECommandOptions::IgnoreFilesNotOpenedOnThisClientError) && Text.StartsWith(TEXT("error: ")) && Text.EndsWith(TEXT(" - file(s) not opened on this client."))) { return true; } return false; } const uint8* FPerforceConnection::ReadBinaryField(const uint8* Pos, const uint8* End, FString* OutField) { // Read the field type if(End - Pos < sizeof(uint8)) { return nullptr; } uint8 KeyFieldType = *Pos; Pos++; // Skip over the field data if(KeyFieldType == 's') { // Read the string length if(End - Pos < sizeof(uint32)) { return nullptr; } uint32 KeyLength; memcpy(&KeyLength, Pos, sizeof(uint32)); Pos += sizeof(uint32); // Read the string data if(End - Pos < KeyLength) { return nullptr; } if(OutField != nullptr) { TStringConversion Converter((const ANSICHAR*)Pos, KeyLength); *OutField = FString(Converter.Length(), Converter.Get()); } Pos += KeyLength; return Pos; } else if(KeyFieldType == 'i') { // Skip over the integer data if(End - Pos < sizeof(uint32)) { return nullptr; } if(OutField != nullptr) { int32 Value; memcpy(&Value, Pos, sizeof(int32)); *OutField = FString::Printf(TEXT("%d"), Value); } Pos += sizeof(uint32); return Pos; } else { checkf(false, TEXT("Invalid field type '%d' (%c)"), (int)*Pos, (TCHAR)*Pos); return nullptr; } } const uint8* FPerforceConnection::ReadBinaryRecord(const uint8* Pos, const uint8* End, TMap* OutRecord) { if(End > Pos && *Pos == '{') { Pos++; while(Pos < End) { // If this is the end of the dictionary, return success if(*Pos == '0') { return Pos + 1; } // Read or skip over the field data if(OutRecord == nullptr) { Pos = ReadBinaryField(Pos, End, nullptr); if(Pos == nullptr) { return nullptr; } Pos = ReadBinaryField(Pos, End, nullptr); if(Pos == nullptr) { return nullptr; } } else { FString Key; Pos = ReadBinaryField(Pos, End, &Key); if(Pos == nullptr) { return nullptr; } FString Value; Pos = ReadBinaryField(Pos, End, &Value); if(Pos == nullptr) { return nullptr; } OutRecord->FindOrAdd(MoveTemp(Key)) = MoveTemp(Value); } } } return nullptr; } /// /// Execute a Perforce command and parse the output as marshalled Python objects. This is more robustly defined than the text-based tagged output /// format, because it avoids ambiguity when returned fields can have newlines. /// /// Command line to execute Perforce with /// List that receives the output records /// Whether to include client information on the command line bool FPerforceConnection::RunCommandWithBinaryOutput(const FString& CommandLine, TArray>& OutRecords, ECommandOptions Options, FEvent* AbortEvent, FOutputDevice& Log) const { return RunCommandWithBinaryOutput(CommandLine, [&OutRecords](const TMap& Record){ OutRecords.Add(Record); return true; }, Options, AbortEvent, Log); } /// /// Execute a Perforce command and parse the output as marshalled Python objects. This is more robustly defined than the text-based tagged output /// format, because it avoids ambiguity when returned fields can have newlines. /// /// Command line to execute Perforce with /// List that receives the output records /// Whether to include client information on the command line bool FPerforceConnection::RunCommandWithBinaryOutput(const FString& CommandLine, TFunction&)> HandleOutput, ECommandOptions Options, FEvent* AbortEvent, FOutputDevice& Log) const { FString FullCommandLine(TEXT("-G ") + CommandLine); uint32 ProcId; void* ChildWritePipe = nullptr; void* ParentReadPipe = nullptr; FPlatformProcess::CreatePipe(ParentReadPipe, ChildWritePipe); FProcHandle P4Proc = FPlatformProcess::CreateProc(*GPerforceExe.GetPerforceExe(), *FullCommandLine, false, true, true, &ProcId, 0, nullptr, ChildWritePipe); size_t BufferPos = 0; size_t BufferEnd = 0; TArray Buffer; TMap Record; for(;;) { // Determine if the process has exited. Do this before reading, so we'll only exit the loop if no data is read AND the process has exited. bool bHasExited = !FPlatformProcess::IsProcRunning(P4Proc); // Check that we don't have an abort event if (AbortEvent->Wait(FTimespan::Zero())) { FPlatformProcess::TerminateProc(P4Proc); return false; //throw FAbortException(); } // Read data from the child process TArray TempBuffer; bool bRead = FPlatformProcess::ReadPipeToArray(ParentReadPipe, TempBuffer); size_t BytesRead = TempBuffer.Num(); // Add new data to the end of the Buffer Buffer += TempBuffer; if(BytesRead == 0) { // If it exited, quit. Otherwise sleep until data is available. if(bHasExited) { break; } else if(AbortEvent->Wait(FTimespan::FromMilliseconds(50))) { FPlatformProcess::TerminateProc(P4Proc); return false; //throw FAbortException(); } } else { // Add the read bytes to the buffer BufferEnd += BytesRead; // Process all the records in the buffer that we can const uint8* RecordPos = Buffer.GetData() + BufferPos; const uint8* RecordMaxPos = Buffer.GetData() + BufferEnd; for(;;) { const uint8* RecordEnd = ReadBinaryRecord(RecordPos, RecordMaxPos, &Record); if(RecordEnd == nullptr) { break; } HandleOutput(Record); RecordPos = RecordEnd; } BufferPos = RecordPos - Buffer.GetData(); // Shrink the buffer down if we've got spare space if(BufferPos > 64) { BufferEnd -= BufferPos; memmove(Buffer.GetData(), Buffer.GetData() + BufferPos, BufferEnd); BufferPos = 0; } } } FPlatformProcess::ClosePipe(ParentReadPipe, ChildWritePipe); int ExitCode = -1; checkf(BufferPos == BufferEnd, TEXT("%d bytes of incomplete record data received from P4"), BufferEnd - BufferPos); FPlatformProcess::GetProcReturnCode(P4Proc, &ExitCode); return ExitCode == 0; } void FPerforceConnection::OpenP4V(const FString& AdditionalArgs) const { #if PLATFORM_WINDOWS const TCHAR* P4VExe = TEXT("p4v.exe"); #else const TCHAR* P4VExe = TEXT("p4v"); #endif FString P4VArgs = GetArgumentsForExternalProgram() + TEXT(" ") + AdditionalArgs; FPlatformProcess::CreateProc(P4VExe, *P4VArgs, true, true, true, nullptr, 0, nullptr, nullptr); } void FPerforceConnection::OpenP4VC(const FString& AdditionalArgs) const { #if PLATFORM_WINDOWS const TCHAR* P4VCExe = TEXT("p4vc.exe"); #else const TCHAR* P4VCExe = TEXT("p4vc"); #endif FString P4VCArgs = GetArgumentsForExternalProgram() + TEXT(" ") + AdditionalArgs; FPlatformProcess::CreateProc(P4VCExe, *P4VCArgs, true, true, true, nullptr, 0, nullptr, nullptr); } FString FPerforceConnection::GetArgumentsForExternalProgram(bool bIncludeClient) const { FString ExternalArgs = FString::Printf(TEXT("-p \"%s\" -u \"%s\""), *ServerAndPort, *UserName); if (bIncludeClient && !ClientName.IsEmpty()) { ExternalArgs += FString::Printf(TEXT(" -c \"%s\""), *ClientName); } return ExternalArgs; } //// FPerforceUtils //// FString FPerforceUtils::GetClientOrDepotDirectoryName(const TCHAR* ClientFile) { const TCHAR* LastSlash = FCString::Strrchr(ClientFile, '/'); if(LastSlash == nullptr) { return ""; } else { return FString(LastSlash - ClientFile, ClientFile); } } bool FPerforceUtils::TryParseDateTime(const FString& Date, const FString& Time, FDateTime& OutDate) { // Parse the date const TCHAR* Pos = *Date; TCHAR* End = nullptr; int32 Year = FCString::Strtoi(Pos, &End, 10); if(End <= Pos || *End != '/') { return false; } Pos = End + 1; int32 Month = FCString::Strtoi(Pos, &End, 10); if(End <= Pos || *End != '/') { return false; } Pos = End + 1; int32 Day = FCString::Strtoi(Pos, &End, 10); if(End <= Pos || *End != 0) { return false; } // Parse the time Pos = *Time; int Hour = FCString::Strtoi(Pos, &End, 10); if(End <= Pos || *End != ':') { return false; } Pos = End + 1; int Minute = FCString::Strtoi(Pos, &End, 10); if(End <= Pos || *End != ':') { return false; } Pos = End + 1; int Second = FCString::Strtoi(Pos, &End, 10); if(End <= Pos || *End != 0) { return false; } OutDate = FDateTime(Year, Month, Day, Hour, Minute, Second); return true; } } // namespace UGSCore