diff --git a/src/Orleans.Core/Orleans.Core.csproj b/src/Orleans.Core/Orleans.Core.csproj index 58d7786ea85..c5d9cdce083 100644 --- a/src/Orleans.Core/Orleans.Core.csproj +++ b/src/Orleans.Core/Orleans.Core.csproj @@ -7,6 +7,7 @@ Orleans true README.md + true diff --git a/src/Orleans.Core/Runtime/RuntimeVersion.cs b/src/Orleans.Core/Runtime/RuntimeVersion.cs index d162763309c..5dea03f27c1 100644 --- a/src/Orleans.Core/Runtime/RuntimeVersion.cs +++ b/src/Orleans.Core/Runtime/RuntimeVersion.cs @@ -6,23 +6,20 @@ namespace Orleans.Runtime internal static class RuntimeVersion { /// - /// The full version string of the Orleans runtime, eg: '2012.5.9.51607 Build:12345 Timestamp: 20120509-185359' + /// The informational version of the Orleans runtime and its build configuration, + /// or the assembly version when informational version metadata is unavailable. /// - public static string Current + public static string Current => GetVersion(typeof(RuntimeVersion).Assembly); + + internal static string GetVersion(Assembly assembly) { - get - { - Assembly thisProg = typeof(RuntimeVersion).Assembly; - var ApiVersion = thisProg.GetName().Version!.ToString(); - if (string.IsNullOrWhiteSpace(thisProg.Location)) - { - return ApiVersion; - } - FileVersionInfo progVersionInfo = FileVersionInfo.GetVersionInfo(thisProg.Location); - bool isDebug = IsAssemblyDebugBuild(thisProg); - string productVersion = progVersionInfo.ProductVersion + (isDebug ? " (Debug)." : " (Release)."); // progVersionInfo.IsDebug; does not work - return string.IsNullOrEmpty(productVersion) ? ApiVersion : productVersion; - } + ArgumentNullException.ThrowIfNull(assembly); + + var apiVersion = assembly.GetName().Version!.ToString(); + var productVersion = assembly.GetCustomAttribute()?.InformationalVersion; + return string.IsNullOrWhiteSpace(productVersion) + ? apiVersion + : productVersion + (IsAssemblyDebugBuild(assembly) ? " (Debug)." : " (Release)."); } /// diff --git a/src/Orleans.Serialization/Hosting/ReferencedAssemblyProvider.cs b/src/Orleans.Serialization/Hosting/ReferencedAssemblyProvider.cs index 344d75bd08e..033b9bd58f8 100644 --- a/src/Orleans.Serialization/Hosting/ReferencedAssemblyProvider.cs +++ b/src/Orleans.Serialization/Hosting/ReferencedAssemblyProvider.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -23,8 +24,17 @@ public static class ReferencedAssemblyProvider public static IEnumerable GetRelevantAssemblies() { var parts = new HashSet(); + var entryAssembly = Assembly.GetEntryAssembly(); - AddFromDependencyContext(parts); + if (entryAssembly is not null) + { + AddAssembly(parts, entryAssembly); + } + + if (AssemblyFilesAvailable) + { + AddFromDependencyContext(parts, entryAssembly); + } #if NETCOREAPP3_1_OR_GREATER AddFromAssemblyLoadContext(parts); @@ -60,8 +70,6 @@ public static void AddAssembly(HashSet parts, Assembly assembly) return; } - AddAssembly(parts, assembly); - // Add all referenced application parts. foreach (var referencedAsm in GetApplicationPartAssemblies(assembly)) { @@ -120,6 +128,7 @@ public static void AddFromAssemblyLoadContext(HashSet parts, Assembly? /// /// The assembly whose dependency context is inspected, or to use the entry assembly. /// + [RequiresAssemblyFiles("Dependency-context discovery reads assembly files. Use " + nameof(GetRelevantAssemblies) + " for single-file-compatible discovery.")] public static void AddFromDependencyContext(HashSet parts, Assembly? assembly = null) { assembly ??= Assembly.GetEntryAssembly(); @@ -193,6 +202,25 @@ public static void AddFromDependencyContext(HashSet parts, Assembly? a } } +#if NET9_0_OR_GREATER + [FeatureGuard(typeof(RequiresAssemblyFilesAttribute))] +#endif + internal static bool AssemblyFilesAvailable => AreAssemblyFilesAvailable(Assembly.GetEntryAssembly()); + +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage( + "SingleFile", + "IL3000", + Justification = "Assembly.Location is used only as the documented availability check for bundled assembly files.")] +#endif + internal static bool AreAssemblyFilesAvailable(Assembly? entryAssembly) + { + var assembly = entryAssembly is null || entryAssembly.IsDynamic + ? typeof(ReferencedAssemblyProvider).Assembly + : entryAssembly; + return File.Exists(assembly.Location); + } + private static IEnumerable GetApplicationPartAssemblies(Assembly assembly) { if (!assembly.IsDefined(typeof(ApplicationPartAttribute))) diff --git a/src/Orleans.Serialization/Orleans.Serialization.csproj b/src/Orleans.Serialization/Orleans.Serialization.csproj index 43abd2aa35b..d59aa1ab2f8 100644 --- a/src/Orleans.Serialization/Orleans.Serialization.csproj +++ b/src/Orleans.Serialization/Orleans.Serialization.csproj @@ -8,6 +8,7 @@ true false README.md + true diff --git a/src/Orleans.Serialization/Properties/RequiresAssemblyFilesAttribute.cs b/src/Orleans.Serialization/Properties/RequiresAssemblyFilesAttribute.cs new file mode 100644 index 00000000000..e767b1f2b7d --- /dev/null +++ b/src/Orleans.Serialization/Properties/RequiresAssemblyFilesAttribute.cs @@ -0,0 +1,23 @@ +#if NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Event | AttributeTargets.Method | AttributeTargets.Property, + Inherited = false, + AllowMultiple = false)] +internal sealed class RequiresAssemblyFilesAttribute : Attribute +{ + public RequiresAssemblyFilesAttribute() + { + } + + public RequiresAssemblyFilesAttribute(string message) + { + Message = message; + } + + public string? Message { get; } + + public string? Url { get; set; } +} +#endif diff --git a/src/api/Orleans.Serialization/Orleans.Serialization.cs b/src/api/Orleans.Serialization/Orleans.Serialization.cs index a1165ae43b1..2760519c249 100644 --- a/src/api/Orleans.Serialization/Orleans.Serialization.cs +++ b/src/api/Orleans.Serialization/Orleans.Serialization.cs @@ -3451,6 +3451,7 @@ public static void AddFromAssemblyLoadContext(System.Collections.Generic.HashSet public static void AddFromAssemblyLoadContext(System.Collections.Generic.HashSet parts, System.Runtime.Loader.AssemblyLoadContext context) { } + [System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute("Dependency-context discovery reads assembly files. Use GetRelevantAssemblies for single-file-compatible discovery.")] public static void AddFromDependencyContext(System.Collections.Generic.HashSet parts, System.Reflection.Assembly? assembly = null) { } public static System.Collections.Generic.IEnumerable GetRelevantAssemblies() { throw null; } diff --git a/test/Orleans.Core.Tests/RuntimeVersionTests.cs b/test/Orleans.Core.Tests/RuntimeVersionTests.cs new file mode 100644 index 00000000000..c64634625e2 --- /dev/null +++ b/test/Orleans.Core.Tests/RuntimeVersionTests.cs @@ -0,0 +1,60 @@ +using System.Diagnostics; +using System.Reflection; +using System.Reflection.Emit; +using Orleans.Runtime; +using Xunit; + +namespace NonSilo.Tests; + +[TestCategory("BVT")] +[TestSuite("BVT")] +[TestProvider("None")] +[TestArea("Runtime")] +public sealed class RuntimeVersionTests +{ + [Fact] + public void GetVersionUsesInformationalVersionMetadataForFilelessAssembly() + { + var assembly = CreateAssembly( + new Version(1, 2, 3, 4), + informationalVersion: "9.8.7+metadata", + isDebug: true); + + var result = RuntimeVersion.GetVersion(assembly); + + Assert.Equal("9.8.7+metadata (Debug).", result); + } + + [Fact] + public void GetVersionUsesAssemblyVersionWhenInformationalVersionIsUnavailable() + { + var assembly = CreateAssembly(new Version(1, 2, 3, 4)); + + var result = RuntimeVersion.GetVersion(assembly); + + Assert.Equal("1.2.3.4", result); + } + + private static Assembly CreateAssembly( + Version version, + string? informationalVersion = null, + bool isDebug = false) + { + var name = new AssemblyName($"RuntimeVersionTests_{Guid.NewGuid():N}") { Version = version }; + var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); + if (informationalVersion is not null) + { + var constructor = typeof(AssemblyInformationalVersionAttribute).GetConstructor([typeof(string)])!; + assembly.SetCustomAttribute(new CustomAttributeBuilder(constructor, [informationalVersion])); + } + + if (isDebug) + { + var constructor = typeof(DebuggableAttribute).GetConstructor([typeof(DebuggableAttribute.DebuggingModes)])!; + var modes = DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations; + assembly.SetCustomAttribute(new CustomAttributeBuilder(constructor, [modes])); + } + + return assembly; + } +} diff --git a/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj b/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj index a210f5a211b..16942d7f27c 100644 --- a/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj +++ b/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj @@ -40,4 +40,19 @@ + + + + + diff --git a/test/Orleans.Serialization.UnitTests/ReferencedAssemblyProviderTests.cs b/test/Orleans.Serialization.UnitTests/ReferencedAssemblyProviderTests.cs new file mode 100644 index 00000000000..e71cd1beda7 --- /dev/null +++ b/test/Orleans.Serialization.UnitTests/ReferencedAssemblyProviderTests.cs @@ -0,0 +1,158 @@ +using System; +#if NET10_0_OR_GREATER +using System.Buffers.Binary; +#endif +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +#if NET10_0_OR_GREATER +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +#endif +using Orleans.Serialization.Internal; + +namespace Orleans.Serialization.UnitTests; + +[Trait("Category", "BVT")] +[TestSuite("BVT")] +[TestProvider("None")] +[TestArea("Serialization")] +public sealed class ReferencedAssemblyProviderTests +{ + [Fact] + public void AddAssemblyDiscoversGeneratedApplicationPartsWithoutAssemblyFiles() + { + var referencedAssembly = typeof(ReferencedAssemblyProviderTests).Assembly; + var assembly = CreateApplicationPartAssembly(referencedAssembly.GetName().Name!); + var result = new HashSet(); + + ReferencedAssemblyProvider.AddAssembly(result, assembly); + + Assert.True(assembly.IsDynamic); + Assert.Contains(assembly, result); + Assert.Contains(referencedAssembly, result); + } + + [Fact] + public void GetRelevantAssembliesIncludesLoadedGeneratedAssembly() + { + var assembly = typeof(ReferencedAssemblyProviderTests).Assembly; + var result = ReferencedAssemblyProvider.GetRelevantAssemblies().ToHashSet(); + + Assert.True(assembly.IsDefined(typeof(ApplicationPartAttribute))); + Assert.Contains(assembly, result); + } + + [Fact] + public void DependencyContextDiscoveryExposesAssemblyFileRequirement() + { + var method = typeof(ReferencedAssemblyProvider).GetMethod( + nameof(ReferencedAssemblyProvider.AddFromDependencyContext)); + + var attribute = method!.GetCustomAttribute(); + + Assert.NotNull(attribute); + Assert.Contains(nameof(ReferencedAssemblyProvider.GetRelevantAssemblies), attribute!.Message); + } + + [Fact] + public void AssemblyFileAvailabilityUsesProviderAssemblyForDynamicEntryAssembly() + { + var dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName($"DynamicEntryAssembly_{Guid.NewGuid():N}"), + AssemblyBuilderAccess.Run); + + Assert.True(ReferencedAssemblyProvider.AreAssemblyFilesAvailable(null)); + Assert.Equal( + ReferencedAssemblyProvider.AreAssemblyFilesAvailable(null), + ReferencedAssemblyProvider.AreAssemblyFilesAvailable(dynamicAssembly)); + } + + [Fact] + public void AssemblyFileAvailabilityIsFalseForMemoryLoadedAssembly() + { + var assembly = Assembly.Load(File.ReadAllBytes(typeof(Enumerable).Assembly.Location)); + + Assert.Empty(assembly.Location); + Assert.False(ReferencedAssemblyProvider.AreAssemblyFilesAvailable(assembly)); + } + +#if NET10_0_OR_GREATER + [Fact] + public void NetStandardAssetDependencyContextDiscoveryExposesAssemblyFileRequirement() + { + var assemblyPath = Path.Combine( + AppContext.BaseDirectory, + "TestAssets", + "Orleans.Serialization.dll"); + using var stream = File.OpenRead(assemblyPath); + using var peReader = new PEReader(stream); + var metadata = peReader.GetMetadataReader(); + var providerType = metadata.TypeDefinitions + .Select(metadata.GetTypeDefinition) + .Single(type => + metadata.GetString(type.Namespace) == "Orleans.Serialization.Internal" + && metadata.GetString(type.Name) == nameof(ReferencedAssemblyProvider)); + var method = providerType.GetMethods() + .Select(metadata.GetMethodDefinition) + .Single(method => metadata.GetString(method.Name) == nameof(ReferencedAssemblyProvider.AddFromDependencyContext)); + var attributeNames = method.GetCustomAttributes() + .Select(handle => GetAttributeTypeName(metadata, metadata.GetCustomAttribute(handle))); + var assemblyFilesAvailableGetter = providerType.GetMethods() + .Single(handle => metadata.GetString(metadata.GetMethodDefinition(handle).Name) == "get_AssemblyFilesAvailable"); + var getRelevantAssemblies = providerType.GetMethods() + .Select(handle => (Handle: handle, Definition: metadata.GetMethodDefinition(handle))) + .Single(method => metadata.GetString(method.Definition.Name) == nameof(ReferencedAssemblyProvider.GetRelevantAssemblies)); + var methodBody = peReader.GetMethodBody(getRelevantAssemblies.Definition.RelativeVirtualAddress); + var expectedCall = new byte[5]; + expectedCall[0] = 0x28; + BinaryPrimitives.WriteInt32LittleEndian( + expectedCall.AsSpan(1), + MetadataTokens.GetToken(assemblyFilesAvailableGetter)); + + Assert.Contains( + "System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute", + attributeNames); + Assert.True(methodBody.GetILBytes().AsSpan().IndexOf(expectedCall) >= 0); + } +#endif + + private static Assembly CreateApplicationPartAssembly(string referencedAssemblyName) + { + var name = new AssemblyName($"ReferencedAssemblyProviderTests_{Guid.NewGuid():N}"); + var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); + var constructor = typeof(ApplicationPartAttribute).GetConstructor([typeof(string)])!; + assembly.SetCustomAttribute(new CustomAttributeBuilder(constructor, [referencedAssemblyName])); + assembly.DefineDynamicModule("Main"); + return assembly; + } + +#if NET10_0_OR_GREATER + private static string GetAttributeTypeName(MetadataReader metadata, CustomAttribute attribute) + { + var typeHandle = attribute.Constructor.Kind switch + { + HandleKind.MemberReference => metadata.GetMemberReference((MemberReferenceHandle)attribute.Constructor).Parent, + HandleKind.MethodDefinition => metadata.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor).GetDeclaringType(), + _ => throw new InvalidOperationException($"Unsupported attribute constructor handle: {attribute.Constructor.Kind}."), + }; + + return typeHandle.Kind switch + { + HandleKind.TypeDefinition => GetTypeName(metadata, metadata.GetTypeDefinition((TypeDefinitionHandle)typeHandle)), + HandleKind.TypeReference => GetTypeName(metadata, metadata.GetTypeReference((TypeReferenceHandle)typeHandle)), + _ => throw new InvalidOperationException($"Unsupported attribute type handle: {typeHandle.Kind}."), + }; + } + + private static string GetTypeName(MetadataReader metadata, TypeDefinition type) + => $"{metadata.GetString(type.Namespace)}.{metadata.GetString(type.Name)}"; + + private static string GetTypeName(MetadataReader metadata, TypeReference type) + => $"{metadata.GetString(type.Namespace)}.{metadata.GetString(type.Name)}"; +#endif +}