Initial debugger support

This commit is contained in:
Steve Sanderson 2018-07-03 14:57:10 +01:00
parent c881a63a78
commit cafb56569d
22 changed files with 411 additions and 86 deletions

View File

@ -68,7 +68,7 @@
<script type="blazor-boot"></script>
<script src="loader.js"></script>
<script>
initMono(['/_framework/_bin/MonoSanityClient.dll'], function () {
initMono(['_framework/_bin/MonoSanityClient.dll', '_framework/_bin/MonoSanityClient.pdb'], function () {
var buttons = document.getElementsByTagName('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].disabled = false;

View File

@ -30,8 +30,9 @@
preloadAssemblies(loadAssemblyUrls);
}],
postRun: [function () {
var load_runtime = Module.cwrap('mono_wasm_load_runtime', null, ['string']);
load_runtime('appBinDir');
var load_runtime = Module.cwrap('mono_wasm_load_runtime', null, ['string', 'number']);
load_runtime('appBinDir', 1);
MONO.mono_wasm_runtime_is_ready = true;
onReadyCallback();
}]
};
@ -85,13 +86,22 @@
.concat(loadBclAssemblies.map(function (name) { return '_framework/_bin/' + name + '.dll'; }));
Module.FS_createPath('/', 'appBinDir', true, true);
MONO.loaded_files = []; // Used by debugger
allAssemblyUrls.forEach(function (url) {
FS.createPreloadedFile('appBinDir', getAssemblyNameFromUrl(url) + '.dll', url, true, false, null, function onError(err) {
throw err;
});
FS.createPreloadedFile('appBinDir', getFileNameFromUrl(url), url, true, false,
/* success */ function() { MONO.loaded_files.push(toAbsoluteUrl(url)); },
/* failure */ function onError(err) { throw err; }
);
});
}
var anchorTagForAbsoluteUrlConversions = document.createElement('a');
function toAbsoluteUrl(possiblyRelativeUrl) {
anchorTagForAbsoluteUrlConversions.href = possiblyRelativeUrl;
return anchorTagForAbsoluteUrlConversions.href;
}
function asyncLoad(url, onload, onerror) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, /* async: */ true);
@ -161,11 +171,11 @@
document.body.appendChild(scriptElem);
}
function getAssemblyNameFromUrl(url) {
function getFileNameFromUrl(url) {
var lastSegment = url.substring(url.lastIndexOf('/') + 1);
var queryStringStartPos = lastSegment.indexOf('?');
var filename = queryStringStartPos < 0 ? lastSegment : lastSegment.substring(0, queryStringStartPos);
return filename.replace(/\.dll$/, '');
return filename;
}
})();

View File

@ -1,9 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Razor" TreatAsLocalProperty="BlazorLinkOnBuild">
<Project Sdk="Microsoft.NET.Sdk.Razor" TreatAsLocalProperty="BlazorLinkOnBuild">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<BlazorLinkOnBuild>false</BlazorLinkOnBuild>
<!-- loader.js is hard-coded to assume it can load .pdbs regardless of Debug/Release configuration -->
<BlazorEnableDebugging>true</BlazorEnableDebugging>
</PropertyGroup>
<!-- Local alternative to <PackageReference Include="Microsoft.AspNetCore.Blazor.Build" /> -->

View File

