Skip to content

Commit 9a51025

Browse files
committed
Add support for launching a browser on file change
1 parent 5266918 commit 9a51025

14 files changed

+545
-21
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Net.WebSockets;
7+
using System.Runtime.ExceptionServices;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.AspNetCore.Builder;
11+
using Microsoft.AspNetCore.Hosting;
12+
using Microsoft.AspNetCore.Hosting.Server;
13+
using Microsoft.AspNetCore.Hosting.Server.Features;
14+
using Microsoft.AspNetCore.Http;
15+
using Microsoft.Extensions.DependencyInjection;
16+
using Microsoft.Extensions.Hosting;
17+
using Microsoft.Extensions.Logging;
18+
using Microsoft.Extensions.Tools.Internal;
19+
20+
namespace Microsoft.DotNet.Watcher.Tools
21+
{
22+
public class BrowserRefreshServer : IDisposable
23+
{
24+
private readonly IReporter _reporter;
25+
private readonly TaskCompletionSource _taskCompletionSource;
26+
private IHost _refreshServer;
27+
private WebSocket _webSocket;
28+
29+
public BrowserRefreshServer(
30+
IReporter reporter)
31+
{
32+
_reporter = reporter;
33+
_taskCompletionSource = new TaskCompletionSource();
34+
}
35+
36+
public string Start()
37+
{
38+
_refreshServer = new HostBuilder()
39+
.ConfigureWebHost(builder =>
40+
{
41+
builder.UseKestrel();
42+
builder.UseUrls("http://127.0.0.1:0");
43+
44+
builder.Configure(app =>
45+
{
46+
app.UseWebSockets();
47+
app.Run(WebSocketRequest);
48+
});
49+
})
50+
.Build();
51+
52+
RunInBackgroundThread(_refreshServer);
53+
54+
var serverUrl = _refreshServer.Services
55+
.GetRequiredService<IServer>()
56+
.Features
57+
.Get<IServerAddressesFeature>()
58+
.Addresses
59+
.First();
60+
61+
return serverUrl.Replace("http://", "ws://");
62+
}
63+
64+
static void RunInBackgroundThread(IHost host)
65+
{
66+
var isDone = new ManualResetEvent(false);
67+
68+
ExceptionDispatchInfo edi = null;
69+
Task.Run(() =>
70+
{
71+
try
72+
{
73+
host.Start();
74+
}
75+
catch (Exception ex)
76+
{
77+
edi = ExceptionDispatchInfo.Capture(ex);
78+
}
79+
80+
isDone.Set();
81+
});
82+
83+
if (!isDone.WaitOne(TimeSpan.FromSeconds(30)))
84+
{
85+
throw new TimeoutException("Timed out waiting to start the host");
86+
}
87+
88+
if (edi != null)
89+
{
90+
throw edi.SourceException;
91+
}
92+
}
93+
94+
async Task WebSocketRequest(HttpContext context)
95+
{
96+
if (!context.WebSockets.IsWebSocketRequest)
97+
{
98+
context.Response.StatusCode = 400;
99+
return;
100+
}
101+
102+
_webSocket = await context.WebSockets.AcceptWebSocketAsync();
103+
await _taskCompletionSource.Task;
104+
}
105+
106+
public async void SendMessage(byte[] messageBytes)
107+
{
108+
if (_webSocket == null || _webSocket.CloseStatus.HasValue)
109+
{
110+
return;
111+
}
112+
113+
try
114+
{
115+
await _webSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None);
116+
}
117+
catch (Exception ex)
118+
{
119+
_reporter.Output($"Refresh server error: {ex}");
120+
}
121+
}
122+
123+
public void Dispose()
124+
{
125+
_refreshServer?.Dispose();
126+
_taskCompletionSource.TrySetResult();
127+
}
128+
}
129+
}

