Skip to content

Commit cd1ecc6

Browse files
committed
more
1 parent fdb6009 commit cd1ecc6

File tree

3 files changed

+181
-93
lines changed

3 files changed

+181
-93
lines changed

src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs

Lines changed: 80 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.Playwright;
1515
using Xunit;
1616
using Xunit.Abstractions;
17+
using Microsoft.Diagnostics.Tracing.Etlx;
1718

1819
#nullable enable
1920

@@ -27,121 +28,140 @@ public EventPipeDiagnosticsTests(ITestOutputHelper output, SharedBuildPerTestCla
2728
_enablePerTestCleanup = true;
2829
}
2930

30-
[Theory]
31-
[InlineData(Configuration.Debug)]
32-
[InlineData(Configuration.Release)]
33-
public async Task BlazorEventPipeTestWithCpuSamples(Configuration config)
34-
{ const string tracesPath = "traces";
31+
[Fact]
32+
public async Task BlazorEventPipeTestWithCpuSamples()
33+
{
34+
const string tracesPath = "traces";
3535
Directory.CreateDirectory(tracesPath);
3636

3737
string extraProperties = @"
3838
<WasmPerfInstrumentation>all,interval=0</WasmPerfInstrumentation>
3939
<WasmPerfTracing>true</WasmPerfTracing>
4040
";
4141

42-
ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.BlazorBasicTestApp, "blazor_eventpipe", extraProperties: extraProperties);
42+
ProjectInfo info = CopyTestAsset(Configuration.Release, aot: false, TestAsset.BlazorBasicTestApp, "blazor_eventpipe", extraProperties: extraProperties);
43+
44+
UpdateFile(Path.Combine("Pages", "Counter.razor"), new Dictionary<string, string> {
45+
{
46+
@"currentCount++;",
47+
"""
48+
for(int i = 0; i < 1000; i++)
49+
{
50+
Console.WriteLine($"Incrementing count: {i}");
51+
}
52+
currentCount++;
53+
"""
54+
}
55+
});
56+
4357

4458
// Build the project
45-
BuildProject(info, config, new BuildOptions(AssertAppBundle: false));
59+
BuildProject(info, Configuration.Release, new BuildOptions(AssertAppBundle: false));
4660

4761
// Setup the environment for file uploads
48-
string traceFilePath = Path.Combine(tracesPath, "cpuprofile.nettrace");
4962
var serverEnv = new Dictionary<string, string>
5063
{
51-
["FILE_UPLOAD_PATH"] = tracesPath,
52-
["TRACE_FILE_PATH"] = traceFilePath
64+
["DEVSERVER_UPLOAD_PATH"] = tracesPath
5365
};
5466

5567
// Create a custom test handler that will navigate to Counter page, collect CPU samples,
5668
// click the button, and upload the trace
5769
async Task CpuProfileTest(IPage page)
5870
{
71+
await Task.Delay(1000);
72+
_testOutput.WriteLine("XXXXXXX 1");
5973
// Navigate to the Counter page
6074
await page.Locator("text=Counter").ClickAsync();
6175

6276
// Verify we're on the Counter page
6377
var txt = await page.Locator("p[role='status']").InnerHTMLAsync();
6478
Assert.Equal("Current count: 0", txt);
79+
_testOutput.WriteLine("XXXXXXX 2");
6580

6681
// Collect CPU samples for 5 seconds
6782
await page.EvaluateAsync(@"
68-
window.cpuSamplesPromise = globalThis.getDotnetRuntime(0).collectCpuSamples({durationSeconds: 5});
83+
console.log(`AAAAAAAAAAAAAAAAAAAAAA`);
84+
globalThis.getDotnetRuntime(0)
85+
.collectCpuSamples({durationSeconds: 2, skipDownload:true}).then(traces => {
86+
console.log(`DDDDDDDDDDDDDDDDDDDDDDD`);
87+
// concatenate the buffers into a single Uint8Array
88+
const concatenated = new Uint8Array(traces.reduce((acc, curr) => acc + curr.byteLength, 0));
89+
let offset = 0;
90+
for (const trace of traces) {
91+
concatenated.set(new Uint8Array(trace), offset);
92+
offset += trace.byteLength;
93+
}
94+
console.log(`EEEEEEEEEEEEEEEEEEEEEEEEE`);
95+
96+
return fetch('/upload/cpuprofile.nettrace', {
97+
headers: {
98+
'Content-Type': 'application/octet-stream'
99+
},
100+
method: 'POST',
101+
body: concatenated
102+
});
103+
}).then(() => {
104+
console.log(`XXXXXXXXXXXXXXX`);
105+
}).catch(err => {
106+
console.log(`ERROR: ${err}`);
107+
});
108+
console.log(`BBBBBBBBBBBBBBBBBBBBBBBB`);
69109
");
110+
_testOutput.WriteLine("XXXXXXX 2a");
70111

71112
// Click the button a few times
72113
for (int i = 0; i < 5; i++)
73114
{
115+
_testOutput.WriteLine("XXXXXXX 4 " + i);
74116
await page.Locator("text=\"Click me\"").ClickAsync();
75117
await Task.Delay(300);
76-
} // Wait for the CPU samples promise to complete
77-
await page.EvaluateAsync(@"
78-
window.cpuSamplesPromise.then(trace => {
79-
return fetch('/upload', {
80-
method: 'POST',
81-
headers: {
82-
'File-Name': 'cpuprofile.nettrace'
83-
},
84-
body: trace
85-
});
86-
});
87-
");
118+
}
119+
_testOutput.WriteLine("XXXXXXX 3");
88120

121+
var txt2 = await page.Locator("p[role='status']").InnerHTMLAsync();
122+
_testOutput.WriteLine("XXXXXXX T " + txt2);
123+
Assert.NotEqual("Current count: 0", txt2);
124+
125+
_testOutput.WriteLine("XXXXXXX 6");
89126
// Give time for the upload to complete
90-
await Task.Delay(1000);
127+
await Task.Delay(5000);
91128
}
92129

130+
string extraArgs = " --web-server-use-cors --web-server-use-https";
131+
132+
_testOutput.WriteLine("XXXXXXX 7");
93133
// Run the test using the custom handler
94134
await RunForBuildWithDotnetRun(new BlazorRunOptions(
95-
Configuration: config,
135+
ExtraArgs: extraArgs,
136+
Configuration: Configuration.Release,
96137
Test: CpuProfileTest,
97138
CheckCounter: false,
98139
ServerEnvironment: serverEnv
99140
));
100141

142+
_testOutput.WriteLine("XXXXXXX 8");
143+
101144
// Verify the trace file was created
145+
var traceFilePath = Path.Combine(tracesPath, "cpuprofile.nettrace");
102146
Assert.True(File.Exists(traceFilePath), $"Trace file {traceFilePath} was not created");
147+
var converted = TraceLog.CreateFromEventTraceLogFile(traceFilePath);
148+
Assert.True(File.Exists(converted), $"Trace file {converted} was not created");
149+
_testOutput.WriteLine("XXXXXXX 9");
103150

104-
// Analyze the trace file
105-
using (var source = new ETWTraceEventSource(traceFilePath))
151+
var methodFound = false;
152+
using (var source = TraceLog.OpenOrConvert(converted))
106153
{
107-
var methodFound = false;
108-
var sampledMethodNames = new List<string>();
109-
110-
// Get all sample profile events
111-
source.Clr.All += (TraceEvent data) =>
154+
methodFound = source.CallStacks.Any(stack => stack.CodeAddress.FullMethodName=="BlazorBasicTestApp.Counter.IncrementCount()");
155+
if(!methodFound)
112156
{
113-
if (data.EventName == "Sample" || data.EventName == "GC/SampledObjectAllocation")
157+
foreach (var stack in source.CallStacks)
114158
{
115-
var stackEvent = data as ClrStackTraceTraceData;
116-
if (stackEvent != null && stackEvent.CallStack != null)
117-
{
118-
var stack = stackEvent.CallStack;
119-
while (stack != null)
120-
{
121-
var methodName = stack.CodeAddress.FullMethodName;
122-
sampledMethodNames.Add(methodName);
123-
124-
// Check if IncrementCount method is in the stack
125-
if (methodName.Contains("IncrementCount"))
126-
{
127-
methodFound = true;
128-
}
129-
stack = stack.Caller;
130-
}
131-
}
159+
_testOutput.WriteLine($"Stack: {stack.CodeAddress.FullMethodName}");
132160
}
133-
};
134-
135-
source.Process();
136-
137-
// Output all sampled method names for diagnostic purposes
138-
foreach (var methodName in sampledMethodNames.Distinct().Take(20))
139-
{
140-
_testOutput.WriteLine($"Sampled method: {methodName}");
141161
}
142-
143-
Assert.True(methodFound, "The trace should contain stack frames for the 'IncrementCount' method");
144162
}
163+
164+
Assert.True(methodFound, "The trace should contain stack frames for the 'IncrementCount' method");
145165
}
146166
}
147167
}

