Merged PR 799117: Print the log directory as a hyperlink in the BuildXL console

Windows Terminal and other terminals support clickable hyperlinks. This change makes the link to the BuildXL log directory a clickable hyperlink to be opened by the operating system's file explorer of choice. This works on Windows and Linux.

I disabled it when run through an SSH session since the link would be a path on the host and the files may not exist on the SSH client. If run in a VSCode SSH session, VSCode has parsing for paths and does the right thing anyway.

This does change the behavior if you're running through a VSCode wrapped terminal locally. Previously, VSCode would attempt to show it through its file browser. Now it will use the file explorer of the OS instead. I didn't think this was a big deal and wasn't worthy of a config option.
This commit is contained in:
Michael Pysson 2024-08-17 00:23:15 +00:00
Родитель 267a6c0b73
Коммит d9beb69fcb
6 изменённых файлов: 134 добавлений и 45 удалений

Просмотреть файл

@ -55,10 +55,12 @@ namespace BuildXL
// Message prefixes for the app-server protocol.
private const byte ServerHelloMessage = 0xD0;
private const byte ConsoleOutputMessage = 0xC0;
private const byte ConsoleOutputLine = 0xC0;
private const byte ConsoleProgressMessage = 0xC1;
private const byte ConsoleTemporaryMessage = 0xC2;
private const byte ConsoleTemporaryOnlyMessage = 0xC3;
private const byte ConsoleOutput = 0xC4;
private const byte ConsoleHyperlink = 0xC5;
private const byte ExitCodeMessage = 0xEC;
private const byte CancelMessage = 0xCD;
private const byte TerminateMessage = 0xCE;
@ -432,7 +434,7 @@ namespace BuildXL
{
WriteMessage(() =>
{
m_writer.Write(ConsoleOutputMessage);
m_writer.Write(ConsoleOutputLine);
m_writer.Write((byte)messageLevel);
m_writer.Write(line);
});
@ -528,11 +530,32 @@ namespace BuildXL
});
}
public void WriteHyperlink(MessageLevel messageLevel, string text, string target)
{
WriteMessage(() =>
{
m_writer.Write(ConsoleHyperlink);
m_writer.Write((byte)messageLevel);
m_writer.Write(text);
m_writer.Write(target);
});
}
/// <inheritdoc/>
public void SetRecoverableErrorAction(Action<Exception> errorAction)
{
// noop
}
public void WriteOutput(MessageLevel messageLevel, string text)
{
WriteMessage(() =>
{
m_writer.Write(ConsoleOutput);
m_writer.Write((byte)messageLevel);
m_writer.Write(text);
});
}
}
#endregion
@ -970,7 +993,7 @@ namespace BuildXL
switch (messageId)
{
case ConsoleOutputMessage:
case ConsoleOutputLine:
ReadAndForwardConsoleMessage(reader);
break;
case ConsoleProgressMessage:
@ -982,6 +1005,12 @@ namespace BuildXL
case ConsoleTemporaryOnlyMessage:
ReadAndForwardTemporaryOnlyMessage(reader);
break;
case ConsoleOutput:
ReadAndForwardConsoleOutput(reader);
break;
case ConsoleHyperlink:
ReadAndForwardHyperlink(reader);
break;
case ExitCodeMessage:
ExitKind exit;
if (!EnumTraits<ExitKind>.TryConvert(reader.ReadByte(), out exit))
@ -1019,27 +1048,30 @@ namespace BuildXL
private void ReadAndForwardConsoleMessage(BinaryReader reader)
{
MessageLevel messageLevel;
if (!EnumTraits<MessageLevel>.TryConvert(reader.ReadByte(), out messageLevel))
{
throw new BuildXLException("Unknown console message level received from app server");
}
m_console.WriteOutputLine(messageLevel, reader.ReadString());
m_console.WriteOutputLine(ReadMessageLevel(reader), reader.ReadString());
}
private void ReadAndForwardTemporaryMessage(BinaryReader reader)
{
MessageLevel messageLevel;
if (!EnumTraits<MessageLevel>.TryConvert(reader.ReadByte(), out messageLevel))
{
throw new BuildXLException("Unknown console message level received from app server");
}
m_console.WriteOverwritableOutputLine(messageLevel, reader.ReadString(), reader.ReadString());
m_console.WriteOverwritableOutputLine(ReadMessageLevel(reader), reader.ReadString(), reader.ReadString());
}
private void ReadAndForwardTemporaryOnlyMessage(BinaryReader reader)
{
m_console.WriteOverwritableOutputLineOnlyIfSupported(ReadMessageLevel(reader), reader.ReadString(), reader.ReadString());
}
private void ReadAndForwardConsoleOutput(BinaryReader reader)
{
m_console.WriteOutput(ReadMessageLevel(reader), reader.ReadString());
}
private void ReadAndForwardHyperlink(BinaryReader reader)
{
m_console.WriteHyperlink(ReadMessageLevel(reader), reader.ReadString(), reader.ReadString());
}
private static MessageLevel ReadMessageLevel(BinaryReader reader)
{
MessageLevel messageLevel;
if (!EnumTraits<MessageLevel>.TryConvert(reader.ReadByte(), out messageLevel))
@ -1047,7 +1079,7 @@ namespace BuildXL
throw new BuildXLException("Unknown console message level received from app server");
}
m_console.WriteOverwritableOutputLineOnlyIfSupported(messageLevel, reader.ReadString(), reader.ReadString());
return messageLevel;
}
private void ReadAndForwardProgress(BinaryReader reader)

