diff --git a/src/coreclr/vm/debugdebugger.cpp b/src/coreclr/vm/debugdebugger.cpp index 20bf5c940726e9..89074d9810d91d 100644 --- a/src/coreclr/vm/debugdebugger.cpp +++ b/src/coreclr/vm/debugdebugger.cpp @@ -26,12 +26,89 @@ #include "corsym.h" #include "generics.h" #include "stackwalk.h" +#include "hostinformation.h" #define PORTABLE_PDB_MINOR_VERSION 20557 #define IMAGE_DEBUG_TYPE_EMBEDDED_PORTABLE_PDB 17 #ifndef DACCESS_COMPILE +#ifdef TARGET_BROWSER +static void TryLoadInMemorySymbols(Module* pModule) +{ + STANDARD_VM_CONTRACT; + + PEAssembly* pAssembly = pModule->GetPEAssembly(); + if (!HostInformation::HasExternalProbe() || !pAssembly->HasLoadedPEImage()) + return; + + PEImageLayout* pLayout = pAssembly->GetLoadedLayout(); + if (!pLayout->HasDirectoryEntry(IMAGE_DIRECTORY_ENTRY_DEBUG)) + return; + + COUNT_T debugDirectorySize = 0; + IMAGE_DEBUG_DIRECTORY* entries = reinterpret_cast( + pLayout->GetDirectoryEntryData(IMAGE_DIRECTORY_ENTRY_DEBUG, &debugDirectorySize)); + if (entries == nullptr || debugDirectorySize % sizeof(IMAGE_DEBUG_DIRECTORY) != 0) + return; + + for (COUNT_T i = 0; i < debugDirectorySize / sizeof(IMAGE_DEBUG_DIRECTORY); i++) + { + const IMAGE_DEBUG_DIRECTORY& entry = entries[i]; + if (entry.Type != IMAGE_DEBUG_TYPE_CODEVIEW || entry.MinorVersion != PORTABLE_PDB_MINOR_VERSION) + continue; + + RVA rva = entry.AddressOfRawData != 0 ? entry.AddressOfRawData : pLayout->OffsetToRva(entry.PointerToRawData); + if (entry.SizeOfData <= offsetof(CV_INFO_PDB70, path) || !pLayout->CheckRva(rva, entry.SizeOfData)) + return; + + const CV_INFO_PDB70* codeView = reinterpret_cast(pLayout->GetRvaData(rva)); + if (codeView->magic != CV_SIGNATURE_RSDS) + return; + + size_t pathSize = entry.SizeOfData - offsetof(CV_INFO_PDB70, path); + size_t pathLength = strnlen(codeView->path, pathSize); + if (pathLength == pathSize) + return; + + // CodeView paths can come from either Windows or Unix, regardless of the runtime platform. + const char* fileName = codeView->path; + for (size_t j = 0; j < pathLength; j++) + { + if (codeView->path[j] == '/' || codeView->path[j] == '\\') + fileName = codeView->path + j + 1; + } + + if (*fileName == '\0') + return; + + StackSString pdbPath; + const SString& assemblyPath = pAssembly->GetIdentityPath(); + SString::CIterator directoryEnd = assemblyPath.End(); + if (assemblyPath.FindBack(directoryEnd, DIRECTORY_SEPARATOR_CHAR_W)) + { + directoryEnd++; + pdbPath.Set(assemblyPath, assemblyPath.Begin(), directoryEnd); + } + pdbPath.AppendUTF8(fileName); + + void* pdbData = nullptr; + int64_t pdbSize = 0; + if (!HostInformation::ExternalAssemblyProbe(pdbPath, &pdbData, &pdbSize)) + return; + + if (pdbData == nullptr || pdbSize <= 0 || pdbSize > INT32_MAX) + COMPlusThrowHR(COR_E_BADIMAGEFORMAT); + + GCX_PREEMP(); + ReleaseHolder symbolStream(new CGrowableStream(1.0f, 1)); + IfFailThrow(symbolStream->Write(pdbData, static_cast(pdbSize), nullptr)); + pModule->SetInMemorySymbolStream(symbolStream); + return; + } +} +#endif // TARGET_BROWSER + // // Notes: // If a managed debugger is attached, this should send the managed UserBreak event. @@ -791,6 +868,13 @@ extern "C" void QCALLTYPE StackTrace_GetStackFramesInternal( // If there is a in memory symbol stream CGrowableStream* stream = pModule->GetInMemorySymbolStream(); +#ifdef TARGET_BROWSER + if (stream == nullptr) + { + TryLoadInMemorySymbols(pModule); + stream = pModule->GetInMemorySymbolStream(); + } +#endif // TARGET_BROWSER if (stream != NULL) { MemoryRange range = stream->GetRawBuffer(); @@ -802,6 +886,14 @@ extern "C" void QCALLTYPE StackTrace_GetStackFramesInternal( CLR_I4 *pInMemoryPdbSize = (CLR_I4 *)((I4ARRAYREF)gc.pStackFrameHelper->rgiInMemoryPdbSize)->GetDirectPointerToNonObjectElements(); pInMemoryPdbSize[iNumValidFrames] = (CLR_I4)range.Size(); } +#ifdef TARGET_BROWSER + else if (pPEAssembly->HasLoadedPEImage() && pPEAssembly->GetLoadedLayout()->IsWebcilFormat()) + { + // PEReader cannot read Webcil. A failed lookup must not prevent symbol lookup for other frames. + CLR_I4* methodTokens = (CLR_I4*)((I4ARRAYREF)gc.pStackFrameHelper->rgiMethodToken)->GetDirectPointerToNonObjectElements(); + methodTokens[iNumValidFrames] = 0; + } +#endif // TARGET_BROWSER else { // Set the pdb path (assembly file name) diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceSymbolsTests.cs b/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceSymbolsTests.cs index c10bf376a70224..5eed16a1167a8d 100644 --- a/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceSymbolsTests.cs +++ b/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceSymbolsTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Runtime.CompilerServices; using Xunit; namespace System.Diagnostics.SymbolStore.Tests @@ -16,9 +17,41 @@ public void StackTraceSymbolsDoNotLockFile() var asmPath = AssemblyPathHelper.GetAssemblyLocation(typeof(StackTraceSymbolsTests).Assembly); var pdbPath = Path.ChangeExtension(asmPath, ".pdb"); - Assert.True(File.Exists(pdbPath)); + bool fileBackedSymbols = !PlatformDetection.IsBrowser; + Assert.Equal(fileBackedSymbols, File.Exists(pdbPath)); new StackTrace(true).GetFrames(); - File.Move(pdbPath, pdbPath); + if (fileBackedSymbols) + { + File.Move(pdbPath, pdbPath); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] + public void StackTraceSymbolsFromMemory() + { + string assemblyPath = AssemblyPathHelper.GetAssemblyLocation(typeof(StackTraceSymbolsTests).Assembly); + Assert.False(File.Exists(Path.ChangeExtension(assemblyPath, ".pdb"))); + + StackFrame frameWithoutSymbols = CaptureFrame(needFileInfo: false); + Assert.Null(frameWithoutSymbols.GetFileName()); + Assert.Equal(0, frameWithoutSymbols.GetFileLineNumber()); + + for (int i = 0; i < 2; i++) + { + StackFrame frame = CaptureFrame(needFileInfo: true); + Assert.Equal(nameof(CaptureFrame), frame.GetMethod().Name); + Assert.True(frame.GetILOffset() >= 0, $"Expected an IL offset for {frame}."); + Assert.Equal("InMemorySymbols.cs", Path.GetFileName(frame.GetFileName()?.Replace('\\', '/'))); + Assert.Equal(123, frame.GetFileLineNumber()); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static StackFrame CaptureFrame(bool needFileInfo) + { +#line 123 "InMemorySymbols.cs" + return new StackFrame(needFileInfo); +#line default } } } diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj b/src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj index 8611372f552f6a..5f452fb440aa65 100644 --- a/src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj +++ b/src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj @@ -9,6 +9,7 @@ true true + 1 false false @@ -27,7 +28,6 @@ - diff --git a/src/native/libs/Common/JavaScript/host/assets.ts b/src/native/libs/Common/JavaScript/host/assets.ts index 16b2d5ecaa9862..d3e12d1756aa08 100644 --- a/src/native/libs/Common/JavaScript/host/assets.ts +++ b/src/native/libs/Common/JavaScript/host/assets.ts @@ -11,22 +11,23 @@ const loadedAssemblies: Map = new Map() // Self-installing wrapper version; keep in sync with WebcilConstants.WASM_WRAPPER_VERSION_SELF_INSTALLING. const webcilWrapperVersion = 2; -export function registerPdbBytes(bytes: Uint8Array, virtualPath: string) { - const lastSlash = virtualPath.lastIndexOf("/"); - let parentDirectory = lastSlash > 0 - ? virtualPath.substring(0, lastSlash) - : browserVirtualAppBase; - let fileName = lastSlash > 0 ? virtualPath.substring(lastSlash + 1) : virtualPath; - if (fileName.startsWith("/")) { - fileName = fileName.substring(1); - } - if (!parentDirectory.startsWith("/")) { - parentDirectory = browserVirtualAppBase + parentDirectory; - } +export function registerPdbBytes(bytes: Uint8Array, virtualPath: string, shortName: string) { + const sp = _ems_.stackSave(); + try { + const ptrPtr = _ems_.stackAlloc(sizeOfPtr); + if (_ems_._posix_memalign(ptrPtr as any, 16, bytes.length)) { + throw new Error("posix_memalign failed for PDB"); + } - _ems_.dotnetLogger.debug(`Registering PDB '${fileName}' in directory '${parentDirectory}'`); - _ems_.FS.createPath("/", parentDirectory, true, true); - _ems_.FS.createDataFile(parentDirectory, fileName, bytes, true /* canRead */, true /* canWrite */, true /* canOwn */); + const ptr = _ems_.HEAPU32[ptrPtr as any >>> 2]; + _ems_.HEAPU8.set(bytes, ptr >>> 0); + + _ems_.dotnetLogger.debug(`Registered PDB '${virtualPath}' (shortName: '${shortName}') at ${ptr.toString(16)} length ${bytes.length}`); + loadedAssemblies.set(virtualPath, { ptr, length: bytes.length }); + loadedAssemblies.set(shortName, { ptr, length: bytes.length }); + } finally { + _ems_.stackRestore(sp); + } } export function registerDllBytes(bytes: Uint8Array, virtualPath: string, shortName: string) { @@ -170,7 +171,13 @@ function finishWebcilInstance(instance: WebAssembly.Instance, payloadPtr: number export function BrowserHost_ExternalAssemblyProbe(pathPtr: CharPtr, outDataStartPtr: VoidPtrPtr, outSize: VoidPtr): boolean { const path = _ems_.UTF8ArrayToString(_ems_.dotnetApi.localHeapViewU8(), pathPtr as any); - const assembly = loadedAssemblies.get(path); + let assembly = loadedAssemblies.get(path); + if (!assembly) { + const lastSlash = path.lastIndexOf("/"); + if (lastSlash >= 0) { + assembly = loadedAssemblies.get(path.substring(lastSlash + 1)); + } + } if (assembly) { _ems_.HEAPU32[outDataStartPtr as any >>> 2] = assembly.ptr; // int64_t target diff --git a/src/native/libs/Common/JavaScript/loader/assets.ts b/src/native/libs/Common/JavaScript/loader/assets.ts index cc6dfe7deae4f0..73ee929b60e42c 100644 --- a/src/native/libs/Common/JavaScript/loader/assets.ts +++ b/src/native/libs/Common/JavaScript/loader/assets.ts @@ -219,7 +219,7 @@ export async function fetchPdb(asset: AssemblyAsset): Promise { } if (bytes) { await nativeModulePromiseController.promise; - dotnetBrowserHostExports.registerPdbBytes(bytes, assetInternal.virtualPath); + dotnetBrowserHostExports.registerPdbBytes(bytes, assetInternal.virtualPath, assetInternal.shortName!); } }