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

API for runtime warnings #750

Merged
merged 2 commits into from Oct 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DotVVM.Framework.Runtime;
using Microsoft.Extensions.Logging;

namespace DotVVM.Framework.Hosting.AspNetCore
{
public class AspNetCoreLoggerWarningSink: IDotvvmWarningSink
{
private ILogger logger;

public AspNetCoreLoggerWarningSink(ILogger<AspNetCoreLoggerWarningSink> logger = null)
{
this.logger = logger;
}

public void RuntimeWarning(DotvvmRuntimeWarning warning)
{
logger?.Log(LogLevel.Warning, "{0}", warning);
}
}
}
Expand Up @@ -8,6 +8,8 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Builder;
using System.Reflection;
using DotVVM.Framework.Runtime;
using DotVVM.Framework.Hosting.AspNetCore;

namespace Microsoft.Extensions.DependencyInjection
{
Expand Down Expand Up @@ -62,6 +64,8 @@ private static void AddDotVVMServices(IServiceCollection services)
services.TryAddSingleton<IEnvironmentNameProvider, DotvvmEnvironmentNameProvider>();
services.TryAddScoped<DotvvmRequestContextStorage>(_ => new DotvvmRequestContextStorage());
services.TryAddScoped<IDotvvmRequestContext>(s => s.GetRequiredService<DotvvmRequestContextStorage>().Context);

services.AddTransient<IDotvvmWarningSink, AspNetCoreLoggerWarningSink>();
}
}
}
65 changes: 65 additions & 0 deletions src/DotVVM.Framework/Controls/DotvvmBindableObjectHelper.cs
@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using DotVVM.Framework.Binding;
using DotVVM.Framework.Binding.Expressions;
using DotVVM.Framework.Configuration;
using DotVVM.Framework.Hosting;

