Skip to content

Commit

Permalink
Part 2 in the move to IOCing the world.
Browse files Browse the repository at this point in the history
  • Loading branch information
asherw committed Aug 27, 2020
1 parent b782135 commit 12f1053
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 124 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace NetDaemon.Infrastructure.Extensions
{
public static class AssemblyExtensions
{
public static IEnumerable<Type> GetTypesWhereSubclassOf<T>(this Assembly assembly)
{
return assembly.GetTypes().Where(x => x.IsClass && x.IsSubclassOf(typeof(T)));
}
}
}
28 changes: 27 additions & 1 deletion src/DaemonRunner/DaemonRunner/NetDaemonExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Microsoft.AspNetCore.Hosting;
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NetDaemon.Common.Configuration;
using NetDaemon.Daemon.Config;
using NetDaemon.Service;
using NetDaemon.Service.App;

namespace NetDaemon
{
Expand All @@ -17,12 +19,36 @@ public static IHostBuilder UseNetDaemon(this IHostBuilder hostBuilder)
services.Configure<HomeAssistantSettings>(context.Configuration.GetSection("HomeAssistant"));
services.Configure<NetDaemonSettings>(context.Configuration.GetSection("NetDaemon"));
services.AddSingleton<IYamlConfig, YamlConfig>();
RegisterNetDaemonAssembly(services);
})
.ConfigureWebHostDefaults(webbuilder =>
{
webbuilder.UseKestrel(options => { });
webbuilder.UseStartup<ApiStartup>();
});
}

private static void RegisterNetDaemonAssembly(IServiceCollection services)
{
if (BypassLocalAssemblyLoading())
services.AddSingleton<IDaemonAppCompiler, LocalDaemonAppCompiler>();
else
services.AddSingleton<IDaemonAppCompiler, DaemonAppCompiler>();
}

private static bool BypassLocalAssemblyLoading()
{
var value = Environment.GetEnvironmentVariable("HASS_DISABLE_LOCAL_ASM");

if (string.IsNullOrWhiteSpace(value))
return false;

if (bool.TryParse(value, out var boolResult))
return boolResult;

return false;
}
}
}
43 changes: 43 additions & 0 deletions src/DaemonRunner/DaemonRunner/Service/App/DaemonAppCompiler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NetDaemon.Common;
using NetDaemon.Common.Configuration;
using NetDaemon.Infrastructure.Extensions;

namespace NetDaemon.Service.App
{
public class DaemonAppCompiler : IDaemonAppCompiler
{
private readonly ILogger<DaemonAppCompiler> _logger;
private readonly IOptions<NetDaemonSettings> _netDaemonSettings;

public DaemonAppCompiler(ILogger<DaemonAppCompiler> logger, IOptions<NetDaemonSettings> netDaemonSettings)
{
_logger = logger;
_netDaemonSettings = netDaemonSettings;
}

public IEnumerable<Type> GetApps()
{
var assembly = Load();
var apps = assembly.GetTypesWhereSubclassOf<NetDaemonAppBase>();

if (!apps.Any())
_logger.LogWarning("No .cs files found, please add files to {sourceFolder}/apps", _netDaemonSettings.Value.SourceFolder);

return apps;
}

public Assembly Load()
{
CollectibleAssemblyLoadContext alc;
var appFolder = Path.Combine(_netDaemonSettings.Value.SourceFolder!, "apps");
return DaemonCompiler.GetCompiledAppAssembly(out alc, appFolder!, _logger);
}
}
}
158 changes: 67 additions & 91 deletions src/DaemonRunner/DaemonRunner/Service/App/DaemonCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
using NetDaemon.Common;
using NetDaemon.Common.Reactive;
using NetDaemon.Daemon;
using NetDaemon.Infrastructure.Extensions;

[assembly: InternalsVisibleTo("NetDaemon.Daemon.Tests")]

