diff --git a/.vsts-ci.yml b/.vsts-ci.yml
index 0855f3480606..7cd6e7e314dc 100644
--- a/.vsts-ci.yml
+++ b/.vsts-ci.yml
@@ -97,14 +97,14 @@ extends:
parameters:
pool:
name: $(DncEngInternalBuildPool)
- image: 1es-windows-2022
+ image: windows.vs2022.amd64
os: windows
helixTargetQueue: windows.amd64.vs2022.pre
oneESCompat:
templateFolderName: templates-official
publishTaskPrefix: 1ES.
runtimeSourceProperties: /p:DotNetRuntimeSourceFeed=https://dotnetbuilds.blob.core.windows.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64)
- locBranch: release/9.0.1xx
+ locBranch: release/9.0.2xx
${{ if and(eq(parameters.runTestBuild, false), ne(variables['Build.Reason'], 'PullRequest')) }}:
timeoutInMinutes: 90
windowsJobParameterSets:
diff --git a/CODEOWNERS b/CODEOWNERS
index 4c48269fd3e0..9f5157802082 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -50,6 +50,10 @@
/src/Cli/dotnet/commands/dotnet-vstest @dotnet/dotnet-testing-admin
/test/dotnet-test.Tests @dotnet/dotnet-testing-admin
/test/dotnet-vstest.Tests @dotnet/dotnet-testing-admin
+/test/dotnet-new.Tests @dotnet/dotnet-testing-admin
+/template_feed/Microsoft.DotNet.Common.*/content/MSTest* @dotnet/dotnet-testing-admin
+/template_feed/Microsoft.DotNet.Common.*/content/NUnit* @dotnet/dotnet-testing-admin
+/template_feed/Microsoft.DotNet.Common.*/content/XUnit* @dotnet/dotnet-testing-admin
# Area-Templates
/src/Cli/dotnet/commands/dotnet-new @dotnet/templating-engine-maintainers
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1d941c1212fd..dc9e43222cf0 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,5 +1,5 @@
-
+
$(NoWarn);NU1507
@@ -12,7 +12,7 @@
-
+
@@ -42,7 +42,7 @@
-
+
@@ -66,6 +66,7 @@
+
@@ -94,11 +95,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -118,7 +119,7 @@
-
+
+
+
@@ -14,7 +16,6 @@
-
diff --git a/build.cmd b/build.cmd
index 44e859e0e9f1..132cbefcda29 100644
--- a/build.cmd
+++ b/build.cmd
@@ -4,7 +4,9 @@ echo %* | findstr /C:"-pack" >nul
if %errorlevel%==0 (
set PackInstaller=
) else (
+ REM disable crossgen for inner-loop builds to save a ton of time
set PackInstaller=/p:PackInstaller=false
+ set DISABLE_CROSSGEN=true
)
powershell -NoLogo -NoProfile -ExecutionPolicy ByPass -command "& """%~dp0eng\common\build.ps1""" -restore -build -nativeToolsOnMachine -msbuildEngine dotnet %PackInstaller% %*"
exit /b %ErrorLevel%
diff --git a/build.sh b/build.sh
index 26fd3fae9c2d..0d8b76aca39e 100755
--- a/build.sh
+++ b/build.sh
@@ -9,6 +9,8 @@ done
ScriptRoot="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
if [[ "$@" != *"-pack"* ]]; then
+ # disable crossgen for inner-loop builds to save a ton of time
+ export DISABLE_CROSSGEN=true
packInstallerFlag="/p:PackInstaller=false"
else
packInstallerFlag=
diff --git a/eng/ManualVersions.props b/eng/ManualVersions.props
index e0f285d96bdc..9b67e035d26a 100644
--- a/eng/ManualVersions.props
+++ b/eng/ManualVersions.props
@@ -9,20 +9,20 @@
Basically: In this file, choose the highest version when resolving merge conflicts.
-->
- 10.0.17763.54
- 10.0.18362.54
- 10.0.19041.54
- 10.0.20348.54
- 10.0.22000.54
- 10.0.22621.54
- 10.0.26100.54
- 10.0.17763.52
- 10.0.18362.52
- 10.0.19041.52
- 10.0.20348.52
- 10.0.22000.52
- 10.0.22621.52
- 10.0.26100.52
+ 10.0.17763.57
+ 10.0.18362.57
+ 10.0.19041.57
+ 10.0.20348.57
+ 10.0.22000.57
+ 10.0.22621.57
+ 10.0.26100.57
+ 10.0.17763.55
+ 10.0.18362.55
+ 10.0.19041.55
+ 10.0.20348.55
+ 10.0.22000.55
+ 10.0.22621.55
+ 10.0.26100.55
diff --git a/eng/SourceBuildPrebuiltBaseline.xml b/eng/SourceBuildPrebuiltBaseline.xml
index 3d0527bc5ad8..b729cc725f8c 100644
--- a/eng/SourceBuildPrebuiltBaseline.xml
+++ b/eng/SourceBuildPrebuiltBaseline.xml
@@ -45,6 +45,11 @@
+
+
+
+
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index fffb6f7022f4..d72b5dbc014c 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -15,42 +15,42 @@
44a0b49cb0ae4d3c5400360b5124d451a9c5362e
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
@@ -59,154 +59,154 @@
https://github.com/dotnet/core-setup
7d57652f33493fa022125b7f63aad0d70c52d810
-
+
https://github.com/dotnet/emsdk
- 0b30e0253a0d7c47a99cecd51b0d5ff5c83ad1df
+ 763d10a1a251be35337ee736832bfde3f9200672
-
+
https://github.com/dotnet/emsdk
- 0b30e0253a0d7c47a99cecd51b0d5ff5c83ad1df
+ 763d10a1a251be35337ee736832bfde3f9200672
-
+
https://github.com/dotnet/msbuild
- 1f83ac1d48374297806bda821d894ef2cd8f0352
+ 8f6b8ad0ace90c777c66711c907227fcfb6f2efe
-
+
https://github.com/dotnet/msbuild
- 1f83ac1d48374297806bda821d894ef2cd8f0352
+ 8f6b8ad0ace90c777c66711c907227fcfb6f2efe
-
+
https://github.com/dotnet/msbuild
- 1f83ac1d48374297806bda821d894ef2cd8f0352
+ 8f6b8ad0ace90c777c66711c907227fcfb6f2efe
-
+
https://github.com/dotnet/fsharp
- 02f6bb08e742b0bb1cfd8cd7c104c5ddbcc4cbf8
+ 5af96504f10836eca6ce804ab3cdd28c042fa35a
-
+
https://github.com/dotnet/fsharp
- 02f6bb08e742b0bb1cfd8cd7c104c5ddbcc4cbf8
+ 5af96504f10836eca6ce804ab3cdd28c042fa35a
-
+
https://github.com/dotnet/roslyn
- eadb8aee946dcc1fd8889d3a8ab6e8398514275e
+ f9ccce030d8fdcc2ea095e095b71316d243c5b35
-
+
https://github.com/dotnet/roslyn
- eadb8aee946dcc1fd8889d3a8ab6e8398514275e
+ f9ccce030d8fdcc2ea095e095b71316d243c5b35
-
+
https://github.com/dotnet/roslyn
- eadb8aee946dcc1fd8889d3a8ab6e8398514275e
+ f9ccce030d8fdcc2ea095e095b71316d243c5b35
-
+
https://github.com/dotnet/roslyn
- eadb8aee946dcc1fd8889d3a8ab6e8398514275e
+ f9ccce030d8fdcc2ea095e095b71316d243c5b35
-
+
https://github.com/dotnet/roslyn
- eadb8aee946dcc1fd8889d3a8ab6e8398514275e
+ f9ccce030d8fdcc2ea095e095b71316d243c5b35
-
+
https://github.com/dotnet/roslyn
- eadb8aee946dcc1fd8889d3a8ab6e8398514275e
+ f9ccce030d8fdcc2ea095e095b71316d243c5b35
-
+
https://github.com/dotnet/roslyn
- eadb8aee946dcc1fd8889d3a8ab6e8398514275e
+ f9ccce030d8fdcc2ea095e095b71316d243c5b35
-
+
https://github.com/dotnet/roslyn
- eadb8aee946dcc1fd8889d3a8ab6e8398514275e
+ f9ccce030d8fdcc2ea095e095b71316d243c5b35
-
+
https://github.com/dotnet/roslyn
- eadb8aee946dcc1fd8889d3a8ab6e8398514275e
+ f9ccce030d8fdcc2ea095e095b71316d243c5b35
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
-
+
https://github.com/nuget/nuget.client
- 1a8ca6067347fd68340090e29464ef58a0c9ef62
+ c60352c138a25e8a4bde0775a7c2e0600b186b0b
https://github.com/microsoft/vstest
@@ -226,196 +226,166 @@
bc9161306b23641b0364b8f93d546da4d48da1eb
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-windowsdesktop
- 4c2c5a0efe35521e7637d228bfba823f6b63dd74
+ 308dc7955704be60afc72ec00902cc18e028c3c2
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-windowsdesktop
- 4c2c5a0efe35521e7637d228bfba823f6b63dd74
+ 308dc7955704be60afc72ec00902cc18e028c3c2
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-windowsdesktop
- 4c2c5a0efe35521e7637d228bfba823f6b63dd74
+ 308dc7955704be60afc72ec00902cc18e028c3c2
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-windowsdesktop
- 4c2c5a0efe35521e7637d228bfba823f6b63dd74
+ 308dc7955704be60afc72ec00902cc18e028c3c2
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-wpf
- 49ab7e335401671d3909153f2ca0dddc6030628d
+ a04736acb8edb533756131d3d5fc55f15cd03d6a
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://github.com/dotnet/razor
- e22180b5fd5d599723561ca6f85b58f6adb1cbce
+ 60d689b3add229efff4c6ab7611dbc615961fa2c
-
+
https://github.com/dotnet/razor
- e22180b5fd5d599723561ca6f85b58f6adb1cbce
+ 60d689b3add229efff4c6ab7611dbc615961fa2c
-
+
https://github.com/dotnet/razor
- e22180b5fd5d599723561ca6f85b58f6adb1cbce
+ 60d689b3add229efff4c6ab7611dbc615961fa2c
-
+
https://github.com/dotnet/razor
- e22180b5fd5d599723561ca6f85b58f6adb1cbce
+ 60d689b3add229efff4c6ab7611dbc615961fa2c
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
-
-
- https://github.com/dotnet/test-templates
- 0385265f4d0b6413d64aea0223172366a9b9858c
-
-
- https://github.com/dotnet/test-templates
- 307b8f538d83a955d8f6dd909eee41a5555f2f4d
-
-
- https://github.com/dotnet/test-templates
- becc4bd157cd6608b51a5ffe414a5d2de6330272
-
-
- https://github.com/dotnet/test-templates
- becc4bd157cd6608b51a5ffe414a5d2de6330272
-
-
- https://github.com/dotnet/test-templates
- 49c9ad01f057b3c6352bbec12b117acc2224493c
-
-
- https://github.com/dotnet/test-templates
- 764e98851850e9ebdf7c3ff556f893068bc5871d
-
-
-
- https://github.com/dotnet/test-templates
- 764e98851850e9ebdf7c3ff556f893068bc5871d
-
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-winforms
- d30c019e3116e813215903d7330ea18e620f8d16
+ 62ebdb4b0d5cc7e163b8dc9331dc196e576bf162
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-wpf
- 49ab7e335401671d3909153f2ca0dddc6030628d
+ a04736acb8edb533756131d3d5fc55f15cd03d6a
-
+
https://github.com/dotnet/xdt
- 1a54480f52703fb45fac2a6b955247d33758383e
+ 63ae81154c50a1cf9287cc47d8351d55b4289e6d
-
+
https://github.com/dotnet/xdt
- 1a54480f52703fb45fac2a6b955247d33758383e
+ 63ae81154c50a1cf9287cc47d8351d55b4289e6d
-
+
https://github.com/dotnet/roslyn-analyzers
- 3d61c57c73c3dd5f1f407ef9cd3414d94bf0eaf2
+ 5bfaf6aea5cf9d1c924d9adc69916eac3be07880
-
+
https://github.com/dotnet/roslyn-analyzers
- 3d61c57c73c3dd5f1f407ef9cd3414d94bf0eaf2
+ 5bfaf6aea5cf9d1c924d9adc69916eac3be07880
-
+
https://github.com/dotnet/roslyn-analyzers
- 3d61c57c73c3dd5f1f407ef9cd3414d94bf0eaf2
+ 5bfaf6aea5cf9d1c924d9adc69916eac3be07880
@@ -441,9 +411,9 @@
-
+
https://github.com/dotnet/source-build-externals
- b11ed370b79aa475535a5803856b7c7d0977235e
+ 1ffe36b3379a0b7ced63d134e5daf30948c03306
@@ -456,34 +426,34 @@
https://github.com/dotnet/deployment-tools
7871ee378dce87b64d930d4f33dca9c888f4034d
-
+
https://github.com/dotnet/sourcelink
- 196983c6e97621b051c67cd898c4db630168cd81
+ 4e176206614b345352885b55491aeb51bf77526b
-
+
https://github.com/dotnet/sourcelink
- 196983c6e97621b051c67cd898c4db630168cd81
+ 4e176206614b345352885b55491aeb51bf77526b
-
+
https://github.com/dotnet/sourcelink
- 196983c6e97621b051c67cd898c4db630168cd81
+ 4e176206614b345352885b55491aeb51bf77526b
-
+
https://github.com/dotnet/sourcelink
- 196983c6e97621b051c67cd898c4db630168cd81
+ 4e176206614b345352885b55491aeb51bf77526b
-
+
https://github.com/dotnet/sourcelink
- 196983c6e97621b051c67cd898c4db630168cd81
+ 4e176206614b345352885b55491aeb51bf77526b
-
+
https://github.com/dotnet/sourcelink
- 196983c6e97621b051c67cd898c4db630168cd81
+ 4e176206614b345352885b55491aeb51bf77526b
-
+
https://github.com/dotnet/sourcelink
- 196983c6e97621b051c67cd898c4db630168cd81
+ 4e176206614b345352885b55491aeb51bf77526b
@@ -499,125 +469,125 @@
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore
- c70204ae3c91d2b48fa6d9b92b62265f368421b4
+ af22effae4069a5dfb9b0735859de48820104f5b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
-
+
https://github.com/dotnet/arcade
- 3c393bbd85ae16ddddba20d0b75035b0c6f1a52d
+ b41381d5cd633471265e9cd72e933a7048e03062
-
+
https://github.com/dotnet/arcade
- 3c393bbd85ae16ddddba20d0b75035b0c6f1a52d
+ b41381d5cd633471265e9cd72e933a7048e03062
-
+
https://github.com/dotnet/arcade
- 3c393bbd85ae16ddddba20d0b75035b0c6f1a52d
+ b41381d5cd633471265e9cd72e933a7048e03062
-
+
https://github.com/dotnet/arcade
- 3c393bbd85ae16ddddba20d0b75035b0c6f1a52d
+ b41381d5cd633471265e9cd72e933a7048e03062
-
+
https://github.com/dotnet/arcade
- 3c393bbd85ae16ddddba20d0b75035b0c6f1a52d
+ b41381d5cd633471265e9cd72e933a7048e03062
-
+
https://github.com/dotnet/arcade
- 3c393bbd85ae16ddddba20d0b75035b0c6f1a52d
+ b41381d5cd633471265e9cd72e933a7048e03062
-
+
https://github.com/dotnet/arcade
- 3c393bbd85ae16ddddba20d0b75035b0c6f1a52d
+ b41381d5cd633471265e9cd72e933a7048e03062
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- 990ebf52fc408ca45929fd176d2740675a67fab8
+ 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3
https://github.com/dotnet/arcade-services
@@ -627,14 +597,14 @@
https://github.com/dotnet/arcade-services
47e3672c762970073e4282bd563233da86bcca3e
-
+
https://github.com/dotnet/scenario-tests
- 1009e3b6d23e049de56b91de82fe975fe84444f8
+ 643219946c3266f3a41bf7b7e2d0224865d2ddf8
-
+
https://github.com/dotnet/scenario-tests
- 1009e3b6d23e049de56b91de82fe975fe84444f8
+ 643219946c3266f3a41bf7b7e2d0224865d2ddf8
<_NET70ILLinkPackVersion>7.0.100-1.23211.1
-
-
- $(VersionFeature)
-
- true
- true
- true
-
https://dotnetbuilds.blob.core.windows.net/public/
https://dotnetclimsrc.blob.core.windows.net/dotnet/
- 9.0.0-preview.24522.2
+ 10.0.0-preview.24609.2
1.0.0-20230414.1
2.22.0
2.0.1-servicing-26011-01
@@ -91,81 +75,78 @@
1.1.0-beta.24367.3
-
- 9.0.0-rc.2.24474.1
+
+ 9.1.0-preview.1.24555.3
-
-
- 1.1.0-rc.24069.1
- 1.1.0-rc.24202.1
- 1.1.0-rtm.24530.3
+
+ 9.0.0-rtm.24529.1
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
+ 9.0.0
+ 9.0.0-rtm.24528.9
+ 9.0.0
+ 9.0.0
+ 9.0.0-rtm.24528.9
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
8.0.0-rc.1.23414.4
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
+ 9.0.0-rtm.24528.9
+ 9.0.0-rtm.24528.9
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
2.1.0
- 9.0.0-rc.2.24473.5
+ 9.0.0
8.0.0
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
8.0.0
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
+ 9.0.0
8.0.4
- 9.0.0-rc.2.24473.5
- 9.0.0-rc.2.24473.5
+ 9.0.0
+ 9.0.0
- 9.0.0-rc.2.24474.4
- 9.0.0-rc.2.24474.4
- 9.0.0-rc.2.24474.4
- 9.0.0-rc.2.24474.4
+ 9.0.0-rtm.24529.2
+ 9.0.0-rtm.24529.2
+ 9.0.0
+ 9.0.0
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
- 6.13.0-preview.1.23
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
+ 6.13.0-rc.98
@@ -175,8 +156,8 @@
- 9.0.0-preview.24508.2
- 3.11.0-beta1.24508.2
+ 9.0.0-preview.24527.2
+ 3.11.0-beta1.24527.2
@@ -187,17 +168,20 @@
Some .NET Framework tasks and the resolver will need to run in a VS/MSBuild that is older
than the very latest, based on what we want the SDK to support. So use a version that matches the version
- in minimumMSBuildVersion. Note that MSBuild has started versioning before release so the version we use as the Minimum should be .0
- to ensure we load in VS but the version we build against should be the version of MSBuild that ships in the .0 VS release.
+ in minimumMSBuildVersion. Note that MSBuild has started versioning before release so the version we use as the Minimum should be .0
+ to ensure we load in VS but the version we build against should be the version of MSBuild that ships in the .0 VS release.
In these cases, we don't want to use MicrosoftBuildVersion and other
associated properties that are updated by the VMR infrastructure. So, we read this version
from the 'minimumMSBuildVersion' file in non-source-only cases into MicrosoftBuildMinimumVersion,
then use that in Directory.Packages.props.
- At usage sites, either we use MicrosoftBuildMinimumVersion, or MicrosoftBuildVersion in source-only modes. -->
- 17.13.0-preview-24530-03
- 17.13.0-preview-24530-03
+ At usage sites, either we use MicrosoftBuildMinimumVersion, or MicrosoftBuildVersion in source-only modes.
+
+ Additionally, set the MinimumVSVersion for the installer UI that's required for targeting NetCurrent -->
+ 17.13.0-preview-24569-04
+ 17.13.0-preview-24569-04
17.11.4
+ 17.12
@@ -214,45 +198,45 @@
- 13.9.200-beta.24529.3
+ 13.9.200-beta.24617.1
- 4.13.0-2.24529.5
- 4.13.0-2.24529.5
- 4.13.0-2.24529.5
- 4.13.0-2.24529.5
- 4.13.0-2.24529.5
- 4.13.0-2.24529.5
- 4.13.0-2.24529.5
- 4.13.0-2.24529.5
+ 4.13.0-3.24617.3
+ 4.13.0-3.24617.3
+ 4.13.0-3.24617.3
+ 4.13.0-3.24617.3
+ 4.13.0-3.24617.3
+ 4.13.0-3.24617.3
+ 4.13.0-3.24617.3
+ 4.13.0-3.24617.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
+ 9.0.0
+ 9.0.0-rtm.24529.3
+ 9.0.0-rtm.24529.3
+ 9.0.0-rtm.24529.3
+ 9.0.0-rtm.24529.3
+ 9.0.0-rtm.24529.3
+ 9.0.0
+ 9.0.0
+ 9.0.0-rtm.24529.3
+ 9.0.0-rtm.24529.3
+ 9.0.0-rtm.24529.3
+ 9.0.0-rtm.24529.3
+ 9.0.0-rtm.24529.3
- 9.0.0-preview.24529.2
- 9.0.0-preview.24529.2
- 9.0.0-preview.24529.2
+ 9.0.0-preview.24612.1
+ 9.0.0-preview.24612.1
+ 9.0.0-preview.24612.1
- 9.0.0-rc.2.24474.3
- 9.0.0-rc.2.24474.3
+ 9.0.0-rtm.24529.2
+ 9.0.0-rtm.24529.2
@@ -275,7 +259,7 @@
2.2.0-beta.19072.10
2.0.0
- 9.0.0-preview.24514.1
+ 9.0.0-preview.24617.2
@@ -284,19 +268,19 @@
- 9.0.0-beta.24516.2
- 9.0.0-beta.24516.2
- 9.0.0-beta.24516.2
- 9.0.0-beta.24516.2
+ 9.0.0-beta.24572.2
+ 9.0.0-beta.24572.2
+ 9.0.0-beta.24572.2
+ 9.0.0-beta.24572.2
- 9.0.0-beta.24529.3
- 9.0.0-beta.24529.3
- 9.0.0-beta.24529.3
- 9.0.0-beta.24529.3
- 9.0.0-beta.24529.3
- 9.0.0-beta.24529.3
+ 9.0.0-beta.24617.1
+ 9.0.0-beta.24617.1
+ 9.0.0-beta.24617.1
+ 9.0.0-beta.24617.1
+ 9.0.0-beta.24617.1
+ 9.0.0-beta.24617.1
@@ -323,16 +307,16 @@
8.0.100
8.2.2
- 9.0.100-rc.2
- 9.0.0-rc.2.24503.2
- 35.0.0-rc.2.152
- 18.0.9600-net9-rc2
- 18.0.9600-net9-rc2
- 15.0.9600-net9-rc2
- 18.0.9600-net9-rc2
+ 9.0.100
+ 9.0.0
+ 35.0.7
+ 18.0.9617
+ 18.0.9617
+ 15.0.9617
+ 18.0.9617
- 9.0.0-rc.2.24468.8
- $(MicrosoftNETWorkloadEmscriptenCurrentManifest90100TransportPackageVersion)
+ 9.0.0
+ $(MicrosoftNETWorkloadEmscriptenCurrentManifest90100PackageVersion)
9.0.100$([System.Text.RegularExpressions.Regex]::Match($(EmscriptenWorkloadManifestVersion), `-(?!rtm)[A-z]*[\.]*\d*`))
diff --git a/eng/common/sdk-task.ps1 b/eng/common/sdk-task.ps1
index aab40de3fd9a..4f0546dce120 100644
--- a/eng/common/sdk-task.ps1
+++ b/eng/common/sdk-task.ps1
@@ -64,7 +64,7 @@ try {
$GlobalJson.tools | Add-Member -Name "vs" -Value (ConvertFrom-Json "{ `"version`": `"16.5`" }") -MemberType NoteProperty
}
if( -not ($GlobalJson.tools.PSObject.Properties.Name -match "xcopy-msbuild" )) {
- $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "17.10.0-pre.4.0" -MemberType NoteProperty
+ $GlobalJson.tools | Add-Member -Name "xcopy-msbuild" -Value "17.12.0" -MemberType NoteProperty
}
if ($GlobalJson.tools."xcopy-msbuild".Trim() -ine "none") {
$xcopyMSBuildToolsFolder = InitializeXCopyMSBuild $GlobalJson.tools."xcopy-msbuild" -install $true
diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1
index 22954477a574..aa94fb174596 100644
--- a/eng/common/tools.ps1
+++ b/eng/common/tools.ps1
@@ -383,8 +383,8 @@ function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements =
# If the version of msbuild is going to be xcopied,
# use this version. Version matches a package here:
- # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/17.10.0-pre.4.0
- $defaultXCopyMSBuildVersion = '17.10.0-pre.4.0'
+ # https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/Microsoft.DotNet.Arcade.MSBuild.Xcopy/versions/17.12.0
+ $defaultXCopyMSBuildVersion = '17.12.0'
if (!$vsRequirements) {
if (Get-Member -InputObject $GlobalJson.tools -Name 'vs') {
diff --git a/eng/pipelines/templates/jobs/sdk-job-matrix.yml b/eng/pipelines/templates/jobs/sdk-job-matrix.yml
index 8a7c75d1ba21..0d863564b914 100644
--- a/eng/pipelines/templates/jobs/sdk-job-matrix.yml
+++ b/eng/pipelines/templates/jobs/sdk-job-matrix.yml
@@ -12,9 +12,10 @@ parameters:
runTestsAsTool: true
# This job uses the build step for testing, so the extra test step is not necessary.
runTests: false
- - categoryName: TemplateEngine
- testProjects: $(Build.SourcesDirectory)/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj;$(Build.SourcesDirectory)/test/dotnet-new.Tests/dotnet-new.IntegrationTests.csproj
- publishXunitResults: true
+ # Turn off template engine runs on Windows temporarily until agent images are updated
+ #- categoryName: TemplateEngine
+ # testProjects: $(Build.SourcesDirectory)/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj;$(Build.SourcesDirectory)/test/dotnet-new.Tests/dotnet-new.IntegrationTests.csproj
+ # publishXunitResults: true
- categoryName: AoT
runAoTTests: true
### LINUX ###
diff --git a/eng/pipelines/templates/steps/vmr-prepare.yml b/eng/pipelines/templates/steps/vmr-prepare.yml
index 0a34630a5400..8cb5bae7815c 100644
--- a/eng/pipelines/templates/steps/vmr-prepare.yml
+++ b/eng/pipelines/templates/steps/vmr-prepare.yml
@@ -10,9 +10,12 @@ steps:
displayName: Clone dotnet/dotnet
path: vmr
clean: true
+ fetchTags: true
+ fetchDepth: 0
- - script: |
- git checkout --track origin/${{ parameters.vmrBranch }}
- echo "##vso[task.setvariable variable=vmrBranch]${{ parameters.vmrBranch }}"
+ - powershell: |
+ $branchName = "${{ parameters.vmrBranch }}" -replace "refs/heads/", ""
+ git checkout --track origin/$branchName || exit 1
+ echo "##vso[task.setvariable variable=vmrBranch]$branchName"
displayName: Check out ${{ parameters.vmrBranch }}
workingDirectory: $(Agent.BuildDirectory)/vmr
diff --git a/eng/pipelines/templates/variables/vmr-build.yml b/eng/pipelines/templates/variables/vmr-build.yml
index e6287d3f5679..20b9c3badec1 100644
--- a/eng/pipelines/templates/variables/vmr-build.yml
+++ b/eng/pipelines/templates/variables/vmr-build.yml
@@ -21,7 +21,7 @@ variables:
- name: centOSStreamContainer
value: mcr.microsoft.com/dotnet-buildtools/prereqs:centos-stream9
- name: fedoraContainer
- value: mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-40
+ value: mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-41
- name: ubuntuContainer
value: mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-24.04
- name: ubuntuArmContainer
@@ -56,7 +56,7 @@ variables:
- name: centOSStreamName
value: CentOSStream9
- name: fedoraName
- value: Fedora40
+ value: Fedora41
- name: ubuntuName
value: Ubuntu2404
@@ -75,7 +75,7 @@ variables:
- name: centOSStreamX64Rid
value: centos.9-x64
- name: fedoraX64Rid
- value: fedora.40-x64
+ value: fedora.41-x64
- name: ubuntux64Rid
value: ubuntu.24.04-x64
- name: ubuntuArm64Rid
diff --git a/eng/pipelines/vmr-build-pr-internal.yml b/eng/pipelines/vmr-build-pr-internal.yml
new file mode 100644
index 000000000000..61a764d172f6
--- /dev/null
+++ b/eng/pipelines/vmr-build-pr-internal.yml
@@ -0,0 +1,73 @@
+# This YAML is used by these PR pipelines:
+#
+# - dotnet-sdk-source-build-internal
+# https://dev.azure.com/dnceng/internal/_build?definitionId=1378
+
+trigger: none
+pr:
+ branches:
+ include:
+ - internal/release/*
+ exclude:
+ - internal/release/*.0.2xx
+ - internal/release/*.0.3xx
+ - internal/release/*.0.4xx
+
+parameters:
+- name: vmrBranch
+ displayName: dotnet/dotnet branch to push to
+ type: string
+ default: ' '
+
+- name: disableBuild
+ displayName: Skip the VMR Build stage
+ type: boolean
+ default: false
+
+variables:
+- template: /eng/common/templates/variables/pool-providers.yml@self
+
+- ${{ if ne(parameters.vmrBranch, ' ') }}:
+ - name: VmrBranch
+ value: ${{ replace(parameters.vmrBranch, ' ', '') }}
+- ${{ else }}:
+ - name: VmrBranch
+ value: ${{ replace(replace(variables['System.PullRequest.TargetBranch'], 'refs/heads/', ''), 'refs/pull/', '') }}
+
+# enable source-only build for pipelines with the -source-build suffix
+- name: isSourceOnlyBuild
+ value: ${{ contains(variables['Build.DefinitionName'], '-source-build') }}
+
+resources:
+ repositories:
+ - repository: vmr
+ name: dotnet-dotnet
+ type: git
+ ref: ${{ variables.VmrBranch }}
+
+stages:
+# You can temporarily disable the VMR Build stage by setting the disableBuild variable
+- ${{ if not(parameters.disableBuild) }}:
+ - template: templates/stages/vmr-build.yml
+ parameters:
+ vmrBranch: ${{ variables.VmrBranch }}
+ isBuiltFromVmr: false
+ isSourceOnlyBuild: ${{ variables.isSourceOnlyBuild }}
+ ${{ if contains(variables['Build.DefinitionName'], '-full') }}:
+ scope: full
+ ${{ elseif eq(variables.isSourceOnlyBuild, 'true') }}:
+ scope: ultralite
+ ${{ else }}:
+ scope: lite
+
+# In case the VMR Build stage is temporarily disabled, the VMR synchronization step is run to validate
+# that the PR can be merged and later synchronized into the VMR without problems.
+- ${{ else }}:
+ - stage: Synchronize_VMR
+ displayName: Synchronize VMR
+ dependsOn: []
+ jobs:
+ - template: templates/jobs/vmr-synchronization.yml
+ parameters:
+ vmrBranch: ${{ variables.VmrBranch }}
+ noPush: true
diff --git a/global.json b/global.json
index d2eb21c71695..10b7560a80ab 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"tools": {
- "dotnet": "9.0.100-rc.2.24474.11",
+ "dotnet": "9.0.100",
"runtimes": {
"dotnet": [
"$(VSRedistCommonNetCoreSharedFrameworkx6490PackageVersion)"
@@ -17,8 +17,8 @@
"cmake": "latest"
},
"msbuild-sdks": {
- "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.24516.2",
- "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.24516.2",
+ "Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.24572.2",
+ "Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.24572.2",
"Microsoft.Build.NoTargets": "3.7.0",
"Microsoft.DotNet.CMake.Sdk": "9.0.0-beta.24217.1"
}
diff --git a/sdk.sln b/sdk.sln
index 8107e6e40cfe..52840d8fce54 100644
--- a/sdk.sln
+++ b/sdk.sln
@@ -210,9 +210,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PublishProfiles", "PublishP
src\WebSdk\Publish\Targets\PublishProfiles\Default.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\Default.pubxml
src\WebSdk\Publish\Targets\PublishProfiles\DefaultMSDeploy.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultMSDeploy.pubxml
src\WebSdk\Publish\Targets\PublishProfiles\DefaultMSDeployPackage.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultMSDeployPackage.pubxml
- src\WebSdk\Publish\Targets\PublishProfiles\DefaultZipDeploy.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultZipDeploy.pubxml
src\WebSdk\Publish\Targets\PublishProfiles\DefaultOneDeploy.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultOneDeploy.pubxml
src\WebSdk\Publish\Targets\PublishProfiles\DefaultWebJobOneDeploy.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultWebJobOneDeploy.pubxml
+ src\WebSdk\Publish\Targets\PublishProfiles\DefaultZipDeploy.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultZipDeploy.pubxml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PublishTargets", "PublishTargets", "{40E6E4B1-286B-4542-B814-2A3DA29510D1}"
@@ -222,8 +222,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PublishTargets", "PublishTa
src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.Kudu.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.Kudu.targets
src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.MSDeploy.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.MSDeploy.targets
src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.MSDeployPackage.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.MSDeployPackage.targets
- src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.ZipDeploy.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.ZipDeploy.targets
src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.OneDeploy.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.OneDeploy.targets
+ src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.ZipDeploy.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.ZipDeploy.targets
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TransformTargets", "TransformTargets", "{DFA91CC3-D6E4-45B7-AF6F-4385288886E4}"
@@ -512,6 +512,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Net.Sdk.Compilers
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.WebTools.AspireService.Tests", "test\Microsoft.WebTools.AspireService.Tests\Microsoft.WebTools.AspireService.Tests.csproj", "{1F0B4B3C-DC88-4740-B04F-1707102E9930}"
EndProject
+Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.DotNet.HotReload.Agent", "src\BuiltInTools\HotReloadAgent\Microsoft.DotNet.HotReload.Agent.shproj", "{418B10BD-CA42-49F3-8F4A-D8CC90C8A17D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.HotReload.Agent.Package", "src\BuiltInTools\HotReloadAgent\Microsoft.DotNet.HotReload.Agent.Package.csproj", "{2FF79F82-60C1-349A-4726-7783D5A6D5DF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -974,6 +978,10 @@ Global
{1F0B4B3C-DC88-4740-B04F-1707102E9930}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F0B4B3C-DC88-4740-B04F-1707102E9930}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F0B4B3C-DC88-4740-B04F-1707102E9930}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2FF79F82-60C1-349A-4726-7783D5A6D5DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2FF79F82-60C1-349A-4726-7783D5A6D5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2FF79F82-60C1-349A-4726-7783D5A6D5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2FF79F82-60C1-349A-4726-7783D5A6D5DF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1153,6 +1161,8 @@ Global
{94C8526E-DCC2-442F-9868-3DD0BA2688BE} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
{FA579C03-2EB4-4D47-88EE-BFF339E96FAF} = {22AB674F-ED91-4FBC-BFEE-8A1E82F9F05E}
{1F0B4B3C-DC88-4740-B04F-1707102E9930} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
+ {418B10BD-CA42-49F3-8F4A-D8CC90C8A17D} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
+ {2FF79F82-60C1-349A-4726-7783D5A6D5DF} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6}
@@ -1160,7 +1170,9 @@ Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{03c5a84a-982b-4f38-ac73-ab832c645c4a}*SharedItemsImports = 5
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{0a3c9afd-f6e6-4a5d-83fb-93bf66732696}*SharedItemsImports = 5
+ src\BuiltInTools\HotReloadAgent\Microsoft.DotNet.HotReload.Agent.projitems*{1bbfa19c-03f0-4d27-9d0d-0f8172642107}*SharedItemsImports = 5
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{1f0b4b3c-dc88-4740-b04f-1707102e9930}*SharedItemsImports = 5
+ src\BuiltInTools\HotReloadAgent\Microsoft.DotNet.HotReload.Agent.projitems*{418b10bd-ca42-49f3-8f4a-d8cc90c8a17d}*SharedItemsImports = 13
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{445efbd5-6730-4f09-943d-278e77501ffd}*SharedItemsImports = 5
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{94c8526e-dcc2-442f-9868-3dd0ba2688be}*SharedItemsImports = 13
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{9d36039f-d0a1-462f-85b4-81763c6b02cb}*SharedItemsImports = 13
diff --git a/src/BuiltInTools/AspireService/AspireServerService.cs b/src/BuiltInTools/AspireService/AspireServerService.cs
index d3450d65ad18..e10019989af3 100644
--- a/src/BuiltInTools/AspireService/AspireServerService.cs
+++ b/src/BuiltInTools/AspireService/AspireServerService.cs
@@ -1,25 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Immutable;
+using System;
+using System.Collections.Generic;
using System.Net;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
+using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using Microsoft.WebTools.AspireServer.Contracts;
-using Microsoft.WebTools.AspireServer.Helpers;
-using Microsoft.WebTools.AspireServer.Models;
-using Microsoft.WebTools.AspireService.Helpers;
using IAsyncDisposable = System.IAsyncDisposable;
-namespace Microsoft.WebTools.AspireServer;
+namespace Aspire.Tools.Service;
///
/// Implementation of the AspireServerService. A new instance of this service will be created for each
@@ -86,7 +86,7 @@ public AspireServerService(IAspireServerEvents aspireServerEvents, string displa
_certificateEncodedBytes = Convert.ToBase64String(certBytes);
// Kick of the web server.
- _requestListener = StartListening();
+ _requestListener = StartListeningAsync();
}
public async ValueTask DisposeAsync()
@@ -183,7 +183,7 @@ bool LogAndPropagate(Exception e)
/// Waits for a connection so that it can get the WebSocket that will be used to send messages tio the client. It accepts messages via Restful http
/// calls.
///
- private Task StartListening()
+ private Task StartListeningAsync()
{
var builder = WebApplication.CreateSlimBuilder();
diff --git a/src/BuiltInTools/AspireService/Contracts/IAspireServerEvents.cs b/src/BuiltInTools/AspireService/Contracts/IAspireServerEvents.cs
index 1a4b5b6ddee2..dda162760e8d 100644
--- a/src/BuiltInTools/AspireService/Contracts/IAspireServerEvents.cs
+++ b/src/BuiltInTools/AspireService/Contracts/IAspireServerEvents.cs
@@ -1,7 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.WebTools.AspireServer.Contracts;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+namespace Aspire.Tools.Service;
internal interface IAspireServerEvents
{
diff --git a/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs b/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs
index a0b0f7766d48..7aff7d6a7126 100644
--- a/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs
+++ b/src/BuiltInTools/AspireService/Helpers/CertGenerator.cs
@@ -1,10 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
-namespace Microsoft.WebTools.AspireServer;
+namespace Aspire.Tools.Service;
internal static class CertGenerator
{
diff --git a/src/BuiltInTools/AspireService/Helpers/ExceptionExtensions.cs b/src/BuiltInTools/AspireService/Helpers/ExceptionExtensions.cs
index ed3fc9321271..3fbb1d0caef1 100644
--- a/src/BuiltInTools/AspireService/Helpers/ExceptionExtensions.cs
+++ b/src/BuiltInTools/AspireService/Helpers/ExceptionExtensions.cs
@@ -3,7 +3,7 @@
using System;
-namespace Microsoft.WebTools.AspireServer.Helpers;
+namespace Aspire.Tools.Service;
internal static class ExceptionExtensions
{
diff --git a/src/BuiltInTools/AspireService/Helpers/HttpContextExtensions.cs b/src/BuiltInTools/AspireService/Helpers/HttpContextExtensions.cs
index 6c04e6553f66..710a1e69baaa 100644
--- a/src/BuiltInTools/AspireService/Helpers/HttpContextExtensions.cs
+++ b/src/BuiltInTools/AspireService/Helpers/HttpContextExtensions.cs
@@ -5,10 +5,8 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
-using Microsoft.WebTools.AspireServer.Contracts;
-using Microsoft.WebTools.AspireServer.Models;
-namespace Microsoft.WebTools.AspireServer.Helpers;
+namespace Aspire.Tools.Service;
internal static class HttpContextExtensions
{
diff --git a/src/BuiltInTools/AspireService/Helpers/LoggerProvider.cs b/src/BuiltInTools/AspireService/Helpers/LoggerProvider.cs
index 52953e211022..499bd506289a 100644
--- a/src/BuiltInTools/AspireService/Helpers/LoggerProvider.cs
+++ b/src/BuiltInTools/AspireService/Helpers/LoggerProvider.cs
@@ -1,9 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
using Microsoft.Extensions.Logging;
-namespace Microsoft.WebTools.AspireService.Helpers;
+namespace Aspire.Tools.Service;
internal sealed class LoggerProvider(Action reporter) : ILoggerProvider
{
diff --git a/src/BuiltInTools/AspireService/Helpers/SocketConnectionManager.cs b/src/BuiltInTools/AspireService/Helpers/SocketConnectionManager.cs
index 8f088759a6a0..46e6388a58c3 100644
--- a/src/BuiltInTools/AspireService/Helpers/SocketConnectionManager.cs
+++ b/src/BuiltInTools/AspireService/Helpers/SocketConnectionManager.cs
@@ -7,7 +7,7 @@
using System.Threading;
using System.Threading.Tasks;
-namespace Microsoft.WebTools.AspireServer;
+namespace Aspire.Tools.Service;
///
/// Manages the set of active socket connections. Since it registers to be notified when a socket has gone bad,
diff --git a/src/BuiltInTools/AspireService/Helpers/SocketUtilities.cs b/src/BuiltInTools/AspireService/Helpers/SocketUtilities.cs
index 76921f3a6a65..3e93de3b0232 100644
--- a/src/BuiltInTools/AspireService/Helpers/SocketUtilities.cs
+++ b/src/BuiltInTools/AspireService/Helpers/SocketUtilities.cs
@@ -2,12 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
using System.Linq;
+using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
-namespace Microsoft.WebTools.AspireServer.Helpers;
+namespace Aspire.Tools.Service;
internal class SocketUtilities
{
diff --git a/src/BuiltInTools/AspireService/Helpers/WebSocketConnection.cs b/src/BuiltInTools/AspireService/Helpers/WebSocketConnection.cs
index bf41ac6ded5c..a39b74bfb6dc 100644
--- a/src/BuiltInTools/AspireService/Helpers/WebSocketConnection.cs
+++ b/src/BuiltInTools/AspireService/Helpers/WebSocketConnection.cs
@@ -2,11 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
+using System.Net.WebSockets;
-namespace Microsoft.WebTools.AspireServer;
+namespace Aspire.Tools.Service;
///
/// Used by the SocketConnectionManager to track one socket connection. It needs to be disposed when done with it
diff --git a/src/BuiltInTools/AspireService/Microsoft.WebTools.AspireService.Package.csproj b/src/BuiltInTools/AspireService/Microsoft.WebTools.AspireService.Package.csproj
index 3b46ec9fdf21..f317ebe3fcf1 100644
--- a/src/BuiltInTools/AspireService/Microsoft.WebTools.AspireService.Package.csproj
+++ b/src/BuiltInTools/AspireService/Microsoft.WebTools.AspireService.Package.csproj
@@ -22,4 +22,9 @@
+
+
+
+
+
diff --git a/src/BuiltInTools/AspireService/Models/ErrorResponse.cs b/src/BuiltInTools/AspireService/Models/ErrorResponse.cs
index 0f5094ef1940..7eb0bcc46a6d 100644
--- a/src/BuiltInTools/AspireService/Models/ErrorResponse.cs
+++ b/src/BuiltInTools/AspireService/Models/ErrorResponse.cs
@@ -3,7 +3,7 @@
using System.Text.Json.Serialization;
-namespace Microsoft.WebTools.AspireServer.Models;
+namespace Aspire.Tools.Service;
///
/// Detailed error information serialized into the body of the response
diff --git a/src/BuiltInTools/AspireService/Models/InfoResponse.cs b/src/BuiltInTools/AspireService/Models/InfoResponse.cs
index ef05bcd6cb11..dda6f8bc3a97 100644
--- a/src/BuiltInTools/AspireService/Models/InfoResponse.cs
+++ b/src/BuiltInTools/AspireService/Models/InfoResponse.cs
@@ -3,7 +3,7 @@
using System.Text.Json.Serialization;
-namespace Microsoft.WebTools.AspireServer.Models;
+namespace Aspire.Tools.Service;
///
/// Response when asked for /info
diff --git a/src/BuiltInTools/AspireService/Models/RunSessionRequest.cs b/src/BuiltInTools/AspireService/Models/RunSessionRequest.cs
index 1d0d55fb2d5f..f2b9bc496675 100644
--- a/src/BuiltInTools/AspireService/Models/RunSessionRequest.cs
+++ b/src/BuiltInTools/AspireService/Models/RunSessionRequest.cs
@@ -2,14 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json.Serialization;
-using Microsoft.WebTools.AspireServer.Contracts;
-namespace Microsoft.WebTools.AspireServer.Models;
+namespace Aspire.Tools.Service;
internal class EnvVar
{
diff --git a/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs b/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs
index d20e38f3360f..fd9803c6e4a3 100644
--- a/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs
+++ b/src/BuiltInTools/AspireService/Models/SessionChangeNotification.cs
@@ -4,7 +4,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
-namespace Microsoft.WebTools.AspireServer.Models;
+namespace Aspire.Tools.Service;
internal static class NotificationType
{
diff --git a/src/BuiltInTools/BrowserRefresh/.editorconfig b/src/BuiltInTools/BrowserRefresh/.editorconfig
new file mode 100644
index 000000000000..4ae90dd7a7a0
--- /dev/null
+++ b/src/BuiltInTools/BrowserRefresh/.editorconfig
@@ -0,0 +1,2 @@
+[*.js]
+indent_size = 2
diff --git a/src/BuiltInTools/BrowserRefresh/BlazorHotReload.js b/src/BuiltInTools/BrowserRefresh/BlazorHotReload.js
index 0c979861ea82..87a203450f01 100644
--- a/src/BuiltInTools/BrowserRefresh/BlazorHotReload.js
+++ b/src/BuiltInTools/BrowserRefresh/BlazorHotReload.js
@@ -1,3 +1,6 @@
+// Used by older versions of Microsoft.AspNetCore.Components.WebAssembly.
+// For back compat only to support WASM packages older than the SDK.
+
export function receiveHotReload() {
return BINDING.js_to_mono_obj(new Promise((resolve) => receiveHotReloadAsync().then(resolve(0))));
}
@@ -5,10 +8,12 @@ export function receiveHotReload() {
export async function receiveHotReloadAsync() {
const response = await fetch('/_framework/blazor-hotreload');
if (response.status === 200) {
- const deltas = await response.json();
- if (deltas) {
+ const updates = await response.json();
+ if (updates) {
try {
- deltas.forEach(d => window.Blazor._internal.applyHotReload(d.moduleId, d.metadataDelta, d.ilDelta, d.pdbDelta, d.updatedTypes));
+ updates.forEach(u => {
+ u.deltas.forEach(d => window.Blazor._internal.applyHotReload(d.moduleId, d.metadataDelta, d.ilDelta, d.pdbDelta, d.updatedTypes));
+ })
} catch (error) {
console.warn(error);
return;
diff --git a/src/BuiltInTools/BrowserRefresh/BlazorWasmHotReloadMiddleware.cs b/src/BuiltInTools/BrowserRefresh/BlazorWasmHotReloadMiddleware.cs
index f5b4da6f12bf..6ccdd09f6bc3 100644
--- a/src/BuiltInTools/BrowserRefresh/BlazorWasmHotReloadMiddleware.cs
+++ b/src/BuiltInTools/BrowserRefresh/BlazorWasmHotReloadMiddleware.cs
@@ -1,10 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
-using Microsoft.Net.Http.Headers;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Watch.BrowserRefresh
{
@@ -12,27 +11,43 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
/// A middleware that manages receiving and sending deltas from a BlazorWebAssembly app.
/// This assembly is shared between Visual Studio and dotnet-watch. By putting some of the complexity
/// in here, we can avoid duplicating work in watch and VS.
+ ///
+ /// Mapped to .
///
internal sealed class BlazorWasmHotReloadMiddleware
{
- private readonly object @lock = new();
- private readonly string EtagDiscriminator = Guid.NewGuid().ToString();
- private readonly JsonSerializerOptions _jsonSerializerOptions = new()
+ internal sealed class Update
+ {
+ public int Id { get; set; }
+ public Delta[] Deltas { get; set; } = default!;
+ }
+
+ internal sealed class Delta
+ {
+ public string ModuleId { get; set; } = default!;
+ public string MetadataDelta { get; set; } = default!;
+ public string ILDelta { get; set; } = default!;
+ public string PdbDelta { get; set; } = default!;
+ public int[] UpdatedTypes { get; set; } = default!;
+ }
+
+ private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
- public BlazorWasmHotReloadMiddleware(RequestDelegate next)
+ public BlazorWasmHotReloadMiddleware(RequestDelegate next, ILogger logger)
{
+ logger.LogDebug("Middleware loaded");
}
- internal List Deltas { get; } = new();
+ internal List Updates { get; } = [];
public Task InvokeAsync(HttpContext context)
{
// Multiple instances of the BlazorWebAssembly app could be running (multiple tabs or multiple browsers).
// We want to avoid serialize reads and writes between then
- lock (@lock)
+ lock (Updates)
{
if (HttpMethods.IsGet(context.Request.Method))
{
@@ -54,85 +69,31 @@ public Task InvokeAsync(HttpContext context)
private async Task OnGet(HttpContext context)
{
- if (Deltas.Count == 0)
+ if (Updates.Count == 0)
{
context.Response.StatusCode = StatusCodes.Status204NoContent;
return;
}
- if (EtagMatches(context))
- {
- context.Response.StatusCode = StatusCodes.Status304NotModified;
- return;
- }
-
- WriteETag(context);
- await JsonSerializer.SerializeAsync(context.Response.Body, Deltas, _jsonSerializerOptions);
- }
-
- private bool EtagMatches(HttpContext context)
- {
- if (context.Request.Headers[HeaderNames.IfNoneMatch] is not { Count: 1 } ifNoneMatch)
- {
- return false;
- }
-
- var expected = GetETag();
- return string.Equals(expected, ifNoneMatch[0], StringComparison.Ordinal);
+ await JsonSerializer.SerializeAsync(context.Response.Body, Updates, s_jsonSerializerOptions);
}
private async Task OnPost(HttpContext context)
{
- var updateDeltas = await JsonSerializer.DeserializeAsync(context.Request.Body, _jsonSerializerOptions);
- AppendDeltas(updateDeltas);
-
- WriteETag(context);
- }
-
- private void WriteETag(HttpContext context)
- {
- var etag = GetETag();
- if (etag is not null)
- {
- context.Response.Headers[HeaderNames.ETag] = etag;
- }
- }
-
- private string? GetETag()
- {
- if (Deltas.Count == 0)
- {
- return null;
- }
-
- return string.Format(CultureInfo.InvariantCulture, "W/\"{0}{1}\"", EtagDiscriminator, Deltas[^1].SequenceId);
- }
-
- private void AppendDeltas(UpdateDelta[]? updateDeltas)
- {
- if (updateDeltas == null || updateDeltas.Length == 0)
+ var update = await JsonSerializer.DeserializeAsync(context.Request.Body, s_jsonSerializerOptions);
+ if (update == null)
{
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
// It's possible that multiple instances of the BlazorWasm are simultaneously executing and could be posting the same deltas
// We'll use the sequence id to ensure that we're not recording duplicate entries. Replaying duplicated values would cause
// ApplyDelta to fail.
- // It's currently not possible to receive different ranges of sequences from different clients (for e.g client 1 sends deltas 1 - 3,
- // and client 2 sends deltas 2 - 4, client 3 sends 1 - 5 etc), so we only need to verify that the first item in the sequence has not already been seen.
- if (Deltas.Count == 0 || Deltas[^1].SequenceId < updateDeltas[0].SequenceId)
+ if (Updates is [] || Updates[^1].Id < update.Id)
{
- Deltas.AddRange(updateDeltas);
+ Updates.Add(update);
}
}
-
- internal class UpdateDelta
- {
- public int SequenceId { get; set; }
- public string ModuleId { get; set; } = default!;
- public string MetadataDelta { get; set; } = default!;
- public string ILDelta { get; set; } = default!;
- public int[]? UpdatedTypes { get; set; } = default!;
- }
}
}
diff --git a/src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs b/src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs
index 4e37ff24ed6a..8def7c99c239 100644
--- a/src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs
+++ b/src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs
@@ -9,22 +9,26 @@
namespace Microsoft.AspNetCore.Watch.BrowserRefresh
{
- public class BrowserRefreshMiddleware
+ public sealed class BrowserRefreshMiddleware
{
- private static readonly MediaTypeHeaderValue _textHtmlMediaType = new("text/html");
- private static readonly MediaTypeHeaderValue _applicationJsonMediaType = new("application/json");
- private readonly string? _dotnetModifiableAssemblies = GetNonEmptyEnvironmentVariableValue("DOTNET_MODIFIABLE_ASSEMBLIES");
- private readonly string? _aspnetcoreBrowserTools = GetNonEmptyEnvironmentVariableValue("__ASPNETCORE_BROWSER_TOOLS");
-
+ private static readonly MediaTypeHeaderValue s_textHtmlMediaType = new("text/html");
+ private static readonly MediaTypeHeaderValue s_applicationJsonMediaType = new("application/json");
private readonly RequestDelegate _next;
- private readonly ILogger _logger;
+ private readonly ILogger _logger;
+ private string? _dotnetModifiableAssemblies = GetNonEmptyEnvironmentVariableValue("DOTNET_MODIFIABLE_ASSEMBLIES");
+ private string? _aspnetcoreBrowserTools = GetNonEmptyEnvironmentVariableValue("__ASPNETCORE_BROWSER_TOOLS");
+
+ public BrowserRefreshMiddleware(RequestDelegate next, ILogger logger)
+ {
+ _next = next;
+ _logger = logger;
+
+ logger.LogDebug("Middleware loaded: DOTNET_MODIFIABLE_ASSEMBLIES={ModifiableAssemblies}, __ASPNETCORE_BROWSER_TOOLS={BrowserTools}", _dotnetModifiableAssemblies, _aspnetcoreBrowserTools);
+ }
private static string? GetNonEmptyEnvironmentVariableValue(string name)
=> Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : null;
- public BrowserRefreshMiddleware(RequestDelegate next, ILogger logger) =>
- (_next, _logger) = (next, logger);
-
public async Task InvokeAsync(HttpContext context)
{
if (IsWebAssemblyBootRequest(context))
@@ -76,7 +80,7 @@ private void AttachWebAssemblyHeaders(HttpContext context)
{
if (!context.Response.Headers.ContainsKey("DOTNET-MODIFIABLE-ASSEMBLIES"))
{
- if(_dotnetModifiableAssemblies != null)
+ if (_dotnetModifiableAssemblies != null)
{
context.Response.Headers.Add("DOTNET-MODIFIABLE-ASSEMBLIES", _dotnetModifiableAssemblies);
}
@@ -141,7 +145,7 @@ internal static bool IsWebAssemblyBootRequest(HttpContext context)
for (var i = 0; i < acceptHeaders.Count; i++)
{
- if (acceptHeaders[i].MatchesAllTypes || acceptHeaders[i].IsSubsetOf(_applicationJsonMediaType))
+ if (acceptHeaders[i].MatchesAllTypes || acceptHeaders[i].IsSubsetOf(s_applicationJsonMediaType))
{
return true;
}
@@ -175,7 +179,7 @@ internal static bool IsBrowserDocumentRequest(HttpContext context)
for (var i = 0; i < acceptHeaders.Count; i++)
{
- if (acceptHeaders[i].IsSubsetOf(_textHtmlMediaType))
+ if (acceptHeaders[i].IsSubsetOf(s_textHtmlMediaType))
{
return true;
}
@@ -184,6 +188,12 @@ internal static bool IsBrowserDocumentRequest(HttpContext context)
return false;
}
+ internal void Test_SetEnvironment(string dotnetModifiableAssemblies, string aspnetcoreBrowserTools)
+ {
+ _dotnetModifiableAssemblies = dotnetModifiableAssemblies;
+ _aspnetcoreBrowserTools = aspnetcoreBrowserTools;
+ }
+
internal static class Log
{
private static readonly Action _setupResponseForBrowserRefresh = LoggerMessage.Define(
diff --git a/src/BuiltInTools/BrowserRefresh/BrowserScriptMiddleware.cs b/src/BuiltInTools/BrowserRefresh/BrowserScriptMiddleware.cs
index aa82bf273d0c..167be5d4aec6 100644
--- a/src/BuiltInTools/BrowserRefresh/BrowserScriptMiddleware.cs
+++ b/src/BuiltInTools/BrowserRefresh/BrowserScriptMiddleware.cs
@@ -3,6 +3,7 @@
using System.Globalization;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Watch.BrowserRefresh
{
@@ -12,13 +13,19 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
///
public sealed class BrowserScriptMiddleware
{
- private readonly byte[] _scriptBytes;
+ private readonly PathString _scriptPath;
+ private readonly ReadOnlyMemory _scriptBytes;
+ private readonly ILogger _logger;
private readonly string _contentLength;
- public BrowserScriptMiddleware(RequestDelegate next, byte[] scriptBytes)
+ public BrowserScriptMiddleware(RequestDelegate next, PathString scriptPath, ReadOnlyMemory scriptBytes, ILogger logger)
{
+ _scriptPath = scriptPath;
_scriptBytes = scriptBytes;
+ _logger = logger;
_contentLength = _scriptBytes.Length.ToString(CultureInfo.InvariantCulture);
+
+ logger.LogDebug("Middleware loaded. Script {scriptPath} ({size} B).", scriptPath, _contentLength);
}
public async Task InvokeAsync(HttpContext context)
@@ -27,10 +34,13 @@ public async Task InvokeAsync(HttpContext context)
context.Response.Headers["Content-Length"] = _contentLength;
context.Response.Headers["Content-Type"] = "application/javascript; charset=utf-8";
- await context.Response.Body.WriteAsync(_scriptBytes.AsMemory(), context.RequestAborted);
+ await context.Response.Body.WriteAsync(_scriptBytes, context.RequestAborted);
+
+ _logger.LogDebug("Script injected: {scriptPath}", _scriptPath);
}
- internal static byte[] GetBlazorHotReloadJS()
+ // for backwards compat only
+ internal static ReadOnlyMemory GetBlazorHotReloadJS()
{
var jsFileName = "Microsoft.AspNetCore.Watch.BrowserRefresh.BlazorHotReload.js";
using var stream = new MemoryStream();
@@ -40,7 +50,7 @@ internal static byte[] GetBlazorHotReloadJS()
return stream.ToArray();
}
- internal static byte[] GetBrowserRefreshJS()
+ internal static ReadOnlyMemory GetBrowserRefreshJS()
{
var endpoint = Environment.GetEnvironmentVariable("ASPNETCORE_AUTO_RELOAD_WS_ENDPOINT")!;
var serverKey = Environment.GetEnvironmentVariable("ASPNETCORE_AUTO_RELOAD_WS_KEY") ?? string.Empty;
@@ -48,7 +58,7 @@ internal static byte[] GetBrowserRefreshJS()
return GetWebSocketClientJavaScript(endpoint, serverKey);
}
- internal static byte[] GetWebSocketClientJavaScript(string hostString, string serverKey)
+ internal static ReadOnlyMemory GetWebSocketClientJavaScript(string hostString, string serverKey)
{
var jsFileName = "Microsoft.AspNetCore.Watch.BrowserRefresh.WebSocketScriptInjection.js";
using var reader = new StreamReader(typeof(WebSocketScriptInjection).Assembly.GetManifestResourceStream(jsFileName)!);
diff --git a/src/BuiltInTools/BrowserRefresh/HostingStartup.cs b/src/BuiltInTools/BrowserRefresh/HostingStartup.cs
index 06d400d26578..369133ccbec3 100644
--- a/src/BuiltInTools/BrowserRefresh/HostingStartup.cs
+++ b/src/BuiltInTools/BrowserRefresh/HostingStartup.cs
@@ -44,10 +44,11 @@ public Action Configure(Action next)
app.Map(ApplicationPaths.BlazorHotReloadMiddleware, static app => app.UseMiddleware());
app.Map(ApplicationPaths.BrowserRefreshJS,
- static app => app.UseMiddleware(BrowserScriptMiddleware.GetBrowserRefreshJS()));
+ static app => app.UseMiddleware(ApplicationPaths.BrowserRefreshJS, BrowserScriptMiddleware.GetBrowserRefreshJS()));
+ // backwards compat only:
app.Map(ApplicationPaths.BlazorHotReloadJS,
- static app => app.UseMiddleware(BrowserScriptMiddleware.GetBlazorHotReloadJS()));
+ static app => app.UseMiddleware(ApplicationPaths.BlazorHotReloadJS, BrowserScriptMiddleware.GetBlazorHotReloadJS()));
});
app.UseMiddleware();
diff --git a/src/BuiltInTools/BrowserRefresh/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj b/src/BuiltInTools/BrowserRefresh/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj
index 1bcb35c0ad9d..c0074ad0fc3d 100644
--- a/src/BuiltInTools/BrowserRefresh/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj
+++ b/src/BuiltInTools/BrowserRefresh/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj
@@ -11,17 +11,19 @@
+
+
-
-
+
+
+
+
+
+
diff --git a/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js b/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js
index 6530cd2c8ae0..aa8f1fd60f98 100644
--- a/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js
+++ b/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js
@@ -44,8 +44,9 @@ setTimeout(async function () {
const payload = JSON.parse(message.data);
const action = {
'UpdateStaticFile': () => updateStaticFile(payload.path),
- 'BlazorHotReloadDeltav1': () => applyBlazorDeltas(payload.sharedSecret, payload.deltas, false),
- 'BlazorHotReloadDeltav2': () => applyBlazorDeltas(payload.sharedSecret, payload.deltas, true),
+ 'BlazorHotReloadDeltav1': () => applyBlazorDeltas_legacy(payload.sharedSecret, payload.deltas, false),
+ 'BlazorHotReloadDeltav2': () => applyBlazorDeltas_legacy(payload.sharedSecret, payload.deltas, true),
+ 'BlazorHotReloadDeltav3': () => applyBlazorDeltas(payload.sharedSecret, payload.updateId, payload.deltas, payload.responseLoggingLevel),
'HotReloadDiagnosticsv1': () => displayDiagnostics(payload.diagnostics),
'BlazorRequestApplyUpdateCapabilities': () => getBlazorWasmApplyUpdateCapabilities(false),
'BlazorRequestApplyUpdateCapabilities2': () => getBlazorWasmApplyUpdateCapabilities(true),
@@ -95,19 +96,22 @@ setTimeout(async function () {
.forEach(e => updateCssElement(e));
}
+ function getMessageAndStack(error) {
+ const message = error.message || ''
+ let messageAndStack = error.stack || message
+ if (!messageAndStack.includes(message)) {
+ messageAndStack = message + "\n" + messageAndStack;
+ }
+
+ return messageAndStack
+ }
+
function getBlazorWasmApplyUpdateCapabilities(sendErrorToClient) {
let applyUpdateCapabilities;
try {
applyUpdateCapabilities = window.Blazor._internal.getApplyUpdateCapabilities();
} catch (error) {
- const message = error.message || ''
- let messageAndStack = error.stack || message
- if (!messageAndStack.includes(message))
- {
- messageAndStack = message + "\n" + messageAndStack;
- }
-
- applyUpdateCapabilities = sendErrorToClient ? "!" + messageAndStack : '';
+ applyUpdateCapabilities = sendErrorToClient ? "!" + getMessageAndStack(error) : '';
}
connection.send(applyUpdateCapabilities);
}
@@ -133,7 +137,7 @@ setTimeout(async function () {
styleElement.parentNode.insertBefore(newElement, styleElement.nextSibling);
}
- async function applyBlazorDeltas(serverSecret, deltas, sendErrorToClient) {
+ async function applyBlazorDeltas_legacy(serverSecret, deltas, sendErrorToClient) {
if (sharedSecret && (serverSecret != sharedSecret.encodedSharedSecret)) {
// Validate the shared secret if it was specified. It might be unspecified in older versions of VS
// that do not support this feature as yet.
@@ -141,22 +145,20 @@ setTimeout(async function () {
}
let applyError = undefined;
- if (window.Blazor?._internal?.applyHotReload) {
- // Only apply hot reload deltas if Blazor has been initialized.
- // It's possible for Blazor to start after the initial page load, so we don't consider skipping this step
- // to be a failure. These deltas will get applied later, when Blazor completes initialization.
- deltas.forEach(d => {
- try {
- window.Blazor._internal.applyHotReload(d.moduleId, d.metadataDelta, d.ilDelta, d.pdbDelta, d.updatedTypes)
- } catch (error) {
- console.warn(error);
- applyError = error;
- }
- });
+
+ try {
+ applyDeltas_legacy(deltas)
+ } catch (error) {
+ console.warn(error);
+ applyError = error;
}
+ const body = JSON.stringify({
+ id: deltas[0].sequenceId,
+ deltas: deltas
+ });
try {
- await fetch('/_framework/blazor-hotreload', { method: 'post', headers: { 'content-type': 'application/json' }, body: JSON.stringify(deltas) });
+ await fetch('/_framework/blazor-hotreload', { method: 'post', headers: { 'content-type': 'application/json' }, body: body });
} catch (error) {
console.warn(error);
applyError = error;
@@ -170,6 +172,99 @@ setTimeout(async function () {
}
}
+ function applyDeltas_legacy(deltas) {
+ let apply = window.Blazor?._internal?.applyHotReload
+
+ // Only apply hot reload deltas if Blazor has been initialized.
+ // It's possible for Blazor to start after the initial page load, so we don't consider skipping this step
+ // to be a failure. These deltas will get applied later, when Blazor completes initialization.
+ if (apply) {
+ deltas.forEach(d => {
+ if (apply.length == 5) {
+ // WASM 8.0
+ apply(d.moduleId, d.metadataDelta, d.ilDelta, d.pdbDelta, d.updatedTypes)
+ } else {
+ // WASM 9.0
+ apply(d.moduleId, d.metadataDelta, d.ilDelta, d.pdbDelta)
+ }
+ });
+ }
+ }
+ function sendDeltaApplied() {
+ connection.send(new Uint8Array([1]).buffer);
+ }
+
+ function sendDeltaNotApplied(error) {
+ if (error) {
+ let encoder = new TextEncoder()
+ connection.send(encoder.encode("\0" + error.message + "\0" + error.stack));
+ } else {
+ connection.send(new Uint8Array([0]).buffer);
+ }
+ }
+
+ async function applyBlazorDeltas(serverSecret, updateId, deltas, responseLoggingLevel) {
+ if (sharedSecret && (serverSecret != sharedSecret.encodedSharedSecret)) {
+ // Validate the shared secret if it was specified. It might be unspecified in older versions of VS
+ // that do not support this feature as yet.
+ throw 'Unable to validate the server. Rejecting apply-update payload.';
+ }
+
+ const AgentMessageSeverity_Error = 2
+
+ let applyError = undefined;
+ let log = [];
+ try {
+ let applyDeltas = window.Blazor?._internal?.applyHotReloadDeltas
+ if (applyDeltas) {
+ // Only apply hot reload deltas if Blazor has been initialized.
+ // It's possible for Blazor to start after the initial page load, so we don't consider skipping this step
+ // to be a failure. These deltas will get applied later, when Blazor completes initialization.
+
+ let wasmDeltas = deltas.map(delta => {
+ return {
+ "moduleId": delta.moduleId,
+ "metadataDelta": delta.metadataDelta,
+ "ilDelta": delta.ilDelta,
+ "pdbDelta": delta.pdbDelta,
+ "updatedTypes": delta.updatedTypes,
+ };
+ });
+
+ log = applyDeltas(wasmDeltas, responseLoggingLevel);
+ } else {
+ // Try invoke older WASM API:
+ applyDeltas_legacy(deltas)
+ }
+ } catch (error) {
+ console.warn(error);
+ applyError = error;
+ log.push({ "message": getMessageAndStack(error), "severity": AgentMessageSeverity_Error });
+ }
+
+ try {
+ let body = JSON.stringify({
+ "id": updateId,
+ "deltas": deltas
+ });
+
+ await fetch('/_framework/blazor-hotreload', { method: 'post', headers: { 'content-type': 'application/json' }, body: body });
+ } catch (error) {
+ console.warn(error);
+ applyError = error;
+ log.push({ "message": getMessageAndStack(error), "severity": AgentMessageSeverity_Error });
+ }
+
+ connection.send(JSON.stringify({
+ "success": !applyError,
+ "log": log
+ }));
+
+ if (!applyError) {
+ notifyHotReloadApplied();
+ }
+ }
+
function displayDiagnostics(diagnostics) {
document.querySelectorAll('#dotnet-compile-error').forEach(el => el.remove());
const el = document.body.appendChild(document.createElement('div'));
@@ -222,19 +317,6 @@ setTimeout(async function () {
}
}
- function sendDeltaApplied() {
- connection.send(new Uint8Array([1]).buffer);
- }
-
- function sendDeltaNotApplied(error) {
- if (error) {
- let encoder = new TextEncoder()
- connection.send(encoder.encode("\0" + error.message + "\0" + error.stack));
- } else {
- connection.send(new Uint8Array([0]).buffer);
- }
- }
-
async function getSecret(serverKeyString) {
if (!serverKeyString || !window.crypto || !window.crypto.subtle) {
return null;
diff --git a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs
deleted file mode 100644
index 36dbbae52846..000000000000
--- a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs
+++ /dev/null
@@ -1,217 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Concurrent;
-using System.Diagnostics;
-using System.IO.Pipes;
-using System.Reflection;
-
-namespace Microsoft.Extensions.HotReload
-{
- internal sealed class HotReloadAgent : IDisposable
- {
- private const string MetadataUpdaterTypeName = "System.Reflection.Metadata.MetadataUpdater";
- private const string ApplyUpdateMethodName = "ApplyUpdate";
- private const string GetCapabilitiesMethodName = "GetCapabilities";
-
- private delegate void ApplyUpdateDelegate(Assembly assembly, ReadOnlySpan metadataDelta, ReadOnlySpan ilDelta, ReadOnlySpan pdbDelta);
-
- private readonly AgentReporter _reporter = new();
- private readonly NamedPipeClientStream _pipeClient;
- private readonly Action _stdOutLog;
- private readonly AssemblyLoadEventHandler _assemblyLoad;
- private readonly ConcurrentDictionary> _deltas = new();
- private readonly ConcurrentDictionary _appliedAssemblies = new();
- private readonly ApplyUpdateDelegate? _applyUpdate;
- private readonly string? _capabilities;
- private readonly MetadataUpdateHandlerInvoker _metadataUpdateHandlerInvoker;
-
- public HotReloadAgent(NamedPipeClientStream pipeClient, Action stdOutLog)
- {
- _assemblyLoad = OnAssemblyLoad;
- _pipeClient = pipeClient;
- _stdOutLog = stdOutLog;
- _metadataUpdateHandlerInvoker = new(_reporter);
-
- GetUpdaterMethods(out _applyUpdate, out _capabilities);
- AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad;
- }
-
- private void GetUpdaterMethods(out ApplyUpdateDelegate? applyUpdate, out string? capabilities)
- {
- applyUpdate = null;
- capabilities = null;
-
- var metadataUpdater = Type.GetType(MetadataUpdaterTypeName + ", System.Runtime.Loader", throwOnError: false);
- if (metadataUpdater == null)
- {
- _reporter.Report($"Type not found: {MetadataUpdaterTypeName}", AgentMessageSeverity.Error);
- return;
- }
-
- var applyUpdateMethod = metadataUpdater.GetMethod(ApplyUpdateMethodName, BindingFlags.Public | BindingFlags.Static, binder: null, [typeof(Assembly), typeof(ReadOnlySpan), typeof(ReadOnlySpan), typeof(ReadOnlySpan)], modifiers: null);
- if (applyUpdateMethod == null)
- {
- _reporter.Report($"{MetadataUpdaterTypeName}.{ApplyUpdateMethodName} not found.", AgentMessageSeverity.Error);
- return;
- }
-
- applyUpdate = (ApplyUpdateDelegate)applyUpdateMethod.CreateDelegate(typeof(ApplyUpdateDelegate));
-
- var getCapabilities = metadataUpdater.GetMethod(GetCapabilitiesMethodName, BindingFlags.NonPublic | BindingFlags.Static, binder: null, Type.EmptyTypes, modifiers: null);
- if (getCapabilities == null)
- {
- _reporter.Report($"{MetadataUpdaterTypeName}.{GetCapabilitiesMethodName} not found.", AgentMessageSeverity.Error);
- return;
- }
-
- try
- {
- capabilities = getCapabilities.Invoke(obj: null, parameters: null) as string;
- }
- catch (Exception e)
- {
- _reporter.Report($"Error retrieving capabilities: {e.Message}", AgentMessageSeverity.Error);
- }
- }
-
- public async Task ReceiveDeltasAsync()
- {
- _reporter.Report("Writing capabilities: " + Capabilities, AgentMessageSeverity.Verbose);
-
- var initPayload = new ClientInitializationPayload(Capabilities);
- initPayload.Write(_pipeClient);
-
- while (_pipeClient.IsConnected)
- {
- var update = await UpdatePayload.ReadAsync(_pipeClient, CancellationToken.None);
-
- _stdOutLog($"ResponseLoggingLevel = {update.ResponseLoggingLevel}");
-
- _reporter.Report("Attempting to apply deltas.", AgentMessageSeverity.Verbose);
-
- ApplyDeltas(update.Deltas);
-
- _pipeClient.WriteByte(UpdatePayload.ApplySuccessValue);
-
- UpdatePayload.WriteLog(_pipeClient, _reporter.GetAndClearLogEntries(update.ResponseLoggingLevel));
- }
- }
-
- public string Capabilities => _capabilities ?? string.Empty;
-
- private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs)
- {
- _metadataUpdateHandlerInvoker.Clear();
-
- var loadedAssembly = eventArgs.LoadedAssembly;
- var moduleId = TryGetModuleId(loadedAssembly);
- if (moduleId is null)
- {
- return;
- }
-
- if (_deltas.TryGetValue(moduleId.Value, out var updateDeltas) && _appliedAssemblies.TryAdd(loadedAssembly, loadedAssembly))
- {
- // A delta for this specific Module exists and we haven't called ApplyUpdate on this instance of Assembly as yet.
- ApplyDeltas(loadedAssembly, updateDeltas);
- }
- }
-
- public void ApplyDeltas(IReadOnlyList deltas)
- {
- Debug.Assert(Capabilities.Length > 0);
- Debug.Assert(_applyUpdate != null);
-
- for (var i = 0; i < deltas.Count; i++)
- {
- var item = deltas[i];
- foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
- {
- if (TryGetModuleId(assembly) is Guid moduleId && moduleId == item.ModuleId)
- {
- _applyUpdate(assembly, item.MetadataDelta, item.ILDelta, pdbDelta: []);
- }
- }
-
- // Additionally stash the deltas away so it may be applied to assemblies loaded later.
- var cachedDeltas = _deltas.GetOrAdd(item.ModuleId, static _ => new());
- cachedDeltas.Add(item);
- }
-
- _metadataUpdateHandlerInvoker.Invoke(GetMetadataUpdateTypes(deltas));
- }
-
- private Type[] GetMetadataUpdateTypes(IReadOnlyList deltas)
- {
- List? types = null;
-
- foreach (var delta in deltas)
- {
- var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => TryGetModuleId(assembly) is Guid moduleId && moduleId == delta.ModuleId);
- if (assembly is null)
- {
- continue;
- }
-
- foreach (var updatedType in delta.UpdatedTypes)
- {
- // Must be a TypeDef.
- Debug.Assert(updatedType >> 24 == 0x02);
-
- // The type has to be in the manifest module since Hot Reload does not support multi-module assemblies:
- try
- {
- var type = assembly.ManifestModule.ResolveType(updatedType);
- types ??= new();
- types.Add(type);
- }
- catch (Exception e)
- {
- _reporter.Report($"Failed to load type 0x{updatedType:X8}: {e.Message}", AgentMessageSeverity.Warning);
- }
- }
- }
-
- return types?.ToArray() ?? Type.EmptyTypes;
- }
-
- public void ApplyDeltas(Assembly assembly, IReadOnlyList deltas)
- {
- Debug.Assert(_applyUpdate != null);
-
- try
- {
- foreach (var item in deltas)
- {
- _applyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty);
- }
-
- _reporter.Report("Deltas applied.", AgentMessageSeverity.Verbose);
- }
- catch (Exception ex)
- {
- _reporter.Report(ex.ToString(), AgentMessageSeverity.Warning);
- }
- }
-
- public void Dispose()
- {
- AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoad;
- }
-
- private static Guid? TryGetModuleId(Assembly loadedAssembly)
- {
- try
- {
- return loadedAssembly.Modules.FirstOrDefault()?.ModuleVersionId;
- }
- catch
- {
- // Assembly.Modules might throw. See https://github.com/dotnet/aspnetcore/issues/33152
- }
-
- return default;
- }
- }
-}
diff --git a/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj b/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj
index 097eb0287927..c59a80a6e1c1 100644
--- a/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj
+++ b/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj
@@ -1,4 +1,5 @@
+
+ $(VisualStudioServiceTargetFramework)
+ false
+ none
+ false
+ enable
+ preview
+
+
+ true
+ true
+ Microsoft.DotNet.HotReload.Agent
+ false
+
+ Package containing sources of Hot Reload agent.
+
+
+ $(NoWarn);NU5128
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.projitems b/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.projitems
new file mode 100644
index 000000000000..c28d7de9cdf7
--- /dev/null
+++ b/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.projitems
@@ -0,0 +1,14 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ 418B10BD-CA42-49F3-8F4A-D8CC90C8A17D
+
+
+ Microsoft.DotNet.HotReload
+
+
+
+
+
\ No newline at end of file
diff --git a/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.shproj b/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.shproj
new file mode 100644
index 000000000000..8bec0ced75ec
--- /dev/null
+++ b/src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.shproj
@@ -0,0 +1,13 @@
+
+
+
+ {418B10BD-CA42-49F3-8F4A-D8CC90C8A17D}
+ 14.0
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/BuiltInTools/HotReloadAgent/ResponseLoggingLevel.cs b/src/BuiltInTools/HotReloadAgent/ResponseLoggingLevel.cs
new file mode 100644
index 000000000000..b081d74326f1
--- /dev/null
+++ b/src/BuiltInTools/HotReloadAgent/ResponseLoggingLevel.cs
@@ -0,0 +1,10 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.HotReload;
+
+internal enum ResponseLoggingLevel : byte
+{
+ WarningsAndErrors = 0,
+ Verbose = 1,
+}
diff --git a/src/BuiltInTools/HotReloadAgent/UpdateDelta.cs b/src/BuiltInTools/HotReloadAgent/UpdateDelta.cs
new file mode 100644
index 000000000000..3277755dea56
--- /dev/null
+++ b/src/BuiltInTools/HotReloadAgent/UpdateDelta.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Microsoft.DotNet.Watch;
+
+internal readonly struct UpdateDelta(Guid moduleId, byte[] metadataDelta, byte[] ilDelta, byte[] pdbDelta, int[] updatedTypes)
+{
+ public Guid ModuleId { get; } = moduleId;
+ public byte[] MetadataDelta { get; } = metadataDelta;
+ public byte[] ILDelta { get; } = ilDelta;
+ public byte[] PdbDelta { get; } = pdbDelta;
+ public int[] UpdatedTypes { get; } = updatedTypes;
+}
diff --git a/src/BuiltInTools/dotnet-format/Commands/FormatCommandCommon.cs b/src/BuiltInTools/dotnet-format/Commands/FormatCommandCommon.cs
index 20486f7baa0e..a801de15dfbf 100644
--- a/src/BuiltInTools/dotnet-format/Commands/FormatCommandCommon.cs
+++ b/src/BuiltInTools/dotnet-format/Commands/FormatCommandCommon.cs
@@ -15,7 +15,6 @@ internal static class FormatCommandCommon
internal const int UnhandledExceptionExitCode = 1;
internal const int CheckFailedExitCode = 2;
internal const int UnableToLocateMSBuildExitCode = 3;
- internal const int UnableToLocateDotNetCliExitCode = 4;
private static string[] VerbosityLevels => new[] { "q", "quiet", "m", "minimal", "n", "normal", "d", "detailed", "diag", "diagnostic" };
private static string[] SeverityLevels => new[] { "info", "warn", "error" };
@@ -102,14 +101,6 @@ internal static async Task FormatAsync(FormatOptions formatOptions, ILogger
var runtimeVersion = GetRuntimeVersion();
logger.LogDebug(Resources.The_dotnet_runtime_version_is_0, runtimeVersion);
- if (!TryGetDotNetCliVersion(out var dotnetVersion))
- {
- logger.LogError(Resources.Unable_to_locate_dotnet_CLI_Ensure_that_it_is_on_the_PATH);
- return UnableToLocateDotNetCliExitCode;
- }
-
- logger.LogTrace(Resources.The_dotnet_CLI_version_is_0, dotnetVersion);
-
if (!TryLoadMSBuild(out var msBuildPath))
{
logger.LogError(Resources.Unable_to_locate_MSBuild_Ensure_the_NET_SDK_was_installed_with_the_official_installer);
@@ -354,23 +345,6 @@ private static string EnsureTrailingSlash(string path)
?.InformationalVersion;
}
- internal static bool TryGetDotNetCliVersion([NotNullWhen(returnValue: true)] out string? dotnetVersion)
- {
- try
- {
- var processInfo = ProcessRunner.CreateProcess("dotnet", "--version", captureOutput: true, displayWindow: false);
- var versionResult = processInfo.Result.GetAwaiter().GetResult();
-
- dotnetVersion = versionResult.OutputLines[0].Trim();
- return true;
- }
- catch
- {
- dotnetVersion = null;
- return false;
- }
- }
-
internal static bool TryLoadMSBuild([NotNullWhen(returnValue: true)] out string? msBuildPath)
{
try
diff --git a/src/BuiltInTools/dotnet-format/Resources.resx b/src/BuiltInTools/dotnet-format/Resources.resx
index 999aa66a5a2d..ec627d90ce53 100644
--- a/src/BuiltInTools/dotnet-format/Resources.resx
+++ b/src/BuiltInTools/dotnet-format/Resources.resx
@@ -225,9 +225,6 @@
Standard input markers ('/dev/stdin', '-') can only be used either with `--include` or `--exclude`, but not both.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
-
The dotnet CLI version is '{0}'.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.cs.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.cs.xlf
index f86d04054ce8..f3a21d499b4d 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.cs.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.cs.xlf
@@ -377,11 +377,6 @@
Nepovedlo se najít MSBuild. Ujistěte se, že se sada .NET SDK nainstalovala pomocí oficiálního instalačního programu.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- Nepovedlo se najít .NET CLI. Ujistěte se, že se nachází v proměnné PATH.
-
-
Unable to organize imports for '{0}'. The document is too complex.
Nepovedlo se uspořádat importy pro {0}. Dokument je příliš složitý.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.de.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.de.xlf
index 677400e07673..40a49347e259 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.de.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.de.xlf
@@ -377,11 +377,6 @@
MSBuild wurde nicht gefunden. Stellen Sie sicher, dass das .NET SDK mit dem offiziellen Installationsprogramm installiert wurde.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- Die dotnet-CLI wurde nicht gefunden. Stellen Sie sicher, dass sie sich im Pfad befindet.
-
-
Unable to organize imports for '{0}'. The document is too complex.
Importe für "{0}" können nicht organisiert werden. Das Dokument ist zu komplex.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.es.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.es.xlf
index 79008550cfdd..775cdef7cdf3 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.es.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.es.xlf
@@ -377,11 +377,6 @@
No se encuentra MSBuild. Asegúrese de que el SDK de .NET se haya instalado con el instalador oficial.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- No se encuentra la CLI de dotnet. Asegúrese de que esté en la ruta de acceso (PATH).
-
-
Unable to organize imports for '{0}'. The document is too complex.
No se pueden organizar las importaciones para "{0}". El documento es demasiado complejo.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.fr.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.fr.xlf
index 7115ef2dd68a..c29da3ec3b44 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.fr.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.fr.xlf
@@ -377,11 +377,6 @@
Impossible de localiser MSBuild. Vérifiez que le SDK .NET a été installé avec le programme d'installation officiel.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- Impossible de localiser l'interface CLI dotnet. Vérifiez qu'elle est dans le chemin.
-
-
Unable to organize imports for '{0}'. The document is too complex.
Impossible d'organiser les importations pour '{0}'. Le document est trop complexe.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.it.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.it.xlf
index 3ffa3a39380f..2aeb29336f18 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.it.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.it.xlf
@@ -377,11 +377,6 @@
Non è possibile individuare MSBuild. Assicurarsi che .NET SDK sia stato installato con il programma di installazione ufficiale.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- Non è possibile individuare l'interfaccia della riga di comando di dotnet. Assicurarsi che sia indicata in PATH.
-
-
Unable to organize imports for '{0}'. The document is too complex.
Non è possibile organizzare le importazioni per '{0}'. Il documento è troppo complesso.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.ja.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.ja.xlf
index 11fa905930d7..4cf225955ceb 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.ja.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.ja.xlf
@@ -377,11 +377,6 @@
MSBuild が見つかりません。.NET SDK が正式なインストーラーでインストールされたことを確認してください。
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- dotnet CLI が見つかりません。PATH 上にあることを確認してください。
-
-
Unable to organize imports for '{0}'. The document is too complex.
'{0}' のインポートを整理できません。ドキュメントが複雑すぎます。
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.ko.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.ko.xlf
index e8d31acc2adb..5fe1286052e1 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.ko.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.ko.xlf
@@ -377,11 +377,6 @@
MSBuild를 찾을 수 없습니다. 공식 설치 관리자를 사용하여 .NET SDK를 설치했는지 확인하세요.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- dotnet CLI를 찾을 수 없습니다. PATH에 있는지 확인하세요.
-
-
Unable to organize imports for '{0}'. The document is too complex.
'{0}'에 대한 가져오기를 구성할 수 없습니다. 문서가 너무 복잡합니다.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.pl.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.pl.xlf
index 936d356f2ac0..37b8a4e1f484 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.pl.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.pl.xlf
@@ -377,11 +377,6 @@
Nie można zlokalizować programu MSBuild. Upewnij się, że zestaw .NET SDK został zainstalowany przy użyciu oficjalnego instalatora.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- Nie można zlokalizować wiersza polecenia dotnet. Upewnij się, że znajduje się on w ścieżce.
-
-
Unable to organize imports for '{0}'. The document is too complex.
Nie można zorganizować importów dla elementu „{0}”. Dokument jest zbyt złożony.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.pt-BR.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.pt-BR.xlf
index 8061e736ecbe..b57c3e3b44f4 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.pt-BR.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.pt-BR.xlf
@@ -377,11 +377,6 @@
Não é possível localizar o MSBuild. Verifique se o SDK do .NET foi instalado com o instalador oficial.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- Não é possível localizar a CLI do dotnet. Verifique se está no CAMINHO.
-
-
Unable to organize imports for '{0}'. The document is too complex.
Não é possível organizar importações para '{0}'. O documento é muito complexo.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.ru.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.ru.xlf
index 738cdc715ecd..876497b11b1e 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.ru.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.ru.xlf
@@ -377,11 +377,6 @@
Не удается найти MSBuild. Убедитесь, что пакет SDK для .NET был установлен с официальным установщиком.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- Не удалось найти CLI dotnet. Убедитесь, что путь к нему добавлен в переменную среды PATH.
-
-
Unable to organize imports for '{0}'. The document is too complex.
Не удается организовать импорты для "{0}". Слишком сложный документ.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.tr.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.tr.xlf
index a2e299a0719d..c20259208904 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.tr.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.tr.xlf
@@ -377,11 +377,6 @@
MSBuild bulunamıyor. .NET SDK'nın resmi yükleyici kullanılarak yüklendiğinden emin olun.
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- dotnet CLI bulunamıyor. dotnet CLI'nin PATH üzerinde olduğundan emin olun.
-
-
Unable to organize imports for '{0}'. The document is too complex.
'{0}' için içeri aktarmalar düzenlenemiyor. Belge çok karmaşık.
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.zh-Hans.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.zh-Hans.xlf
index 2159fb65a4d1..88482781e59c 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.zh-Hans.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.zh-Hans.xlf
@@ -377,11 +377,6 @@
无法找到 MSBuild。请确保 .NET SDK 是与官方安装程序一起安装的。
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- 找不到 dotnet CLI。请确保它在路径上。
-
-
Unable to organize imports for '{0}'. The document is too complex.
无法整理“{0}”的导入项。文档太复杂。
diff --git a/src/BuiltInTools/dotnet-format/xlf/Resources.zh-Hant.xlf b/src/BuiltInTools/dotnet-format/xlf/Resources.zh-Hant.xlf
index 68ef3223f390..10d7cfa16699 100644
--- a/src/BuiltInTools/dotnet-format/xlf/Resources.zh-Hant.xlf
+++ b/src/BuiltInTools/dotnet-format/xlf/Resources.zh-Hant.xlf
@@ -377,11 +377,6 @@
找不到 MSBuild。請確認已使用正式安裝程式安裝了 .NET SDK。
-
- Unable to locate dotnet CLI. Ensure that it is on the PATH.
- 找不到 dotnet CLI。請確認其位於 PATH 上。
-
-
Unable to organize imports for '{0}'. The document is too complex.
無法組織 '{0}' 的匯入。文件太複雜。
diff --git a/src/BuiltInTools/dotnet-watch.slnf b/src/BuiltInTools/dotnet-watch.slnf
index 5baaf69115c2..2484e98c2da7 100644
--- a/src/BuiltInTools/dotnet-watch.slnf
+++ b/src/BuiltInTools/dotnet-watch.slnf
@@ -7,12 +7,14 @@
"src\\BuiltInTools\\BrowserRefresh\\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj",
"src\\BuiltInTools\\DotNetDeltaApplier\\Microsoft.Extensions.DotNetDeltaApplier.csproj",
"src\\BuiltInTools\\DotNetWatchTasks\\DotNetWatchTasks.csproj",
+ "src\\BuiltInTools\\HotReloadAgent\\Microsoft.DotNet.HotReload.Agent.Package.csproj",
+ "src\\BuiltInTools\\HotReloadAgent\\Microsoft.DotNet.HotReload.Agent.shproj",
"src\\BuiltInTools\\dotnet-watch\\dotnet-watch.csproj",
"test\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj",
"test\\Microsoft.Extensions.DotNetDeltaApplier.Tests\\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj",
"test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj",
- "test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj",
- "test\\Microsoft.WebTools.AspireService.Tests\\Microsoft.WebTools.AspireService.Tests.csproj"
+ "test\\Microsoft.WebTools.AspireService.Tests\\Microsoft.WebTools.AspireService.Tests.csproj",
+ "test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj"
]
}
}
\ No newline at end of file
diff --git a/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs b/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs
index 753d6dd66cfb..d806dd11ba45 100644
--- a/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs
+++ b/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs
@@ -5,14 +5,10 @@
using System.Diagnostics;
using System.Globalization;
using System.Threading.Channels;
+using Aspire.Tools.Service;
using Microsoft.Build.Graph;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.DotNet.Watcher.Tools;
-using Microsoft.Extensions.Tools.Internal;
-using Microsoft.WebTools.AspireServer;
-using Microsoft.WebTools.AspireServer.Contracts;
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal class AspireServiceFactory : IRuntimeProcessLauncherFactory
{
@@ -34,7 +30,7 @@ private readonly struct Session(string dcpId, string sessionId, RunningProject r
private readonly ProjectLauncher _projectLauncher;
private readonly AspireServerService _service;
- private readonly IReadOnlyList<(string name, string value)> _buildProperties;
+ private readonly IReadOnlyList _buildArguments;
///
/// Lock to access:
@@ -47,10 +43,10 @@ private readonly struct Session(string dcpId, string sessionId, RunningProject r
private int _sessionIdDispenser;
private volatile bool _isDisposed;
- public SessionManager(ProjectLauncher projectLauncher, IReadOnlyList<(string name, string value)> buildProperties)
+ public SessionManager(ProjectLauncher projectLauncher, IReadOnlyList buildArguments)
{
_projectLauncher = projectLauncher;
- _buildProperties = buildProperties;
+ _buildArguments = buildArguments;
_service = new AspireServerService(
this,
@@ -104,11 +100,11 @@ async ValueTask IAspireServerEvents.StartProjectAsync(string dcpId, Proj
var projectOptions = GetProjectOptions(projectLaunchInfo);
var sessionId = Interlocked.Increment(ref _sessionIdDispenser).ToString(CultureInfo.InvariantCulture);
- await StartProjectAsync(dcpId, sessionId, projectOptions, build: false, isRestart: false, cancellationToken);
+ await StartProjectAsync(dcpId, sessionId, projectOptions, isRestart: false, cancellationToken);
return sessionId;
}
- public async ValueTask StartProjectAsync(string dcpId, string sessionId, ProjectOptions projectOptions, bool build, bool isRestart, CancellationToken cancellationToken)
+ public async ValueTask StartProjectAsync(string dcpId, string sessionId, ProjectOptions projectOptions, bool isRestart, CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(_isDisposed, this);
@@ -125,9 +121,8 @@ public async ValueTask StartProjectAsync(string dcpId, string se
var writeResult = outputChannel.Writer.TryWrite(line);
Debug.Assert(writeResult);
},
- restartOperation: (build, cancellationToken) =>
- StartProjectAsync(dcpId, sessionId, projectOptions, build, isRestart: true, cancellationToken),
- build: build,
+ restartOperation: cancellationToken =>
+ StartProjectAsync(dcpId, sessionId, projectOptions, isRestart: true, cancellationToken),
cancellationToken);
if (runningProject == null)
@@ -239,7 +234,7 @@ private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo)
IsRootProject = false,
ProjectPath = projectLaunchInfo.ProjectPath,
WorkingDirectory = _projectLauncher.EnvironmentOptions.WorkingDirectory, // TODO: Should DCP protocol specify?
- BuildProperties = _buildProperties, // TODO: Should DCP protocol specify?
+ BuildArguments = _buildArguments, // TODO: Should DCP protocol specify?
Command = "run",
CommandArguments = arguments,
LaunchEnvironmentVariables = projectLaunchInfo.Environment?.Select(kvp => (kvp.Key, kvp.Value)).ToArray() ?? [],
@@ -255,8 +250,8 @@ private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo)
public static readonly AspireServiceFactory Instance = new();
public const string AppHostProjectCapability = "Aspire";
- public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, IReadOnlyList<(string name, string value)> buildProperties)
+ public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, IReadOnlyList buildArguments)
=> projectNode.GetCapabilities().Contains(AppHostProjectCapability)
- ? new SessionManager(projectLauncher, buildProperties)
+ ? new SessionManager(projectLauncher, buildArguments)
: null;
}
diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserConnection.cs b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnection.cs
new file mode 100644
index 000000000000..b7824f636011
--- /dev/null
+++ b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnection.cs
@@ -0,0 +1,84 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+
+using System.Buffers;
+using System.Net.WebSockets;
+
+namespace Microsoft.DotNet.Watch;
+
+internal readonly struct BrowserConnection : IAsyncDisposable
+{
+ private static int s_lastId;
+
+ public WebSocket ClientSocket { get; }
+ public string? SharedSecret { get; }
+ public int Id { get; }
+ public IReporter Reporter { get; }
+
+ public BrowserConnection(WebSocket clientSocket, string? sharedSecret, IReporter reporter)
+ {
+ ClientSocket = clientSocket;
+ SharedSecret = sharedSecret;
+ Id = Interlocked.Increment(ref s_lastId);
+ Reporter = new BrowserSpecificReporter(Id, reporter);
+
+ Reporter.Verbose($"Connected to referesh server.");
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await ClientSocket.CloseOutputAsync(WebSocketCloseStatus.Empty, null, default);
+ ClientSocket.Dispose();
+
+ Reporter.Verbose($"Disconnected.");
+ }
+
+ internal async ValueTask TrySendMessageAsync(ReadOnlyMemory messageBytes, CancellationToken cancellationToken)
+ {
+ try
+ {
+ await ClientSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ Reporter.Verbose($"Failed to send message: {e.Message}");
+ return false;
+ }
+
+ return true;
+ }
+
+ internal async ValueTask TryReceiveMessageAsync(Action, IReporter> receiver, CancellationToken cancellationToken)
+ {
+ var writer = new ArrayBufferWriter(initialCapacity: 1024);
+
+ while (true)
+ {
+ ValueWebSocketReceiveResult result;
+ try
+ {
+ result = await ClientSocket.ReceiveAsync(writer.GetMemory(), cancellationToken);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ Reporter.Verbose($"Failed to receive response: {e.Message}");
+ return false;
+ }
+
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ return false;
+ }
+
+ writer.Advance(result.Count);
+ if (result.EndOfMessage)
+ {
+ break;
+ }
+ }
+
+ receiver(writer.WrittenSpan, Reporter);
+ return true;
+ }
+}
diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs
index 21d2f2cfce13..dc120099b3b7 100644
--- a/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs
+++ b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs
@@ -6,10 +6,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using Microsoft.Build.Graph;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal sealed partial class BrowserConnector(DotNetWatchContext context) : IAsyncDisposable
{
@@ -150,7 +148,7 @@ void handler(OutputLine line)
// Subsequent iterations (project has been rebuilt and relaunched).
// Use refresh server to reload the browser, if available.
context.Reporter.Verbose("Reloading browser.");
- _ = server.ReloadAsync(cancellationToken);
+ _ = server.SendReloadMessageAsync(cancellationToken);
}
}
}
@@ -223,9 +221,9 @@ private bool CanLaunchBrowser(DotNetWatchContext context, ProjectGraphNode proje
return false;
}
- if (projectOptions.Command != "run")
+ if (!CommandLineOptions.IsCodeExecutionCommand(projectOptions.Command))
{
- reporter.Verbose("Browser refresh is only supported for run commands.");
+ reporter.Verbose($"Command '{projectOptions.Command}' does not support browser refresh.");
return false;
}
diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs b/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs
index c3547bd5b146..0f7c737e5687 100644
--- a/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs
+++ b/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServer.cs
@@ -16,24 +16,23 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
///
/// Communicates with aspnetcore-browser-refresh.js loaded in the browser.
///
internal sealed class BrowserRefreshServer : IAsyncDisposable
{
- private readonly byte[] ReloadMessage = Encoding.UTF8.GetBytes("Reload");
- private readonly byte[] WaitMessage = Encoding.UTF8.GetBytes("Wait");
- private readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web);
- private readonly List<(WebSocket clientSocket, string? sharedSecret)> _clientSockets = new();
- private readonly RSA _rsa;
+ private static readonly ReadOnlyMemory s_reloadMessage = Encoding.UTF8.GetBytes("Reload");
+ private static readonly ReadOnlyMemory s_waitMessage = Encoding.UTF8.GetBytes("Wait");
+ private static readonly JsonSerializerOptions s_jsonSerializerOptions = new(JsonSerializerDefaults.Web);
+ private readonly List _activeConnections = [];
+ private readonly RSA _rsa;
private readonly IReporter _reporter;
private readonly TaskCompletionSource _terminateWebSocket;
- private readonly TaskCompletionSource _clientConnected;
+ private readonly TaskCompletionSource _browserConnected;
private readonly string? _environmentHostName;
// initialized by StartAsync
@@ -48,10 +47,32 @@ public BrowserRefreshServer(EnvironmentOptions options, IReporter reporter)
Options = options;
_reporter = reporter;
_terminateWebSocket = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- _clientConnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ _browserConnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_environmentHostName = EnvironmentVariables.AutoReloadWSHostName;
}
+ public async ValueTask DisposeAsync()
+ {
+ _rsa.Dispose();
+
+ BrowserConnection[] connectionsToDispose;
+ lock (_activeConnections)
+ {
+ connectionsToDispose = [.. _activeConnections];
+ _activeConnections.Clear();
+ }
+
+ foreach (var connection in connectionsToDispose)
+ {
+ _reporter.Verbose($"Disconnecting from browser #{connection.Id}.");
+ await connection.DisposeAsync();
+ }
+
+ _refreshServer?.Dispose();
+
+ _terminateWebSocket.TrySetResult();
+ }
+
public void SetEnvironmentVariables(EnvironmentVariablesBuilder environmentBuilder)
{
Debug.Assert(_refreshServer != null);
@@ -62,6 +83,12 @@ public void SetEnvironmentVariables(EnvironmentVariablesBuilder environmentBuild
environmentBuilder.DotNetStartupHookDirective.Add(Path.Combine(AppContext.BaseDirectory, "middleware", "Microsoft.AspNetCore.Watch.BrowserRefresh.dll"));
environmentBuilder.AspNetCoreHostingStartupAssembliesVariable.Add("Microsoft.AspNetCore.Watch.BrowserRefresh");
+
+ if (_reporter.IsVerbose)
+ {
+ // enable debug logging from middleware:
+ environmentBuilder.SetVariable("Logging__LogLevel__Microsoft.AspNetCore.Watch", "Debug");
+ }
}
public string GetServerKey()
@@ -73,7 +100,7 @@ public async ValueTask StartAsync(CancellationToken cancellationToken)
var hostName = _environmentHostName ?? "127.0.0.1";
- var supportsTLS = await SupportsTLS();
+ var supportsTLS = await SupportsTlsAsync();
_refreshServer = new HostBuilder()
.ConfigureWebHost(builder =>
@@ -91,7 +118,7 @@ public async ValueTask StartAsync(CancellationToken cancellationToken)
builder.Configure(app =>
{
app.UseWebSockets();
- app.Run(WebSocketRequest);
+ app.Run(WebSocketRequestAsync);
});
})
.Build();
@@ -129,7 +156,7 @@ private IEnumerable GetServerUrls(IHost server)
];
}
- private async Task WebSocketRequest(HttpContext context)
+ private async Task WebSocketRequestAsync(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
@@ -147,8 +174,14 @@ private async Task WebSocketRequest(HttpContext context)
}
var clientSocket = await context.WebSockets.AcceptWebSocketAsync(subProtocol);
- _clientSockets.Add((clientSocket, sharedSecret));
- _clientConnected.TrySetResult();
+ var connection = new BrowserConnection(clientSocket, sharedSecret, _reporter);
+
+ lock (_activeConnections)
+ {
+ _activeConnections.Add(connection);
+ }
+
+ _browserConnected.TrySetResult();
await _terminateWebSocket.Task;
}
@@ -157,7 +190,7 @@ private async Task WebSocketRequest(HttpContext context)
///
internal void EmulateClientConnected()
{
- _clientConnected.TrySetResult();
+ _browserConnected.TrySetResult();
}
public async Task WaitForClientConnectionAsync(CancellationToken cancellationToken)
@@ -186,7 +219,7 @@ public async Task WaitForClientConnectionAsync(CancellationToken cancellationTok
try
{
- await _clientConnected.Task.WaitAsync(cancellationToken);
+ await _browserConnected.Task.WaitAsync(cancellationToken);
}
finally
{
@@ -199,127 +232,99 @@ public async Task WaitForClientConnectionAsync(CancellationToken cancellationTok
}
}
- public ValueTask SendJsonSerlialized(TValue value, CancellationToken cancellationToken = default)
+ private IReadOnlyCollection GetOpenBrowserConnections()
{
- var jsonSerialized = JsonSerializer.SerializeToUtf8Bytes(value, _jsonSerializerOptions);
- return SendMessage(jsonSerialized, cancellationToken);
+ lock (_activeConnections)
+ {
+ return [.. _activeConnections.Where(b => b.ClientSocket.State == WebSocketState.Open)];
+ }
}
- public async ValueTask SendJsonWithSecret(Func valueFactory, CancellationToken cancellationToken = default)
+ private async ValueTask DisposeClosedBrowserConnectionsAsync()
{
- try
- {
- bool messageSent = false;
+ List? lazyConnectionsToDispose = null;
- for (var i = 0; i < _clientSockets.Count; i++)
+ lock (_activeConnections)
+ {
+ var j = 0;
+ for (var i = 0; i < _activeConnections.Count; i++)
{
- var (clientSocket, secret) = _clientSockets[i];
- if (clientSocket.State is not WebSocketState.Open)
+ var connection = _activeConnections[i];
+ if (connection.ClientSocket.State == WebSocketState.Open)
{
- continue;
+ _activeConnections[j++] = connection;
+ }
+ else
+ {
+ lazyConnectionsToDispose ??= [];
+ lazyConnectionsToDispose.Add(connection);
}
-
- var value = valueFactory(secret);
- var messageBytes = JsonSerializer.SerializeToUtf8Bytes(value, _jsonSerializerOptions);
-
- await clientSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken);
- messageSent = true;
}
- _reporter.Verbose(messageSent ? "Browser message sent." : "Unable to send message to browser, no socket is open.");
- }
- catch (TaskCanceledException)
- {
- _reporter.Verbose("WebSocket connection has been terminated.");
+ _activeConnections.RemoveRange(j, _activeConnections.Count - j);
}
- catch (Exception ex)
- {
- _reporter.Verbose($"Refresh server error: {ex}");
- }
- }
- public async ValueTask SendMessage(ReadOnlyMemory messageBytes, CancellationToken cancellationToken = default)
- {
- try
+ if (lazyConnectionsToDispose != null)
{
- bool messageSent = false;
-
- for (var i = 0; i < _clientSockets.Count; i++)
+ foreach (var connection in lazyConnectionsToDispose)
{
- var (clientSocket, _) = _clientSockets[i];
- if (clientSocket.State is not WebSocketState.Open)
- {
- continue;
- }
-
- await clientSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken);
- messageSent = true;
+ await connection.DisposeAsync();
}
-
- _reporter.Verbose(messageSent ? "Browser message sent." : "Unable to send message to browser, no socket is open.");
- }
- catch (TaskCanceledException)
- {
- _reporter.Verbose("WebSocket connection has been terminated.");
- }
- catch (Exception ex)
- {
- _reporter.Verbose($"Refresh server error: {ex}");
}
}
- public async ValueTask DisposeAsync()
- {
- _rsa.Dispose();
+ public static ReadOnlyMemory SerializeJson(TValue value)
+ => JsonSerializer.SerializeToUtf8Bytes(value, s_jsonSerializerOptions);
- for (var i = 0; i < _clientSockets.Count; i++)
- {
- var (clientSocket, _) = _clientSockets[i];
- await clientSocket.CloseOutputAsync(WebSocketCloseStatus.Empty, null, default);
- clientSocket.Dispose();
- }
+ public static TValue DeserializeJson(ReadOnlySpan value)
+ => JsonSerializer.Deserialize(value, s_jsonSerializerOptions) ?? throw new InvalidDataException("Unexpected null object");
- _refreshServer?.Dispose();
+ public ValueTask SendJsonMessageAsync(TValue value, CancellationToken cancellationToken)
+ => SendAsync(SerializeJson(value), cancellationToken);
- _terminateWebSocket.TrySetResult();
- }
+ public ValueTask SendReloadMessageAsync(CancellationToken cancellationToken)
+ => SendAsync(s_reloadMessage, cancellationToken);
+
+ public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken)
+ => SendAsync(s_waitMessage, cancellationToken);
- public async ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken)
+ public ValueTask SendAsync(ReadOnlyMemory messageBytes, CancellationToken cancellationToken)
+ => SendAndReceiveAsync(request: _ => messageBytes, response: null, cancellationToken);
+
+ public async ValueTask SendAndReceiveAsync(
+ Func request,
+ Action, IReporter>? response,
+ CancellationToken cancellationToken)
{
- for (int i = 0; i < _clientSockets.Count; i++)
+ var responded = false;
+
+ foreach (var connection in GetOpenBrowserConnections())
{
- var (clientSocket, _) = _clientSockets[i];
+ var requestValue = request(connection.SharedSecret);
+ var requestBytes = requestValue is ReadOnlyMemory bytes ? bytes : SerializeJson(requestValue);
- if (clientSocket.State != WebSocketState.Open)
+ if (!await connection.TrySendMessageAsync(requestBytes, cancellationToken))
{
continue;
}
- try
+ if (response != null && !await connection.TryReceiveMessageAsync(response, cancellationToken))
{
- var result = await clientSocket.ReceiveAsync(buffer, cancellationToken);
+ continue;
+ }
- if (result.MessageType == WebSocketMessageType.Close)
- {
- continue;
- }
+ responded = true;
+ }
- return result;
- }
- catch (Exception ex)
- {
- _reporter.Verbose($"Refresh server error: {ex}");
- }
+ if (!responded)
+ {
+ _reporter.Verbose($"Failed to receive response from a connected browser.");
}
- return default;
+ await DisposeClosedBrowserConnectionsAsync();
}
- public ValueTask ReloadAsync(CancellationToken cancellationToken) => SendMessage(ReloadMessage, cancellationToken);
-
- public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken) => SendMessage(WaitMessage, cancellationToken);
-
- private async Task SupportsTLS()
+ private async Task SupportsTlsAsync()
{
try
{
@@ -334,18 +339,18 @@ private async Task SupportsTLS()
}
public ValueTask RefreshBrowserAsync(CancellationToken cancellationToken)
- => SendJsonSerlialized(new AspNetCoreHotReloadApplied(), cancellationToken);
+ => SendJsonMessageAsync(new AspNetCoreHotReloadApplied(), cancellationToken);
public ValueTask ReportCompilationErrorsInBrowserAsync(ImmutableArray compilationErrors, CancellationToken cancellationToken)
{
_reporter.Verbose($"Updating diagnostics in the browser.");
if (compilationErrors.IsEmpty)
{
- return SendJsonSerlialized(new AspNetCoreHotReloadApplied(), cancellationToken);
+ return SendJsonMessageAsync(new AspNetCoreHotReloadApplied(), cancellationToken);
}
else
{
- return SendJsonSerlialized(new HotReloadDiagnostics { Diagnostics = compilationErrors }, cancellationToken);
+ return SendJsonMessageAsync(new HotReloadDiagnostics { Diagnostics = compilationErrors }, cancellationToken);
}
}
diff --git a/src/BuiltInTools/dotnet-watch/CommandLineOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLineOptions.cs
index e49c07ba48e2..7441aaaa0496 100644
--- a/src/BuiltInTools/dotnet-watch/CommandLineOptions.cs
+++ b/src/BuiltInTools/dotnet-watch/CommandLineOptions.cs
@@ -7,53 +7,28 @@
using System.CommandLine.Parsing;
using System.Data;
using System.Diagnostics;
-using Microsoft.DotNet.Watcher.Tools;
-using Microsoft.Extensions.Tools.Internal;
+using Microsoft.DotNet.Cli;
+using Microsoft.DotNet.Tools.Run;
+using NuGet.Common;
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal sealed class CommandLineOptions
{
public const string DefaultCommand = "run";
- private static readonly ImmutableArray s_knownCommands =
- [
- "add",
- "build",
- "build-server",
- "clean",
- "format",
- "help",
- "list",
- "msbuild",
- "new",
- "nuget",
- "pack",
- "publish",
- "remove",
- "restore",
- "run",
- "sdk",
- "sln",
- "store",
- "test",
- "tool",
- "vstest",
- "workload"
- ];
-
public bool List { get; init; }
required public GlobalOptions GlobalOptions { get; init; }
public string? ProjectPath { get; init; }
public string? TargetFramework { get; init; }
- public IReadOnlyList<(string name, string value)>? BuildProperties { get; init; }
public bool NoLaunchProfile { get; init; }
public string? LaunchProfileName { get; init; }
public string? ExplicitCommand { get; init; }
public required IReadOnlyList CommandArguments { get; init; }
+ public required IReadOnlyList BuildArguments { get; init; }
public string Command => ExplicitCommand ?? DefaultCommand;
@@ -90,12 +65,10 @@ internal sealed class CommandLineOptions
var longProjectOption = new CliOption("--project") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false };
var launchProfileOption = new CliOption("--launch-profile", "-lp") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false };
var noLaunchProfileOption = new CliOption("--no-launch-profile") { Hidden = true };
- var targetFrameworkOption = new CliOption("--framework", "-f") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false };
- var propertyOption = new CliOption("--property") { Hidden = true, Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false };
var rootCommand = new CliRootCommand(Resources.Help)
{
- Directives = { new EnvironmentVariablesDirective() }
+ Directives = { new EnvironmentVariablesDirective() },
};
foreach (var watchOption in watchOptions)
@@ -107,8 +80,6 @@ internal sealed class CommandLineOptions
rootCommand.Options.Add(shortProjectOption);
rootCommand.Options.Add(launchProfileOption);
rootCommand.Options.Add(noLaunchProfileOption);
- rootCommand.Options.Add(targetFrameworkOption);
- rootCommand.Options.Add(propertyOption);
// We process all tokens that do not match any of the above options
// to find the subcommand (the first unmatched token preceding "--")
@@ -125,16 +96,33 @@ internal sealed class CommandLineOptions
{
Output = output,
Error = output,
+
+ // To match dotnet command line parsing (see https://github.com/dotnet/sdk/blob/4712b35b94f2ad672e69ec35097cf86fc16c2e5e/src/Cli/dotnet/Parser.cs#L169):
+ EnablePosixBundling = false,
};
+ // parse without forwarded options first:
var parseResult = rootCommand.Parse(args, cliConfig);
- if (parseResult.Errors.Any())
+ if (ReportErrors(parseResult, reporter))
{
- foreach (var error in parseResult.Errors)
- {
- reporter.Error(error.Message);
- }
+ errorCode = 1;
+ return null;
+ }
+
+ // determine subcommand:
+ var explicitCommand = TryGetSubcommand(parseResult);
+ var command = explicitCommand ?? RunCommandParser.GetCommand();
+ var buildOptions = command.Options.Where(o => o is IForwardedOption);
+
+ foreach (var buildOption in buildOptions)
+ {
+ rootCommand.Options.Add(buildOption);
+ }
+ // reparse with forwarded options:
+ parseResult = rootCommand.Parse(args, cliConfig);
+ if (ReportErrors(parseResult, reporter))
+ {
errorCode = 1;
return null;
}
@@ -158,6 +146,12 @@ internal sealed class CommandLineOptions
}
}
+ var commandArguments = GetCommandArguments(parseResult, watchOptions, explicitCommand);
+
+ // We assume that forwarded options, if any, are intended for dotnet build.
+ var buildArguments = buildOptions.Select(option => ((IForwardedOption)option).GetForwardingFunction()(parseResult)).SelectMany(args => args).ToArray();
+ var targetFrameworkOption = (CliOption?)buildOptions.SingleOrDefault(option => option.Name == "--framework");
+
return new()
{
List = parseResult.GetValue(listOption),
@@ -169,32 +163,21 @@ internal sealed class CommandLineOptions
Verbose = parseResult.GetValue(verboseOption),
},
- CommandArguments = GetCommandArguments(parseResult, watchOptions, out var explicitCommand),
- ExplicitCommand = explicitCommand,
+ CommandArguments = commandArguments,
+ ExplicitCommand = explicitCommand?.Name,
ProjectPath = projectValue,
LaunchProfileName = parseResult.GetValue(launchProfileOption),
NoLaunchProfile = parseResult.GetValue(noLaunchProfileOption),
- TargetFramework = parseResult.GetValue(targetFrameworkOption),
- BuildProperties = ParseBuildProperties(parseResult.GetValue(propertyOption) ?? []).ToArray(),
+ BuildArguments = buildArguments,
+ TargetFramework = targetFrameworkOption != null ? parseResult.GetValue(targetFrameworkOption) : null,
};
-
- // Parses name=value pairs passed to --property. Skips invalid input.
- // We don't report error here as it will be reported by dotnet run.
- static IEnumerable<(string key, string value)> ParseBuildProperties(string[] properties)
- => from property in properties
- let index = property.IndexOf('=')
- where index >= 0
- let name = property[..index].Trim()
- let value = property[(index + 1)..]
- where name is not []
- select (name, value);
}
private static IReadOnlyList GetCommandArguments(
ParseResult parseResult,
IReadOnlyList watchOptions,
- out string? explicitCommand)
+ CliCommand? explicitCommand)
{
var arguments = new List();
@@ -211,6 +194,16 @@ private static IReadOnlyList GetCommandArguments(
{
arguments.Add(optionResult.IdentifierToken.Value);
}
+ else if (optionResult.Option.Name == "--property")
+ {
+ foreach (var token in optionResult.Tokens)
+ {
+ // While dotnet-build allows "/p Name=Value", dotnet-msbuild does not.
+ // Any command that forwards args to dotnet-msbuild will fail if we don't use colon.
+ // See https://github.com/dotnet/sdk/issues/44655.
+ arguments.Add($"{optionResult.IdentifierToken.Value}:{token.Value}");
+ }
+ }
else
{
foreach (var token in optionResult.Tokens)
@@ -228,33 +221,66 @@ private static IReadOnlyList GetCommandArguments(
var dashDashIndex = IndexOf(parseResult.Tokens, t => t.Value == "--");
var unmatchedTokensBeforeDashDash = parseResult.UnmatchedTokens.Count - (dashDashIndex >= 0 ? parseResult.Tokens.Count - dashDashIndex - 1 : 0);
- explicitCommand = null;
+ var seenCommand = false;
var dashDashInserted = false;
for (int i = 0; i < parseResult.UnmatchedTokens.Count; i++)
{
var token = parseResult.UnmatchedTokens[i];
- // command token can't follow "--"
- if (i < unmatchedTokensBeforeDashDash && explicitCommand == null && s_knownCommands.Contains(token))
+ if (i < unmatchedTokensBeforeDashDash && !seenCommand && token == explicitCommand?.Name)
{
- explicitCommand = token;
+ seenCommand = true;
+ continue;
}
- else
- {
- if (!dashDashInserted && i >= unmatchedTokensBeforeDashDash)
- {
- arguments.Add("--");
- dashDashInserted = true;
- }
- arguments.Add(token);
+ if (!dashDashInserted && i >= unmatchedTokensBeforeDashDash)
+ {
+ arguments.Add("--");
+ dashDashInserted = true;
}
+
+ arguments.Add(token);
}
return arguments;
}
+ private static CliCommand? TryGetSubcommand(ParseResult parseResult)
+ {
+ // Assuming that all tokens after "--" are unmatched:
+ var dashDashIndex = IndexOf(parseResult.Tokens, t => t.Value == "--");
+ var unmatchedTokensBeforeDashDash = parseResult.UnmatchedTokens.Count - (dashDashIndex >= 0 ? parseResult.Tokens.Count - dashDashIndex - 1 : 0);
+
+ var knownCommandsByName = Parser.Subcommands.ToDictionary(keySelector: c => c.Name, elementSelector: c => c);
+
+ for (int i = 0; i < unmatchedTokensBeforeDashDash; i++)
+ {
+ // command token can't follow "--"
+ if (knownCommandsByName.TryGetValue(parseResult.UnmatchedTokens[i], out var explicitCommand))
+ {
+ return explicitCommand;
+ }
+ }
+
+ return null;
+ }
+
+ private static bool ReportErrors(ParseResult parseResult, IReporter reporter)
+ {
+ if (parseResult.Errors.Any())
+ {
+ foreach (var error in parseResult.Errors)
+ {
+ reporter.Error(error.Message);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
private static int IndexOf(IReadOnlyList list, Func predicate)
{
for (var i = 0; i < list.Count; i++)
@@ -274,12 +300,30 @@ public ProjectOptions GetProjectOptions(string projectPath, string workingDirect
IsRootProject = true,
ProjectPath = projectPath,
WorkingDirectory = workingDirectory,
- BuildProperties = BuildProperties ?? [],
Command = Command,
CommandArguments = CommandArguments,
LaunchEnvironmentVariables = [],
LaunchProfileName = LaunchProfileName,
NoLaunchProfile = NoLaunchProfile,
+ BuildArguments = BuildArguments,
TargetFramework = TargetFramework,
};
+
+ // Parses name=value pairs passed to --property. Skips invalid input.
+ public static IEnumerable<(string key, string value)> ParseBuildProperties(IEnumerable arguments)
+ => from argument in arguments
+ let colon = argument.IndexOf(':')
+ where colon >= 0 && argument[0..colon] is "--property" or "-property" or "/property" or "/p" or "-p" or "--p"
+ let eq = argument.IndexOf('=', colon)
+ where eq >= 0
+ let name = argument[(colon + 1)..eq].Trim()
+ let value = argument[(eq + 1)..]
+ where name is not []
+ select (name, value);
+
+ ///
+ /// Returns true if the command executes the code of the target project.
+ ///
+ public static bool IsCodeExecutionCommand(string commandName)
+ => commandName is "run" or "test";
}
diff --git a/src/BuiltInTools/dotnet-watch/DotNetWatchContext.cs b/src/BuiltInTools/dotnet-watch/DotNetWatchContext.cs
index 95fd1ccb98ed..5b875b1181e3 100644
--- a/src/BuiltInTools/dotnet-watch/DotNetWatchContext.cs
+++ b/src/BuiltInTools/dotnet-watch/DotNetWatchContext.cs
@@ -1,9 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal sealed class DotNetWatchContext
{
diff --git a/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
index 9c4ab9f433c7..321ad2924813 100644
--- a/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
@@ -4,11 +4,8 @@
using System.Diagnostics;
using System.Globalization;
using Microsoft.Build.Graph;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.DotNet.Watcher.Tools;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher
+namespace Microsoft.DotNet.Watch
{
internal sealed class DotNetWatcher(DotNetWatchContext context, MSBuildFileSetFactory fileSetFactory) : Watcher(context, fileSetFactory)
{
@@ -80,7 +77,9 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
using var currentRunCancellationSource = new CancellationTokenSource();
using var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellationToken, currentRunCancellationSource.Token);
- using var fileSetWatcher = new FileWatcher(evaluationResult.Files, Context.Reporter);
+ using var fileSetWatcher = new FileWatcher(Context.Reporter);
+
+ fileSetWatcher.WatchContainingDirectories(evaluationResult.Files.Keys);
var processTask = ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: true, launchResult: null, combinedCancellationSource.Token);
@@ -89,7 +88,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
while (true)
{
- fileSetTask = fileSetWatcher.GetChangedFileAsync(startedWatching: null, combinedCancellationSource.Token);
+ fileSetTask = fileSetWatcher.WaitForFileChangeAsync(evaluationResult.Files, startedWatching: null, combinedCancellationSource.Token);
finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task);
if (staticFileHandler != null && finishedTask == fileSetTask && fileSetTask.Result.HasValue)
@@ -119,9 +118,11 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
{
// Process exited. Redo evalulation
buildEvaluator.RequiresRevaluation = true;
+
// Now wait for a file to change before restarting process
- changedFile = await fileSetWatcher.GetChangedFileAsync(
- () => Context.Reporter.Report(MessageDescriptor.WaitingForFileChangeBeforeRestarting),
+ changedFile = await fileSetWatcher.WaitForFileChangeAsync(
+ evaluationResult.Files,
+ startedWatching: () => Context.Reporter.Report(MessageDescriptor.WaitingForFileChangeBeforeRestarting),
shutdownCancellationToken);
}
else
diff --git a/src/BuiltInTools/dotnet-watch/EnvironmentOptions.cs b/src/BuiltInTools/dotnet-watch/EnvironmentOptions.cs
index f2fb13a8c123..43d17a2312cb 100644
--- a/src/BuiltInTools/dotnet-watch/EnvironmentOptions.cs
+++ b/src/BuiltInTools/dotnet-watch/EnvironmentOptions.cs
@@ -2,9 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher
+namespace Microsoft.DotNet.Watch
{
[Flags]
internal enum TestFlags
@@ -17,6 +16,12 @@ internal enum TestFlags
/// Elevates the severity of from .
///
ElevateWaitingForChangesMessageSeverity = 1 << 2,
+
+ ///
+ /// Instead of using to watch for Ctrl+C, Ctlr+R, and other keys, read from standard input.
+ /// This allows tests to trigger key based events.
+ ///
+ ReadKeyFromStdin = 1 << 3,
}
internal sealed record EnvironmentOptions(
@@ -28,7 +33,8 @@ internal sealed record EnvironmentOptions(
bool SuppressLaunchBrowser = false,
bool SuppressBrowserRefresh = false,
bool SuppressEmojis = false,
- TestFlags TestFlags = TestFlags.None)
+ TestFlags TestFlags = TestFlags.None,
+ string TestOutput = "")
{
public static EnvironmentOptions FromEnvironment() => new
(
@@ -40,7 +46,8 @@ internal sealed record EnvironmentOptions(
SuppressLaunchBrowser: EnvironmentVariables.SuppressLaunchBrowser,
SuppressBrowserRefresh: EnvironmentVariables.SuppressBrowserRefresh,
SuppressEmojis: EnvironmentVariables.SuppressEmojis,
- TestFlags: EnvironmentVariables.TestFlags
+ TestFlags: EnvironmentVariables.TestFlags,
+ TestOutput: EnvironmentVariables.TestOutputDir
);
public bool RunningAsTest { get => (TestFlags & TestFlags.RunningAsTest) != TestFlags.None; }
diff --git a/src/BuiltInTools/dotnet-watch/EnvironmentVariables.cs b/src/BuiltInTools/dotnet-watch/EnvironmentVariables.cs
index f0ee60534903..c31413b1d3f2 100644
--- a/src/BuiltInTools/dotnet-watch/EnvironmentVariables.cs
+++ b/src/BuiltInTools/dotnet-watch/EnvironmentVariables.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal static partial class EnvironmentVariables
{
@@ -36,6 +36,7 @@ public static partial class Names
public static bool SuppressBrowserRefresh => ReadBool("DOTNET_WATCH_SUPPRESS_BROWSER_REFRESH");
public static TestFlags TestFlags => Environment.GetEnvironmentVariable("__DOTNET_WATCH_TEST_FLAGS") is { } value ? Enum.Parse(value) : TestFlags.None;
+ public static string TestOutputDir => Environment.GetEnvironmentVariable("__DOTNET_WATCH_TEST_OUTPUT_DIR") ?? "";
public static string? AutoReloadWSHostName => Environment.GetEnvironmentVariable("DOTNET_WATCH_AUTO_RELOAD_WS_HOSTNAME");
public static string? BrowserPath => Environment.GetEnvironmentVariable("DOTNET_WATCH_BROWSER_PATH");
diff --git a/src/BuiltInTools/dotnet-watch/EnvironmentVariablesBuilder.cs b/src/BuiltInTools/dotnet-watch/EnvironmentVariablesBuilder.cs
index 37f17e94c315..aec5634a4acf 100644
--- a/src/BuiltInTools/dotnet-watch/EnvironmentVariablesBuilder.cs
+++ b/src/BuiltInTools/dotnet-watch/EnvironmentVariablesBuilder.cs
@@ -3,7 +3,7 @@
using System.Diagnostics;
-namespace Microsoft.DotNet.Watcher
+namespace Microsoft.DotNet.Watch
{
internal sealed class EnvironmentVariablesBuilder
{
diff --git a/src/BuiltInTools/dotnet-watch/EnvironmentVariables_StartupHook.cs b/src/BuiltInTools/dotnet-watch/EnvironmentVariables_StartupHook.cs
index 9534a3694695..6a9191c1dab7 100644
--- a/src/BuiltInTools/dotnet-watch/EnvironmentVariables_StartupHook.cs
+++ b/src/BuiltInTools/dotnet-watch/EnvironmentVariables_StartupHook.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal static partial class EnvironmentVariables
{
diff --git a/src/BuiltInTools/dotnet-watch/EvaluationResult.cs b/src/BuiltInTools/dotnet-watch/EvaluationResult.cs
index 48ca7f6902af..b551c518acf5 100644
--- a/src/BuiltInTools/dotnet-watch/EvaluationResult.cs
+++ b/src/BuiltInTools/dotnet-watch/EvaluationResult.cs
@@ -3,7 +3,7 @@
using Microsoft.Build.Graph;
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal sealed class EvaluationResult(IReadOnlyDictionary files, ProjectGraph? projectGraph)
{
diff --git a/src/BuiltInTools/dotnet-watch/FileItem.cs b/src/BuiltInTools/dotnet-watch/FileItem.cs
index 7fc91cd6fa1c..8e0a028d4d09 100644
--- a/src/BuiltInTools/dotnet-watch/FileItem.cs
+++ b/src/BuiltInTools/dotnet-watch/FileItem.cs
@@ -1,18 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.DotNet.Watcher.Internal;
-namespace Microsoft.DotNet.Watcher
+namespace Microsoft.DotNet.Watch
{
internal readonly record struct FileItem
{
- public string FilePath { get; init; }
+ public required string FilePath { get; init; }
///
/// List of all projects that contain this file (does not contain duplicates).
+ /// Empty if is and the
+ /// item has not been assigned to a project yet.
///
- public List ContainingProjectPaths { get; init; }
+ public required List ContainingProjectPaths { get; init; }
public string? StaticWebAssetPath { get; init; }
diff --git a/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs b/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs
index 3221f60278f2..ae32359c539f 100644
--- a/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs
+++ b/src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs
@@ -3,9 +3,8 @@
using System.Diagnostics;
-using Microsoft.DotNet.Watcher.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal class BuildEvaluator(DotNetWatchContext context, MSBuildFileSetFactory rootProjectFileSetFactory)
{
@@ -30,7 +29,7 @@ public IReadOnlyList GetProcessArguments(int iteration)
{
if (!context.EnvironmentOptions.SuppressMSBuildIncrementalism &&
iteration > 0 &&
- context.RootProjectOptions.Command is "run" or "test")
+ CommandLineOptions.IsCodeExecutionCommand(context.RootProjectOptions.Command))
{
if (RequiresRevaluation)
{
@@ -84,8 +83,11 @@ private async ValueTask CreateEvaluationResult(CancellationTok
return result;
}
- context.Reporter.Warn("Fix the error to continue or press Ctrl+C to exit.");
- await FileWatcher.WaitForFileChangeAsync(rootProjectFileSetFactory.RootProjectFile, context.Reporter, cancellationToken);
+ await FileWatcher.WaitForFileChangeAsync(
+ rootProjectFileSetFactory.RootProjectFile,
+ context.Reporter,
+ startedWatching: () => context.Reporter.Report(MessageDescriptor.FixBuildError),
+ cancellationToken);
}
}
diff --git a/src/BuiltInTools/dotnet-watch/GlobalOptions.cs b/src/BuiltInTools/dotnet-watch/GlobalOptions.cs
index 090da3fc0c67..9012920b5d2e 100644
--- a/src/BuiltInTools/dotnet-watch/GlobalOptions.cs
+++ b/src/BuiltInTools/dotnet-watch/GlobalOptions.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal sealed class GlobalOptions
{
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs
index 1b9342ed8cf6..ef22b5f8ffc5 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyDeltaApplier.cs
@@ -1,25 +1,35 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-
using System.Buffers;
using System.Collections.Immutable;
-using System.Diagnostics;
-using System.Net.WebSockets;
+using Microsoft.Build.Graph;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
-using Microsoft.Extensions.Tools.Internal;
+using Microsoft.DotNet.HotReload;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
- internal sealed class BlazorWebAssemblyDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, Version? targetFrameworkVersion) : SingleProcessDeltaApplier(reporter)
+ internal sealed class BlazorWebAssemblyDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, ProjectGraphNode project) : SingleProcessDeltaApplier(reporter)
{
- private const string DefaultCapabilities60 = "Baseline";
- private const string DefaultCapabilities70 = "Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes";
- private const string DefaultCapabilities80 = "Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType";
+ private static readonly ImmutableArray s_defaultCapabilities60 =
+ ["Baseline"];
+
+ private static readonly ImmutableArray s_defaultCapabilities70 =
+ ["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "NewTypeDefinition", "ChangeCustomAttributes"];
+
+ private static readonly ImmutableArray s_defaultCapabilities80 =
+ ["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "NewTypeDefinition", "ChangeCustomAttributes",
+ "AddInstanceFieldToExistingType", "GenericAddMethodToExistingType", "GenericUpdateMethod", "UpdateParameters", "GenericAddFieldToExistingType"];
+
+ private static readonly ImmutableArray s_defaultCapabilities90 =
+ s_defaultCapabilities80;
- private ImmutableArray _cachedCapabilities;
- private readonly SemaphoreSlim _capabilityRetrievalSemaphore = new(initialCount: 1);
- private int _sequenceId;
+ private int _updateId;
+
+ public override void Dispose()
+ {
+ // Do nothing.
+ }
public override void CreateConnection(string namedPipeName, CancellationToken cancellationToken)
{
@@ -30,104 +40,31 @@ public override async Task WaitForProcessRunningAsync(CancellationToken cancella
// Alternatively, we could inject agent into blazor-devserver.dll and establish a connection on the named pipe.
=> await browserRefreshServer.WaitForClientConnectionAsync(cancellationToken);
- public override async Task> GetApplyUpdateCapabilitiesAsync(CancellationToken cancellationToken)
+ public override Task> GetApplyUpdateCapabilitiesAsync(CancellationToken cancellationToken)
{
- var cachedCapabilities = _cachedCapabilities;
- if (!cachedCapabilities.IsDefault)
- {
- return cachedCapabilities;
- }
-
- await _capabilityRetrievalSemaphore.WaitAsync(cancellationToken);
- try
- {
- if (_cachedCapabilities.IsDefault)
- {
- _cachedCapabilities = await RetrieveAsync(cancellationToken);
- }
- }
- finally
- {
- _capabilityRetrievalSemaphore.Release();
- }
-
- return _cachedCapabilities;
+ var capabilities = project.GetWebAssemblyCapabilities();
- async Task> RetrieveAsync(CancellationToken cancellationToken)
+ if (capabilities.IsEmpty)
{
- var buffer = ArrayPool.Shared.Rent(32 * 1024);
-
- try
- {
- Reporter.Verbose("Connecting to the browser.");
-
- await browserRefreshServer.WaitForClientConnectionAsync(cancellationToken);
-
- string capabilities;
- if (browserRefreshServer.Options.TestFlags.HasFlag(TestFlags.MockBrowser))
- {
- // When testing return default capabilities without connecting to an actual browser.
- capabilities = GetDefaultCapabilities(targetFrameworkVersion);
- }
- else
- {
- await browserRefreshServer.SendJsonSerlialized(default(BlazorRequestApplyUpdateCapabilities), cancellationToken);
-
- // We'll query the browser and ask it send capabilities.
- var response = await browserRefreshServer.ReceiveAsync(buffer, cancellationToken);
- if (!response.HasValue || !response.Value.EndOfMessage || response.Value.MessageType != WebSocketMessageType.Text)
- {
- throw new ApplicationException("Unable to connect to the browser refresh server.");
- }
-
- capabilities = Encoding.UTF8.GetString(buffer.AsSpan(0, response.Value.Count));
-
- var shouldFallBackToDefaultCapabilities = false;
-
- // error while fetching capabilities from WASM:
- if (capabilities.StartsWith('!'))
- {
- Reporter.Verbose($"Exception while reading WASM runtime capabilities: {capabilities[1..]}");
- shouldFallBackToDefaultCapabilities = true;
- }
- else if (capabilities.Length == 0)
- {
- Reporter.Verbose($"Unable to read WASM runtime capabilities");
- shouldFallBackToDefaultCapabilities = true;
- }
-
- if (shouldFallBackToDefaultCapabilities)
- {
- capabilities = GetDefaultCapabilities(targetFrameworkVersion);
- Reporter.Verbose($"Falling back to default WASM capabilities: '{capabilities}'");
- }
- }
+ var targetFramework = project.GetTargetFrameworkVersion();
- // Capabilities are expressed a space-separated string.
- // e.g. https://github.com/dotnet/runtime/blob/14343bdc281102bf6fffa1ecdd920221d46761bc/src/coreclr/System.Private.CoreLib/src/System/Reflection/Metadata/AssemblyExtensions.cs#L87
- return capabilities.Split(' ').ToImmutableArray();
- }
- catch (Exception e) when (!cancellationToken.IsCancellationRequested)
- {
- Reporter.Error($"Failed to read capabilities: {e.Message}");
+ Reporter.Verbose($"Using capabilities based on target framework: '{targetFramework}'.");
- // Do not attempt to retrieve capabilities again if it fails once, unless the operation is canceled.
- return [];
- }
- finally
+ capabilities = targetFramework?.Major switch
{
- ArrayPool.Shared.Return(buffer);
- }
+ 9 => s_defaultCapabilities90,
+ 8 => s_defaultCapabilities80,
+ 7 => s_defaultCapabilities70,
+ 6 => s_defaultCapabilities60,
+ _ => [],
+ };
+ }
+ else
+ {
+ Reporter.Verbose($"Project specifies capabilities.");
}
- static string GetDefaultCapabilities(Version? targetFrameworkVersion)
- => targetFrameworkVersion?.Major switch
- {
- >= 8 => DefaultCapabilities80,
- >= 7 => DefaultCapabilities70,
- >= 6 => DefaultCapabilities60,
- _ => string.Empty,
- };
+ return Task.FromResult(capabilities);
}
public override async Task Apply(ImmutableArray updates, CancellationToken cancellationToken)
@@ -144,91 +81,89 @@ public override async Task Apply(ImmutableArray new UpdatePayload
- {
- SharedSecret = sharedSecret,
- Deltas = updates.Select(update => new UpdateDelta
- {
- SequenceId = _sequenceId++,
- ModuleId = update.ModuleId,
- MetadataDelta = update.MetadataDelta.ToArray(),
- ILDelta = update.ILDelta.ToArray(),
- UpdatedTypes = update.UpdatedTypes.ToArray(),
- })
- }, cancellationToken);
-
- bool result = await ReceiveApplyUpdateResult(browserRefreshServer, cancellationToken);
-
- return !result ? ApplyStatus.Failed : (applicableUpdates.Count < updates.Length) ? ApplyStatus.SomeChangesApplied : ApplyStatus.AllChangesApplied;
- }
-
- private async Task ReceiveApplyUpdateResult(BrowserRefreshServer browserRefresh, CancellationToken cancellationToken)
- {
- var buffer = new byte[1];
+ var anySuccess = false;
+ var anyFailure = false;
- var result = await browserRefresh.ReceiveAsync(buffer, cancellationToken);
- if (result is not { MessageType: WebSocketMessageType.Binary })
- {
- // A null result indicates no clients are connected. No deltas could have been applied in this state.
- Reporter.Verbose("Apply confirmation: No browser is connected");
- return false;
- }
+ // Make sure to send the same update to all browsers, the only difference is the shared secret.
- if (result is { Count: 1, EndOfMessage: true })
+ var updateId = _updateId++;
+ var deltas = updates.Select(update => new JsonDelta
{
- return buffer[0] == 1;
- }
-
- Reporter.Verbose("Browser failed to apply the change and reported error:");
+ ModuleId = update.ModuleId,
+ MetadataDelta = [.. update.MetadataDelta],
+ ILDelta = [.. update.ILDelta],
+ PdbDelta = [.. update.PdbDelta],
+ UpdatedTypes = [.. update.UpdatedTypes],
+ }).ToArray();
- buffer = new byte[1024];
- var messageStream = new MemoryStream();
+ var loggingLevel = Reporter.IsVerbose ? ResponseLoggingLevel.Verbose : ResponseLoggingLevel.WarningsAndErrors;
- while (true)
- {
- result = await browserRefresh.ReceiveAsync(buffer, cancellationToken);
- if (result is not { MessageType: WebSocketMessageType.Binary })
+ await browserRefreshServer.SendAndReceiveAsync(
+ request: sharedSecret => new JsonApplyHotReloadDeltasRequest
{
- Reporter.Verbose("Failed to receive error message");
- break;
- }
-
- messageStream.Write(buffer, 0, result.Value.Count);
-
- if (result is { EndOfMessage: true })
+ SharedSecret = sharedSecret,
+ UpdateId = updateId,
+ Deltas = deltas,
+ ResponseLoggingLevel = (int)loggingLevel
+ },
+ response: (value, reporter) =>
{
- // message and stack trace are separated by '\0'
- Reporter.Verbose(Encoding.UTF8.GetString(messageStream.ToArray()).Replace("\0", Environment.NewLine));
- break;
- }
- }
+ var data = BrowserRefreshServer.DeserializeJson(value);
- return false;
- }
+ if (data.Success)
+ {
+ anySuccess = true;
+ }
+ else
+ {
+ anyFailure = true;
+ }
- public override void Dispose()
- {
- // Do nothing.
+ ReportLog(reporter, data.Log.Select(entry => (entry.Message, (AgentMessageSeverity)entry.Severity)));
+ },
+ cancellationToken);
+
+ // If no browser is connected we assume the changes have been applied.
+ // If at least one browser suceeds we consider the changes successfully applied.
+ // TODO:
+ // The refresh server should remember the deltas and apply them to browsers connected in future.
+ // Currently the changes are remembered on the dev server and sent over there from the browser.
+ // If no browser is connected the changes are not sent though.
+ return (!anySuccess && anyFailure) ? ApplyStatus.Failed : (applicableUpdates.Count < updates.Length) ? ApplyStatus.SomeChangesApplied : ApplyStatus.AllChangesApplied;
}
- private readonly struct UpdatePayload
+ private readonly struct JsonApplyHotReloadDeltasRequest
{
- public string Type => "BlazorHotReloadDeltav2";
+ public string Type => "BlazorHotReloadDeltav3";
public string? SharedSecret { get; init; }
- public IEnumerable Deltas { get; init; }
+
+ public int UpdateId { get; init; }
+ public JsonDelta[] Deltas { get; init; }
+ public int ResponseLoggingLevel { get; init; }
}
- private readonly struct UpdateDelta
+ private readonly struct JsonDelta
{
- public int SequenceId { get; init; }
- public string ServerId { get; init; }
public Guid ModuleId { get; init; }
public byte[] MetadataDelta { get; init; }
public byte[] ILDelta { get; init; }
+ public byte[] PdbDelta { get; init; }
public int[] UpdatedTypes { get; init; }
}
- private readonly struct BlazorRequestApplyUpdateCapabilities
+ private readonly struct JsonApplyDeltasResponse
+ {
+ public bool Success { get; init; }
+ public IEnumerable Log { get; init; }
+ }
+
+ private readonly struct JsonLogEntry
+ {
+ public string Message { get; init; }
+ public int Severity { get; init; }
+ }
+
+ private readonly struct JsonGetApplyUpdateCapabilitiesRequest
{
public string Type => "BlazorRequestApplyUpdateCapabilities2";
}
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyHostedDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyHostedDeltaApplier.cs
index 630cf05a85c6..f2eb00de24bc 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyHostedDeltaApplier.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/BlazorWebAssemblyHostedDeltaApplier.cs
@@ -3,14 +3,14 @@
using System.Collections.Immutable;
+using Microsoft.Build.Graph;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
- internal sealed class BlazorWebAssemblyHostedDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, Version? targetFrameworkVersion) : DeltaApplier(reporter)
+ internal sealed class BlazorWebAssemblyHostedDeltaApplier(IReporter reporter, BrowserRefreshServer browserRefreshServer, ProjectGraphNode project) : DeltaApplier(reporter)
{
- private readonly BlazorWebAssemblyDeltaApplier _wasmApplier = new(reporter, browserRefreshServer, targetFrameworkVersion);
+ private readonly BlazorWebAssemblyDeltaApplier _wasmApplier = new(reporter, browserRefreshServer, project);
private readonly DefaultDeltaApplier _hostApplier = new(reporter);
public override void Dispose()
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs
index 73b3329f8fdb..a2c8b0a906ff 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs
@@ -9,10 +9,8 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.EditAndContinue;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal sealed class CompilationHandler : IDisposable
{
@@ -74,7 +72,7 @@ public async ValueTask TerminateNonRootProcessesAndDispose(CancellationToken can
Dispose();
}
- public ValueTask RestartSessionAsync(IReadOnlySet projectsToBeRebuilt, CancellationToken cancellationToken)
+ public void DiscardProjectBaselines(ImmutableDictionary projectsToBeRebuilt, CancellationToken cancellationToken)
{
// Remove previous updates to all modules that were affected by rude edits.
// All running projects that statically reference these modules have been terminated.
@@ -84,12 +82,16 @@ public ValueTask RestartSessionAsync(IReadOnlySet projectsToBeRebuilt
lock (_runningProjectsAndUpdatesGuard)
{
- _previousUpdates = _previousUpdates.RemoveAll(update => projectsToBeRebuilt.Contains(update.ProjectId));
+ _previousUpdates = _previousUpdates.RemoveAll(update => projectsToBeRebuilt.ContainsKey(update.ProjectId));
}
- _hotReloadService.EndSession();
- _reporter.Report(MessageDescriptor.HotReloadSessionEnded);
- return StartSessionAsync(cancellationToken);
+ _hotReloadService.UpdateBaselines(Workspace.CurrentSolution, projectsToBeRebuilt.Keys.ToImmutableArray());
+ }
+
+ public void UpdateProjectBaselines(ImmutableDictionary projectsToBeRebuilt, CancellationToken cancellationToken)
+ {
+ _hotReloadService.UpdateBaselines(Workspace.CurrentSolution, projectsToBeRebuilt.Keys.ToImmutableArray());
+ _reporter.Report(MessageDescriptor.ProjectBaselinesUpdated);
}
public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
@@ -101,17 +103,18 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
_reporter.Report(MessageDescriptor.HotReloadSessionStarted);
}
- private DeltaApplier CreateDeltaApplier(ProjectGraphNode projectNode, BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
- => HotReloadProfileReader.InferHotReloadProfile(projectNode, _reporter) switch
+ private static DeltaApplier CreateDeltaApplier(HotReloadProfile profile, ProjectGraphNode project, BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
+ => profile switch
{
- HotReloadProfile.BlazorWebAssembly => new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer!, projectNode.GetTargetFrameworkVersion()),
- HotReloadProfile.BlazorHosted => new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer!, projectNode.GetTargetFrameworkVersion()),
+ HotReloadProfile.BlazorWebAssembly => new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer!, project),
+ HotReloadProfile.BlazorHosted => new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer!, project),
_ => new DefaultDeltaApplier(processReporter),
};
public async Task TrackRunningProjectAsync(
ProjectGraphNode projectNode,
ProjectOptions projectOptions,
+ HotReloadProfile profile,
string namedPipeName,
BrowserRefreshServer? browserRefreshServer,
ProcessSpec processSpec,
@@ -122,7 +125,7 @@ private DeltaApplier CreateDeltaApplier(ProjectGraphNode projectNode, BrowserRef
{
var projectPath = projectNode.ProjectInstance.FullPath;
- var deltaApplier = CreateDeltaApplier(projectNode, browserRefreshServer, processReporter);
+ var deltaApplier = CreateDeltaApplier(profile, projectNode, browserRefreshServer, processReporter);
var processExitedSource = new CancellationTokenSource();
var processCommunicationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(processExitedSource.Token, cancellationToken);
@@ -274,15 +277,20 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
}
}
- public async ValueTask<(IReadOnlySet projectsToBeRebuilt, IEnumerable terminatedProjects)> HandleFileChangesAsync(
- Func, CancellationToken, Task> restartPrompt,
+ public async ValueTask<(ImmutableDictionary projectsToRebuild, ImmutableArray terminatedProjects)> HandleFileChangesAsync(
+ Func, CancellationToken, Task> restartPrompt,
CancellationToken cancellationToken)
{
var currentSolution = Workspace.CurrentSolution;
var runningProjects = _runningProjects;
- var updates = await _hotReloadService.GetUpdatesAsync(currentSolution, isRunningProject: p => runningProjects.ContainsKey(p.FilePath!), cancellationToken);
- var anyProcessNeedsRestart = updates.ProjectsToRestart.Count > 0;
+ var runningProjectIds = currentSolution.Projects
+ .Where(project => project.FilePath != null && runningProjects.ContainsKey(project.FilePath))
+ .Select(project => project.Id)
+ .ToImmutableHashSet();
+
+ var updates = await _hotReloadService.GetUpdatesAsync(currentSolution, runningProjectIds, cancellationToken);
+ var anyProcessNeedsRestart = !updates.ProjectIdsToRestart.IsEmpty;
await DisplayResultsAsync(updates, cancellationToken);
@@ -290,23 +298,23 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
{
// If Hot Reload is blocked (due to compilation error) we ignore the current
// changes and await the next file change.
- return (ImmutableHashSet.Empty, []);
+ return (ImmutableDictionary.Empty, []);
}
if (updates.Status == ModuleUpdateStatus.RestartRequired)
{
if (!anyProcessNeedsRestart)
{
- return (ImmutableHashSet.Empty, []);
+ return (ImmutableDictionary.Empty, []);
}
- await restartPrompt.Invoke(updates.ProjectsToRestart, cancellationToken);
+ await restartPrompt.Invoke(updates.ProjectIdsToRestart.Select(id => currentSolution.GetProject(id)!.Name), cancellationToken);
// Terminate all tracked processes that need to be restarted,
// except for the root process, which will terminate later on.
- var terminatedProjects = await TerminateNonRootProcessesAsync(updates.ProjectsToRestart.Select(p => p.FilePath!), cancellationToken);
+ var terminatedProjects = await TerminateNonRootProcessesAsync(updates.ProjectIdsToRestart.Select(id => currentSolution.GetProject(id)!.FilePath!), cancellationToken);
- return (updates.ProjectsToRebuild.Select(p => p.Id).ToHashSet(), terminatedProjects);
+ return (updates.ProjectIdsToRebuild.ToImmutableDictionary(keySelector: id => id, elementSelector: id => currentSolution.GetProject(id)!.FilePath!), terminatedProjects);
}
Debug.Assert(updates.Status == ModuleUpdateStatus.Ready);
@@ -346,17 +354,17 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT
}
}, cancellationToken);
- return (ImmutableHashSet.Empty, []);
+ return (ImmutableDictionary.Empty, []);
}
private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates updates, CancellationToken cancellationToken)
{
- var anyProcessNeedsRestart = updates.ProjectsToRestart.Count > 0;
+ var anyProcessNeedsRestart = !updates.ProjectIdsToRestart.IsEmpty;
switch (updates.Status)
{
case ModuleUpdateStatus.None:
- _reporter.Output("No hot reload changes to apply.");
+ _reporter.Output("No C# changes to apply.");
break;
case ModuleUpdateStatus.Ready:
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs
index 0bdceee705ca..2fade347530f 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs
@@ -7,10 +7,9 @@
using System.Diagnostics;
using System.IO.Pipes;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
-using Microsoft.Extensions.HotReload;
-using Microsoft.Extensions.Tools.Internal;
+using Microsoft.DotNet.HotReload;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal sealed class DefaultDeltaApplier(IReporter reporter) : SingleProcessDeltaApplier(reporter)
{
@@ -41,6 +40,11 @@ async Task> ConnectAsync()
Reporter.Verbose($"Capabilities: '{capabilities}'");
return capabilities.Split(' ').ToImmutableArray();
}
+ catch (EndOfStreamException)
+ {
+ // process terminated before capabilities sent:
+ return [];
+ }
catch (Exception e) when (e is not OperationCanceledException)
{
// pipe might throw another exception when forcibly closed on process termination:
@@ -88,6 +92,7 @@ public override async Task Apply(ImmutableArray ReceiveApplyUpdateResult(CancellationToken cancellation
var status = ArrayPool.Shared.Rent(1);
try
{
- var statusBytesRead = await _pipe.ReadAsync(status, cancellationToken);
+ var statusBytesRead = await _pipe.ReadAsync(status, offset: 0, count: 1, cancellationToken);
if (statusBytesRead != 1 || status[0] != UpdatePayload.ApplySuccessValue)
{
- Reporter.Error($"Change failed to apply (error code: '{BitConverter.ToString(status, 0, statusBytesRead)}'). Further changes won't be applied to this process.");
+ var message = (statusBytesRead == 0) ? "received no data" : $"received status 0x{status[0]:x2}";
+ Reporter.Error($"Change failed to apply ({message}). Further changes won't be applied to this process.");
return false;
}
- foreach (var (message, severity) in UpdatePayload.ReadLog(_pipe))
- {
- switch (severity)
- {
- case AgentMessageSeverity.Verbose:
- Reporter.Verbose(message, emoji: "🕵️");
- break;
-
- case AgentMessageSeverity.Error:
- Reporter.Error(message);
- break;
-
- case AgentMessageSeverity.Warning:
- Reporter.Warn(message, emoji: "⚠");
- break;
-
- default:
- Reporter.Error($"Unexpected message severity: {severity}");
- return false;
- }
- }
-
+ ReportLog(Reporter, UpdatePayload.ReadLog(_pipe));
return true;
}
finally
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/DeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/DeltaApplier.cs
index 8dfd06743312..897dc710d841 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/DeltaApplier.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/DeltaApplier.cs
@@ -4,9 +4,9 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
-using Microsoft.Extensions.Tools.Internal;
+using Microsoft.DotNet.HotReload;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal abstract class DeltaApplier(IReporter reporter) : IDisposable
{
@@ -27,6 +27,27 @@ internal abstract class DeltaApplier(IReporter reporter) : IDisposable
public abstract Task Apply(ImmutableArray updates, CancellationToken cancellationToken);
public abstract void Dispose();
+
+ public static void ReportLog(IReporter reporter, IEnumerable<(string message, AgentMessageSeverity severity)> log)
+ {
+ foreach (var (message, severity) in log)
+ {
+ switch (severity)
+ {
+ case AgentMessageSeverity.Error:
+ reporter.Error(message);
+ break;
+
+ case AgentMessageSeverity.Warning:
+ reporter.Warn(message, emoji: "⚠");
+ break;
+
+ default:
+ reporter.Verbose(message, emoji: "🕵️");
+ break;
+ }
+ }
+ }
}
internal enum ApplyStatus
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadEventSource.cs b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadEventSource.cs
index 2aff913ac55a..04b3dbb70d52 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadEventSource.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadEventSource.cs
@@ -4,7 +4,7 @@
using System.Diagnostics.Tracing;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
[EventSource(Name = "HotReload")]
internal sealed class HotReloadEventSource : EventSource
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfile.cs b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfile.cs
index cf56c3819975..898148d2f331 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfile.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfile.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal enum HotReloadProfile
{
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfileReader.cs b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfileReader.cs
index 38755a8c795b..fe8e9a968e01 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfileReader.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/HotReloadProfileReader.cs
@@ -4,9 +4,8 @@
using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal static class HotReloadProfileReader
{
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncher.cs b/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncher.cs
index 375e2a9b3248..f91f9342155b 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncher.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncher.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
///
/// Process launcher that triggers process launches at runtime of the watched application,
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncherFactory.cs b/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncherFactory.cs
index e805e1c3652f..431e79830ae3 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncherFactory.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/IRuntimeProcessLauncherFactory.cs
@@ -3,7 +3,7 @@
using Microsoft.Build.Graph;
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
///
/// Creates for a given root project.
@@ -12,5 +12,5 @@ namespace Microsoft.DotNet.Watcher;
///
internal interface IRuntimeProcessLauncherFactory
{
- public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, IReadOnlyList<(string name, string value)> buildProperties);
+ public IRuntimeProcessLauncher? TryCreate(ProjectGraphNode projectNode, ProjectLauncher projectLauncher, IReadOnlyList buildArguments);
}
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs b/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs
index 90ec68694946..38fcc3cc1afc 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs
@@ -8,10 +8,8 @@
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Text;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools;
+namespace Microsoft.DotNet.Watch;
internal class IncrementalMSBuildWorkspace : Workspace
{
@@ -38,7 +36,17 @@ public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationTok
var loader = new MSBuildProjectLoader(this);
var projectMap = ProjectMap.Create();
- var projectInfos = await loader.LoadProjectInfoAsync(rootProjectPath, projectMap, progress: null, msbuildLogger: null, cancellationToken).ConfigureAwait(false);
+
+ ImmutableArray projectInfos;
+ try
+ {
+ projectInfos = await loader.LoadProjectInfoAsync(rootProjectPath, projectMap, progress: null, msbuildLogger: null, cancellationToken).ConfigureAwait(false);
+ }
+ catch (InvalidOperationException)
+ {
+ // TODO: workaround for https://github.com/dotnet/roslyn/issues/75956
+ projectInfos = [];
+ }
var oldProjectIdsByPath = oldSolution.Projects.ToDictionary(keySelector: static p => p.FilePath!, elementSelector: static p => p.Id);
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs b/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs
index d5ce198f6518..d241241cc7de 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs
@@ -1,21 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.Extensions.HotReload
-{
- internal enum ResponseLoggingLevel : byte
- {
- WarningsAndErrors = 0,
- Verbose = 1,
- }
-
- internal enum AgentMessageSeverity : byte
- {
- Verbose = 0,
- Warning = 1,
- Error = 2,
- }
+using Microsoft.DotNet.HotReload;
+namespace Microsoft.DotNet.Watch
+{
internal readonly struct UpdatePayload(IReadOnlyList deltas, ResponseLoggingLevel responseLoggingLevel)
{
public const byte ApplySuccessValue = 0;
@@ -40,6 +29,7 @@ public async ValueTask WriteAsync(Stream stream, CancellationToken cancellationT
binaryWriter.Write(delta.ModuleId.ToString());
await WriteBytesAsync(binaryWriter, delta.MetadataDelta, cancellationToken);
await WriteBytesAsync(binaryWriter, delta.ILDelta, cancellationToken);
+ await WriteBytesAsync(binaryWriter, delta.PdbDelta, cancellationToken);
WriteIntArray(binaryWriter, delta.UpdatedTypes);
}
@@ -104,9 +94,10 @@ public static async ValueTask ReadAsync(Stream stream, Cancellati
var moduleId = Guid.Parse(binaryReader.ReadString());
var metadataDelta = await ReadBytesAsync(binaryReader, cancellationToken);
var ilDelta = await ReadBytesAsync(binaryReader, cancellationToken);
+ var pdbDelta = await ReadBytesAsync(binaryReader, cancellationToken);
var updatedTypes = ReadIntArray(binaryReader);
- deltas[i] = new UpdateDelta(moduleId, metadataDelta: metadataDelta, ilDelta: ilDelta, updatedTypes);
+ deltas[i] = new UpdateDelta(moduleId, metadataDelta: metadataDelta, ilDelta: ilDelta, pdbDelta: pdbDelta, updatedTypes);
}
var responseLoggingLevel = (ResponseLoggingLevel)binaryReader.ReadByte();
@@ -163,22 +154,6 @@ static int[] ReadIntArray(BinaryReader binaryReader)
}
}
- internal readonly struct UpdateDelta
- {
- public Guid ModuleId { get; }
- public byte[] MetadataDelta { get; }
- public byte[] ILDelta { get; }
- public int[] UpdatedTypes { get; }
-
- public UpdateDelta(Guid moduleId, byte[] metadataDelta, byte[] ilDelta, int[] updatedTypes)
- {
- ModuleId = moduleId;
- MetadataDelta = metadataDelta;
- ILDelta = ilDelta;
- UpdatedTypes = updatedTypes;
- }
- }
-
internal readonly struct ClientInitializationPayload
{
private const byte Version = 0;
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs b/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs
index 9185f32044c2..46a33b2b301d 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs
@@ -3,11 +3,8 @@
using System.Globalization;
using Microsoft.Build.Graph;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.DotNet.Watcher.Tools;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal delegate ValueTask ProcessExitAction(int processId, int? exitCode);
@@ -31,7 +28,6 @@ public EnvironmentOptions EnvironmentOptions
CancellationTokenSource processTerminationSource,
Action? onOutput,
RestartOperation restartOperation,
- bool build,
CancellationToken cancellationToken)
{
var projectNode = projectMap.TryGetProjectNode(projectOptions.ProjectPath, projectOptions.TargetFramework);
@@ -47,14 +43,18 @@ public EnvironmentOptions EnvironmentOptions
return null;
}
+ var profile = HotReloadProfileReader.InferHotReloadProfile(projectNode, Reporter);
+
+ // Blazor WASM does not need dotnet applier as all changes are applied in the browser,
+ // the process being launched is a dev server.
+ var injectDeltaApplier = profile != HotReloadProfile.BlazorWebAssembly;
+
var processSpec = new ProcessSpec
{
Executable = EnvironmentOptions.MuxerPath,
WorkingDirectory = projectOptions.WorkingDirectory,
OnOutput = onOutput,
- Arguments = build || projectOptions.Command is not ("run" or "test")
- ? [projectOptions.Command, .. projectOptions.CommandArguments]
- : [projectOptions.Command, "--no-build", .. projectOptions.CommandArguments]
+ Arguments = [projectOptions.Command, "--no-build", .. projectOptions.CommandArguments]
};
var environmentBuilder = EnvironmentVariablesBuilder.FromCurrentEnvironment();
@@ -62,10 +62,6 @@ public EnvironmentOptions EnvironmentOptions
// Directives:
- environmentBuilder.DotNetStartupHookDirective.Add(DeltaApplier.StartupHookPath);
- environmentBuilder.SetDirective(EnvironmentVariables.Names.DotnetModifiableAssemblies, "debug");
- environmentBuilder.SetDirective(EnvironmentVariables.Names.DotnetWatchHotReloadNamedPipeName, namedPipeName);
-
// Variables:
foreach (var (name, value) in projectOptions.LaunchEnvironmentVariables)
@@ -84,19 +80,32 @@ public EnvironmentOptions EnvironmentOptions
environmentBuilder.SetVariable(EnvironmentVariables.Names.DotnetWatch, "1");
environmentBuilder.SetVariable(EnvironmentVariables.Names.DotnetWatchIteration, (Iteration + 1).ToString(CultureInfo.InvariantCulture));
- // Do not ask agent to log to stdout until https://github.com/dotnet/sdk/issues/40484 is fixed.
- // For now we need to set the env variable explicitly when we need to diagnose issue with the agent.
- // Build targets might launch a process and read it's stdout. If the agent is loaded into such process and starts logging
- // to stdout it might interfere with the expected output.
- //if (context.Options.Verbose)
- //{
- // environmentBuilder.SetVariable(EnvironmentVariables.Names.HotReloadDeltaClientLogMessages, "1");
- //}
+ // Note:
+ // Microsoft.AspNetCore.Components.WebAssembly.Server.ComponentWebAssemblyConventions and Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware
+ // expect DOTNET_MODIFIABLE_ASSEMBLIES to be set in the blazor-devserver process, even though we are not performing Hot Reload in this process.
+ // The value is converted to DOTNET-MODIFIABLE-ASSEMBLIES header, which is in turn converted back to environment variable in Mono browser runtime loader:
+ // https://github.com/dotnet/runtime/blob/342936c5a88653f0f622e9d6cb727a0e59279b31/src/mono/browser/runtime/loader/config.ts#L330
+ environmentBuilder.SetDirective(EnvironmentVariables.Names.DotnetModifiableAssemblies, "debug");
- // TODO: workaround for https://github.com/dotnet/sdk/issues/40484
- var targetPath = projectNode.ProjectInstance.GetPropertyValue("RunCommand");
- environmentBuilder.SetVariable(EnvironmentVariables.Names.DotnetWatchHotReloadTargetProcessPath, targetPath);
- Reporter.Verbose($"Target process is '{targetPath}'");
+ if (injectDeltaApplier)
+ {
+ environmentBuilder.DotNetStartupHookDirective.Add(DeltaApplier.StartupHookPath);
+ environmentBuilder.SetDirective(EnvironmentVariables.Names.DotnetWatchHotReloadNamedPipeName, namedPipeName);
+
+ // Do not ask agent to log to stdout until https://github.com/dotnet/sdk/issues/40484 is fixed.
+ // For now we need to set the env variable explicitly when we need to diagnose issue with the agent.
+ // Build targets might launch a process and read it's stdout. If the agent is loaded into such process and starts logging
+ // to stdout it might interfere with the expected output.
+ //if (context.Options.Verbose)
+ //{
+ // environmentBuilder.SetVariable(EnvironmentVariables.Names.HotReloadDeltaClientLogMessages, "1");
+ //}
+
+ // TODO: workaround for https://github.com/dotnet/sdk/issues/40484
+ var targetPath = projectNode.ProjectInstance.GetPropertyValue("RunCommand");
+ environmentBuilder.SetVariable(EnvironmentVariables.Names.DotnetWatchHotReloadTargetProcessPath, targetPath);
+ Reporter.Verbose($"Target process is '{targetPath}'");
+ }
var browserRefreshServer = await browserConnector.LaunchOrRefreshBrowserAsync(projectNode, processSpec, environmentBuilder, projectOptions, cancellationToken);
environmentBuilder.ConfigureProcess(processSpec);
@@ -106,6 +115,7 @@ public EnvironmentOptions EnvironmentOptions
return await compilationHandler.TrackRunningProjectAsync(
projectNode,
projectOptions,
+ profile,
namedPipeName,
browserRefreshServer,
processSpec,
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ProjectNodeMap.cs b/src/BuiltInTools/dotnet-watch/HotReload/ProjectNodeMap.cs
index d84046666ccf..40412bb9978f 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/ProjectNodeMap.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/ProjectNodeMap.cs
@@ -3,14 +3,14 @@
using Microsoft.Build.Graph;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal readonly struct ProjectNodeMap(ProjectGraph graph, IReporter reporter)
{
public readonly ProjectGraph Graph = graph;
+ // full path of proj file to list of nodes representing all target frameworks of the project:
public readonly IReadOnlyDictionary> Map =
graph.ProjectNodes.GroupBy(n => n.ProjectInstance.FullPath).ToDictionary(
keySelector: static g => g.Key,
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/RestartPrompt.cs b/src/BuiltInTools/dotnet-watch/HotReload/RestartPrompt.cs
index d64a10b80ec3..488a7b5018b7 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/RestartPrompt.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/RestartPrompt.cs
@@ -2,9 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Tasks;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher
+namespace Microsoft.DotNet.Watch
{
internal sealed class RestartPrompt(IReporter reporter, ConsoleInputReader requester, bool? noPrompt)
{
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs b/src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs
index 99b04b828a63..49d8268cf0cf 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/RunningProject.cs
@@ -4,11 +4,10 @@
using System.Collections.Immutable;
using Microsoft.Build.Graph;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
- internal delegate ValueTask RestartOperation(bool build, CancellationToken cancellationToken);
+ internal delegate ValueTask RestartOperation(CancellationToken cancellationToken);
internal sealed class RunningProject(
ProjectGraphNode projectNode,
@@ -68,7 +67,6 @@ public void Dispose()
public async ValueTask WaitForProcessRunningAsync(CancellationToken cancellationToken)
{
await DeltaApplier.WaitForProcessRunningAsync(cancellationToken);
- Reporter.Report(MessageDescriptor.BuildCompleted);
}
}
}
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
index 3a6961854987..d9f241eb0fab 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
@@ -2,13 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections;
-using System.Diagnostics;
+using Microsoft.Build.Framework;
using Microsoft.Build.Graph;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.Extensions.Tools.Internal;
+using Microsoft.TemplateEngine.Utils;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal sealed class ScopedCssFileHandler(IReporter reporter, ProjectNodeMap projectMap, BrowserConnector browserConnector)
{
@@ -52,41 +50,61 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList files,
}
}
- var logger = reporter.IsVerbose ? new[] { new Build.Logging.ConsoleLogger() } : null;
+ if (!hasApplicableFiles)
+ {
+ return;
+ }
+
+ var logger = reporter.IsVerbose ? new[] { new Build.Logging.ConsoleLogger(LoggerVerbosity.Minimal) } : null;
- var tasks = projectsToRefresh.Select(async projectNode =>
+ var buildTasks = projectsToRefresh.Select(projectNode => Task.Run(() =>
{
- if (!projectNode.ProjectInstance.DeepCopy().Build(BuildTargetName, logger))
+ try
{
- return false;
+ if (!projectNode.ProjectInstance.DeepCopy().Build(BuildTargetName, logger))
+ {
+ return null;
+ }
}
-
- if (browserConnector.TryGetRefreshServer(projectNode, out var browserRefreshServer))
+ catch (Exception e)
{
- await HandleBrowserRefresh(browserRefreshServer, projectNode.ProjectInstance.FullPath, cancellationToken);
+ reporter.Error($"[{projectNode.GetDisplayName()}] Target {BuildTargetName} failed to build: {e}");
+ return null;
}
- return true;
- });
+ return projectNode;
+ }));
- var results = await Task.WhenAll(tasks).WaitAsync(cancellationToken);
+ var buildResults = await Task.WhenAll(buildTasks).WaitAsync(cancellationToken);
- if (hasApplicableFiles)
+ var browserRefreshTasks = buildResults.Where(p => p != null)!.GetTransitivelyReferencingProjects().Select(async projectNode =>
{
- var successfulCount = results.Sum(r => r ? 1 : 0);
-
- if (successfulCount == results.Length)
- {
- reporter.Output("Hot reload of scoped css succeeded.", emoji: "🔥");
- }
- else if (successfulCount > 0)
+ if (browserConnector.TryGetRefreshServer(projectNode, out var browserRefreshServer))
{
- reporter.Output($"Hot reload of scoped css partially succeeded: {successfulCount} project(s) out of {results.Length} were updated.", emoji: "🔥");
+ reporter.Verbose($"[{projectNode.GetDisplayName()}] Refreshing browser.");
+ await HandleBrowserRefresh(browserRefreshServer, projectNode.ProjectInstance.FullPath, cancellationToken);
}
else
{
- reporter.Output("Hot reload of scoped css failed.", emoji: "🔥");
+ reporter.Verbose($"[{projectNode.GetDisplayName()}] No refresh server.");
}
+ });
+
+ await Task.WhenAll(browserRefreshTasks).WaitAsync(cancellationToken);
+
+ var successfulCount = buildResults.Sum(r => r != null ? 1 : 0);
+
+ if (successfulCount == buildResults.Length)
+ {
+ reporter.Output("Hot reload of scoped css succeeded.", emoji: "🔥");
+ }
+ else if (successfulCount > 0)
+ {
+ reporter.Output($"Hot reload of scoped css partially succeeded: {successfulCount} project(s) out of {buildResults.Length} were updated.", emoji: "🔥");
+ }
+ else
+ {
+ reporter.Output("Hot reload of scoped css failed.", emoji: "🔥");
}
}
@@ -99,7 +117,7 @@ private static async Task HandleBrowserRefresh(BrowserRefreshServer browserRefre
// referenced project.
var cssFilePath = Path.GetFileNameWithoutExtension(containingProjectPath) + ".css";
var message = new UpdateStaticFileMessage { Path = cssFilePath };
- await browserRefreshServer.SendJsonSerlialized(message, cancellationToken);
+ await browserRefreshServer.SendJsonMessageAsync(message, cancellationToken);
}
private readonly struct UpdateStaticFileMessage
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/SingleProcessDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/SingleProcessDeltaApplier.cs
index f83d52ee2784..5ddd10428664 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/SingleProcessDeltaApplier.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/SingleProcessDeltaApplier.cs
@@ -5,9 +5,8 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal abstract class SingleProcessDeltaApplier(IReporter reporter) : DeltaApplier(reporter)
{
diff --git a/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs
index ce53bc8a7ce3..05fde9ab4001 100644
--- a/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs
@@ -2,15 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections;
-using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
-using Microsoft.CodeAnalysis.StackTraceExplorer;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.Extensions.Tools.Internal;
+using Microsoft.Build.Graph;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal sealed class StaticFileHandler(IReporter reporter, ProjectNodeMap projectMap, BrowserConnector browserConnector)
{
@@ -23,6 +19,8 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList f
{
var allFilesHandled = true;
var refreshRequests = new Dictionary>();
+ var projectsWithoutRefreshServer = new HashSet();
+
for (int i = 0; i < files.Count; i++)
{
var file = files[i].Item;
@@ -50,11 +48,16 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList f
{
if (!refreshRequests.TryGetValue(refreshServer, out var filesPerServer))
{
+ reporter.Verbose($"[{projectNode.GetDisplayName()}] Refreshing browser.");
refreshRequests.Add(refreshServer, filesPerServer = []);
}
filesPerServer.Add(file.StaticWebAssetPath);
}
+ else if (projectsWithoutRefreshServer.Add(projectNode))
+ {
+ reporter.Verbose($"[{projectNode.GetDisplayName()}] No refresh server.");
+ }
}
}
}
@@ -69,9 +72,9 @@ public async ValueTask HandleFileChangesAsync(IReadOnlyList f
// Serialize all requests sent to a single server:
foreach (var path in request.Value)
{
- reporter.Verbose($"Sending static file update request for asset '{path}'");
+ reporter.Verbose($"Sending static file update request for asset '{path}'.");
var message = JsonSerializer.SerializeToUtf8Bytes(new UpdateStaticFileMessage { Path = path }, s_jsonSerializerOptions);
- await request.Key.SendMessage(message, cancellationToken);
+ await request.Key.SendAsync(message, cancellationToken);
}
});
diff --git a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
index 3ca446c43b30..ca5348aea93b 100644
--- a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
@@ -1,15 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Immutable;
using System.Diagnostics;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.DotNet.Watcher.Tools;
-using Microsoft.Extensions.Tools.Internal;
+using Microsoft.CodeAnalysis;
-namespace Microsoft.DotNet.Watcher
+namespace Microsoft.DotNet.Watch
{
- internal sealed class HotReloadDotNetWatcher : Watcher
+ internal sealed partial class HotReloadDotNetWatcher : Watcher
{
+ private static readonly DateTime s_fileNotExistFileTime = DateTime.FromFileTime(0);
+
private readonly IConsole _console;
private readonly IRuntimeProcessLauncherFactory? _runtimeProcessLauncherFactory;
private readonly RestartPrompt? _rudeEditRestartPrompt;
@@ -44,8 +45,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
_console.KeyPressed += (key) =>
{
- var modifiers = ConsoleModifiers.Control;
- if ((key.Modifiers & modifiers) == modifiers && key.Key == ConsoleKey.R && forceRestartCancellationSource is { } source)
+ if (key.Modifiers.HasFlag(ConsoleModifiers.Control) && key.Key == ConsoleKey.R && forceRestartCancellationSource is { } source)
{
// provide immediate feedback to the user:
Context.Reporter.Report(source.IsCancellationRequested ? MessageDescriptor.RestartInProgress : MessageDescriptor.RestartRequested);
@@ -58,6 +58,8 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
Context.Reporter.Output(hotReloadEnabledMessage, emoji: "🔥");
}
+ using var fileWatcher = new FileWatcher(Context.Reporter);
+
for (var iteration = 0; !shutdownCancellationToken.IsCancellationRequested; iteration++)
{
Interlocked.Exchange(ref forceRestartCancellationSource, new CancellationTokenSource())?.Dispose();
@@ -69,12 +71,12 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
var iterationCancellationToken = iterationCancellationSource.Token;
var waitForFileChangeBeforeRestarting = true;
- HotReloadFileSetWatcher? fileSetWatcher = null;
EvaluationResult? evaluationResult = null;
RunningProject? rootRunningProject = null;
- Task? fileSetWatcherTask = null;
+ Task>? fileWatcherTask = null;
IRuntimeProcessLauncher? runtimeProcessLauncher = null;
CompilationHandler? compilationHandler = null;
+ Action? fileChangedCallback = null;
try
{
@@ -91,12 +93,13 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
// use normalized MSBuild path so that we can index into the ProjectGraph
rootProjectOptions = rootProjectOptions with { ProjectPath = rootProject.ProjectInstance.FullPath };
- if (rootProject.GetCapabilities().Contains(AspireServiceFactory.AppHostProjectCapability))
+ var rootProjectCapabilities = rootProject.GetCapabilities();
+ if (rootProjectCapabilities.Contains(AspireServiceFactory.AppHostProjectCapability))
{
runtimeProcessLauncherFactory ??= AspireServiceFactory.Instance;
Context.Reporter.Verbose("Using Aspire process launcher.");
}
-
+
await using var browserConnector = new BrowserConnector(Context);
var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, Context.Reporter);
compilationHandler = new CompilationHandler(Context.Reporter);
@@ -106,7 +109,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
var rootProjectNode = evaluationResult.ProjectGraph.GraphRoots.Single();
- runtimeProcessLauncher = runtimeProcessLauncherFactory?.TryCreate(rootProjectNode, projectLauncher, rootProjectOptions.BuildProperties);
+ runtimeProcessLauncher = runtimeProcessLauncherFactory?.TryCreate(rootProjectNode, projectLauncher, rootProjectOptions.BuildArguments);
if (runtimeProcessLauncher != null)
{
var launcherEnvironment = runtimeProcessLauncher.GetEnvironmentVariables();
@@ -116,12 +119,17 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
};
}
+ if (!await BuildProjectAsync(rootProjectOptions.ProjectPath, rootProjectOptions.BuildArguments, iterationCancellationToken))
+ {
+ // error has been reported:
+ continue;
+ }
+
rootRunningProject = await projectLauncher.TryLaunchProcessAsync(
rootProjectOptions,
rootProcessTerminationSource,
onOutput: null,
- restartOperation: new RestartOperation((_, _) => throw new InvalidOperationException("Root project shouldn't be restarted")),
- build: true,
+ restartOperation: new RestartOperation(_ => throw new InvalidOperationException("Root project shouldn't be restarted")),
iterationCancellationToken);
if (rootRunningProject == null)
@@ -172,65 +180,65 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
return;
}
- fileSetWatcher = new HotReloadFileSetWatcher(evaluationResult.Files, buildCompletionTime, Context.Reporter, Context.EnvironmentOptions.TestFlags);
+ fileWatcher.WatchContainingDirectories(evaluationResult.Files.Keys);
+
+ var changedFilesAccumulator = ImmutableList.Empty;
+
+ void FileChangedCallback(string path, ChangeKind kind)
+ {
+ if (TryGetChangedFile(evaluationResult.Files, buildCompletionTime, path, kind) is { } changedFile)
+ {
+ ImmutableInterlocked.Update(ref changedFilesAccumulator, changedFiles => changedFiles.Add(changedFile));
+ }
+ else
+ {
+ Context.Reporter.Verbose($"Change ignored: {kind} '{path}'.");
+ }
+ }
+
+ fileChangedCallback = FileChangedCallback;
+ fileWatcher.OnFileChange += fileChangedCallback;
+ ReportWatchingForChanges();
// Hot Reload loop - exits when the root process needs to be restarted.
while (true)
{
- fileSetWatcherTask = fileSetWatcher.GetChangedFilesAsync(iterationCancellationToken);
-
- var finishedTask = await Task.WhenAny(rootRunningProject.RunningProcess, fileSetWatcherTask).WaitAsync(iterationCancellationToken);
- if (finishedTask == rootRunningProject.RunningProcess)
+ try
{
- // Cancel the iteration, but wait for a file change before starting a new one.
+ // Use timeout to batch file changes. If the process doesn't exit within the given timespan we'll check
+ // for accumulated file changes. If there are any we attempt Hot Reload. Otherwise we come back here to wait again.
+ _ = await rootRunningProject.RunningProcess.WaitAsync(TimeSpan.FromMilliseconds(50), iterationCancellationToken);
+
+ // Process exited: cancel the iteration, but wait for a file change before starting a new one
+ waitForFileChangeBeforeRestarting = true;
iterationCancellationSource.Cancel();
break;
}
-
- // File watcher returns null when canceled:
- if (fileSetWatcherTask.Result is not { } changedFiles)
+ catch (TimeoutException)
+ {
+ // check for changed files
+ }
+ catch (OperationCanceledException)
{
Debug.Assert(iterationCancellationToken.IsCancellationRequested);
waitForFileChangeBeforeRestarting = false;
break;
}
- ReportFileChanges(changedFiles);
-
- // When a new file is added we need to run design-time build to find out
- // what kind of the file it is and which project(s) does it belong to (can be linked, web asset, etc.).
- // We don't need to rebuild and restart the application though.
- if (changedFiles.Any(f => f.Change is ChangeKind.Add))
+ var changedFiles = await CaptureChangedFilesSnapshot(rebuiltProjects: null);
+ if (changedFiles is [])
{
- Context.Reporter.Verbose("File addition triggered re-evaluation.");
-
- evaluationResult = await EvaluateRootProjectAsync(iterationCancellationToken);
-
- await compilationHandler.Workspace.UpdateProjectConeAsync(RootFileSetFactory.RootProjectFile, iterationCancellationToken);
-
- if (shutdownCancellationToken.IsCancellationRequested)
- {
- // Ctrl+C:
- return;
- }
-
- // update files in the change set with new evaluation info:
- for (int i = 0; i < changedFiles.Length; i++)
- {
- if (evaluationResult.Files.TryGetValue(changedFiles[i].Item.FilePath, out var evaluatedFile))
- {
- changedFiles[i] = changedFiles[i] with { Item = evaluatedFile };
- }
- }
-
- ReportFileChanges(changedFiles);
-
- fileSetWatcher = new HotReloadFileSetWatcher(evaluationResult.Files, buildCompletionTime, Context.Reporter, Context.EnvironmentOptions.TestFlags);
+ continue;
}
- else
+
+ if (!rootProjectCapabilities.Contains("SupportsHotReload"))
{
- // update the workspace to reflect changes in the file content:
- await compilationHandler.Workspace.UpdateFileContentAsync(changedFiles, iterationCancellationToken);
+ Context.Reporter.Warn($"Project '{rootProject.GetDisplayName()}' does not support Hot Reload and must be rebuilt.");
+
+ // file change already detected
+ waitForFileChangeBeforeRestarting = false;
+ iterationCancellationSource.Cancel();
+ break;
}
HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.Main);
@@ -246,7 +254,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.CompilationHandler);
- var (projectsToBeRebuilt, projectsToRestart) = await compilationHandler.HandleFileChangesAsync(restartPrompt: async (projects, cancellationToken) =>
+ var (projectsToRebuild, projectsToRestart) = await compilationHandler.HandleFileChangesAsync(restartPrompt: async (projectNames, cancellationToken) =>
{
if (_rudeEditRestartPrompt != null)
{
@@ -262,9 +270,9 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
{
Context.Reporter.Output("Affected projects:");
- foreach (var project in projects.OrderBy(p => p.Name))
+ foreach (var projectName in projectNames.OrderBy(n => n))
{
- Context.Reporter.Output(" " + project.Name);
+ Context.Reporter.Output(" " + projectName);
}
question = "Do you want to restart these projects?";
@@ -280,9 +288,9 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
{
Context.Reporter.Verbose("Restarting without prompt since dotnet-watch is running in non-interactive mode.");
- foreach (var project in projects)
+ foreach (var projectName in projectNames)
{
- Context.Reporter.Verbose($" Project to restart: '{project.Name}'");
+ Context.Reporter.Verbose($" Project to restart: '{projectName}'");
}
}
}, iterationCancellationToken);
@@ -301,23 +309,144 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
break;
}
- if (projectsToRestart.Any())
+ if (projectsToRebuild.Count > 0)
+ {
+ // Discard baselines before build.
+ compilationHandler.DiscardProjectBaselines(projectsToRebuild, iterationCancellationToken);
+
+ while (true)
+ {
+ iterationCancellationToken.ThrowIfCancellationRequested();
+
+ // pause accumulating file changes during build:
+ fileWatcher.OnFileChange -= fileChangedCallback;
+ try
+ {
+ var buildResults = await Task.WhenAll(
+ projectsToRebuild.Values.Select(projectPath => BuildProjectAsync(projectPath, rootProjectOptions.BuildArguments, iterationCancellationToken)));
+
+ if (buildResults.All(success => success))
+ {
+ break;
+ }
+ }
+ finally
+ {
+ fileWatcher.OnFileChange += fileChangedCallback;
+ }
+
+ iterationCancellationToken.ThrowIfCancellationRequested();
+
+ _ = await fileWatcher.WaitForFileChangeAsync(
+ startedWatching: () => Context.Reporter.Report(MessageDescriptor.FixBuildError),
+ shutdownCancellationToken);
+ }
+
+ // Update build completion time, so that file changes caused by the rebuild do not affect our file watcher:
+ buildCompletionTime = DateTime.UtcNow;
+
+ // Changes made since last snapshot of the accumulator shouldn't be included in next Hot Reload update.
+ // Apply them to the workspace.
+ _ = await CaptureChangedFilesSnapshot(projectsToRebuild);
+
+ // Update project baselines to reflect changes to the restarted projects.
+ compilationHandler.UpdateProjectBaselines(projectsToRebuild, iterationCancellationToken);
+ }
+
+ if (projectsToRestart is not [])
{
- // Restart all terminated child processes and wait until their build completes:
await Task.WhenAll(
projectsToRestart.Select(async runningProject =>
{
- var newRunningProject = await runningProject.RestartOperation(build: true, shutdownCancellationToken);
- runningProject.Dispose();
- await newRunningProject.WaitForProcessRunningAsync(shutdownCancellationToken);
+ var newRunningProject = await runningProject.RestartOperation(shutdownCancellationToken);
+
+ try
+ {
+ await newRunningProject.WaitForProcessRunningAsync(shutdownCancellationToken);
+ }
+ catch (OperationCanceledException) when (!shutdownCancellationToken.IsCancellationRequested)
+ {
+ // Process might have exited while we were trying to communicate with it.
+ }
+ finally
+ {
+ runningProject.Dispose();
+ }
}))
.WaitAsync(shutdownCancellationToken);
+ }
- // Update build completion time, so that file changes caused by the rebuild do not affect our file watcher:
- fileSetWatcher.UpdateBuildCompletionTime(DateTime.UtcNow);
+ async Task> CaptureChangedFilesSnapshot(ImmutableDictionary? rebuiltProjects)
+ {
+ var changedFiles = Interlocked.Exchange(ref changedFilesAccumulator, []);
+ if (changedFiles is [])
+ {
+ return [];
+ }
+
+ // When a new file is added we need to run design-time build to find out
+ // what kind of the file it is and which project(s) does it belong to (can be linked, web asset, etc.).
+ // We don't need to rebuild and restart the application though.
+ var hasAddedFile = changedFiles.Any(f => f.Change is ChangeKind.Add);
+
+ if (hasAddedFile)
+ {
+ Context.Reporter.Verbose("File addition triggered re-evaluation.");
+
+ evaluationResult = await EvaluateRootProjectAsync(iterationCancellationToken);
+
+ // additional directories may have been added:
+ fileWatcher.WatchContainingDirectories(evaluationResult.Files.Keys);
+
+ await compilationHandler.Workspace.UpdateProjectConeAsync(RootFileSetFactory.RootProjectFile, iterationCancellationToken);
+
+ if (shutdownCancellationToken.IsCancellationRequested)
+ {
+ // Ctrl+C:
+ return [];
+ }
+
+ // Update files in the change set with new evaluation info.
+ changedFiles = changedFiles
+ .Select(f => evaluationResult.Files.TryGetValue(f.Item.FilePath, out var evaluatedFile) ? f with { Item = evaluatedFile } : f)
+ .ToImmutableList();
+ }
+
+ if (rebuiltProjects != null)
+ {
+ // Filter changed files down to those contained in projects being rebuilt.
+ // File changes that affect projects that are not being rebuilt will stay in the accumulator
+ // and be included in the next Hot Reload change set.
+ var rebuiltProjectPaths = rebuiltProjects.Values.ToHashSet();
+
+ var newAccumulator = ImmutableList.Empty;
+ var newChangedFiles = ImmutableList.Empty;
- // Restart session to capture new baseline that reflects the changes to the restarted projects.
- await compilationHandler.RestartSessionAsync(projectsToBeRebuilt, iterationCancellationToken);
+ foreach (var file in changedFiles)
+ {
+ if (file.Item.ContainingProjectPaths.All(containingProjectPath => rebuiltProjectPaths.Contains(containingProjectPath)))
+ {
+ newChangedFiles = newChangedFiles.Add(file);
+ }
+ else
+ {
+ newAccumulator = newAccumulator.Add(file);
+ }
+ }
+
+ changedFiles = newChangedFiles;
+ ImmutableInterlocked.Update(ref changedFilesAccumulator, accumulator => accumulator.AddRange(newAccumulator));
+ }
+
+ ReportFileChanges(changedFiles);
+
+ if (!hasAddedFile)
+ {
+ // update the workspace to reflect changes in the file content:
+ await compilationHandler.Workspace.UpdateFileContentAsync(changedFiles, iterationCancellationToken);
+ }
+
+ return changedFiles;
}
}
}
@@ -327,9 +456,10 @@ await Task.WhenAll(
}
finally
{
- if (!rootProcessTerminationSource.IsCancellationRequested)
+ // stop watching file changes:
+ if (fileChangedCallback != null)
{
- rootProcessTerminationSource.Cancel();
+ fileWatcher.OnFileChange -= fileChangedCallback;
}
if (runtimeProcessLauncher != null)
@@ -345,10 +475,15 @@ await Task.WhenAll(
await compilationHandler.TerminateNonRootProcessesAndDispose(CancellationToken.None);
}
+ if (!rootProcessTerminationSource.IsCancellationRequested)
+ {
+ rootProcessTerminationSource.Cancel();
+ }
+
try
{
// Wait for the root process to exit.
- await Task.WhenAll(new[] { (Task?)rootRunningProject?.RunningProcess, fileSetWatcherTask }.Where(t => t != null)!);
+ await Task.WhenAll(new[] { (Task?)rootRunningProject?.RunningProcess, fileWatcherTask }.Where(t => t != null)!);
}
catch (OperationCanceledException) when (!shutdownCancellationToken.IsCancellationRequested)
{
@@ -356,7 +491,7 @@ await Task.WhenAll(
}
finally
{
- fileSetWatcherTask = null;
+ fileWatcherTask = null;
if (runtimeProcessLauncher != null)
{
@@ -365,22 +500,119 @@ await Task.WhenAll(
rootRunningProject?.Dispose();
- if (evaluationResult != null &&
- waitForFileChangeBeforeRestarting &&
+ if (waitForFileChangeBeforeRestarting &&
!shutdownCancellationToken.IsCancellationRequested &&
!forceRestartCancellationSource.IsCancellationRequested)
{
- fileSetWatcher ??= new HotReloadFileSetWatcher(evaluationResult.Files, DateTime.MinValue, Context.Reporter, Context.EnvironmentOptions.TestFlags);
- Context.Reporter.Report(MessageDescriptor.WaitingForFileChangeBeforeRestarting);
-
using var shutdownOrForcedRestartSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellationToken, forceRestartCancellationSource.Token);
- await fileSetWatcher.GetChangedFilesAsync(shutdownOrForcedRestartSource.Token, forceWaitForNewUpdate: true);
+ await WaitForFileChangeBeforeRestarting(fileWatcher, evaluationResult, shutdownOrForcedRestartSource.Token);
}
+ }
+ }
+ }
+ }
+
+ private async ValueTask WaitForFileChangeBeforeRestarting(FileWatcher fileWatcher, EvaluationResult? evaluationResult, CancellationToken cancellationToken)
+ {
+ if (evaluationResult != null)
+ {
+ if (!fileWatcher.WatchingDirectories)
+ {
+ fileWatcher.WatchContainingDirectories(evaluationResult.Files.Keys);
+ }
+
+ _ = await fileWatcher.WaitForFileChangeAsync(
+ evaluationResult.Files,
+ startedWatching: () => Context.Reporter.Report(MessageDescriptor.WaitingForFileChangeBeforeRestarting),
+ cancellationToken);
+ }
+ else
+ {
+ // evaluation cancelled - watch for any changes in the directory containing the root project:
+ fileWatcher.WatchContainingDirectories([RootFileSetFactory.RootProjectFile]);
+
+ _ = await fileWatcher.WaitForFileChangeAsync(
+ startedWatching: () => Context.Reporter.Report(MessageDescriptor.WaitingForFileChangeBeforeRestarting),
+ cancellationToken);
+ }
+ }
- fileSetWatcher?.Dispose();
+ private ChangedFile? TryGetChangedFile(IReadOnlyDictionary fileSet, DateTime buildCompletionTime, string path, ChangeKind kind)
+ {
+ // only handle file changes:
+ if (Directory.Exists(path))
+ {
+ return null;
+ }
+
+ if (kind != ChangeKind.Delete)
+ {
+ try
+ {
+ // Do not report changes to files that happened during build:
+ var creationTime = File.GetCreationTimeUtc(path);
+ var writeTime = File.GetLastWriteTimeUtc(path);
+
+ if (creationTime == s_fileNotExistFileTime || writeTime == s_fileNotExistFileTime)
+ {
+ // file might have been deleted since we received the event
+ kind = ChangeKind.Delete;
+ }
+ else if (creationTime.Ticks < buildCompletionTime.Ticks && writeTime.Ticks < buildCompletionTime.Ticks)
+ {
+ Context.Reporter.Verbose(
+ $"Ignoring file change during build: {kind} '{path}' " +
+ $"(created {FormatTimestamp(creationTime)} and written {FormatTimestamp(writeTime)} before {FormatTimestamp(buildCompletionTime)}).");
+
+ return null;
}
+ else if (writeTime > creationTime)
+ {
+ Context.Reporter.Verbose($"File change: {kind} '{path}' (written {FormatTimestamp(writeTime)} after {FormatTimestamp(buildCompletionTime)}).");
+ }
+ else
+ {
+ Context.Reporter.Verbose($"File change: {kind} '{path}' (created {FormatTimestamp(creationTime)} after {FormatTimestamp(buildCompletionTime)}).");
+ }
+ }
+ catch (Exception e)
+ {
+ Context.Reporter.Verbose($"Ignoring file '{path}' due to access error: {e.Message}.");
+ return null;
}
}
+
+ if (kind == ChangeKind.Delete)
+ {
+ Context.Reporter.Verbose($"File '{path}' deleted after {FormatTimestamp(buildCompletionTime)}.");
+ }
+
+ if (fileSet.TryGetValue(path, out var fileItem))
+ {
+ // For some reason we are sometimes seeing Add events raised whan an existing file is updated:
+ return new ChangedFile(fileItem, (kind == ChangeKind.Add) ? ChangeKind.Update : kind);
+ }
+
+ if (kind == ChangeKind.Add)
+ {
+ return new ChangedFile(new FileItem { FilePath = path, ContainingProjectPaths = [] }, kind);
+ }
+
+ return null;
+ }
+
+ internal static string FormatTimestamp(DateTime time)
+ => time.ToString("HH:mm:ss.fffffff");
+
+ private void ReportWatchingForChanges()
+ {
+ var waitingForChanges = MessageDescriptor.WaitingForChanges;
+ if (Context.EnvironmentOptions.TestFlags.HasFlag(TestFlags.ElevateWaitingForChangesMessageSeverity))
+ {
+ waitingForChanges = waitingForChanges with { Severity = MessageSeverity.Output };
+ }
+
+ Context.Reporter.Report(waitingForChanges);
}
private void ReportFileChanges(IReadOnlyList changedFiles)
@@ -435,11 +667,46 @@ private async ValueTask EvaluateRootProjectAsync(CancellationT
return result;
}
- Context.Reporter.Report(MessageDescriptor.FixBuildError);
- await FileWatcher.WaitForFileChangeAsync(RootFileSetFactory.RootProjectFile, Context.Reporter, cancellationToken);
+ await FileWatcher.WaitForFileChangeAsync(
+ RootFileSetFactory.RootProjectFile,
+ Context.Reporter,
+ startedWatching: () => Context.Reporter.Report(MessageDescriptor.FixBuildError),
+ cancellationToken);
}
}
+ private async Task BuildProjectAsync(string projectPath, IReadOnlyList buildArguments, CancellationToken cancellationToken)
+ {
+ var buildOutput = new List();
+
+ var processSpec = new ProcessSpec
+ {
+ Executable = Context.EnvironmentOptions.MuxerPath,
+ WorkingDirectory = Path.GetDirectoryName(projectPath)!,
+ OnOutput = line =>
+ {
+ lock (buildOutput)
+ {
+ buildOutput.Add(line);
+ }
+ },
+ // pass user-specified build arguments last to override defaults:
+ Arguments = ["build", projectPath, "-consoleLoggerParameters:NoSummary;Verbosity=minimal", .. buildArguments]
+ };
+
+ Context.Reporter.Output($"Building '{projectPath}' ...");
+
+ var exitCode = await ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: false, launchResult: null, cancellationToken);
+ BuildUtilities.ReportBuildOutput(Context.Reporter, buildOutput, verboseOutput: exitCode == 0);
+
+ if (exitCode == 0)
+ {
+ Context.Reporter.Output("Build succeeded.");
+ }
+
+ return exitCode == 0;
+ }
+
private string GetRelativeFilePath(string path)
{
var relativePath = path;
diff --git a/src/BuiltInTools/dotnet-watch/Internal/BrowserSpecificReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/BrowserSpecificReporter.cs
new file mode 100644
index 000000000000..2d15a2d78ba8
--- /dev/null
+++ b/src/BuiltInTools/dotnet-watch/Internal/BrowserSpecificReporter.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Build.Graph;
+
+namespace Microsoft.DotNet.Watch;
+
+internal sealed class BrowserSpecificReporter(int browserId, IReporter underlyingReporter) : IReporter
+{
+ private readonly string _prefix = $"[Browser #{browserId}] ";
+
+ public bool IsVerbose
+ => underlyingReporter.IsVerbose;
+
+ public bool EnableProcessOutputReporting
+ => false;
+
+ public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
+ => throw new InvalidOperationException();
+
+ public void ReportProcessOutput(OutputLine line)
+ => throw new InvalidOperationException();
+
+ public void Report(MessageDescriptor descriptor, string prefix, object?[] args)
+ => underlyingReporter.Report(descriptor, _prefix + prefix, args);
+}
diff --git a/src/BuiltInTools/dotnet-watch/Internal/CommandLineUtilities.cs b/src/BuiltInTools/dotnet-watch/Internal/CommandLineUtilities.cs
index ff710c7666b8..587087f49365 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/CommandLineUtilities.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/CommandLineUtilities.cs
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// Copied from dotnet/runtime/src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal static class CommandLineUtilities
{
diff --git a/src/BuiltInTools/dotnet-watch/Internal/ConsoleInputReader.cs b/src/BuiltInTools/dotnet-watch/Internal/ConsoleInputReader.cs
index c9483fe920ef..3d9a3d4eb09b 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/ConsoleInputReader.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/ConsoleInputReader.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.Extensions.Tools.Internal
+namespace Microsoft.DotNet.Watch
{
internal sealed class ConsoleInputReader(IConsole console, bool quiet, bool suppressEmojis)
{
diff --git a/src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs
index 0d1b8be10797..d50f49e8b1cc 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs
@@ -2,9 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Graph;
-using Microsoft.DotNet.Watcher.Internal;
-namespace Microsoft.Extensions.Tools.Internal
+namespace Microsoft.DotNet.Watch
{
///
/// This API supports infrastructure and is not intended to be used
diff --git a/src/BuiltInTools/dotnet-watch/Internal/Ensure.cs b/src/BuiltInTools/dotnet-watch/Internal/Ensure.cs
index fdd7bfaa33f9..21ab1478ab41 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/Ensure.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/Ensure.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.Extensions.Tools.Internal
+namespace Microsoft.DotNet.Watch
{
internal static class Ensure
{
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs
index 3a339bf63fb1..94d4323096f4 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs
@@ -1,13 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Extensions.Tools.Internal;
-
-namespace Microsoft.DotNet.Watcher.Internal
+namespace Microsoft.DotNet.Watch
{
- internal sealed class FileWatcher(IReadOnlyDictionary fileSet, IReporter reporter) : IDisposable
+ internal sealed class FileWatcher(IReporter reporter) : IDisposable
{
- private readonly Dictionary _watchers = [];
+ // Directory watcher for each watched directory
+ private readonly Dictionary _watchers = [];
private bool _disposed;
public event Action? OnFileChange;
@@ -29,13 +28,19 @@ public void Dispose()
}
}
- public void StartWatching()
+ public bool WatchingDirectories
+ => _watchers.Count > 0;
+
+ public void WatchContainingDirectories(IEnumerable filePaths)
+ => WatchDirectories(filePaths.Select(path => Path.GetDirectoryName(path)!));
+
+ public void WatchDirectories(IEnumerable directories)
{
- EnsureNotDisposed();
+ ObjectDisposedException.ThrowIf(_disposed, this);
- foreach (var (filePath, _) in fileSet)
+ foreach (var dir in directories)
{
- var directory = EnsureTrailingSlash(Path.GetDirectoryName(filePath)!);
+ var directory = EnsureTrailingSlash(dir);
var alreadyWatched = _watchers
.Where(d => directory.StartsWith(d.Key))
@@ -67,9 +72,9 @@ public void StartWatching()
private void WatcherErrorHandler(object? sender, Exception error)
{
- if (sender is IFileSystemWatcher watcher)
+ if (sender is IDirectoryWatcher watcher)
{
- reporter.Warn($"The file watcher observing '{watcher.BasePath}' encountered an error: {error.Message}");
+ reporter.Warn($"The file watcher observing '{watcher.WatchedDirectory}' encountered an error: {error.Message}");
}
}
@@ -90,29 +95,31 @@ private void DisposeWatcher(string directory)
watcher.Dispose();
}
- private void EnsureNotDisposed()
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(nameof(FileWatcher));
- }
- }
-
private static string EnsureTrailingSlash(string path)
=> (path is [.., var last] && last != Path.DirectorySeparatorChar) ? path + Path.DirectorySeparatorChar : path;
- public async Task GetChangedFileAsync(Action? startedWatching, CancellationToken cancellationToken)
- {
- StartWatching();
+ public Task WaitForFileChangeAsync(Action? startedWatching, CancellationToken cancellationToken)
+ => WaitForFileChangeAsync(
+ changeFilter: (path, kind) => new ChangedFile(new FileItem() { FilePath = path, ContainingProjectPaths = [] }, kind),
+ startedWatching,
+ cancellationToken);
+ public Task WaitForFileChangeAsync(IReadOnlyDictionary fileSet, Action? startedWatching, CancellationToken cancellationToken)
+ => WaitForFileChangeAsync(
+ changeFilter: (path, kind) => fileSet.TryGetValue(path, out var fileItem) ? new ChangedFile(fileItem, kind) : null,
+ startedWatching,
+ cancellationToken);
+
+ public async Task WaitForFileChangeAsync(Func changeFilter, Action? startedWatching, CancellationToken cancellationToken)
+ {
var fileChangedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
cancellationToken.Register(() => fileChangedSource.TrySetResult(null));
void FileChangedCallback(string path, ChangeKind kind)
{
- if (fileSet.TryGetValue(path, out var fileItem))
+ if (changeFilter(path, kind) is { } changedFile)
{
- fileChangedSource.TrySetResult(new ChangedFile(fileItem, kind));
+ fileChangedSource.TrySetResult(changedFile);
}
}
@@ -132,14 +139,21 @@ void FileChangedCallback(string path, ChangeKind kind)
return changedFile;
}
- public static async ValueTask WaitForFileChangeAsync(string path, IReporter reporter, CancellationToken cancellationToken)
+ public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter reporter, Action? startedWatching, CancellationToken cancellationToken)
{
- var fileSet = new Dictionary() { { path, new FileItem { FilePath = path } } };
+ using var watcher = new FileWatcher(reporter);
- using var watcher = new FileWatcher(fileSet, reporter);
- await watcher.GetChangedFileAsync(startedWatching: null, cancellationToken);
+ watcher.WatchDirectories([Path.GetDirectoryName(filePath)!]);
- reporter.Output($"File changed: {path}");
+ var fileChange = await watcher.WaitForFileChangeAsync(
+ changeFilter: (path, kind) => path == filePath ? new ChangedFile(new FileItem { FilePath = path, ContainingProjectPaths = [] }, kind) : null,
+ startedWatching,
+ cancellationToken);
+
+ if (fileChange != null)
+ {
+ reporter.Output($"File changed: {filePath}");
+ }
}
}
}
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs
index 5fef3b698624..ca4cd28c9171 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher.Internal;
+namespace Microsoft.DotNet.Watch;
internal enum ChangeKind
{
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/EventBasedDirectoryWatcher.cs
similarity index 79%
rename from src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs
rename to src/BuiltInTools/dotnet-watch/Internal/FileWatcher/EventBasedDirectoryWatcher.cs
index 7040fe1a0763..508475247cc7 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/DotnetFileWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/EventBasedDirectoryWatcher.cs
@@ -2,48 +2,35 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Internal
+namespace Microsoft.DotNet.Watch
{
- internal class DotnetFileWatcher : IFileSystemWatcher
+ internal sealed class EventBasedDirectoryWatcher : IDirectoryWatcher
{
- internal Action? Logger { get; set; }
+ public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
- private volatile bool _disposed;
+ public event EventHandler? OnError;
+
+ public string WatchedDirectory { get; }
- private readonly Func _watcherFactory;
+ internal Action? Logger { get; set; }
+
+ private volatile bool _disposed;
private FileSystemWatcher? _fileSystemWatcher;
private readonly object _createLock = new();
- public DotnetFileWatcher(string watchedDirectory)
- : this(watchedDirectory, DefaultWatcherFactory)
+ internal EventBasedDirectoryWatcher(string watchedDirectory)
{
- }
-
- internal DotnetFileWatcher(string watchedDirectory, Func fileSystemWatcherFactory)
- {
- Ensure.NotNull(fileSystemWatcherFactory, nameof(fileSystemWatcherFactory));
- Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
-
- BasePath = watchedDirectory;
- _watcherFactory = fileSystemWatcherFactory;
+ WatchedDirectory = watchedDirectory;
CreateFileSystemWatcher();
}
- public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
-
- public event EventHandler? OnError;
-
- public string BasePath { get; }
-
- private static FileSystemWatcher DefaultWatcherFactory(string watchedDirectory)
+ public void Dispose()
{
- Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
-
- return new FileSystemWatcher(watchedDirectory);
+ _disposed = true;
+ DisposeInnerWatcher();
}
private void WatcherErrorHandler(object sender, ErrorEventArgs e)
@@ -62,7 +49,7 @@ private void WatcherErrorHandler(object sender, ErrorEventArgs e)
// Win32Exception may be triggered when setting EnableRaisingEvents on a file system type
// that is not supported, such as a network share. Don't attempt to recreate the watcher
// in this case as it will cause a StackOverflowException
- if (!(exception is Win32Exception))
+ if (exception is not Win32Exception)
{
// Recreate the watcher if it is a recoverable error.
CreateFileSystemWatcher();
@@ -147,8 +134,10 @@ private void CreateFileSystemWatcher()
DisposeInnerWatcher();
}
- _fileSystemWatcher = _watcherFactory(BasePath);
- _fileSystemWatcher.IncludeSubdirectories = true;
+ _fileSystemWatcher = new FileSystemWatcher(WatchedDirectory)
+ {
+ IncludeSubdirectories = true
+ };
_fileSystemWatcher.Created += WatcherAddedHandler;
_fileSystemWatcher.Deleted += WatcherDeletedHandler;
@@ -162,7 +151,7 @@ private void CreateFileSystemWatcher()
private void DisposeInnerWatcher()
{
- if ( _fileSystemWatcher != null )
+ if (_fileSystemWatcher != null)
{
_fileSystemWatcher.EnableRaisingEvents = false;
@@ -181,11 +170,5 @@ public bool EnableRaisingEvents
get => _fileSystemWatcher!.EnableRaisingEvents;
set => _fileSystemWatcher!.EnableRaisingEvents = value;
}
-
- public void Dispose()
- {
- _disposed = true;
- DisposeInnerWatcher();
- }
}
}
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/FileWatcherFactory.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/FileWatcherFactory.cs
index b05242d80ecd..7baa775806eb 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/FileWatcherFactory.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/FileWatcherFactory.cs
@@ -1,18 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher.Internal
+namespace Microsoft.DotNet.Watch
{
internal static class FileWatcherFactory
{
- public static IFileSystemWatcher CreateWatcher(string watchedDirectory)
+ public static IDirectoryWatcher CreateWatcher(string watchedDirectory)
=> CreateWatcher(watchedDirectory, EnvironmentVariables.IsPollingEnabled);
- public static IFileSystemWatcher CreateWatcher(string watchedDirectory, bool usePollingWatcher)
+ public static IDirectoryWatcher CreateWatcher(string watchedDirectory, bool usePollingWatcher)
{
return usePollingWatcher ?
- new PollingFileWatcher(watchedDirectory) :
- new DotnetFileWatcher(watchedDirectory);
+ new PollingDirectoryWatcher(watchedDirectory) :
+ new EventBasedDirectoryWatcher(watchedDirectory);
}
}
}
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IDirectoryWatcher.cs
similarity index 71%
rename from src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs
rename to src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IDirectoryWatcher.cs
index ebdef49913ff..6b1eb73671a9 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IFileSystemWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/IDirectoryWatcher.cs
@@ -1,15 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher.Internal
+namespace Microsoft.DotNet.Watch
{
- internal interface IFileSystemWatcher : IDisposable
+ internal interface IDirectoryWatcher : IDisposable
{
event EventHandler<(string filePath, ChangeKind kind)> OnFileChange;
event EventHandler OnError;
- string BasePath { get; }
+ string WatchedDirectory { get; }
bool EnableRaisingEvents { get; set; }
}
diff --git a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingDirectoryWatcher.cs
similarity index 78%
rename from src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs
rename to src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingDirectoryWatcher.cs
index 2df2004ee84b..1477e7239783 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingFileWatcher.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/FileWatcher/PollingDirectoryWatcher.cs
@@ -2,37 +2,44 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Internal
+namespace Microsoft.DotNet.Watch
{
- internal class PollingFileWatcher : IFileSystemWatcher
+ internal sealed class PollingDirectoryWatcher : IDirectoryWatcher
{
// The minimum interval to rerun the scan
private static readonly TimeSpan _minRunInternal = TimeSpan.FromSeconds(.5);
private readonly DirectoryInfo _watchedDirectory;
- private Dictionary _knownEntities = new();
- private Dictionary _tempDictionary = new();
- private Dictionary _changes = new();
+ private Dictionary _knownEntities = [];
+ private Dictionary _tempDictionary = [];
+ private readonly Dictionary _changes = [];
private Thread _pollingThread;
private bool _raiseEvents;
- private bool _disposed;
+ private volatile bool _disposed;
- public PollingFileWatcher(string watchedDirectory)
+ public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
+
+#pragma warning disable CS0067 // not used
+ public event EventHandler? OnError;
+#pragma warning restore
+
+ public string WatchedDirectory { get; }
+
+ public PollingDirectoryWatcher(string watchedDirectory)
{
Ensure.NotNullOrEmpty(watchedDirectory, nameof(watchedDirectory));
_watchedDirectory = new DirectoryInfo(watchedDirectory);
- BasePath = _watchedDirectory.FullName;
+ WatchedDirectory = _watchedDirectory.FullName;
_pollingThread = new Thread(new ThreadStart(PollingLoop))
{
IsBackground = true,
- Name = nameof(PollingFileWatcher)
+ Name = nameof(PollingDirectoryWatcher)
};
CreateKnownFilesSnapshot();
@@ -40,20 +47,18 @@ public PollingFileWatcher(string watchedDirectory)
_pollingThread.Start();
}
- public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
-
-#pragma warning disable CS0067 // not used
- public event EventHandler? OnError;
-#pragma warning restore
-
- public string BasePath { get; }
+ public void Dispose()
+ {
+ EnableRaisingEvents = false;
+ _disposed = true;
+ }
public bool EnableRaisingEvents
{
get => _raiseEvents;
set
{
- EnsureNotDisposed();
+ ObjectDisposedException.ThrowIf(_disposed, this);
_raiseEvents = value;
}
}
@@ -90,9 +95,9 @@ private void CreateKnownFilesSnapshot()
{
_knownEntities.Clear();
- ForeachEntityInDirectory(_watchedDirectory, f =>
+ ForeachEntityInDirectory(_watchedDirectory, fileInfo =>
{
- _knownEntities.Add(f.FullName, new FileMeta(f));
+ _knownEntities.Add(fileInfo.FullName, new FileMeta(fileInfo, foundAgain: false));
});
}
@@ -100,14 +105,14 @@ private void CheckForChangedFiles()
{
_changes.Clear();
- ForeachEntityInDirectory(_watchedDirectory, f =>
+ ForeachEntityInDirectory(_watchedDirectory, fileInfo =>
{
- var fullFilePath = f.FullName;
+ var fullFilePath = fileInfo.FullName;
if (!_knownEntities.ContainsKey(fullFilePath))
{
// New file or directory
- RecordChange(f, ChangeKind.Add);
+ RecordChange(fileInfo, ChangeKind.Add);
}
else
{
@@ -116,10 +121,10 @@ private void CheckForChangedFiles()
try
{
if (!fileMeta.FileInfo.Attributes.HasFlag(FileAttributes.Directory) &&
- fileMeta.FileInfo.LastWriteTime != f.LastWriteTime)
+ fileMeta.FileInfo.LastWriteTime != fileInfo.LastWriteTime)
{
// File changed
- RecordChange(f, ChangeKind.Update);
+ RecordChange(fileInfo, ChangeKind.Update);
}
_knownEntities[fullFilePath] = new FileMeta(fileMeta.FileInfo, foundAgain: true);
@@ -130,7 +135,7 @@ private void CheckForChangedFiles()
}
}
- _tempDictionary.Add(f.FullName, new FileMeta(f));
+ _tempDictionary.Add(fileInfo.FullName, new FileMeta(fileInfo, foundAgain: false));
});
foreach (var file in _knownEntities)
@@ -211,31 +216,10 @@ private void NotifyChanges()
}
}
- private void EnsureNotDisposed()
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(nameof(PollingFileWatcher));
- }
- }
-
- public void Dispose()
- {
- EnableRaisingEvents = false;
- _disposed = true;
- }
-
- private struct FileMeta
+ private readonly struct FileMeta(FileSystemInfo fileInfo, bool foundAgain)
{
- public FileMeta(FileSystemInfo fileInfo, bool foundAgain = false)
- {
- FileInfo = fileInfo;
- FoundAgain = foundAgain;
- }
-
- public FileSystemInfo FileInfo;
-
- public bool FoundAgain;
+ public readonly FileSystemInfo FileInfo = fileInfo;
+ public readonly bool FoundAgain = foundAgain;
}
}
}
diff --git a/src/BuiltInTools/dotnet-watch/Internal/HotReloadFileSetWatcher.cs b/src/BuiltInTools/dotnet-watch/Internal/HotReloadFileSetWatcher.cs
deleted file mode 100644
index 255f7c1474f1..000000000000
--- a/src/BuiltInTools/dotnet-watch/Internal/HotReloadFileSetWatcher.cs
+++ /dev/null
@@ -1,178 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-
-using System.Collections.Concurrent;
-using System.Diagnostics;
-using Microsoft.Extensions.Tools.Internal;
-
-namespace Microsoft.DotNet.Watcher.Internal
-{
- internal sealed class HotReloadFileSetWatcher(IReadOnlyDictionary fileSet, DateTime buildCompletionTime, IReporter reporter, TestFlags testFlags) : IDisposable
- {
- private static readonly TimeSpan s_debounceInterval = TimeSpan.FromMilliseconds(50);
- private static readonly DateTime s_fileNotExistFileTime = DateTime.FromFileTime(0);
-
- private readonly FileWatcher _fileWatcher = new(fileSet, reporter);
- private readonly object _changedFilesLock = new();
- private readonly ConcurrentDictionary _changedFiles = new(StringComparer.Ordinal);
-
- private TaskCompletionSource? _tcs;
- private bool _initialized;
- private bool _disposed;
-
- public void Dispose()
- {
- _disposed = true;
- _fileWatcher.Dispose();
- }
-
- public void UpdateBuildCompletionTime(DateTime value)
- {
- lock (_changedFilesLock)
- {
- buildCompletionTime = value;
- _changedFiles.Clear();
- }
- }
-
- private void EnsureInitialized()
- {
- if (_initialized)
- {
- return;
- }
-
- _initialized = true;
-
- _fileWatcher.StartWatching();
- _fileWatcher.OnFileChange += FileChangedCallback;
-
- var waitingForChanges = MessageDescriptor.WaitingForChanges;
- if (testFlags.HasFlag(TestFlags.ElevateWaitingForChangesMessageSeverity))
- {
- waitingForChanges = waitingForChanges with { Severity = MessageSeverity.Output };
- }
-
- reporter.Report(waitingForChanges);
-
- Task.Factory.StartNew(async () =>
- {
- // Debounce / polling loop
- while (!_disposed)
- {
- await Task.Delay(s_debounceInterval);
- if (_changedFiles.IsEmpty)
- {
- continue;
- }
-
- var tcs = Interlocked.Exchange(ref _tcs, null!);
- if (tcs is null)
- {
- continue;
- }
-
- ChangedFile[] changedFiles;
- lock (_changedFilesLock)
- {
- changedFiles = _changedFiles.Values.ToArray();
- _changedFiles.Clear();
- }
-
- if (changedFiles is [])
- {
- continue;
- }
-
- tcs.TrySetResult(changedFiles);
- }
-
- }, default, TaskCreationOptions.LongRunning, TaskScheduler.Default);
-
- void FileChangedCallback(string path, ChangeKind kind)
- {
- // only handle file changes:
- if (Directory.Exists(path))
- {
- return;
- }
-
- if (kind != ChangeKind.Delete)
- {
- try
- {
- // Do not report changes to files that happened during build:
- var creationTime = File.GetCreationTimeUtc(path);
- var writeTime = File.GetLastWriteTimeUtc(path);
-
- if (creationTime == s_fileNotExistFileTime || writeTime == s_fileNotExistFileTime)
- {
- // file might have been deleted since we received the event
- kind = ChangeKind.Delete;
- }
- else if (creationTime.Ticks < buildCompletionTime.Ticks && writeTime.Ticks < buildCompletionTime.Ticks)
- {
- reporter.Verbose(
- $"Ignoring file change during build: {kind} '{path}' " +
- $"(created {FormatTimestamp(creationTime)} and written {FormatTimestamp(writeTime)} before {FormatTimestamp(buildCompletionTime)}).");
-
- return;
- }
- else if (writeTime > creationTime)
- {
- reporter.Verbose($"File change: {kind} '{path}' (written {FormatTimestamp(writeTime)} after {FormatTimestamp(buildCompletionTime)}).");
- }
- else
- {
- reporter.Verbose($"File change: {kind} '{path}' (created {FormatTimestamp(creationTime)} after {FormatTimestamp(buildCompletionTime)}).");
- }
- }
- catch (Exception e)
- {
- reporter.Verbose($"Ignoring file '{path}' due to access error: {e.Message}.");
- return;
- }
- }
-
- if (kind == ChangeKind.Delete)
- {
- reporter.Verbose($"File '{path}' deleted after {FormatTimestamp(buildCompletionTime)}.");
- }
-
- if (kind == ChangeKind.Add)
- {
- lock (_changedFilesLock)
- {
- _changedFiles.TryAdd(path, new ChangedFile(new FileItem { FilePath = path }, kind));
- }
- }
- else if (fileSet.TryGetValue(path, out var fileItem))
- {
- lock (_changedFilesLock)
- {
- _changedFiles.TryAdd(path, new ChangedFile(fileItem, kind));
- }
- }
- }
- }
-
- public Task GetChangedFilesAsync(CancellationToken cancellationToken, bool forceWaitForNewUpdate = false)
- {
- EnsureInitialized();
-
- var tcs = _tcs;
- if (!forceWaitForNewUpdate && tcs is not null)
- {
- return tcs.Task;
- }
-
- _tcs = tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
- cancellationToken.Register(() => tcs.TrySetResult(null));
- return tcs.Task;
- }
-
- internal static string FormatTimestamp(DateTime time)
- => time.ToString("HH:mm:ss.fffffff");
- }
-}
diff --git a/src/BuiltInTools/dotnet-watch/Internal/IConsole.cs b/src/BuiltInTools/dotnet-watch/Internal/IConsole.cs
index b166d6ce93b8..0860a30eeae6 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/IConsole.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/IConsole.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.Extensions.Tools.Internal
+namespace Microsoft.DotNet.Watch
{
///
/// This API supports infrastructure and is not intended to be used
@@ -9,14 +9,9 @@ namespace Microsoft.Extensions.Tools.Internal
///
internal interface IConsole
{
- event ConsoleCancelEventHandler CancelKeyPress;
event Action KeyPressed;
TextWriter Out { get; }
TextWriter Error { get; }
- TextReader In { get; }
- bool IsInputRedirected { get; }
- bool IsOutputRedirected { get; }
- bool IsErrorRedirected { get; }
ConsoleColor ForegroundColor { get; set; }
void ResetColor();
void Clear();
diff --git a/src/BuiltInTools/dotnet-watch/Internal/IReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/IReporter.cs
index 892d21e5b2e0..932c92d59fdd 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/IReporter.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/IReporter.cs
@@ -4,11 +4,8 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Build.Graph;
-using Microsoft.Build.Tasks;
-using Microsoft.DotNet.Watcher;
-using Microsoft.DotNet.Watcher.Internal;
-namespace Microsoft.Extensions.Tools.Internal
+namespace Microsoft.DotNet.Watch
{
internal enum MessageSeverity
{
@@ -55,16 +52,15 @@ public bool TryGetMessage(string? prefix, object?[] args, [NotNullWhen(true)] ou
// predefined messages used for testing:
public static readonly MessageDescriptor HotReloadSessionStarting = new(Format: null, Emoji: null, MessageSeverity.None, s_id++);
public static readonly MessageDescriptor HotReloadSessionStarted = new("Hot reload session started.", "🔥", MessageSeverity.Verbose, s_id++);
- public static readonly MessageDescriptor HotReloadSessionEnded = new("Hot reload session ended.", "🔥", MessageSeverity.Verbose, s_id++);
+ public static readonly MessageDescriptor ProjectBaselinesUpdated = new("Project baselines updated.", "🔥", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor FixBuildError = new("Fix the error to continue or press Ctrl+C to exit.", "⌚", MessageSeverity.Warning, s_id++);
public static readonly MessageDescriptor WaitingForChanges = new("Waiting for changes", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor LaunchedProcess = new("Launched '{0}' with arguments '{1}': process id {2}", "🚀", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor KillingProcess = new("Killing process {0}", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor HotReloadChangeHandled = new("Hot reload change handled in {0}ms.", "🔥", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor HotReloadSucceeded = new("Hot reload succeeded.", "🔥", MessageSeverity.Output, s_id++);
- public static readonly MessageDescriptor BuildCompleted = new("Build completed.", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor UpdatesApplied = new("Updates applied: {0} out of {1}.", "🔥", MessageSeverity.Verbose, s_id++);
- public static readonly MessageDescriptor WaitingForFileChangeBeforeRestarting = new("Waiting for a file to change before restarting dotnet...", "⏳", MessageSeverity.Warning, s_id++);
+ public static readonly MessageDescriptor WaitingForFileChangeBeforeRestarting = new("Waiting for a file to change before restarting ...", "⏳", MessageSeverity.Warning, s_id++);
public static readonly MessageDescriptor WatchingWithHotReload = new("Watching with Hot Reload.", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor RestartInProgress = new("Restart in progress.", "🔄", MessageSeverity.Output, s_id++);
public static readonly MessageDescriptor RestartRequested = new("Restart requested.", "🔄", MessageSeverity.Output, s_id++);
diff --git a/src/BuiltInTools/dotnet-watch/Internal/MSBuildFileSetResult.cs b/src/BuiltInTools/dotnet-watch/Internal/MSBuildFileSetResult.cs
index c214abea2938..0067b8542d34 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/MSBuildFileSetResult.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/MSBuildFileSetResult.cs
@@ -3,7 +3,7 @@
using System.Runtime.Serialization;
-namespace Microsoft.DotNet.Watcher.Internal
+namespace Microsoft.DotNet.Watch
{
[DataContract]
internal sealed class MSBuildFileSetResult
diff --git a/src/BuiltInTools/dotnet-watch/Internal/MsBuildFileSetFactory.cs b/src/BuiltInTools/dotnet-watch/Internal/MsBuildFileSetFactory.cs
index 85405a76f4a7..e2be958e813a 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/MsBuildFileSetFactory.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/MsBuildFileSetFactory.cs
@@ -4,10 +4,8 @@
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Build.Graph;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
///
/// Used to collect a set of files to watch.
@@ -20,8 +18,7 @@ namespace Microsoft.DotNet.Watcher.Tools
///
internal class MSBuildFileSetFactory(
string rootProjectFile,
- string? targetFramework,
- IReadOnlyList<(string name, string value)> buildProperties,
+ IEnumerable buildArguments,
EnvironmentOptions environmentOptions,
IReporter reporter)
{
@@ -58,28 +55,17 @@ internal class MSBuildFileSetFactory(
var exitCode = await ProcessRunner.RunAsync(processSpec, reporter, isUserApplication: false, launchResult: null, cancellationToken);
- if (exitCode != 0 || !File.Exists(watchList))
- {
- reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(rootProjectFile)}'");
+ var success = exitCode == 0 && File.Exists(watchList);
+ if (!success)
+ {
+ reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(rootProjectFile)}'.");
reporter.Output($"MSBuild output from target '{TargetName}':");
- reporter.Output(string.Empty);
-
- foreach (var (line, isError) in capturedOutput)
- {
- var message = " " + line;
- if (isError)
- {
- reporter.Error(message);
- }
- else
- {
- reporter.Output(message);
- }
- }
-
- reporter.Output(string.Empty);
+ }
+ BuildUtilities.ReportBuildOutput(reporter, capturedOutput, verboseOutput: success);
+ if (!success)
+ {
return null;
}
@@ -167,10 +153,10 @@ private IReadOnlyList GetMSBuildArguments(string watchListFilePath)
if (environmentOptions.TestFlags.HasFlag(TestFlags.RunningAsTest))
#endif
{
- arguments.Add("/bl:DotnetWatch.GenerateWatchList.binlog");
+ arguments.Add($"/bl:{Path.Combine(environmentOptions.TestOutput, "DotnetWatch.GenerateWatchList.binlog")}");
}
- arguments.AddRange(buildProperties.Select(p => $"/p:{p.name}={p.value}"));
+ arguments.AddRange(buildArguments);
// Set dotnet-watch reserved properties after the user specified propeties,
// so that the former take precedence.
@@ -180,11 +166,6 @@ private IReadOnlyList GetMSBuildArguments(string watchListFilePath)
arguments.Add("/p:DotNetWatchContentFiles=false");
}
- if (targetFramework != null)
- {
- arguments.Add("/p:TargetFramework=" + targetFramework);
- }
-
arguments.Add("/p:_DotNetWatchListFile=" + watchListFilePath);
arguments.Add("/p:DotNetWatchBuild=true"); // extensibility point for users
arguments.Add("/p:DesignTimeBuild=true"); // don't do expensive things
@@ -215,12 +196,8 @@ private static string FindTargetsFile()
internal ProjectGraph? TryLoadProjectGraph(bool projectGraphRequired)
{
var globalOptions = new Dictionary();
- if (targetFramework != null)
- {
- globalOptions.Add("TargetFramework", targetFramework);
- }
- foreach (var (name, value) in buildProperties)
+ foreach (var (name, value) in CommandLineOptions.ParseBuildProperties(buildArguments))
{
globalOptions[name] = value;
}
diff --git a/src/BuiltInTools/dotnet-watch/Internal/MsBuildProjectFinder.cs b/src/BuiltInTools/dotnet-watch/Internal/MsBuildProjectFinder.cs
index 0ee164e0233b..eb7ba724fa5c 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/MsBuildProjectFinder.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/MsBuildProjectFinder.cs
@@ -2,10 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
-using Microsoft.DotNet.Watcher.Tools;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Internal
+namespace Microsoft.DotNet.Watch
{
internal class MsBuildProjectFinder
{
diff --git a/src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs
index 6812973c3b80..4e9ead24dcce 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs
@@ -2,9 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Graph;
-using Microsoft.DotNet.Watcher.Internal;
-namespace Microsoft.Extensions.Tools.Internal
+namespace Microsoft.DotNet.Watch
{
///
/// This API supports infrastructure and is not intended to be used
diff --git a/src/BuiltInTools/dotnet-watch/Internal/OutputLine.cs b/src/BuiltInTools/dotnet-watch/Internal/OutputLine.cs
index f80037321819..be29eb65cfc8 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/OutputLine.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/OutputLine.cs
@@ -1,6 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher.Internal;
+namespace Microsoft.DotNet.Watch;
internal readonly record struct OutputLine(string Content, bool IsError);
diff --git a/src/BuiltInTools/dotnet-watch/Internal/PhysicalConsole.cs b/src/BuiltInTools/dotnet-watch/Internal/PhysicalConsole.cs
index c78bb694b219..3e3e14195dec 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/PhysicalConsole.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/PhysicalConsole.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.Extensions.Tools.Internal
+namespace Microsoft.DotNet.Watch
{
///
/// This API supports infrastructure and is not intended to be used
@@ -9,52 +9,82 @@ namespace Microsoft.Extensions.Tools.Internal
///
internal sealed class PhysicalConsole : IConsole
{
- private readonly List> _keyPressedListeners = new();
+ public const char CtrlC = '\x03';
+ public const char CtrlR = '\x12';
- private PhysicalConsole()
+ public event Action? KeyPressed;
+
+ public PhysicalConsole(TestFlags testFlags)
{
Console.OutputEncoding = Encoding.UTF8;
- Console.CancelKeyPress += (o, e) =>
+
+ bool readFromStdin;
+ if (testFlags.HasFlag(TestFlags.ReadKeyFromStdin))
+ {
+ readFromStdin = true;
+ }
+ else
{
- CancelKeyPress?.Invoke(o, e);
- };
+ try
+ {
+ Console.TreatControlCAsInput = true;
+ readFromStdin = false;
+ }
+ catch
+ {
+ // fails when stdin is redirected
+ readFromStdin = true;
+ }
+ }
+
+ _ = readFromStdin ? ListenToStandardInputAsync() : ListenToConsoleKeyPressAsync();
}
- public event Action KeyPressed
+ private async Task ListenToStandardInputAsync()
{
- add
+ using var stream = Console.OpenStandardInput();
+ var buffer = new byte[1];
+
+ while (true)
{
- _keyPressedListeners.Add(value);
- ListenToConsoleKeyPress();
- }
+ var bytesRead = await stream.ReadAsync(buffer, CancellationToken.None);
+ if (bytesRead != 1)
+ {
+ break;
+ }
+
+ var c = (char)buffer[0];
- remove => _keyPressedListeners.Remove(value);
+ // handle all input keys that watcher might consume:
+ var key = c switch
+ {
+ CtrlC => new ConsoleKeyInfo('C', ConsoleKey.C, shift: false, alt: false, control: true),
+ CtrlR => new ConsoleKeyInfo('R', ConsoleKey.R, shift: false, alt: false, control: true),
+ >= 'a' and <= 'z' => new ConsoleKeyInfo(c, ConsoleKey.A + (c - 'a'), shift: false, alt: false, control: false),
+ >= 'A' and <= 'Z' => new ConsoleKeyInfo(c, ConsoleKey.A + (c - 'A'), shift: true, alt: false, control: false),
+ _ => default
+ };
+
+ if (key.Key != ConsoleKey.None)
+ {
+ KeyPressed?.Invoke(key);
+ }
+ }
}
- private void ListenToConsoleKeyPress()
- {
- Task.Factory.StartNew(() =>
+ private Task ListenToConsoleKeyPressAsync()
+ => Task.Factory.StartNew(() =>
{
while (true)
{
var key = Console.ReadKey(intercept: true);
- for (var i = 0; i < _keyPressedListeners.Count; i++)
- {
- _keyPressedListeners[i](key);
- }
+ KeyPressed?.Invoke(key);
}
}, TaskCreationOptions.LongRunning);
- }
-
- public static IConsole Singleton { get; } = new PhysicalConsole();
- public event ConsoleCancelEventHandler? CancelKeyPress;
public TextWriter Error => Console.Error;
- public TextReader In => Console.In;
public TextWriter Out => Console.Out;
- public bool IsInputRedirected => Console.IsInputRedirected;
- public bool IsOutputRedirected => Console.IsOutputRedirected;
- public bool IsErrorRedirected => Console.IsErrorRedirected;
+
public ConsoleColor ForegroundColor
{
get => Console.ForegroundColor;
diff --git a/src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs b/src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs
index 06d9fd194327..40a42278e38f 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs
@@ -3,9 +3,8 @@
using System.Diagnostics;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Internal
+namespace Microsoft.DotNet.Watch
{
internal sealed class ProcessRunner
{
@@ -246,19 +245,11 @@ private static void TerminateProcess(Process process, ProcessState state, IRepor
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- process.Kill();
+ TerminateWindowsProcess(process, state, reporter);
}
else
{
- [DllImport("libc", SetLastError = true, EntryPoint = "kill")]
- static extern int sys_kill(int pid, int sig);
-
- var result = sys_kill(state.ProcessId, state.ForceExit ? SIGKILL : SIGTERM);
- if (result != 0)
- {
- var error = Marshal.GetLastPInvokeError();
- reporter.Verbose($"Error while sending SIGTERM to process {state.ProcessId}: {Marshal.GetPInvokeErrorMessage(error)} (code {error}).");
- }
+ TerminateUnixProcess(state, reporter);
}
reporter.Verbose($"Process {state.ProcessId} killed.");
@@ -272,5 +263,51 @@ private static void TerminateProcess(Process process, ProcessState state, IRepor
#endif
}
}
+
+ private static void TerminateWindowsProcess(Process process, ProcessState state, IReporter reporter)
+ {
+ // Needs API: https://github.com/dotnet/runtime/issues/109432
+ // Code below does not work because the process creation needs CREATE_NEW_PROCESS_GROUP flag.
+#if TODO
+ if (!state.ForceExit)
+ {
+ const uint CTRL_C_EVENT = 0;
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern bool AttachConsole(uint dwProcessId);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern bool FreeConsole();
+
+ if (AttachConsole((uint)state.ProcessId) &&
+ GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0) &&
+ FreeConsole())
+ {
+ return;
+ }
+
+ var error = Marshal.GetLastPInvokeError();
+ reporter.Verbose($"Failed to send Ctrl+C to process {state.ProcessId}: {Marshal.GetPInvokeErrorMessage(error)} (code {error})");
+ }
+#endif
+
+ process.Kill();
+ }
+
+ private static void TerminateUnixProcess(ProcessState state, IReporter reporter)
+ {
+ [DllImport("libc", SetLastError = true, EntryPoint = "kill")]
+ static extern int sys_kill(int pid, int sig);
+
+ var result = sys_kill(state.ProcessId, state.ForceExit ? SIGKILL : SIGTERM);
+ if (result != 0)
+ {
+ var error = Marshal.GetLastPInvokeError();
+ reporter.Verbose($"Error while sending SIGTERM to process {state.ProcessId}: {Marshal.GetPInvokeErrorMessage(error)} (code {error}).");
+ }
+ }
}
}
diff --git a/src/BuiltInTools/dotnet-watch/Internal/ProjectSpecificReporter.cs b/src/BuiltInTools/dotnet-watch/Internal/ProjectSpecificReporter.cs
index a46b9d078904..e2746996b589 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/ProjectSpecificReporter.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/ProjectSpecificReporter.cs
@@ -2,10 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Graph;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal sealed class ProjectSpecificReporter(ProjectGraphNode node, IReporter underlyingReporter) : IReporter
{
diff --git a/src/BuiltInTools/dotnet-watch/Internal/ReporterTraceListener.cs b/src/BuiltInTools/dotnet-watch/Internal/ReporterTraceListener.cs
index 756b62be0160..f67f58af6708 100644
--- a/src/BuiltInTools/dotnet-watch/Internal/ReporterTraceListener.cs
+++ b/src/BuiltInTools/dotnet-watch/Internal/ReporterTraceListener.cs
@@ -2,9 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal class ReporterTraceListener(IReporter reporter, string emoji) : TraceListener
{
diff --git a/src/BuiltInTools/dotnet-watch/LaunchSettingsProfile.cs b/src/BuiltInTools/dotnet-watch/LaunchSettingsProfile.cs
index 0579ab45f0eb..c1ee9346924f 100644
--- a/src/BuiltInTools/dotnet-watch/LaunchSettingsProfile.cs
+++ b/src/BuiltInTools/dotnet-watch/LaunchSettingsProfile.cs
@@ -4,9 +4,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-using Microsoft.Extensions.Tools.Internal;
-namespace Microsoft.DotNet.Watcher.Tools
+namespace Microsoft.DotNet.Watch
{
internal sealed class LaunchSettingsProfile
{
diff --git a/src/BuiltInTools/dotnet-watch/ProcessLaunchResult.cs b/src/BuiltInTools/dotnet-watch/ProcessLaunchResult.cs
index 3c58c69946a9..6084bc11b213 100644
--- a/src/BuiltInTools/dotnet-watch/ProcessLaunchResult.cs
+++ b/src/BuiltInTools/dotnet-watch/ProcessLaunchResult.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher
+namespace Microsoft.DotNet.Watch
{
internal sealed class ProcessLaunchResult
{
diff --git a/src/BuiltInTools/dotnet-watch/ProcessSpec.cs b/src/BuiltInTools/dotnet-watch/ProcessSpec.cs
index c6b651c91b55..e8b9f7c53a7b 100644
--- a/src/BuiltInTools/dotnet-watch/ProcessSpec.cs
+++ b/src/BuiltInTools/dotnet-watch/ProcessSpec.cs
@@ -1,9 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.DotNet.Watcher.Internal;
-namespace Microsoft.DotNet.Watcher
+namespace Microsoft.DotNet.Watch
{
internal sealed class ProcessSpec
{
diff --git a/src/BuiltInTools/dotnet-watch/Program.cs b/src/BuiltInTools/dotnet-watch/Program.cs
index 75106f6e67f8..ffe38eea3a13 100644
--- a/src/BuiltInTools/dotnet-watch/Program.cs
+++ b/src/BuiltInTools/dotnet-watch/Program.cs
@@ -2,17 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Loader;
-using Microsoft.Build.Graph;
using Microsoft.Build.Locator;
-using Microsoft.CodeAnalysis.ChangeSignature;
-using Microsoft.DotNet.Watcher.Internal;
-using Microsoft.DotNet.Watcher.Tools;
-using Microsoft.Extensions.Tools.Internal;
-using IConsole = Microsoft.Extensions.Tools.Internal.IConsole;
-namespace Microsoft.DotNet.Watcher
+namespace Microsoft.DotNet.Watch
{
internal sealed class Program(IConsole console, IReporter reporter, ProjectOptions rootProjectOptions, CommandLineOptions options, EnvironmentOptions environmentOptions)
{
@@ -36,10 +31,12 @@ public static async Task Main(string[] args)
// Register listeners that load Roslyn-related assemblies from the `Roslyn/bincore` directory.
RegisterAssemblyResolutionEvents(sdkRootDirectory);
+ var environmentOptions = EnvironmentOptions.FromEnvironment();
+
var program = TryCreate(
args,
- PhysicalConsole.Singleton,
- EnvironmentOptions.FromEnvironment(),
+ new PhysicalConsole(environmentOptions.TestFlags),
+ environmentOptions,
EnvironmentVariables.VerboseCliOutput,
out var exitCode);
@@ -77,6 +74,11 @@ public static async Task Main(string[] args)
var workingDirectory = environmentOptions.WorkingDirectory;
reporter.Verbose($"Working directory: '{workingDirectory}'");
+ if (environmentOptions.TestFlags != TestFlags.None)
+ {
+ reporter.Verbose($"Test flags: {environmentOptions.TestFlags}");
+ }
+
string projectPath;
try
{
@@ -97,9 +99,28 @@ public static async Task Main(string[] args)
// internal for testing
internal async Task RunAsync()
{
+ var shutdownCancellationSourceDisposed = false;
var shutdownCancellationSource = new CancellationTokenSource();
var shutdownCancellationToken = shutdownCancellationSource.Token;
- console.CancelKeyPress += OnCancelKeyPress;
+
+ console.KeyPressed += key =>
+ {
+ if (!shutdownCancellationSourceDisposed && key.Modifiers.HasFlag(ConsoleModifiers.Control) && key.Key == ConsoleKey.C)
+ {
+ // if we already canceled, we force immediate shutdown:
+ var forceShutdown = shutdownCancellationSource.IsCancellationRequested;
+
+ if (!forceShutdown)
+ {
+ reporter.Report(MessageDescriptor.ShutdownRequested);
+ shutdownCancellationSource.Cancel();
+ }
+ else
+ {
+ Environment.Exit(0);
+ }
+ }
+ };
try
{
@@ -130,26 +151,9 @@ internal async Task RunAsync()
}
finally
{
- console.CancelKeyPress -= OnCancelKeyPress;
+ shutdownCancellationSourceDisposed = true;
shutdownCancellationSource.Dispose();
}
-
- void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs args)
- {
- // if we already canceled, we force immediate shutdown:
- var forceShutdown = shutdownCancellationSource.IsCancellationRequested;
-
- if (!forceShutdown)
- {
- reporter.Report(MessageDescriptor.ShutdownRequested);
- shutdownCancellationSource.Cancel();
- args.Cancel = true;
- }
- else
- {
- Environment.Exit(0);
- }
- }
}
// internal for testing
@@ -162,8 +166,7 @@ internal Watcher CreateWatcher(IRuntimeProcessLauncherFactory? runtimeProcessLau
var fileSetFactory = new MSBuildFileSetFactory(
rootProjectOptions.ProjectPath,
- rootProjectOptions.TargetFramework,
- rootProjectOptions.BuildProperties,
+ rootProjectOptions.BuildArguments,
environmentOptions,
reporter);
@@ -201,8 +204,7 @@ private async Task ListFilesAsync(CancellationToken cancellationToken)
{
var fileSetFactory = new MSBuildFileSetFactory(
rootProjectOptions.ProjectPath,
- rootProjectOptions.TargetFramework,
- rootProjectOptions.BuildProperties,
+ rootProjectOptions.BuildArguments,
environmentOptions,
reporter);
diff --git a/src/BuiltInTools/dotnet-watch/ProjectOptions.cs b/src/BuiltInTools/dotnet-watch/ProjectOptions.cs
index 1e231f807eb1..12b8b889f1f7 100644
--- a/src/BuiltInTools/dotnet-watch/ProjectOptions.cs
+++ b/src/BuiltInTools/dotnet-watch/ProjectOptions.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal sealed record ProjectOptions
{
@@ -9,7 +9,7 @@ internal sealed record ProjectOptions
public required string ProjectPath { get; init; }
public required string WorkingDirectory { get; init; }
public required string? TargetFramework { get; init; }
- public required IReadOnlyList<(string name, string value)> BuildProperties { get; init; }
+ public required IReadOnlyList BuildArguments { get; init; }
public required bool NoLaunchProfile { get; init; }
public required string? LaunchProfileName { get; init; }
diff --git a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
index dafd8c0ab7ef..de41cecc021a 100644
--- a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
+++ b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json
@@ -5,7 +5,10 @@
"commandLineArgs": "--verbose /bl:DotnetRun.binlog",
"workingDirectory": "$(RepoRoot)src\\Assets\\TestProjects\\BlazorWasmWithLibrary\\blazorwasm",
"environmentVariables": {
- "DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)"
+ "DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)",
+ "DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000",
+ "DCP_IDE_NOTIFICATION_TIMEOUT_SECONDS": "100000",
+ "DCP_IDE_NOTIFICATION_KEEPALIVE_SECONDS": "100000"
}
}
}
diff --git a/src/BuiltInTools/dotnet-watch/Utilities/BuildUtilities.cs b/src/BuiltInTools/dotnet-watch/Utilities/BuildUtilities.cs
new file mode 100644
index 000000000000..3c9d85e0be4b
--- /dev/null
+++ b/src/BuiltInTools/dotnet-watch/Utilities/BuildUtilities.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.RegularExpressions;
+
+namespace Microsoft.DotNet.Watch;
+
+internal static partial class BuildUtilities
+{
+ private static readonly Regex s_buildDiagnosticRegex = GetBuildDiagnosticRegex();
+
+ [GeneratedRegex(@"[^:]+: (error|warning) [A-Za-z]+[0-9]+: .+")]
+ private static partial Regex GetBuildDiagnosticRegex();
+
+ public static void ReportBuildOutput(IReporter reporter, IEnumerable buildOutput, bool verboseOutput)
+ {
+ const string BuildEmoji = "🔨";
+
+ foreach (var (line, isError) in buildOutput)
+ {
+ if (isError)
+ {
+ reporter.Error(line);
+ }
+ else if (s_buildDiagnosticRegex.Match(line) is { Success: true } match)
+ {
+ if (match.Groups[1].Value == "error")
+ {
+ reporter.Error(line);
+ }
+ else
+ {
+ reporter.Warn(line);
+ }
+ }
+ else if (verboseOutput)
+ {
+ reporter.Verbose(line, BuildEmoji);
+ }
+ else
+ {
+ reporter.Output(line, BuildEmoji);
+ }
+ }
+ }
+}
diff --git a/src/BuiltInTools/dotnet-watch/Utilities/Disposables.cs b/src/BuiltInTools/dotnet-watch/Utilities/Disposables.cs
index 88f5373cdaf0..7aa9de719419 100644
--- a/src/BuiltInTools/dotnet-watch/Utilities/Disposables.cs
+++ b/src/BuiltInTools/dotnet-watch/Utilities/Disposables.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal readonly record struct Disposables(List disposables) : IDisposable
{
diff --git a/src/BuiltInTools/dotnet-watch/Utilities/ProjectGraphNodeExtensions.cs b/src/BuiltInTools/dotnet-watch/Utilities/ProjectGraphNodeExtensions.cs
index 762811f2d372..9df4691e434c 100644
--- a/src/BuiltInTools/dotnet-watch/Utilities/ProjectGraphNodeExtensions.cs
+++ b/src/BuiltInTools/dotnet-watch/Utilities/ProjectGraphNodeExtensions.cs
@@ -1,10 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Immutable;
using Microsoft.Build.Graph;
using Microsoft.DotNet.Cli;
-namespace Microsoft.DotNet.Watcher;
+namespace Microsoft.DotNet.Watch;
internal static class ProjectGraphNodeExtensions
{
@@ -17,6 +18,9 @@ public static string GetTargetFramework(this ProjectGraphNode projectNode)
public static Version? GetTargetFrameworkVersion(this ProjectGraphNode projectNode)
=> EnvironmentVariableNames.TryParseTargetFrameworkVersion(projectNode.ProjectInstance.GetPropertyValue("TargetFrameworkVersion"));
+ public static ImmutableArray