src/Tools/dotnet-watch/src/DotNetWatcher.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
namespace Microsoft.DotNet.Watcher
1616
{
17-
public class DotNetWatcher
17+
public class DotNetWatcher : IDisposable
1818
{
1919
private readonly IReporter _reporter;
2020
private readonly ProcessRunner _processRunner;
@@ -31,6 +31,7 @@ public DotNetWatcher(IReporter reporter, IFileSetFactory fileSetFactory)
3131
{
3232
new MSBuildEvaluationFilter(fileSetFactory),
3333
new NoRestoreFilter(),
34+
new LaunchBrowserFilter(),
3435
};
3536
}
3637

@@ -140,5 +141,16 @@ public async Task WatchAsync(ProcessSpec processSpec, CancellationToken cancella
140141
}
141142
}
142143
}
144+
145+
public void Dispose()
146+
{
147+
foreach (var filter in _filters)
148+
{
149+
if (filter is IDisposable disposable)
150+
{
151+
disposable.Dispose();
152+
}
153+
}
154+
}
143155
}
144156
}

src/Tools/dotnet-watch/src/Internal/MsBuildFileSetFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public async Task<IFileSet> CreateAsync(CancellationToken cancellationToken)
7272
Arguments = new[]
7373
{
7474
"msbuild",
75+
"/nologo",
7576
_projectFile,
7677
$"/p:_DotNetWatchListFile={watchList}"
7778
}.Concat(_buildFlags),

src/Tools/dotnet-watch/src/Internal/ProcessRunner.cs

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,37 +37,49 @@ public async Task<int> RunAsync(ProcessSpec processSpec, CancellationToken cance
3737
{
3838
cancellationToken.Register(() => processState.TryKill());
3939

40-
process.OutputDataReceived += (_, a) =>
40+
var readOutput = false;
41+
var readError = false;
42+
if (processSpec.IsOutputCaptured)
4143
{
42-
if (!string.IsNullOrEmpty(a.Data))
44+
readOutput = true;
45+
readError = true;
46+
process.OutputDataReceived += (_, a) =>
4347
{
44-
processSpec.OutputCapture.AddLine(a.Data);
45-
}
46-
};
47-
process.ErrorDataReceived += (_, a) =>
48-
{
49-
if (!string.IsNullOrEmpty(a.Data))
48+
if (!string.IsNullOrEmpty(a.Data))
49+
{
50+
processSpec.OutputCapture.AddLine(a.Data);
51+
}
52+
};
53+
process.ErrorDataReceived += (_, a) =>
5054
{
51-
processSpec.OutputCapture.AddLine(a.Data);
52-
}
53-
};
55+
if (!string.IsNullOrEmpty(a.Data))
56+
{
57+
processSpec.OutputCapture.AddLine(a.Data);
58+
}
59+
};
60+
}
61+
else if (processSpec.OnOutput != null)
62+
{
63+
readOutput = true;
64+
process.OutputDataReceived += processSpec.OnOutput;
65+
}
5466

5567
stopwatch.Start();
5668
process.Start();
5769

5870
_reporter.Verbose($"Started '{processSpec.Executable}' with process id {process.Id}");
5971

60-
if (processSpec.IsOutputCaptured)
72+
if (readOutput)
6173
{
62-
process.BeginErrorReadLine();
6374
process.BeginOutputReadLine();
64-
await processState.Task;
6575
}
66-
else
76+
if (readError)
6777
{
68-
await processState.Task;
78+
process.BeginErrorReadLine();
6979
}
7080

81+
await processState.Task;
82+
7183
exitCode = process.ExitCode;
7284
stopwatch.Stop();
7385
_reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms");
@@ -87,7 +99,7 @@ private Process CreateProcess(ProcessSpec processSpec)
8799
Arguments = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments),
88100
UseShellExecute = false,
89101
WorkingDirectory = processSpec.WorkingDirectory,
90-
RedirectStandardOutput = processSpec.IsOutputCaptured,
102+
RedirectStandardOutput = processSpec.IsOutputCaptured || (processSpec.OnOutput != null),
91103
RedirectStandardError = processSpec.IsOutputCaptured,
92104
}
93105
};

0 commit comments

Comments
 (0)