// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
namespace EpicGames.Perforce
{
///
/// Logger which adds Perforce depot path and changelist information to file annotations
///
public class PerforceMetadataLogger : ILogger
{
class ClientView
{
public readonly DirectoryReference BaseDir;
public readonly PerforceViewMap ViewMap;
public readonly int Change;
public ClientView(DirectoryReference baseDir, PerforceViewMap viewMap, int change)
{
BaseDir = baseDir;
ViewMap = viewMap;
Change = change;
}
}
readonly ILogger _inner;
readonly List _clients = new List();
///
/// Accessor for the inner logger
///
public ILogger Inner => _inner;
///
/// Constructor
///
///
public PerforceMetadataLogger(ILogger inner)
{
_inner = inner;
}
///
/// Adds a new client to be included in the mapping
///
/// Base directory for the client
/// Depot path for the workspace mapping, in the form //foo/bar...
/// Changelist for the client
public void AddClientView(DirectoryReference baseDir, string depotPath, int change)
{
PerforceViewMap viewMap = new PerforceViewMap();
viewMap.Entries.Add(new PerforceViewMapEntry(true, depotPath, "..."));
AddClientView(baseDir, viewMap, change);
}
///
/// Adds a new client to be included in the mapping
///
public void AddClientView(DirectoryReference baseDir, PerforceViewMap viewMap, int change)
{
_clients.Add(new ClientView(baseDir, viewMap.Invert(), change));
}
///
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
{
JsonLogEvent logEvent = JsonLogEvent.FromLoggerState(logLevel, eventId, state, exception, formatter);
try
{
logEvent = new JsonLogEvent(logLevel, eventId, logEvent.LineIndex, logEvent.LineCount, Annotate(logEvent.Data));
}
catch (Exception ex)
{
_inner.LogWarning(KnownLogEvents.Systemic_LogEventMatcher, ex, "Unable to annotate source files with Perforce metadata: {Message}", ex.Message);
}
_inner.Log(logLevel, eventId, logEvent, null, JsonLogEvent.Format);
}
///
public bool IsEnabled(LogLevel logLevel) => _inner.IsEnabled(logLevel);
///
public IDisposable? BeginScope(TState state) where TState : notnull => _inner.BeginScope(state);
static bool ReadFirstLogProperty(ref Utf8JsonReader reader)
{
// Enter the main object
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
return false;
}
// Find the 'properties' property
for (; ; )
{
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName)
{
return false;
}
if (reader.ValueTextEquals(LogEventPropertyName.Properties))
{
break;
}
reader.Skip();
}
// Enter the properties object
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
return false;
}
// Find the first structured property object
return ReadNextLogProperty(ref reader);
}
static bool ReadNextLogProperty(ref Utf8JsonReader reader)
{
for (; ; )
{
// Move to the next property name
if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName)
{
return false;
}
// Move to the next property value
if (!reader.Read())
{
return false;
}
// If it's an object, enter it
if (reader.TokenType == JsonTokenType.StartObject)
{
return true;
}
// Otherwise skip this value
reader.Skip();
}
}
static readonly Utf8String s_sourceFileType = LogValueType.SourceFile;
static readonly Utf8String s_assetType = LogValueType.Asset;
static readonly Utf8String s_file = new Utf8String("file");
ReadOnlyMemory Annotate(ReadOnlyMemory data)
{
ReadOnlyMemory output = data;
int shift = 0;
Utf8JsonReader reader = new Utf8JsonReader(data.Span);
for (bool valid = ReadFirstLogProperty(ref reader); valid; valid = ReadNextLogProperty(ref reader))
{
ReadOnlySpan type = ReadOnlySpan.Empty;
ReadOnlySpan text = ReadOnlySpan.Empty;
string? file = null;
// Get the $type and $text properties
while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName)
{
if (type.Length == 0 && reader.ValueTextEquals(LogEventPropertyName.Type))
{
if (reader.Read() && reader.TokenType == JsonTokenType.String)
{
type = reader.GetUtf8String();
continue;
}
}
if (text.Length == 0 && reader.ValueTextEquals(LogEventPropertyName.Text))
{
if (reader.Read() && reader.TokenType == JsonTokenType.String)
{
text = reader.GetUtf8String();
continue;
}
}
if (file == null && reader.ValueTextEquals(s_file))
{
if (reader.Read() && reader.TokenType == JsonTokenType.String)
{
file = reader.GetString();
continue;
}
}
reader.Skip();
}
// If we're at the end of the object, append any additional data
byte[]? annotationBytes = GetAnnotations(type, text, file);
if (annotationBytes != null)
{
int position = (int)reader.TokenStartIndex + shift;
byte[] newOutput = new byte[output.Length + annotationBytes.Length];
output.Span.Slice(0, position).CopyTo(newOutput);
annotationBytes.CopyTo(newOutput.AsSpan(position));
output.Span.Slice(position).CopyTo(newOutput.AsSpan(position + annotationBytes.Length));
output = newOutput;
shift += annotationBytes.Length;
}
}
return output;
}
byte[]? GetAnnotations(ReadOnlySpan type, ReadOnlySpan text, string? file)
{
if (type.SequenceEqual(s_sourceFileType) || type.SequenceEqual(s_assetType))
{
StringBuilder annotations = new StringBuilder();
file ??= Encoding.UTF8.GetString(text);
file = file.Replace(@"\\", Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal);
file = file.Replace('\\', Path.DirectorySeparatorChar);
foreach (ClientView client in _clients)
{
FileReference location = FileReference.Combine(client.BaseDir, file);
if (location.IsUnderDirectory(client.BaseDir))
{
string relativePath = location.MakeRelativeTo(client.BaseDir).Replace('\\', '/');
annotations.Append($",\"relativePath\":\"{JsonEncodedText.Encode(relativePath)}\"");
if (client.ViewMap.TryMapFile(relativePath, StringComparison.OrdinalIgnoreCase, out string depotFile))
{
depotFile = $"{depotFile}@{client.Change}";
annotations.Append($",\"depotPath\":\"{JsonEncodedText.Encode(depotFile)}\"");
break;
}
}
}
return Encoding.UTF8.GetBytes(annotations.ToString());
}
return null;
}
}
}