namespace DotVVM.Framework.Controls
{
Expand Down Expand Up @@ -43,5 +46,67 @@ public static class DotvvmBindableObjectHelper
public static TProperty GetValue<TControl, TProperty>(this TControl control, Expression<Func<TControl, TProperty>> prop)
where TControl : DotvvmBindableObject
=> control.GetValue<TProperty>(control.GetDotvvmProperty(prop));

internal static object TryGeyValue(this DotvvmBindableObject control, DotvvmProperty property)
{
try
{
return control.GetValue(property);
}
catch
{
return property.DefaultValue;
}
}

public static string DebugString(this DotvvmBindableObject control, DotvvmConfiguration config = null, bool multiline = true)
{
if (control == null) return "null";

config = config ?? (control.TryGeyValue(Internal.RequestContextProperty) as IDotvvmRequestContext)?.Configuration;

var type = control.GetType();
var properties = (from kvp in control.Properties
let p = kvp.Key
let rawValue = kvp.Value
where p.DeclaringType != typeof(Internal)
let isAttached = !p.DeclaringType.IsAssignableFrom(type)
orderby !isAttached, p.Name
let name = isAttached ? p.DeclaringType.Name + "." + p.Name : p.Name
let value = rawValue == null ? "<null>" :
rawValue is ITemplate ? "<a template>" :
rawValue is DotvvmBindableObject ? $"<control {rawValue.GetType()}>" :
rawValue is IEnumerable<DotvvmBindableObject> controlCollection ? $"<{controlCollection.Count()} controls>" :
rawValue is IEnumerable<object> collection ? string.Join(", ", collection) :
rawValue.ToString()
let croppedValue = value.Length > 41 ? value.Substring(0, 40) + "…" : value
select new { p, name, croppedValue, value, isAttached }
).ToArray();

var location = (file: control.TryGeyValue(Internal.MarkupFileNameProperty) as string, line: control.TryGeyValue(Internal.MarkupLineNumberProperty) as int? ?? -1);
var reg = config.Markup.Controls.FirstOrDefault(c => c.Namespace == type.Namespace && Type.GetType(c.Namespace + "." + type.Name + ", " + c.Assembly) == type) ??
config.Markup.Controls.FirstOrDefault(c => c.Namespace == type.Namespace) ??
config.Markup.Controls.FirstOrDefault(c => c.Assembly == type.Assembly.GetName().Name);
var ns = reg?.TagPrefix ?? "?";
var tagName = type == typeof(HtmlGenericControl) ? ((HtmlGenericControl)control).TagName : ns + ":" + type.Name;

var dothtmlString = $"<{tagName} ";
var prefixLength = dothtmlString.Length;

foreach (var p in properties)
{
if (multiline && p != properties[0])
dothtmlString += "\n" + new string(' ', prefixLength);
dothtmlString += $"{p.name}={p.croppedValue}";
}
dothtmlString += "/>";

var from = (location.file)
+ (location.line >= 0 ? ":" + location.line : "");
if (!String.IsNullOrWhiteSpace(from))
from = (multiline ? "\n" : " ") + "from " + from.Trim();

return dothtmlString + from;
}
}
}
2 changes: 2 additions & 0 deletions src/DotVVM.Framework/Controls/DotvvmControl.cs
Expand Up @@ -179,6 +179,8 @@ public virtual void Render(IHtmlWriter writer, IDotvvmRequestContext context)
{
this.Children.ValidateParentsLifecycleEvents(); // debug check

writer.SetErrorContext(this);

if (properties.Contains(PostBack.UpdateProperty))
{
AddDotvvmUniqueIdAttribute();
Expand Down
20 changes: 19 additions & 1 deletion src/DotVVM.Framework/Controls/HtmlWriter.cs
Expand Up @@ -10,6 +10,7 @@
using DotVVM.Framework.Hosting;
using DotVVM.Framework.Resources;
using DotVVM.Framework.Runtime;
using Microsoft.Extensions.DependencyInjection;

namespace DotVVM.Framework.Controls
{
Expand All @@ -19,13 +20,16 @@ namespace DotVVM.Framework.Controls
public class HtmlWriter : IHtmlWriter
{
private readonly TextWriter writer;
private readonly bool debug;
private readonly IDotvvmRequestContext requestContext;
private readonly bool debug;
private readonly bool enableWarnings;

private DotvvmBindableObject errorContext;
private List<(string name, string val, string separator, bool allowAppending)> attributes = new List<(string, string, string separator, bool allowAppending)>();
private OrderedDictionary dataBindAttributes = new OrderedDictionary();
private Stack<string> openTags = new Stack<string>();
private bool tagFullyOpen = true;
private RuntimeWarningCollector WarningCollector => requestContext.Services.GetRequiredService<RuntimeWarningCollector>();

public static bool IsSelfClosing(string s)
{
Expand Down Expand Up @@ -60,6 +64,13 @@ public HtmlWriter(TextWriter writer, IDotvvmRequestContext requestContext)
this.writer = writer;
this.requestContext = requestContext;
this.debug = requestContext.Configuration.Debug;
this.enableWarnings = this.WarningCollector.Enabled;
}

internal void Warn(string message, Exception ex = null)
{
Debug.Assert(this.enableWarnings);
this.WarningCollector.Warn(new DotvvmRuntimeWarning(message, ex, this.errorContext));
}

public static string GetSeparatorForAttribute(string attributeName)
Expand Down Expand Up @@ -191,6 +202,9 @@ public void RenderSelfClosingTag(string name)
{
RenderBeginTagCore(name);
writer.Write("/>");

if (this.enableWarnings && !IsSelfClosing(name))
Warn($"Element {name} is not self-closing but is rendered as so. It may be interpreted as a start tag without an end tag by the browsers.");
}

private Dictionary<string, string> attributeMergeTable = new Dictionary<string, string>(23);
Expand Down Expand Up @@ -352,6 +366,9 @@ public void RenderEndTag()
writer.Write("</");
writer.Write(tag);
writer.Write(">");

if (this.enableWarnings && IsSelfClosing(tag))
Warn($"Element {tag} is self-closing but contains content. The browser may interpret the start tag as self-closing and put the 'content' into its parent.");
}
else
{
Expand Down Expand Up @@ -379,6 +396,7 @@ public void WriteUnencodedText(string text)
writer.Write(text ?? "");
}

public void SetErrorContext(DotvvmBindableObject obj) => this.errorContext = obj;
}
public class HtmlElementInfo
{
Expand Down
7 changes: 6 additions & 1 deletion src/DotVVM.Framework/Controls/IHtmlWriter.cs
Expand Up @@ -69,5 +69,10 @@ public interface IHtmlWriter
/// This method is typically used from <see cref="IHtmlAttributeTransformer"/> implementations.
/// </summary>
void WriteHtmlAttribute(string attributeName, string attributeValue);

/// <summary>
/// Passes information about the currently rendered control to the HtmlWriter. Used only to provide more accurate error/warning/debug information, does not alter the main behavior.
/// </summary>
void SetErrorContext(DotvvmBindableObject obj);
}
}
}
17 changes: 17 additions & 0 deletions src/DotVVM.Framework/Controls/Infrastructure/BodyResourceLinks.cs
Expand Up @@ -7,6 +7,7 @@
using DotVVM.Framework.ResourceManagement;
using Newtonsoft.Json;
using DotVVM.Framework.ViewModel.Serialization;
using DotVVM.Framework.Runtime;

namespace DotVVM.Framework.Controls
{
Expand Down Expand Up @@ -36,7 +37,23 @@ protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext
window.dotvvm.domUtils.onDocumentReady(function () {{
window.dotvvm.init('root', {JsonConvert.ToString(CultureInfo.CurrentCulture.Name, '"', StringEscapeHandling.EscapeHtml)});
}});");
writer.WriteUnencodedText(RenderWarnings(context));
writer.RenderEndTag();
}