@ -1,6 +1,6 @@
import '../../Microsoft.JSInterop/JavaScriptRuntime/src/Microsoft.JSInterop';
import { platform } from './Environment';
import { getAssemblyNameFromUrl } from './Platform/DotNet';
import { getAssemblyNameFromUrl } from './Platform/Url';
import './GlobalExports';
async function boot() {

View File

@ -1,6 +0,0 @@
export function getAssemblyNameFromUrl(url: string) {
const lastSegment = url.substring(url.lastIndexOf('/') + 1);
const queryStringStartPos = lastSegment.indexOf('?');
const filename = queryStringStartPos < 0 ? lastSegment : lastSegment.substring(0, queryStringStartPos);
return filename.replace(/\.dll$/, '');
}

View File

@ -0,0 +1,50 @@
import { getAssemblyNameFromUrl, getFileNameFromUrl } from '../Url';
const currentBrowserIsChrome = (window as any).chrome
&& navigator.userAgent.indexOf('Edge') < 0; // Edge pretends to be Chrome
let hasReferencedPdbs = false;
export function hasDebuggingEnabled() {
return hasReferencedPdbs && currentBrowserIsChrome;
}
export function attachDebuggerHotkey(loadAssemblyUrls: string[]) {
hasReferencedPdbs = loadAssemblyUrls
.some(url => /\.pdb$/.test(getFileNameFromUrl(url)));
// Use the combination shift+alt+D because it isn't used by the major browsers
// for anything else by default
const altKeyName = navigator.platform.match(/^Mac/i) ? 'Cmd' : 'Alt';
if (hasDebuggingEnabled()) {
console.info(`Debugging hotkey: Shift+${altKeyName}+D (when application has focus)`);
}
// Even if debugging isn't enabled, we register the hotkey so we can report why it's not enabled
document.addEventListener('keydown', evt => {
if (evt.shiftKey && (evt.metaKey || evt.altKey) && evt.code === 'KeyD') {
if (!hasReferencedPdbs) {
console.error('Cannot start debugging, because the application was not compiled with debugging enabled.');
} else if (!currentBrowserIsChrome) {
console.error('Currently, only Chrome is supported for debugging.');
} else {
launchDebugger();
}
}
});
}
function launchDebugger() {
// The noopener flag is essential, because otherwise Chrome tracks the association with the
// parent tab, and then when the parent tab pauses in the debugger, the child tab does so
// too (even if it's since navigated to a different page). This means that the debugger
// itself freezes, and not just the page being debugged.
//
// We have to construct a link element and simulate a click on it, because the more obvious
// window.open(..., 'noopener') always opens a new window instead of a new tab.
const link = document.createElement('a');
link.href = `_framework/debug?url=${encodeURIComponent(location.href)}`;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.click();
}

View File

@ -1,5 +1,6 @@
import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform';
import { getAssemblyNameFromUrl } from '../DotNet';
import { getAssemblyNameFromUrl, getFileNameFromUrl } from '../Url';
import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger';
const assemblyHandleCache: { [assemblyName: string]: number } = {};
const typeHandleCache: { [fullyQualifiedTypeName: string]: number } = {};
@ -11,14 +12,16 @@ let find_method: (typeHandle: number, methodName: string, unknownArg: number) =>
let invoke_method: (method: MethodHandle, target: System_Object, argsArrayPtr: number, exceptionFlagIntPtr: number) => System_Object;
let mono_string_get_utf8: (managedString: System_String) => Mono.Utf8Ptr;
let mono_string: (jsString: string) => System_String;
const appBinDirName = 'appBinDir';
export const monoPlatform: Platform = {
start: function start(loadAssemblyUrls: string[]) {
return new Promise<void>((resolve, reject) => {
attachDebuggerHotkey(loadAssemblyUrls);
// mono.js assumes the existence of this
window['Browser'] = {
init: () => { },
asyncLoad: asyncLoad
init: () => { }
};
// Emscripten works by expecting the module config to be a global
window['Module'] = createEmscriptenModuleInstance(loadAssemblyUrls, resolve, reject);
@ -192,8 +195,9 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: ()
const module = {} as typeof Module;
const wasmBinaryFile = '_framework/wasm/mono.wasm';
const asmjsCodeFile = '_framework/asmjs/mono.asm.js';
const suppressMessages = ['DEBUGGING ENABLED'];
module.print = line => console.log(`WASM: ${line}`);
module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(`WASM: ${line}`));
module.printErr = line => console.error(`WASM: ${line}`);
module.preRun = [];
module.postRun = [];
@ -216,14 +220,39 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: ()
mono_string_get_utf8 = Module.cwrap('mono_wasm_string_get_utf8', 'number', ['number']);
mono_string = Module.cwrap('mono_wasm_string_from_js', 'number', ['string']);
Module.FS_createPath('/', 'appBinDir', true, true);
loadAssemblyUrls.forEach(url =>
FS.createPreloadedFile('appBinDir', `${getAssemblyNameFromUrl(url)}.dll`, url, true, false, undefined, onError));
Module.FS_createPath('/', appBinDirName, true, true);
MONO.loaded_files = [];
loadAssemblyUrls.forEach(url => {
const filename = getFileNameFromUrl(url);
const runDependencyId = `blazor:${filename}`;
addRunDependency(runDependencyId);
asyncLoad(url).then(
data => {
Module.FS_createDataFile(appBinDirName, filename, data, true, false, false);
MONO.loaded_files.push(toAbsoluteUrl(url));
removeRunDependency(runDependencyId);
},
errorInfo => {
// If it's a 404 on a .pdb, we don't want to block the app from starting up.
// We'll just skip that file and continue (though the 404 is logged in the console).
// This happens if you build a Debug build but then run in Production environment.
const isPdb404 = errorInfo instanceof XMLHttpRequest
&& errorInfo.status === 404
&& filename.match(/\.pdb$/);
if (!isPdb404) {
onError(errorInfo);
}
removeRunDependency(runDependencyId);
}
);
});
});
module.postRun.push(() => {
const load_runtime = Module.cwrap('mono_wasm_load_runtime', null, ['string']);
load_runtime('appBinDir');
const load_runtime = Module.cwrap('mono_wasm_load_runtime', null, ['string', 'number']);
load_runtime(appBinDirName, hasDebuggingEnabled() ? 1 : 0);
MONO.mono_wasm_runtime_is_ready = true;
attachInteropInvoker();
onReady();
});
@ -231,20 +260,28 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: ()
return module;
}
function asyncLoad(url, onload, onerror) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, /* async: */ true);
xhr.responseType = 'arraybuffer';
xhr.onload = function xhr_onload() {
if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
var asm = new Uint8Array(xhr.response);
onload(asm);
} else {
onerror(xhr);
}
};
xhr.onerror = onerror;
xhr.send(null);
const anchorTagForAbsoluteUrlConversions = document.createElement('a');
function toAbsoluteUrl(possiblyRelativeUrl: string) {
anchorTagForAbsoluteUrlConversions.href = possiblyRelativeUrl;
return anchorTagForAbsoluteUrlConversions.href;
}
function asyncLoad(url) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, /* async: */ true);
xhr.responseType = 'arraybuffer';
xhr.onload = function xhr_onload() {
if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
var asm = new Uint8Array(xhr.response);
resolve(asm);
} else {
reject(xhr);
}
};
xhr.onerror = reject;
xhr.send(null);
});
}
function getArrayDataPointer<T>(array: System_Array<T>): number {

View File

@ -1,4 +1,4 @@
declare namespace Module {
declare namespace Module {
function UTF8ToString(utf8: Mono.Utf8Ptr): string;
var preloadPlugins: any[];
@ -11,7 +11,17 @@
function FS_createDataFile(parent, name, data, canRead, canWrite, canOwn);
}
// Emscripten declares these globals
declare const addRunDependency: any;
declare const removeRunDependency: any;
declare namespace Mono {
interface Utf8Ptr { Utf8Ptr__DO_NOT_IMPLEMENT: any }
interface StackSaveHandle { StackSaveHandle__DO_NOT_IMPLEMENT: any }
}
// Mono uses this global to hang various debugging-related items on
declare namespace MONO {
var loaded_files: string[];
var mono_wasm_runtime_is_ready: boolean;
}

View File

@ -0,0 +1,11 @@
export function getFileNameFromUrl(url: string) {
// This could also be called "get last path segment from URL", but the primary
// use case is to extract things that look like filenames
const lastSegment = url.substring(url.lastIndexOf('/') + 1);
const queryStringStartPos = lastSegment.indexOf('?');
return queryStringStartPos < 0 ? lastSegment : lastSegment.substring(0, queryStringStartPos);
}
export function getAssemblyNameFromUrl(url: string) {
return getFileNameFromUrl(url).replace(/\.dll$/, '');
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -45,7 +46,14 @@ namespace Microsoft.AspNetCore.Blazor.Build
assemblyResolutionContext.ResolveAssemblies();
var paths = assemblyResolutionContext.Results.Select(r => r.Path);
return paths;
return paths.Concat(FindPdbs(paths));
}
private static IEnumerable<string> FindPdbs(IEnumerable<string> dllPaths)
{
return dllPaths
.Select(path => Path.ChangeExtension(path, "pdb"))
.Where(path => File.Exists(path));
}
public class AssemblyResolutionContext

View File

@ -1,12 +1,15 @@
<Project>
<Project>
<Import Project="Blazor.MonoRuntime.props" />
<PropertyGroup>
<DefaultWebContentItemExcludes>$(DefaultWebContentItemExcludes);wwwroot\**</DefaultWebContentItemExcludes>
<!-- By default, enable auto rebuilds for debug builds. Note that the server will not enable it in production environments regardless. -->
<BlazorRebuildOnFileChange Condition="'$(Configuration)' == 'Debug'">true</BlazorRebuildOnFileChange>
<BlazorRebuildOnFileChange Condition="'$(Configuration)' == 'Debug' AND '$(BlazorRebuildOnFileChange)' == ''">true</BlazorRebuildOnFileChange>
<!-- By default, enable debugging for debug builds. -->
<BlazorEnableDebugging Condition="'$(Configuration)' == 'Debug' AND '$(BlazorEnableDebugging)' == ''">true</BlazorEnableDebugging>
<!-- When using IISExpress with a standalone app, there's no point restarting IISExpress after build. It slows things unnecessarily and breaks in-flight HTTP requests. -->
<NoRestartServerOnBuild>true</NoRestartServerOnBuild>
</PropertyGroup>

View File

@ -24,6 +24,7 @@
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(MSBuildProjectFullPath)" Overwrite="true" Encoding="Unicode"/>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(OutDir)$(AssemblyName).dll" Overwrite="false" Encoding="Unicode"/>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Condition="'$(BlazorRebuildOnFileChange)'=='true'" Lines="autorebuild:true" Overwrite="false" Encoding="Unicode"/>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Condition="'$(BlazorEnableDebugging)'=='true'" Lines="debug:true" Overwrite="false" Encoding="Unicode"/>
<ItemGroup>
<ContentWithTargetPath Include="$(BlazorMetadataFilePath)" TargetPath="$(BlazorMetadataFileName)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -11,7 +11,7 @@
<PropertyGroup Label="Blazor build outputs">
<MonoLinkerI18NAssemblies>none</MonoLinkerI18NAssemblies> <!-- See Mono linker docs - allows comma-separated values from: none,all,cjk,mideast,other,rare,west -->
<AdditionalMonoLinkerOptions>-c link -u link -t --verbose </AdditionalMonoLinkerOptions>
<AdditionalMonoLinkerOptions>-c link -u link -b true -t --verbose </AdditionalMonoLinkerOptions>
<BaseBlazorDistPath>dist/</BaseBlazorDistPath>
<BaseBlazorPackageContentOutputPath>$(BaseBlazorDistPath)_content/</BaseBlazorPackageContentOutputPath>
<BaseBlazorRuntimeOutputPath>$(BaseBlazorDistPath)_framework/</BaseBlazorRuntimeOutputPath>

View File

@ -272,6 +272,7 @@
<_BlazorCommonInput Include="@(IntermediateAssembly)" />
<_BlazorCommonInput Include="@(_BlazorDependencyInput)" />
<_BlazorCommonInput Include="$(_BlazorShouldLinkApplicationAssemblies)" />
<_BlazorCommonInput Include="$(BlazorEnableDebugging)" />
<_BlazorLinkingOption Condition="'$(_BlazorShouldLinkApplicationAssemblies)' == ''" Include="false" />
<_BlazorLinkingOption Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''" Include="true" />
</ItemGroup>
@ -340,11 +341,15 @@
</ReadLinesFromFile>
<ItemGroup>
<BlazorItemOutput Include="@(_OptimizedFiles)">
<BlazorItemOutput Include="@(_OptimizedFiles->WithMetadataValue('Extension','.dll'))">
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>Assembly</Type>
<PrimaryOutput Condition="'%(FileName)' == @(IntermediateAssembly->'%(FileName)')">true</PrimaryOutput>
</BlazorItemOutput>
<BlazorItemOutput Include="@(_OptimizedFiles->WithMetadataValue('Extension','.pdb'))">
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>Pdb</Type>
</BlazorItemOutput>
<FileWrites Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(TargetOutputPath)')" />
</ItemGroup>
@ -443,6 +448,7 @@
<!-- Collect the contents of /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker/ -->
<ItemGroup>
<_BlazorLinkerOutput Include="$(BlazorIntermediateLinkerOutputPath)*.dll" />
<_BlazorLinkerOutput Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" />
</ItemGroup>
<!--
@ -497,14 +503,17 @@
-->
<ItemGroup>
<BlazorItemOutput Include="@(_IntermediateResolvedRuntimeDependencies)">
<BlazorItemOutput Include="@(_IntermediateResolvedRuntimeDependencies->WithMetadataValue('Extension','.dll'))">
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>Assembly</Type>
<PrimaryOutput Condition="'%(FileName)' == @(IntermediateAssembly->'%(FileName)')">true</PrimaryOutput>
</BlazorItemOutput>
<BlazorItemOutput Include="@(_IntermediateResolvedRuntimeDependencies->WithMetadataValue('Extension','.pdb'))">
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>Pdb</Type>
</BlazorItemOutput>
<FileWrites Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(TargetOutputPath)')" />
</ItemGroup>
</Target>
<Target
@ -583,8 +592,11 @@
<Target Name="_ResolveBlazorIndexHtmlInputs">
<ItemGroup>
<BlazorIndexHtmlInput Include="$(BlazorIndexHtml)" />
<BlazorIndexHtmlInput Include="$(Configuration)" />
<BlazorIndexHtmlInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Pdb')->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(_BlazorLinkingOption)" />
<BlazorIndexHtmlInput Include="$(BlazorEnableDebugging)" />
</ItemGroup>
<WriteLinesToFile
@ -607,6 +619,7 @@
<ItemGroup>
<_UnlinkedAppReferencesPaths Include="@(_BlazorDependencyInput)" />
<_AppReferences Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->WithMetadataValue('PrimaryOutput','')->'%(FileName)%(Extension)')" />
<_AppReferences Include="@(BlazorItemOutput->WithMetadataValue('Type','Pdb')->'%(FileName)%(Extension)')" Condition="'$(BlazorEnableDebugging)' == 'true'" />
</ItemGroup>
<PropertyGroup>
<_LinkerEnabledFlag Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''">--linker-enabled</_LinkerEnabledFlag>

View File

@ -8,20 +8,6 @@
<!-- Preserve all methods on WasmRuntime, because these are called by JS-side code
to implement timers. Fixes https://github.com/aspnet/Blazor/issues/239 -->
<type fullname="System.Threading.WasmRuntime" />
<!-- Mono redirects certain string constructor calls to CreateString internally, so preserve them -->
<type fullname="System.String">
<method signature="System.String CreateString(System.Char,System.Int32)" />
<method signature="System.String CreateString(System.Char[])" />
<method signature="System.String CreateString(System.Char[],System.Int32,System.Int32)" />
<method signature="System.String CreateString(System.Char*)" />
<method signature="System.String CreateString(System.Char*,System.Int32,System.Int32)" />
<method signature="System.String CreateString(System.Byte*)" />
<method signature="System.String CreateString(System.Byte*,System.Int32,System.Int32)" />
<method signature="System.String CreateString(System.Byte*,System.Int32,System.Int32,System.Text.Encoding)" />
<method signature="System.String CreateString(System.SByte*)" />
<method signature="System.String CreateString(System.SByte*,System.Int32,System.Int32,System.Text.Encoding)" />
</type>
</assembly>
</linker>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Server;
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Builder
// way to remove middleware once it's registered.
private static List<object> _uncollectableWatchers = new List<object>();
public static void UseHostedAutoRebuild(this IApplicationBuilder appBuilder, BlazorConfig config, string hostAppContentRootPath)
public static void UseHostedAutoRebuild(this IApplicationBuilder app, BlazorConfig config, string hostAppContentRootPath)
{
var isFirstFileWrite = true;
WatchFileSystem(config, () =>
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Builder
catch (Exception ex)
{
// If we don't have permission to write these files, autorebuild will not be enabled
var loggerFactory = appBuilder.ApplicationServices.GetRequiredService<ILoggerFactory>();
var loggerFactory = app.ApplicationServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(typeof (AutoRebuildExtensions));
logger?.LogWarning(ex,
"Cannot autorebuild because there was an error when writing to a file in '{0}'.",
@ -63,7 +63,7 @@ namespace Microsoft.AspNetCore.Builder
});
}
public static void UseDevServerAutoRebuild(this IApplicationBuilder appBuilder, BlazorConfig config)
public static void UseDevServerAutoRebuild(this IApplicationBuilder app, BlazorConfig config)
{
// Currently this only supports VS for Windows. Later on we can add
// an IRebuildService implementation for VS for Mac, etc.
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Builder
buildToken = new RebuildToken(DateTime.Now);
});
appBuilder.Use(async (context, next) =>
app.Use(async (context, next) =>
{
try
{

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
@ -23,12 +23,12 @@ namespace Microsoft.AspNetCore.Builder
/// Configures the middleware pipeline to work with Blazor.
/// </summary>
/// <typeparam name="TProgram">Any type from the client app project. This is used to identify the client app assembly.</typeparam>
/// <param name="applicationBuilder"></param>
/// <param name="app"></param>
public static void UseBlazor<TProgram>(
this IApplicationBuilder applicationBuilder)
this IApplicationBuilder app)
{
var clientAssemblyInServerBinDir = typeof(TProgram).Assembly;
applicationBuilder.UseBlazor(new BlazorOptions
app.UseBlazor(new BlazorOptions
{
ClientAssemblyPath = clientAssemblyInServerBinDir.Location,
});
@ -37,21 +37,21 @@ namespace Microsoft.AspNetCore.Builder
/// <summary>
/// Configures the middleware pipeline to work with Blazor.
/// </summary>
/// <param name="applicationBuilder"></param>
/// <param name="app"></param>
/// <param name="options"></param>
public static void UseBlazor(
this IApplicationBuilder applicationBuilder,
this IApplicationBuilder app,
BlazorOptions options)
{
// TODO: Make the .blazor.config file contents sane
// Currently the items in it are bizarre and don't relate to their purpose,
// hence all the path manipulation here. We shouldn't be hardcoding 'dist' here either.
var env = (IHostingEnvironment)applicationBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment));
var env = (IHostingEnvironment)app.ApplicationServices.GetService(typeof(IHostingEnvironment));
var config = BlazorConfig.Read(options.ClientAssemblyPath);
var distDirStaticFiles = new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(config.DistPath),
ContentTypeProvider = CreateContentTypeProvider(),
ContentTypeProvider = CreateContentTypeProvider(config.EnableDebugging),
OnPrepareResponse = SetCacheHeaders
};
@ -59,16 +59,16 @@ namespace Microsoft.AspNetCore.Builder
{
if (env.ApplicationName.Equals(DevServerApplicationName, StringComparison.OrdinalIgnoreCase))
{
applicationBuilder.UseDevServerAutoRebuild(config);
app.UseDevServerAutoRebuild(config);
}
else
{
applicationBuilder.UseHostedAutoRebuild(config, env.ContentRootPath);
app.UseHostedAutoRebuild(config, env.ContentRootPath);
}
}
// First, match the request against files in the client app dist directory
applicationBuilder.UseStaticFiles(distDirStaticFiles);
app.UseStaticFiles(distDirStaticFiles);
// Next, match the request against static files in wwwroot
if (!string.IsNullOrEmpty(config.WebRootPath))
@ -77,16 +77,22 @@ namespace Microsoft.AspNetCore.Builder
// (and don't require them to be copied into dist).
// TODO: When publishing is implemented, have config.WebRootPath set
// to null so that it only serves files that were copied to dist
applicationBuilder.UseStaticFiles(new StaticFileOptions
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(config.WebRootPath),
OnPrepareResponse = SetCacheHeaders
});
}
// Accept debugger connections
if (config.EnableDebugging)
{
app.UseMonoDebugProxy();
}
// Finally, use SPA fallback routing (serve default page for anything else,
// excluding /_framework/*)
applicationBuilder.MapWhen(IsNotFrameworkDir, childAppBuilder =>
app.MapWhen(IsNotFrameworkDir, childAppBuilder =>
{
childAppBuilder.UseSpa(spa =>
{
@ -116,12 +122,18 @@ namespace Microsoft.AspNetCore.Builder
private static bool IsNotFrameworkDir(HttpContext context)
=> !context.Request.Path.StartsWithSegments("/_framework");
private static IContentTypeProvider CreateContentTypeProvider()
private static IContentTypeProvider CreateContentTypeProvider(bool enableDebugging)
{
var result = new FileExtensionContentTypeProvider();
result.Mappings.Add(".dll", MediaTypeNames.Application.Octet);
result.Mappings.Add(".mem", MediaTypeNames.Application.Octet);
result.Mappings.Add(".wasm", WasmMediaTypeNames.Application.Wasm);
if (enableDebugging)
{
result.Mappings.Add(".pdb", MediaTypeNames.Application.Octet);
}
return result;
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
public string DistPath
=> Path.Combine(Path.GetDirectoryName(SourceOutputAssemblyPath), "dist");
public bool EnableAutoRebuilding { get; }
public bool EnableDebugging { get; }
public static BlazorConfig Read(string assemblyPath)
=> new BlazorConfig(assemblyPath);
@ -44,6 +45,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
}
EnableAutoRebuilding = configLines.Contains("autorebuild:true", StringComparer.Ordinal);
EnableDebugging = configLines.Contains("debug:true", StringComparer.Ordinal);
}
}
}

View File

@ -0,0 +1,178 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using WsProxy;
namespace Microsoft.AspNetCore.Builder
{
internal static class MonoDebugProxyAppBuilderExtensions
{
public static void UseMonoDebugProxy(this IApplicationBuilder app)
{
app.UseWebSockets();
app.Use((context, next) =>
{
var requestPath = context.Request.Path;
if (!requestPath.StartsWithSegments("/_framework/debug"))
{
return next();
}
if (requestPath.Equals("/_framework/debug/ws-proxy", StringComparison.OrdinalIgnoreCase))
{
return DebugWebSocketProxyRequest(context);
}
if (requestPath.Equals("/_framework/debug", StringComparison.OrdinalIgnoreCase))
{
return DebugHome(context);
}
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
return Task.CompletedTask;
});
}
private static async Task DebugWebSocketProxyRequest(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
return;
}
var browserUri = new Uri(context.Request.Query["browser"]);
await new MonoProxy().Run(context, browserUri);
}
private static async Task DebugHome(HttpContext context)
{
context.Response.ContentType = "text/html";
var request = context.Request;
var appRootUrl = $"{request.Scheme}://{request.Host}{request.PathBase}/";
var targetTabUrl = request.Query["url"];
if (string.IsNullOrEmpty(targetTabUrl))
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync("No value specified for 'url'");
return;
}
// TODO: Allow overriding port (but not hostname, as we're connecting to the
// local browser, not to the webserver serving the app)
var debuggerHost = "http://localhost:9222";
var debuggerTabsListUrl = $"{debuggerHost}/json";
IEnumerable<BrowserTab> availableTabs;
try
{
availableTabs = await GetOpenedBrowserTabs(debuggerHost);
}
catch (Exception ex)
{
await context.Response.WriteAsync($@"
<h1>Unable to find debuggable browser tab</h1>
<p>
Could not get a list of browser tabs from <code>{debuggerTabsListUrl}</code>.
Ensure Chrome is running with debugging enabled.
</p>
<h2>Resolution</h2>
{GetLaunchChromeInstructions(appRootUrl)}
<p>... then use that new tab for debugging.</p>
<h2>Underlying exception:</h2>
<pre>{ex}</pre>
");
return;
}
var matchingTabs = availableTabs
.Where(t => t.Url.Equals(targetTabUrl, StringComparison.Ordinal))
.ToList();
if (matchingTabs.Count == 0)
{
await context.Response.WriteAsync($@"
<h1>Unable to find debuggable browser tab</h1>
<p>
The response from <code>{debuggerTabsListUrl}</code> does not include
any entry for <code>{targetTabUrl}</code>.
</p>");
return;
}
else if (matchingTabs.Count > 1)
{
// TODO: Automatically disambiguate by adding a GUID to the page title
// when you press the debugger hotkey, include it in the querystring passed
// here, then remove it once the debugger connects.
await context.Response.WriteAsync($@"
<h1>Multiple matching tabs are open</h1>
<p>
There is more than one browser tab at <code>{targetTabUrl}</code>.
Close the ones you do not wish to debug, then refresh this page.
</p>");
return;
}
// Now we know uniquely which tab to debug, construct the URL to the debug
// page and redirect there
var tabToDebug = matchingTabs.Single();
var underlyingV8Endpoint = tabToDebug.WebSocketDebuggerUrl;
var proxyEndpoint = $"{request.Host}{request.PathBase}/_framework/debug/ws-proxy?browser={WebUtility.UrlEncode(underlyingV8Endpoint)}";
var devToolsUrlAbsolute = new Uri(debuggerHost + tabToDebug.DevtoolsFrontendUrl);
var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?ws={proxyEndpoint}";
context.Response.Redirect(devToolsUrlWithProxy);
}
private static string GetLaunchChromeInstructions(string appRootUrl)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $@"<p>Close this browser, then press Win+R and enter the following:</p>
<p><strong><code>""%programfiles(x86)%\Google\Chrome\Application\chrome.exe"" --remote-debugging-port=9222 {appRootUrl}</code></strong></p>";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return $@"<p>Close this browser, then in a terminal window execute the following:</p>
<p><strong><code>google-chrome --remote-debugging-port=9222 {appRootUrl}</code></strong></p>";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return $@"<p>Close this browser, then in a terminal window execute the following:</p>
<p><strong><code>open /Applications/Google\ Chrome.app --remote-debugging-port=9222 {appRootUrl}</code></strong></p>";
}
else
{
throw new InvalidOperationException("Unknown OS platform");
}
}
private static async Task<IEnumerable<BrowserTab>> GetOpenedBrowserTabs(string debuggerHost)
{
using (var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) })
{
var jsonResponse = await httpClient.GetStringAsync($"{debuggerHost}/json");
return JsonConvert.DeserializeObject<BrowserTab[]>(jsonResponse);
}
}
class BrowserTab
{
public string Type { get; set; }
public string Url { get; set; }
public string Title { get; set; }
public string DevtoolsFrontendUrl { get; set; }
public string WebSocketDebuggerUrl { get; set; }
}
}
}

View File

@ -44,11 +44,12 @@ namespace WsProxy
}
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/*
public void Configure (IApplicationBuilder app, IHostingEnvironment env)
{
//loggerFactory.AddConsole();
//loggerFactory.AddDebug();
//app.UseDeveloperExceptionPage ();
app.UseDeveloperExceptionPage ();
app.UseWebSockets (); app.UseRouter (router => {
router.MapGet ("devtools/page/{pageId}", async context => {
@ -66,5 +67,6 @@ namespace WsProxy
});
});
}
*/
}
}

