using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using SDKDocGenerator.Writers; using System.Diagnostics; using System.Reflection; namespace SDKDocGenerator { /// /// Wraps the generation information required to emit documentation for /// a service, which may have assemblies in all or some of the platforms /// subfolders. /// public class GenerationManifest { /// /// The expected naming pattern of AWS generated assemblies is 'awssdk.*.dll' with /// the variable part being the service name (long or short form). /// public const string AWSAssemblyNamePrefix = "AWSSDK"; /// /// The preferred platform against which we generate documentation, if it's available. /// public const string PreferredPlatform = "net45"; /// /// Represents a single service, supported on one or more platforms, that we will be /// generating documentation for. /// /// /// The full path and filename of the assembly. The .Net platform for the assembly /// is assumed to be the name of the folder containing the assembly. The name of the /// service will be inferred from the name pattern of the assembly. /// /// /// The root output folder that the artifacts should be placed in. A further subfolder /// representing the service (or 'core' if the assembly is the runtime) is added. /// /// The set of platform subfolders to use to discover ndoc tables /// The user options governing doc generation /// public GenerationManifest(string assemblyPath, string outputFolderRoot, IEnumerable allPlatforms, GeneratorOptions options, bool useAppDomain) { AssemblyPath = Path.GetFullPath(assemblyPath); AssemblyName = Path.GetFileNameWithoutExtension(AssemblyPath); ServiceName = AssemblyName.StartsWith(AWSAssemblyNamePrefix + ".", StringComparison.OrdinalIgnoreCase) ? AssemblyName.Substring(AWSAssemblyNamePrefix.Length + 1) : AssemblyName; Options = options; OutputFolder = Path.GetFullPath(outputFolderRoot); AllPlatforms = allPlatforms; UseAppDomain = useAppDomain; if (Options.Verbose) { Trace.WriteLine("\tConstructed GenerationManifest:"); Trace.WriteLine($"\t...AssemblyPath: {AssemblyPath}"); Trace.WriteLine($"\t...ServiceName: {ServiceName}"); Trace.WriteLine($"\t...OutputFolder: {OutputFolder}"); } } /// /// The path and filename to the assembly represented by this set of artifacts /// public string AssemblyPath { get; } /// /// The basename of the sdk assembly we're generating docs for, less extension. /// public string AssemblyName { get; } public IEnumerable AllPlatforms { get; } /// /// The root output folder; the artifacts will be placed under /// here in their top-level namespace subfolders (less the Amazon. /// part). So Amazon.DynamoDBv2.* types get emitted to 'dynamodbv2' /// underneath this root. /// public string OutputFolder { get; } /// /// The generation options specified by the user /// public GeneratorOptions Options { get; } /// /// If true, sdk assemblies are loaded into a separate app domain prior to doc /// generation. /// public bool UseAppDomain { get; } /// /// Returns the discovered NDoc table for a given platform, if it existed. If platform /// is not specified, we attempt to return the NDoc for the primary platform specified /// in the generator options. /// /// /// public IDictionary NDocForPlatform(string platform = null) { if (string.IsNullOrEmpty(platform)) { platform = Options.Platform; } return NDocUtilities.GetDocumentationInstance(ServiceName, platform); } /// /// The logical name of the service. This is assumed to exist /// in the assembly name after 'awssdk.' and before the extension. /// public string ServiceName { get; } private ManifestAssemblyWrapper _manifestAssemblyWrapper; /// /// Contains the wrapped sdk assembly and the app domain we loaded it /// into, if so configured. Dispose of this when processing of the /// assembly is complete if the assembly contained no types that need /// deferred processing alongside the core sdk assembly. /// public ManifestAssemblyWrapper ManifestAssemblyContext { get { if (_manifestAssemblyWrapper == null) { _manifestAssemblyWrapper = new ManifestAssemblyWrapper(ServiceName, Options.Platform, AssemblyPath, UseAppDomain); } return _manifestAssemblyWrapper; } private set { if (value == null) { _manifestAssemblyWrapper?.Dispose(); } _manifestAssemblyWrapper = value; } } /// /// Returns the subfolder name that should be used for Amazon artifacts /// belonging to the specified namespace. Typically we use the service /// root level (the second part) in the namespace. If the namespace is /// not recognized as belonging to Amazon, an empty string is returned. /// /// The namespace of a discovered type (Amazon or 3rd party) /// public static string OutputSubFolderFromNamespace(string ns) { if (ns.IndexOf('.') == -1) return ns; if (!ns.StartsWith("Amazon.")) return string.Empty; var nsParts = ns.Split('.'); string nsPart; switch (nsParts.Length) { case 0: nsPart = ns; break; case 1: nsPart = nsParts[0]; break; default: nsPart = nsParts[1]; break; } if (FilenameGenerator.ServiceNamespaceContractions.ContainsKey(nsPart)) return FilenameGenerator.ServiceNamespaceContractions[nsPart]; return nsPart; } /// /// Generates the documentation for the artifacts represented by this /// manifest, starting at the namespace(s) in the assembly and working /// down through the type hierarchy. Types that exist in the deferable /// namespaces will be processed later in generation, when the awssdk.core /// assembly is processed. /// /// /// Collection for types in service assemblies that we want to defer processing /// on until we process awssdk.core. /// /// /// Toc generation handler to which each processed namespace is registered /// public void Generate(DeferredTypesProvider deferrableTypes, TOCWriter tocWriter) { Trace.WriteLine($"\tgenerating from {Options.Platform}/{Path.GetFileName(AssemblyPath)}"); // load the assembly and ndoc dataset for the service we're about to generate; assuming // they contain no deferrable types we'll release them when done var discardAssemblyOnExit = true; foreach (var platform in AllPlatforms) { NDocUtilities.LoadDocumentation(AssemblyName, ServiceName, platform, Options); } var namespaceNames = ManifestAssemblyContext.SdkAssembly.GetNamespaces(); var frameworkVersion = FrameworkVersion.FromPlatformFolder(Options.Platform); var processed = 0; foreach (var namespaceName in namespaceNames) { // when processing awssdk.core, we don't get handed a collection to hold // deferrable types if (deferrableTypes != null) { if (deferrableTypes.Namespaces.Contains(namespaceName)) { var types = ManifestAssemblyContext.SdkAssembly.GetTypesForNamespace(namespaceName); if (types.Any()) { Trace.WriteLine($"\t\tdeferring processing of types in namespace {namespaceName} for {Path.GetFileName(AssemblyPath)}"); deferrableTypes.AddTypes(types); discardAssemblyOnExit = false; } continue; } } WriteNamespace(frameworkVersion, namespaceName); tocWriter.BuildNamespaceToc(namespaceName, ManifestAssemblyContext.SdkAssembly); Trace.WriteLine($"\t\t{namespaceName} processed ({++processed} of {namespaceNames.Count()})"); } if (discardAssemblyOnExit) { // release artifact roots for future GC collections to operate on foreach (var platform in AllPlatforms) { NDocUtilities.UnloadDocumentation(ServiceName, platform); } ManifestAssemblyContext.Dispose(); ManifestAssemblyContext = null; } } void WriteNamespace(FrameworkVersion version, string namespaceName) { var writer = new NamespaceWriter(this, version, namespaceName); writer.Write(); foreach (var type in ManifestAssemblyContext.SdkAssembly.GetTypesForNamespace(namespaceName)) { WriteType(version, type); } } void WriteType(FrameworkVersion version, TypeWrapper type) { var writer = new ClassWriter(this, version, type); writer.Write(); foreach (var item in type.GetConstructors().Where(x => x.IsPublic)) { var itemWriter = new ConstructorWriter(this, version, item); itemWriter.Write(); } foreach (var item in type.GetMethodsToDocument()) { // If a method is in another namespace, it is inherited and should not be overwritten if (item.DeclaringType.Namespace == type.Namespace) { var itemWriter = new MethodWriter(this, version, item); itemWriter.Write(); } } foreach (var item in type.GetEvents()) { // If an event is in another namespace, it is inherited and should not be overwritten if (item.DeclaringType.Namespace == type.Namespace) { var itemWriter = new EventWriter(this, version, item); itemWriter.Write(); } } } public class ManifestAssemblyWrapper : IDisposable { public AssemblyWrapper SdkAssembly { get; private set; } public AppDomain Domain { get; private set; } public ManifestAssemblyWrapper(string serviceName, string platform, string assemblyPath, bool useNewAppDomain) { var docId = NDocUtilities.GenerateDocId(serviceName, platform); if (useNewAppDomain) { Domain = AppDomain.CreateDomain(assemblyPath); var inst = Domain.CreateInstance(this.GetType().Assembly.FullName, typeof(AssemblyWrapper).FullName, true, BindingFlags.CreateInstance | BindingFlags.Public | BindingFlags.Instance, null, new object[] { docId }, null, null); SdkAssembly = (AssemblyWrapper)inst.Unwrap(); SdkAssembly.LoadAssembly(assemblyPath); } else { SdkAssembly = new AssemblyWrapper(docId); SdkAssembly.LoadAssembly(assemblyPath); } } #region IDisposable Support private bool _disposedValue; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { // unlink roots, then drop the domain if we used one SdkAssembly = null; if (Domain != null) { AppDomain.Unload(Domain); Domain = null; } } _disposedValue = true; } } public void Dispose() { // we have no finalizer and/or unmanaged resources, so no need to use GC.SuppressFinalize() Dispose(true); } #endregion } } }