internal static string RenderWarnings(IDotvvmRequestContext context)
{
var result = "";
// propagate warnings to JS console
var collector = context.Services.GetService<RuntimeWarningCollector>();
if (!collector.Enabled) return result;

foreach (var w in collector.GetWarnings())
{
var msg = JsonConvert.ToString(w.ToString(), '"', StringEscapeHandling.EscapeHtml);
result += $"console.warn({msg});\n";
}
return result;
}
}
}
Expand Up @@ -65,7 +65,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi
services.TryAddSingleton<IHttpRedirectService, DefaultHttpRedirectService>();
services.TryAddSingleton<IExpressionToDelegateCompiler, DefaultExpressionToDelegateCompiler>();


services.TryAddScoped<RuntimeWarningCollector>();
services.TryAddScoped<AggregateRequestTracer, AggregateRequestTracer>();
services.TryAddScoped<ResourceManager, ResourceManager>();
services.TryAddSingleton(s => DotvvmConfiguration.CreateDefault(s));
Expand Down
2 changes: 1 addition & 1 deletion src/DotVVM.Framework/DotVVM.Framework.csproj
Expand Up @@ -25,7 +25,7 @@
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
<PreserveCompilationContext>true</PreserveCompilationContext>
<LangVersion>7.2</LangVersion>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<Content Include="..\DotVVM.Compiler\bin\$(Configuration)\net461\DotVVM.Compiler.exe">
Expand Down
3 changes: 3 additions & 0 deletions src/DotVVM.Framework/Hosting/DotvvmPresenter.cs
Expand Up @@ -272,6 +272,9 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context)
var postBackUpdates = OutputRenderer.RenderPostbackUpdatedControls(context, page);
ViewModelSerializer.AddPostBackUpdatedControls(context, postBackUpdates);

// resources must be added after the HTML is rendered - some controls may request resources in the render phase
ViewModelSerializer.AddNewResources(context);

await OutputRenderer.WriteViewModelResponse(context, page);
}
await requestTracer.TraceEvent(RequestTracingConstants.OutputRendered, context);
Expand Down
32 changes: 32 additions & 0 deletions src/DotVVM.Framework/Runtime/DotvvmRuntimeWarning.cs
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Utils;

namespace DotVVM.Framework.Runtime
{
public class DotvvmRuntimeWarning
{
public DotvvmRuntimeWarning(string message, Exception relatedException = null, DotvvmBindableObject relatedControl = null)
{
this.Message = message ?? throw new ArgumentNullException(nameof(message));
this.RelatedException = relatedException;
this.RelatedControl = relatedControl;
}

public string Message { get; }
public Exception RelatedException { get; }
public DotvvmBindableObject RelatedControl { get; }

public override string ToString()
{
var sections = new string[] {
Message,
RelatedControl?.Apply(c => "related to:\n" + c.DebugString()),
RelatedException?.ToString(),
};
return string.Join("\n\n", sections.Where(s => s is object));
}
}
}
7 changes: 7 additions & 0 deletions src/DotVVM.Framework/Runtime/IDotvvmWarningSink.cs
@@ -0,0 +1,7 @@
namespace DotVVM.Framework.Runtime
{
public interface IDotvvmWarningSink
{
void RuntimeWarning(DotvvmRuntimeWarning warning);
}
}
44 changes: 44 additions & 0 deletions src/DotVVM.Framework/Runtime/RuntimeWarningCollector.cs
@@ -0,0 +1,44 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using DotVVM.Framework.Configuration;
using DotVVM.Framework.Controls;
using Microsoft.Extensions.Options;

namespace DotVVM.Framework.Runtime
{

/// <summary>
/// A request-scoped service used to collect warnings for debugging. By default these warnings are displayed in browser console and pushed to ASP.NET Core logging (thus probably displayed in the web server console).
///
/// Although this is request scoped, it is thread-safe.
/// </summary>
public class RuntimeWarningCollector
{
IDotvvmWarningSink[] sinks;
ConcurrentStack<DotvvmRuntimeWarning> warnings = new ConcurrentStack<DotvvmRuntimeWarning>();
public bool Enabled { get; }

public RuntimeWarningCollector(IEnumerable<IDotvvmWarningSink> sinks, DotvvmConfiguration config)
{
this.Enabled = config.Debug;
this.sinks = sinks.ToArray();
}

public void Warn(DotvvmRuntimeWarning warning)
{
if (!Enabled) return;

this.warnings.Push(warning);
foreach (var s in sinks)
s.RuntimeWarning(warning);
}


public List<DotvvmRuntimeWarning> GetWarnings() => warnings.ToList();

// public void Warn(string message, Exception relatedException = null, DotvvmBindableObject relatedControl = null) =>
// Warn(message, relatedException, relatedControl);
}
}