Skip to content

Commit

Permalink
Improved experience, warn forget Execute and pdb
Browse files Browse the repository at this point in the history
  • Loading branch information
helto4real committed Apr 25, 2020
1 parent c631775 commit 5e59d95
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 9 deletions.
130 changes: 122 additions & 8 deletions src/DaemonRunner/DaemonRunner/Service/App/CodeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using JoySoftware.HomeAssistant.NetDaemon.DaemonRunner.Service.Config;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using System;
Expand Down Expand Up @@ -434,7 +436,7 @@ private void LoadLocalAssemblyApplicationsForDevelopment()
}
}

private void CompileScriptsInCodeFolder()
internal void CompileScriptsInCodeFolder()
{
// If provided code folder and we dont have local loaded daemon apps
if (!string.IsNullOrEmpty(_codeFolder) && _loadedDaemonApps.Count() == 0)
Expand All @@ -447,20 +449,26 @@ private void LoadAllCodeToLoadContext()
var alc = new CollectibleAssemblyLoadContext();

using (var peStream = new MemoryStream())
using (var symbolsStream = new MemoryStream())
{

var csFiles = GetCsFiles(_codeFolder);
if (csFiles.Count() == 0 && _loadedDaemonApps.Count() == 0)
{
// Only log when not have locally built assemblies, typically in dev environment
_logger.LogWarning("No .cs files files found, please add files to [netdaemonfolder]/apps");
}
var embeddedTexts = new List<EmbeddedText>();

foreach (var csFile in csFiles)
{
var sourceText = SourceText.From(File.ReadAllText(csFile));
var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: csFile);
syntaxTrees.Add(syntaxTree);
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));

var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, path: csFile);
syntaxTrees.Add(syntaxTree);
}
}

var metaDataReference = new List<MetadataReference>(10)
Expand All @@ -486,10 +494,24 @@ private void LoadAllCodeToLoadContext()
syntaxTrees.ToArray(),
references: metaDataReference.ToArray(),
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
optimizationLevel: OptimizationLevel.Release,
optimizationLevel: OptimizationLevel.Debug,
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default));

var emitResult = compilation.Emit(peStream);
foreach (var syntaxTree in syntaxTrees)
{
WarnIfExecuteIsMissing(syntaxTree, compilation);
}

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

var emitResult = compilation.Emit(
peStream: peStream,
pdbStream: symbolsStream,
embeddedTexts: embeddedTexts,
options: emitOptions);

if (emitResult.Success)
{
peStream.Seek(0, SeekOrigin.Begin);
Expand Down Expand Up @@ -517,12 +539,104 @@ private void LoadAllCodeToLoadContext()
_logger.LogError(err);
}
}
alc.Unload();

// Finally do cleanup and release memory
alc.Unload();
GC.Collect();
GC.WaitForPendingFinalizers();
}

/// <summary>
/// All NetDaemonApp methods that needs to be closed with Execute or ExecuteAsync
/// </summary>
private static string[] _executeWarningOnInvocationNames = new string[]
{
"Entity",
"Entities",
"Event",
"Events",
"InputSelect",
"InputSelects",
"MediaPlayer",
"MediaPlayers",
"Camera",
"Cameras",
"RunScript"
};