Просмотреть файл

@ -169,6 +169,8 @@ namespace BuildXL
private TimeSpan TelemetryFlushTimeout => m_configuration.Logging.RemoteTelemetryFlushTimeout ?? AriaV2StaticState.DefaultShutdownTimeout;
private static readonly Lazy<bool> m_isSShSession = new Lazy<bool>(() => Environment.GetEnvironmentVariable("SSH_CLIENT") != null);
/// <nodoc />
[SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope")]
public BuildXLApp(
@ -716,16 +718,16 @@ namespace BuildXL
try
{
string filePath = buildSummary.RenderMarkdown();
WriteToConsole("##vso[task.uploadsummary]" + filePath);
WriteLineToConsole("##vso[task.uploadsummary]" + filePath);
}
catch (IOException e)
{
WriteErrorToConsole(Strings.App_Main_FailedToWriteSummary, e.Message);
WriteErrorLineToConsole(Strings.App_Main_FailedToWriteSummary, e.Message);
// No need to change exit code, only behavior is lack of log in the extensions page.
}
catch (UnauthorizedAccessException e)
{
WriteErrorToConsole(Strings.App_Main_FailedToWriteSummary, e.Message);
WriteErrorLineToConsole(Strings.App_Main_FailedToWriteSummary, e.Message);
// No need to change exit code, only behavior is lack of log in the extensions page.
}
}
@ -735,7 +737,7 @@ namespace BuildXL
if (appLoggers.TrackingEventListener.HasFailures)
{
WriteErrorToConsoleWithDefaultColor(Strings.App_Main_BuildFailed);
WriteErrorLineToConsoleWithDefaultColor(Strings.App_Main_BuildFailed);
LogGeneratedFiles(pm.LoggingContext, appLoggers.TrackingEventListener, translator: appLoggers.PathTranslatorForLogging);
@ -749,7 +751,7 @@ namespace BuildXL
internalWarnings: internalWarnings);
}
WriteToConsole(Strings.App_Main_BuildSucceeded);
WriteLineToConsole(Strings.App_Main_BuildSucceeded);
LogGeneratedFiles(pm.LoggingContext, appLoggers.TrackingEventListener, translator: appLoggers.PathTranslatorForLogging);
@ -758,17 +760,13 @@ namespace BuildXL
var translator = appLoggers.PathTranslatorForLogging;
var configFile = m_initialConfiguration.Startup.ConfigFile;
IdeGenerator.WriteCmd(GetExpandedCmdLine(m_commandLineArguments), m_configuration.Ide, configFile, m_pathTable, translator);
var solutionFile = IdeGenerator.GetSolutionPath(m_configuration.Ide, m_pathTable).ToString(m_pathTable);
if (translator != null)
{
solutionFile = translator.Translate(solutionFile);
}
WriteToConsole(Strings.App_Vs_SolutionFile, solutionFile);
WriteToConsole(Strings.App_Vs_SolutionFile);
WritePathLineAsLinkToConsole(IdeGenerator.GetSolutionPath(m_configuration.Ide, m_pathTable).ToString(m_pathTable), translator);
var vsVersions = IdeGenerator.GetVersionsNotHavingLatestPlugin();
if (vsVersions != null)
{
WriteWarningToConsole(Strings.App_Vs_InstallPlugin, vsVersions, IdeGenerator.LatestPluginVersion);
WriteWarningLineToConsole(Strings.App_Vs_InstallPlugin, vsVersions, IdeGenerator.LatestPluginVersion);
}
}
@ -909,7 +907,8 @@ namespace BuildXL
if (m_configuration.Logging.LogsDirectory.IsValid)
{
// When using the new style logging configuration, just show the path to the logs directory
WriteToConsole(Strings.App_LogsDirectory, m_configuration.Logging.LogsDirectory.ToString(m_pathTable));
WriteToConsole(Strings.App_LogsDirectory);
WritePathLineAsLinkToConsole(m_configuration.Logging.LogsDirectory.ToString(m_pathTable), translator);
}
else
{
@ -931,11 +930,11 @@ namespace BuildXL
{
if (trackingListener.HasFailures)
{
WriteToConsole(message, path);
WriteLineToConsole(message, path);
}
else
{
WriteErrorToConsoleWithDefaultColor(message, path);
WriteErrorLineToConsoleWithDefaultColor(message, path);
}
}
}
@ -1333,7 +1332,7 @@ namespace BuildXL
}
catch (Exception ex)
{
WriteErrorToConsole(
WriteErrorLineToConsole(
Strings.App_RootMapping_CantCreateDirectory,
mapping.Value,
mapping.Key,
@ -1347,7 +1346,7 @@ namespace BuildXL
!ProcessNativeMethods.ApplyDriveMappings(
rootMappings.Select(kvp => new PathMapping(kvp.Key[0], kvp.Value.ToString(m_pathTable))).ToArray()))
{
WriteErrorToConsole(Strings.App_RootMapping_CantApplyRootMappings);
WriteErrorLineToConsole(Strings.App_RootMapping_CantApplyRootMappings);
return false;
}
}
@ -2297,8 +2296,9 @@ namespace BuildXL
break;
default:
Logger.Log.CatastrophicFailure(pm.LoggingContext, failureMessage, s_buildInfo?.CommitId ?? string.Empty, s_buildInfo?.Build ?? string.Empty);
WriteToConsole(Strings.App_LogsDirectory, loggers.RootLogDirectory);
WriteToConsole("Collecting some information about this crash...");
WriteToConsole(Strings.App_LogsDirectory);
WritePathLineAsLinkToConsole(loggers.RootLogDirectory, GetPathTranslator(m_configuration.Logging, m_pathTable));
WriteLineToConsole("Collecting some information about this crash...");
break;
}
@ -2380,8 +2380,8 @@ namespace BuildXL
catch (Exception ex)
{
// Oh my, this isn't going very well.
WriteErrorToConsole("Unhandled exception in exception handler");
WriteErrorToConsole(ex.DemystifyToString());
WriteErrorLineToConsole("Unhandled exception in exception handler");
WriteErrorLineToConsole(ex.DemystifyToString());
}
#pragma warning restore ERP022 // Unobserved exception in generic exception handler
finally
@ -2848,22 +2848,44 @@ namespace BuildXL
return new StandardConsole(loggingConfiguration.Color, loggingConfiguration.AnimateTaskbar, loggingConfiguration.FancyConsole, translator);
}
private void WriteToConsole(string format, params object[] args)
private void WriteToConsole(string text)
{
m_console.WriteOutput(MessageLevel.Info, text);
}
private void WritePathLineAsLinkToConsole(string path, PathTranslator translator)
{
path = translator?.Translate(path) ?? path;
// Don't make the path a hyperlink if running in an SSH session since that link won't
// be opened reasonably on the client of that SSH session.
if (m_configuration.Logging.FancyConsole && !m_isSShSession.Value)
{
m_console.WriteHyperlink(MessageLevel.Info, path, @"file://" + path.TrimStart('/'));
WriteLineToConsole(string.Empty);
}
else
{
WriteLineToConsole(path);
}
}
private void WriteLineToConsole(string format, params object[] args)
{
m_console.WriteOutputLine(MessageLevel.Info, string.Format(CultureInfo.InvariantCulture, format, args));
}
private void WriteWarningToConsole(string format, params object[] args)
private void WriteWarningLineToConsole(string format, params object[] args)
{
m_console.WriteOutputLine(MessageLevel.Warning, string.Format(CultureInfo.InvariantCulture, format, args));
}
private void WriteErrorToConsole(string format, params object[] args)
private void WriteErrorLineToConsole(string format, params object[] args)
{
m_console.WriteOutputLine(MessageLevel.Error, string.Format(CultureInfo.InvariantCulture, format, args));
}
private void WriteErrorToConsoleWithDefaultColor(string format, params object[] args)
private void WriteErrorLineToConsoleWithDefaultColor(string format, params object[] args)
{
m_console.WriteOutputLine(MessageLevel.ErrorNoColor, string.Format(CultureInfo.InvariantCulture, format, args));
}