src/mono/wasm/host/DevServer/DevServerStartup.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.IO;
56
using System.Net.WebSockets;
67
using System.Threading.Tasks;
@@ -115,6 +116,58 @@ public static void Configure(IApplicationBuilder app, IOptions<DevServerOptions>
115116
}
116117
}
117118
});
119+
120+
// Add general-purpose file upload endpoint when DEVSERVER_UPLOAD_PATH is set
121+
string? fileUploadPath = Environment.GetEnvironmentVariable("DEVSERVER_UPLOAD_PATH");
122+
if (!string.IsNullOrEmpty(fileUploadPath))
123+
{
124+
// Ensure the upload directory exists
125+
if (!Directory.Exists(fileUploadPath))
126+
{
127+
Directory.CreateDirectory(fileUploadPath!);
128+
}
129+
130+
// Route with filename parameter
131+
endpoints.MapPost("/upload/{filename}", async context =>
132+
{
133+
try
134+
{
135+
// Get the filename from the route
136+
var routeValues = context.Request.RouteValues;
137+
string? rawFileName = routeValues["filename"]?.ToString();
138+
139+
// Generate a unique name if none provided
140+
if (string.IsNullOrEmpty(rawFileName))
141+
{
142+
rawFileName = $"upload_{Guid.NewGuid():N}";
143+
}
144+
145+
// Sanitize filename - IMPORTANT: Only use GetFileName to strip any path components
146+
// This prevents directory traversal attacks like "../../../etc/passwd"
147+
string fileName = Path.GetFileName(rawFileName);
148+
149+
if (string.IsNullOrEmpty(fileName))
150+
{
151+
fileName = $"upload_{Guid.NewGuid():N}";
152+
}
153+
154+
string filePath = Path.Combine(fileUploadPath!, fileName);
155+
156+
using (var outputStream = new FileStream(filePath, FileMode.Create))
157+
{
158+
await context.Request.Body.CopyToAsync(outputStream);
159+
}
160+
161+
await context.Response.WriteAsync($"File saved to {filePath}");
162+
}
163+
catch (Exception ex)
164+
{
165+
context.Response.StatusCode = 500;
166+
await context.Response.WriteAsync($"Error processing upload: {ex.Message}");
167+
}
168+
});
169+
}
170+
118171
});
119172