/// <summary>
/// Warn user if fluent command chain not ending with Execute or ExecuteAsync
/// </summary>
/// <param name="syntaxTree">The parsed syntax tree</param>
/// <param name="compilation">Compilated code</param>
private void WarnIfExecuteIsMissing(SyntaxTree syntaxTree, CSharpCompilation compilation)
{
var semModel = compilation.GetSemanticModel(syntaxTree);

var invocationExpressions = syntaxTree.GetRoot().DescendantNodes().OfType<InvocationExpressionSyntax>();
var linesReported = new List<int>();

foreach (var invocationExpression in invocationExpressions)
{
var symbol = (IMethodSymbol?)semModel?.GetSymbolInfo(invocationExpression).Symbol;
if (symbol is null)
continue;

if (string.IsNullOrEmpty(symbol?.Name) ||
_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;

// Now find top invocation to match whole expression
InvocationExpressionSyntax topInvocationExpression = invocationExpression;

if (symbol is object && symbol.ContainingType.Name == "NetDaemonApp")
{
var symbolName = symbol.Name;

SyntaxNode? parentInvocationExpression = invocationExpression.Parent;

while (parentInvocationExpression is object)
{
if (parentInvocationExpression is InvocationExpressionSyntax)
{
var parentSymbol = (IMethodSymbol?)semModel?.GetSymbolInfo(invocationExpression).Symbol;
if (parentSymbol?.Name == symbolName)
topInvocationExpression = (InvocationExpressionSyntax)parentInvocationExpression;

}
parentInvocationExpression = parentInvocationExpression.Parent;
}

// Now when we have the top InvocationExpression,
// lets check for Execute and ExecuteAsync
if (ExpressionContainsExecuteInvocations(topInvocationExpression) == false)
{
var x = syntaxTree.GetLineSpan(topInvocationExpression.Span);
if (linesReported.Contains(x.StartLinePosition.Line) == false)
{
_logger.LogWarning($"Missing Execute or ExecuteAsync in {syntaxTree.FilePath} ({x.StartLinePosition.Line},{x.StartLinePosition.Character}) near {topInvocationExpression.ToFullString()}");
linesReported.Add(x.StartLinePosition.Line);
}
}
}
}
}

// Todo: Refactor using something smarter than string match. In future use Roslyn
private bool ExpressionContainsExecuteInvocations(InvocationExpressionSyntax invocation)
{
var invocationString = invocation.ToFullString();

if (invocationString.Contains("ExecuteAsync()") || invocationString.Contains("Execute()"))
{
return true;
}

return false;
}

public static IEnumerable<string> GetCsFiles(string configFixturePath)
{
return Directory.EnumerateFiles(configFixturePath, "*.cs", SearchOption.AllDirectories);
Expand Down
17 changes: 16 additions & 1 deletion tests/NetDaemon.Daemon.Tests/DaemonRunner/App/DaemonAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,13 +338,28 @@ public async Task InstanceAppsThatHasCircularDependenciesShouldReturnNull()

moqDaemon.SetupGet(n => n.Logger).Returns(moqLogger.Logger);
var codeManager = CM(path);
codeManager.DaemonAppTypes.Append(typeof(AssmeblyDaemonApp));
// ACT
// ASSERT
var ex = await Assert.ThrowsAsync<ApplicationException>(async () => { await codeManager.InstanceAndInitApplications(moqDaemon.Object); });
Assert.Contains("Application dependencies is wrong", ex.Message);
}

[Fact]
public void InsanceAppsThatHasMissingExecuteShouldLogWarning()
{
// ARRANGE
var path = Path.Combine(FaultyAppPath, "ExecuteWarnings");
var moqDaemon = new Mock<INetDaemonHost>();
var moqLogger = new LoggerMock();

// ACT
var codeManager = new CodeManager(path, moqLogger.Logger);

// ASSERT
moqLogger.AssertLogged(LogLevel.Warning, Times.Exactly(13));

}

public static CodeManager CM(string path) => new CodeManager(path, new LoggerMock().Logger);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using JoySoftware.HomeAssistant.NetDaemon.Common;

/// <summary>
/// Greets (or insults) people when coming home :)
/// </summary>
public class MissingExecuteApp : NetDaemonApp
{
public override async Task InitializeAsync()
{
// Do nothing
Entity("Test");

Entity("Test").TurnOn();

await Entity("jalll").TurnOn()
.WithAttribute("test", "lslslsl").ExecuteAsync();

await TestFailInAMethod();

// Do rest of the needed commands the warning system needs to find
Event("test").Call((a, b) => Task.CompletedTask);
Events(n => n.EventId.StartsWith("hello_")).Call((a, b) => Task.CompletedTask);
InputSelect("i").SetOption("test");
InputSelects(n => n.EntityId == "lslsls").SetOption("test");
MediaPlayer("i").Play();
MediaPlayers(n => n.EntityId == "lslsls").PlayPause();
Camera("sasdds").Snapshot("asdas");
Cameras(n => n.EntityId == "lslsls").Snapshot("asdas");
RunScript("test");
}

private async Task TestFailInAMethod()
{
await Entity("jalll").TurnOn()
.WithAttribute("test", "lslslsl").ExecuteAsync();

Entity("jalll").WhenStateChange(to: "test")
.AndNotChangeFor(System.TimeSpan.FromSeconds(1))
.Call(async (e, n, o) =>
{
Entity("Test").TurnOn();
var x = new NestedClass(this);
await x.DoAThing();
});

}
}

public class NestedClass
{
private readonly NetDaemonApp _app;

public NestedClass(NetDaemonApp app)
{
_app = app;
}

public async Task DoAThing()
{
// Should find this error
_app.Entities(new string[] { "test.test", "test.test2" }).TurnOn();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

missing_executes:
class: MissingExecuteApp

0 comments on commit 5e59d95

Please sign in to comment.