Просмотреть файл

@ -686,10 +686,10 @@ Example: ad2d42d2ec5d2ca0c0b7ad65402d07c7ef40b91e</value>
<value>Standard help shown. For complete help options, use /help:verbose</value>
</data>
<data name="App_Vs_SolutionFile" xml:space="preserve">
<value> VS Solution File: {0}</value>
<value> VS Solution File: </value>
</data>
<data name="App_LogsDirectory" xml:space="preserve">
<value> Log Directory: {0}</value>
<value> Log Directory: </value>
</data>
<data name="HelpText_DisplayHelp_SubstSource" xml:space="preserve">
<value>Path of the original root that has been substituted to another. Log messages will be converted back to this path root. Must be specified with /substTarget.</value>

Просмотреть файл

@ -138,5 +138,17 @@ namespace Test.BuildXL
{
throw new NotImplementedException();
}
/// <inheritdoc/>
public void WriteHyperlink(MessageLevel messageLevel, string text, string target)
{
throw new NotImplementedException();
}
/// <inheritdoc/>
public void WriteOutput(MessageLevel messageLevel, string text)
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -46,6 +46,16 @@ namespace BuildXL.Utilities.Tracing
/// </remarks>
bool UpdatingConsole { get; }
/// <summary>
/// Writes a hyperlink with the given message level.
/// </summary>
void WriteHyperlink(MessageLevel messageLevel, string text, string target);
/// <summary>
/// Writes output with the given message level.
/// </summary>
void WriteOutput(MessageLevel messageLevel, string text);
/// <summary>
/// Writes a line with the given message level.
/// </summary>

Просмотреть файл

@ -121,6 +121,19 @@ namespace BuildXL.Utilities.Tracing
}
}
/// <inheritdoc/>
public void WriteHyperlink(MessageLevel messageLevel, string text, string target)
{
const string Esc = "\x1b";
WriteOutput(messageLevel, $@"{Esc}]8;;{target}{Esc}\{text}{Esc}]8;;{Esc}\");
}
/// <inheritdoc/>
public void WriteOutput(MessageLevel messageLevel, string text)
{
Console.Write(text);
}
public static int GetConsoleWidth()
{
// Not all consoles have a width defined, therefore return 150 which is a reasonable default on Developer boxes.