diff --git a/src/Samples/AspNetCore/Startup.cs b/src/Samples/AspNetCore/Startup.cs index 4c569954b..097957df9 100644 --- a/src/Samples/AspNetCore/Startup.cs +++ b/src/Samples/AspNetCore/Startup.cs @@ -94,6 +94,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF } private string GetApplicationPath(IWebHostEnvironment env) - => Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common"); + { + var common = Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common"); + if (Directory.Exists(common)) + { + return common; + } + if (File.Exists(Path.Combine(env.ContentRootPath, "Views/Default.dothtml"))) + { + return env.ContentRootPath; + } + throw new DirectoryNotFoundException("Cannot find the 'Common' directory nor the 'Views' directory in the application root."); + } } } diff --git a/src/Samples/AspNetCoreLatest/Startup.cs b/src/Samples/AspNetCoreLatest/Startup.cs index cb24824f6..4342847d5 100644 --- a/src/Samples/AspNetCoreLatest/Startup.cs +++ b/src/Samples/AspNetCoreLatest/Startup.cs @@ -105,6 +105,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF } private string GetApplicationPath(IWebHostEnvironment env) - => Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common"); + { + if (File.Exists(Path.Combine(env.ContentRootPath, "Views/Default.dothtml"))) + { + return env.ContentRootPath; + } + var common = Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common"); + if (File.Exists(Path.Combine(common, "Views/Default.dothtml"))) + { + return common; + } + throw new DirectoryNotFoundException("Cannot find the 'Common' directory nor the 'Views' directory in the application root."); + } } } diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index 216f55da7..41bcf2a7e 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -19,167 +19,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index fc32374ec..9172d4433 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -31,6 +31,7 @@ using DotVVM.Samples.Common.ViewModels.FeatureSamples.BindingVariables; using DotVVM.Samples.Common.Views.ControlSamples.TemplateHost; using DotVVM.Framework.ResourceManagement; +using DotVVM.Samples.Common.Presenters; using DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes; namespace DotVVM.Samples.BasicSamples @@ -251,6 +252,8 @@ private static void AddRoutes(DotvvmConfiguration config) config.RouteTable.Add("FeatureSamples_PostBack_PostBackHandlers_Localized", "FeatureSamples/PostBack/PostBackHandlers_Localized", "Views/FeatureSamples/PostBack/ConfirmPostBackHandler.dothtml", presenterFactory: LocalizablePresenter.BasedOnQuery("lang")); config.RouteTable.Add("Errors_UndefinedRouteLinkParameters-PageDetail", "Erros/UndefinedRouteLinkParameters/{Id}", "Views/Errors/UndefinedRouteLinkParameters.dothtml", new { Id = 0 }); + + config.RouteTable.Add("DumpExtensionsMethods", "dump-extension-methods", _ => new DumpExtensionMethodsPresenter()); } private static void AddControls(DotvvmConfiguration config) diff --git a/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs b/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs new file mode 100644 index 000000000..6b418fb28 --- /dev/null +++ b/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.Compilation; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; + +namespace DotVVM.Samples.Common.Presenters +{ + public class DumpExtensionMethodsPresenter : IDotvvmPresenter + { + public async Task ProcessRequest(IDotvvmRequestContext context) + { + var cache = context.Configuration.ServiceProvider.GetService(); + + var contents = typeof(ExtensionMethodsCache) + .GetField("methodsCache", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(cache) as ConcurrentDictionary>; + + var dump = contents.SelectMany(p => p.Value.Select(m => new { + Namespace = p.Key, + m.Name, + m.DeclaringType!.FullName, + Params = m.GetParameters().Select(p => new { + p.Name, + Type = p.ParameterType!.FullName + }), + m.IsGenericMethodDefinition, + GenericParameters = m.IsGenericMethodDefinition ? m.GetGenericArguments().Select(a => new { + a.Name + }) : null + })) + .OrderBy(m => m.Namespace).ThenBy(m => m.Name); + + await context.HttpContext.Response.WriteAsync("ExtensionMethodsCache dump: " + JsonConvert.SerializeObject(dump, Formatting.Indented)); + } + } +} diff --git a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs index 164bf1fa7..3f9807656 100644 --- a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs +++ b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs @@ -1,6 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Net.Http; using System.Threading; +using System.Threading.Tasks; using DotVVM.Samples.Tests.Base; using DotVVM.Testing.Abstractions; using OpenQA.Selenium; @@ -814,7 +818,7 @@ public void Feature_List_Translation_Remove_Range() Assert.Equal(new List { "1", "2", "8", "9", "10" }, column); }); } - + [Fact] public void Feature_List_Translation_Remove_Reverse() { @@ -828,6 +832,27 @@ public void Feature_List_Translation_Remove_Reverse() }); } + [Fact] + public async Task Feature_ExtensionMethodsNotResolvedOnStartup() + { + var client = new HttpClient(); + + // try to visit the page + var pageResponse = await client.GetAsync(TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + "/" + SamplesRouteUrls.FeatureSamples_JavascriptTranslation_ListMethodTranslations); + TestOutput.WriteLine($"Page response: {(int)pageResponse.StatusCode}"); + var wasError = pageResponse.StatusCode != HttpStatusCode.OK; + + // dump extension methods on the output + var json = await client.GetStringAsync(TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + "/dump-extension-methods"); + TestOutput.WriteLine(json); + + if (wasError) + { + // fail the test on error + throw new Exception("Extension methods were not resolved on application startup."); + } + } + protected IElementWrapperCollection GetSortedRow(IBrowserWrapper browser, string btn) { var orderByBtn = browser.First($"//input[@value='{btn}']", By.XPath); diff --git a/src/Tools/AppStartupInstabilityTester.py b/src/Tools/AppStartupInstabilityTester.py new file mode 100644 index 000000000..80e6f77f3 --- /dev/null +++ b/src/Tools/AppStartupInstabilityTester.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import subprocess, requests, os, time, argparse + +parser = argparse.ArgumentParser(description="Repeatedly starts the server and every time checks if some pages are working, use to find startup-time race condition bugs") +parser.add_argument("--port", type=int, default=16017, help="Port to run the server on") +parser.add_argument("--working-directory", type=str, default=".", help="Working directory to run the server in") +parser.add_argument("--server-path", type=str, default="bin/Debug/net8.0/DotVVM.Samples.BasicSamples.AspNetCoreLatest", help="Path to the server executable") +parser.add_argument("--environment", type=str, default="Development", help="Asp.Net Core environment (Development, Production)") +args = parser.parse_args() + +port = args.port + +def server_start() -> subprocess.Popen: + """Starts the server and returns the process object""" + server = subprocess.Popen([ + args.server_path, "--environment", args.environment, "--urls", f"http://localhost:{port}"], + cwd=args.working_directory, + ) + return server + +def req(path): + try: + response = requests.get(f"http://localhost:{port}{path}") + return response.status_code + except requests.exceptions.ConnectionError: + return None + +iteration = 0 +while True: + iteration += 1 + print(f"Starting iteration {iteration}") + server = server_start() + time.sleep(0.1) + while req("/") is None: + time.sleep(0.1) + + probes = [ + req("/"), + req("/FeatureSamples/LambdaExpressions/StaticCommands"), + req("/FeatureSamples/LambdaExpressions/ClientSideFiltering"), + req("/FeatureSamples/LambdaExpressions/LambdaExpressions") + ] + if set(probes) != {200}: + print(f"Iteration {iteration} failed: {probes}") + time.sleep(100000000) + + server.terminate() + server.wait()