Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Razor language server telemetry to C# DevKit #9283

Merged
merged 19 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Razor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.ProjectEngineHost.Test", "src\Razor\test\Microsoft.AspNetCore.Razor.ProjectEngineHost.Test\Microsoft.AspNetCore.Razor.ProjectEngineHost.Test.csproj", "{4126E0A6-1CA9-44B1-AD22-66EDB9FEE7AD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.DevKit.Razor", "src\Razor\src\Microsoft.VisualStudio.DevKit.Razor\Microsoft.VisualStudio.DevKit.Razor.csproj", "{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB}"
allisonchou marked this conversation as resolved.
Show resolved Hide resolved
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -737,6 +739,14 @@ Global
{4126E0A6-1CA9-44B1-AD22-66EDB9FEE7AD}.Release|Any CPU.Build.0 = Release|Any CPU
{4126E0A6-1CA9-44B1-AD22-66EDB9FEE7AD}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{4126E0A6-1CA9-44B1-AD22-66EDB9FEE7AD}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB}.Release|Any CPU.Build.0 = Release|Any CPU
{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -818,6 +828,7 @@ Global
{53977089-1A87-4521-8368-0D50DFDA1279} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
{C0C2AD17-5F5B-4B11-956D-203D91C377FB} = {92463391-81BE-462B-AC3C-78C6C760741F}
{4126E0A6-1CA9-44B1-AD22-66EDB9FEE7AD} = {92463391-81BE-462B-AC3C-78C6C760741F}
{0F2D75D1-89AD-40A6-9F02-D45708AEA3EB} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0035341D-175A-4D05-95E6-F1C2785A1E26}
Expand Down
31 changes: 31 additions & 0 deletions eng/AfterSigning.targets
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,35 @@
<!-- Build a .zip file from each platform's directory and write it to 'artifacts' -->
<ZipDirectory SourceDirectory="%(LanguageServiceBinary.SourceDir)" DestinationFile="%(LanguageServiceBinary.ZipFile)" />
</Target>

<Target Name="_ZipDevKitTelemetryBinaries" AfterTargets="_ZipLanguageServerBinaries" Condition="'$(ArcadeBuildFromSource)' != 'true'">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We zip the files to publish to the CDN so we can consume the telemetry binaries on the vscode-csharp side.

<!-- This target is defined in eng/targets/Packaging.targets and included in every project. -->
<MSBuild Projects="$(RepoRoot)src\Razor\src\Microsoft.VisualStudio.DevKit.Razor\Microsoft.VisualStudio.DevKit.Razor.csproj"
Targets="_GetPackageVersionInfo"
SkipNonexistentProjects="false">
<Output TaskParameter="TargetOutputs" ItemName="_ResolvedPackageVersionInfo" />
</MSBuild>

<!-- Build a .zip file from each platform's directory and write it to 'artifacts' -->
<PropertyGroup>
<RidsPublishDir>$(ArtifactsDir)DevKitTelemetry\$(Configuration)\</RidsPublishDir>
<ZipOutputDir>$(RidsPublishDir)</ZipOutputDir>
<_DotNetPath>$(DOTNET_HOST_PATH)</_DotNetPath>
<_DotNetPath Condition="'$(_DotNetPath)' == ''">dotnet</_DotNetPath>
</PropertyGroup>

<ItemGroup>
<DevKitTelemetryBinaryDir Include="$([System.IO.Directory]::GetDirectories(&quot;$(RidsPublishDir)&quot;))" />
<DevKitTelemetryBinary Include="@(DevKitTelemetryBinaryDir)">
<SourceDir>%(DevKitTelemetryBinaryDir.Identity)</SourceDir>
<ZipFile>$(ZipOutputDir)DevKitTelemetry-%(DevKitTelemetryBinaryDir.Filename)-$(_PackageVersion).zip</ZipFile>
</DevKitTelemetryBinary>
</ItemGroup>

<MakeDir Directories="$(ZipOutputDir)" />
<Delete Files="%(DevKitTelemetryBinary.ZipFile)" />

<!-- Build a .zip file from each platform's directory and write it to 'artifacts' -->
<ZipDirectory SourceDirectory="%(DevKitTelemetryBinary.SourceDir)" DestinationFile="%(DevKitTelemetryBinary.ZipFile)" />
</Target>
</Project>
12 changes: 12 additions & 0 deletions eng/AfterSolutionBuild.targets
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,16 @@
Targets="PublishAllRids" />
</Target>