namespace NetDaemon.Service.App
{

internal class CollectibleAssemblyLoadContext : AssemblyLoadContext
public class CollectibleAssemblyLoadContext : AssemblyLoadContext
{
public CollectibleAssemblyLoadContext() : base(isCollectible: true)
{
Expand All @@ -39,21 +39,6 @@ public static (IEnumerable<Type>, CollectibleAssemblyLoadContext?) GetDaemonApps
{
var loadedApps = new List<Type>(50);

// Load the internal apps (mainly for )
var disableLoadLocalAssemblies = Environment.GetEnvironmentVariable("HASS_DISABLE_LOCAL_ASM");
if (!(disableLoadLocalAssemblies is object && disableLoadLocalAssemblies == "true"))
{
var localApps = LoadLocalAssemblyApplicationsForDevelopment();
if (localApps is object)
loadedApps.AddRange(localApps);
}
if (loadedApps.Count() > 0)
{
// We do not want to get and compile the apps if it is includer
// this is typically when in dev environment
logger.LogInformation("Loading compiled built-in apps");
return (loadedApps, null);
}
CollectibleAssemblyLoadContext alc;
// Load the compiled apps
var (compiledApps, compileErrorText) = GetCompiledApps(out alc, codeFolder, logger);
Expand All @@ -68,29 +53,74 @@ public static (IEnumerable<Type>, CollectibleAssemblyLoadContext?) GetDaemonApps
return (loadedApps, alc);
}

private static IEnumerable<Type>? LoadLocalAssemblyApplicationsForDevelopment()
public static (IEnumerable<Type>?, string) GetCompiledApps(out CollectibleAssemblyLoadContext alc, string codeFolder, ILogger logger)
{
// Get daemon apps in entry assembly (mainly for development)
return Assembly.GetEntryAssembly()?.GetTypes()
.Where(type => type.IsClass && type.IsSubclassOf(typeof(NetDaemonAppBase)));
var assembly = GetCompiledAppAssembly(out alc, codeFolder, logger);

if (assembly == null)
return (null, "Compile error");

return (assembly.GetTypesWhereSubclassOf<NetDaemonAppBase>(), string.Empty);
}

public static Assembly GetCompiledAppAssembly(out CollectibleAssemblyLoadContext alc, string codeFolder, ILogger logger)
{
alc = new CollectibleAssemblyLoadContext();

try
{
var compilation = GetCsCompilation(codeFolder);

foreach (var syntaxTree in compilation.SyntaxTrees)
{
if (Path.GetFileName(syntaxTree.FilePath) != "_EntityExtensions.cs")
WarnIfExecuteIsMissing(syntaxTree, compilation, logger);

InterceptAppInfo(syntaxTree, compilation);
}

using (var peStream = new MemoryStream())
{
var emitResult = compilation.Emit(peStream: peStream);

if (emitResult.Success)
{
peStream.Seek(0, SeekOrigin.Begin);

return alc!.LoadFromStream(peStream);
}
else
{
PrettyPrintCompileError(emitResult, logger);

return null!;
}
}
}
finally
{
alc.Unload();
// Finally do cleanup and release memory
GC.Collect();
GC.WaitForPendingFinalizers();
}
}

private static List<SyntaxTree> LoadSyntaxTree(string codeFolder)
{
var result = new List<SyntaxTree>(50);

// Get the paths for all .cs files recurcivlely in app folder
// Get the paths for all .cs files recursively in app folder
var csFiles = Directory.EnumerateFiles(codeFolder, "*.cs", SearchOption.AllDirectories);


var embeddedTexts = new List<EmbeddedText>();
//var embeddedTexts = new List<EmbeddedText>();

foreach (var csFile in csFiles)
{
using (var fs = new FileStream(csFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
var sourceText = SourceText.From(fs, encoding: Encoding.UTF8, canBeEmbedded: true);
embeddedTexts.Add(EmbeddedText.FromSource(csFile, sourceText));
//embeddedTexts.Add(EmbeddedText.FromSource(csFile, sourceText));

var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: csFile);
result.Add(syntaxTree);
Expand Down Expand Up @@ -140,70 +170,19 @@ private static CSharpCompilation GetCsCompilation(string codeFolder)
var metaDataReference = GetDefaultReferences();


return CSharpCompilation.Create($"net_{Path.GetRandomFileName()}.dll",
return CSharpCompilation.Create(
$"net_{Path.GetRandomFileName()}.dll",
syntaxTrees.ToArray(),
references: metaDataReference.ToArray(),
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
options: new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
optimizationLevel: OptimizationLevel.Release,
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default));
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default
)
);
}

public static (IEnumerable<Type>?, string) GetCompiledApps(out CollectibleAssemblyLoadContext alc, string codeFolder, ILogger logger)
{

alc = new CollectibleAssemblyLoadContext();


try
{
var compilation = GetCsCompilation(codeFolder);

foreach (var syntaxTree in compilation.SyntaxTrees)
{
if (Path.GetFileName(syntaxTree.FilePath) != "_EntityExtensions.cs")
WarnIfExecuteIsMissing(syntaxTree, compilation, logger);

InterceptAppInfo(syntaxTree, compilation, logger);
}

// var emitOptions = new EmitOptions(
// debugInformationFormat: DebugInformationFormat.PortablePdb,
// pdbFilePath: "netdaemondynamic.pdb");

using (var peStream = new MemoryStream())
// using (var symbolsStream = new MemoryStream())
{
var emitResult = compilation.Emit(
peStream: peStream
// pdbStream: symbolsStream,
// embeddedTexts: embeddedTexts,
/*options: emitOptions*/);

if (emitResult.Success)
{
peStream.Seek(0, SeekOrigin.Begin);

var asm = alc!.LoadFromStream(peStream);
return (asm.GetTypes() // Get all types
.Where(type => type.IsClass && type.IsSubclassOf(typeof(NetDaemonAppBase))) // That is a app
, ""); // And return a list apps
}
else
{
return (null, PrettyPrintCompileError(emitResult));
}
}
}
finally
{
alc.Unload();
// Finally do cleanup and release memory
GC.Collect();
GC.WaitForPendingFinalizers();
}
}

private static string PrettyPrintCompileError(EmitResult emitResult)
private static void PrettyPrintCompileError(EmitResult emitResult, ILogger logger)
{
var msg = new StringBuilder();
msg.AppendLine($"Compiler error!");
Expand All @@ -215,13 +194,13 @@ private static string PrettyPrintCompileError(EmitResult emitResult)
msg.AppendLine(emitResultDiagnostic.ToString());
}
}
return msg.ToString();

logger.LogError(msg.ToString());
}
/// <summary>
/// All NetDaemonApp methods that needs to be closed with Execute or ExecuteAsync
/// </summary>
private static string[] _executeWarningOnInvocationNames = new string[]
private static readonly string[] ExecuteWarningOnInvocationNames = new string[]
{
"Entity",
"Entities",
Expand All @@ -236,7 +215,7 @@ private static string PrettyPrintCompileError(EmitResult emitResult)
"RunScript"
};

private static void InterceptAppInfo(SyntaxTree syntaxTree, CSharpCompilation compilation, ILogger logger)
private static void InterceptAppInfo(SyntaxTree syntaxTree, CSharpCompilation compilation)
{
var semModel = compilation.GetSemanticModel(syntaxTree);

Expand Down Expand Up @@ -285,7 +264,6 @@ private static void InterceptAppInfo(SyntaxTree syntaxTree, CSharpCompilation co
}

}
var linesReported = new List<int>();
}
/// <summary>
/// Warn user if fluent command chain not ending with Execute or ExecuteAsync
Expand All @@ -307,7 +285,7 @@ private static void WarnIfExecuteIsMissing(SyntaxTree syntaxTree, CSharpCompilat
continue;

if (string.IsNullOrEmpty(symbol?.Name) ||
_executeWarningOnInvocationNames.Contains(symbol?.Name) == false)
ExecuteWarningOnInvocationNames.Contains(symbol?.Name) == false)
// The invocation name is empty or not in list of invocations
// that needs to be closed with Execute or ExecuteAsync
continue;
Expand All @@ -317,8 +295,6 @@ private static void WarnIfExecuteIsMissing(SyntaxTree syntaxTree, CSharpCompilat

if (symbol is object && symbol.ContainingType.Name == "NetDaemonApp")
{
var comment = symbol.GetDocumentationCommentXml();
System.Console.WriteLine("HELLO COMMENT: " + comment);
var disableLogging = false;

var symbolName = symbol.Name;
Expand Down
17 changes: 17 additions & 0 deletions src/DaemonRunner/DaemonRunner/Service/App/IDaemonAppCompiler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Reflection;

namespace NetDaemon.Service.App
{
public interface IDaemonAppCompiler
{
/// <summary>
/// Temporary
/// </summary>
/// <returns></returns>
[Obsolete("Only exists while migrating the world to IOC.")]
IEnumerable<Type> GetApps();
Assembly Load();
}
}
Loading

0 comments on commit 12f1053

Please sign in to comment.