// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using System.ServiceProcess; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; using UnrealBuildTool.Artifacts; namespace UnrealBuildTool { sealed class SNDBS : ActionExecutor { public override string Name => "SNDBS"; private static readonly string? ProgramFilesx86 = Environment.GetEnvironmentVariable("ProgramFiles(x86)"); private static readonly string? SCERoot = Environment.GetEnvironmentVariable("SCE_ROOT_DIR"); private static string FindDbsExe(string ExeName) { string InstallPath = Path.Combine(ProgramFilesx86 ?? String.Empty, "SCE", "Common", "SN-DBS", "bin", ExeName); if (File.Exists(InstallPath)) { return InstallPath; } else { // Legacy install location using SCE_ROOT_DIR return Path.Combine(SCERoot ?? String.Empty, "Common", "SN-DBS", "bin", ExeName); } } private static string SNDBSBuildExe => FindDbsExe("dbsbuild.exe"); private static string SNDBSUtilExe => FindDbsExe("dbsutil.exe"); private static readonly DirectoryReference IntermediateDir = DirectoryReference.Combine(Unreal.EngineDirectory, "Intermediate", "Build", "SNDBS"); private static readonly FileReference IncludeRewriteRulesFile = FileReference.Combine(IntermediateDir, "include-rewrite-rules.ini"); private static readonly FileReference ScriptFile = FileReference.Combine(IntermediateDir, "sndbs.json"); private Dictionary ActiveTemplates = BuiltInTemplates.ToDictionary(p => p.Key, p => p.Value); /// /// When enabled, SN-DBS will stop compiling targets after a compile error occurs. Recommended, as it saves computing resources for others. /// [XmlConfigFile(Category = "BuildConfiguration")] bool bStopSNDBSCompilationAfterErrors = false; /// /// When set to false, SNDBS will not be enabled when running connected to the coordinator over VPN. Configure VPN-assigned subnets via the VpnSubnets parameter. /// [XmlConfigFile(Category = "SNDBS")] static bool bAllowOverVpn = true; /// /// List of subnets containing IP addresses assigned by VPN /// [XmlConfigFile(Category = "SNDBS")] static string[]? VpnSubnets = null; private const string ProgressMarkupPrefix = "@action:"; private List TargetDescriptors; public SNDBS(List InTargetDescriptors, ILogger Logger) : base(Logger) { XmlConfig.ApplyTo(this); TargetDescriptors = InTargetDescriptors; } public SNDBS AddTemplate(string ExeName, string TemplateContents) { ActiveTemplates.Add(ExeName, TemplateContents); return this; } static bool TryGetBrokerHost([NotNullWhen(true)] out string? OutBroker) { if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64) { string BrokerHostName = ""; Regex FindHost = new Regex(@"Active broker is ""(\S +)"" \((\S+)\)"); Process LocalProcess = new Process(); LocalProcess.StartInfo = new ProcessStartInfo(SNDBSUtilExe, $"-connected"); LocalProcess.OutputDataReceived += (Sender, Args) => { if (Args.Data != null) { Match Result = FindHost.Match(Args.Data); if (Result.Success) { BrokerHostName = Result.Groups[1].Value; } } }; if (Utils.RunLocalProcess(LocalProcess) == 1 && BrokerHostName.Length > 0) { OutBroker = BrokerHostName; return true; } } OutBroker = null; return false; } [DllImport("iphlpapi")] static extern int GetBestInterface(uint dwDestAddr, ref int pdwBestIfIndex); static NetworkInterface? GetInterfaceForHost(string Host) { if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64) { IPHostEntry HostEntry = Dns.GetHostEntry(Host); foreach (IPAddress HostAddress in HostEntry.AddressList) { int InterfaceIdx = 0; if (GetBestInterface(BitConverter.ToUInt32(HostAddress.GetAddressBytes(), 0), ref InterfaceIdx) == 0) { foreach (NetworkInterface Interface in NetworkInterface.GetAllNetworkInterfaces()) { IPv4InterfaceProperties Properties = Interface.GetIPProperties().GetIPv4Properties(); if (Properties.Index == InterfaceIdx) { return Interface; } } } } } return null; } public static bool IsHostOnVpn(string HostName, ILogger Logger) { // If there aren't any defined subnets, just early out if (VpnSubnets == null || VpnSubnets.Length == 0) { return false; } // Parse all the subnets from the config file List ParsedVpnSubnets = new List(); foreach (string VpnSubnet in VpnSubnets) { ParsedVpnSubnets.Add(Subnet.Parse(VpnSubnet)); } // Check if any network adapters have an IP within one of these subnets try { NetworkInterface? Interface = GetInterfaceForHost(HostName); if (Interface != null && Interface.OperationalStatus == OperationalStatus.Up) { IPInterfaceProperties Properties = Interface.GetIPProperties(); foreach (UnicastIPAddressInformation UnicastAddressInfo in Properties.UnicastAddresses) { byte[] AddressBytes = UnicastAddressInfo.Address.GetAddressBytes(); foreach (Subnet Subnet in ParsedVpnSubnets) { if (Subnet.Contains(AddressBytes)) { if (!bAllowOverVpn) { Log.TraceInformationOnce("XGE coordinator {0} will be not be used over VPN (adapter '{1}' with IP {2} is in subnet {3}). Set true in BuildConfiguration.xml to override.", HostName, Interface.Description, UnicastAddressInfo.Address, Subnet); } return true; } } } } } catch (Exception Ex) { Logger.LogWarning("Unable to check whether host {HostName} is connected to VPN:\n{Ex}", HostName, ExceptionUtils.FormatExceptionDetails(Ex)); } return false; } public static bool IsAvailable(ILogger Logger) { // Check the executable exists on disk if (!File.Exists(SNDBSBuildExe)) { return false; } // Check the service is running if (OperatingSystem.IsWindows() && !ServiceController.GetServices().Any(s => OperatingSystem.IsWindows() && s.ServiceName.StartsWith("SNDBS") && s.Status == ServiceControllerStatus.Running)) { return false; } // Check if we're connected over VPN if (!bAllowOverVpn && VpnSubnets != null && VpnSubnets.Length > 0) { string? BrokerHost; if (TryGetBrokerHost(out BrokerHost) && IsHostOnVpn(BrokerHost, Logger)) { return false; } } return true; } private TelemetryExecutorEvent? telemetryEvent; /// public override TelemetryExecutorEvent? GetTelemetryEvent() => telemetryEvent; /// public override Task ExecuteActionsAsync(IEnumerable ActionsToExecute, ILogger Logger, IActionArtifactCache? actionArtifactCache) { return Task.FromResult(ExecuteActions(ActionsToExecute, Logger)); } bool ExecuteActions(IEnumerable Actions, ILogger Logger) { if (!Actions.Any()) { return true; } // Clean the intermediate directory in case there are any leftovers from previous builds if (DirectoryReference.Exists(IntermediateDir)) { DirectoryReference.Delete(IntermediateDir, true); } DirectoryReference.CreateDirectory(IntermediateDir); if (!DirectoryReference.Exists(IntermediateDir)) { throw new BuildException($"Failed to create directory \"{IntermediateDir}\"."); } int IdCounter = 0; // Build the json script file to describe all the actions and their dependencies Dictionary ActionIds = Actions.ToDictionary(a => a, a => new Guid(++IdCounter, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0).ToString()); JsonSerializerOptions JsonOption = new JsonSerializerOptions(); JsonOption.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; File.WriteAllText(ScriptFile.FullName, JsonSerializer.Serialize(new Dictionary() { ["jobs"] = Actions.Select(a => { Dictionary Job = new Dictionary() { ["id"] = ActionIds[a], ["title"] = a.StatusDescription, ["command"] = $"\"{a.CommandPath}\" {a.CommandArguments}", ["working_directory"] = a.WorkingDirectory.FullName, ["dependencies"] = a.PrerequisiteActions.Select(p => ActionIds[p]).ToArray(), ["run_locally"] = !(a.bCanExecuteRemotely && a.bCanExecuteRemotelyWithSNDBS) }; if (a.PrerequisiteItems.Any()) { Job["explicit_input_files"] = a.PrerequisiteItems.Where(i => !(i.AbsolutePath.EndsWith(".rsp") || i.AbsolutePath.EndsWith(".response"))).Select(i => new Dictionary() { ["filename"] = i.AbsolutePath }).ToList(); } // Add PCH source file dependencies for clang or clang-cl if (a.bIsGCCCompiler) { // Look for any prerequisite actions that produce .pch files and add any .cpp files they depend on List>? ExplicitInputFiles = Job["explicit_input_files"] as List>; ExplicitInputFiles?.AddRange(a.PrerequisiteActions .Where(Prereq => Prereq.ProducedItems.Any(Produced => Produced.AbsolutePath.EndsWith(".gch"))) .SelectMany(Prereq => Prereq.PrerequisiteItems, (_, PrereqFile) => PrereqFile.AbsolutePath) .Where(Path => Path.EndsWith(".cpp")) .Select(Path => new Dictionary() { ["filename"] = Path })); } string CommandDescription = String.IsNullOrWhiteSpace(a.CommandDescription) ? a.ActionType.ToString() : a.CommandDescription; Job["echo"] = $"{ProgressMarkupPrefix}{CommandDescription}:{a.StatusDescription}"; return Job; }).ToArray() }, JsonOption)); PrepareToolTemplates(); bool bHasRewrites = GenerateSNDBSIncludeRewriteRules(); IEnumerable ConfigList = TargetDescriptors.Select(Descriptor => $"{Descriptor.Name}|{Descriptor.Platform}|{Descriptor.Configuration}"); string ConfigDescription = String.Join(",", ConfigList); ProcessStartInfo StartInfo = new ProcessStartInfo( SNDBSBuildExe, $"-q -p \"{ConfigDescription}\" -s \"{ScriptFile}\" -templates \"{IntermediateDir}\"" ); StartInfo.UseShellExecute = false; if (bHasRewrites) { StartInfo.Arguments += $" --include-rewrite-rules \"{IncludeRewriteRulesFile}\""; } if (!bStopSNDBSCompilationAfterErrors) { StartInfo.Arguments += " -k"; } return ExecuteProcessWithProgressMarkup(StartInfo, Actions.Count(), Logger); } /// /// Executes the process, parsing progress markup as part of the output. /// private bool ExecuteProcessWithProgressMarkup(ProcessStartInfo SnDbsStartInfo, int NumActions, ILogger Logger) { using (ProgressWriter Writer = new ProgressWriter("Compiling C++ source files...", false, Logger)) { int NumCompletedActions = 0; string CurrentStatus = ""; // Create a wrapper delegate that will parse the output actions DataReceivedEventHandler EventHandlerWrapper = (Sender, Args) => { if (Args.Data != null) { string Text = Args.Data; if (Text.StartsWith(ProgressMarkupPrefix)) { Writer.Write(++NumCompletedActions, NumActions); Text = Args.Data.Substring(ProgressMarkupPrefix.Length).Trim(); string[] ActionInfo = Text.Split(':'); Logger.LogInformation("[{NumCompletedActions}/{NumActions}] {ActionInfo0} {ActionInfo1}", NumCompletedActions, NumActions, ActionInfo[0], ActionInfo[1]); CurrentStatus = ActionInfo[1]; return; } // Suppress redundant tool output of status we already printed (e.g., msvc cl prints compile unit name always) if (!Text.Equals(CurrentStatus)) { WriteToolOutput(Text); } } }; try { // Start the process, redirecting stdout/stderr if requested. DateTime startTimeUTC = DateTime.UtcNow; Process LocalProcess = new Process(); LocalProcess.StartInfo = SnDbsStartInfo; bool bShouldRedirectOuput = EventHandlerWrapper != null; if (bShouldRedirectOuput) { SnDbsStartInfo.RedirectStandardError = true; SnDbsStartInfo.RedirectStandardOutput = true; LocalProcess.EnableRaisingEvents = true; LocalProcess.OutputDataReceived += EventHandlerWrapper; LocalProcess.ErrorDataReceived += EventHandlerWrapper; } LocalProcess.Start(); if (bShouldRedirectOuput) { LocalProcess.BeginOutputReadLine(); LocalProcess.BeginErrorReadLine(); } Logger.LogInformation("Distributing {NumAction} action{ActionS} to SN-DBS", NumActions, NumActions == 1 ? "" : "s"); // Wait until the process is finished and return whether it all the tasks successfully executed. LocalProcess.WaitForExit(); bool result = LocalProcess.ExitCode == 0; telemetryEvent = new TelemetryExecutorEvent(Name, startTimeUTC, result, NumActions, -1, -1, 0, 0, DateTime.UtcNow); return result; } catch (Exception Ex) { Log.WriteException(Ex, null); return false; } } } private void PrepareToolTemplates() { foreach (KeyValuePair Template in ActiveTemplates) { FileReference TemplateFile = FileReference.Combine(IntermediateDir, $"{Template.Key}.sn-dbs-tool.ini"); string TemplateText = Template.Value; foreach (Nullable Variable in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process)) { if (Variable.HasValue) { TemplateText = TemplateText.Replace($"{{{Variable.Value.Key}}}", Variable.Value.Value!.ToString()); } } File.WriteAllText(TemplateFile.FullName, TemplateText); } } private bool GenerateSNDBSIncludeRewriteRules() { // Get all distinct platform names being used in this build. List Platforms = TargetDescriptors .Select(TargetDescriptor => UEBuildPlatform.GetBuildPlatform(TargetDescriptor.Platform).GetPlatformName()) .Distinct() .ToList(); if (Platforms.Count > 0) { // language=regex string[] Lines = new[] { @"pattern1=^COMPILED_PLATFORM_HEADER\(\s*([^ ,]+)\s*\)", $"expansions1={String.Join("|", Platforms.Select(Name => $"{Name}/{Name}$1|{Name}$1"))}", @"pattern2=^COMPILED_PLATFORM_HEADER_WITH_PREFIX\(\s*([^ ,]+)\s*,\s*([^ ,]+)\s*\)", $"expansions2={String.Join("|", Platforms.Select(Name => $"$1/{Name}/{Name}$2|$1/{Name}$2"))}", @"pattern3=^[A-Z]{5}_PLATFORM_HEADER_NAME\(\s*([^ ,]+)\s*\)", $"expansions3={String.Join("|", Platforms.Select(Name => $"{Name}/{Name}$1|{Name}$1"))}", @"pattern4=^[A-Z]{5}_PLATFORM_HEADER_NAME_WITH_PREFIX\(\s*([^ ,]+)\s*,\s*([^ ,]+)\s*\)", $"expansions4={String.Join("|", Platforms.Select(Name => $"$1/{Name}/{Name}$2|$1/{Name}$2"))}" }; File.WriteAllText(IncludeRewriteRulesFile.FullName, String.Join(Environment.NewLine, new[] { "[computed-include-rules]" }.Concat(Lines))); return true; } else { return false; } } /// /// SN-DBS templates that are automatically included in the build. /// private static readonly IReadOnlyDictionary BuiltInTemplates = new Dictionary() { ["cl-filter.exe"] = @" [tool] family=msvc vc_major_version=14 use_surrogate=true force_synchronous_pdb_writes=true error_report_mode=prompt response_file_content_pattern=\s--\s"".*?cl\.exe""\s(.*) [group] server={VC_TOOLCHAIN_DIR}\mspdbsrv.exe [files] main=cl-filter.exe file01={VC_TOOLCHAIN_DIR}\c1.dll file01={VC_TOOLCHAIN_DIR}\c1ui.dll file02={VC_TOOLCHAIN_DIR}\c1xx.dll file03={VC_TOOLCHAIN_DIR}\c2.dll file04={VC_TOOLCHAIN_DIR}\mspdb140.dll file05={VC_TOOLCHAIN_DIR}\mspdbcore.dll file06={VC_TOOLCHAIN_DIR}\mspdbsrv.exe file07={VC_TOOLCHAIN_DIR}\mspft140.dll file08={VC_TOOLCHAIN_DIR}\vcmeta.dll file09={VC_TOOLCHAIN_DIR}\*\clui.dll file10={VC_TOOLCHAIN_DIR}\*\mspft140ui.dll file11={VC_TOOLCHAIN_DIR}\localespc.dll file12={VC_TOOLCHAIN_DIR}\cppcorecheck.dll file13={VC_TOOLCHAIN_DIR}\experimentalcppcorecheck.dll file14={VC_TOOLCHAIN_DIR}\espxengine.dll file15={VC_TOOLCHAIN_DIR}\c1.exe [output-file-patterns] outputfile01=\s*""([^ "",]+\.cpp\.txt)\"" [output-file-rules] rule01=*\sqmcpp*.log|discard=true rule02=*\vctoolstelemetry*.dat|discard=true rule03=*\Microsoft\Windows\Temporary Internet Files\*|discard=true rule04=*\Microsoft\Windows\INetCache\*|discard=true [input-file-rules] rule01=*\sqmcpp*.log|ignore_transient_errors=true;ignore_unexpected_input=true rule02=*\vctoolstelemetry*.dat|ignore_transient_errors=true;ignore_unexpected_input=true rule03=*\Microsoft\Windows\Temporary Internet Files\*|ignore_transient_errors=true;ignore_unexpected_input=true rule04=*\Microsoft\Windows\INetCache\*|ignore_transient_errors=true;ignore_unexpected_input=true [system-file-filters] filter01=msvcr*.dll filter02=msvcp*.dll filter03=vcruntime140*.dll filter04=appcrt140*.dll filter05=desktopcrt140*.dll filter06=concrt140*.dll", ["cl.exe"] = @" [tool] family=msvc vc_major_version=14 use_surrogate=true force_synchronous_pdb_writes=true error_report_mode=prompt [group] server={VC_TOOLCHAIN_DIR}\mspdbsrv.exe [files] main={VC_TOOLCHAIN_DIR}\cl.exe file01={VC_TOOLCHAIN_DIR}\c1.dll file01={VC_TOOLCHAIN_DIR}\c1ui.dll file02={VC_TOOLCHAIN_DIR}\c1xx.dll file03={VC_TOOLCHAIN_DIR}\c2.dll file04={VC_TOOLCHAIN_DIR}\mspdb140.dll file05={VC_TOOLCHAIN_DIR}\mspdbcore.dll file06={VC_TOOLCHAIN_DIR}\mspdbsrv.exe file07={VC_TOOLCHAIN_DIR}\mspft140.dll file08={VC_TOOLCHAIN_DIR}\vcmeta.dll file09={VC_TOOLCHAIN_DIR}\*\clui.dll file10={VC_TOOLCHAIN_DIR}\*\mspft140ui.dll file11={VC_TOOLCHAIN_DIR}\localespc.dll file12={VC_TOOLCHAIN_DIR}\cppcorecheck.dll file13={VC_TOOLCHAIN_DIR}\experimentalcppcorecheck.dll file14={VC_TOOLCHAIN_DIR}\espxengine.dll [output-file-patterns] outputfile01=\s*""([^ "",]+\.cpp\.txt\.json)\"" [output-file-rules] rule01=*\sqmcpp*.log|discard=true rule02=*\vctoolstelemetry*.dat|discard=true rule03=*\Microsoft\Windows\Temporary Internet Files\*|discard=true rule04=*\Microsoft\Windows\INetCache\*|discard=true [input-file-rules] rule01=*\sqmcpp*.log|ignore_transient_errors=true;ignore_unexpected_input=true rule02=*\vctoolstelemetry*.dat|ignore_transient_errors=true;ignore_unexpected_input=true rule03=*\Microsoft\Windows\Temporary Internet Files\*|ignore_transient_errors=true;ignore_unexpected_input=true rule04=*\Microsoft\Windows\INetCache\*|ignore_transient_errors=true;ignore_unexpected_input=true [system-file-filters] filter01=msvcr*.dll filter02=msvcp*.dll filter03=vcruntime140*.dll filter04=appcrt140*.dll filter05=desktopcrt140*.dll filter06=concrt140*.dll", ["mspdbsrv.exe"] = @" [tool] use_cache=no [files] main={VC_TOOLCHAIN_DIR}\mspdbsrv.exe [openmp] omp=true omp_min_threads=1", ["clang++.exe"] = @" [tool] family=clang extensions=.c;.cc;.cpp;.cxx;.c++;.h;.hpp;.s;.asm adjacent_metadata_extensions=.pch;.gch;.pth;.o;.obj include_path01=..\include include_path02=..\include\c++\v1 include_path03=..\lib\clang\*\include include_path04=%CPATH%;%C_INCLUDE_PATH%;%CPLUS_INCLUDE_PATH% [files] main=clang++.exe [include-path-patterns] include01=-(?:I|F|isystem|idirafter)[ \t]*(\""[^\""]+\""|[^ ]+) [additional-include-file-patterns] includefile01=--?include[ \t]*(\""[^\""]+\""|[^ ]+) [additional-input-file-patterns] inputfile01=--?(?:include-pch|fprofile-instr-use=|fprofile-sample-use=|fprofile-use=|fsanitize-ignorelist=)[ \t]*(\""[^\""]+\""|[^ ]+) [output-file-patterns] outputfile01=-o[ \t]*(\""[^\""]+\""|[^ ]+) [definition-patterns] definition01=-D[ \t]*""?([a-zA-Z_0-9]+)(?:[\s""]|$)() definition02=-D[ \t]*""?([a-zA-Z_0-9]+)=(?:\\""|<)(.+?)(?:\\""|>) definition03=-D[ \t]*""?([a-zA-Z_0-9]+)=((?!(?:\\""|<))[^ ""]*) [input-scanners] scanner01=c .* scanner02=assembler .s;.asm", }; } }