<Target Name="_PublishDevKitTelemetryRids" AfterTargets="Pack" Condition="'$(DotNetBuildFromSource)' != 'true'">
<PropertyGroup>
<DevKitTelemetryProject>$(MSBuildThisFileDirectory)..\src\Razor\src\Microsoft.VisualStudio.DevKit.Razor\Microsoft.VisualStudio.DevKit.Razor.csproj</DevKitTelemetryProject>
<RazorSolutionPath>$(MSBuildThisFileDirectory)..\Razor.sln</RazorSolutionPath>
</PropertyGroup>

<MSBuild Projects="$(RazorSolutionPath)"
Targets="Restore" />
<MSBuild Projects="$(DevKitTelemetryProject)"
Targets="PublishAllRids" />
</Target>

</Project>
4 changes: 2 additions & 2 deletions eng/Publishing.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

<ItemGroup>
<!-- Prepare for _UpdatePublishItems target. -->
<_ItemsToPublish Include="$(ArtifactsPackagesDir)**\*.tgz" Condition="'$(OS)' == 'Windows_NT'" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is leftover from O# and is no longer needed

<_ItemsToPublish Include="$(ArtifactsDir)LanguageServer\**\*.zip" />
<_ItemsToPublish Include="$(ArtifactsDir)DevKitTelemetry\**\*.zip" />
</ItemGroup>

<Target Name="_UpdatePublishItems">
Expand All @@ -28,7 +28,7 @@
<!-- Packages can be built on all platforms, but are only published on Windows to avoid collisions from the other
platforms. This does not affect the SB intermediate package. -->
<ItemsToPushToBlobFeed Remove="$(ArtifactsDir)**\*.nupkg" Condition="'$(OS)' != 'Windows_NT' and '$(ArcadeBuildFromSource)' != 'true'" />

<ItemsToPushToBlobFeed Include="@(_ItemsToPublish)">
<IsShipping>false</IsShipping>
<ManifestArtifactData>ShipInstaller=dotnetcli;NonShipping=true</ManifestArtifactData>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright (c) .NET Foundation. All rights reserved.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is almost entirely taken from Roslyn with a few modifications to make it work for Razor

// Licensed under the MIT license. See License.txt in the project root for license information.

#if !NET472

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Exports;

internal sealed class AssemblyLoadContextWrapper
{
private static readonly ConcurrentDictionary<AssemblyName, Assembly?> s_loadedSharedAssemblies = new(AssemblyNameComparer.Default);

private AssemblyLoadContext? _assemblyLoadContext;
private readonly ImmutableDictionary<string, Assembly> _loadedAssemblies;

private AssemblyLoadContextWrapper(AssemblyLoadContext assemblyLoadContext, ImmutableDictionary<string, Assembly> loadedFiles)
{
_assemblyLoadContext = assemblyLoadContext;
_loadedAssemblies = loadedFiles;
}

public static bool TryLoadExtension(string assemblyFilePath, string? sharedDependenciesPath, [NotNullWhen(true)] out Assembly? assembly)
{
var dir = Path.GetDirectoryName(assemblyFilePath);
var fileName = Path.GetFileName(assemblyFilePath);
var fileNameNoExt = Path.GetFileNameWithoutExtension(assemblyFilePath);

var loadContext = TryCreate(fileNameNoExt, dir!, sharedDependenciesPath);
if (loadContext != null)
{
assembly = loadContext.GetAssembly(fileName);
return true;
}

assembly = null;
return false;
}

public static AssemblyLoadContextWrapper? TryCreate(string name, string assembliesDirectoryPath, string? sharedDependenciesPath)
{
try
{
var loadContext = CreateLoadContext(name, sharedDependenciesPath);
var directory = new DirectoryInfo(assembliesDirectoryPath);
var builder = new Dictionary<string, Assembly>();
foreach (var file in directory.GetFiles("*.dll"))
{
// HACK: For some reason trying to load ProjectEngineHost here causes a second version of the
allisonchou marked this conversation as resolved.
Show resolved Hide resolved
// expected dll to be loaded (one from rzls folder instead of the Razor telemetry folder). We'll
// skip loading it (and investigate later) since the one from the Razor telemetry folder is the
// one we want.
if (!file.Name.Contains("ProjectEngineHost"))
{
builder.Add(file.Name, loadContext.LoadFromAssemblyPath(file.FullName));
}
}

return new AssemblyLoadContextWrapper(loadContext, builder.ToImmutableDictionary());
}
catch
{
return null;
}
}

private static AssemblyLoadContext CreateLoadContext(string name, string? sharedDependenciesPath)
{
var loadContext = new AssemblyLoadContext(name);

if (sharedDependenciesPath != null)
{
loadContext.Resolving += (_, assemblyName) =>
{
if (assemblyName.Name is null)
{
return null;
}

if (s_loadedSharedAssemblies.TryGetValue(assemblyName, out var loadedAssembly))
{
return loadedAssembly;
}

var candidatePath = assemblyName.CultureName is not null
? Path.Combine(sharedDependenciesPath, assemblyName.CultureName, $"{assemblyName.Name}.dll")
: Path.Combine(sharedDependenciesPath, $"{assemblyName.Name}.dll");

if (File.Exists(candidatePath))
{
loadedAssembly = loadContext.LoadFromAssemblyPath(candidatePath);
}

s_loadedSharedAssemblies.TryAdd(assemblyName, loadedAssembly);

return s_loadedSharedAssemblies[assemblyName];
};
}

return loadContext;
}

public Assembly GetAssembly(string name) => _loadedAssemblies[name];

public MethodInfo? TryGetMethodInfo(string assemblyName, string className, string methodName)
{
try
{
return GetMethodInfo(assemblyName, className, methodName);
}
catch
{
return null;
}
}

public MethodInfo GetMethodInfo(string assemblyName, string className, string methodName)
{
var assembly = GetAssembly(assemblyName);
var completionHelperType = assembly.GetType(className);
if (completionHelperType == null)
{
throw new ArgumentException($"{assembly.FullName} assembly did not contain {className} class");
}
var createCompletionProviderMethodInto = completionHelperType?.GetMethod(methodName);
if (createCompletionProviderMethodInto == null)
{
throw new ArgumentException($"{className} from {assembly.FullName} assembly did not contain {methodName} method");
}
return createCompletionProviderMethodInto;
}

public void Dispose()
{
_assemblyLoadContext?.Unload();
_assemblyLoadContext = null;
}

private sealed class AssemblyNameComparer : IEqualityComparer<AssemblyName>
{
public static readonly AssemblyNameComparer Default = new();

public bool Equals(AssemblyName? x, AssemblyName? y)
{
if (ReferenceEquals(x, y))
return true;

if (x == null || y == null)
return false;

return string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase)
&& string.Equals(x.CultureName, y.CultureName, StringComparison.OrdinalIgnoreCase);
}

public int GetHashCode(AssemblyName obj)
{
var hashCodeCombiner = new HashCodeCombiner();
hashCodeCombiner.Add(new int[] {
StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name ?? string.Empty),
StringComparer.OrdinalIgnoreCase.GetHashCode(obj.CultureName ?? string.Empty)
});

return hashCodeCombiner.CombinedHash;
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) .NET Foundation. All rights reserved.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is almost entirely taken from Roslyn with a few modifications to make it work for Razor

// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using Microsoft.VisualStudio.Composition;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Exports;

internal class CustomExportAssemblyLoader(string baseDirectory) : IAssemblyLoader
{
/// <summary>
/// Cache assemblies that are already loaded by AssemblyName comparison
/// </summary>
private readonly Dictionary<AssemblyName, Assembly> _loadedAssemblies = new Dictionary<AssemblyName, Assembly>(AssemblyNameComparer.Instance);

/// <summary>
/// Base directory to search for <see cref="Assembly.LoadFrom(string)"/> if initial load fails
/// </summary>
private readonly string _baseDirectory = baseDirectory;

public Assembly LoadAssembly(AssemblyName assemblyName)
{
Assembly? value;
lock (_loadedAssemblies)
{
_loadedAssemblies.TryGetValue(assemblyName, out value);
}

if (value == null)
{
// Attempt to load the assembly normally, but fall back to Assembly.LoadFrom in the base
// directory if the assembly load fails
try
{
value = Assembly.Load(assemblyName);
}
catch (FileNotFoundException) when (assemblyName.Name is not null)
{
var filePath = Path.Combine(_baseDirectory, assemblyName.Name)
+ (assemblyName.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
? ""
: ".dll");

value = Assembly.LoadFrom(filePath);

if (value is null)
{
throw;
}
}

lock (_loadedAssemblies)
{
_loadedAssemblies[assemblyName] = value;
return value;
}
}

return value;
}

public Assembly LoadAssembly(string assemblyFullName, string? codeBasePath)
{
var assemblyName = new AssemblyName(assemblyFullName);
return LoadAssembly(assemblyName);
}

private class AssemblyNameComparer : IEqualityComparer<AssemblyName>
{
public static AssemblyNameComparer Instance = new AssemblyNameComparer();

public bool Equals(AssemblyName? x, AssemblyName? y)
{
if (x == null && y == null)
{
return true;
}

if (x == null || y == null)
{
return false;
}

return x.Name == y.Name;
}

public int GetHashCode([DisallowNull] AssemblyName obj)
{
return obj.Name?.GetHashCode() ?? 0;
}
}
}
Loading
Loading