120173
ServerURLsProvider.ResolveServerUrlsOnApplicationStarted(app, logger, applicationLifetime, realUrlsAvailableTcs, "/_framework/debug");

src/mono/wasm/host/WebServerStartup.cs

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -166,45 +166,60 @@ public void Configure(IApplicationBuilder app,
166166
context.Response.Redirect("index.html", permanent: false);
167167
return Task.CompletedTask;
168168
});
169-
}); // Add general-purpose file upload endpoint when DEVSERVER_UPLOAD_PATH is set
170-
string? fileUploadPath = Environment.GetEnvironmentVariable("DEVSERVER_UPLOAD_PATH");
171-
if (!string.IsNullOrEmpty(fileUploadPath))
172-
{
173-
// General file upload endpoint
174-
app.MapPost("/upload", async context =>
169+
170+
// Add general-purpose file upload endpoint when DEVSERVER_UPLOAD_PATH is set
171+
string? fileUploadPath = Environment.GetEnvironmentVariable("DEVSERVER_UPLOAD_PATH");
172+
if (!string.IsNullOrEmpty(fileUploadPath))
175173
{
176-
try
174+
// Ensure the upload directory exists
175+
if (!Directory.Exists(fileUploadPath))
177176
{
178-
if (!Directory.Exists(fileUploadPath))
177+
Directory.CreateDirectory(fileUploadPath!);
178+
}
179+
180+
// Route with filename parameter
181+
endpoints.MapPost("/upload/{filename}", async context =>
182+
{
183+
try
179184
{
180-
Directory.CreateDirectory(fileUploadPath!);
185+
// Get the filename from the route
186+
var routeValues = context.Request.RouteValues;
187+
string? rawFileName = routeValues["filename"]?.ToString();
188+
189+
// Generate a unique name if none provided
190+
if (string.IsNullOrEmpty(rawFileName))
191+
{
192+
rawFileName = $"upload_{Guid.NewGuid():N}";
193+
}
194+
195+
// Sanitize filename - IMPORTANT: Only use GetFileName to strip any path components
196+
// This prevents directory traversal attacks like "../../../etc/passwd"
197+
string fileName = Path.GetFileName(rawFileName);
198+
199+
if (string.IsNullOrEmpty(fileName))
200+
{
201+
fileName = $"upload_{Guid.NewGuid():N}";
202+
}
203+
204+
string filePath = Path.Combine(fileUploadPath!, fileName);
205+
206+
using (var outputStream = new FileStream(filePath, FileMode.Create))
207+
{
208+
await context.Request.Body.CopyToAsync(outputStream);
209+
}
210+
211+
_logger?.LogInformation("File uploaded to {FilePath}", filePath);
212+
await context.Response.WriteAsync($"File saved to {filePath}");
181213
}
182-
183-
// Get the filename from the "File-Name" header, or generate a unique one
184-
string fileName = context.Request.Headers["File-Name"].FirstOrDefault() ??
185-
$"upload_{Guid.NewGuid():N}";
186-
187-
// Clean the filename to prevent directory traversal
188-
fileName = Path.GetFileName(fileName);
189-
190-
string filePath = Path.Combine(fileUploadPath!, fileName);
191-
192-
using (var outputStream = new FileStream(filePath, FileMode.Create))
214+
catch (Exception ex)
193215
{
194-
await context.Request.Body.CopyToAsync(outputStream);
216+
_logger?.LogError(ex, "Error processing file upload");
217+
context.Response.StatusCode = 500;
218+
await context.Response.WriteAsync($"Error processing upload: {ex.Message}");
195219
}
196-
197-
_logger?.LogInformation("File uploaded to {FilePath}", filePath);
198-
await context.Response.WriteAsync($"File saved to {filePath}");
199-
}
200-
catch (Exception ex)
201-
{
202-
_logger?.LogError(ex, "Error processing file upload");
203-
context.Response.StatusCode = 500;
204-
await context.Response.WriteAsync($"Error processing upload: {ex.Message}");
205-
}
206-
});
207-
}
220+
});
221+
}
222+
});
208223

209224
ServerURLsProvider.ResolveServerUrlsOnApplicationStarted(app, logger, applicationLifetime, realUrlsAvailableTcs);
210225
}

0 commit comments

Comments
 (0)