View File

@ -263,9 +263,9 @@ namespace WsProxy {
Send (this.ide, o, token);
}
public async Task Run (HttpContext context)
public async Task Run (HttpContext context, Uri browserUri)
{
var browserUri = GetBrowserUri (context.Request.Path.ToString ());
//var browserUri = GetBrowserUri (context.Request.Path.ToString ());
Debug ("wsproxy start");
using (this.ide = await context.WebSockets.AcceptWebSocketAsync ()) {
Debug ("ide connected");

View File

@ -58,15 +58,20 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
uncalled implementation code from mscorlib.dll anyway.
*/
"Microsoft.AspNetCore.Blazor.Browser.dll",
"Microsoft.AspNetCore.Blazor.Browser.pdb",
"Microsoft.AspNetCore.Blazor.dll",
"Microsoft.AspNetCore.Blazor.pdb",
"Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"Microsoft.Extensions.DependencyInjection.dll",
"Microsoft.JSInterop.dll",
"Microsoft.JSInterop.pdb",
"Mono.Security.dll",
"Mono.WebAssembly.Interop.dll",
"Mono.WebAssembly.Interop.pdb",
"mscorlib.dll",
"netstandard.dll",
"StandaloneApp.dll",
"StandaloneApp.pdb",
"System.dll",
"System.Collections.Concurrent.dll",
"System.Collections.dll",