diff --git a/Directory.Build.props b/Directory.Build.props
index e84845c5fe8b..358a629fabb2 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -66,9 +66,11 @@
$(NetCurrent)
net9.0
+
- $(NoWarn);NU1507;NU1202;NU5039
+ $(NoWarn);NU1701;NU1507;NU1202;NU5039
true
false
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 0ef1d2edf1c4..057e70d4eb7d 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -25,6 +25,16 @@
+
+
+
+
+ false
+ false
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 26224a9a7e62..21d623676105 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,23 +8,19 @@
+
-
-
+
-
-
-
-
@@ -58,7 +54,7 @@
-
+
@@ -79,11 +75,11 @@
-
+
-
+
-
+
@@ -148,9 +144,10 @@
+
-
-
+
+
-
+
diff --git a/eng/Version.Details.props b/eng/Version.Details.props
index 02ef66f064b4..f1955acb69a7 100644
--- a/eng/Version.Details.props
+++ b/eng/Version.Details.props
@@ -8,103 +8,103 @@ This file should be imported by eng/Versions.props
2.1.0
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 10.0.0-preview.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 10.0.0-preview.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
18.6.0-preview-26208-110
18.6.0-preview-26208-110
7.6.0-rc.20910
- 11.0.100-preview.4.26208.110
- 5.7.0-1.26208.110
- 5.7.0-1.26208.110
- 5.7.0-1.26208.110
- 5.7.0-1.26208.110
- 5.7.0-1.26208.110
- 5.7.0-1.26208.110
- 5.7.0-1.26208.110
- 5.7.0-1.26208.110
- 10.0.0-preview.26208.110
- 5.7.0-1.26208.110
- 5.7.0-1.26208.110
- 2.0.0-preview.1.26208.110
- 3.0.0-preview.4.26208.110
- 11.0.0-beta.26208.110
- 11.0.0-beta.26208.110
- 11.0.0-beta.26208.110
- 11.0.0-beta.26208.110
- 11.0.0-beta.26208.110
- 11.0.0-beta.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-beta.26208.110
- 11.0.0-beta.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 15.2.100-preview4.26208.110
- 11.0.0-preview.4.26208.110
- 5.7.0-1.26208.110
- 5.7.0-1.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
+ 11.0.100-preview.4.26208.106
+ 5.7.0-1.26208.106
+ 5.7.0-1.26208.106
+ 5.7.0-1.26208.106
+ 5.7.0-1.26208.106
+ 5.7.0-1.26208.106
+ 5.7.0-1.26208.106
+ 5.7.0-1.26208.106
+ 5.7.0-1.26208.106
+ 10.0.0-preview.26208.106
+ 5.7.0-1.26208.106
+ 5.7.0-1.26208.106
+ 2.0.0-preview.1.26208.106
+ 3.0.0-preview.4.26208.106
+ 11.0.0-beta.26208.106
+ 11.0.0-beta.26208.106
+ 11.0.0-beta.26208.106
+ 11.0.0-beta.26208.106
+ 11.0.0-beta.26208.106
+ 11.0.0-beta.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-beta.26208.106
+ 11.0.0-beta.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 15.2.100-preview4.26208.106
+ 11.0.0-preview.4.26208.106
+ 5.7.0-1.26208.106
+ 5.7.0-1.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
10.0.0-preview.7.25377.103
- 10.0.0-preview.26208.110
- 11.0.0-preview.4.26208.110
+ 10.0.0-preview.26208.106
+ 11.0.0-preview.4.26208.106
18.7.0-preview-26208-110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
- 11.0.100-preview.4.26208.110
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
+ 11.0.100-preview.4.26208.106
18.7.0-preview-26208-110
18.7.0-preview-26208-110
- 3.3.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
+ 3.3.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
7.6.0-rc.20910
7.6.0-rc.20910
7.6.0-rc.20910
@@ -121,28 +121,28 @@ This file should be imported by eng/Versions.props
7.6.0-rc.20910
7.6.0-rc.20910
7.6.0-rc.20910
- 11.0.0-preview.4.26208.110
- 3.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
- 11.0.0-preview.4.26208.110
+ 11.0.0-preview.4.26208.106
+ 3.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
+ 11.0.0-preview.4.26208.106
2.3.0-preview.26203.3
4.3.0-preview.26203.3
@@ -205,7 +205,7 @@ This file should be imported by eng/Versions.props
$(MicrosoftDotnetWinFormsProjectTemplatesPackageVersion)
$(MicrosoftDotNetWpfProjectTemplatesPackageVersion)
$(MicrosoftDotNetXliffTasksPackageVersion)
- $(MicrosoftDotNetXUnitV3ExtensionsPackageVersion)
+ $(MicrosoftDotNetXUnitExtensionsPackageVersion)
$(MicrosoftExtensionsConfigurationIniPackageVersion)
$(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)
$(MicrosoftExtensionsDependencyModelPackageVersion)
@@ -235,11 +235,11 @@ This file should be imported by eng/Versions.props
$(MicrosoftSourceLinkGitHubPackageVersion)
$(MicrosoftSourceLinkGitLabPackageVersion)
$(MicrosoftTemplateEngineAbstractionsPackageVersion)
- $(MicrosoftTemplateEngineAuthoringTemplateVerifierXunitV3PackageVersion)
+ $(MicrosoftTemplateEngineAuthoringTemplateVerifierPackageVersion)
$(MicrosoftTemplateEngineEdgePackageVersion)
- $(MicrosoftTemplateEngineMocksXunitV3PackageVersion)
+ $(MicrosoftTemplateEngineMocksPackageVersion)
$(MicrosoftTemplateEngineOrchestratorRunnableProjectsPackageVersion)
- $(MicrosoftTemplateEngineTestHelperXunitV3PackageVersion)
+ $(MicrosoftTemplateEngineTestHelperPackageVersion)
$(MicrosoftTemplateEngineUtilsPackageVersion)
$(MicrosoftTemplateSearchCommonPackageVersion)
$(MicrosoftTemplateSearchTemplateDiscoveryPackageVersion)
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 2f41e44f544e..94ae00970131 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -1,62 +1,62 @@
-
+
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
@@ -68,174 +68,174 @@
https://github.com/dotnet/dotnet
6a953e76162f3f079405f80e28664fa51b136740
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
https://github.com/microsoft/testfx
@@ -577,9 +577,9 @@
https://github.com/microsoft/testfx
1ddd2f1a558e9c79b5327c5ccc0e9e89df39d4da
-
+
https://github.com/dotnet/dotnet
- 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
+ e52493553982a80cd4c7d372c104e3a9dbff0e0d
diff --git a/eng/Versions.props b/eng/Versions.props
index 6cdcadaf47c9..a858ff7d865d 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -47,6 +47,7 @@
1.0.0-20230414.1
+ 2.23.0
2.0.1-servicing-26011-01
2.0.3
13.0.3
@@ -59,8 +60,6 @@
0.3.264
1.0.52
- 1.4.0
- 1.12.0
@@ -137,7 +136,7 @@
8.0.2
8.0.0
4.18.4
- 2.0.24
+ 1.3.2
3.1.4
8.0.0-beta.23607.1
0.15.6
diff --git a/eng/dependabot/Packages.props b/eng/dependabot/Packages.props
index 6128e5be0b3d..76181ba6cfc0 100644
--- a/eng/dependabot/Packages.props
+++ b/eng/dependabot/Packages.props
@@ -7,7 +7,7 @@
-
+
diff --git a/global.json b/global.json
index 6caa2e64febd..d467aee51da9 100644
--- a/global.json
+++ b/global.json
@@ -21,8 +21,8 @@
}
},
"msbuild-sdks": {
- "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26208.110",
- "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26208.110",
+ "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26208.106",
+ "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26208.106",
"Microsoft.Build.NoTargets": "3.7.0",
"Microsoft.Build.Traversal": "3.4.0",
"Microsoft.WixToolset.Sdk": "5.0.2-dotnet.2811440"
diff --git a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs
index 13a6bf284167..a7357a9f75bc 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs
@@ -58,14 +58,6 @@ internal sealed class DotNetCommandDefinition : RootCommand
Arity = ArgumentArity.Zero
};
- public readonly Option CliSchemaOption = new("--cli-schema")
- {
- Description = CommandDefinitionStrings.SDKSchemaCommandDefinition,
- Arity = ArgumentArity.Zero,
- Recursive = true,
- Hidden = true,
- };
-
public readonly Option ListSdksOption = new("--list-sdks")
{
Arity = ArgumentArity.Zero
@@ -76,6 +68,14 @@ internal sealed class DotNetCommandDefinition : RootCommand
Arity = ArgumentArity.Zero
};
+ public readonly Option CliSchemaOption = new("--cli-schema")
+ {
+ Description = CommandDefinitionStrings.SDKSchemaCommandDefinition,
+ Arity = ArgumentArity.Zero,
+ Recursive = true,
+ Hidden = true,
+ };
+
public readonly AddCommandDefinition AddCommand;
public readonly BuildCommandDefinition BuildCommand;
public readonly BuildServerCommandDefinition BuildServerCommand;
@@ -121,11 +121,9 @@ public DotNetCommandDefinition()
Options.Add(DiagOption);
Options.Add(VersionOption);
Options.Add(InfoOption);
- Options.Add(CliSchemaOption);
-
- // Host-handled options. Only defined to be shown in help.
Options.Add(ListSdksOption);
Options.Add(ListRuntimesOption);
+ Options.Add(CliSchemaOption);
Subcommands.Add(AddCommand = new());
Subcommands.Add(BuildCommand = new());
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs
index 6b22d2ad94c4..23180a6b69d1 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs
@@ -3,6 +3,8 @@
#if NET
+using System.Diagnostics;
+
namespace Microsoft.DotNet.Cli.Utils;
///
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs
index d8fb130f272f..4bf3d94e2773 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs
@@ -1,6 +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 System.Diagnostics;
+
namespace Microsoft.DotNet.Cli.Utils;
///
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/ITelemetryFilter.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/ITelemetryFilter.cs
index 593edab4ef76..2e4ff73d8935 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/ITelemetryFilter.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/ITelemetryFilter.cs
@@ -1,17 +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.CommandLine;
-
namespace Microsoft.DotNet.Cli.Utils;
public interface ITelemetryFilter
{
- IEnumerable Filter(ParseResult parseResult);
-
- IEnumerable Filter(ParseResultWithGlobalJsonState parseData);
-
- IEnumerable Filter(InstallerSuccessReport report);
-
- IEnumerable Filter(Exception exception);
+ IEnumerable Filter(object o);
}
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/InstallerSuccessReport.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/InstallerSuccessReport.cs
deleted file mode 100644
index 42d77b93ae4b..000000000000
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/InstallerSuccessReport.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-// 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.Cli.Utils;
-
-public class InstallerSuccessReport(string? exeName)
-{
- public string ExeName { get; } = exeName ?? throw new ArgumentNullException(nameof(exeName));
-}
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs
index 1f94685c28a8..87d021eda157 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs
@@ -3,6 +3,7 @@
#if NET
+using System.Diagnostics;
using Microsoft.DotNet.Cli.Utils.Extensions;
namespace Microsoft.DotNet.Cli.Utils;
@@ -15,7 +16,7 @@ internal sealed class MSBuildForwardingAppWithoutLogging
public static string MSBuildVersion
{
- get => Build.Evaluation.ProjectCollection.DisplayVersion;
+ get => Microsoft.Build.Evaluation.ProjectCollection.DisplayVersion;
}
private const string MSBuildExeName = "MSBuild.dll";
@@ -193,7 +194,7 @@ private static string GetMSBuildExePath()
MSBuildExeName);
}
- public static string GetMSBuildSDKsPath()
+ private static string GetMSBuildSDKsPath()
{
var envMSBuildSDKsPath = Environment.GetEnvironmentVariable("MSBuildSDKsPath");
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj b/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj
index 935472e224f2..cfa596437414 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj
@@ -79,6 +79,10 @@
+
+
+
+
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/TelemetryEventEntry.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/TelemetryEventEntry.cs
index 032f82bf49e9..772e9845b678 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/TelemetryEventEntry.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/TelemetryEventEntry.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.
-using System.CommandLine;
+using System.Diagnostics;
namespace Microsoft.DotNet.Cli.Utils;
@@ -10,71 +10,104 @@ public static class TelemetryEventEntry
public static event EventHandler? EntryPosted;
public static ITelemetryFilter TelemetryFilter { get; set; } = new BlockFilter();
- public static void TrackEvent(string eventName, IDictionary? properties = null)
+ public static void TrackEvent(
+ string? eventName = null,
+ IDictionary? properties = null,
+ IDictionary? measurements = null)
{
- EntryPosted?.Invoke(typeof(TelemetryEventEntry), new InstrumentationEventArgs(eventName, properties));
+ EntryPosted?.Invoke(typeof(TelemetryEventEntry),
+ new InstrumentationEventArgs(eventName, properties, measurements));
}
- public static void SendFiltered(ParseResult parseResult) =>
- SendFiltered(TelemetryFilter.Filter(parseResult));
-
- public static void SendFiltered(ParseResultWithGlobalJsonState parseData) =>
- SendFiltered(TelemetryFilter.Filter(parseData));
-
- public static void SendFiltered(InstallerSuccessReport report) =>
- SendFiltered(TelemetryFilter.Filter(report));
-
- public static void SendFiltered(Exception exception) =>
- SendFiltered(TelemetryFilter.Filter(exception));
-
- private static void SendFiltered(IEnumerable entries)
+ public static void SendFiltered(object? o = null)
{
- foreach (TelemetryEntryFormat entry in entries)
+ if (o == null)
+ {
+ return;
+ }
+
+ foreach (ApplicationInsightsEntryFormat entry in TelemetryFilter.Filter(o))
{
- TrackEvent(entry.EventName, entry.Properties);
+ TrackEvent(entry.EventName, entry.Properties, entry.Measurements);
}
}
- public static void Subscribe(Action?> subscriber)
+ public static void Subscribe(Action?, IDictionary?> subscriber)
{
void Handler(object? sender, InstrumentationEventArgs eventArgs)
{
- subscriber(eventArgs.EventName, eventArgs.Properties);
+ subscriber(eventArgs.EventName, eventArgs.Properties, eventArgs.Measurements);
}
EntryPosted += Handler;
}
}
-public class BlockFilter : ITelemetryFilter
+public sealed class PerformanceMeasurement : IDisposable
{
- private static readonly TelemetryEntryFormat[] s_emptyEntries = [];
+ private readonly Stopwatch? _timer;
+ private readonly Dictionary? _data;
+ private readonly string? _name;
- public IEnumerable Filter(ParseResult parseResult) => s_emptyEntries;
+ public PerformanceMeasurement(Dictionary? data, string name)
+ {
+ // Measurement is a no-op if we don't have a dictionary to store the entry.
+ if (data == null)
+ {
+ return;
+ }
- public IEnumerable Filter(ParseResultWithGlobalJsonState parseData) => s_emptyEntries;
+ _data = data;
+ _name = name;
+ _timer = Stopwatch.StartNew();
+ }
- public IEnumerable Filter(InstallerSuccessReport report) => s_emptyEntries;
+ public void Dispose()
+ {
+ if (_name is not null && _timer is not null)
+ {
+ _data?.Add(_name, _timer.Elapsed.TotalMilliseconds);
+ }
+ }
+}
- public IEnumerable Filter(Exception exception) => s_emptyEntries;
+public class BlockFilter : ITelemetryFilter
+{
+ public IEnumerable Filter(object o)
+ {
+ return [];
+ }
}
-public class InstrumentationEventArgs(string eventName, IDictionary? properties = null) : EventArgs
+public class InstrumentationEventArgs : EventArgs
{
- public string EventName { get; } = eventName;
- public IDictionary? Properties { get; } = properties;
+ internal InstrumentationEventArgs(
+ string? eventName,
+ IDictionary? properties,
+ IDictionary? measurements)
+ {
+ EventName = eventName;
+ Properties = properties;
+ Measurements = measurements;
+ }
+
+ public string? EventName { get; }
+ public IDictionary? Properties { get; }
+ public IDictionary? Measurements { get; }
}
-public class TelemetryEntryFormat(string eventName, IDictionary? properties = null)
+public class ApplicationInsightsEntryFormat(
+ string? eventName = null,
+ IDictionary? properties = null,
+ IDictionary? measurements = null)
{
- public string EventName { get; } = eventName;
+ public string? EventName { get; } = eventName;
public IDictionary? Properties { get; } = properties;
+ public IDictionary? Measurements { get; } = measurements;
- public TelemetryEntryFormat WithAppliedToPropertiesValue(Func func)
+ public ApplicationInsightsEntryFormat WithAppliedToPropertiesValue(Func func)
{
- var appliedProperties = Properties?.ToDictionary(p => p.Key, p => (string?)func(p.Value ?? string.Empty));
- return new TelemetryEntryFormat(EventName, appliedProperties);
+ var appliedProperties = Properties?.ToDictionary(p => p.Key, p => (string?)func(p.Value));
+ return new ApplicationInsightsEntryFormat(EventName, appliedProperties, Measurements);
}
}
-
-public record ParseResultWithGlobalJsonState(ParseResult ParseResult, string? GlobalJsonState);
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/UILanguageOverride.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/UILanguageOverride.cs
index 6f66646cfd93..32e773f1261b 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/UILanguageOverride.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/UILanguageOverride.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
using System.Globalization;
using System.Security;
using Microsoft.Win32;
@@ -10,7 +11,6 @@ namespace Microsoft.DotNet.Cli.Utils;
internal static class UILanguageOverride
{
internal const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE);
- private const string DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING = nameof(DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING);
private const string VSLANG = nameof(VSLANG);
private const string PreferredUILang = nameof(PreferredUILang);
// We choose UTF8 as the default encoding as opposed to specific language encodings because it supports emojis & other chars in .NET.
@@ -25,7 +25,7 @@ public static void Setup()
FlowOverrideToChildProcesses(language);
}
- if (Env.GetEnvironmentVariable(DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING) != "1")
+ if (Env.GetEnvironmentVariable("DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING") != "1")
{
if (
!CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.Equals("en", StringComparison.InvariantCultureIgnoreCase) &&
diff --git a/src/Cli/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs b/src/Cli/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs
index a364b4191d2d..c52628b044ca 100644
--- a/src/Cli/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs
+++ b/src/Cli/Microsoft.DotNet.Configurer/DotnetFirstTimeUseConfigurer.cs
@@ -15,6 +15,7 @@ public class DotnetFirstTimeUseConfigurer
private readonly IAspNetCoreCertificateGenerator _aspNetCoreCertificateGenerator;
private readonly IFileSentinel _toolPathSentinel;
private readonly IEnvironmentPath _pathAdder;
+ private readonly Dictionary? _performanceMeasurements;
private readonly bool _skipFirstTimeUseCheck;
public DotnetFirstTimeUseConfigurer(
@@ -25,6 +26,7 @@ public DotnetFirstTimeUseConfigurer(
DotnetFirstRunConfiguration dotnetFirstRunConfiguration,
IReporter reporter,
IEnvironmentPath pathAdder,
+ Dictionary? performanceMeasurements = null,
bool skipFirstTimeUseCheck = false)
{
_firstTimeUseNoticeSentinel = firstTimeUseNoticeSentinel;
@@ -34,6 +36,7 @@ public DotnetFirstTimeUseConfigurer(
_dotnetFirstRunConfiguration = dotnetFirstRunConfiguration;
_reporter = reporter;
_pathAdder = pathAdder ?? throw new ArgumentNullException(nameof(pathAdder));
+ _performanceMeasurements ??= performanceMeasurements;
_skipFirstTimeUseCheck = skipFirstTimeUseCheck;
}
@@ -41,45 +44,54 @@ public void Configure()
{
if (_dotnetFirstRunConfiguration.AddGlobalToolsToPath && !_toolPathSentinel.Exists())
{
- _pathAdder.AddPackageExecutablePathToUserPath();
- _toolPathSentinel.Create();
+ using (new PerformanceMeasurement(_performanceMeasurements, "AddPackageExecutablePath Time"))
+ {
+ _pathAdder.AddPackageExecutablePathToUserPath();
+ _toolPathSentinel.Create();
+ }
}
var isFirstTimeUse = !_skipFirstTimeUseCheck && !_firstTimeUseNoticeSentinel.Exists();
var canShowFirstUseMessages = isFirstTimeUse && !_dotnetFirstRunConfiguration.NoLogo;
if (isFirstTimeUse)
{
- // Migrate the NuGet state from earlier SDKs
- NuGet.Common.Migrations.MigrationRunner.Run();
-
- if (canShowFirstUseMessages)
+ using (new PerformanceMeasurement(_performanceMeasurements, "FirstTimeUseNotice Time"))
{
- _reporter.WriteLine();
- string productVersion = Product.Version;
- _reporter.WriteLine(string.Format(LocalizableStrings.FirstTimeMessageWelcome, ParseDotNetVersion(productVersion), productVersion));
+ // Migrate the NuGet state from earlier SDKs
+ NuGet.Common.Migrations.MigrationRunner.Run();
- if (!_dotnetFirstRunConfiguration.TelemetryOptout)
+ if (canShowFirstUseMessages)
{
_reporter.WriteLine();
- _reporter.WriteLine(LocalizableStrings.TelemetryMessage);
+ string productVersion = Product.Version;
+ _reporter.WriteLine(string.Format(LocalizableStrings.FirstTimeMessageWelcome, ParseDotNetVersion(productVersion), productVersion));
+
+ if (!_dotnetFirstRunConfiguration.TelemetryOptout)
+ {
+ _reporter.WriteLine();
+ _reporter.WriteLine(LocalizableStrings.TelemetryMessage);
+ }
}
- }
- _firstTimeUseNoticeSentinel.CreateIfNotExists();
+ _firstTimeUseNoticeSentinel.CreateIfNotExists();
+ }
}
if (CanGenerateAspNetCertificate())
{
- _aspNetCoreCertificateGenerator.GenerateAspNetCoreDevelopmentCertificate();
- _aspNetCertificateSentinel.CreateIfNotExists();
-
- if (canShowFirstUseMessages)
+ using (new PerformanceMeasurement(_performanceMeasurements, "GenerateAspNetCertificate Time"))
{
- // This message is slightly misleading for (e.g.) FreeBSD, which doesn't officially
- // support `dotnet dev-certs https --trust`, but the link in the message should help
- // users find the right steps for their platform.
- _reporter.WriteLine();
- _reporter.WriteLine(LocalizableStrings.FirstTimeMessageAspNetCertificate);
+ _aspNetCoreCertificateGenerator.GenerateAspNetCoreDevelopmentCertificate();
+ _aspNetCertificateSentinel.CreateIfNotExists();
+
+ if (canShowFirstUseMessages)
+ {
+ // This message is slightly misleading for (e.g.) FreeBSD, which doesn't officially
+ // support `dotnet dev-certs https --trust`, but the link in the message should help
+ // users find the right steps for their platform.
+ _reporter.WriteLine();
+ _reporter.WriteLine(LocalizableStrings.FirstTimeMessageAspNetCertificate);
+ }
}
}
diff --git a/src/Cli/Microsoft.DotNet.InternalAbstractions/FilePath.cs b/src/Cli/Microsoft.DotNet.InternalAbstractions/FilePath.cs
index f08c1f1a759a..d9ad822f9517 100644
--- a/src/Cli/Microsoft.DotNet.InternalAbstractions/FilePath.cs
+++ b/src/Cli/Microsoft.DotNet.InternalAbstractions/FilePath.cs
@@ -1,33 +1,34 @@
// 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.EnvironmentAbstractions;
-
-public readonly struct FilePath
+namespace Microsoft.Extensions.EnvironmentAbstractions
{
- public string Value { get; }
-
- ///
- /// Create FilePath to represent an absolute file path. Note: It may not exist.
- ///
- /// If the value is not rooted. Path.GetFullPath will be called during the constructor.
- public FilePath(string value)
+ public struct FilePath
{
- if (!Path.IsPathRooted(value))
+ public string Value { get; }
+
+ ///
+ /// Create FilePath to represent an absolute file path. Note it may not exist.
+ ///
+ /// If the value is not rooted. Path.GetFullPath will be called during the constructor.
+ public FilePath(string value)
{
- value = Path.GetFullPath(value);
- }
+ if (!Path.IsPathRooted(value))
+ {
+ value = Path.GetFullPath(value);
+ }
- Value = value;
- }
+ Value = value;
+ }
- public override string ToString()
- {
- return Value;
- }
+ public override string ToString()
+ {
+ return Value;
+ }
- public DirectoryPath GetDirectoryPath()
- {
- return new DirectoryPath(Path.GetDirectoryName(Value)!);
+ public DirectoryPath GetDirectoryPath()
+ {
+ return new DirectoryPath(Path.GetDirectoryName(Value)!);
+ }
}
}
diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/create/TemplateCommand.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/create/TemplateCommand.cs
index 2aee7d6e9360..68e6c1162794 100644
--- a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/create/TemplateCommand.cs
+++ b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/create/TemplateCommand.cs
@@ -22,6 +22,7 @@ internal class TemplateCommand : Command
private readonly TemplatePackageManager _templatePackageManager;
private readonly IEngineEnvironmentSettings _environmentSettings;
private readonly Command _instantiateCommand;
+ private readonly TemplateGroup _templateGroup;
private readonly CliTemplateInfo _template;
private Dictionary _templateSpecificOptions = new();
@@ -43,6 +44,7 @@ public TemplateCommand(
_instantiateCommand = instantiateCommand;
_environmentSettings = environmentSettings;
_templatePackageManager = templatePackageManager;
+ _templateGroup = templateGroup;
_template = template;
foreach (var item in templateGroup.ShortNames.Skip(1))
{
diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Help/DotnetHelpAction.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Help/DotnetHelpAction.cs
new file mode 100644
index 000000000000..884867e08e2f
--- /dev/null
+++ b/src/Cli/Microsoft.TemplateEngine.Cli/Help/DotnetHelpAction.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using Microsoft.DotNet.Cli.Help;
+
+namespace Microsoft.TemplateEngine.Cli.Help;
+
+///
+/// Provides command line help.
+///
+public sealed class DotnetHelpAction : SynchronousCommandLineAction
+{
+ private HelpBuilder? _builder;
+
+ ///
+ /// Specifies an to be used to format help output when help is requested.
+ ///
+ public HelpBuilder Builder
+ {
+ get => _builder ??= new HelpBuilder(Console.IsOutputRedirected ? int.MaxValue : Console.WindowWidth);
+ set => _builder = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ ///
+ public override bool ClearsParseErrors => true;
+
+ ///
+ public override int Invoke(ParseResult parseResult)
+ {
+ var output = parseResult.InvocationConfiguration.Output;
+
+ var helpContext = new HelpContext(
+ Builder,
+ parseResult.CommandResult.Command,
+ output,
+ parseResult);
+
+ Builder.Write(helpContext);
+
+ return 0;
+ }
+}
diff --git a/src/Cli/dotnet/CliSchema.cs b/src/Cli/dotnet/CliSchema.cs
index 770f33b65fab..99d039df1db2 100644
--- a/src/Cli/dotnet/CliSchema.cs
+++ b/src/Cli/dotnet/CliSchema.cs
@@ -7,7 +7,6 @@
using System.Text.Json;
using System.Text.Json.Schema;
using System.Text.Json.Serialization;
-using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Telemetry;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
@@ -32,20 +31,8 @@ internal static class CliSchema
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
});
- public record ArgumentDetails(
- string? description,
- int order,
- bool hidden,
- string? helpName,
- string valueType,
- bool hasDefaultValue,
- object? defaultValue,
- ArityDetails arity);
-
- public record ArityDetails(
- int minimum,
- int? maximum);
-
+ public record ArgumentDetails(string? description, int order, bool hidden, string? helpName, string valueType, bool hasDefaultValue, object? defaultValue, ArityDetails arity);
+ public record ArityDetails(int minimum, int? maximum);
public record OptionDetails(
string? description,
bool hidden,
@@ -56,8 +43,8 @@ public record OptionDetails(
object? defaultValue,
ArityDetails arity,
bool required,
- bool recursive);
-
+ bool recursive
+ );
public record CommandDetails(
string? description,
bool hidden,
@@ -65,7 +52,6 @@ public record CommandDetails(
Dictionary? arguments,
Dictionary? options,
Dictionary? subcommands);
-
public record RootCommandDetails(
string name,
string version,
@@ -77,16 +63,17 @@ public record RootCommandDetails(
Dictionary? subcommands
) : CommandDetails(description, hidden, aliases, arguments, options, subcommands);
- public static void PrintCliSchema(ParseResult parseResult, TextWriter outputWriter, ITelemetryClient? telemetryClient)
+
+ public static void PrintCliSchema(CommandResult commandResult, TextWriter outputWriter, ITelemetry? telemetryClient)
{
- var command = parseResult.CommandResult.Command;
+ var command = commandResult.Command;
RootCommandDetails transportStructure = CreateRootCommandDetails(command);
var result = JsonSerializer.Serialize(transportStructure, s_jsonContext.RootCommandDetails);
outputWriter.Write(result.AsSpan());
outputWriter.Flush();
- var commandString = parseResult.GetCommandName();
- var telemetryProperties = new Dictionary { { "command", commandString } };
- telemetryClient?.TrackEvent("schema", telemetryProperties);
+ var commandString = CommandHierarchyAsString(commandResult);
+ var telemetryProperties = new Dictionary { { "command", commandString } };
+ telemetryClient?.TrackEvent("schema", telemetryProperties, null);
}
public static object GetJsonSchema()
@@ -217,6 +204,21 @@ private static RootCommandDetails CreateRootCommandDetails(Command command)
argument.HasDefaultValue ? HumanizeValue(argument.GetDefaultValue()) : null,
CreateArityDetails(argument.Arity)
);
+
+ // Produces a string that represents the command call.
+ // For example, calling the workload install command produces `dotnet workload install`.
+ private static string CommandHierarchyAsString(CommandResult commandResult)
+ {
+ var commands = new List();
+ var currentResult = commandResult;
+ while (currentResult is not null)
+ {
+ commands.Add(currentResult.Command.Name);
+ currentResult = currentResult.Parent as CommandResult;
+ }
+
+ return string.Join(" ", commands.AsEnumerable().Reverse());
+ }
}
[JsonSerializable(typeof(CliSchema.RootCommandDetails))]
diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/ActivityContextFactory.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/ActivityContextFactory.cs
deleted file mode 100644
index bd8616a1e06a..000000000000
--- a/src/Cli/dotnet/CommandFactory/CommandResolution/ActivityContextFactory.cs
+++ /dev/null
@@ -1,48 +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.Diagnostics;
-using Microsoft.DotNet.Cli.Utils;
-#if TARGET_WINDOWS
-using OpenTelemetry;
-using OpenTelemetry.Context.Propagation;
-#endif
-
-namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
-
-public static class ActivityContextFactory
-{
- public static Dictionary? MakeActivityContextEnvironment()
- {
- var currentActivity = Activity.Current;
- if (currentActivity is null)
- {
- return null;
- }
- var activityContext = currentActivity.Context;
- if (activityContext.TraceState is null && activityContext.TraceId == default && activityContext.SpanId == default)
- {
- return null;
- }
-
- var environment = new Dictionary(capacity: 2);
-#if TARGET_WINDOWS
- var propagationContext = new PropagationContext(activityContext, Baggage.Current);
- Propagators.DefaultTextMapPropagator.Inject(propagationContext, environment, WriteTraceStateIntoEnvironment);
-#endif
- return environment;
- }
-
- private static void WriteTraceStateIntoEnvironment(Dictionary environment, string key, string value)
- {
- switch (key)
- {
- case "traceparent":
- environment[Activities.TRACEPARENT] = value;
- break;
- case "tracestate":
- environment[Activities.TRACESTATE] = value;
- break;
- }
- }
-}
diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs
index 836fde7c220d..81b98215429d 100644
--- a/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs
+++ b/src/Cli/dotnet/CommandFactory/CommandResolution/DotnetToolsCommandResolver.cs
@@ -1,18 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
public class DotnetToolsCommandResolver : ICommandResolver
{
private readonly string _dotnetToolPath;
- public DotnetToolsCommandResolver(string? dotnetToolPath = null)
+ public DotnetToolsCommandResolver(string dotnetToolPath = null)
{
- _dotnetToolPath = dotnetToolPath ?? Path.Combine(AppContext.BaseDirectory, "DotnetTools");
+ if (dotnetToolPath == null)
+ {
+ _dotnetToolPath = Path.Combine(AppContext.BaseDirectory,
+ "DotnetTools");
+ }
+ else
+ {
+ _dotnetToolPath = dotnetToolPath;
+ }
}
- public CommandSpec? Resolve(CommandResolverArguments arguments)
+ public CommandSpec Resolve(CommandResolverArguments arguments)
{
if (string.IsNullOrEmpty(arguments.CommandName))
{
@@ -33,6 +43,6 @@ public DotnetToolsCommandResolver(string? dotnetToolPath = null)
return MuxerCommandSpecMaker.CreatePackageCommandSpecUsingMuxer(
dll.FullName,
- arguments.CommandArguments ?? []);
+ arguments.CommandArguments);
}
}
diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/LocalToolsCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/LocalToolsCommandResolver.cs
index 70ba8d1e2056..ac22545eddec 100644
--- a/src/Cli/dotnet/CommandFactory/CommandResolution/LocalToolsCommandResolver.cs
+++ b/src/Cli/dotnet/CommandFactory/CommandResolution/LocalToolsCommandResolver.cs
@@ -1,27 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using Microsoft.DotNet.Cli.Commands.Tool;
using Microsoft.DotNet.Cli.ToolManifest;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.EnvironmentAbstractions;
+using NuGet.DependencyResolver;
using NuGet.Frameworks;
namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
internal class LocalToolsCommandResolver(
- ToolManifestFinder? toolManifest = null,
- ILocalToolsResolverCache? localToolsResolverCache = null,
- IFileSystem? fileSystem = null,
- string? currentWorkingDirectory = null) : ICommandResolver
+ ToolManifestFinder toolManifest = null,
+ ILocalToolsResolverCache localToolsResolverCache = null,
+ IFileSystem fileSystem = null,
+ string currentWorkingDirectory = null) : ICommandResolver
{
private readonly ToolManifestFinder _toolManifest = toolManifest ?? new ToolManifestFinder(new DirectoryPath(currentWorkingDirectory ?? Directory.GetCurrentDirectory()));
private readonly ILocalToolsResolverCache _localToolsResolverCache = localToolsResolverCache ?? new LocalToolsResolverCache();
private readonly IFileSystem _fileSystem = fileSystem ?? new FileSystemWrapper();
private const string LeadingDotnetPrefix = "dotnet-";
- public CommandSpec? ResolveStrict(CommandResolverArguments arguments, bool allowRollForward = false)
+ public CommandSpec ResolveStrict(CommandResolverArguments arguments, bool allowRollForward = false)
{
if (arguments == null || string.IsNullOrWhiteSpace(arguments.CommandName))
{
@@ -39,7 +42,7 @@ internal class LocalToolsCommandResolver(
return resolveResult;
}
- public CommandSpec? Resolve(CommandResolverArguments arguments)
+ public CommandSpec Resolve(CommandResolverArguments arguments)
{
if (arguments == null || string.IsNullOrWhiteSpace(arguments.CommandName))
{
@@ -66,7 +69,7 @@ internal class LocalToolsCommandResolver(
return GetPackageCommandSpecUsingMuxer(arguments, new ToolCommandName(arguments.CommandName));
}
- private CommandSpec? GetPackageCommandSpecUsingMuxer(CommandResolverArguments arguments,
+ private CommandSpec GetPackageCommandSpecUsingMuxer(CommandResolverArguments arguments,
ToolCommandName toolCommandName, bool allowRollForward = false)
{
if (!_toolManifest.TryFind(toolCommandName, out var toolManifestPackage))
@@ -90,7 +93,7 @@ internal class LocalToolsCommandResolver(
}
return ToolCommandSpecCreator.CreateToolCommandSpec(toolCommand.Name.Value, toolCommand.Executable.Value, toolCommand.Runner,
- toolManifestPackage.RollForward || allowRollForward, arguments.CommandArguments ?? []);
+ toolManifestPackage.RollForward || allowRollForward, arguments.CommandArguments);
}
else
{
diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandResolver.cs
index 92f7d50f6abd..1998ae144809 100644
--- a/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandResolver.cs
+++ b/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandResolver.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
@@ -8,15 +10,14 @@ namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
public class MuxerCommandResolver : ICommandResolver
{
- public CommandSpec? Resolve(CommandResolverArguments commandResolverArguments)
+ public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
{
if (commandResolverArguments.CommandName == Muxer.MuxerName)
{
var muxer = new Muxer();
var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(
commandResolverArguments.CommandArguments.OrEmptyIfNull());
- var environment = ActivityContextFactory.MakeActivityContextEnvironment();
- return new CommandSpec(muxer.MuxerPath, escapedArgs, environment);
+ return new CommandSpec(muxer.MuxerPath, escapedArgs);
}
return null;
}
diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandSpecMaker.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandSpecMaker.cs
index 12f1a372e652..63a3ecae1d5b 100644
--- a/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandSpecMaker.cs
+++ b/src/Cli/dotnet/CommandFactory/CommandResolution/MuxerCommandSpecMaker.cs
@@ -1,16 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using Microsoft.DotNet.Cli.Utils;
namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
internal static class MuxerCommandSpecMaker
{
- internal static CommandSpec CreatePackageCommandSpecUsingMuxer(string commandPath, IEnumerable commandArguments, IDictionary? environment = null)
+ internal static CommandSpec CreatePackageCommandSpecUsingMuxer(
+ string commandPath,
+ IEnumerable commandArguments)
{
var arguments = new List();
- var rollForwardArgument = commandArguments.Where(arg => arg.Equals("--allow-roll-forward", StringComparison.OrdinalIgnoreCase));
+
+ var muxer = new Muxer();
+
+ var host = muxer.MuxerPath;
+
+ if (host == null)
+ {
+ throw new Exception(LocalizableStrings.UnableToLocateDotnetMultiplexer);
+ }
+
+ var rollForwardArgument = (commandArguments ?? []).Where(arg => arg.Equals("--allow-roll-forward", StringComparison.OrdinalIgnoreCase));
+
if (rollForwardArgument.Any())
{
arguments.Add("--roll-forward");
@@ -18,13 +33,27 @@ internal static CommandSpec CreatePackageCommandSpecUsingMuxer(string commandPat
}
arguments.Add(commandPath);
- var filteredCommandArgs = rollForwardArgument.Any()
- ? commandArguments.Except(rollForwardArgument)
- : commandArguments;
- arguments.AddRange(filteredCommandArgs);
-
- var host = new Muxer().MuxerPath;
- var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(arguments);
- return new CommandSpec(host, escapedArgs, environment);
+
+ if (commandArguments != null)
+ {
+ if (rollForwardArgument.Any())
+ {
+ arguments.AddRange(commandArguments.Except(rollForwardArgument));
+ }
+ else
+ {
+ arguments.AddRange(commandArguments);
+ }
+ }
+ return CreateCommandSpec(host, arguments);
+ }
+
+ private static CommandSpec CreateCommandSpec(
+ string commandPath,
+ IEnumerable commandArguments)
+ {
+ var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(commandArguments);
+
+ return new CommandSpec(commandPath, escapedArgs);
}
}
diff --git a/src/Cli/dotnet/CommandFactory/CommandSpec.cs b/src/Cli/dotnet/CommandFactory/CommandSpec.cs
index 0b0cf47a9c56..6af14dd0b931 100644
--- a/src/Cli/dotnet/CommandFactory/CommandSpec.cs
+++ b/src/Cli/dotnet/CommandFactory/CommandSpec.cs
@@ -5,13 +5,16 @@
namespace Microsoft.DotNet.Cli.CommandFactory;
-public class CommandSpec(string path, string? args, IDictionary? environmentVariables = null)
+public class CommandSpec(
+ string path,
+ string? args,
+ Dictionary? environmentVariables = null)
{
public string Path { get; } = path;
public string? Args { get; } = args;
- public IDictionary EnvironmentVariables { get; } = environmentVariables ?? new Dictionary();
+ public Dictionary EnvironmentVariables { get; } = environmentVariables ?? [];
internal void AddEnvironmentVariablesFromProject(IProject project)
{
diff --git a/src/Cli/dotnet/CommandParsingException.cs b/src/Cli/dotnet/CommandParsingException.cs
index 6582c7373d84..f932b55aefb6 100644
--- a/src/Cli/dotnet/CommandParsingException.cs
+++ b/src/Cli/dotnet/CommandParsingException.cs
@@ -1,18 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.CommandLine;
namespace Microsoft.DotNet.Cli;
internal class CommandParsingException : Exception
{
- public CommandParsingException(string message, ParseResult? parseResult = null)
- : base(message)
+ public CommandParsingException(
+ string message,
+ ParseResult parseResult = null) : base(message)
{
ParseResult = parseResult;
Data.Add("CLI_User_Displayed_Exception", true);
}
- public ParseResult? ParseResult;
+ public ParseResult ParseResult;
}
diff --git a/src/Cli/dotnet/Commands/Build/BuildCommand.cs b/src/Cli/dotnet/Commands/Build/BuildCommand.cs
index b0276710516d..cde474ec73c2 100644
--- a/src/Cli/dotnet/Commands/Build/BuildCommand.cs
+++ b/src/Cli/dotnet/Commands/Build/BuildCommand.cs
@@ -29,7 +29,7 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui
bool noRestore = parseResult.HasOption(definition.NoRestoreOption);
- return DotNetCommandFactory.CreateVirtualOrPhysicalCommand(
+ return CommandFactory.CreateVirtualOrPhysicalCommand(
definition,
definition.SlnOrProjectOrFileArgument,
createVirtualCommand: (msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand(
diff --git a/src/Cli/dotnet/Commands/Build/BuildCommandParser.cs b/src/Cli/dotnet/Commands/Build/BuildCommandParser.cs
index bc1289c7ae2e..316b422c8a85 100644
--- a/src/Cli/dotnet/Commands/Build/BuildCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Build/BuildCommandParser.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.CommandLine;
using Microsoft.DotNet.Cli.CommandLine;
namespace Microsoft.DotNet.Cli.Commands.Build;
diff --git a/src/Cli/dotnet/Commands/Clean/CleanCommand.cs b/src/Cli/dotnet/Commands/Clean/CleanCommand.cs
index 5584a77f2461..d1772e247a4f 100644
--- a/src/Cli/dotnet/Commands/Clean/CleanCommand.cs
+++ b/src/Cli/dotnet/Commands/Clean/CleanCommand.cs
@@ -22,7 +22,7 @@ public static CommandBase FromParseResult(ParseResult result, string? msbuildPat
var definition = (CleanCommandDefinition)result.CommandResult.Command;
result.ShowHelpOrErrorIfAppropriate();
- return DotNetCommandFactory.CreateVirtualOrPhysicalCommand(
+ return CommandFactory.CreateVirtualOrPhysicalCommand(
definition,
definition.SlnOrProjectOrFileArgument,
createVirtualCommand: static (msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand(
diff --git a/src/Cli/dotnet/Commands/DotNetCommandFactory.cs b/src/Cli/dotnet/Commands/CommandFactory.cs
similarity index 54%
rename from src/Cli/dotnet/Commands/DotNetCommandFactory.cs
rename to src/Cli/dotnet/Commands/CommandFactory.cs
index ba08b4864f7d..6f50a6b2d4d7 100644
--- a/src/Cli/dotnet/Commands/DotNetCommandFactory.cs
+++ b/src/Cli/dotnet/Commands/CommandFactory.cs
@@ -1,49 +1,16 @@
-// 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.CommandLine;
-using System.CommandLine.Invocation;
-using System.Diagnostics;
-using Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectTools;
-using NuGet.Frameworks;
-namespace Microsoft.DotNet.Cli;
+namespace Microsoft.DotNet.Cli.Commands;
-public class DotNetCommandFactory(bool alwaysRunOutOfProc = false, string? currentWorkingDirectory = null) : ICommandFactory
+public static class CommandFactory
{
- private readonly bool _alwaysRunOutOfProc = alwaysRunOutOfProc;
- private readonly string? _currentWorkingDirectory = currentWorkingDirectory;
-
- public ICommand Create(string commandName, IEnumerable args, NuGetFramework? framework = null, string configuration = Constants.DefaultConfiguration)
- {
- if (!_alwaysRunOutOfProc && TryGetBuiltInCommand(commandName, out var builtInCommand))
- {
- Debug.Assert(framework == null, "BuiltInCommand doesn't support the 'framework' argument.");
- Debug.Assert(configuration == Constants.DefaultConfiguration, "BuiltInCommand doesn't support the 'configuration' argument.");
-
- return new BuiltInCommand(commandName, args, builtInCommand);
- }
-
- return CommandFactoryUsingResolver.CreateDotNet(commandName, args, framework, configuration, _currentWorkingDirectory);
- }
-
- private static bool TryGetBuiltInCommand(string commandName, out Func commandFunc)
- {
- var command = Parser.GetBuiltInCommand(commandName);
- if (command?.Action is AsynchronousCommandLineAction action)
- {
- commandFunc = (args) => Parser.Invoke([commandName, .. args]);
- return true;
- }
- // No-op delegate for failure case.
- commandFunc = (args) => 1;
- return false;
- }
-
internal static CommandBase CreateVirtualOrPhysicalCommand(
System.CommandLine.Command commandDefinition,
Argument catchAllUserInputArgument,
diff --git a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs
index c48ff5353e5c..c6a6d87ff9df 100644
--- a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs
+++ b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs
@@ -1,29 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.CommandLine;
+using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Telemetry;
using Microsoft.DotNet.Cli.Utils;
+using Microsoft.DotNet.Configurer;
using Microsoft.DotNet.Utilities;
namespace Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess;
public class InternalReportInstallSuccessCommand
{
+ internal const string TelemetrySessionIdEnvironmentVariableName = "DOTNET_CLI_TELEMETRY_SESSIONID";
+
public static int Run(ParseResult parseResult)
{
var telemetry = new ThreadBlockingTelemetry();
ProcessInputAndSendTelemetry(parseResult, telemetry);
+ telemetry.Dispose();
return 0;
}
- public static void ProcessInputAndSendTelemetry(string[] args, ITelemetryClient telemetry)
+ public static void ProcessInputAndSendTelemetry(string[] args, ITelemetry telemetry)
{
var result = Parser.Parse(["dotnet", "internal-reportinstallsuccess", .. args]);
ProcessInputAndSendTelemetry(result, telemetry);
}
- public static void ProcessInputAndSendTelemetry(ParseResult result, ITelemetryClient telemetry)
+ public static void ProcessInputAndSendTelemetry(ParseResult result, ITelemetry telemetry)
{
var definition = (InternalReportInstallSuccessCommandDefinition)result.CommandResult.Command;
var exeName = Path.GetFileName(result.GetValue(definition.Argument));
@@ -31,25 +38,39 @@ public static void ProcessInputAndSendTelemetry(ParseResult result, ITelemetryCl
var filter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);
foreach (var e in filter.Filter(new InstallerSuccessReport(exeName)))
{
- telemetry.TrackEvent(e.EventName, e.Properties);
+ telemetry.TrackEvent(e.EventName, e.Properties, null);
}
}
- internal class ThreadBlockingTelemetry : ITelemetryClient
+ internal class ThreadBlockingTelemetry : ITelemetry
{
- private readonly TelemetryClient _telemetry;
+ private readonly Telemetry.Telemetry telemetry;
internal ThreadBlockingTelemetry()
{
- var sessionId = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_SESSIONID);
- _telemetry = new TelemetryClient(sessionId);
+ var sessionId =
+ Environment.GetEnvironmentVariable(TelemetrySessionIdEnvironmentVariableName);
+ telemetry = new Telemetry.Telemetry(new NoOpFirstTimeUseNoticeSentinel(), sessionId, blockThreadInitialization: true);
+ }
+ public bool Enabled => telemetry.Enabled;
+
+ public void Flush()
+ {
}
- public bool Enabled => _telemetry.Enabled;
+ public void Dispose()
+ {
+ telemetry.Dispose();
+ }
- public void TrackEvent(string eventName, IDictionary? properties)
+ public void TrackEvent(string eventName, IDictionary properties, IDictionary measurements)
{
- _telemetry.ThreadBlockingTrackEvent(eventName, properties);
+ telemetry.ThreadBlockingTrackEvent(eventName, properties, measurements);
}
}
}
+
+internal class InstallerSuccessReport(string exeName)
+{
+ public string ExeName { get; } = exeName ?? throw new ArgumentNullException(nameof(exeName));
+}
diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs
index 7f8364ddf11c..d6fb42ac39f0 100644
--- a/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs
+++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs
@@ -4,7 +4,6 @@
using System.Diagnostics;
using System.Reflection;
using Microsoft.DotNet.Cli.Commands.Run;
-using Microsoft.DotNet.Cli.Telemetry;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
@@ -12,6 +11,8 @@ namespace Microsoft.DotNet.Cli.Commands.MSBuild;
public class MSBuildForwardingApp : CommandBase
{
+ internal const string TelemetrySessionIdEnvironmentVariableName = "DOTNET_CLI_TELEMETRY_SESSIONID";
+
private readonly MSBuildForwardingAppWithoutLogging _forwardingAppWithoutLogging;
///
@@ -19,7 +20,7 @@ public class MSBuildForwardingApp : CommandBase
///
private static MSBuildArgs ConcatTelemetryLogger(MSBuildArgs msbuildArgs)
{
- if (TelemetryClient.CurrentSessionId != null)
+ if (Telemetry.Telemetry.CurrentSessionId != null)
{
try
{
@@ -54,6 +55,12 @@ public MSBuildForwardingApp(MSBuildArgs msBuildArgs, string? msbuildPath = null)
_forwardingAppWithoutLogging = new MSBuildForwardingAppWithoutLogging(
modifiedMSBuildArgs,
msbuildPath: msbuildPath);
+
+ // Add the performance log location to the environment of the target process.
+ if (PerformanceLogManager.Instance != null && !string.IsNullOrEmpty(PerformanceLogManager.Instance.CurrentLogDirectory))
+ {
+ EnvironmentVariable(PerformanceLogManager.PerfLogDirEnvVar, PerformanceLogManager.Instance.CurrentLogDirectory);
+ }
}
public IEnumerable MSBuildArguments { get { return _forwardingAppWithoutLogging.GetAllArguments(); } }
@@ -72,7 +79,7 @@ public ProcessStartInfo GetProcessStartInfo()
private void InitializeRequiredEnvironmentVariables()
{
- EnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_SESSIONID, TelemetryClient.CurrentSessionId);
+ EnvironmentVariable(TelemetrySessionIdEnvironmentVariableName, Telemetry.Telemetry.CurrentSessionId);
}
///
@@ -92,13 +99,23 @@ public override int Execute()
if (_forwardingAppWithoutLogging.ExecuteMSBuildOutOfProc)
{
ProcessStartInfo startInfo = GetProcessStartInfo();
+
+ PerformanceLogEventSource.Log.LogMSBuildStart(startInfo.FileName, startInfo.Arguments);
exitCode = startInfo.Execute();
+ PerformanceLogEventSource.Log.MSBuildStop(exitCode);
}
else
{
InitializeRequiredEnvironmentVariables();
string[] arguments = _forwardingAppWithoutLogging.GetAllArguments();
+ if (PerformanceLogEventSource.Log.IsEnabled())
+ {
+ PerformanceLogEventSource.Log.LogMSBuildStart(
+ _forwardingAppWithoutLogging.MSBuildPath,
+ ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(arguments));
+ }
exitCode = _forwardingAppWithoutLogging.ExecuteInProc(arguments);
+ PerformanceLogEventSource.Log.MSBuildStop(exitCode);
}
return exitCode;
diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs
index 608f4caf5273..bf770da1c33b 100644
--- a/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs
+++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildLogger.cs
@@ -1,16 +1,20 @@
-// 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.Globalization;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Cli.Telemetry;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.DotNet.Configurer;
using Microsoft.DotNet.Utilities;
namespace Microsoft.DotNet.Cli.Commands.MSBuild;
public sealed class MSBuildLogger : INodeLogger
{
- private readonly ITelemetryClient? _telemetry;
+ private readonly IFirstTimeUseNoticeSentinel _sentinel =
+ new FirstTimeUseNoticeSentinel();
+ private readonly ITelemetry? _telemetry;
internal const string TargetFrameworkTelemetryEventName = "targetframeworkeval";
internal const string BuildTelemetryEventName = "build";
@@ -62,11 +66,19 @@ public MSBuildLogger()
{
try
{
- string? sessionId = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_SESSIONID);
+ string? sessionId =
+ Environment.GetEnvironmentVariable(MSBuildForwardingApp.TelemetrySessionIdEnvironmentVariableName);
if (sessionId != null)
{
- _telemetry = new TelemetryClient(sessionId);
+ // senderCount: 0 to disable sender.
+ // When senders in different process running at the same
+ // time they will read from the same global queue and cause
+ // sending duplicated events. Disable sender to reduce it.
+ _telemetry = new Telemetry.Telemetry(
+ _sentinel,
+ sessionId,
+ senderCount: 0);
}
}
catch (Exception)
@@ -78,7 +90,7 @@ public MSBuildLogger()
///
/// Constructor for testing purposes.
///
- internal MSBuildLogger(ITelemetryClient telemetry)
+ internal MSBuildLogger(ITelemetry telemetry)
{
_telemetry = telemetry;
}
@@ -105,6 +117,8 @@ public void Initialize(IEventSource eventSource)
{
eventSource2.TelemetryLogged += OnTelemetryLogged;
}
+
+ eventSource.BuildFinished += OnBuildFinished;
}
eventSource.BuildFinished += OnBuildFinished;
@@ -120,14 +134,14 @@ private void OnBuildFinished(object sender, BuildFinishedEventArgs e)
SendAggregatedEventsOnBuildFinished(_telemetry);
}
- internal void SendAggregatedEventsOnBuildFinished(ITelemetryClient? telemetry)
+ internal void SendAggregatedEventsOnBuildFinished(ITelemetry? telemetry)
{
if (telemetry is null) return;
if (_aggregatedEvents.TryGetValue(TaskFactoryTelemetryAggregatedEventName, out var taskFactoryData))
{
Dictionary taskFactoryProperties = ConvertToStringDictionary(taskFactoryData);
- TrackEvent(telemetry, $"msbuild/{TaskFactoryTelemetryAggregatedEventName}", taskFactoryProperties, toBeHashed: []);
+ TrackEvent(telemetry, $"msbuild/{TaskFactoryTelemetryAggregatedEventName}", taskFactoryProperties, toBeHashed: [], toBeMeasured: []);
_aggregatedEvents.Remove(TaskFactoryTelemetryAggregatedEventName);
}
@@ -135,7 +149,7 @@ internal void SendAggregatedEventsOnBuildFinished(ITelemetryClient? telemetry)
{
Dictionary tasksProperties = ConvertToStringDictionary(tasksData);
- TrackEvent(telemetry, $"msbuild/{TasksTelemetryAggregatedEventName}", tasksProperties, toBeHashed: []);
+ TrackEvent(telemetry, $"msbuild/{TasksTelemetryAggregatedEventName}", tasksProperties, toBeHashed: [], toBeMeasured: []);
_aggregatedEvents.Remove(TasksTelemetryAggregatedEventName);
}
}
@@ -176,7 +190,7 @@ internal void AggregateEvent(TelemetryEventArgs args)
}
}
- internal static void FormatAndSend(ITelemetryClient? telemetry, TelemetryEventArgs args)
+ internal static void FormatAndSend(ITelemetry? telemetry, TelemetryEventArgs args)
{
switch (args.EventName)
{
@@ -185,13 +199,14 @@ internal static void FormatAndSend(ITelemetryClient? telemetry, TelemetryEventAr
break;
case BuildTelemetryEventName:
TrackEvent(telemetry, $"msbuild/{BuildTelemetryEventName}", args.Properties,
- toBeHashed: ["ProjectPath", "BuildTarget"]
+ toBeHashed: ["ProjectPath", "BuildTarget"],
+ toBeMeasured: ["BuildDurationInMilliseconds", "InnerBuildDurationInMilliseconds"]
);
break;
case LoggingConfigurationTelemetryEventName:
TrackEvent(telemetry, $"msbuild/{LoggingConfigurationTelemetryEventName}", args.Properties,
- toBeHashed: []
- );
+ toBeHashed: [],
+ toBeMeasured: []);
break;
case BuildcheckAcquisitionFailureEventName:
TrackEvent(telemetry, $"msbuild/{BuildcheckAcquisitionFailureEventName}", args.Properties,
@@ -199,11 +214,14 @@ internal static void FormatAndSend(ITelemetryClient? telemetry, TelemetryEventAr
);
break;
case BuildcheckRunEventName:
- TrackEvent(telemetry, $"msbuild/{BuildcheckRunEventName}", args.Properties);
+ TrackEvent(telemetry, $"msbuild/{BuildcheckRunEventName}", args.Properties,
+ toBeMeasured: ["TotalRuntimeInMilliseconds"]
+ );
break;
case BuildcheckRuleStatsEventName:
TrackEvent(telemetry, $"msbuild/{BuildcheckRuleStatsEventName}", args.Properties,
- toBeHashed: ["RuleId", "CheckFriendlyName"]
+ toBeHashed: ["RuleId", "CheckFriendlyName"],
+ toBeMeasured: ["TotalRuntimeInMilliseconds"]
);
break;
// Pass through events that don't need special handling
@@ -222,7 +240,7 @@ internal static void FormatAndSend(ITelemetryClient? telemetry, TelemetryEventAr
}
}
- private static void TrackEvent(ITelemetryClient? telemetry, string eventName, IDictionary eventProperties, string[]? toBeHashed = null)
+ private static void TrackEvent(ITelemetry? telemetry, string eventName, IDictionary eventProperties, string[]? toBeHashed = null, string[]? toBeMeasured = null)
{
if (telemetry == null || !telemetry.Enabled)
{
@@ -230,6 +248,7 @@ private static void TrackEvent(ITelemetryClient? telemetry, string eventName, ID
}
Dictionary? properties = null;
+ Dictionary? measurements = null;
if (toBeHashed is not null)
{
@@ -244,7 +263,26 @@ private static void TrackEvent(ITelemetryClient? telemetry, string eventName, ID
}
}
- telemetry?.TrackEvent(eventName, properties ?? eventProperties);
+ if (toBeMeasured is not null)
+ {
+ foreach (var propertyToBeMeasured in toBeMeasured)
+ {
+ if (eventProperties.TryGetValue(propertyToBeMeasured, out var value))
+ {
+ // Lets lazy allocate in case there is tons of telemetry
+ properties ??= new(eventProperties);
+ properties.Remove(propertyToBeMeasured);
+ if (double.TryParse(value, CultureInfo.InvariantCulture, out double realValue))
+ {
+ // Lets lazy allocate in case there is tons of telemetry
+ measurements ??= [];
+ measurements[propertyToBeMeasured] = realValue;
+ }
+ }
+ }
+ }
+
+ telemetry.TrackEvent(eventName, properties ?? eventProperties, measurements);
}
private void OnTelemetryLogged(object sender, TelemetryEventArgs args)
@@ -261,6 +299,14 @@ private void OnTelemetryLogged(object sender, TelemetryEventArgs args)
public void Shutdown()
{
+ try
+ {
+ _sentinel?.Dispose();
+ }
+ catch (Exception)
+ {
+ // Exceptions during telemetry shouldn't cause anything else to fail
+ }
}
public LoggerVerbosity Verbosity { get; set; }
diff --git a/src/Cli/dotnet/Commands/New/BuiltInTemplatePackageProvider.cs b/src/Cli/dotnet/Commands/New/BuiltInTemplatePackageProvider.cs
index f25795528dd4..8858145a6c0a 100644
--- a/src/Cli/dotnet/Commands/New/BuiltInTemplatePackageProvider.cs
+++ b/src/Cli/dotnet/Commands/New/BuiltInTemplatePackageProvider.cs
@@ -1,7 +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.Cli.Utils;
+#nullable disable
+
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.TemplatePackage;
using NuGet.Versioning;
@@ -17,16 +18,14 @@ internal sealed class BuiltInTemplatePackageProvider(BuiltInTemplatePackageProvi
public ITemplatePackageProviderFactory Factory { get; } = factory;
+#pragma warning disable CS0067
///
/// We don't trigger this event, we could complicate our life with FileSystemWatcher.
- /// But since "dotnet new" is short lived process is not worth it, plus it would cause some perf hit...
- /// To avoid warnings about being unused, implement empty add/remove accessors.
+ /// But since "dotnet new" is short lived process is not worth it, plus it would cause
+ /// some perf hit...
///
- public event Action? TemplatePackagesChanged
- {
- add { }
- remove { }
- }
+ public event Action TemplatePackagesChanged;
+#pragma warning restore CS0067
public Task> GetAllTemplatePackagesAsync(CancellationToken cancellationToken)
{
@@ -45,12 +44,14 @@ private static IEnumerable GetTemplateFolders(IEngineEnvironmentSettings
{
var templateFoldersToInstall = new List();
- var sdksDirectory = new DirectoryInfo(MSBuildForwardingAppWithoutLogging.GetMSBuildSDKsPath());
- var sdkDirectory = sdksDirectory.Parent;
- var sdkPath = sdkDirectory?.FullName ?? string.Empty;
- var dotnetRootPath = sdkDirectory?.Parent?.Parent?.FullName ?? string.Empty;
+#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file
+ var sdkDirectory = Path.GetDirectoryName(typeof(Utils.DotnetFiles).Assembly.Location);
+#pragma warning restore IL3000
+
+ var dotnetRootPath = Path.GetDirectoryName(Path.GetDirectoryName(sdkDirectory));
+
// First grab templates from dotnet\templates\M.m folders, in ascending order, up to our version
- string templatesRootFolder = Path.Combine(dotnetRootPath, "templates");
+ string templatesRootFolder = Path.GetFullPath(Path.Combine(dotnetRootPath, "templates"));
if (Directory.Exists(templatesRootFolder))
{
IReadOnlyDictionary parsedNames = GetVersionDirectoriesInDirectory(templatesRootFolder);
@@ -61,7 +62,7 @@ private static IEnumerable GetTemplateFolders(IEngineEnvironmentSettings
}
// Now grab templates from our base folder, if present.
- string templatesDir = Path.Combine(sdkPath, "Templates");
+ string templatesDir = Path.Combine(sdkDirectory, "Templates");
if (Directory.Exists(templatesDir))
{
templateFoldersToInstall.Add(templatesDir);
@@ -78,7 +79,7 @@ private static IReadOnlyDictionary GetVersionDirectorie
foreach (string directory in Directory.EnumerateDirectories(fullPath, "*.*", SearchOption.TopDirectoryOnly))
{
- if (SemanticVersion.TryParse(Path.GetFileName(directory), out SemanticVersion? versionInfo) && versionInfo is not null)
+ if (SemanticVersion.TryParse(Path.GetFileName(directory), out SemanticVersion versionInfo))
{
versionFileInfo.Add(directory, versionInfo);
}
@@ -91,7 +92,7 @@ internal static IList GetBestVersionsByMajorMinor(IReadOnlyDictionary bestVersionsByBucket = new Dictionary();
- Version? sdkVersion = typeof(NewCommandParser).Assembly.GetName().Version;
+ Version sdkVersion = typeof(NewCommandParser).Assembly.GetName().Version;
foreach (KeyValuePair dirInfo in versionDirInfo)
{
var majorMinorDirVersion = new Version(dirInfo.Value.Major, dirInfo.Value.Minor);
diff --git a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs
index 3ddb261d7340..318f1ec64418 100644
--- a/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs
+++ b/src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs
@@ -110,6 +110,8 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin
projectPath = _projectFullPath;
}
+ Stopwatch watch = new();
+ Stopwatch innerBuildWatch = new();
bool IsSdkStyleProject = false;
IReadOnlyList? targetFrameworks = null;
string? targetFramework = null;
@@ -117,6 +119,7 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin
try
{
+ watch.Start();
_logger?.LogDebug("Evaluating project: {0}", projectPath);
MSBuildProject evaluatedProject = RunEvaluate(projectPath);
@@ -161,11 +164,13 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin
//For multi-target project, we need to do additional evaluation for each target framework.
Dictionary evaluatedTfmBasedProjects = [];
+ innerBuildWatch.Start();
foreach (string tfm in targetFrameworks)
{
_logger?.LogDebug("Evaluating project for target framework: {0}", tfm);
evaluatedTfmBasedProjects[tfm] = RunEvaluate(projectPath, tfm);
}
+ innerBuildWatch.Stop();
_logger?.LogDebug("Project is SDK style, multi-target, evaluation succeeded.");
return result = MultiTargetEvaluationResult.CreateSuccess(projectPath, evaluatedProject, evaluatedTfmBasedProjects);
@@ -177,6 +182,9 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin
}
finally
{
+ watch.Stop();
+ innerBuildWatch.Stop();
+
string? targetFrameworksString = null;
if (targetFrameworks != null)
@@ -196,7 +204,13 @@ private MSBuildEvaluationResult EvaluateProjectInternal(IEngineEnvironmentSettin
{ "TargetFrameworks", targetFrameworksString ?? ""},
};
- TelemetryEventEntry.TrackEvent("new/msbuild-eval", properties);
+ Dictionary measurements = new()
+ {
+ { "EvaluationTime", watch.ElapsedMilliseconds },
+ { "InnerEvaluationTime", innerBuildWatch.ElapsedMilliseconds }
+ };
+
+ TelemetryEventEntry.TrackEvent("new/msbuild-eval", properties, measurements);
}
}
diff --git a/src/Cli/dotnet/Commands/New/NewCommandParser.cs b/src/Cli/dotnet/Commands/New/NewCommandParser.cs
index 68b5268c8579..4a2f614f2ca8 100644
--- a/src/Cli/dotnet/Commands/New/NewCommandParser.cs
+++ b/src/Cli/dotnet/Commands/New/NewCommandParser.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.CommandLine;
+using System.Diagnostics;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Commands.New.MSBuildEvaluation;
using Microsoft.DotNet.Cli.Commands.New.PostActions;
@@ -15,6 +16,7 @@
using Microsoft.TemplateEngine.Abstractions.Constraints;
using Microsoft.TemplateEngine.Abstractions.TemplatePackage;
using Microsoft.TemplateEngine.Cli;
+using Microsoft.TemplateEngine.Cli.Commands;
using Microsoft.TemplateEngine.Cli.PostActionProcessors;
using Command = System.CommandLine.Command;
diff --git a/src/Cli/dotnet/Commands/New/OptionalWorkloadProvider.cs b/src/Cli/dotnet/Commands/New/OptionalWorkloadProvider.cs
index 77e48fe7b32a..cf803c32c3c3 100644
--- a/src/Cli/dotnet/Commands/New/OptionalWorkloadProvider.cs
+++ b/src/Cli/dotnet/Commands/New/OptionalWorkloadProvider.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer;
using Microsoft.TemplateEngine.Abstractions;
@@ -20,8 +22,8 @@ internal OptionalWorkloadProvider(ITemplatePackageProviderFactory factory, IEngi
public ITemplatePackageProviderFactory Factory { get; }
- // To avoid warnings about being unused, implement empty add/remove accessors.
- event Action? ITemplatePackageProvider.TemplatePackagesChanged
+ // To avoid warnings about unused, its implemented via add/remove
+ event Action ITemplatePackageProvider.TemplatePackagesChanged
{
add { }
remove { }
@@ -31,13 +33,14 @@ public Task> GetAllTemplatePackagesAsync(Cancell
{
var list = new List();
var optionalWorkloadLocator = new TemplateLocator.TemplateLocator();
- var sdksDirectory = new DirectoryInfo(MSBuildForwardingAppWithoutLogging.GetMSBuildSDKsPath());
- var sdkDirectory = sdksDirectory?.Parent;
- var sdkVersion = sdkDirectory?.Name;
- var dotnetRootPath = sdkDirectory?.Parent?.Parent;
+#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file
+ var sdkDirectory = Path.GetDirectoryName(typeof(DotnetFiles).Assembly.Location);
+#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file
+ var sdkVersion = Path.GetFileName(sdkDirectory);
+ var dotnetRootPath = Path.GetDirectoryName(Path.GetDirectoryName(sdkDirectory));
string userProfileDir = CliFolderPathCalculator.DotnetUserProfileFolderPath;
- var packages = optionalWorkloadLocator.GetDotnetSdkTemplatePackages(sdkVersion, dotnetRootPath?.FullName, userProfileDir);
+ var packages = optionalWorkloadLocator.GetDotnetSdkTemplatePackages(sdkVersion, dotnetRootPath, userProfileDir);
var fileSystem = _environmentSettings.Host.FileSystem;
foreach (var packageInfo in packages)
{
diff --git a/src/Cli/dotnet/Commands/Pack/PackCommand.cs b/src/Cli/dotnet/Commands/Pack/PackCommand.cs
index 9300fa1b8310..ab7d0ba0542b 100644
--- a/src/Cli/dotnet/Commands/Pack/PackCommand.cs
+++ b/src/Cli/dotnet/Commands/Pack/PackCommand.cs
@@ -5,6 +5,7 @@
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.DotNet.Cli.CommandLine;
+using Microsoft.DotNet.Cli.Commands.Build;
using Microsoft.DotNet.Cli.Commands.Restore;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Extensions;
@@ -12,6 +13,7 @@
using Microsoft.DotNet.Cli.Utils;
using NuGet.Commands;
using NuGet.Common;
+using NuGet.Packaging;
namespace Microsoft.DotNet.Cli.Commands.Pack;
@@ -38,7 +40,7 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui
bool noRestore = noBuild || parseResult.HasOption(definition.NoRestoreOption);
- return DotNetCommandFactory.CreateVirtualOrPhysicalCommand(
+ return CommandFactory.CreateVirtualOrPhysicalCommand(
definition,
definition.SlnOrProjectOrFileArgument,
(msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand(
diff --git a/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs b/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs
index 370d35d6c816..7c418218f52d 100644
--- a/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Pack/PackCommandParser.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.CommandLine;
using Microsoft.DotNet.Cli.CommandLine;
namespace Microsoft.DotNet.Cli.Commands.Pack;
diff --git a/src/Cli/dotnet/Commands/Project/ProjectCommandParser.cs b/src/Cli/dotnet/Commands/Project/ProjectCommandParser.cs
index 9ec9c4acd7a1..36d44cee3931 100644
--- a/src/Cli/dotnet/Commands/Project/ProjectCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Project/ProjectCommandParser.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.CommandLine;
using Microsoft.DotNet.Cli.Commands.Project.Convert;
using Microsoft.DotNet.Cli.Extensions;
diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs
index 6188060bf16d..bb68394da143 100644
--- a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs
+++ b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs
@@ -44,7 +44,7 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui
bool noRestore = noBuild || parseResult.HasOption(definition.NoRestoreOption);
- return DotNetCommandFactory.CreateVirtualOrPhysicalCommand(
+ return CommandFactory.CreateVirtualOrPhysicalCommand(
definition,
definition.SlnOrProjectOrFileArgument,
(msbuildArgs, appFilePath) => new VirtualProjectBuildingCommand(
diff --git a/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs b/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs
index 8f2e96fb3f91..2743d1ffe048 100644
--- a/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs
+++ b/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs
@@ -24,7 +24,7 @@ public static CommandBase FromParseResult(ParseResult result, string? msbuildPat
result.HandleDebugSwitch();
result.ShowHelpOrErrorIfAppropriate();
- return DotNetCommandFactory.CreateVirtualOrPhysicalCommand(
+ return CommandFactory.CreateVirtualOrPhysicalCommand(
definition,
definition.SlnOrProjectOrFileArgument,
static (msbuildArgs, appFilePath) =>
diff --git a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs
index 4a16683d53f8..0d3ab62507b8 100644
--- a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs
+++ b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs
@@ -48,9 +48,13 @@ public static void TrackRunEvent(
{
["app_type"] = isFileBased ? "file_based" : "project_based",
["project_id"] = projectIdentifier,
- ["sdk_count"] = sdkCount.ToString(),
- ["package_reference_count"] = packageReferenceCount.ToString(),
- ["project_reference_count"] = projectReferenceCount.ToString(),
+ };
+
+ var measurements = new Dictionary
+ {
+ ["sdk_count"] = sdkCount,
+ ["package_reference_count"] = packageReferenceCount,
+ ["project_reference_count"] = projectReferenceCount,
};
// Launch profile telemetry
@@ -76,7 +80,7 @@ public static void TrackRunEvent(
// File-based app specific telemetry
if (isFileBased)
{
- properties["additional_properties_count"] = additionalPropertiesCount.ToString();
+ measurements["additional_properties_count"] = additionalPropertiesCount;
if (usedMSBuild.HasValue)
{
properties["used_msbuild"] = usedMSBuild.Value ? "true" : "false";
@@ -87,7 +91,7 @@ public static void TrackRunEvent(
}
}
- TelemetryEventEntry.TrackEvent(RunEventName, properties);
+ TelemetryEventEntry.TrackEvent(RunEventName, properties, measurements);
}
///
diff --git a/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs b/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs
index 708e9d0fa93e..61d4b803baa7 100644
--- a/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs
+++ b/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs
@@ -38,7 +38,7 @@ public ToolExecuteCommand(ParseResult result, ToolManifestFinder? toolManifestFi
: base(result)
{
_packageToolIdentityArgument = result.GetValue(Definition.PackageIdentityArgument);
- _forwardArguments = result.GetValue(Definition.CommandArgument) ?? [];
+ _forwardArguments = result.GetValue(Definition.CommandArgument) ?? Enumerable.Empty();
_allowRollForward = result.GetValue(Definition.RollForwardOption);
_configFile = result.GetValue(Definition.ConfigOption);
_sources = result.GetValue(Definition.SourceOption) ?? [];
diff --git a/src/Cli/dotnet/Commands/Tool/Run/ToolRunCommand.cs b/src/Cli/dotnet/Commands/Tool/Run/ToolRunCommand.cs
index 72262f1294c0..4b42713ae006 100644
--- a/src/Cli/dotnet/Commands/Tool/Run/ToolRunCommand.cs
+++ b/src/Cli/dotnet/Commands/Tool/Run/ToolRunCommand.cs
@@ -34,7 +34,7 @@ public override int Execute()
public static int ExecuteCommand(LocalToolsCommandResolver commandResolver, string? toolCommandName, IEnumerable? argumentsToForward, bool allowRollForward)
{
using var _ = Activities.Source.StartActivity("execute-local-tool");
- CommandSpec? commandSpec = commandResolver.ResolveStrict(new CommandResolverArguments()
+ CommandSpec commandSpec = commandResolver.ResolveStrict(new CommandResolverArguments()
{
// since LocalToolsCommandResolver is a resolver, and all resolver input have dotnet-
CommandName = $"dotnet-{toolCommandName}",
@@ -49,4 +49,4 @@ public static int ExecuteCommand(LocalToolsCommandResolver commandResolver, stri
var result = CommandFactoryUsingResolver.Create(commandSpec).Execute();
return result.ExitCode;
}
-}
+}
\ No newline at end of file
diff --git a/src/Cli/dotnet/Commands/Tool/ToolCommandSpecCreator.cs b/src/Cli/dotnet/Commands/Tool/ToolCommandSpecCreator.cs
index 84249ba7b5c4..f043eafc93be 100644
--- a/src/Cli/dotnet/Commands/Tool/ToolCommandSpecCreator.cs
+++ b/src/Cli/dotnet/Commands/Tool/ToolCommandSpecCreator.cs
@@ -11,20 +11,30 @@ internal class ToolCommandSpecCreator
{
public static CommandSpec CreateToolCommandSpec(string toolName, string toolExecutable, string toolRunner, bool allowRollForward, IEnumerable commandArguments)
{
- var environment = ActivityContextFactory.MakeActivityContextEnvironment();
- switch (toolRunner)
+ if (toolRunner == "dotnet")
{
- case "dotnet":
- if (allowRollForward)
- {
- commandArguments = ["--allow-roll-forward", .. commandArguments];
- }
- return MuxerCommandSpecMaker.CreatePackageCommandSpecUsingMuxer(toolExecutable, commandArguments, environment);
- case "executable":
- var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(commandArguments);
- return new CommandSpec(toolExecutable, escapedArgs, environment);
- default:
- throw new GracefulException(string.Format(CliStrings.ToolSettingsUnsupportedRunner, toolName, toolRunner));
+ if (allowRollForward)
+ {
+ commandArguments = ["--allow-roll-forward", .. commandArguments];
+ }
+
+ return MuxerCommandSpecMaker.CreatePackageCommandSpecUsingMuxer(
+ toolExecutable,
+ commandArguments);
+ }
+ else if (toolRunner == "executable")
+ {
+ var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(
+ commandArguments);
+
+ return new CommandSpec(
+ toolExecutable,
+ escapedArgs);
+ }
+ else
+ {
+ throw new GracefulException(string.Format(CliStrings.ToolSettingsUnsupportedRunner,
+ toolName, toolRunner));
}
}
}
diff --git a/src/Cli/dotnet/DotNetCommandFactory.cs b/src/Cli/dotnet/DotNetCommandFactory.cs
new file mode 100644
index 000000000000..dcb70b05e6c9
--- /dev/null
+++ b/src/Cli/dotnet/DotNetCommandFactory.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.CommandLine.Invocation;
+using System.Diagnostics;
+using Microsoft.DotNet.Cli.CommandFactory;
+using Microsoft.DotNet.Cli.Utils;
+using NuGet.Frameworks;
+
+namespace Microsoft.DotNet.Cli;
+
+public class DotNetCommandFactory(bool alwaysRunOutOfProc = false, string currentWorkingDirectory = null) : ICommandFactory
+{
+ private readonly bool _alwaysRunOutOfProc = alwaysRunOutOfProc;
+ private readonly string _currentWorkingDirectory = currentWorkingDirectory;
+
+ public ICommand Create(
+ string commandName,
+ IEnumerable args,
+ NuGetFramework framework = null,
+ string configuration = Constants.DefaultConfiguration)
+ {
+ if (!_alwaysRunOutOfProc && TryGetBuiltInCommand(commandName, out var builtInCommand))
+ {
+ Debug.Assert(framework == null, "BuiltInCommand doesn't support the 'framework' argument.");
+ Debug.Assert(configuration == Constants.DefaultConfiguration, "BuiltInCommand doesn't support the 'configuration' argument.");
+
+ return new BuiltInCommand(commandName, args, builtInCommand);
+ }
+
+ return CommandFactoryUsingResolver.CreateDotNet(commandName, args, framework, configuration, _currentWorkingDirectory);
+ }
+
+ private static bool TryGetBuiltInCommand(string commandName, out Func commandFunc)
+ {
+ var command = Parser.GetBuiltInCommand(commandName);
+ if (command?.Action is AsynchronousCommandLineAction action)
+ {
+ commandFunc = (args) => Parser.Invoke([commandName, .. args]);
+ return true;
+ }
+ commandFunc = null;
+ return false;
+ }
+}
diff --git a/src/Cli/dotnet/Extensions/ActivityExtensions.cs b/src/Cli/dotnet/Extensions/ActivityExtensions.cs
deleted file mode 100644
index b29d0d86de94..000000000000
--- a/src/Cli/dotnet/Extensions/ActivityExtensions.cs
+++ /dev/null
@@ -1,22 +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.CommandLine;
-using System.Diagnostics;
-
-namespace Microsoft.DotNet.Cli.Extensions;
-
-internal static class ActivityExtensions
-{
- public static void SetDisplayName(this Activity? activity, ParseResult parseResult)
- {
- if (activity is null)
- {
- return;
- }
-
- var name = parseResult.GetCommandName();
- activity.DisplayName = name;
- activity.SetTag("command.name", name);
- }
-}
diff --git a/src/Cli/dotnet/Extensions/ParseResultExtensions.cs b/src/Cli/dotnet/Extensions/ParseResultExtensions.cs
index 865ae37cdf18..dc23ee379040 100644
--- a/src/Cli/dotnet/Extensions/ParseResultExtensions.cs
+++ b/src/Cli/dotnet/Extensions/ParseResultExtensions.cs
@@ -14,27 +14,25 @@ namespace Microsoft.DotNet.Cli.Extensions;
public static class ParseResultExtensions
{
- ///
+ ///
/// Finds the command of the parse result and invokes help for that command.
/// If no command is specified, invokes help for the application.
- ///
- ///
+ ///
+ ///
/// This is accomplished by finding a set of tokens that should be valid and appending a help token
/// to that list, then re-parsing the list of tokens. This is not ideal - either we should have a direct way
/// of invoking help for a ParseResult, or we should eliminate this custom, ad-hoc help invocation by moving
/// more situations that want to show help into Parsing Errors (which trigger help in the default System.CommandLine pipeline)
/// or custom Invocation Middleware, so we can more easily create our version of a HelpResult type.
- ///
+ ///
public static void ShowHelp(this ParseResult parseResult)
{
- // Take from the start of the list until we hit an option/--/unparsed token.
- // Since commands can have arguments, we must take those as well in order to get accurate help.
- var filteredTokenValues = parseResult.Tokens.TakeWhile(token =>
- token.Type == TokenType.Argument
- || token.Type == TokenType.Command
- || token.Type == TokenType.Directive)
- .Select(t => t.Value);
- Parser.Parse([.. filteredTokenValues, "-h"]).Invoke();
+ // take from the start of the list until we hit an option/--/unparsed token
+ // since commands can have arguments, we must take those as well in order to get accurate help
+ Parser.Parse([
+ ..parseResult.Tokens.TakeWhile(token => token.Type == TokenType.Argument || token.Type == TokenType.Command || token.Type == TokenType.Directive).Select(t => t.Value),
+ "-h"
+ ]).Invoke();
}
public static void ShowHelpOrErrorIfAppropriate(this ParseResult parseResult)
@@ -47,26 +45,24 @@ public static void ShowHelpOrErrorIfAppropriate(this ParseResult parseResult)
var rawResourcePartsForThisLocale = DistinctFormatStringParts(CliStrings.UnrecognizedCommandOrArgument);
return ErrorContainsAllParts(error.Message, rawResourcePartsForThisLocale);
});
-
- if (parseResult.CommandResult.Command.TreatUnmatchedTokensAsErrors
- || parseResult.Errors.Except(unrecognizedTokenErrors).Any())
+ if (parseResult.CommandResult.Command.TreatUnmatchedTokensAsErrors ||
+ parseResult.Errors.Except(unrecognizedTokenErrors).Any())
{
throw new CommandParsingException(
- message: string.Join(Environment.NewLine, parseResult.Errors.Select(e => e.Message)),
+ message: string.Join(Environment.NewLine,
+ parseResult.Errors.Select(e => e.Message)),
parseResult: parseResult);
}
}
- ///
- /// Splits a .NET format string by the format placeholders (the {N} parts) to get an array of the literal parts, to be used in message-checking.
- ///
- static string[] DistinctFormatStringParts(string formatString) =>
- // Match the literal '{', followed by any of 0-9 one or more times, followed by the literal '}'.
- Regex.Split(formatString, @"{[0-9]+}");
+ ///Splits a .NET format string by the format placeholders (the {N} parts) to get an array of the literal parts, to be used in message-checking
+ static string[] DistinctFormatStringParts(string formatString)
+ {
+ return Regex.Split(formatString, @"{[0-9]+}"); // match the literal '{', followed by any of 0-9 one or more times, followed by the literal '}'
+ }
+
- ///
- /// Given a string and a series of parts, ensures that all parts are present in the string in sequential order.
- ///
+ /// given a string and a series of parts, ensures that all parts are present in the string in sequential order
static bool ErrorContainsAllParts(ReadOnlySpan error, string[] parts)
{
foreach (var part in parts)
@@ -77,29 +73,39 @@ static bool ErrorContainsAllParts(ReadOnlySpan error, string[] parts)
error = error.Slice(foundIndex + part.Length);
continue;
}
-
- return false;
+ else
+ {
+ return false;
+ }
}
-
return true;
}
}
- public static string RootSubCommandResult(this ParseResult parseResult) => parseResult.RootCommandResult.Children?
- .Select(child => parseResult.GetSymbolResultValue(child))
- .FirstOrDefault(subcommand => !string.IsNullOrEmpty(subcommand)) ?? string.Empty;
+ public static string RootSubCommandResult(this ParseResult parseResult)
+ {
+ return parseResult.RootCommandResult.Children?
+ .Select(child => GetSymbolResultValue(parseResult, child))
+ .FirstOrDefault(subcommand => !string.IsNullOrEmpty(subcommand)) ?? string.Empty;
+ }
- public static bool IsDotnetBuiltInCommand(this ParseResult parseResult) =>
- string.IsNullOrEmpty(parseResult.RootSubCommandResult())
- || Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null;
+ public static bool IsDotnetBuiltInCommand(this ParseResult parseResult)
+ {
+ return string.IsNullOrEmpty(parseResult.RootSubCommandResult()) ||
+ Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null;
+ }
- public static bool IsTopLevelDotnetCommand(this ParseResult parseResult) =>
- parseResult.CommandResult.Command.Equals(Parser.RootCommand) && string.IsNullOrEmpty(parseResult.RootSubCommandResult());
+ public static bool IsTopLevelDotnetCommand(this ParseResult parseResult)
+ {
+ return parseResult.CommandResult.Command.Equals(Parser.RootCommand) && string.IsNullOrEmpty(parseResult.RootSubCommandResult());
+ }
- public static bool CanBeInvoked(this ParseResult parseResult) =>
- Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null
- || parseResult.Tokens.Any(token => token.Type == TokenType.Directive)
- || (parseResult.IsTopLevelDotnetCommand() && string.IsNullOrEmpty(parseResult.GetValue(Parser.RootCommand.DotnetSubCommand)));
+ public static bool CanBeInvoked(this ParseResult parseResult)
+ {
+ return Parser.GetBuiltInCommand(parseResult.RootSubCommandResult()) != null ||
+ parseResult.Tokens.Any(token => token.Type == TokenType.Directive) ||
+ (parseResult.IsTopLevelDotnetCommand() && string.IsNullOrEmpty(parseResult.GetValue(Parser.RootCommand.DotnetSubCommand)));
+ }
public static int HandleMissingCommand(this ParseResult parseResult)
{
@@ -108,8 +114,12 @@ public static int HandleMissingCommand(this ParseResult parseResult)
return 1;
}
- public static string[] GetArguments(this ParseResult parseResult) =>
- parseResult.Tokens.Select(t => t.Value).ToArray().GetSubArguments();
+ public static string[] GetArguments(this ParseResult parseResult)
+ {
+ return parseResult.Tokens.Select(t => t.Value)
+ .ToArray()
+ .GetSubArguments();
+ }
public static string[] GetSubArguments(this string[] args)
{
@@ -121,30 +131,54 @@ public static string[] GetSubArguments(this string[] args)
var runArgs = dashDashIndex > -1 ? subargs.GetRange(dashDashIndex, subargs.Count() - dashDashIndex) : [];
subargs = dashDashIndex > -1 ? subargs.GetRange(0, dashDashIndex) : subargs;
- // Remove top level command (ex build or publish).
- var subargsFiltered = subargs
- .SkipWhile(arg => Parser.RootCommand.DiagOption.Name.Equals(arg)
- || Parser.RootCommand.DiagOption.Aliases.Contains(arg)
- || arg.Equals("dotnet"))
- .Skip(1);
+ return
+ [
+ .. subargs
+ .SkipWhile(arg => Parser.RootCommand.DiagOption.Name.Equals(arg) || Parser.RootCommand.DiagOption.Aliases.Contains(arg) || arg.Equals("dotnet"))
+ .Skip(1), // remove top level command (ex build or publish)
+ .. runArgs
+ ];
+ }
+
+ public static bool DiagOptionPrecedesSubcommand(this string[] args, string subCommand)
+ {
+ if (string.IsNullOrEmpty(subCommand))
+ {
+ return true;
+ }
+
+ for (var i = 0; i < args.Length; i++)
+ {
+ if (args[i].Equals(subCommand))
+ {
+ return false;
+ }
+ else if (Parser.RootCommand.DiagOption.Name.Equals(args) || Parser.RootCommand.DiagOption.Aliases.Contains(args[i]))
+ {
+ return true;
+ }
+ }
- return [.. subargsFiltered, .. runArgs];
+ return false;
}
- private static string? GetSymbolResultValue(this ParseResult parseResult, SymbolResult symbolResult) => symbolResult switch
+ private static string? GetSymbolResultValue(ParseResult parseResult, SymbolResult symbolResult) => symbolResult switch
{
CommandResult commandResult => commandResult.Command.Name,
ArgumentResult argResult => argResult.Tokens.FirstOrDefault()?.Value,
_ => parseResult.GetResult(Parser.RootCommand.DotnetSubCommand)?.GetValueOrDefault()
};
- public static IEnumerable? GetRunCommandShorthandProjectValues(this ParseResult parseResult) =>
- parseResult.GetRunPropertyOptions(true)?.Where(property => !property.Contains("="));
+ public static IEnumerable? GetRunCommandShorthandProjectValues(this ParseResult parseResult)
+ {
+ var properties = GetRunPropertyOptions(parseResult, true);
+ return properties?.Where(property => !property.Contains("="));
+ }
public static IEnumerable GetRunCommandPropertyValues(this ParseResult parseResult)
{
- var shorthandProperties = parseResult.GetRunPropertyOptions(true)?.Where(property => property.Contains("="));
- var longhandProperties = parseResult.GetRunPropertyOptions(false);
+ var shorthandProperties = GetRunPropertyOptions(parseResult, true)?.Where(property => property.Contains("="));
+ var longhandProperties = GetRunPropertyOptions(parseResult, false);
return (shorthandProperties, longhandProperties) switch
{
(null, null) => Enumerable.Empty(),
@@ -154,7 +188,7 @@ public static IEnumerable GetRunCommandPropertyValues(this ParseResult p
};
}
- private static IEnumerable? GetRunPropertyOptions(this ParseResult parseResult, bool shorthand)
+ private static IEnumerable? GetRunPropertyOptions(ParseResult parseResult, bool shorthand)
{
var optionString = shorthand ? "-p" : "--property";
var propertyOptions = parseResult.CommandResult.Children.Where(c => GetOptionTokenOrDefault(c)?.Value.Equals(optionString) ?? false);
@@ -180,26 +214,4 @@ public static void HandleDebugSwitch(this ParseResult parseResult)
DebugHelper.WaitForDebugger();
}
}
-
- public static string GetCommandName(this ParseResult parseResult)
- {
- // Walk the parent command tree to find the top-level command name and get the full command name for this ParseResult.
- List parentNames = [parseResult.CommandResult.Command.Name];
- var current = parseResult.CommandResult.Parent;
- while (current is CommandResult parentCommandResult)
- {
- parentNames.Add(parentCommandResult.Command.Name);
- current = parentCommandResult.Parent;
- }
- parentNames.Reverse();
-
- // Options that perform terminating actions are considered part of the command name as they are essentially subcommands themselves.
- // Example: dotnet --version
- if (parseResult.Action is InvocableOptionAction { Terminating: true } optionAction)
- {
- parentNames.Add(optionAction.Option.Name);
- }
-
- return string.Join(' ', parentNames);
- }
}
diff --git a/src/Cli/dotnet/Parser.cs b/src/Cli/dotnet/Parser.cs
index 42b129db7067..475d59655a3b 100644
--- a/src/Cli/dotnet/Parser.cs
+++ b/src/Cli/dotnet/Parser.cs
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.CommandLine;
-using System.CommandLine.Help;
+using System.CommandLine.Invocation;
using System.CommandLine.StaticCompletions;
using System.Reflection;
using Microsoft.DotNet.Cli.Commands;
@@ -45,6 +47,7 @@
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.TemplateEngine.Cli;
+using Microsoft.TemplateEngine.Cli.Help;
using Command = System.CommandLine.Command;
namespace Microsoft.DotNet.Cli;
@@ -63,14 +66,19 @@ private static DotNetCommandDefinition CreateCommand()
{
rootCommand.Options.RemoveAt(i);
}
- else if (option is HelpOption helpOption)
+ else if (option is System.CommandLine.Help.HelpOption helpOption)
{
- helpOption.Action = new PrintHelpAction(helpOption, DotnetHelpBuilder.Instance.Value);
+ helpOption.Action = new DotnetHelpAction()
+ {
+ Builder = DotnetHelpBuilder.Instance.Value
+ };
+
option.Description = CliStrings.ShowHelpDescription;
}
}
// Augment the definition of each subcommand with command-specific actions and completions.
+
AddCommandParser.ConfigureCommand(rootCommand.AddCommand);
BuildCommandParser.ConfigureCommand(rootCommand.BuildCommand);
BuildServerCommandParser.ConfigureCommand(rootCommand.BuildServerCommand);
@@ -114,10 +122,7 @@ private static DotNetCommandDefinition CreateCommand()
WorkloadCommandParser.ConfigureCommand(rootCommand.WorkloadCommand);
CompletionsCommandParser.ConfigureCommand(rootCommand.CompletionsCommand);
- rootCommand.DiagOption.Action = new HandleDiagnosticAction(rootCommand.DiagOption);
- rootCommand.VersionOption.Action = new PrintVersionAction(rootCommand.VersionOption);
- rootCommand.InfoOption.Action = new PrintInfoAction(rootCommand.InfoOption);
- rootCommand.CliSchemaOption.Action = new PrintCliSchemaAction(rootCommand.CliSchemaOption);
+ rootCommand.CliSchemaOption.Action = new PrintCliSchemaAction();
// TODO: https://github.com/dotnet/sdk/issues/52661
// https://github.com/NuGet/NuGet.Client/blob/bf048eb714eb6b1912ba868edca4c7cfec454841/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs
@@ -128,13 +133,13 @@ private static DotNetCommandDefinition CreateCommand()
{
if (parseResult.GetValue(rootCommand.DiagOption) && parseResult.Tokens.Count == 1)
{
- // When user does not specify any args except of diagnostics ("dotnet -d"),
- // we do nothing as HandleDiagnosticAction already enabled the diagnostic output.
+ // when user does not specify any args except of diagnostics ("dotnet -d"), we do nothing
+ // as Program.ProcessArgs already enabled the diagnostic output
return 0;
}
else
{
- // When user does not specify any args (just "dotnet"), a usage needs to be printed.
+ // when user does not specify any args (just "dotnet"), a usage needs to be printed
parseResult.InvocationConfiguration.Output.WriteLine(CliUsage.HelpText);
return 0;
}
@@ -143,14 +148,14 @@ private static DotNetCommandDefinition CreateCommand()
return rootCommand;
}
- public static Command? GetBuiltInCommand(string commandName) =>
+ public static Command GetBuiltInCommand(string commandName) =>
RootCommand.Subcommands.FirstOrDefault(c => c.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
///
/// Implements token-per-line response file handling for the CLI. We use this instead of the built-in S.CL handling
/// to ensure backwards-compatibility with MSBuild.
///
- public static bool TokenPerLine(string tokenToReplace, out IReadOnlyList? replacementTokens, out string? errorMessage)
+ public static bool TokenPerLine(string tokenToReplace, out IReadOnlyList replacementTokens, out string errorMessage)
{
var filePath = Path.GetFullPath(tokenToReplace);
if (File.Exists(filePath))
@@ -210,7 +215,7 @@ public static bool TokenPerLine(string tokenToReplace, out IReadOnlyList
public static int Invoke(string[] args) => Invoke(Parse(args));
public static Task InvokeAsync(string[] args, CancellationToken cancellationToken = default) => InvokeAsync(Parse(args), cancellationToken);
- internal static int ExceptionHandler(Exception? exception, ParseResult parseResult)
+ internal static int ExceptionHandler(Exception exception, ParseResult parseResult)
{
if (exception is TargetInvocationException)
{
@@ -230,13 +235,13 @@ internal static int ExceptionHandler(Exception? exception, ParseResult parseResu
exception.Message.Red().Bold());
parseResult.ShowHelp();
}
- else if (exception is not null && exception.GetType().Name.Equals("WorkloadManifestCompositionException"))
+ else if (exception.GetType().Name.Equals("WorkloadManifestCompositionException"))
{
Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ?
exception.ToString().Red().Bold() :
exception.Message.Red().Bold());
}
- else if (exception is not null)
+ else
{
Reporter.Error.Write("Unhandled exception: ".Red().Bold());
Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ?
@@ -324,7 +329,7 @@ public override void Write(HelpContext context)
}
else if (command is FormatCommandDefinition format)
{
- var arguments = context.ParseResult.GetValue(format.Arguments) ?? [];
+ var arguments = context.ParseResult.GetValue(format.Arguments);
new FormatForwardingApp([.. arguments, .. helpArgs]).Execute();
}
else if (command is FsiCommandDefinition)
@@ -346,16 +351,14 @@ public override void Write(HelpContext context)
if (command.Name.Equals(ListReferenceCommandDefinition.Name))
{
- Command? listCommand = command.Parents.Single() as Command;
- if (listCommand is not null)
+ Command listCommand = command.Parents.Single() as Command;
+
+ for (int i = 0; i < listCommand.Arguments.Count; i++)
{
- for (int i = 0; i < listCommand.Arguments.Count; i++)
+ if (listCommand.Arguments[i].Name == CliStrings.SolutionOrProjectArgumentName)
{
- if (listCommand.Arguments[i].Name == CliStrings.SolutionOrProjectArgumentName)
- {
- // Name is immutable now, so we create a new Argument with the right name..
- listCommand.Arguments[i] = ListCommandDefinition.CreateSlnOrProjectArgument(CliStrings.ProjectArgumentName, CliStrings.ProjectArgumentDescription);
- }
+ // Name is immutable now, so we create a new Argument with the right name..
+ listCommand.Arguments[i] = Commands.Hidden.List.ListCommandDefinition.CreateSlnOrProjectArgument(CliStrings.ProjectArgumentName, CliStrings.ProjectArgumentDescription);
}
}
}
@@ -377,4 +380,15 @@ public override void Write(HelpContext context)
}
}
}
+
+ private class PrintCliSchemaAction : SynchronousCommandLineAction
+ {
+ public override bool Terminating => true;
+
+ public override int Invoke(ParseResult parseResult)
+ {
+ CliSchema.PrintCliSchema(parseResult.CommandResult, parseResult.InvocationConfiguration.Output, Program.TelemetryClient);
+ return 0;
+ }
+ }
}
diff --git a/src/Cli/dotnet/ParserOptionActions.cs b/src/Cli/dotnet/ParserOptionActions.cs
deleted file mode 100644
index 8186712fa4de..000000000000
--- a/src/Cli/dotnet/ParserOptionActions.cs
+++ /dev/null
@@ -1,171 +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.CommandLine;
-using System.CommandLine.Invocation;
-using Microsoft.DotNet.Cli.Commands.Workload;
-using Microsoft.DotNet.Cli.Extensions;
-using Microsoft.DotNet.Cli.Help;
-using Microsoft.DotNet.Cli.Utils;
-using Microsoft.DotNet.Configurer;
-using RuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment;
-
-namespace Microsoft.DotNet.Cli;
-
-///
-/// Represents an option that contains an invocable action.
-/// These are essentially commands that are only defined as an option.
-///
-internal abstract class InvocableOptionAction(Option option) : SynchronousCommandLineAction
-{
- ///
- /// The option for which this action is bound.
- ///
- public Option Option { get; } = option;
-}
-
-internal class HandleDiagnosticAction(Option option) : InvocableOptionAction(option)
-{
- public override bool Terminating => false;
-
- public override int Invoke(ParseResult parseResult)
- {
- // Only set verbose output on built-in commands.
- if (!parseResult.IsDotnetBuiltInCommand())
- {
- return 0;
- }
-
- // Determine whether the diagnostic option should be attached to the dotnet command or the subcommand.
- if (DiagOptionPrecedesSubcommand(parseResult.Tokens.Select(t => t.Value), parseResult.RootSubCommandResult()))
- {
- Environment.SetEnvironmentVariable(CommandLoggingContext.Variables.Verbose, bool.TrueString);
- CommandLoggingContext.SetVerbose(true);
- Reporter.Reset();
-
- var home = Env.GetEnvironmentVariable(CliFolderPathCalculator.DotnetHomeVariableName);
- if (!string.IsNullOrEmpty(home))
- {
- // Output DOTNET_CLI_HOME usage when verbosity is enabled.
- Reporter.Verbose.WriteLine(string.Format(LocalizableStrings.DotnetCliHomeUsed, home, CliFolderPathCalculator.DotnetHomeVariableName));
- }
- }
-
- return 0;
- }
-
- private static bool DiagOptionPrecedesSubcommand(IEnumerable tokens, string subCommand)
- {
- if (string.IsNullOrEmpty(subCommand))
- {
- return true;
- }
-
- foreach (var token in tokens)
- {
- if (token == subCommand)
- {
- return false;
- }
-
- if (Parser.RootCommand.DiagOption.Name == token
- || Parser.RootCommand.DiagOption.Aliases.Contains(token))
- {
- return true;
- }
- }
-
- return false;
- }
-}
-
-internal class PrintHelpAction(Option option, HelpBuilder builder) : InvocableOptionAction(option)
-{
- public override bool Terminating => true;
- public override bool ClearsParseErrors => true;
-
- private HelpBuilder Builder { get; } = builder;
-
- public override int Invoke(ParseResult parseResult)
- {
- var command = parseResult.CommandResult.Command;
- var output = parseResult.InvocationConfiguration.Output;
- var helpContext = new HelpContext(Builder, command, output, parseResult);
- Builder.Write(helpContext);
-
- return 0;
- }
-}
-
-internal class PrintVersionAction(Option option) : InvocableOptionAction(option)
-{
- public override bool Terminating => true;
-
- public override int Invoke(ParseResult parseResult)
- {
- // Only print for top-level commands.
- if (!parseResult.IsTopLevelDotnetCommand())
- {
- return 0;
- }
-
- Reporter.Output.WriteLine(Product.Version);
-
- return 0;
- }
-}
-
-internal class PrintInfoAction(Option option) : InvocableOptionAction(option)
-{
- public override bool Terminating => true;
-
- public override int Invoke(ParseResult parseResult)
- {
- // Only print for top-level commands.
- if (!parseResult.IsTopLevelDotnetCommand())
- {
- return 0;
- }
-
- DotnetVersionFile versionFile = DotnetFiles.VersionFileObject;
- var commitSha = versionFile.CommitSha ?? "N/A";
- Reporter.Output.WriteLine($"{LocalizableStrings.DotNetSdkInfoLabel}");
- Reporter.Output.WriteLine($" Version: {Product.Version}");
- Reporter.Output.WriteLine($" Commit: {commitSha}");
- Reporter.Output.WriteLine($" Workload version: {WorkloadInfoHelper.GetWorkloadsVersion()}");
- Reporter.Output.WriteLine($" MSBuild version: {MSBuildForwardingAppWithoutLogging.MSBuildVersion}");
- Reporter.Output.WriteLine();
- Reporter.Output.WriteLine($"{LocalizableStrings.DotNetRuntimeInfoLabel}");
- Reporter.Output.WriteLine($" OS Name: {RuntimeEnvironment.OperatingSystem}");
- Reporter.Output.WriteLine($" OS Version: {RuntimeEnvironment.OperatingSystemVersion}");
- Reporter.Output.WriteLine($" OS Platform: {RuntimeEnvironment.OperatingSystemPlatform}");
- Reporter.Output.WriteLine($" RID: {GetDisplayRid(versionFile)}");
- Reporter.Output.WriteLine($" Base Path: {AppContext.BaseDirectory}");
- Reporter.Output.WriteLine();
- Reporter.Output.WriteLine($"{LocalizableStrings.DotnetWorkloadInfoLabel}");
- new WorkloadInfoHelper(isInteractive: false).ShowWorkloadsInfo(showVersion: false);
-
- return 0;
- }
-
- private static string? GetDisplayRid(DotnetVersionFile versionFile)
- {
- FrameworkDependencyFile fxDepsFile = new();
- string currentRid = RuntimeInformation.RuntimeIdentifier;
- // If the current RID isn't supported by the shared framework, display the RID the CLI was built with instead,
- // so the user knows which RID they should put in their "runtimes" section.
- return fxDepsFile.IsRuntimeSupported(currentRid) ? currentRid : versionFile.BuildRid;
- }
-}
-
-internal class PrintCliSchemaAction(Option option) : InvocableOptionAction(option)
-{
- public override bool Terminating => true;
-
- public override int Invoke(ParseResult parseResult)
- {
- CliSchema.PrintCliSchema(parseResult, parseResult.InvocationConfiguration.Output, Program.TelemetryInstance);
-
- return 0;
- }
-}
diff --git a/src/Cli/dotnet/PerformanceLogEventListener.cs b/src/Cli/dotnet/PerformanceLogEventListener.cs
new file mode 100644
index 000000000000..201006e5c011
--- /dev/null
+++ b/src/Cli/dotnet/PerformanceLogEventListener.cs
@@ -0,0 +1,160 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Diagnostics.Tracing;
+using Microsoft.Extensions.EnvironmentAbstractions;
+
+namespace Microsoft.DotNet.Cli;
+
+internal sealed class PerformanceLogEventListener : EventListener
+{
+ internal struct ProviderConfiguration
+ {
+ internal string Name { get; set; }
+ internal EventKeywords Keywords { get; set; }
+ internal EventLevel Level { get; set; }
+ }
+
+ private static readonly ProviderConfiguration[] s_config =
+ [
+ new ProviderConfiguration()
+ {
+ Name = "Microsoft-Dotnet-CLI-Performance",
+ Keywords = EventKeywords.All,
+ Level = EventLevel.Verbose
+ }
+ ];
+
+ private const char EventDelimiter = '\n';
+ private StreamWriter _writer;
+
+ [ThreadStatic]
+ private static StringBuilder s_builder = new();
+
+ internal static PerformanceLogEventListener Create(IFileSystem fileSystem, string logDirectory)
+ {
+ // Only create a listener if the log directory exists.
+ if (string.IsNullOrWhiteSpace(logDirectory) || !fileSystem.Directory.Exists(logDirectory))
+ {
+ return null;
+ }
+
+ PerformanceLogEventListener eventListener = null;
+ try
+ {
+ // Initialization happens as a separate step and not in the constructor to ensure that
+ // if an exception is thrown during init, we have the opportunity to dispose of the listener,
+ // which will disable any EventSources that have been enabled. Any EventSources that existed before
+ // this EventListener will be passed to OnEventSourceCreated before our constructor is called, so
+ // we if we do this work in the constructor, and don't get an opportunity to call Dispose, the
+ // EventSources will remain enabled even if there aren't any consuming EventListeners.
+ eventListener = new PerformanceLogEventListener();
+ eventListener.Initialize(fileSystem, logDirectory);
+ }
+ catch
+ {
+ if (eventListener != null)
+ {
+ eventListener.Dispose();
+ }
+ }
+
+ return eventListener;
+ }
+
+ private PerformanceLogEventListener()
+ {
+ }
+
+ internal void Initialize(IFileSystem fileSystem, string logDirectory)
+ {
+ // Use a GUID disambiguator to make sure that we have a unique file name.
+ string logFilePath = Path.Combine(logDirectory, $"perf-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")}.log");
+
+ Stream outputStream = fileSystem.File.OpenFile(
+ logFilePath,
+ FileMode.Create, // Create or overwrite.
+ FileAccess.Write, // Open for writing.
+ FileShare.Read, // Allow others to read.
+ 4096, // Default buffer size.
+ FileOptions.None); // No hints about how the file will be written.
+
+ _writer = new StreamWriter(outputStream);
+ }
+
+ public override void Dispose()
+ {
+ lock (this)
+ {
+ if (_writer != null)
+ {
+ _writer.Dispose();
+ _writer = null;
+ }
+ }
+
+ base.Dispose();
+ }
+
+ protected override void OnEventSourceCreated(EventSource eventSource)
+ {
+ try
+ {
+ // Enable the provider if it matches a requested configuration.
+ foreach (ProviderConfiguration entry in s_config)
+ {
+ if (entry.Name.Equals(eventSource.Name))
+ {
+ EnableEvents(eventSource, entry.Level, entry.Keywords);
+ }
+ }
+ }
+ catch
+ {
+ // If we fail to enable, just skip it and continue.
+ }
+
+ base.OnEventSourceCreated(eventSource);
+ }
+
+ protected override void OnEventWritten(EventWrittenEventArgs eventData)
+ {
+ try
+ {
+ if (s_builder == null)
+ {
+ s_builder = new StringBuilder();
+ }
+ else
+ {
+ s_builder.Clear();
+ }
+
+ s_builder.Append($"[{DateTime.UtcNow.ToString("o")}] Event={eventData.EventSource.Name}/{eventData.EventName} ProcessID={Environment.ProcessId} ThreadID={Thread.CurrentThread.ManagedThreadId}\t ");
+ for (int i = 0; i < eventData.PayloadNames.Count; i++)
+ {
+ s_builder.Append($"{eventData.PayloadNames[i]}=\"{eventData.Payload[i]}\" ");
+ }
+
+ lock (this)
+ {
+ if (_writer != null)
+ {
+ foreach (ReadOnlyMemory mem in s_builder.GetChunks())
+ {
+ _writer.Write(mem);
+ }
+ _writer.Write(EventDelimiter);
+ }
+ }
+ }
+ catch
+ {
+ // If we fail to log an event, just skip it and continue.
+ }
+
+ base.OnEventWritten(eventData);
+ }
+}
diff --git a/src/Cli/dotnet/PerformanceLogEventSource.cs b/src/Cli/dotnet/PerformanceLogEventSource.cs
new file mode 100644
index 000000000000..0d1912db6c72
--- /dev/null
+++ b/src/Cli/dotnet/PerformanceLogEventSource.cs
@@ -0,0 +1,455 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Diagnostics;
+using System.Diagnostics.Tracing;
+using System.Reflection;
+using Microsoft.DotNet.Cli.Utils;
+using RuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment;
+
+namespace Microsoft.DotNet.Cli;
+
+[EventSource(Name = "Microsoft-Dotnet-CLI-Performance", Guid = "cbd57d06-3b9f-5374-ed53-cfbcc23cf44f")]
+internal sealed partial class PerformanceLogEventSource : EventSource
+{
+ internal static PerformanceLogEventSource Log = new();
+
+ [NonEvent]
+ internal void LogStartUpInformation(PerformanceLogStartupInformation startupInfo)
+ {
+ if (!IsEnabled())
+ {
+ return;
+ }
+
+ DotnetVersionFile versionFile = DotnetFiles.VersionFileObject;
+ string commitSha = versionFile.CommitSha ?? "N/A";
+
+ LogMachineConfiguration();
+ OSInfo(RuntimeEnvironment.OperatingSystem, RuntimeEnvironment.OperatingSystemVersion, RuntimeEnvironment.OperatingSystemPlatform.ToString());
+ SDKInfo(Product.Version, commitSha, RuntimeInformation.RuntimeIdentifier, versionFile.BuildRid, AppContext.BaseDirectory);
+ EnvironmentInfo(Environment.CommandLine);
+ LogMemoryConfiguration();
+ LogDrives();
+
+ // It's possible that IsEnabled returns true if an out-of-process collector such as ETW is enabled.
+ // If the perf log hasn't been enabled, then startupInfo will be null, so protect against nullref here.
+ if (startupInfo != null)
+ {
+ if (startupInfo.TimedAssembly != null)
+ {
+ AssemblyLoad(startupInfo.TimedAssembly.GetName().Name, startupInfo.AssemblyLoadTime.TotalMilliseconds);
+ }
+
+ Process currentProcess = Process.GetCurrentProcess();
+ TimeSpan latency = startupInfo.MainTimeStamp - currentProcess.StartTime;
+ HostLatency(latency.TotalMilliseconds);
+ }
+ }
+
+ [Event(1)]
+ internal void OSInfo(string osname, string osversion, string osplatform)
+ {
+ WriteEvent(1, osname, osversion, osplatform);
+ }
+
+ [Event(2)]
+ internal void SDKInfo(string version, string commit, string currentRid, string buildRid, string basePath)
+ {
+ WriteEvent(2, version, commit, currentRid, buildRid, basePath);
+ }
+
+ [Event(3)]
+ internal void EnvironmentInfo(string commandLine)
+ {
+ WriteEvent(3, commandLine);
+ }
+
+ [Event(4)]
+ internal void HostLatency(double timeInMs)
+ {
+ WriteEvent(4, timeInMs);
+ }
+
+ [Event(5)]
+ internal void CLIStart()
+ {
+ WriteEvent(5);
+ }
+
+ [Event(6)]
+ internal void CLIStop()
+ {
+ WriteEvent(6);
+ }
+
+ [Event(7)]
+ internal void FirstTimeConfigurationStart()
+ {
+ WriteEvent(7);
+ }
+
+ [Event(8)]
+ internal void FirstTimeConfigurationStop()
+ {
+ WriteEvent(8);
+ }
+
+ [Event(9)]
+ internal void TelemetryRegistrationStart()
+ {
+ WriteEvent(9);
+ }
+
+ [Event(10)]
+ internal void TelemetryRegistrationStop()
+ {
+ WriteEvent(10);
+ }
+
+ [Event(11)]
+ internal void TelemetrySaveIfEnabledStart()
+ {
+ WriteEvent(11);
+ }
+
+ [Event(12)]
+ internal void TelemetrySaveIfEnabledStop()
+ {
+ WriteEvent(12);
+ }
+
+ [Event(13)]
+ internal void BuiltInCommandStart()
+ {
+ WriteEvent(13);
+ }
+
+ [Event(14)]
+ internal void BuiltInCommandStop()
+ {
+ WriteEvent(14);
+ }
+
+ [Event(15)]
+ internal void BuiltInCommandParserStart()
+ {
+ WriteEvent(15);
+ }
+
+ [Event(16)]
+ internal void BuiltInCommandParserStop()
+ {
+ WriteEvent(16);
+ }
+
+ [Event(17)]
+ internal void ExtensibleCommandResolverStart()
+ {
+ WriteEvent(17);
+ }
+
+ [Event(18)]
+ internal void ExtensibleCommandResolverStop()
+ {
+ WriteEvent(18);
+ }
+
+ [Event(19)]
+ internal void ExtensibleCommandStart()
+ {
+ WriteEvent(19);
+ }
+
+ [Event(20)]
+ internal void ExtensibleCommandStop()
+ {
+ WriteEvent(20);
+ }
+
+ [Event(21)]
+ internal void TelemetryClientFlushStart()
+ {
+ WriteEvent(21);
+ }
+
+ [Event(22)]
+ internal void TelemetryClientFlushStop()
+ {
+ WriteEvent(22);
+ }
+
+ [NonEvent]
+ internal void LogMachineConfiguration()
+ {
+ if (IsEnabled())
+ {
+ MachineConfiguration(Environment.MachineName, Environment.ProcessorCount);
+ }
+ }
+
+ [Event(23)]
+ internal void MachineConfiguration(string machineName, int processorCount)
+ {
+ WriteEvent(23, machineName, processorCount);
+ }
+
+ [NonEvent]
+ internal void LogDrives()
+ {
+ if (IsEnabled())
+ {
+ foreach (DriveInfo driveInfo in DriveInfo.GetDrives())
+ {
+ try
+ {
+ DriveConfiguration(driveInfo.Name, driveInfo.DriveFormat, driveInfo.DriveType.ToString(),
+ (double)driveInfo.TotalSize / 1024 / 1024, (double)driveInfo.AvailableFreeSpace / 1024 / 1024);
+ }
+ catch
+ {
+ // If we fail to log a drive, skip it and continue.
+ }
+ }
+ }
+ }
+
+ [Event(24)]
+ internal void DriveConfiguration(string name, string format, string type, double totalSizeMB, double availableFreeSpaceMB)
+ {
+ WriteEvent(24, name, format, type, totalSizeMB, availableFreeSpaceMB);
+ }
+
+ [Event(25)]
+ internal void AssemblyLoad(string assemblyName, double timeInMs)
+ {
+ WriteEvent(25, assemblyName, timeInMs);
+ }
+
+ [NonEvent]
+ internal void LogMemoryConfiguration()
+ {
+ if (IsEnabled())
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ Interop.MEMORYSTATUSEX memoryStatusEx = new();
+ memoryStatusEx.dwLength = (uint)Marshal.SizeOf(memoryStatusEx);
+
+ if (Interop.GlobalMemoryStatusEx(ref memoryStatusEx))
+ {
+ MemoryConfiguration((int)memoryStatusEx.dwMemoryLoad, (int)(memoryStatusEx.ullAvailPhys / 1024 / 1024),
+ (int)(memoryStatusEx.ullTotalPhys / 1024 / 1024));
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ ProcMemInfo memInfo = new();
+ if (memInfo.Valid)
+ {
+ MemoryConfiguration(memInfo.MemoryLoad, memInfo.AvailableMemoryMB, memInfo.TotalMemoryMB);
+ }
+ }
+ }
+ }
+
+ [Event(26)]
+ internal void MemoryConfiguration(int memoryLoad, int availablePhysicalMB, int totalPhysicalMB)
+ {
+ WriteEvent(26, memoryLoad, availablePhysicalMB, totalPhysicalMB);
+ }
+
+ [NonEvent]
+ internal void LogMSBuildStart(string fileName, string arguments)
+ {
+ if (IsEnabled())
+ {
+ MSBuildStart($"{fileName} {arguments}");
+ }
+ }
+
+ [Event(27)]
+ internal void MSBuildStart(string cmdline)
+ {
+ WriteEvent(27, cmdline);
+ }
+
+ [Event(28)]
+ internal void MSBuildStop(int exitCode)
+ {
+ WriteEvent(28, exitCode);
+ }
+
+ [Event(29)]
+ internal void CreateBuildCommandStart()
+ {
+ WriteEvent(29);
+ }
+
+ [Event(30)]
+ internal void CreateBuildCommandStop()
+ {
+ WriteEvent(30);
+ }
+}
+
+internal class PerformanceLogStartupInformation
+{
+ public PerformanceLogStartupInformation(DateTime mainTimeStamp)
+ {
+ // Save the main timestamp.
+ MainTimeStamp = mainTimeStamp;
+
+ // Attempt to load an assembly.
+ // Ideally, we've picked one that we'll already need, so we're not adding additional overhead.
+ MeasureModuleLoad();
+ }
+
+ internal DateTime MainTimeStamp { get; private set; }
+ internal Assembly TimedAssembly { get; private set; }
+ internal TimeSpan AssemblyLoadTime { get; private set; }
+
+ private void MeasureModuleLoad()
+ {
+ // Make sure the assembly hasn't been loaded yet.
+ string assemblyName = "Microsoft.DotNet.Configurer";
+ try
+ {
+ foreach (Assembly loadedAssembly in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ if (loadedAssembly.GetName().Name.Equals(assemblyName))
+ {
+ // If the assembly is already loaded, then bail.
+ return;
+ }
+ }
+ }
+ catch
+ {
+ // If we fail to enumerate, just bail.
+ return;
+ }
+
+ Stopwatch stopWatch = Stopwatch.StartNew();
+ Assembly assembly;
+ try
+ {
+ assembly = Assembly.Load(assemblyName);
+ }
+ catch
+ {
+ return;
+ }
+ stopWatch.Stop();
+ if (assembly != null)
+ {
+ // Save the results.
+ TimedAssembly = assembly;
+ AssemblyLoadTime = stopWatch.Elapsed;
+ }
+ }
+}
+
+///
+/// Global memory statistics on Windows.
+///
+internal static class Interop
+{
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct MEMORYSTATUSEX
+ {
+ // The length field must be set to the size of this data structure.
+ internal uint dwLength;
+ internal uint dwMemoryLoad;
+ internal ulong ullTotalPhys;
+ internal ulong ullAvailPhys;
+ internal ulong ullTotalPageFile;
+ internal ulong ullAvailPageFile;
+ internal ulong ullTotalVirtual;
+ internal ulong ullAvailVirtual;
+ internal ulong ullAvailExtendedVirtual;
+ }
+
+ [DllImport("kernel32.dll")]
+ internal static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
+}
+
+///
+/// Global memory statistics on Linux.
+///
+internal sealed class ProcMemInfo
+{
+ private const string MemTotal = "MemTotal:";
+ private const string MemAvailable = "MemAvailable:";
+
+ private short _matchingLineCount = 0;
+
+ internal ProcMemInfo()
+ {
+ Initialize();
+ }
+
+ ///
+ /// The data in this class is valid if we parsed the file, found, and properly parsed the two matching lines.
+ ///
+ internal bool Valid
+ {
+ get { return _matchingLineCount == 2; }
+ }
+
+ internal int MemoryLoad
+ {
+ get { return (int)((double)(TotalMemoryMB - AvailableMemoryMB) / TotalMemoryMB * 100); }
+ }
+
+ internal int AvailableMemoryMB
+ {
+ get;
+ private set;
+ }
+
+ internal int TotalMemoryMB
+ {
+ get;
+ private set;
+ }
+
+ private void Initialize()
+ {
+ try
+ {
+ using (StreamReader reader = new(File.OpenRead("/proc/meminfo")))
+ {
+ string line;
+ while (!Valid && ((line = reader.ReadLine()) != null))
+ {
+ if (line.StartsWith(MemTotal) || line.StartsWith(MemAvailable))
+ {
+ string[] tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (tokens.Length == 3)
+ {
+ if (MemTotal.Equals(tokens[0]))
+ {
+ TotalMemoryMB = (int)Convert.ToUInt64(tokens[1]) / 1024;
+ _matchingLineCount++;
+ }
+ else if (MemAvailable.Equals(tokens[0]))
+ {
+ AvailableMemoryMB = (int)Convert.ToUInt64(tokens[1]) / 1024;
+ _matchingLineCount++;
+ }
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex) when (ex is IOException || ex.InnerException is IOException)
+ {
+ // in some environments (restricted docker container, shared hosting etc.),
+ // procfs is not accessible and we get UnauthorizedAccessException while the
+ // inner exception is set to IOException. Ignore and continue when that happens.
+ }
+ }
+}
diff --git a/src/Cli/dotnet/PerformanceLogManager.cs b/src/Cli/dotnet/PerformanceLogManager.cs
new file mode 100644
index 000000000000..3864adceb84c
--- /dev/null
+++ b/src/Cli/dotnet/PerformanceLogManager.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.DotNet.Configurer;
+using Microsoft.Extensions.EnvironmentAbstractions;
+
+namespace Microsoft.DotNet.Cli;
+
+internal sealed class PerformanceLogManager
+{
+ internal const string PerfLogDirEnvVar = "DOTNET_PERFLOG_DIR";
+ private const string PerfLogRoot = "PerformanceLogs";
+ private const int DefaultNumLogsToKeep = 10;
+
+ private readonly IFileSystem _fileSystem;
+ private string _perfLogRoot;
+
+ internal static PerformanceLogManager Instance
+ {
+ get;
+ private set;
+ }
+
+ internal static void InitializeAndStartCleanup(IFileSystem fileSystem)
+ {
+ if (Instance == null)
+ {
+ Instance = new PerformanceLogManager(fileSystem);
+
+ // Check to see if this instance is part of an already running chain of processes.
+ string perfLogDir = Env.GetEnvironmentVariable(PerfLogDirEnvVar);
+ if (!string.IsNullOrEmpty(perfLogDir))
+ {
+ // This process has been provided with a log directory, so use it.
+ Instance.UseExistingLogDirectory(perfLogDir);
+ }
+ else
+ {
+ // This process was not provided with a log root, so make a new one.
+ Instance._perfLogRoot = Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, PerfLogRoot);
+ Instance.CreateLogDirectory();
+
+ Task.Factory.StartNew(() =>
+ {
+ Instance.CleanupOldLogs();
+ });
+ }
+ }
+ }
+
+ internal PerformanceLogManager(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ internal string CurrentLogDirectory { get; private set; }
+
+ private void CreateLogDirectory()
+ {
+ // Ensure the log root directory exists.
+ if (!_fileSystem.Directory.Exists(_perfLogRoot))
+ {
+ _fileSystem.Directory.CreateDirectory(_perfLogRoot);
+ }
+
+ // Create a new perf log directory.
+ CurrentLogDirectory = Path.Combine(_perfLogRoot, Guid.NewGuid().ToString("N"));
+ _fileSystem.Directory.CreateDirectory(CurrentLogDirectory);
+ }
+
+ private void UseExistingLogDirectory(string logDirectory)
+ {
+ CurrentLogDirectory = logDirectory;
+ }
+
+ private void CleanupOldLogs()
+ {
+ if (_fileSystem.Directory.Exists(_perfLogRoot))
+ {
+ List logDirectories = [];
+ foreach (string directoryPath in _fileSystem.Directory.EnumerateDirectories(_perfLogRoot))
+ {
+ logDirectories.Add(new DirectoryInfo(directoryPath));
+ }
+
+ // Sort the list.
+ logDirectories.Sort(new LogDirectoryComparer());
+
+ // Figure out how many logs to keep.
+ int numLogsToKeep;
+ string strNumLogsToKeep = Env.GetEnvironmentVariable("DOTNET_PERF_LOG_COUNT");
+ if (!int.TryParse(strNumLogsToKeep, out numLogsToKeep))
+ {
+ numLogsToKeep = DefaultNumLogsToKeep;
+
+ // -1 == keep all logs
+ if (numLogsToKeep == -1)
+ {
+ numLogsToKeep = int.MaxValue;
+ }
+ }
+
+ // Skip the first numLogsToKeep elements.
+ if (logDirectories.Count > numLogsToKeep)
+ {
+ // Prune the old logs.
+ for (int i = logDirectories.Count - numLogsToKeep - 1; i >= 0; i--)
+ {
+ try
+ {
+ logDirectories[i].Delete(true);
+ }
+ catch
+ {
+ // Do nothing if a log can't be deleted.
+ // We'll get another chance next time around.
+ }
+ }
+ }
+ }
+ }
+}
+
+///
+/// Used to sort log directories when deciding which ones to delete.
+///
+internal sealed class LogDirectoryComparer : IComparer
+{
+ int IComparer.Compare(DirectoryInfo x, DirectoryInfo y)
+ {
+ return x.CreationTime.CompareTo(y.CreationTime);
+ }
+}
diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs
index f0558ad174f8..73a7d7eb6990 100644
--- a/src/Cli/dotnet/Program.cs
+++ b/src/Cli/dotnet/Program.cs
@@ -1,12 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics;
+using System.Runtime.InteropServices;
using Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
+using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess;
+using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Commands.Workload;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.ShellShim;
@@ -24,329 +29,422 @@ namespace Microsoft.DotNet.Cli;
public class Program
{
- private static readonly string s_toolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel";
-
- private static readonly Activity? s_mainActivity;
- private static readonly PosixSignalRegistration s_sigIntRegistration;
- private static readonly PosixSignalRegistration s_sigQuitRegistration;
- private static readonly PosixSignalRegistration s_sigTermRegistration;
- private static readonly string? s_globalJsonState;
-
- public static ITelemetryClient TelemetryInstance { get; private set; }
+ private static readonly string ToolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel";
- static Program()
+ public static ITelemetry TelemetryClient;
+ public static int Main(string[] args)
{
- var mainTimeStamp = DateTime.Now;
- s_sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, Shutdown);
- s_sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, Shutdown);
- s_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, Shutdown);
-
- // Note: This TelemetryClient instance needs to be created prior to calculating ActivityKind and ParentActivityContext,
- // used in the main activity creation below.
- TelemetryInstance = new TelemetryClient();
- TelemetryEventEntry.Subscribe(TelemetryInstance.TrackEvent);
- TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);
-
- s_mainActivity = Activities.Source.CreateActivity("main", TelemetryClient.ActivityKind, TelemetryClient.ParentActivityContext)
- ?.Start()
- ?.SetStartTime(Process.GetCurrentProcess().StartTime)
- ?.AddTag("process.pid", Process.GetCurrentProcess().Id)
- ?.AddTag("process.executable.name", "dotnet");
+ // Register a handler for SIGTERM to allow graceful shutdown of the application on Unix.
+ // See https://github.com/dotnet/docs/issues/46226.
+ using var termSignalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => Environment.Exit(0));
- if (CommandLoggingContext.IsVerbose)
- {
- Console.WriteLine($"Telemetry is: {(TelemetryInstance.Enabled ? "Enabled" : "Disabled")}");
- }
+ using AutomaticEncodingRestorer _ = new();
- // Creates a host-startup activity which includes the global.json state.
- using (var hostStartupActivity = Activities.Source.StartActivity("host-startup"))
+ if (Env.GetEnvironmentVariable("DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING") != "1")
{
- hostStartupActivity?.SetStartTime(Process.GetCurrentProcess().StartTime);
- if (TelemetryInstance.Enabled && hostStartupActivity is not null)
+ // Setting output encoding is not available on those platforms
+ if (UILanguageOverride.OperatingSystemSupportsUtf8())
{
- // Get the global.json state to report in telemetry along with this command invocation.
- s_globalJsonState = NativeWrapper.NETCoreSdkResolverNativeWrapper.GetGlobalJsonState(Environment.CurrentDirectory);
- hostStartupActivity?.AddTag("dotnet.globalJson", s_globalJsonState);
+ Console.OutputEncoding = Encoding.UTF8;
}
- hostStartupActivity?.SetEndTime(mainTimeStamp)?.SetStatus(ActivityStatusCode.Ok);
}
- // We have some behaviors in MSBuild that we want to enforce (either when using MSBuild API or by shelling out to it),
- // so we set those ASAP as globally as possible.
+ DebugHelper.HandleDebugSwitch(ref args);
+
+ // Capture the current timestamp to calculate the host overhead.
+ DateTime mainTimeStamp = DateTime.Now;
+ TimeSpan startupTime = mainTimeStamp - Process.GetCurrentProcess().StartTime;
+
+ bool perfLogEnabled = Env.GetEnvironmentVariableAsBool("DOTNET_CLI_PERF_LOG", false);
+
if (string.IsNullOrEmpty(Env.GetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD")))
{
Environment.SetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD", "1");
}
- }
-
- public static int Main(string[] args)
- {
- // Register a handler for SIGTERM to allow graceful shutdown of the application on Unix.
- // See https://github.com/dotnet/docs/issues/46226.
- using var termSignalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => Environment.Exit(0));
- using AutomaticEncodingRestorer _ = new();
-
- if (Env.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING) != "1"
- // Setting output encoding is not available on those platforms
- && UILanguageOverride.OperatingSystemSupportsUtf8())
+ // Avoid create temp directory with root permission and later prevent access in non sudo
+ if (SudoEnvironmentDirectoryOverride.IsRunningUnderSudo())
{
- Console.OutputEncoding = Encoding.UTF8;
+ perfLogEnabled = false;
}
- DebugHelper.HandleDebugSwitch(ref args);
- // By default, .NET Core doesn't have all code pages needed for Console apps.
- // See the .NET Core Notes: https://docs.microsoft.com/dotnet/api/system.diagnostics.process#-notes
- Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
- UILanguageOverride.Setup();
-
- var exitCode = 1;
- try
+ PerformanceLogStartupInformation startupInfo = null;
+ if (perfLogEnabled)
{
- exitCode = ProcessArgsAndExecute(args);
- s_mainActivity?.AddTag("process.exit.code", exitCode)?.SetStatus(ActivityStatusCode.Ok);
- return exitCode;
+ startupInfo = new PerformanceLogStartupInformation(mainTimeStamp);
+ PerformanceLogManager.InitializeAndStartCleanup(FileSystemWrapper.Default);
}
- catch (Exception e) when (e.ShouldBeDisplayedAsError())
+
+ PerformanceLogEventListener perLogEventListener = null;
+ try
{
- Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose
- ? e.ToString().Red().Bold()
- : e.Message.Red().Bold());
+ if (perfLogEnabled)
+ {
+ perLogEventListener = PerformanceLogEventListener.Create(FileSystemWrapper.Default, PerformanceLogManager.Instance.CurrentLogDirectory);
+ }
+
+ PerformanceLogEventSource.Log.LogStartUpInformation(startupInfo);
+ PerformanceLogEventSource.Log.CLIStart();
- if (e is CommandParsingException { ParseResult: {} exceptionParseResult } )
+ InitializeProcess();
+
+ try
{
- exceptionParseResult.ShowHelp();
+ return ProcessArgs(args, startupTime);
+ }
+ catch (Exception e) when (e.ShouldBeDisplayedAsError())
+ {
+ Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose
+ ? e.ToString().Red().Bold()
+ : e.Message.Red().Bold());
+
+ var commandParsingException = e as CommandParsingException;
+ if (commandParsingException != null && commandParsingException.ParseResult != null)
+ {
+ commandParsingException.ParseResult.ShowHelp();
+ }
+
+ return 1;
+ }
+ catch (Exception e) when (!e.ShouldBeDisplayedAsError())
+ {
+ // If telemetry object has not been initialized yet. It cannot be collected
+ TelemetryEventEntry.SendFiltered(e);
+ Reporter.Error.WriteLine(e.ToString().Red().Bold());
+
+ return 1;
+ }
+ finally
+ {
+ PerformanceLogEventSource.Log.CLIStop();
}
- s_mainActivity?.AddTag("process.exit.code", exitCode)?.SetStatus(ActivityStatusCode.Error);
- return exitCode;
- }
- catch (Exception e) when (!e.ShouldBeDisplayedAsError())
- {
- TelemetryEventEntry.SendFiltered(e);
- Reporter.Error.WriteLine(e.ToString().Red().Bold());
- s_mainActivity?.AddTag("process.exit.code", exitCode)?.SetStatus(ActivityStatusCode.Error);
- return exitCode;
}
finally
{
- TelemetryInstance.TrackEvent("command/finish", new Dictionary { { "exitCode", exitCode.ToString() } });
- Shutdown(default!);
- TelemetryClient.WriteLogIfNecessary();
+ if (perLogEventListener != null)
+ {
+ perLogEventListener.Dispose();
+ }
}
}
- internal static int ProcessArgsAndExecute(string[] args)
+ internal static int ProcessArgs(string[] args)
{
- ParseResult parseResult = ParseArgs(args);
- // Options that perform terminating actions are considered to essentially be subcommands.
- // These are special as they should not run the first-run setup.
- // Example: dotnet --version
- if (!(parseResult.Action is InvocableOptionAction { Terminating: true }))
+ return ProcessArgs(args, new TimeSpan(0));
+ }
+
+ internal static int ProcessArgs(string[] args, TimeSpan startupTime)
+ {
+ Dictionary performanceData = [];
+
+ PerformanceLogEventSource.Log.BuiltInCommandParserStart();
+ ParseResult parseResult;
+ using (new PerformanceMeasurement(performanceData, "Parse Time"))
{
- SetupFirstRun(parseResult);
+ parseResult = Parser.Parse(args);
+
+ // Avoid create temp directory with root permission and later prevent access in non sudo
+ // This method need to be run very early before temp folder get created
+ // https://github.com/dotnet/sdk/issues/20195
+ SudoEnvironmentDirectoryOverride.OverrideEnvironmentVariableToTmp(parseResult);
}
+ PerformanceLogEventSource.Log.BuiltInCommandParserStop();
- TelemetryEventEntry.SendFiltered(new ParseResultWithGlobalJsonState(parseResult, s_globalJsonState));
- if (parseResult.CanBeInvoked())
+ using (IFirstTimeUseNoticeSentinel disposableFirstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel())
{
- return ExecuteInternalCommand(parseResult);
+ IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = disposableFirstTimeUseNoticeSentinel;
+ IAspNetCertificateSentinel aspNetCertificateSentinel = new AspNetCertificateSentinel();
+ IFileSentinel toolPathSentinel = new FileSentinel(new FilePath(Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, ToolPathSentinelFileName)));
+
+ PerformanceLogEventSource.Log.TelemetryRegistrationStart();
+
+ TelemetryClient ??= new Telemetry.Telemetry(firstTimeUseNoticeSentinel);
+ TelemetryEventEntry.Subscribe(TelemetryClient.TrackEvent);
+ TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);
+
+ PerformanceLogEventSource.Log.TelemetryRegistrationStop();
+
+ if (parseResult.GetValue(Parser.RootCommand.DiagOption) && parseResult.IsDotnetBuiltInCommand())
+ {
+ // We found --diagnostic or -d, but we still need to determine whether the option should
+ // be attached to the dotnet command or the subcommand.
+ if (args.DiagOptionPrecedesSubcommand(parseResult.RootSubCommandResult()))
+ {
+ Environment.SetEnvironmentVariable(CommandLoggingContext.Variables.Verbose, bool.TrueString);
+ CommandLoggingContext.SetVerbose(true);
+ Reporter.Reset();
+ }
+ }
+ if (parseResult.HasOption(Parser.RootCommand.VersionOption) && parseResult.IsTopLevelDotnetCommand())
+ {
+ CommandLineInfo.PrintVersion();
+ return 0;
+ }
+ else if (parseResult.HasOption(Parser.RootCommand.InfoOption) && parseResult.IsTopLevelDotnetCommand())
+ {
+ CommandLineInfo.PrintInfo();
+ return 0;
+ }
+ else
+ {
+ PerformanceLogEventSource.Log.FirstTimeConfigurationStart();
+
+ var environmentProvider = new EnvironmentProvider();
+
+ bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_GENERATE_ASPNET_CERTIFICATE, defaultValue: true);
+ bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault);
+ bool addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_ADD_GLOBAL_TOOLS_TO_PATH, defaultValue: true);
+ bool nologo = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_NOLOGO, defaultValue: false);
+ bool skipWorkloadIntegrityCheck = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK,
+ // Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false.
+ defaultValue: new CIEnvironmentDetectorForTelemetry().IsCIEnvironment());
+
+ ReportDotnetHomeUsage(environmentProvider);
+
+ var isDotnetBeingInvokedFromNativeInstaller = false;
+ if (parseResult.CommandResult.Command is InternalReportInstallSuccessCommandDefinition)
+ {
+ aspNetCertificateSentinel = new NoOpAspNetCertificateSentinel();
+ firstTimeUseNoticeSentinel = new NoOpFirstTimeUseNoticeSentinel();
+ toolPathSentinel = new NoOpFileSentinel(exists: false);
+ isDotnetBeingInvokedFromNativeInstaller = true;
+ }
+
+ var dotnetFirstRunConfiguration = new DotnetFirstRunConfiguration(
+ generateAspNetCertificate: generateAspNetCertificate,
+ telemetryOptout: telemetryOptout,
+ addGlobalToolsToPath: addGlobalToolsToPath,
+ nologo: nologo,
+ skipWorkloadIntegrityCheck: skipWorkloadIntegrityCheck);
+
+ string[] getStarOperators = ["getProperty", "getItem", "getTargetResult"];
+ char[] switchIndicators = ['-', '/'];
+ var getStarOptionPassed = parseResult.CommandResult.Tokens.Any(t =>
+ getStarOperators.Any(o =>
+ switchIndicators.Any(i => t.Value.StartsWith(i + o, StringComparison.OrdinalIgnoreCase))));
+
+ ConfigureDotNetForFirstTimeUse(
+ firstTimeUseNoticeSentinel,
+ aspNetCertificateSentinel,
+ toolPathSentinel,
+ isDotnetBeingInvokedFromNativeInstaller,
+ dotnetFirstRunConfiguration,
+ environmentProvider,
+ performanceData,
+ skipFirstTimeUseCheck: getStarOptionPassed);
+ PerformanceLogEventSource.Log.FirstTimeConfigurationStop();
+ }
}
- try
+ if (CommandLoggingContext.IsVerbose)
{
- return ExecuteExternalCommand(args, parseResult);
+ Console.WriteLine($"Telemetry is: {(TelemetryClient.Enabled ? "Enabled" : "Disabled")}");
}
- catch (CommandUnknownException e)
+ PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStart();
+ performanceData.Add("Startup Time", startupTime.TotalMilliseconds);
+
+ string globalJsonState = string.Empty;
+ if (TelemetryClient.Enabled)
{
- Reporter.Error.WriteLine(e.Message.Red());
- Reporter.Output.WriteLine(e.InstructionMessage);
- return 1;
+ // Get the global.json state to report in telemetry along with this command invocation.
+ globalJsonState = NativeWrapper.NETCoreSdkResolverNativeWrapper.GetGlobalJsonState(Environment.CurrentDirectory);
}
- static ParseResult ParseArgs(string[] args)
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, performanceData, globalJsonState));
+ PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStop();
+
+ int exitCode;
+ if (parseResult.CanBeInvoked())
+ {
+ InvokeBuiltInCommand(parseResult, out exitCode);
+ }
+ else
{
- ParseResult parseResult;
- using (var parseActivity = Activities.Source.StartActivity("parse"))
+ PerformanceLogEventSource.Log.ExtensibleCommandResolverStart();
+ try
{
- parseResult = Parser.Parse(args);
+ string commandName = "dotnet-" + parseResult.GetValue(Parser.RootCommand.DotnetSubCommand);
+ var resolvedCommandSpec = CommandResolver.TryResolveCommandSpec(
+ new DefaultCommandResolverPolicy(),
+ commandName,
+ args.GetSubArguments(),
+ FrameworkConstants.CommonFrameworks.NetStandardApp15);
+
+ if (resolvedCommandSpec is null && TryRunFileBasedApp(parseResult) is { } fileBasedAppExitCode)
+ {
+ exitCode = fileBasedAppExitCode;
+ }
+ else
+ {
+ var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec);
+ PerformanceLogEventSource.Log.ExtensibleCommandResolverStop();
- // Avoid create temp directory with root permission and later prevent access in non sudo
- // This method need to be run very early before temp folder get created
- // https://github.com/dotnet/sdk/issues/20195
- SudoEnvironmentDirectoryOverride.OverrideEnvironmentVariableToTmp(parseResult);
- }
- s_mainActivity.SetDisplayName(parseResult);
- return parseResult;
- }
- }
+ PerformanceLogEventSource.Log.ExtensibleCommandStart();
+ var result = resolvedCommand.Execute();
+ PerformanceLogEventSource.Log.ExtensibleCommandStop();
- private static void SetupFirstRun(ParseResult parseResult)
- {
- using var _ = Activities.Source.StartActivity("first-time-use");
- IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel();
- IAspNetCertificateSentinel aspNetCertificateSentinel = new AspNetCertificateSentinel();
- string toolPath = Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, s_toolPathSentinelFileName);
- IFileSentinel toolPathSentinel = new FileSentinel(new FilePath(toolPath));
-
- var environmentProvider = new EnvironmentProvider();
- bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_GENERATE_ASPNET_CERTIFICATE, defaultValue: true);
- bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault);
- bool addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_ADD_GLOBAL_TOOLS_TO_PATH, defaultValue: true);
- bool nologo = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_NOLOGO, defaultValue: false);
- bool skipWorkloadIntegrityCheck = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK,
- // Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false.
- defaultValue: new CIEnvironmentDetectorForTelemetry().IsCIEnvironment());
-
- var isDotnetBeingInvokedFromNativeInstaller = false;
- // Note: This should not be special cased like this. Determine if we can skip first run setup entirely for this command.
- if (parseResult.CommandResult.Command is InternalReportInstallSuccessCommandDefinition)
- {
- aspNetCertificateSentinel = new NoOpAspNetCertificateSentinel();
- firstTimeUseNoticeSentinel = new NoOpFirstTimeUseNoticeSentinel();
- toolPathSentinel = new NoOpFileSentinel(exists: false);
- isDotnetBeingInvokedFromNativeInstaller = true;
+ exitCode = result.ExitCode;
+ }
+ }
+ catch (CommandUnknownException e)
+ {
+ Reporter.Error.WriteLine(e.Message.Red());
+ Reporter.Output.WriteLine(e.InstructionMessage);
+ exitCode = 1;
+ }
}
- var dotnetFirstRunConfiguration = new DotnetFirstRunConfiguration(
- generateAspNetCertificate,
- telemetryOptout,
- addGlobalToolsToPath,
- nologo,
- skipWorkloadIntegrityCheck);
+ TelemetryClient.TrackEvent("command/finish", properties: new Dictionary
+ {
+ { "exitCode", exitCode.ToString() }
+ },
+ measurements: new Dictionary());
- string[] getStarOperators = ["getProperty", "getItem", "getTargetResult"];
- char[] switchIndicators = ['-', '/'];
- var skipFirstTimeUseCheck = parseResult.CommandResult.Tokens.Any(t =>
- getStarOperators.Any(o =>
- switchIndicators.Any(i => t.Value.StartsWith(i + o, StringComparison.OrdinalIgnoreCase))));
+ PerformanceLogEventSource.Log.TelemetryClientFlushStart();
+ TelemetryClient.Flush();
+ PerformanceLogEventSource.Log.TelemetryClientFlushStop();
- var isFirstTimeUse = !firstTimeUseNoticeSentinel.Exists() && !skipFirstTimeUseCheck;
- var environmentPath = EnvironmentPathFactory.CreateEnvironmentPath(isDotnetBeingInvokedFromNativeInstaller, environmentProvider);
- // Note: Not sure why this unused instance type is created.
- var __ = new DotNetCommandFactory(alwaysRunOutOfProc: true);
- var aspnetCertificateGenerator = new AspNetCoreCertificateGenerator();
- var reporter = Reporter.Error;
- var dotnetConfigurer = new DotnetFirstTimeUseConfigurer(
- firstTimeUseNoticeSentinel,
- aspNetCertificateSentinel,
- aspnetCertificateGenerator,
- toolPathSentinel,
- dotnetFirstRunConfiguration,
- reporter,
- environmentPath,
- skipFirstTimeUseCheck);
+ TelemetryClient.Dispose();
- dotnetConfigurer.Configure();
+ return exitCode;
-#if TARGET_WINDOWS
- if (isDotnetBeingInvokedFromNativeInstaller && OperatingSystem.IsWindows())
+ static int? TryRunFileBasedApp(ParseResult parseResult)
{
- DotDefaultPathCorrector.Correct();
+ // If we didn't match any built-in commands, and a C# file path is the first argument,
+ // parse as `dotnet run --file file.cs ..rest_of_args` instead.
+ if (parseResult.GetResult(Parser.RootCommand.DotnetSubCommand) is { Tokens: [{ Type: TokenType.Argument, Value: { } } unmatchedCommandOrFile] }
+ && VirtualProjectBuilder.IsValidEntryPointPath(unmatchedCommandOrFile.Value))
+ {
+ List otherTokens = new(parseResult.Tokens.Count - 1);
+ foreach (var token in parseResult.Tokens)
+ {
+ if (token != unmatchedCommandOrFile)
+ {
+ otherTokens.Add(token.Value);
+ }
+ }
+
+ parseResult = Parser.Parse(["run", "--file", unmatchedCommandOrFile.Value, .. otherTokens]);
+
+ InvokeBuiltInCommand(parseResult, out var exitCode);
+ return exitCode;
+ }
+
+ return null;
}
-#endif
- if (isFirstTimeUse && !skipWorkloadIntegrityCheck)
+ static void InvokeBuiltInCommand(ParseResult parseResult, out int exitCode)
{
+ Debug.Assert(parseResult.CanBeInvoked());
+
+ PerformanceLogEventSource.Log.BuiltInCommandStart();
+
try
{
- WorkloadIntegrityChecker.RunFirstUseCheck(reporter);
+ exitCode = Parser.Invoke(parseResult);
+ exitCode = AdjustExitCode(parseResult, exitCode);
}
- catch (Exception)
+ catch (Exception exception)
{
- // If the workload check fails for any reason, we want to eat the failure and continue running the command.
- reporter.WriteLine(CliStrings.WorkloadIntegrityCheckError.Yellow());
+ exitCode = Parser.ExceptionHandler(exception, parseResult);
}
+
+ PerformanceLogEventSource.Log.BuiltInCommandStop();
}
}
- private static int ExecuteInternalCommand(ParseResult parseResult)
+ private static int AdjustExitCode(ParseResult parseResult, int exitCode)
{
- Debug.Assert(parseResult.CanBeInvoked());
- int exitCode;
- using var _ = Activities.Source.StartActivity("invocation");
- try
- {
- exitCode = Parser.Invoke(parseResult);
- if (parseResult.Errors.Any())
- {
- exitCode = AdjustExitCodeForNew();
- }
- }
- catch (Exception exception)
- {
- exitCode = Parser.ExceptionHandler(exception, parseResult);
- }
- return exitCode;
-
- int AdjustExitCodeForNew()
+ if (parseResult.Errors.Count > 0)
{
var commandResult = parseResult.CommandResult;
+
while (commandResult is not null)
{
if (commandResult.Command.Name == "new")
{
- // Default parse error exit code is 1.
- // For the "new" command and its subcommands, it needs to be 127.
+ // default parse error exit code is 1
+ // for the "new" command and its subcommands it needs to be 127
return 127;
}
+
commandResult = commandResult.Parent as CommandResult;
}
- return exitCode;
}
+
+ return exitCode;
}
- private static int ExecuteExternalCommand(string[] args, ParseResult parseResult)
+ private static void ReportDotnetHomeUsage(IEnvironmentProvider provider)
{
- string commandName = "dotnet-" + parseResult.GetValue(Parser.RootCommand.DotnetSubCommand);
- CommandSpec? resolvedCommandSpec = null;
- using (var _ = Activities.Source.StartActivity("lookup-external-command"))
- {
- resolvedCommandSpec = CommandResolver.TryResolveCommandSpec(
- new DefaultCommandResolverPolicy(),
- commandName,
- args.GetSubArguments(),
- FrameworkConstants.CommonFrameworks.NetStandardApp15);
- }
-
- if (resolvedCommandSpec is null && TryRunFileBasedApp(parseResult) is { } fileBasedAppExitCode)
+ var home = provider.GetEnvironmentVariable(CliFolderPathCalculator.DotnetHomeVariableName);
+ if (string.IsNullOrEmpty(home))
{
- return fileBasedAppExitCode;
+ return;
}
- var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec);
- using var __ = Activities.Source.StartActivity("execute-extensible-command");
- return resolvedCommand.Execute().ExitCode;
+ Reporter.Verbose.WriteLine(
+ string.Format(
+ LocalizableStrings.DotnetCliHomeUsed,
+ home,
+ CliFolderPathCalculator.DotnetHomeVariableName));
}
- private static int? TryRunFileBasedApp(ParseResult parseResult)
+ private static void ConfigureDotNetForFirstTimeUse(
+ IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel,
+ IAspNetCertificateSentinel aspNetCertificateSentinel,
+ IFileSentinel toolPathSentinel,
+ bool isDotnetBeingInvokedFromNativeInstaller,
+ DotnetFirstRunConfiguration dotnetFirstRunConfiguration,
+ IEnvironmentProvider environmentProvider,
+ Dictionary performanceMeasurements,
+ bool skipFirstTimeUseCheck)
{
- // If we didn't match any built-in commands, and a C# file path is the first argument,
- // parse as `dotnet run file.cs ..rest_of_args` instead.
- if (parseResult.GetResult(Parser.RootCommand.DotnetSubCommand) is { Tokens: [{ Type: TokenType.Argument, Value: { } } unmatchedCommandOrFile] }
- && VirtualProjectBuilder.IsValidEntryPointPath(unmatchedCommandOrFile.Value))
+ var isFirstTimeUse = !firstTimeUseNoticeSentinel.Exists() && !skipFirstTimeUseCheck;
+ var environmentPath = EnvironmentPathFactory.CreateEnvironmentPath(isDotnetBeingInvokedFromNativeInstaller, environmentProvider);
+ _ = new DotNetCommandFactory(alwaysRunOutOfProc: true);
+ var aspnetCertificateGenerator = new AspNetCoreCertificateGenerator();
+ var reporter = Reporter.Error;
+ var dotnetConfigurer = new DotnetFirstTimeUseConfigurer(
+ firstTimeUseNoticeSentinel,
+ aspNetCertificateSentinel,
+ aspnetCertificateGenerator,
+ toolPathSentinel,
+ dotnetFirstRunConfiguration,
+ reporter,
+ environmentPath,
+ performanceMeasurements,
+ skipFirstTimeUseCheck: skipFirstTimeUseCheck);
+
+ dotnetConfigurer.Configure();
+
+#if TARGET_WINDOWS
+ if (isDotnetBeingInvokedFromNativeInstaller && OperatingSystem.IsWindows())
+ {
+ DotDefaultPathCorrector.Correct();
+ }
+#endif
+
+ if (isFirstTimeUse && !dotnetFirstRunConfiguration.SkipWorkloadIntegrityCheck)
{
- List otherTokens = new(parseResult.Tokens.Count - 1);
- foreach (var token in parseResult.Tokens)
+ try
{
- if (token.Type != TokenType.Argument || token != unmatchedCommandOrFile)
- {
- otherTokens.Add(token.Value);
- }
+ WorkloadIntegrityChecker.RunFirstUseCheck(reporter);
+ }
+ catch (Exception)
+ {
+ // If the workload check fails for any reason, we want to eat the failure and continue running the command.
+ reporter.WriteLine(CliStrings.WorkloadIntegrityCheckError.Yellow());
}
- parseResult = Parser.Parse(["run", "--file", unmatchedCommandOrFile.Value, .. otherTokens]);
- return ExecuteInternalCommand(parseResult);
}
-
- return null;
}
- public static void Shutdown(PosixSignalContext context)
+ private static void InitializeProcess()
{
- s_sigIntRegistration.Dispose();
- s_sigQuitRegistration.Dispose();
- s_sigTermRegistration.Dispose();
- s_mainActivity?.Stop();
- TelemetryClient.FlushProviders();
- Activities.Source.Dispose();
+ // by default, .NET Core doesn't have all code pages needed for Console apps.
+ // see the .NET Core Notes in https://docs.microsoft.com/dotnet/api/system.diagnostics.process#-notes
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+
+ UILanguageOverride.Setup();
}
}
diff --git a/src/Cli/dotnet/Telemetry/AllowListToSendFirstAppliedOptions.cs b/src/Cli/dotnet/Telemetry/AllowListToSendFirstAppliedOptions.cs
index 86e124e8e668..abe2058763ff 100644
--- a/src/Cli/dotnet/Telemetry/AllowListToSendFirstAppliedOptions.cs
+++ b/src/Cli/dotnet/Telemetry/AllowListToSendFirstAppliedOptions.cs
@@ -14,10 +14,10 @@ internal class AllowListToSendFirstAppliedOptions(
{
private HashSet _topLevelCommandNameAllowList { get; } = topLevelCommandNameAllowList;
- public List AllowList(ParseResult parseResult)
+ public List AllowList(ParseResult parseResult, Dictionary measurements = null)
{
var topLevelCommandNameFromParse = parseResult.RootSubCommandResult();
- var result = new List();
+ var result = new List();
if (_topLevelCommandNameAllowList.Contains(topLevelCommandNameFromParse))
{
var firstOption = parseResult.RootCommandResult.Children
@@ -25,13 +25,14 @@ public List AllowList(ParseResult parseResult)
.Children.OfType().FirstOrDefault()?.Command.Name ?? null;
if (firstOption != null)
{
- result.Add(new TelemetryEntryFormat(
+ result.Add(new ApplicationInsightsEntryFormat(
"sublevelparser/command",
new Dictionary
{
- {"verb", topLevelCommandNameFromParse},
+ { "verb", topLevelCommandNameFromParse},
{"argument", firstOption}
- }));
+ },
+ measurements));
}
}
return result;
diff --git a/src/Cli/dotnet/Telemetry/AllowListToSendFirstArgument.cs b/src/Cli/dotnet/Telemetry/AllowListToSendFirstArgument.cs
index 4961193ca740..0b303a14bcd7 100644
--- a/src/Cli/dotnet/Telemetry/AllowListToSendFirstArgument.cs
+++ b/src/Cli/dotnet/Telemetry/AllowListToSendFirstArgument.cs
@@ -1,19 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.DotNet.Cli.Utils;
namespace Microsoft.DotNet.Cli.Telemetry;
-internal class AllowListToSendFirstArgument(HashSet topLevelCommandNameAllowList) : IParseResultLogRule
+internal class AllowListToSendFirstArgument(
+ HashSet topLevelCommandNameAllowList) : IParseResultLogRule
{
private HashSet _topLevelCommandNameAllowList { get; } = topLevelCommandNameAllowList;
- public List AllowList(ParseResult parseResult)
+ public List AllowList(ParseResult parseResult, Dictionary measurements = null)
{
- var result = new List();
+ var result = new List();
var topLevelCommandNameFromParse = parseResult.RootCommandResult.Children.FirstOrDefault() switch
{
System.CommandLine.Parsing.CommandResult commandResult => commandResult.Command.Name,
@@ -25,17 +28,17 @@ public List AllowList(ParseResult parseResult)
{
if (_topLevelCommandNameAllowList.Contains(topLevelCommandNameFromParse))
{
- var firstArgument = parseResult.RootCommandResult.Children.FirstOrDefault()?.Tokens
- .Where(t => t.Type.Equals(TokenType.Argument)).FirstOrDefault()?.Value ?? null;
+ var firstArgument = parseResult.RootCommandResult.Children.FirstOrDefault()?.Tokens.Where(t => t.Type.Equals(TokenType.Argument)).FirstOrDefault()?.Value ?? null;
if (firstArgument != null)
{
- result.Add(new TelemetryEntryFormat(
+ result.Add(new ApplicationInsightsEntryFormat(
"sublevelparser/command",
- new Dictionary
+ new Dictionary
{
{"verb", topLevelCommandNameFromParse},
{"argument", firstArgument}
- }));
+ },
+ measurements));
}
}
}
diff --git a/src/Cli/dotnet/Telemetry/AllowListToSendVerbSecondVerbFirstArgument.cs b/src/Cli/dotnet/Telemetry/AllowListToSendVerbSecondVerbFirstArgument.cs
index 7159b126ac2a..44eff7eb5707 100644
--- a/src/Cli/dotnet/Telemetry/AllowListToSendVerbSecondVerbFirstArgument.cs
+++ b/src/Cli/dotnet/Telemetry/AllowListToSendVerbSecondVerbFirstArgument.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.DotNet.Cli.Extensions;
@@ -8,13 +10,14 @@
namespace Microsoft.DotNet.Cli.Telemetry;
-internal class AllowListToSendVerbSecondVerbFirstArgument(HashSet topLevelCommandNameAllowList) : IParseResultLogRule
+internal class AllowListToSendVerbSecondVerbFirstArgument(
+ HashSet topLevelCommandNameAllowList) : IParseResultLogRule
{
private HashSet TopLevelCommandNameAllowList { get; } = topLevelCommandNameAllowList;
- public List AllowList(ParseResult parseResult)
+ public List AllowList(ParseResult parseResult, Dictionary measurements = null)
{
- var result = new List();
+ var result = new List();
var topLevelCommandNameFromParse = parseResult.RootSubCommandResult();
if (topLevelCommandNameFromParse != null)
@@ -26,14 +29,15 @@ public List AllowList(ParseResult parseResult)
var firstArgument = parseResult.Tokens.FirstOrDefault(t => t.Type.Equals(TokenType.Argument))?.Value ?? "";
if (secondVerb != null)
{
- result.Add(new TelemetryEntryFormat(
+ result.Add(new ApplicationInsightsEntryFormat(
"sublevelparser/command",
- new Dictionary
+ new Dictionary
{
{"verb", topLevelCommandNameFromParse},
{"subcommand", secondVerb},
{"argument", firstArgument}
- }));
+ },
+ measurements));
}
}
}
diff --git a/src/Cli/dotnet/Telemetry/ExternalTelemetryProperties.cs b/src/Cli/dotnet/Telemetry/ExternalTelemetryProperties.cs
index b1de4ad11aa7..a333f4127416 100644
--- a/src/Cli/dotnet/Telemetry/ExternalTelemetryProperties.cs
+++ b/src/Cli/dotnet/Telemetry/ExternalTelemetryProperties.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.Diagnostics;
using System.Globalization;
using System.Security;
@@ -16,7 +18,7 @@ internal static class ExternalTelemetryProperties
/// For Windows, returns the OS installation type, eg. "Nano Server", "Server Core", "Server", or "Client".
/// For Unix, or on error, currently returns empty string.
///
- internal static string? GetInstallationType()
+ internal static string GetInstallationType()
{
if (!OperatingSystem.IsWindows())
{
@@ -28,7 +30,7 @@ internal static class ExternalTelemetryProperties
try
{
- return (string?)Registry.GetValue(Key, ValueName, defaultValue: "");
+ return (string)Registry.GetValue(Key, ValueName, defaultValue: "");
}
// Catch everything: this is for telemetry only.
catch (Exception e)
@@ -48,7 +50,7 @@ internal static class ExternalTelemetryProperties
/// We're not attempting to decode the value on the client side as new Windows releases may add new values.
/// For Unix, or on error, returns an empty string.
///
- internal static string? GetProductType()
+ internal static string GetProductType()
{
if (!OperatingSystem.IsWindows())
{
@@ -82,7 +84,7 @@ internal static class ExternalTelemetryProperties
/// If the libc is musl, currently returns empty string.
/// Otherwise returns empty string.
///
- internal static string? GetLibcRelease()
+ internal static string GetLibcRelease()
{
if (OperatingSystem.IsWindows())
{
@@ -106,7 +108,7 @@ internal static class ExternalTelemetryProperties
/// If the libc is musl, currently returns empty string. (In future could run "ldd -version".)
/// Otherwise returns empty string.
///
- internal static string? GetLibcVersion()
+ internal static string GetLibcVersion()
{
if (OperatingSystem.IsWindows())
{
diff --git a/src/Cli/dotnet/Telemetry/IParseResultLogRule.cs b/src/Cli/dotnet/Telemetry/IParseResultLogRule.cs
index 7dfe292c6c0c..6384f7dbd59a 100644
--- a/src/Cli/dotnet/Telemetry/IParseResultLogRule.cs
+++ b/src/Cli/dotnet/Telemetry/IParseResultLogRule.cs
@@ -10,5 +10,5 @@ namespace Microsoft.DotNet.Cli.Telemetry;
internal interface IParseResultLogRule
{
- List AllowList(ParseResult parseResult);
+ List AllowList(ParseResult parseResult, Dictionary measurements = null);
}
diff --git a/src/Cli/dotnet/Telemetry/ITelemetryClient.cs b/src/Cli/dotnet/Telemetry/ITelemetry.cs
similarity index 65%
rename from src/Cli/dotnet/Telemetry/ITelemetryClient.cs
rename to src/Cli/dotnet/Telemetry/ITelemetry.cs
index f022648a59ff..b8ee7c98e118 100644
--- a/src/Cli/dotnet/Telemetry/ITelemetryClient.cs
+++ b/src/Cli/dotnet/Telemetry/ITelemetry.cs
@@ -1,11 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
namespace Microsoft.DotNet.Cli.Telemetry;
-public interface ITelemetryClient
+public interface ITelemetry
{
bool Enabled { get; }
- void TrackEvent(string eventName, IDictionary? properties);
+ void TrackEvent(string eventName, IDictionary properties, IDictionary measurements);
+
+ void Flush();
+
+ void Dispose();
}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/BaseStorageService.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/BaseStorageService.cs
new file mode 100644
index 000000000000..2966dfc205a8
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/BaseStorageService.cs
@@ -0,0 +1,61 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using Microsoft.ApplicationInsights.Channel;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+internal abstract class BaseStorageService
+{
+ ///
+ /// Peeked transmissions dictionary (maps file name to its full path). Holds all the transmissions that were peeked.
+ ///
+ ///
+ /// Note: The value (=file's full path) is not required in the Storage implementation.
+ /// If there was a concurrent Abstract Data Type Set it would have been used instead.
+ /// However, since there is no concurrent Set, dictionary is used and the second value is ignored.
+ ///
+ protected IDictionary PeekedTransmissions;
+
+ ///
+ /// Gets or sets the maximum size of the storage in bytes. When limit is reached, the Enqueue method will drop new
+ /// transmissions.
+ ///
+ internal ulong CapacityInBytes { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of files. When limit is reached, the Enqueue method will drop new transmissions.
+ ///
+ internal uint MaxFiles { get; set; }
+
+ internal abstract string StorageDirectoryPath { get; }
+
+ ///
+ /// Initializes the
+ ///
+ /// A folder name. Under this folder all the transmissions will be saved.
+ internal abstract void Init(string desireStorageDirectoryPath);
+
+ internal abstract StorageTransmission Peek();
+
+ internal abstract void Delete(StorageTransmission transmission);
+
+ internal abstract Task EnqueueAsync(Transmission transmission);
+
+ protected void OnPeekedItemDisposed(string fileName)
+ {
+ try
+ {
+ if (PeekedTransmissions.ContainsKey(fileName))
+ {
+ PeekedTransmissions.Remove(fileName);
+ }
+ }
+ catch (Exception e)
+ {
+ PersistenceChannelDebugLog.WriteException(e, "Failed to remove the item from storage items.");
+ }
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/FixedSizeQueue.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/FixedSizeQueue.cs
new file mode 100644
index 000000000000..4a80be224e9c
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/FixedSizeQueue.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+///
+/// A light fixed size queue. If Enqueue is called and queue's limit has reached the last item will be removed.
+/// This data structure is thread safe.
+///
+internal class FixedSizeQueue
+{
+ private readonly int _maxSize;
+ private readonly Queue _queue = new();
+ private readonly object _queueLockObj = new();
+
+ internal FixedSizeQueue(int maxSize)
+ {
+ _maxSize = maxSize;
+ }
+
+ internal void Enqueue(T item)
+ {
+ lock (_queueLockObj)
+ {
+ if (_queue.Count == _maxSize)
+ {
+ _queue.Dequeue();
+ }
+
+ _queue.Enqueue(item);
+ }
+ }
+
+ internal bool Contains(T item)
+ {
+ lock (_queueLockObj)
+ {
+ return _queue.Contains(item);
+ }
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/FlushManager.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/FlushManager.cs
new file mode 100644
index 000000000000..8d14b740cca0
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/FlushManager.cs
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using Microsoft.ApplicationInsights.Channel;
+using Microsoft.ApplicationInsights.Extensibility.Implementation;
+using IChannelTelemetry = Microsoft.ApplicationInsights.Channel.ITelemetry;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+///
+/// This class handles all the logic for flushing the In Memory buffer to the persistent storage.
+///
+internal class FlushManager
+{
+ ///
+ /// The storage that is used to persist all the transmissions.
+ ///
+ private readonly BaseStorageService _storage;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The storage that persists the telemetries.
+ internal FlushManager(BaseStorageService storage)
+ {
+ _storage = storage;
+ }
+
+ ///
+ /// Gets or sets the service endpoint.
+ ///
+ ///
+ /// Q: Why flushManager knows about the endpoint?
+ /// A: Storage stores Transmission objects and Transmission objects contain the endpoint address.
+ ///
+ internal Uri EndpointAddress { get; set; }
+
+ ///
+ /// Persist the in-memory telemetry items.
+ ///
+ internal void Flush(IChannelTelemetry telemetryItem)
+ {
+ if (telemetryItem != null)
+ {
+ byte[] data = JsonSerializer.Serialize([telemetryItem]);
+ Transmission transmission = new(
+ EndpointAddress,
+ data,
+ "application/x-json-stream",
+ JsonSerializer.CompressionType);
+
+ _storage.EnqueueAsync(transmission).ConfigureAwait(false).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannel.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannel.cs
new file mode 100644
index 000000000000..69affdc523a8
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannel.cs
@@ -0,0 +1,114 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using Microsoft.ApplicationInsights.Channel;
+using IChannelTelemetry = Microsoft.ApplicationInsights.Channel.ITelemetry;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+///
+/// Represents a communication channel for sending telemetry to Application Insights via HTTPS.
+///
+internal sealed class PersistenceChannel : ITelemetryChannel
+{
+ internal const string TelemetryServiceEndpoint = "https://dc.services.visualstudio.com/v2/track";
+
+ private readonly FlushManager _flushManager;
+
+ private int _disposeCount;
+ private readonly BaseStorageService _storage;
+ private readonly PersistenceTransmitter _transmitter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// Full path of a directory name. Under this folder all the transmissions will be saved.
+ /// Setting this value groups channels, even from different processes.
+ /// If 2 (or more) channels has the same storageFolderName only one channel will perform the sending even if the
+ /// channel is in a different process/AppDomain/Thread.
+ ///
+ ///
+ /// Defines the number of senders. A sender is a long-running thread that sends telemetry batches in intervals defined
+ /// by .
+ /// So the amount of senders also defined the maximum amount of http channels opened at the same time.
+ ///
+ public PersistenceChannel(string storageDirectoryPath = null, int sendersCount = 1)
+ {
+ _storage = new StorageService();
+ _storage.Init(storageDirectoryPath);
+ _transmitter = new PersistenceTransmitter(_storage, sendersCount);
+ _flushManager = new FlushManager(_storage);
+ EndpointAddress = TelemetryServiceEndpoint;
+ }
+
+ ///
+ /// Gets or sets an interval between each successful sending.
+ ///
+ ///
+ /// On error scenario this value is ignored and the interval will be defined using an exponential back-off
+ /// algorithm.
+ ///
+ public TimeSpan? SendingInterval
+ {
+ get => _transmitter.SendingInterval;
+ set => _transmitter.SendingInterval = value;
+ }
+
+
+ ///
+ /// Gets or sets the maximum amount of files allowed in storage. When the limit is reached telemetries will be dropped.
+ ///
+ public uint MaxTransmissionStorageFilesCapacity
+ {
+ get => _storage.MaxFiles;
+ set => _storage.MaxFiles = value;
+ }
+
+ ///
+ /// This flag has no effect. But it is required by base class
+ ///
+ public bool? DeveloperMode { get; set; }
+
+ ///
+ /// Gets or sets the HTTP address where the telemetry is sent.
+ ///
+ public string EndpointAddress
+ {
+ get => _flushManager.EndpointAddress.ToString();
+
+ set
+ {
+ string address = value ?? TelemetryServiceEndpoint;
+ _flushManager.EndpointAddress = new Uri(address);
+ }
+ }
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ public void Dispose()
+ {
+ if (Interlocked.Increment(ref _disposeCount) == 1)
+ {
+ _transmitter?.Dispose();
+ }
+ }
+
+ ///
+ /// Sends an instance of ITelemetry through the channel.
+ ///
+ public void Send(IChannelTelemetry item)
+ {
+ _flushManager.Flush(item);
+ }
+
+ ///
+ /// No operation, send will always flush. So nothing will be in memory
+ ///
+ public void Flush()
+ {
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannelDebugLog.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannelDebugLog.cs
new file mode 100644
index 000000000000..ff695b79e6c3
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceChannelDebugLog.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Globalization;
+using Microsoft.DotNet.Cli.Utils;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+internal static class PersistenceChannelDebugLog
+{
+ private static readonly bool _isEnabled = IsEnabledByEnvironment();
+
+ private static bool IsEnabledByEnvironment()
+ {
+ var environmentProvider = new EnvironmentProvider();
+ return environmentProvider.GetEnvironmentVariableAsBool("DOTNET_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT", false);
+ }
+
+ public static void WriteLine(string message)
+ {
+ if (_isEnabled)
+ {
+ Reporter.Output.WriteLine(message);
+ }
+ }
+
+ internal static void WriteException(Exception exception, string format, params string[] args)
+ {
+ var message = string.Format(CultureInfo.InvariantCulture, format, args);
+ WriteLine(string.Format(CultureInfo.InvariantCulture, "{0} Exception: {1}", message, exception.ToString()));
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceTransmitter.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceTransmitter.cs
new file mode 100644
index 000000000000..0cede405eb04
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/PersistenceTransmitter.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.
+
+#nullable disable
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+///
+/// Implements throttled and persisted transmission of telemetry to Application Insights.
+///
+internal class PersistenceTransmitter : IDisposable
+{
+ ///
+ /// The number of times this object was disposed.
+ ///
+ private int _disposeCount;
+
+ ///
+ /// A list of senders that sends transmissions.
+ ///
+ private readonly List _senders = [];
+
+ ///
+ /// The storage that is used to persist all the transmissions.
+ ///
+ private readonly BaseStorageService _storage;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The transmissions storage.
+ /// The number of senders to create.
+ ///
+ /// A boolean value that indicates if this class should try and create senders. This is a
+ /// workaround for unit tests purposes only.
+ ///
+ internal PersistenceTransmitter(BaseStorageService storage, int sendersCount, bool createSenders = true)
+ {
+ _storage = storage;
+ if (createSenders)
+ {
+ for (int i = 0; i < sendersCount; i++)
+ {
+ _senders.Add(new Sender(_storage, this));
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the interval between each successful sending.
+ ///
+ internal TimeSpan? SendingInterval { get; set; }
+
+ ///
+ /// Disposes the object.
+ ///
+ public void Dispose()
+ {
+ if (Interlocked.Increment(ref _disposeCount) == 1)
+ {
+ StopSenders();
+ }
+ }
+
+ ///
+ /// Stops the senders.
+ ///
+ /// As long as there is no Start implementation, this method should only be called from Dispose.
+ private void StopSenders()
+ {
+ if (_senders == null)
+ {
+ return;
+ }
+
+ List stoppedTasks = [];
+ foreach (Sender sender in _senders)
+ {
+ stoppedTasks.Add(sender.StopAsync());
+ }
+
+ Task.WaitAll([.. stoppedTasks]);
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/Sender.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/Sender.cs
new file mode 100644
index 000000000000..6e0f7ceceaa9
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/Sender.cs
@@ -0,0 +1,336 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Net;
+using System.Net.NetworkInformation;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+///
+/// Fetch transmissions from the storage and sends it.
+///
+internal class Sender : IDisposable
+{
+ ///
+ /// The default sending interval.
+ ///
+ private readonly TimeSpan _defaultSendingInterval;
+
+ ///
+ /// A wait handle that flags the sender when to start sending again. The type is protected for unit test.
+ ///
+ protected readonly AutoResetEvent DelayHandler;
+
+ ///
+ /// Holds the maximum time for the exponential back-off algorithm. The sending interval will grow on every HTTP
+ /// Exception until this max value.
+ ///
+ private readonly TimeSpan _maxIntervalBetweenRetries = TimeSpan.FromHours(1);
+
+ ///
+ /// When storage is empty it will be queried again after this interval.
+ /// Decreasing to 5 sec to send first data (users and sessions).
+ ///
+ private readonly TimeSpan _sendingIntervalOnNoData = TimeSpan.FromSeconds(5);
+
+ ///
+ /// A wait handle that is being set when Sender is no longer sending.
+ ///
+ private readonly AutoResetEvent _stoppedHandler;
+
+ ///
+ /// The number of times this object was disposed.
+ ///
+ private int _disposeCount;
+
+ ///
+ /// The amount of time to wait, in the stop method, until the last transmission is sent.
+ /// If time expires, the stop method will return even if the transmission hasn't been sent.
+ ///
+ private readonly TimeSpan _drainingTimeout;
+
+ ///
+ /// A boolean value that indicates if the sender should be stopped. The sender's while loop is checking this boolean
+ /// value.
+ ///
+ private bool _stopped;
+
+ ///
+ /// The transmissions storage.
+ ///
+ private readonly BaseStorageService _storage;
+
+ ///
+ /// Holds the transmitter.
+ ///
+ private readonly PersistenceTransmitter _transmitter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The storage that holds the transmissions to send.
+ ///
+ /// The persistence transmitter that manages this Sender.
+ /// The transmitter will be used as a configuration class, it exposes properties like SendingInterval that will be read
+ /// by Sender.
+ ///
+ ///
+ /// A boolean value that determines if Sender should start sending immediately. This is only
+ /// used for unit tests.
+ ///
+ internal Sender(BaseStorageService storage, PersistenceTransmitter transmitter, bool startSending = true)
+ {
+ _stopped = false;
+ DelayHandler = new AutoResetEvent(false);
+ _stoppedHandler = new AutoResetEvent(false);
+ _drainingTimeout = TimeSpan.FromSeconds(100);
+ _defaultSendingInterval = TimeSpan.FromSeconds(5);
+
+ _transmitter = transmitter;
+ _storage = storage;
+
+ if (startSending)
+ {
+ // It is currently possible for the long - running task to be executed(and thereby block during WaitOne) on the UI thread when
+ // called by a task scheduled on the UI thread. Explicitly specifying TaskScheduler.Default
+ // when calling StartNew guarantees that Sender never blocks the main thread.
+ Task.Factory.StartNew(SendLoop, CancellationToken.None, TaskCreationOptions.LongRunning,
+ TaskScheduler.Default)
+ .ContinueWith(
+ t => PersistenceChannelDebugLog.WriteException(t.Exception, "Sender: Failure in SendLoop"),
+ TaskContinuationOptions.OnlyOnFaulted);
+ }
+ }
+
+ ///
+ /// Gets the interval between each successful sending.
+ ///
+ private TimeSpan SendingInterval
+ {
+ get
+ {
+ if (_transmitter.SendingInterval != null)
+ {
+ return _transmitter.SendingInterval.Value;
+ }
+
+ return _defaultSendingInterval;
+ }
+ }
+
+ ///
+ /// Disposes the managed objects.
+ ///
+ public void Dispose()
+ {
+ if (Interlocked.Increment(ref _disposeCount) == 1)
+ {
+ StopAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+
+ DelayHandler.Dispose();
+ _stoppedHandler.Dispose();
+ }
+ }
+
+ ///
+ /// Stops the sender.
+ ///
+ internal Task StopAsync()
+ {
+ // After delayHandler is set, a sending iteration will immediately start.
+ // Setting stopped to true, will cause the iteration to skip the actual sending and stop immediately.
+ _stopped = true;
+ DelayHandler.Set();
+
+ // if delayHandler was set while a transmission was being sent, the return task will wait for it to finish, for an additional second,
+ // before it will mark the task as completed.
+ return Task.Run(() =>
+ {
+ try
+ {
+ _stoppedHandler.WaitOne(_drainingTimeout);
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ });
+ }
+
+ ///
+ /// Send transmissions in a loop.
+ ///
+ protected void SendLoop()
+ {
+ TimeSpan prevSendingInterval = TimeSpan.Zero;
+ TimeSpan sendingInterval = _sendingIntervalOnNoData;
+ try
+ {
+ while (!_stopped)
+ {
+ using (StorageTransmission transmission = _storage.Peek())
+ {
+ if (_stopped)
+ {
+ // This second verification is required for cases where 'stopped' was set while peek was happening.
+ // Once the actual sending starts the design is to wait until it finishes and deletes the transmission.
+ // So no extra validation is required.
+ break;
+ }
+
+ // If there is a transmission to send - send it.
+ if (transmission != null)
+ {
+ bool shouldRetry = Send(transmission, ref sendingInterval);
+ if (!shouldRetry)
+ {
+ // If retry is not required - delete the transmission.
+ _storage.Delete(transmission);
+ }
+ }
+ else
+ {
+ sendingInterval = _sendingIntervalOnNoData;
+ }
+ }
+
+ LogInterval(prevSendingInterval, sendingInterval);
+ DelayHandler.WaitOne(sendingInterval);
+ prevSendingInterval = sendingInterval;
+ }
+
+ _stoppedHandler.Set();
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+
+ ///
+ /// Sends a transmission and handle errors.
+ ///
+ /// The transmission to send.
+ ///
+ /// When this value returns it will hold a recommendation for when to start the next sending
+ /// iteration.
+ ///
+ /// True, if there was sent error and we need to retry sending, otherwise false.
+ protected virtual bool Send(StorageTransmission transmission, ref TimeSpan nextSendInterval)
+ {
+ try
+ {
+ if (transmission != null)
+ {
+ bool isConnected = NetworkInterface.GetIsNetworkAvailable();
+
+ // there is no internet connection available, return than.
+ if (!isConnected)
+ {
+ PersistenceChannelDebugLog.WriteLine(
+ "Cannot send data to the server. Internet connection is not available");
+ return true;
+ }
+
+ transmission.SendAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // After a successful sending, try immediately to send another transmission.
+ nextSendInterval = SendingInterval;
+ }
+ }
+ catch (WebException e)
+ {
+ int? statusCode = GetStatusCode(e);
+ nextSendInterval = CalculateNextInterval(statusCode, nextSendInterval, _maxIntervalBetweenRetries);
+ return IsRetryable(statusCode, e.Status);
+ }
+ catch (Exception e)
+ {
+ nextSendInterval = CalculateNextInterval(null, nextSendInterval, _maxIntervalBetweenRetries);
+ PersistenceChannelDebugLog.WriteException(e, "Unknown exception during sending");
+ }
+
+ return false;
+ }
+
+ ///
+ /// Log next interval. Only log the interval when it changes by more then a minute. So if interval grow by 1 minute or
+ /// decreased by 1 minute it will be logged.
+ /// Logging every interval will just make the log noisy.
+ ///
+ private static void LogInterval(TimeSpan prevSendInterval, TimeSpan nextSendInterval)
+ {
+ if (Math.Abs(nextSendInterval.TotalSeconds - prevSendInterval.TotalSeconds) > 60)
+ {
+ PersistenceChannelDebugLog.WriteLine("next sending interval: " + nextSendInterval);
+ }
+ }
+
+ ///
+ /// Return the status code from the web exception or null if no such code exists.
+ ///
+ private static int? GetStatusCode(WebException e)
+ {
+ if (e.Response is HttpWebResponse httpWebResponse)
+ {
+ return (int)httpWebResponse.StatusCode;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns true if or are retryable.
+ ///
+ private static bool IsRetryable(int? httpStatusCode, WebExceptionStatus webExceptionStatus)
+ {
+ switch (webExceptionStatus)
+ {
+ case WebExceptionStatus.ProxyNameResolutionFailure:
+ case WebExceptionStatus.NameResolutionFailure:
+ case WebExceptionStatus.Timeout:
+ case WebExceptionStatus.ConnectFailure:
+ return true;
+ }
+
+ if (httpStatusCode == null)
+ {
+ return false;
+ }
+
+ switch (httpStatusCode.Value)
+ {
+ case 503: // Server in maintenance.
+ case 408: // invalid request
+ case 500: // Internal Server Error
+ case 502: // Bad Gateway, can be common when there is no network.
+ case 511: // Network Authentication Required
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Calculates the next interval using exponential back-off algorithm (with the exceptions of few error codes that
+ /// reset the interval to .
+ ///
+ private TimeSpan CalculateNextInterval(int? httpStatusCode, TimeSpan currentSendInterval, TimeSpan maxInterval)
+ {
+ // if item is expired, no need for exponential back-off
+ if (httpStatusCode != null && httpStatusCode.Value == 400 /* expired */)
+ {
+ return SendingInterval;
+ }
+
+ // exponential back-off.
+ if (Math.Abs(currentSendInterval.TotalSeconds) < 1)
+ {
+ return TimeSpan.FromSeconds(1);
+ }
+
+ double nextIntervalInSeconds = Math.Min(currentSendInterval.TotalSeconds * 2, maxInterval.TotalSeconds);
+
+ return TimeSpan.FromSeconds(nextIntervalInSeconds);
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingCollection.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingCollection.cs
new file mode 100644
index 000000000000..3e98a0d598a8
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingCollection.cs
@@ -0,0 +1,95 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Collections;
+using System.Diagnostics;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+internal abstract class SnapshottingCollection : ICollection
+ where TCollection : class, ICollection
+{
+ protected readonly TCollection Collection;
+ protected TCollection snapshot;
+
+ protected SnapshottingCollection(TCollection collection)
+ {
+ Debug.Assert(collection != null, "collection");
+ Collection = collection;
+ }
+
+ public int Count => GetSnapshot().Count;
+
+ public bool IsReadOnly => false;
+
+ public void Add(TItem item)
+ {
+ lock (Collection)
+ {
+ Collection.Add(item);
+ snapshot = default;
+ }
+ }
+
+ public void Clear()
+ {
+ lock (Collection)
+ {
+ Collection.Clear();
+ snapshot = default;
+ }
+ }
+
+ public bool Contains(TItem item)
+ {
+ return GetSnapshot().Contains(item);
+ }
+
+ public void CopyTo(TItem[] array, int arrayIndex)
+ {
+ GetSnapshot().CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(TItem item)
+ {
+ lock (Collection)
+ {
+ bool removed = Collection.Remove(item);
+ if (removed)
+ {
+ snapshot = default;
+ }
+
+ return removed;
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return GetSnapshot().GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ protected abstract TCollection CreateSnapshot(TCollection collection);
+
+ protected TCollection GetSnapshot()
+ {
+ TCollection localSnapshot = snapshot;
+ if (localSnapshot == null)
+ {
+ lock (Collection)
+ {
+ snapshot = CreateSnapshot(Collection);
+ localSnapshot = snapshot;
+ }
+ }
+
+ return localSnapshot;
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingDictionary.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingDictionary.cs
new file mode 100644
index 000000000000..cc6cc51a1f57
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/SnapshottingDictionary.cs
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+internal class SnapshottingDictionary :
+ SnapshottingCollection, IDictionary>, IDictionary
+{
+ public SnapshottingDictionary()
+ : base(new Dictionary())
+ {
+ }
+
+ public ICollection Keys => GetSnapshot().Keys;
+
+ public ICollection Values => GetSnapshot().Values;
+
+ public TValue this[TKey key]
+ {
+ get => GetSnapshot()[key];
+
+ set
+ {
+ lock (Collection)
+ {
+ Collection[key] = value;
+ snapshot = null;
+ }
+ }
+ }
+
+ public void Add(TKey key, TValue value)
+ {
+ lock (Collection)
+ {
+ Collection.Add(key, value);
+ snapshot = null;
+ }
+ }
+
+ public bool ContainsKey(TKey key)
+ {
+ return GetSnapshot().ContainsKey(key);
+ }
+
+ public bool Remove(TKey key)
+ {
+ lock (Collection)
+ {
+ bool removed = Collection.Remove(key);
+ if (removed)
+ {
+ snapshot = null;
+ }
+
+ return removed;
+ }
+ }
+
+ public bool TryGetValue(TKey key, out TValue value)
+ {
+ return GetSnapshot().TryGetValue(key, out value);
+ }
+
+ protected sealed override IDictionary CreateSnapshot(IDictionary collection)
+ {
+ return new Dictionary(collection);
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageService.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageService.cs
new file mode 100644
index 000000000000..dc44a0bf3cbb
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageService.cs
@@ -0,0 +1,352 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Globalization;
+using Microsoft.ApplicationInsights.Channel;
+using Microsoft.DotNet.Configurer;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+internal sealed class StorageService : BaseStorageService
+{
+ private const string DefaultStorageFolderName = "TelemetryStorageService";
+ private readonly FixedSizeQueue _deletedFilesQueue = new(10);
+
+ private readonly object _peekLockObj = new();
+ private readonly object _storageFolderLock = new();
+ private string _storageDirectoryPath;
+ private string _storageDirectoryPathUsed;
+ private long _storageCountFiles;
+ private bool _storageFolderInitialized;
+ private long _storageSize;
+ private uint _transmissionsDropped;
+
+ ///
+ /// Gets the storage's folder name.
+ ///
+ internal override string StorageDirectoryPath => _storageDirectoryPath;
+
+ ///
+ /// Gets the storage folder. If storage folder couldn't be created, null will be returned.
+ ///
+ private string StorageFolder
+ {
+ get
+ {
+ if (!_storageFolderInitialized)
+ {
+ lock (_storageFolderLock)
+ {
+ if (!_storageFolderInitialized)
+ {
+ try
+ {
+ _storageDirectoryPathUsed = _storageDirectoryPath;
+
+ if (!Directory.Exists(_storageDirectoryPathUsed))
+ {
+ Directory.CreateDirectory(_storageDirectoryPathUsed);
+ }
+ }
+ catch (Exception e)
+ {
+ _storageDirectoryPathUsed = null;
+ PersistenceChannelDebugLog.WriteException(e, "Failed to create storage folder");
+ }
+
+ _storageFolderInitialized = true;
+ }
+ }
+ }
+
+ return _storageDirectoryPathUsed;
+ }
+ }
+
+ internal override void Init(string storageDirectoryPath)
+ {
+ PeekedTransmissions = new SnapshottingDictionary();
+
+ VerifyOrSetDefaultStorageDirectoryPath(storageDirectoryPath);
+
+ CapacityInBytes = 10 * 1024 * 1024; // 10 MB
+ MaxFiles = 100;
+
+ Task.Run(DeleteObsoleteFiles)
+ .ContinueWith(
+ task =>
+ {
+ PersistenceChannelDebugLog.WriteException(
+ task.Exception,
+ "Storage: Unhandled exception in DeleteObsoleteFiles");
+ },
+ TaskContinuationOptions.OnlyOnFaulted);
+ }
+
+ private void VerifyOrSetDefaultStorageDirectoryPath(string desireStorageDirectoryPath)
+ {
+ if (string.IsNullOrEmpty(desireStorageDirectoryPath))
+ {
+ _storageDirectoryPath = Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath,
+ DefaultStorageFolderName);
+ }
+ else
+ {
+ if (!Path.IsPathRooted(desireStorageDirectoryPath))
+ {
+ throw new ArgumentException($"{nameof(desireStorageDirectoryPath)} need to be rooted (full path)");
+ }
+
+ _storageDirectoryPath = desireStorageDirectoryPath;
+ }
+ }
+
+ ///
+ /// Reads an item from the storage. Order is Last-In-First-Out.
+ /// When the Transmission is no longer needed (it was either sent or failed with a non-retryable error) it should be
+ /// disposed.
+ ///
+ internal override StorageTransmission Peek()
+ {
+ IEnumerable files = GetFiles("*.trn", 50);
+
+ lock (_peekLockObj)
+ {
+ foreach (string file in files)
+ {
+ try
+ {
+ // if a file was peeked before, skip it (wait until it is disposed).
+ if (PeekedTransmissions.ContainsKey(file) == false &&
+ _deletedFilesQueue.Contains(file) == false)
+ {
+ // Load the transmission from disk.
+ StorageTransmission storageTransmissionItem = LoadTransmissionFromFileAsync(file)
+ .ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // when item is disposed it should be removed from the peeked list.
+ storageTransmissionItem.Disposing = item => OnPeekedItemDisposed(file);
+
+ // add the transmission to the list.
+ PeekedTransmissions.Add(file, storageTransmissionItem.FullFilePath);
+ return storageTransmissionItem;
+ }
+ }
+ catch (Exception e)
+ {
+ PersistenceChannelDebugLog.WriteException(
+ e,
+ "Failed to load an item from the storage. file: {0}",
+ file);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ internal override void Delete(StorageTransmission item)
+ {
+ try
+ {
+ if (StorageFolder == null)
+ {
+ return;
+ }
+
+ // Initial storage size calculation.
+ CalculateSize();
+
+ long fileSize = GetSize(item.FileName);
+ File.Delete(Path.Combine(StorageFolder, item.FileName));
+
+ _deletedFilesQueue.Enqueue(item.FileName);
+
+ // calculate size
+ Interlocked.Add(ref _storageSize, -fileSize);
+ Interlocked.Decrement(ref _storageCountFiles);
+ }
+ catch (IOException e)
+ {
+ PersistenceChannelDebugLog.WriteException(e, "Failed to delete a file. file: {0}", item == null ? "null" : item.FullFilePath);
+ }
+ }
+
+ internal override async Task EnqueueAsync(Transmission transmission)
+ {
+ try
+ {
+ if (transmission == null || StorageFolder == null)
+ {
+ return;
+ }
+
+ // Initial storage size calculation.
+ CalculateSize();
+
+ if ((ulong)_storageSize >= CapacityInBytes || _storageCountFiles >= MaxFiles)
+ {
+ // if max storage capacity has reached, drop the transmission (but log every 100 lost transmissions).
+ if (_transmissionsDropped++ % 100 == 0)
+ {
+ PersistenceChannelDebugLog.WriteLine("Total transmissions dropped: " + _transmissionsDropped);
+ }
+
+ return;
+ }
+
+ // Writes content to a temporary file and only then rename to avoid the Peek from reading the file before it is being written.
+ // Creates the temp file name
+ string tempFileName = Guid.NewGuid().ToString("N");
+
+ // Now that the file got created we can increase the files count
+ Interlocked.Increment(ref _storageCountFiles);
+
+ // Saves transmission to the temp file
+ await SaveTransmissionToFileAsync(transmission, tempFileName).ConfigureAwait(false);
+
+ // Now that the file is written increase storage size.
+ long temporaryFileSize = GetSize(tempFileName);
+ Interlocked.Add(ref _storageSize, temporaryFileSize);
+
+ // Creates a new file name
+ string now = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
+ string newFileName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}.trn", now, tempFileName);
+
+ // Renames the file
+ File.Move(Path.Combine(StorageFolder, tempFileName), Path.Combine(StorageFolder, newFileName));
+ }
+ catch (Exception e)
+ {
+ PersistenceChannelDebugLog.WriteException(e, "EnqueueAsync");
+ }
+ }
+
+ private async Task SaveTransmissionToFileAsync(Transmission transmission, string file)
+ {
+ try
+ {
+ using (Stream stream = File.OpenWrite(Path.Combine(StorageFolder, file)))
+ {
+ await StorageTransmission.SaveAsync(transmission, stream).ConfigureAwait(false);
+ }
+ }
+ catch (UnauthorizedAccessException)
+ {
+ string message =
+ string.Format(
+ "Failed to save transmission to file. UnauthorizedAccessException. File path: {0}, FileName: {1}",
+ StorageFolder, file);
+ PersistenceChannelDebugLog.WriteLine(message);
+ throw;
+ }
+ }
+
+ private async Task LoadTransmissionFromFileAsync(string file)
+ {
+ try
+ {
+ using (Stream stream = File.OpenRead(Path.Combine(StorageFolder, file)))
+ {
+ StorageTransmission storageTransmissionItem =
+ await StorageTransmission.CreateFromStreamAsync(stream, file).ConfigureAwait(false);
+ return storageTransmissionItem;
+ }
+ }
+ catch (Exception e)
+ {
+ string message =
+ string.Format(
+ "Failed to load transmission from file. File path: {0}, FileName: {1}, Exception: {2}",
+ "storageFolderName", file, e);
+ PersistenceChannelDebugLog.WriteLine(message);
+ throw;
+ }
+ }
+
+ ///
+ /// Get files from .
+ ///
+ /// Define the logic for sorting the files.
+ /// Defines a file extension. This method will return only files with this extension.
+ ///
+ /// Define how many files to return. This can be useful when the directory has a lot of files, in that case
+ /// GetFilesAsync will have a performance hit.
+ ///
+ private IEnumerable GetFiles(string filterByExtension, int top)
+ {
+ try
+ {
+ if (StorageFolder != null)
+ {
+ return Directory.GetFiles(StorageFolder, filterByExtension).Take(top);
+ }
+ }
+ catch (Exception e)
+ {
+ PersistenceChannelDebugLog.WriteException(e, "Peek failed while get files from storage.");
+ }
+
+ return [];
+ }
+
+ ///
+ /// Gets a file's size.
+ ///
+ private long GetSize(string file)
+ {
+ using (FileStream stream = File.OpenRead(Path.Combine(StorageFolder, file)))
+ {
+ return stream.Length;
+ }
+ }
+
+ ///
+ /// Check the storage limits and return true if they reached.
+ /// Storage limits are defined by the number of files and the total size on disk.
+ ///
+ private void CalculateSize()
+ {
+ string[] storageFiles = Directory.GetFiles(StorageFolder, "*.*");
+
+ _storageCountFiles = storageFiles.Count();
+
+ long storageSizeInBytes = 0;
+ foreach (string file in storageFiles)
+ {
+ storageSizeInBytes += GetSize(file);
+ }
+
+ _storageSize = storageSizeInBytes;
+ }
+
+ ///
+ /// Enqueue is saving a transmission to a file with a guid, and after a successful write operation it renames it to a
+ /// trn file.
+ /// A file without a trn extension is ignored by Storage.Peek(), so if a process is taken down before rename
+ /// happens it will stay on the disk forever.
+ /// This thread deletes files with the trn extension that exists on disk for more than 5 minutes.
+ ///
+ private void DeleteObsoleteFiles()
+ {
+ try
+ {
+ IEnumerable files = GetFiles("*.trn", 50);
+ foreach (string file in files)
+ {
+ DateTime creationTime = File.GetCreationTimeUtc(Path.Combine(StorageFolder, file));
+ // if the file is older then 5 minutes - delete it.
+ if (DateTime.UtcNow - creationTime >= TimeSpan.FromMinutes(5))
+ {
+ File.Delete(Path.Combine(StorageFolder, file));
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ PersistenceChannelDebugLog.WriteException(e, "Failed to delete tmp files.");
+ }
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageTransmission.cs b/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageTransmission.cs
new file mode 100644
index 000000000000..69b6132f1b9f
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/PersistenceChannel/StorageTransmission.cs
@@ -0,0 +1,127 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Globalization;
+using Microsoft.ApplicationInsights.Channel;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel;
+
+internal class StorageTransmission : Transmission, IDisposable
+{
+ internal Action Disposing;
+
+ protected StorageTransmission(string fullPath, Uri address, byte[] content, string contentType,
+ string contentEncoding)
+ : base(address, content, contentType, contentEncoding)
+ {
+ FullFilePath = fullPath;
+ FileName = Path.GetFileName(fullPath);
+ }
+
+ internal string FileName { get; }
+
+ internal string FullFilePath { get; }
+
+ ///
+ /// Disposing the storage transmission.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Creates a new transmission from the specified .
+ ///
+ /// Return transmission loaded from file; return null if the file is corrupted.
+ internal static async Task CreateFromStreamAsync(Stream stream, string fileName)
+ {
+ StreamReader reader = new(stream);
+ Uri address = await ReadAddressAsync(reader).ConfigureAwait(false);
+ string contentType = await ReadHeaderAsync(reader, "Content-Type").ConfigureAwait(false);
+ string contentEncoding = await ReadHeaderAsync(reader, "Content-Encoding").ConfigureAwait(false);
+ byte[] content = await ReadContentAsync(reader).ConfigureAwait(false);
+ return new StorageTransmission(fileName, address, content, contentType, contentEncoding);
+ }
+
+ ///
+ /// Saves the transmission to the specified .
+ ///
+ internal static async Task SaveAsync(Transmission transmission, Stream stream)
+ {
+ StreamWriter writer = new(stream);
+ try
+ {
+ await writer.WriteLineAsync(transmission.EndpointAddress.ToString()).ConfigureAwait(false);
+ await writer.WriteLineAsync("Content-Type" + ":" + transmission.ContentType).ConfigureAwait(false);
+ await writer.WriteLineAsync("Content-Encoding" + ":" + transmission.ContentEncoding)
+ .ConfigureAwait(false);
+ await writer.WriteLineAsync(string.Empty).ConfigureAwait(false);
+ await writer.WriteAsync(Convert.ToBase64String(transmission.Content)).ConfigureAwait(false);
+ }
+ finally
+ {
+ writer.Flush();
+ }
+ }
+
+ private static async Task ReadHeaderAsync(TextReader reader, string headerName)
+ {
+ string line = await reader.ReadLineAsync().ConfigureAwait(false);
+ if (string.IsNullOrEmpty(line))
+ {
+ throw new FormatException(string.Format(CultureInfo.InvariantCulture, "{0} header is expected.",
+ headerName));
+ }
+
+ string[] parts = line.Split(':');
+ if (parts.Length != 2)
+ {
+ throw new FormatException(string.Format(CultureInfo.InvariantCulture,
+ "Unexpected header format. {0} header is expected. Actual header: {1}", headerName, line));
+ }
+
+ if (parts[0] != headerName)
+ {
+ throw new FormatException(string.Format(CultureInfo.InvariantCulture,
+ "{0} header is expected. Actual header: {1}", headerName, line));
+ }
+
+ return parts[1].Trim();
+ }
+
+ private static async Task ReadAddressAsync(TextReader reader)
+ {
+ string addressLine = await reader.ReadLineAsync().ConfigureAwait(false);
+ if (string.IsNullOrEmpty(addressLine))
+ {
+ throw new FormatException("Transmission address is expected.");
+ }
+
+ Uri address = new(addressLine);
+ return address;
+ }
+
+ private static async Task ReadContentAsync(TextReader reader)
+ {
+ string content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ if (string.IsNullOrEmpty(content) || content == Environment.NewLine)
+ {
+ throw new FormatException("Content is expected.");
+ }
+
+ return Convert.FromBase64String(content);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Action disposingDelegate = Disposing;
+ disposingDelegate?.Invoke(this);
+ }
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/Telemetry.cs b/src/Cli/dotnet/Telemetry/Telemetry.cs
new file mode 100644
index 000000000000..38f0d1c7ca19
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/Telemetry.cs
@@ -0,0 +1,263 @@
+// 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.Frozen;
+using System.Diagnostics;
+using Microsoft.ApplicationInsights;
+using Microsoft.ApplicationInsights.Extensibility;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.DotNet.Configurer;
+using CLIRuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment;
+
+namespace Microsoft.DotNet.Cli.Telemetry;
+
+public class Telemetry : ITelemetry
+{
+ internal static string? CurrentSessionId = null;
+ internal static bool DisabledForTests = false;
+ private readonly int _senderCount;
+ private TelemetryClient? _client = null;
+ private FrozenDictionary? _commonProperties = null;
+ private FrozenDictionary? _commonMeasurements = null;
+ private Task? _trackEventTask = null;
+
+ private const string ConnectionString = "InstrumentationKey=74cc1c9e-3e6e-4d05-b3fc-dde9101d0254";
+
+ public bool Enabled { get; }
+
+ public Telemetry() : this(null) { }
+
+ public Telemetry(IFirstTimeUseNoticeSentinel? sentinel) : this(sentinel, null) { }
+
+ public Telemetry(
+ IFirstTimeUseNoticeSentinel? sentinel,
+ string? sessionId,
+ bool blockThreadInitialization = false,
+ IEnvironmentProvider? environmentProvider = null,
+ int senderCount = 3)
+ {
+
+ if (DisabledForTests)
+ {
+ return;
+ }
+
+ environmentProvider ??= new EnvironmentProvider();
+
+ Enabled = !environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault)
+ && PermissionExists(sentinel);
+
+ if (!Enabled)
+ {
+ return;
+ }
+
+ // Store the session ID in a static field so that it can be reused
+ CurrentSessionId = sessionId ?? Guid.NewGuid().ToString();
+ _senderCount = senderCount;
+ if (blockThreadInitialization)
+ {
+ InitializeTelemetry();
+ }
+ else
+ {
+ //initialize in task to offload to parallel thread
+ _trackEventTask = Task.Run(() => InitializeTelemetry());
+ }
+ }
+
+ internal static void DisableForTests()
+ {
+ DisabledForTests = true;
+ CurrentSessionId = null;
+ }
+
+ internal static void EnableForTests()
+ {
+ DisabledForTests = false;
+ }
+
+ private static bool PermissionExists(IFirstTimeUseNoticeSentinel? sentinel)
+ {
+ if (sentinel == null)
+ {
+ return false;
+ }
+
+ return sentinel.Exists();
+ }
+
+ public void TrackEvent(string eventName, IDictionary properties,
+ IDictionary measurements)
+ {
+ if (!Enabled)
+ {
+ return;
+ }
+
+ //continue the task in different threads
+ if (_trackEventTask == null)
+ {
+ _trackEventTask = Task.Run(() => TrackEventTask(eventName, properties, measurements));
+ return;
+ }
+ else
+ {
+ _trackEventTask = _trackEventTask.ContinueWith(
+ x => TrackEventTask(eventName, properties, measurements)
+ );
+ }
+ }
+
+ public void Flush()
+ {
+ if (!Enabled || _trackEventTask == null)
+ {
+ return;
+ }
+
+ _trackEventTask.Wait();
+ }
+
+ // Adding dispose on graceful shutdown per https://github.com/microsoft/ApplicationInsights-dotnet/issues/1152#issuecomment-518742922
+ public void Dispose()
+ {
+ if (_client != null)
+ {
+ _client.TelemetryConfiguration.Dispose();
+ _client = null;
+ }
+ }
+
+ public void ThreadBlockingTrackEvent(string eventName, IDictionary properties, IDictionary measurements)
+ {
+ if (!Enabled)
+ {
+ return;
+ }
+ TrackEventTask(eventName, properties, measurements);
+ }
+
+ private void InitializeTelemetry()
+ {
+ try
+ {
+ var persistenceChannel = new PersistenceChannel.PersistenceChannel(sendersCount: _senderCount)
+ {
+ SendingInterval = TimeSpan.FromMilliseconds(1)
+ };
+
+ var config = TelemetryConfiguration.CreateDefault();
+ config.TelemetryChannel = persistenceChannel;
+ config.ConnectionString = ConnectionString;
+ _client = new TelemetryClient(config);
+ _client.Context.Session.Id = CurrentSessionId;
+ _client.Context.Device.OperatingSystem = CLIRuntimeEnvironment.OperatingSystem;
+
+ _commonProperties = new TelemetryCommonProperties().GetTelemetryCommonProperties(CurrentSessionId);
+ _commonMeasurements = FrozenDictionary.Empty;
+ }
+ catch (Exception e)
+ {
+ _client = null;
+ // we dont want to fail the tool if telemetry fails.
+ Debug.Fail(e.ToString());
+ }
+ }
+
+ private void TrackEventTask(
+ string eventName,
+ IDictionary properties,
+ IDictionary measurements)
+ {
+ if (_client == null)
+ {
+ return;
+ }
+
+ try
+ {
+ var eventProperties = GetEventProperties(properties);
+ var eventMeasurements = GetEventMeasures(measurements);
+
+ eventProperties ??= new Dictionary();
+ eventProperties.Add("event id", Guid.NewGuid().ToString());
+
+ _client.TrackEvent(PrependProducerNamespace(eventName), eventProperties, eventMeasurements);
+ Activity.Current?.AddEvent(CreateActivityEvent(eventName, eventProperties, eventMeasurements));
+ }
+ catch (Exception e)
+ {
+ Debug.Fail(e.ToString());
+ }
+ }
+
+ private static ActivityEvent CreateActivityEvent(
+ string eventName,
+ IDictionary? properties,
+ IDictionary? measurements)
+ {
+ var tags = MakeTags(properties, measurements);
+ return new ActivityEvent(
+ PrependProducerNamespace(eventName),
+ tags: tags);
+ }
+
+ private static ActivityTagsCollection? MakeTags(
+ IDictionary? properties,
+ IDictionary? measurements)
+ {
+ if (properties == null && measurements == null)
+ {
+ return null;
+ }
+ else if (properties != null && measurements == null)
+ {
+ return [.. properties.Select(p => new KeyValuePair(p.Key, p.Value))];
+ }
+ else if (properties == null && measurements != null)
+ {
+ return [.. measurements.Select(m => new KeyValuePair(m.Key, m.Value.ToString()))];
+ }
+ else return [ .. properties!.Select(p => new KeyValuePair(p.Key, p.Value)),
+ .. measurements!.Select(m => new KeyValuePair(m.Key, m.Value.ToString())) ];
+ }
+
+ private static string PrependProducerNamespace(string eventName) => $"dotnet/cli/{eventName}";
+
+ private IDictionary? GetEventMeasures(IDictionary? measurements)
+ {
+ return (measurements, _commonMeasurements) switch
+ {
+ (null, null) => null,
+ (null, not null) => _commonMeasurements == FrozenDictionary.Empty ? null : new Dictionary(_commonMeasurements),
+ (not null, null) => measurements,
+ (not null, not null) => Combine(_commonMeasurements, measurements),
+ };
+ }
+
+ private IDictionary? GetEventProperties(IDictionary? properties)
+ {
+ return (properties, _commonProperties) switch
+ {
+ (null, null) => null,
+ (null, not null) => _commonProperties == FrozenDictionary.Empty ? null : new Dictionary(_commonProperties),
+ (not null, null) => properties,
+ (not null, not null) => Combine(_commonProperties, properties),
+ };
+ }
+
+ static IDictionary Combine(IDictionary common, IDictionary specific) where TKey : notnull
+ {
+ IDictionary eventMeasurements = new Dictionary(capacity: common.Count + specific.Count);
+ foreach (KeyValuePair measurement in common)
+ {
+ eventMeasurements[measurement.Key] = measurement.Value;
+ }
+ foreach (KeyValuePair measurement in specific)
+ {
+ eventMeasurements[measurement.Key] = measurement.Value;
+ }
+ return eventMeasurements;
+ }
+}
diff --git a/src/Cli/dotnet/Telemetry/TelemetryClient.cs b/src/Cli/dotnet/Telemetry/TelemetryClient.cs
deleted file mode 100644
index aaba4e686ef1..000000000000
--- a/src/Cli/dotnet/Telemetry/TelemetryClient.cs
+++ /dev/null
@@ -1,231 +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.Frozen;
-using System.Diagnostics;
-using Microsoft.DotNet.Cli.Utils;
-using Microsoft.DotNet.Configurer;
-
-#if TARGET_WINDOWS
-using Azure.Monitor.OpenTelemetry.Exporter;
-using OpenTelemetry;
-using OpenTelemetry.Context.Propagation;
-using OpenTelemetry.Metrics;
-using OpenTelemetry.Resources;
-using OpenTelemetry.Trace;
-#endif
-
-namespace Microsoft.DotNet.Cli.Telemetry;
-
-public class TelemetryClient : ITelemetryClient
-{
- private static FrozenDictionary s_commonProperties = [];
- private Task? _trackEventTask;
-
-#if TARGET_WINDOWS
- private static readonly MeterProviderBuilder s_metricsProviderBuilder;
- private static MeterProvider? s_metricsProvider;
- private static readonly TracerProviderBuilder s_tracerProviderBuilder;
- private static TracerProvider? s_tracerProvider;
- private static readonly List s_activities = [];
-
- private static readonly string s_connectionString = "InstrumentationKey=74cc1c9e-3e6e-4d05-b3fc-dde9101d0254";
- private static readonly string s_defaultStorageDirectory = Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, "TelemetryStorageService");
- // Note: The TelemetryClient instance constructor takes in an environment provider. These fields don't use that currently.
- private static readonly string? s_environmentStoragePath = Env.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_STORAGE_PATH);
- private static readonly string? s_diskLogPath = Env.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_LOG_PATH);
- private static readonly bool s_disableTraceExport = Env.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_CLI_TELEMETRY_DISABLE_TRACE_EXPORT);
- private static readonly int s_flushTimeoutMs = 200;
-#endif
-
- public static string? CurrentSessionId { get; private set; } = null;
- public static bool DisabledForTests
- {
- get => field;
- set
- {
- field = value;
- // When disabled, clear the session ID.
- if (field)
- {
- CurrentSessionId = null;
- }
- }
- } = false;
- public static ActivityContext ParentActivityContext { get; private set; }
- public static ActivityKind ActivityKind { get; private set; }
-
- public bool Enabled { get; }
-
- static TelemetryClient()
- {
-#if TARGET_WINDOWS
- s_metricsProviderBuilder = Sdk.CreateMeterProviderBuilder()
- .ConfigureResource(r => { r.AddService("dotnet-cli", serviceVersion: Product.Version); })
- .AddMeter(Activities.Source.Name)
- .AddHttpClientInstrumentation()
- .AddRuntimeInstrumentation()
- .AddOtlpExporter();
-
- s_tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
- .ConfigureResource(r => { r.AddService("dotnet-cli", serviceVersion: Product.Version); })
- .AddSource(Activities.Source.Name)
- .AddHttpClientInstrumentation()
- .AddOtlpExporter()
- .AddInMemoryExporter(s_activities)
- .SetSampler(new AlwaysOnSampler());
-
- if (!s_disableTraceExport)
- {
- var storageDirectory = string.IsNullOrWhiteSpace(s_environmentStoragePath) ? s_defaultStorageDirectory : s_environmentStoragePath;
- s_tracerProviderBuilder.AddAzureMonitorTraceExporter(o =>
- {
- o.ConnectionString = s_connectionString;
- o.EnableLiveMetrics = false;
- o.StorageDirectory = storageDirectory;
- });
- }
-#endif
-
- var parentActivityContext = GetParentActivityContext();
- ActivityKind = GetActivityKind(parentActivityContext);
- ParentActivityContext = parentActivityContext ?? default;
- }
-
- public TelemetryClient() : this(null) { }
-
- public TelemetryClient(string? sessionId, IEnvironmentProvider? environmentProvider = null)
- {
- // This is some kind of special condition for MSBuild-related tests.
- if (DisabledForTests)
- {
- return;
- }
-
- environmentProvider ??= new EnvironmentProvider();
- Enabled = !environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT,
- // When building in the official CI pipeline, this makes the complier enable telemetry by default. Otherwise, it is disabled.
- // It is the reason tests don't send telemetry, because we don't run tests in the official CI pipeline.
- defaultValue: CompileOptions.TelemetryOptOutDefault);
- if (!Enabled)
- {
- return;
- }
-
-#if TARGET_WINDOWS
- if (s_metricsProvider is null || s_tracerProvider is null)
- {
- // Create a new OTel meter and tracer provider.
- // It is important to keep the provider instances active throughout the process lifetime.
- s_metricsProvider ??= s_metricsProviderBuilder.Build();
- s_tracerProvider ??= s_tracerProviderBuilder.Build();
- }
-#endif
-
- CurrentSessionId ??= !string.IsNullOrEmpty(sessionId) ? sessionId : Guid.NewGuid().ToString();
- s_commonProperties = new TelemetryCommonProperties().GetTelemetryCommonProperties(CurrentSessionId);
- }
-
- ///
- /// Uses the OpenTelemetry SDK's Propagation API to derive the parent activity context from the DOTNET_CLI_TRACEPARENT and DOTNET_CLI_TRACESTATE environment variables.
- ///
- private static ActivityContext? GetParentActivityContext()
- {
- var traceParent = Env.GetEnvironmentVariable(Activities.TRACEPARENT);
- if (string.IsNullOrEmpty(traceParent))
- {
- return null;
- }
-
- var carrierMap = new Dictionary?> { { "traceparent", [traceParent] } };
- var traceState = Env.GetEnvironmentVariable(Activities.TRACESTATE);
- if (!string.IsNullOrEmpty(traceState))
- {
- carrierMap.Add("tracestate", [traceState]);
- }
-
- ActivityContext? parentContext = null;
-#if TARGET_WINDOWS
- // Use the propegator to extract the parent activity context and kind.
- // For some reason, this isn't set by the OTel SDK like docs say it should be.
- Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator([new TraceContextPropagator(), new BaggagePropagator()]));
- parentContext = Propagators.DefaultTextMapPropagator.Extract(default, carrierMap, GetValueFromCarrier).ActivityContext;
-#endif
- return parentContext;
-
-#if TARGET_WINDOWS
- static IEnumerable? GetValueFromCarrier(Dictionary?> carrier, string key) =>
- carrier.TryGetValue(key, out var value) ? value : null;
-#endif
- }
-
- private static ActivityKind GetActivityKind(ActivityContext? parentActivityContext) =>
- parentActivityContext is ActivityContext { IsRemote: true } ? ActivityKind.Server : ActivityKind.Internal;
-
- public static void FlushProviders()
- {
-#if TARGET_WINDOWS
- s_tracerProvider?.ForceFlush(s_flushTimeoutMs);
- s_metricsProvider?.ForceFlush(s_flushTimeoutMs);
-#endif
- }
-
- public static void WriteLogIfNecessary()
- {
-#if TARGET_WINDOWS
- if (!string.IsNullOrWhiteSpace(s_diskLogPath) && s_activities.Any())
- {
- TelemetryDiskLogger.WriteLog(s_diskLogPath, s_activities);
- }
-#endif
- }
-
- public void TrackEvent(string eventName, IDictionary? properties)
- {
- if (!Enabled)
- {
- return;
- }
-
- // Continue the task in different threads.
- _trackEventTask = _trackEventTask == null
- ? Task.Run(() => TrackEventTask(eventName, properties))
- : _trackEventTask.ContinueWith(_ => TrackEventTask(eventName, properties));
- }
-
- public void ThreadBlockingTrackEvent(string eventName, IDictionary? properties)
- {
- if (!Enabled)
- {
- return;
- }
-
- TrackEventTask(eventName, properties);
- }
-
- private static void TrackEventTask(string eventName, IDictionary? properties)
- {
- try
- {
- properties ??= new Dictionary();
- properties.Add("event id", Guid.NewGuid().ToString());
- var @event = new ActivityEvent($"dotnet/cli/{eventName}", tags: MakeTags(properties));
- Activity.Current?.AddEvent(@event);
- }
- catch (Exception e)
- {
- Debug.Fail(e.ToString());
- }
- }
-
- private static ActivityTagsCollection MakeTags(IDictionary eventProperties)
- {
- var common = s_commonProperties
- .Select(p => new KeyValuePair(p.Key, p.Value));
- var properties = eventProperties
- .Where(p => p.Value is not null)
- .Select(p => new KeyValuePair(p.Key, p.Value))
- .OrderBy(p => p.Key);
- return [.. common, .. properties];
- }
-}
diff --git a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs
index 12d8395b7688..8c71bff14e7d 100644
--- a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs
+++ b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.Collections.Frozen;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer;
@@ -11,24 +13,23 @@
namespace Microsoft.DotNet.Cli.Telemetry;
internal class TelemetryCommonProperties(
- Func? getCurrentDirectory = null,
- Func? hasher = null,
- Func? getMACAddress = null,
- Func? getDeviceId = null,
- IDockerContainerDetector? dockerContainerDetector = null,
- IUserLevelCacheWriter? userLevelCacheWriter = null,
- ICIEnvironmentDetector? ciEnvironmentDetector = null,
- ILLMEnvironmentDetector? llmEnvironmentDetector = null)
+ Func getCurrentDirectory = null,
+ Func hasher = null,
+ Func getMACAddress = null,
+ Func getDeviceId = null,
+ IDockerContainerDetector dockerContainerDetector = null,
+ IUserLevelCacheWriter userLevelCacheWriter = null,
+ ICIEnvironmentDetector ciEnvironmentDetector = null,
+ ILLMEnvironmentDetector llmEnvironmentDetector = null)
{
private readonly IDockerContainerDetector _dockerContainerDetector = dockerContainerDetector ?? new DockerContainerDetectorForTelemetry();
private readonly ICIEnvironmentDetector _ciEnvironmentDetector = ciEnvironmentDetector ?? new CIEnvironmentDetectorForTelemetry();
private readonly ILLMEnvironmentDetector _llmEnvironmentDetector = llmEnvironmentDetector ?? new LLMEnvironmentDetectorForTelemetry();
private readonly Func _getCurrentDirectory = getCurrentDirectory ?? Directory.GetCurrentDirectory;
private readonly Func _hasher = hasher ?? Sha256Hasher.Hash;
- private readonly Func _getMACAddress = getMACAddress ?? MacAddressGetter.GetMacAddress;
+ private readonly Func _getMACAddress = getMACAddress ?? MacAddressGetter.GetMacAddress;
private readonly Func _getDeviceId = getDeviceId ?? DeviceIdGetter.GetDeviceId;
private readonly IUserLevelCacheWriter _userLevelCacheWriter = userLevelCacheWriter ?? new UserLevelCacheWriter();
-
private const string OSVersion = "OS Version";
private const string OSPlatform = "OS Platform";
private const string OSArchitecture = "OS Architecture";
@@ -47,39 +48,63 @@ internal class TelemetryCommonProperties(
private const string LibcRelease = "Libc Release";
private const string LibcVersion = "Libc Version";
private const string SessionId = "SessionId";
+
private const string CI = "Continuous Integration";
private const string LLM = "llm";
+
private const string TelemetryProfileEnvironmentVariable = "DOTNET_CLI_TELEMETRY_PROFILE";
+ private const string CannotFindMacAddress = "Unknown";
+
private const string MachineIdCacheKey = "MachineId";
private const string IsDockerContainerCacheKey = "IsDockerContainer";
- public FrozenDictionary GetTelemetryCommonProperties(string? currentSessionId) => new Dictionary
+ public FrozenDictionary GetTelemetryCommonProperties(string currentSessionId)
{
- { OSVersion, RuntimeEnvironment.OperatingSystemVersion },
- { OSPlatform, RuntimeEnvironment.OperatingSystemPlatform.ToString() },
- { OSArchitecture, RuntimeInformation.OSArchitecture.ToString() },
- { OutputRedirected, Console.IsOutputRedirected.ToString() },
- { RuntimeId, RuntimeInformation.RuntimeIdentifier },
- { ProductVersion, Product.Version },
- { TelemetryProfile, Environment.GetEnvironmentVariable(TelemetryProfileEnvironmentVariable) },
- { DockerContainer, _userLevelCacheWriter.RunWithCache(IsDockerContainerCacheKey, () => _dockerContainerDetector.IsDockerContainer().ToString("G") ) },
- { CI, _ciEnvironmentDetector.IsCIEnvironment().ToString() },
- { LLM, _llmEnvironmentDetector.GetLLMEnvironment() },
- { CurrentPathHash, _hasher(_getCurrentDirectory()) },
- { MachineIdOld, _userLevelCacheWriter.RunWithCache(MachineIdCacheKey, GetMachineId) },
- // We don't want to recalcuate a new id for every new SDK version. Reuse the same path across versions.
- // If we change the format of the cache later, we need to rename the cache from v1 to v2.
- { MachineId, _userLevelCacheWriter.RunWithCacheInFilePath(Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, $"{MachineIdCacheKey}.v1.dotnetUserLevelCache"), GetMachineId) },
- { DeviceId, _getDeviceId() },
- { KernelVersion, GetKernelVersion() },
- { InstallationType, ExternalTelemetryProperties.GetInstallationType() },
- { ProductType, ExternalTelemetryProperties.GetProductType() },
- { LibcRelease, ExternalTelemetryProperties.GetLibcRelease() },
- { LibcVersion, ExternalTelemetryProperties.GetLibcVersion() },
- { SessionId, currentSessionId }
- }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
+ return new Dictionary
+ {
+ {OSVersion, RuntimeEnvironment.OperatingSystemVersion},
+ {OSPlatform, RuntimeEnvironment.OperatingSystemPlatform.ToString()},
+ {OSArchitecture, RuntimeInformation.OSArchitecture.ToString()},
+ {OutputRedirected, Console.IsOutputRedirected.ToString()},
+ {RuntimeId, RuntimeInformation.RuntimeIdentifier},
+ {ProductVersion, Product.Version},
+ {TelemetryProfile, Environment.GetEnvironmentVariable(TelemetryProfileEnvironmentVariable)},
+ {DockerContainer, _userLevelCacheWriter.RunWithCache(IsDockerContainerCacheKey, () => _dockerContainerDetector.IsDockerContainer().ToString("G") )},
+ {CI, _ciEnvironmentDetector.IsCIEnvironment().ToString() },
+ {LLM, _llmEnvironmentDetector.GetLLMEnvironment() },
+ {CurrentPathHash, _hasher(_getCurrentDirectory())},
+ {MachineIdOld, _userLevelCacheWriter.RunWithCache(MachineIdCacheKey, GetMachineId)},
+ // we don't want to recalcuate a new id for every new SDK version. Reuse the same path across versions.
+ // If we change the format of the cache later.
+ // We need to rename the cache from v1 to v2
+ {MachineId,
+ _userLevelCacheWriter.RunWithCacheInFilePath(
+ Path.Combine(
+ CliFolderPathCalculator.DotnetUserProfileFolderPath,
+ $"{MachineIdCacheKey}.v1.dotnetUserLevelCache"),
+ GetMachineId)},
+ {DeviceId, _getDeviceId()},
+ {KernelVersion, GetKernelVersion()},
+ {InstallationType, ExternalTelemetryProperties.GetInstallationType()},
+ {ProductType, ExternalTelemetryProperties.GetProductType()},
+ {LibcRelease, ExternalTelemetryProperties.GetLibcRelease()},
+ {LibcVersion, ExternalTelemetryProperties.GetLibcVersion()},
+ {SessionId, currentSessionId}
+ }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
+ }
- private string GetMachineId() => _getMACAddress() is { } macAddress ? _hasher(macAddress) : Guid.NewGuid().ToString();
+ private string GetMachineId()
+ {
+ var macAddress = _getMACAddress();
+ if (macAddress != null)
+ {
+ return _hasher(macAddress);
+ }
+ else
+ {
+ return Guid.NewGuid().ToString();
+ }
+ }
///
/// Returns a string identifying the OS kernel.
@@ -115,5 +140,8 @@ internal class TelemetryCommonProperties(
/// Windows.7 Microsoft Windows 6.1.7601 S
/// Windows.81 Microsoft Windows 6.3.9600
///
- private static string GetKernelVersion() => RuntimeInformation.OSDescription;
+ private static string GetKernelVersion()
+ {
+ return RuntimeInformation.OSDescription;
+ }
}
diff --git a/src/Cli/dotnet/Telemetry/TelemetryDiskLogger.cs b/src/Cli/dotnet/Telemetry/TelemetryDiskLogger.cs
deleted file mode 100644
index aa287369406c..000000000000
--- a/src/Cli/dotnet/Telemetry/TelemetryDiskLogger.cs
+++ /dev/null
@@ -1,94 +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.Diagnostics;
-using System.Text.Json;
-using System.Text.Json.Nodes;
-using System.Text.Json.Serialization;
-
-namespace Microsoft.DotNet.Cli.Telemetry;
-
-internal static class TelemetryDiskLogger
-{
- private static readonly JsonSerializerOptions s_jsonOptions;
-
- private static readonly TelemetryDiskLoggerJsonSerializerContext s_jsonContext;
-
- public record EventModel(
- string name,
- DateTimeOffset timestamp,
- Dictionary tags);
-
- public record SourceModel(
- string name,
- string? version,
- Dictionary? tags);
-
- public record IdentifiersModel(
- string? id,
- string traceId,
- string spanId,
- string parentSpanId,
- string? parentId,
- string? rootId);
-
- public record ActivityModel(
- string operationName,
- string displayName,
- TimeSpan duration,
- IdentifiersModel identifiers,
- SourceModel source,
- Dictionary tags,
- EventModel[] events);
-
- static TelemetryDiskLogger()
- {
- s_jsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = false };
- s_jsonContext = new(s_jsonOptions);
- }
-
- public static void WriteLog(string logPath, IEnumerable activies)
- {
- try
- {
- var jsonText = !File.Exists(logPath) ? """{"activities":[]}""" : File.ReadAllText(logPath);
- var root = JsonNode.Parse(jsonText)!;
- var activitiesArray = root["activities"]!.AsArray();
- activitiesArray.AddRange(activies.Select(r => JsonNode.Parse(JsonSerializer.Serialize(CreateActivityJsonModel(r), s_jsonContext.ActivityModel))));
- root["activities"] = activitiesArray;
- File.WriteAllText(logPath, root.ToJsonString(s_jsonOptions));
- }
- catch
- {
- // Swallow any exceptions to avoid interfering with telemetry shutdown.
- }
- }
-
- private static ActivityModel CreateActivityJsonModel(Activity activity) => new(
- operationName: activity.OperationName,
- displayName: activity.DisplayName,
- duration: activity.Duration,
- identifiers: new(
- id: activity.Id,
- traceId: activity.TraceId.ToString(),
- spanId: activity.SpanId.ToString(),
- parentSpanId: activity.ParentSpanId.ToString(),
- parentId: activity.ParentId,
- rootId: activity.RootId
- ),
- source: new(
- name: activity.Source.Name,
- version: activity.Source.Version,
- tags: activity.Source.Tags?.ToDictionary()
- ),
- tags: activity.Tags.ToDictionary(),
- events: [.. activity.Events.Select(e => new EventModel(
- name: e.Name,
- timestamp: e.Timestamp,
- tags: e.Tags.ToDictionary()
- ))]
- );
-}
-
-[JsonSerializable(typeof(TelemetryDiskLogger.ActivityModel))]
-internal partial class TelemetryDiskLoggerJsonSerializerContext : JsonSerializerContext;
diff --git a/src/Cli/dotnet/Telemetry/TelemetryFilter.cs b/src/Cli/dotnet/Telemetry/TelemetryFilter.cs
index 00a758add1de..36d088115a01 100644
--- a/src/Cli/dotnet/Telemetry/TelemetryFilter.cs
+++ b/src/Cli/dotnet/Telemetry/TelemetryFilter.cs
@@ -1,97 +1,109 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.CommandLine;
using System.Globalization;
using Microsoft.DotNet.Cli.CommandLine;
+using Microsoft.DotNet.Cli.Commands.Build;
+using Microsoft.DotNet.Cli.Commands.Clean;
+using Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess;
+using Microsoft.DotNet.Cli.Commands.Pack;
+using Microsoft.DotNet.Cli.Commands.Publish;
+using Microsoft.DotNet.Cli.Commands.Run;
+using Microsoft.DotNet.Cli.Commands.Test;
using Microsoft.DotNet.Cli.Commands.VSTest;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
namespace Microsoft.DotNet.Cli.Telemetry;
-internal class TelemetryFilter(Func? hash) : ITelemetryFilter
+internal class TelemetryFilter(Func hash) : ITelemetryFilter
{
private const string ExceptionEventName = "mainCatchException/exception";
private readonly Func _hash = hash ?? throw new ArgumentNullException(nameof(hash));
- public IEnumerable Filter(ParseResult parseResult) =>
- Hash(FilterImpl(parseResult, globalJsonState: null));
-
- public IEnumerable Filter(ParseResultWithGlobalJsonState parseData) =>
- Hash(FilterImpl(parseData.ParseResult, parseData.GlobalJsonState));
-
- public IEnumerable Filter(InstallerSuccessReport report)
- {
- var reportProperties = new Dictionary
- {
- { "exeName", report.ExeName }
- };
- return Hash([new TelemetryEntryFormat("install/reportsuccess", reportProperties)]);
- }
-
- public IEnumerable Filter(Exception exception)
+ public IEnumerable Filter(object objectToFilter)
{
- var exceptionProperties = new Dictionary
+ var result = new List();
+ Dictionary measurements = null;
+ string globalJsonState = string.Empty;
+ if (objectToFilter is Tuple> parseResultWithMeasurements)
{
- { "exceptionType", exception.GetType().ToString() },
- { "detail", ExceptionToStringWithoutMessage(exception) }
- };
- return Hash([new TelemetryEntryFormat(ExceptionEventName, exceptionProperties)]);
- }
-
- private static IEnumerable FilterImpl(ParseResult parseResult, string? globalJsonState)
- {
- var topLevelCommandName = parseResult.RootSubCommandResult();
- if (topLevelCommandName is null)
- {
- yield break;
+ objectToFilter = parseResultWithMeasurements.Item1;
+ measurements = parseResultWithMeasurements.Item2;
+ measurements = RemoveZeroTimes(measurements);
}
-
- Dictionary properties = new() { ["verb"] = topLevelCommandName };
- if (!string.IsNullOrEmpty(globalJsonState))
+ else if (objectToFilter is Tuple, string> parseResultWithMeasurementsAndGlobalJsonState)
{
- properties["globalJson"] = globalJsonState;
+ objectToFilter = parseResultWithMeasurementsAndGlobalJsonState.Item1;
+ measurements = parseResultWithMeasurementsAndGlobalJsonState.Item2;
+ measurements = RemoveZeroTimes(measurements);
+ globalJsonState = parseResultWithMeasurementsAndGlobalJsonState.Item3;
}
- yield return new TelemetryEntryFormat("toplevelparser/command", properties);
-
- if (parseResult.IsDotnetBuiltInCommand() &&
- parseResult.SafelyGetValueForOption("--verbosity") is VerbosityOptions verbosity)
+ if (objectToFilter is ParseResult parseResult)
{
- var verbosityProperties = new Dictionary()
+ var topLevelCommandName = parseResult.RootSubCommandResult();
+ if (topLevelCommandName != null)
{
- { "verb", topLevelCommandName},
- { "verbosity", Enum.GetName(verbosity)}
- };
- yield return new TelemetryEntryFormat("sublevelparser/command", verbosityProperties);
+ Dictionary properties = new()
+ {
+ ["verb"] = topLevelCommandName
+ };
+ if (!string.IsNullOrEmpty(globalJsonState))
+ {
+ properties["globalJson"] = globalJsonState;
+ }
+
+ result.Add(new ApplicationInsightsEntryFormat(
+ "toplevelparser/command",
+ properties,
+ measurements
+ ));
+
+ LogVerbosityForAllTopLevelCommand(result, parseResult, topLevelCommandName, measurements);
+ LogVulnerableOptionForPackageUpdateCommand(result, parseResult, topLevelCommandName, measurements);
+
+ foreach (IParseResultLogRule rule in ParseResultLogRules)
+ {
+ result.AddRange(rule.AllowList(parseResult, measurements));
+ }
+ }
}
-
- if (topLevelCommandName == "package" &&
- parseResult.CommandResult.Command != null &&
- parseResult.CommandResult.Command.Name == "update")
+ else if (objectToFilter is InstallerSuccessReport installerSuccessReport)
{
- var hasVulnerableOption = parseResult.HasOption("--vulnerable");
- var vulnerableProperties = new Dictionary()
- {
- { "verb", "package update" },
- { "vulnerable", hasVulnerableOption.ToString()}
- };
- yield return new TelemetryEntryFormat("sublevelparser/command", vulnerableProperties);
+ result.Add(new ApplicationInsightsEntryFormat(
+ "install/reportsuccess",
+ new Dictionary { { "exeName", installerSuccessReport.ExeName } }
+ ));
+ }
+ else if (objectToFilter is Exception exception)
+ {
+ result.Add(new ApplicationInsightsEntryFormat(
+ ExceptionEventName,
+ new Dictionary
+ {
+ {"exceptionType", exception.GetType().ToString()},
+ {"detail", ExceptionToStringWithoutMessage(exception) }
+ }
+ ));
}
- foreach (IParseResultLogRule rule in ParseResultLogRules)
+ return [.. result.Select(r =>
{
- foreach (TelemetryEntryFormat allowList in rule.AllowList(parseResult))
+ if (r.EventName == ExceptionEventName)
{
- yield return allowList;
+ return r;
}
- }
+ else
+ {
+ return r.WithAppliedToPropertiesValue(_hash);
+ }
+ })];
}
- public IEnumerable Hash(IEnumerable entries) =>
- entries.Select(entry => entry.EventName == ExceptionEventName ? entry : entry.WithAppliedToPropertiesValue(_hash));
-
private static List ParseResultLogRules =>
[
new AllowListToSendFirstArgument(["new", "help"]),
@@ -124,6 +136,47 @@ public IEnumerable Hash(IEnumerable
new AllowListToSendVerbSecondVerbFirstArgument(["workload", "tool", "new"]),
];
+ private static void LogVulnerableOptionForPackageUpdateCommand(
+ ICollection result,
+ ParseResult parseResult,
+ string topLevelCommandName,
+ Dictionary measurements = null)
+ {
+ if (topLevelCommandName == "package" && parseResult.CommandResult.Command != null && parseResult.CommandResult.Command.Name == "update")
+ {
+ var hasVulnerableOption = parseResult.HasOption("--vulnerable");
+
+ result.Add(new ApplicationInsightsEntryFormat(
+ "sublevelparser/command",
+ new Dictionary()
+ {
+ { "verb", "package update" },
+ { "vulnerable", hasVulnerableOption.ToString()}
+ },
+ measurements));
+ }
+ }
+
+ private static void LogVerbosityForAllTopLevelCommand(
+ ICollection result,
+ ParseResult parseResult,
+ string topLevelCommandName,
+ Dictionary measurements = null)
+ {
+ if (parseResult.IsDotnetBuiltInCommand() &&
+ parseResult.SafelyGetValueForOption("--verbosity") is VerbosityOptions verbosity)
+ {
+ result.Add(new ApplicationInsightsEntryFormat(
+ "sublevelparser/command",
+ new Dictionary()
+ {
+ { "verb", topLevelCommandName},
+ { "verbosity", Enum.GetName(verbosity)}
+ },
+ measurements));
+ }
+ }
+
private static string ExceptionToStringWithoutMessage(Exception e)
{
const string AggregateException_ToString = "{0}{1}---> (Inner Exception #{2}) {3}{4}{5}";
@@ -156,18 +209,42 @@ private static string NonAggregateExceptionToStringWithoutMessage(Exception e)
string s;
const string Exception_EndOfInnerExceptionStack = "--- End of inner exception stack trace ---";
+
s = e.GetType().ToString();
+
if (e.InnerException != null)
{
s = s + " ---> " + ExceptionToStringWithoutMessage(e.InnerException) + Environment.NewLine +
" " + Exception_EndOfInnerExceptionStack;
+
}
var stackTrace = e.StackTrace;
+
if (stackTrace != null)
{
s += Environment.NewLine + stackTrace;
}
+
return s;
}
+
+ private static Dictionary RemoveZeroTimes(Dictionary measurements)
+ {
+ if (measurements != null)
+ {
+ foreach (var measurement in measurements)
+ {
+ if (measurement.Value == 0)
+ {
+ measurements.Remove(measurement.Key);
+ }
+ }
+ if (measurements.Count == 0)
+ {
+ measurements = null;
+ }
+ }
+ return measurements;
+ }
}
diff --git a/src/Cli/dotnet/Telemetry/TopLevelCommandNameAndOptionToLog.cs b/src/Cli/dotnet/Telemetry/TopLevelCommandNameAndOptionToLog.cs
index a19e98c315ba..62d9d6e93630 100644
--- a/src/Cli/dotnet/Telemetry/TopLevelCommandNameAndOptionToLog.cs
+++ b/src/Cli/dotnet/Telemetry/TopLevelCommandNameAndOptionToLog.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+#nullable disable
+
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.DotNet.Cli.Extensions;
@@ -9,15 +11,17 @@
namespace Microsoft.DotNet.Cli.Telemetry;
-internal class TopLevelCommandNameAndOptionToLog(HashSet topLevelCommandName, HashSet optionsToLog) : IParseResultLogRule
+internal class TopLevelCommandNameAndOptionToLog(
+ HashSet topLevelCommandName,
+ HashSet optionsToLog) : IParseResultLogRule
{
private HashSet _topLevelCommandName { get; } = topLevelCommandName;
private HashSet _optionsToLog { get; } = optionsToLog;
- public List AllowList(ParseResult parseResult)
+ public List AllowList(ParseResult parseResult, Dictionary measurements = null)
{
var topLevelCommandName = parseResult.RootSubCommandResult();
- var result = new List();
+ var result = new List();
foreach (var optionName in _optionsToLog)
{
if (_topLevelCommandName.Contains(topLevelCommandName)
@@ -26,13 +30,14 @@ public List AllowList(ParseResult parseResult)
&& optionResult.GetValueOrDefault
public string? Reason { get; set; }
- public RequiresMSBuildVersionTheoryAttribute(string version, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0)
- : base(sourceFilePath, sourceLineNumber)
+ public RequiresMSBuildVersionTheoryAttribute(string version)
{
CheckForRequiredMSBuildVersion(this, version);
}
diff --git a/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkFactAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkFactAttribute.cs
index d3a76dcd5e97..8bbd151553f6 100644
--- a/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkFactAttribute.cs
+++ b/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkFactAttribute.cs
@@ -3,14 +3,11 @@
#if NETCOREAPP
-using System.Runtime.CompilerServices;
-
namespace Microsoft.NET.TestFramework
{
public class RequiresSpecificFrameworkFactAttribute : FactAttribute
{
- public RequiresSpecificFrameworkFactAttribute(string framework, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0)
- : base(sourceFilePath, sourceLineNumber)
+ public RequiresSpecificFrameworkFactAttribute(string framework)
{
if (!EnvironmentInfo.SupportsTargetFramework(framework))
{
diff --git a/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkTheoryAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkTheoryAttribute.cs
index 95d563362313..65550cdb8879 100644
--- a/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkTheoryAttribute.cs
+++ b/test/Microsoft.NET.TestFramework/Attributes/RequiresSpecificFrameworkTheoryAttribute.cs
@@ -1,17 +1,15 @@
-// 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.
#if NETCOREAPP
-using System.Runtime.CompilerServices;
using Microsoft.DotNet.Tools.Test.Utilities;
namespace Microsoft.NET.TestFramework
{
public class RequiresSpecificFrameworkTheoryAttribute : TheoryAttribute
{
- public RequiresSpecificFrameworkTheoryAttribute(string framework, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0)
- : base(sourceFilePath, sourceLineNumber)
+ public RequiresSpecificFrameworkTheoryAttribute(string framework)
{
if (!EnvironmentInfo.SupportsTargetFramework(framework))
{
diff --git a/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionFactAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionFactAttribute.cs
index 55df869206e1..73121c780841 100644
--- a/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionFactAttribute.cs
+++ b/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionFactAttribute.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Runtime.CompilerServices;
-
namespace Microsoft.NET.TestFramework
{
public class WindowsOnlyRequiresMSBuildVersionFactAttribute : FactAttribute
@@ -11,9 +9,8 @@ public class WindowsOnlyRequiresMSBuildVersionFactAttribute : FactAttribute
/// Gets or sets the reason for potentially skipping the test if conditions are not met.
///
public string? Reason { get; set; }
-
- public WindowsOnlyRequiresMSBuildVersionFactAttribute(string version, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0)
- : base(sourceFilePath, sourceLineNumber)
+
+ public WindowsOnlyRequiresMSBuildVersionFactAttribute(string version)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
diff --git a/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionTheoryAttribute.cs b/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionTheoryAttribute.cs
index 2b8664843cfe..f9c43590585e 100644
--- a/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionTheoryAttribute.cs
+++ b/test/Microsoft.NET.TestFramework/Attributes/WindowsOnlyRequiresMSBuildVersionTheoryAttribute.cs
@@ -1,14 +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.Runtime.CompilerServices;
-
namespace Microsoft.NET.TestFramework
{
public class WindowsOnlyRequiresMSBuildVersionTheoryAttribute : TheoryAttribute
{
- public WindowsOnlyRequiresMSBuildVersionTheoryAttribute(string version, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0)
- : base(sourceFilePath, sourceLineNumber)
+ public WindowsOnlyRequiresMSBuildVersionTheoryAttribute(string version)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
diff --git a/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs b/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs
index 6de64a06b733..9bc513f6ff6c 100644
--- a/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs
+++ b/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs
@@ -3,23 +3,23 @@
using System.Diagnostics;
using Microsoft.DotNet.Cli.Utils;
-#if NET
-using System.Runtime.InteropServices;
-#endif
namespace Microsoft.NET.TestFramework.Commands
{
public class SdkCommandSpec
{
public string? FileName { get; set; }
- public List Arguments { get; set; } = [];
- public Dictionary Environment { get; set; } = [];
- public List EnvironmentToRemove { get; } = [];
+ public List Arguments { get; set; } = new List();
+
+ public Dictionary Environment { get; set; } = new();
+
+ public List EnvironmentToRemove { get; } = new List();
+
public string? WorkingDirectory { get; set; }
+
public bool RedirectStandardInput { get; set; }
- public bool DisableOutputAndErrorRedirection { get; set; }
- public bool CreateNewProcessGroup { get; set; }
+ public bool DisableOutputAndErrorRedirection { get; set; }
private string EscapeArgs()
{
@@ -61,13 +61,6 @@ public ProcessStartInfo ToProcessStartInfo(bool doNotEscapeArguments = false)
ret.WorkingDirectory = WorkingDirectory;
}
-#if NET
- if (CreateNewProcessGroup && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- ret.CreateNewProcessGroup = true;
- }
-#endif
-
return ret;
}
}
diff --git a/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs b/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs
index 499a4f993b35..56cf1002d2ae 100644
--- a/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs
+++ b/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs
@@ -9,20 +9,20 @@ namespace Microsoft.NET.TestFramework.Commands
{
public abstract class TestCommand
{
- private readonly Dictionary _environment = [];
+ private Dictionary _environment = new();
private bool _doNotEscapeArguments;
+
public ITestOutputHelper Log { get; }
+
public string? WorkingDirectory { get; set; }
- public List Arguments { get; set; } = [];
- public List EnvironmentToRemove { get; } = [];
+
+ public List Arguments { get; set; } = new List();
+
+ public List EnvironmentToRemove { get; } = new List();
+
public bool RedirectStandardInput { get; set; }
- public bool DisableOutputAndErrorRedirection { get; set; }
- ///
- /// When true, the child process is launched in a new process group so that
- /// console signals (e.g. Ctrl+C) sent to it do not propagate to the test host.
- ///
- public bool CreateNewProcessGroup { get; set; }
+ public bool DisableOutputAndErrorRedirection { get; set; }
// These only work via Execute(), not when using GetProcessStartInfo()
public Action? CommandOutputHandler { get; set; }
@@ -116,7 +116,6 @@ private SdkCommandSpec CreateCommandSpec(IEnumerable args)
commandSpec.RedirectStandardInput = RedirectStandardInput;
commandSpec.DisableOutputAndErrorRedirection = DisableOutputAndErrorRedirection;
- commandSpec.CreateNewProcessGroup = CreateNewProcessGroup;
return commandSpec;
}
@@ -204,7 +203,7 @@ public virtual CommandResult Execute(IEnumerable args)
public static void LogCommandResult(ITestOutputHelper log, CommandResult result)
{
log.WriteLine($"> {result.StartInfo.FileName} {result.StartInfo.Arguments}");
- log.WriteLine(result.StdOut ?? string.Empty);
+ log.WriteLine(result.StdOut);
if (!string.IsNullOrEmpty(result.StdErr))
{
diff --git a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj
index 6aea55861808..6751b7251938 100644
--- a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj
+++ b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj
@@ -60,13 +60,12 @@
-->
-
+
-
-
+
@@ -76,6 +75,7 @@
+
diff --git a/test/Microsoft.NET.TestFramework/SdkTest.cs b/test/Microsoft.NET.TestFramework/SdkTest.cs
index b377b9b2be92..56c6649e4ae8 100644
--- a/test/Microsoft.NET.TestFramework/SdkTest.cs
+++ b/test/Microsoft.NET.TestFramework/SdkTest.cs
@@ -3,42 +3,43 @@
using System.Runtime.CompilerServices;
-namespace Microsoft.NET.TestFramework;
-
-public abstract class SdkTest
+namespace Microsoft.NET.TestFramework
{
- protected bool? UsingFullFrameworkMSBuild => SdkTestContext.Current.ToolsetUnderTest?.ShouldUseFullFrameworkMSBuild;
+ public abstract class SdkTest
+ {
+ protected bool? UsingFullFrameworkMSBuild => SdkTestContext.Current.ToolsetUnderTest?.ShouldUseFullFrameworkMSBuild;
- protected ITestOutputHelper Log { get; }
+ protected ITestOutputHelper Log { get; }
- protected TestAssetsManager TestAssetsManager { get; }
+ protected TestAssetsManager TestAssetsManager { get; }
- protected SdkTest(ITestOutputHelper log)
- {
- Log = log;
- TestAssetsManager = new TestAssetsManager(log);
- }
-
- protected static void WaitForUtcNowToAdvance()
- {
- var start = DateTime.UtcNow;
+ protected SdkTest(ITestOutputHelper log)
+ {
+ Log = log;
+ TestAssetsManager = new TestAssetsManager(log);
+ }
- while (DateTime.UtcNow <= start)
+ protected static void WaitForUtcNowToAdvance()
{
- Thread.Sleep(millisecondsTimeout: 1);
+ var start = DateTime.UtcNow;
+
+ while (DateTime.UtcNow <= start)
+ {
+ Thread.Sleep(millisecondsTimeout: 1);
+ }
}
- }
- ///
- /// Generates a MSBuild binlog argument with a unique name based on the caller and provided parts, and places it in a location that will be collected by Helix if running in that environment.
- ///
- protected string BinLogArgument(ReadOnlySpan parts, [CallerMemberName] string callerName = "")
- {
- // combine the name and parts into a unique binlog
- var fileName = $"{callerName}{(parts.Length > 0 ? "-" + string.Join("-", parts.ToArray()) : "")}-{{}}.binlog";
- var binlogDestPath = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT") is { } ciOutputRoot && Environment.GetEnvironmentVariable("HELIX_WORKITEM_ID") is { } helixGuid ?
- Path.Combine(ciOutputRoot, "binlog", helixGuid, fileName) :
- $"./{fileName}";
- return $"/bl:{binlogDestPath}";
+ ///
+ /// Generates a MSBuild binlog argument with a unique name based on the caller and provided parts, and places it in a location that will be collected by Helix if running in that environment.
+ ///
+ protected string BinLogArgument(ReadOnlySpan parts, [CallerMemberName] string callerName = "")
+ {
+ // combine the name and parts into a unique binlog
+ var fileName = $"{callerName}{(parts.Length > 0 ? "-" + string.Join("-", parts.ToArray()) : "")}-{{}}.binlog";
+ var binlogDestPath = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT") is { } ciOutputRoot && Environment.GetEnvironmentVariable("HELIX_WORKITEM_ID") is { } helixGuid ?
+ Path.Combine(ciOutputRoot, "binlog", helixGuid, fileName) :
+ $"./{fileName}";
+ return $"/bl:{binlogDestPath}";
+ }
}
}
diff --git a/test/Microsoft.NET.TestFramework/SharedTestOutputHelper.cs b/test/Microsoft.NET.TestFramework/SharedTestOutputHelper.cs
index c7f1a510fbbd..d3591610f624 100644
--- a/test/Microsoft.NET.TestFramework/SharedTestOutputHelper.cs
+++ b/test/Microsoft.NET.TestFramework/SharedTestOutputHelper.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Xunit.Sdk;
-using Xunit.v3;
namespace Microsoft.NET.TestFramework;
@@ -13,38 +12,19 @@ namespace Microsoft.NET.TestFramework;
public class SharedTestOutputHelper : ITestOutputHelper
{
private readonly IMessageSink _sink;
- private readonly StringBuilder _output = new();
public SharedTestOutputHelper(IMessageSink sink)
{
_sink = sink;
}
- public string Output => _output.ToString();
-
- public void Write(string message)
- {
- _output.Append(message);
- _sink.OnMessage(new DiagnosticMessage(message));
- }
-
- public void Write(string format, params object[] args)
- {
- var formatted = string.Format(format, args);
- _output.Append(formatted);
- _sink.OnMessage(new DiagnosticMessage(formatted));
- }
-
public void WriteLine(string message)
{
- _output.AppendLine(message);
_sink.OnMessage(new DiagnosticMessage(message));
}
public void WriteLine(string format, params object[] args)
{
- var formatted = string.Format(format, args);
- _output.AppendLine(formatted);
- _sink.OnMessage(new DiagnosticMessage(formatted));
+ _sink.OnMessage(new DiagnosticMessage(format, args));
}
}
diff --git a/test/Microsoft.NET.TestFramework/StringTestLogger.cs b/test/Microsoft.NET.TestFramework/StringTestLogger.cs
index 1d08f176332b..a5323123fba4 100644
--- a/test/Microsoft.NET.TestFramework/StringTestLogger.cs
+++ b/test/Microsoft.NET.TestFramework/StringTestLogger.cs
@@ -7,18 +7,6 @@ public class StringTestLogger : ITestOutputHelper
{
StringBuilder _stringBuilder = new();
- public string Output => _stringBuilder.ToString();
-
- public void Write(string message)
- {
- _stringBuilder.Append(message);
- }
-
- public void Write(string format, params object[] args)
- {
- _stringBuilder.Append(string.Format(format, args));
- }
-
public void WriteLine(string message)
{
_stringBuilder.AppendLine(message);
diff --git a/test/Microsoft.NET.TestFramework/TestLoggerFactory.cs b/test/Microsoft.NET.TestFramework/TestLoggerFactory.cs
index 02982eb098a9..40ec82ed3041 100644
--- a/test/Microsoft.NET.TestFramework/TestLoggerFactory.cs
+++ b/test/Microsoft.NET.TestFramework/TestLoggerFactory.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Logging;
-using Xunit.Sdk;
namespace Microsoft.NET.TestFramework
{
diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj b/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj
index b1e1394cf095..bf6139c9af64 100644
--- a/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj
+++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/Microsoft.TemplateEngine.Cli.UnitTests.csproj
@@ -16,10 +16,10 @@
-
+
-
-
+
+
diff --git a/test/Microsoft.Win32.Msi.Manual.Tests/Microsoft.Win32.Msi.Manual.Tests.csproj b/test/Microsoft.Win32.Msi.Manual.Tests/Microsoft.Win32.Msi.Manual.Tests.csproj
index c13835ee50fe..a1465b5f3026 100644
--- a/test/Microsoft.Win32.Msi.Manual.Tests/Microsoft.Win32.Msi.Manual.Tests.csproj
+++ b/test/Microsoft.Win32.Msi.Manual.Tests/Microsoft.Win32.Msi.Manual.Tests.csproj
@@ -22,5 +22,6 @@
+
diff --git a/test/Msbuild.Tests.Utilities/Msbuild.Tests.Utilities.csproj b/test/Msbuild.Tests.Utilities/Msbuild.Tests.Utilities.csproj
index 889e80b3c357..9df4924b8ce4 100644
--- a/test/Msbuild.Tests.Utilities/Msbuild.Tests.Utilities.csproj
+++ b/test/Msbuild.Tests.Utilities/Msbuild.Tests.Utilities.csproj
@@ -16,6 +16,7 @@
+
diff --git a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj
index 349ac13587c3..b5959c334766 100644
--- a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj
+++ b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj
@@ -10,7 +10,8 @@
-
+
+
diff --git a/test/UnitTests.proj b/test/UnitTests.proj
index 0920f1672a93..2eef751199e5 100644
--- a/test/UnitTests.proj
+++ b/test/UnitTests.proj
@@ -114,7 +114,6 @@
-
@@ -142,7 +141,7 @@
. $HELIX_CORRELATION_PAYLOAD/t/SetupHelixEnvironment.sh;$(HelixPreCommands)
PowerShell -ExecutionPolicy ByPass "dotnet nuget locals all -l | ForEach-Object { $_.Split(' ')[1]} | Where-Object{$_ -like '*cache'} | Get-ChildItem -Recurse -File -Filter '*.dat' | Measure";$(HelixPostCommands)
PowerShell -ExecutionPolicy ByPass "Get-ChildItem -Recurse -File -Filter '*hangdump.dmp' | Copy-Item -Destination $env:HELIX_WORKITEM_UPLOAD_ROOT";$(HelixPostCommands)
- find "$HELIX_WORKITEM_UPLOAD_ROOT/../../.." -name '*hangdump.dmp' -print0 | xargs -0 -I@ cp @ "$HELIX_WORKITEM_UPLOAD_ROOT";$(HelixPostCommands)
+ find "$HELIX_WORKITEM_UPLOAD_ROOT/../../.." -name '*hangdump.dmp' -exec cp {} "$HELIX_WORKITEM_UPLOAD_ROOT" \;;$(HelixPostCommands)
$(Version)
$(RepoRoot)artifacts\bin\Microsoft.DotNet.MSBuildSdkResolver
$(RepoRoot)artifacts\tmp\HelixStage0.tar.gz
diff --git a/test/dotnet-format.UnitTests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs b/test/dotnet-format.UnitTests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs
index 939463a91d90..07fae27e9946 100644
--- a/test/dotnet-format.UnitTests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs
+++ b/test/dotnet-format.UnitTests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs
@@ -26,7 +26,7 @@ public ThirdPartyAnalyzerFormatterTests(ITestOutputHelper output)
TestOutputHelper = output;
}
- public async ValueTask InitializeAsync()
+ public async Task InitializeAsync()
{
var logger = new TestLogger();
@@ -52,11 +52,11 @@ public async ValueTask InitializeAsync()
}
}
- public ValueTask DisposeAsync()
+ public Task DisposeAsync()
{
_analyzerReferencesProject = null;
- return ValueTask.CompletedTask;
+ return Task.CompletedTask;
}
private IEnumerable GetAnalyzerReferences(string prefix)
diff --git a/test/dotnet-format.UnitTests/XUnit/ConditionalFactAttribute.cs b/test/dotnet-format.UnitTests/XUnit/ConditionalFactAttribute.cs
index c7434c6c9d1e..e887376ef1cf 100644
--- a/test/dotnet-format.UnitTests/XUnit/ConditionalFactAttribute.cs
+++ b/test/dotnet-format.UnitTests/XUnit/ConditionalFactAttribute.cs
@@ -1,8 +1,6 @@
-// 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.Runtime.CompilerServices;
-
namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit
{
public class ConditionalFactAttribute : FactAttribute
@@ -13,7 +11,7 @@ public class ConditionalFactAttribute : FactAttribute
/// skipped vs. conditionally skipped which is the entire point of this attribute.
///
[Obsolete("ConditionalFact should use Reason or AlwaysSkip", error: true)]
- public new string? Skip
+ public new string Skip
{
get => base.Skip;
set => base.Skip = value;
@@ -23,7 +21,7 @@ public class ConditionalFactAttribute : FactAttribute
/// Used to unconditionally Skip a test. For the rare occasion when a conditional test needs to be
/// unconditionally skipped (typically short term for a bug to be fixed).
///
- public string? AlwaysSkip
+ public string AlwaysSkip
{
get => base.Skip;
set => base.Skip = value;
@@ -31,8 +29,7 @@ public string? AlwaysSkip
public string? Reason { get; set; }
- public ConditionalFactAttribute(Type[] skipConditions, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0)
- : base(sourceFilePath, sourceLineNumber)
+ public ConditionalFactAttribute(params Type[] skipConditions)
{
foreach (var skipCondition in skipConditions)
{
@@ -54,7 +51,7 @@ public class ConditionalTheoryAttribute : TheoryAttribute
/// skipped vs. conditionally skipped which is the entire point of this attribute.
///
[Obsolete("ConditionalTheory should use Reason or AlwaysSkip")]
- public new string? Skip
+ public new string Skip
{
get => base.Skip;
set => base.Skip = value;
@@ -64,7 +61,7 @@ public class ConditionalTheoryAttribute : TheoryAttribute
/// Used to unconditionally Skip a test. For the rare occasion when a conditional test needs to be
/// unconditionally skipped (typically short term for a bug to be fixed).
///
- public string? AlwaysSkip
+ public string AlwaysSkip
{
get => base.Skip;
set => base.Skip = value;
@@ -72,8 +69,7 @@ public string? AlwaysSkip
public string? Reason { get; set; }
- public ConditionalTheoryAttribute(Type[] skipConditions, [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0)
- : base(sourceFilePath, sourceLineNumber)
+ public ConditionalTheoryAttribute(params Type[] skipConditions)
{
foreach (var skipCondition in skipConditions)
{
diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildFactAttribute.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildFactAttribute.cs
index 97caeb8011b7..384f87887586 100644
--- a/test/dotnet-format.UnitTests/XUnit/MSBuildFactAttribute.cs
+++ b/test/dotnet-format.UnitTests/XUnit/MSBuildFactAttribute.cs
@@ -1,34 +1,17 @@
-// 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.Reflection;
-using System.Runtime.CompilerServices;
-using Microsoft.CodeAnalysis.Tools.Workspaces;
-using Xunit.v3;
+using Xunit.Sdk;
namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
- public sealed class MSBuildFactAttribute : ConditionalFactAttribute, IBeforeAfterTestAttribute
+ [XunitTestCaseDiscoverer("Microsoft.CodeAnalysis.Tools.Tests.XUnit.MSBuildFactDiscoverer", "dotnet-format.UnitTests")]
+ public sealed class MSBuildFactAttribute : ConditionalFactAttribute
{
public MSBuildFactAttribute(params Type[] skipConditions)
: base(skipConditions)
{
}
-
- public MSBuildFactAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0)
- : base(Array.Empty(), sourceFilePath, sourceLineNumber)
- {
- }
-
- public void Before(MethodInfo methodUnderTest, IXunitTest test)
- {
- MSBuildWorkspaceLoader.Guard.Wait();
- }
-
- public void After(MethodInfo methodUnderTest, IXunitTest test)
- {
- MSBuildWorkspaceLoader.Guard.Release();
- }
}
}
diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildFactDiscoverer.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildFactDiscoverer.cs
new file mode 100644
index 000000000000..6262d6d78a66
--- /dev/null
+++ b/test/dotnet-format.UnitTests/XUnit/MSBuildFactDiscoverer.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit.Sdk;
+
+namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit
+{
+
+ public sealed class MSBuildFactDiscoverer : IXunitTestCaseDiscoverer
+ {
+ private readonly FactDiscoverer _factDiscoverer;
+
+ public MSBuildFactDiscoverer(IMessageSink diagnosticMessageSink)
+ {
+ _factDiscoverer = new FactDiscoverer(diagnosticMessageSink);
+ }
+
+ public IEnumerable Discover(
+ ITestFrameworkDiscoveryOptions discoveryOptions,
+ ITestMethod testMethod,
+ IAttributeInfo factAttribute)
+ {
+ return _factDiscoverer
+ .Discover(discoveryOptions, testMethod, factAttribute)
+ .Select(testCase => new MSBuildTestCase(testCase));
+ }
+ }
+}
diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildTestCase.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildTestCase.cs
new file mode 100644
index 000000000000..0104480fdff9
--- /dev/null
+++ b/test/dotnet-format.UnitTests/XUnit/MSBuildTestCase.cs
@@ -0,0 +1,72 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Diagnostics;
+using Microsoft.CodeAnalysis.Tools.Workspaces;
+using Xunit.Sdk;
+
+namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit
+{
+ [DebuggerDisplay(@"\{ class = {TestMethod.TestClass.Class.Name}, method = {TestMethod.Method.Name}, display = {DisplayName}, skip = {SkipReason} \}")]
+ public sealed class MSBuildTestCase : Xunit.LongLivedMarshalByRefObject, IXunitTestCase
+ {
+ private IXunitTestCase _testCase;
+
+ public string DisplayName => _testCase.DisplayName;
+ public IMethodInfo Method => _testCase.Method;
+ public string SkipReason => _testCase.SkipReason;
+ public ITestMethod TestMethod => _testCase.TestMethod;
+ public object[] TestMethodArguments => _testCase.TestMethodArguments;
+ public Dictionary> Traits => _testCase.Traits;
+ public string UniqueID => _testCase.UniqueID;
+
+ public ISourceInformation SourceInformation
+ {
+ get => _testCase.SourceInformation;
+ set => _testCase.SourceInformation = value;
+ }
+
+ public Exception InitializationException => _testCase.InitializationException;
+
+ public int Timeout => _testCase.Timeout;
+
+ public MSBuildTestCase(IXunitTestCase testCase)
+ {
+ _testCase = testCase ?? throw new ArgumentNullException(nameof(testCase));
+ }
+
+ [Obsolete("Called by the deserializer", error: true)]
+ public MSBuildTestCase() { }
+
+ public async Task RunAsync(
+ IMessageSink diagnosticMessageSink,
+ IMessageBus messageBus,
+ object[] constructorArguments,
+ ExceptionAggregator aggregator,
+ CancellationTokenSource cancellationTokenSource)
+ {
+ await MSBuildWorkspaceLoader.Guard.WaitAsync();
+ try
+ {
+ var runner = new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource);
+ return await runner.RunAsync();
+ }
+ finally
+ {
+ MSBuildWorkspaceLoader.Guard.Release();
+ }
+ }
+
+ public void Deserialize(IXunitSerializationInfo info)
+ {
+ _testCase = info.GetValue("InnerTestCase");
+ }
+
+ public void Serialize(IXunitSerializationInfo info)
+ {
+ info.AddValue("InnerTestCase", _testCase);
+ }
+ }
+}
diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryAttribute.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryAttribute.cs
index abf7c371c024..27b9f39b4d44 100644
--- a/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryAttribute.cs
+++ b/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryAttribute.cs
@@ -1,33 +1,17 @@
-// 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.Reflection;
-using System.Runtime.CompilerServices;
-using Microsoft.CodeAnalysis.Tools.Workspaces;
-using Xunit.v3;
+using Xunit.Sdk;
namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
- public sealed class MSBuildTheoryAttribute : ConditionalTheoryAttribute, IBeforeAfterTestAttribute
+ [XunitTestCaseDiscoverer("Microsoft.CodeAnalysis.Tools.Tests.XUnit.MSBuildTheoryDiscoverer", "dotnet-format.UnitTests")]
+ public sealed class MSBuildTheoryAttribute : ConditionalTheoryAttribute
{
- public MSBuildTheoryAttribute(params Type[] skipConditions) : base(skipConditions)
+ public MSBuildTheoryAttribute(params Type[] skipConditions)
+ : base(skipConditions)
{
}
-
- public MSBuildTheoryAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = 0)
- : base(Array.Empty(), sourceFilePath, sourceLineNumber)
- {
- }
-
- public void Before(MethodInfo methodUnderTest, IXunitTest test)
- {
- MSBuildWorkspaceLoader.Guard.Wait();
- }
-
- public void After(MethodInfo methodUnderTest, IXunitTest test)
- {
- MSBuildWorkspaceLoader.Guard.Release();
- }
}
}
diff --git a/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryDiscoverer.cs b/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryDiscoverer.cs
new file mode 100644
index 000000000000..22ce8042aaf7
--- /dev/null
+++ b/test/dotnet-format.UnitTests/XUnit/MSBuildTheoryDiscoverer.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit.Sdk;
+
+namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit
+{
+
+ public sealed class MSBuildTheoryDiscoverer : IXunitTestCaseDiscoverer
+ {
+ private readonly TheoryDiscoverer _theoryDiscoverer;
+
+ public MSBuildTheoryDiscoverer(IMessageSink diagnosticMessageSink)
+ {
+ _theoryDiscoverer = new TheoryDiscoverer(diagnosticMessageSink);
+ }
+
+ public IEnumerable Discover(
+ ITestFrameworkDiscoveryOptions discoveryOptions,
+ ITestMethod testMethod,
+ IAttributeInfo factAttribute)
+ {
+ return _theoryDiscoverer
+ .Discover(discoveryOptions, testMethod, factAttribute)
+ .Select(testCase => new MSBuildTestCase(testCase));
+ }
+ }
+}
diff --git a/test/dotnet-new.IntegrationTests/Diagnostic/DiagnosticFixture.cs b/test/dotnet-new.IntegrationTests/Diagnostic/DiagnosticFixture.cs
index 2eab6d6d2b9f..df772d9846de 100644
--- a/test/dotnet-new.IntegrationTests/Diagnostic/DiagnosticFixture.cs
+++ b/test/dotnet-new.IntegrationTests/Diagnostic/DiagnosticFixture.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Xunit.Sdk;
-
namespace Microsoft.DotNet.Cli.New.IntegrationTests
{
public class DiagnosticFixture
diff --git a/test/dotnet-new.IntegrationTests/Diagnostic/XunitNuGetLogger.cs b/test/dotnet-new.IntegrationTests/Diagnostic/XunitNuGetLogger.cs
index af6202fb527b..b25feb42326f 100644
--- a/test/dotnet-new.IntegrationTests/Diagnostic/XunitNuGetLogger.cs
+++ b/test/dotnet-new.IntegrationTests/Diagnostic/XunitNuGetLogger.cs
@@ -2,8 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using NuGet.Common;
-using Xunit.Sdk;
-using DiagnosticMessage = Xunit.v3.DiagnosticMessage;
+using DiagnosticMessage = Xunit.Sdk.DiagnosticMessage;
namespace Microsoft.DotNet.Cli.New.IntegrationTests
{
diff --git a/test/dotnet-new.IntegrationTests/DotnetNewDetailsTest.cs b/test/dotnet-new.IntegrationTests/DotnetNewDetailsTest.cs
index 364e95c6580c..80fa07d8d410 100644
--- a/test/dotnet-new.IntegrationTests/DotnetNewDetailsTest.cs
+++ b/test/dotnet-new.IntegrationTests/DotnetNewDetailsTest.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Xunit.Sdk;
-
namespace Microsoft.DotNet.Cli.New.IntegrationTests
{
public partial class DotnetNewDetailsTest : BaseIntegrationTest, IClassFixture
diff --git a/test/dotnet-new.IntegrationTests/DotnetNewHelpTests.Approval.cs b/test/dotnet-new.IntegrationTests/DotnetNewHelpTests.Approval.cs
index 974ef1352b55..464fc117c1c4 100644
--- a/test/dotnet-new.IntegrationTests/DotnetNewHelpTests.Approval.cs
+++ b/test/dotnet-new.IntegrationTests/DotnetNewHelpTests.Approval.cs
@@ -208,16 +208,12 @@ public Task CannotShowHelpForTemplate_FullNameMatch()
[Fact]
public Task CannotShowHelpForTemplate_WhenAmbiguousLanguageChoice()
{
- // Use a dedicated home directory to avoid conflicts with other tests that install
- // templates with the same 'basic' short name. Tests are not guaranteed to execute
- // in declared order.
string workingDirectory = CreateTemporaryFolder();
- string homeDirectory = CreateTemporaryFolder("Home");
- InstallTestTemplate("TemplateResolution/DifferentLanguagesGroup/BasicFSharp", _log, homeDirectory, workingDirectory);
- InstallTestTemplate("TemplateResolution/DifferentLanguagesGroup/BasicVB", _log, homeDirectory, workingDirectory);
+ InstallTestTemplate("TemplateResolution/DifferentLanguagesGroup/BasicFSharp", _log, _fixture.HomeDirectory, workingDirectory);
+ InstallTestTemplate("TemplateResolution/DifferentLanguagesGroup/BasicVB", _log, _fixture.HomeDirectory, workingDirectory);
CommandResult commandResult = new DotnetNewCommand(_log, "basic", "--help")
- .WithCustomHive(homeDirectory)
+ .WithCustomHive(_fixture.HomeDirectory)
.WithWorkingDirectory(workingDirectory)
.Execute();
@@ -397,15 +393,11 @@ public Task CanShowHelpForTemplate_ConditionalParams()
[Fact]
public Task CanShowHelpForTemplateWhenRequiredParamIsMissed()
{
- // Use a dedicated home directory to avoid conflicts with other tests that install
- // templates with the same 'basic' short name. Tests are not guaranteed to execute
- // in declared order.
string workingDirectory = CreateTemporaryFolder();
- string homeDirectory = CreateTemporaryFolder("Home");
- InstallTestTemplate($"TemplateResolution/MissedRequiredParameter/BasicTemplate1", _log, homeDirectory, workingDirectory);
+ InstallTestTemplate($"TemplateResolution/MissedRequiredParameter/BasicTemplate1", _log, _fixture.HomeDirectory, workingDirectory);
CommandResult commandResult = new DotnetNewCommand(_log, "basic", "--help")
- .WithCustomHive(homeDirectory)
+ .WithCustomHive(_fixture.HomeDirectory)
.WithWorkingDirectory(workingDirectory)
.Execute();
diff --git a/test/dotnet-new.IntegrationTests/DotnetNewInstallTests.cs b/test/dotnet-new.IntegrationTests/DotnetNewInstallTests.cs
index d8c4a5d28d58..c88f7e31f49b 100644
--- a/test/dotnet-new.IntegrationTests/DotnetNewInstallTests.cs
+++ b/test/dotnet-new.IntegrationTests/DotnetNewInstallTests.cs
@@ -5,8 +5,7 @@
using System.Text.RegularExpressions;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.TemplateEngine.TestHelper;
-using Xunit.Sdk;
-using DiagnosticMessage = Xunit.v3.DiagnosticMessage;
+using DiagnosticMessage = Xunit.Sdk.DiagnosticMessage;
namespace Microsoft.DotNet.Cli.New.IntegrationTests
{
diff --git a/test/dotnet-new.IntegrationTests/DotnetNewTestTemplatesTests.cs b/test/dotnet-new.IntegrationTests/DotnetNewTestTemplatesTests.cs
index a61bbcc94336..2e6128de171b 100644
--- a/test/dotnet-new.IntegrationTests/DotnetNewTestTemplatesTests.cs
+++ b/test/dotnet-new.IntegrationTests/DotnetNewTestTemplatesTests.cs
@@ -48,12 +48,6 @@ public static class Languages
private class NullTestOutputHelper : ITestOutputHelper
{
- public string Output => string.Empty;
-
- public void Write(string message) { }
-
- public void Write(string format, params object[] args) { }
-
public void WriteLine(string message) { }
public void WriteLine(string format, params object[] args) { }
diff --git a/test/dotnet-new.IntegrationTests/SharedHomeDirectory.cs b/test/dotnet-new.IntegrationTests/SharedHomeDirectory.cs
index fd946bf2aa4d..e7b91db01f32 100644
--- a/test/dotnet-new.IntegrationTests/SharedHomeDirectory.cs
+++ b/test/dotnet-new.IntegrationTests/SharedHomeDirectory.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.
-using Xunit.Sdk;
+using SharedTestOutputHelper = Microsoft.TemplateEngine.TestHelper.SharedTestOutputHelper;
namespace Microsoft.DotNet.Cli.New.IntegrationTests
{
diff --git a/test/dotnet-new.IntegrationTests/TemplateDiscoveryTests.cs b/test/dotnet-new.IntegrationTests/TemplateDiscoveryTests.cs
index c65e813bd950..a87b0fc3cbc2 100644
--- a/test/dotnet-new.IntegrationTests/TemplateDiscoveryTests.cs
+++ b/test/dotnet-new.IntegrationTests/TemplateDiscoveryTests.cs
@@ -17,7 +17,7 @@ public TemplateDiscoveryTests(ITestOutputHelper log, TemplateDiscoveryTool templ
}
#pragma warning disable xUnit1004 // Test methods should not be skipped
- [PlatformSpecificFact(skipPlatforms: TestPlatforms.OSX, skipArchitecture: Architecture.Arm64, skipReason: "https://github.com/dotnet/sdk/issues/53569")]
+ [PlatformSpecificFact(SkipPlatforms = TestPlatforms.OSX, SkipArchitecture = Architecture.Arm64, SkipReason = "https://github.com/dotnet/sdk/issues/53569")]
#pragma warning restore xUnit1004 // Test methods should not be skipped
public async Task CanRunDiscoveryTool()
{
diff --git a/test/dotnet-new.IntegrationTests/TemplateDiscoveryTool.cs b/test/dotnet-new.IntegrationTests/TemplateDiscoveryTool.cs
index 9d2d0a86a677..48034a63a83c 100644
--- a/test/dotnet-new.IntegrationTests/TemplateDiscoveryTool.cs
+++ b/test/dotnet-new.IntegrationTests/TemplateDiscoveryTool.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.
-using Xunit.Sdk;
+using SharedTestOutputHelper = Microsoft.TemplateEngine.TestHelper.SharedTestOutputHelper;
namespace Microsoft.DotNet.Cli.New.IntegrationTests
{
diff --git a/test/dotnet-new.IntegrationTests/WebProjectsTests.cs b/test/dotnet-new.IntegrationTests/WebProjectsTests.cs
index 59cc08e55c0c..7cd274107a21 100644
--- a/test/dotnet-new.IntegrationTests/WebProjectsTests.cs
+++ b/test/dotnet-new.IntegrationTests/WebProjectsTests.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.DotNet.Cli.Utils;
-using Xunit.Sdk;
namespace Microsoft.DotNet.Cli.New.IntegrationTests
{
diff --git a/test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj b/test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj
index e87d35c501b9..06014f7dfcda 100644
--- a/test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj
+++ b/test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj
@@ -13,10 +13,10 @@
-
+
-
-
+
+
diff --git a/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs b/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs
index 00e66f1f7762..3bbbeddb34d3 100644
--- a/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs
+++ b/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs
@@ -19,10 +19,10 @@ public DotNetWatchTestBase(ITestOutputHelper logger)
TestAssets = new TestAssetsManager(App.Logger);
}
- public ValueTask InitializeAsync()
- => default;
+ public Task InitializeAsync()
+ => Task.CompletedTask;
- public async ValueTask DisposeAsync()
+ public async Task DisposeAsync()
{
Log("Disposing test");
await App.DisposeAsync();
diff --git a/test/dotnet-watch.Tests/TestUtilities/MSBuildFixture.cs b/test/dotnet-watch.Tests/TestUtilities/ModuleInitializer.cs
similarity index 84%
rename from test/dotnet-watch.Tests/TestUtilities/MSBuildFixture.cs
rename to test/dotnet-watch.Tests/TestUtilities/ModuleInitializer.cs
index 71a39705de97..84836479d581 100644
--- a/test/dotnet-watch.Tests/TestUtilities/MSBuildFixture.cs
+++ b/test/dotnet-watch.Tests/TestUtilities/ModuleInitializer.cs
@@ -2,20 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using Microsoft.Build.Locator;
-[assembly: AssemblyFixture(typeof(Microsoft.DotNet.Watch.UnitTests.MSBuildFixture))]
-
namespace Microsoft.DotNet.Watch.UnitTests;
-///
-/// Assembly fixture that registers MSBuild and sets up assembly resolution for dotnet-watch tests.
-/// A fixture is preferred over a [ModuleInitializer] because it doesn't get invoked for test discovery.
-///
-public class MSBuildFixture
+public static class ModuleInitializer
{
- public MSBuildFixture()
+ [ModuleInitializer]
+ public static void Initialize()
{
// Ensure that we load the msbuild binaries from redist deployment. Otherwise, msbuild might use target files
// that do not match the implementations of the core tasks.
diff --git a/test/dotnet.Tests/CliSchemaTests.cs b/test/dotnet.Tests/CliSchemaTests.cs
index 71a507d3ebb9..8478d3c0d706 100644
--- a/test/dotnet.Tests/CliSchemaTests.cs
+++ b/test/dotnet.Tests/CliSchemaTests.cs
@@ -1208,7 +1208,7 @@ public void PrintCliSchema_WritesExpectedJson(string[] commandArgs, string json)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
- CliSchema.PrintCliSchema(Parser.Parse(commandArgs), writer, null);
+ CliSchema.PrintCliSchema(Parser.Parse(commandArgs).CommandResult, writer, null);
stream.Position = 0;
var reader = new StreamReader(stream);
var output = reader.ReadToEnd();
diff --git a/test/dotnet.Tests/CommandFactoryTests/GivenALocalToolsCommandResolver.cs b/test/dotnet.Tests/CommandFactoryTests/GivenALocalToolsCommandResolver.cs
index d526082c400d..b0ff2d4f0bd4 100644
--- a/test/dotnet.Tests/CommandFactoryTests/GivenALocalToolsCommandResolver.cs
+++ b/test/dotnet.Tests/CommandFactoryTests/GivenALocalToolsCommandResolver.cs
@@ -214,23 +214,15 @@ [new RestoredCommandIdentifier(
_localToolsResolverCache,
_fileSystem);
- var commandSpecA = localToolsCommandResolver.Resolve(new CommandResolverArguments()
+ localToolsCommandResolver.Resolve(new CommandResolverArguments()
{
CommandName = "dotnet-a",
- });
- commandSpecA.Should().NotBeNull();
- var argsA = commandSpecA.Args;
- argsA.Should().NotBeNull();
- argsA.Trim('"').Should().Be(fakeExecutableA.Value);
+ }).Args!.Trim('"').Should().Be(fakeExecutableA.Value);
- var commandSpecDotnetA = localToolsCommandResolver.Resolve(new CommandResolverArguments()
+ localToolsCommandResolver.Resolve(new CommandResolverArguments()
{
CommandName = "dotnet-dotnet-a",
- });
- commandSpecDotnetA.Should().NotBeNull();
- var argsDotnetA = commandSpecDotnetA.Args;
- argsDotnetA.Should().NotBeNull();
- argsDotnetA.Trim('"').Should().Be(fakeExecutableDotnetA.Value);
+ }).Args!.Trim('"').Should().Be(fakeExecutableDotnetA.Value);
}
private string _jsonContent =
diff --git a/test/dotnet.Tests/CommandTests/CommandDirectoryContextExtensions.cs b/test/dotnet.Tests/CommandTests/CommandDirectoryContextExtensions.cs
index 9f82d05313e5..360342b83c30 100644
--- a/test/dotnet.Tests/CommandTests/CommandDirectoryContextExtensions.cs
+++ b/test/dotnet.Tests/CommandTests/CommandDirectoryContextExtensions.cs
@@ -22,6 +22,7 @@ public static void PerformActionWithBasePath(string basePath, Action action)
}
CommandDirectoryContext.CurrentBaseDirectory_TestOnly = basePath;
+ Telemetry.Telemetry.CurrentSessionId = null;
try
{
action();
diff --git a/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs b/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs
index 433b5b5cf2de..a2a26316e3d8 100644
--- a/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs
+++ b/test/dotnet.Tests/CommandTests/MSBuild/DotnetMsbuildInProcTests.cs
@@ -5,7 +5,6 @@
using System.Reflection;
using Microsoft.DotNet.Cli.Commands.MSBuild;
-using Microsoft.DotNet.Cli.Telemetry;
using Microsoft.DotNet.Configurer;
namespace Microsoft.DotNet.Cli.MSBuild.Tests
@@ -20,7 +19,8 @@ public DotnetMsbuildInProcTests(ITestOutputHelper log) : base(log)
[Fact]
public void WhenTelemetryIsEnabledTheLoggerIsAddedToTheCommandLine()
{
- string[] allArgs = GetArgsForMSBuild(() => true, out TelemetryClient telemetry);
+ Telemetry.Telemetry telemetry;
+ string[] allArgs = GetArgsForMSBuild(() => true, out telemetry);
// telemetry will still be disabled if environment variable is set
if (telemetry.Enabled)
{
@@ -46,13 +46,15 @@ public void WhenTelemetryIsDisabledTheLoggerIsNotAddedToTheCommandLine()
private string[] GetArgsForMSBuild(Func sentinelExists)
{
- return GetArgsForMSBuild(sentinelExists, out TelemetryClient telemetry);
+ Telemetry.Telemetry telemetry;
+ return GetArgsForMSBuild(sentinelExists, out telemetry);
}
- private string[] GetArgsForMSBuild(Func sentinelExists, out TelemetryClient telemetry)
+ private string[] GetArgsForMSBuild(Func sentinelExists, out Telemetry.Telemetry telemetry)
{
- TelemetryClient.DisabledForTests = true; // reset static session id modified by telemetry constructor
- telemetry = new TelemetryClient();
+
+ Telemetry.Telemetry.DisableForTests(); // reset static session id modified by telemetry constructor
+ telemetry = new Telemetry.Telemetry(new MockFirstTimeUseNoticeSentinel(sentinelExists));
MSBuildForwardingApp msBuildForwardingApp = new(Enumerable.Empty());
diff --git a/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs b/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs
index 5bf3435c507f..bcb6a6dbfde1 100644
--- a/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs
+++ b/test/dotnet.Tests/CommandTests/MSBuild/FakeTelemetry.cs
@@ -1,23 +1,34 @@
// 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.Cli.Telemetry;
+#nullable disable
-namespace Microsoft.DotNet.Cli.MSBuild.Tests;
+using Microsoft.DotNet.Cli.Telemetry;
-public class FakeTelemetry : ITelemetryClient
+namespace Microsoft.DotNet.Cli.MSBuild.Tests
{
- public bool Enabled { get; set; } = true;
+ public class FakeTelemetry : ITelemetry
+ {
+ public bool Enabled { get; set; } = true;
- private readonly List _logEntries = new List();
+ private readonly List _logEntries = new List();
- public void TrackEvent(string eventName, IDictionary? properties)
- {
- var entry = new LogEntry { EventName = eventName, Properties = properties };
- _logEntries.Add(entry);
- }
+ public void TrackEvent(string eventName, IDictionary properties, IDictionary measurements)
+ {
+ var entry = new LogEntry { EventName = eventName, Properties = properties, Measurement = measurements };
+ _logEntries.Add(entry);
+ }
+
+ public void Flush()
+ {
+ }
- public LogEntry? LogEntry => _logEntries.Count > 0 ? _logEntries[_logEntries.Count - 1] : null;
+ public void Dispose()
+ {
+ }
- public IReadOnlyList LogEntries => _logEntries.AsReadOnly();
+ public LogEntry LogEntry => _logEntries.Count > 0 ? _logEntries[_logEntries.Count - 1] : null;
+
+ public IReadOnlyList LogEntries => _logEntries.AsReadOnly();
+ }
}
diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs
index 812ec22b2c8f..ff6543518696 100644
--- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs
+++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetBuildInvocation.cs
@@ -3,7 +3,7 @@
using Microsoft.DotNet.Cli.Commands.Restore;
using Microsoft.DotNet.Cli.Utils;
-using Microsoft.DotNet.Tests.TelemetryTests;
+using Microsoft.DotNet.Tests;
using BuildCommand = Microsoft.DotNet.Cli.Commands.Build.BuildCommand;
namespace Microsoft.DotNet.Cli.MSBuild.Tests
@@ -122,7 +122,7 @@ public void MsbuildInvocationIsCorrectForSeparateRestore(
}
[Theory]
- [MemberData(memberName: nameof(TelemetryCommonPropertiesTests.LLMTelemetryTestCases), MemberType = typeof(TelemetryCommonPropertiesTests))]
+ [MemberData(memberName: nameof(TelemetryCommonPropertiesTests.LLMTelemetryTestCases), MemberType =typeof(TelemetryCommonPropertiesTests))]
public void WhenLLMIsDetectedTLLiveUpdateIsDisabled(Dictionary? llmEnvVarsToSet, string? expectedLLMName)
{
CommandDirectoryContext.PerformActionWithBasePath(WorkingDirectory, () =>
diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetMSBuildBuildsProjects.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetMSBuildBuildsProjects.cs
index beeac7148703..e995a09ae611 100644
--- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetMSBuildBuildsProjects.cs
+++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetMSBuildBuildsProjects.cs
@@ -1,7 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-// There are tests which modify static TelemetryClient.CurrentSessionId. They cannot run in parallel.
+
+
+// There are tests which modify static Telemetry.CurrentSessionId and they cannot run in parallel
[assembly: CollectionBehavior(DisableTestParallelization = true)]
namespace Microsoft.DotNet.Cli.MSBuild.Tests
@@ -104,5 +106,11 @@ public void WhenDotnetRunHelpIsInvokedAppArgumentsTextIsIncludedInOutput()
result.ExitCode.Should().Be(0);
result.StdOut.Should().Contain(AppArgumentsText);
}
+
+
+
+
}
+
+
}
diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetRestoreInvocation.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetRestoreInvocation.cs
index db007ec0cdc9..9b4dca228442 100644
--- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetRestoreInvocation.cs
+++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetRestoreInvocation.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.DotNet.Cli.Commands.MSBuild;
-using Microsoft.DotNet.Cli.Telemetry;
using RestoreCommand = Microsoft.DotNet.Cli.Commands.Restore.RestoreCommand;
namespace Microsoft.DotNet.Cli.MSBuild.Tests
@@ -42,7 +41,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona
{
CommandDirectoryContext.PerformActionWithBasePath(WorkingDirectory, () =>
{
- TelemetryClient.DisabledForTests = true;
+ Telemetry.Telemetry.DisableForTests();
expectedAdditionalArgs = expectedAdditionalArgs
.Select(arg => arg.Replace("", WorkingDirectory))
diff --git a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetTestInvocation.cs b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetTestInvocation.cs
index d089f147ff6f..c017646949b0 100644
--- a/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetTestInvocation.cs
+++ b/test/dotnet.Tests/CommandTests/MSBuild/GivenDotnetTestInvocation.cs
@@ -1,7 +1,6 @@
// 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.Cli.Telemetry;
using TestCommand = Microsoft.DotNet.Cli.Commands.Test.TestCommand;
namespace Microsoft.DotNet.Cli.MSBuild.Tests
@@ -27,7 +26,7 @@ public void MsbuildInvocationIsCorrect(string[] args, string[] expectedAdditiona
{
CommandDirectoryContext.PerformActionWithBasePath(WorkingDirectory, () =>
{
- TelemetryClient.DisabledForTests = true;
+ Telemetry.Telemetry.DisableForTests();
expectedAdditionalArgs = expectedAdditionalArgs
.Select(arg => arg.Replace("", WorkingDirectory))
diff --git a/test/dotnet.Tests/CommandTests/MSBuild/NullCurrentSessionIdFixture.cs b/test/dotnet.Tests/CommandTests/MSBuild/NullCurrentSessionIdFixture.cs
index 54ddd2256b19..b2974fba1006 100644
--- a/test/dotnet.Tests/CommandTests/MSBuild/NullCurrentSessionIdFixture.cs
+++ b/test/dotnet.Tests/CommandTests/MSBuild/NullCurrentSessionIdFixture.cs
@@ -1,20 +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.Cli.Telemetry;
-
-namespace Microsoft.DotNet.Cli.MSBuild.Tests;
-
-public class NullCurrentSessionIdFixture
+namespace Microsoft.DotNet.Cli.MSBuild.Tests
{
- public NullCurrentSessionIdFixture()
+ public class NullCurrentSessionIdFixture
{
- // We need to set this to guarantee that the telemetry logging
- // information will not be added to the msbuild generated parameters
- // when testing the translation between CLI params and msbuild params.
- // This is now needed because before we set SKIP FIRST RUN in the CLI
- // build scripts, but now we don't and we don't want to rely on scripts
- // to make our build/tests work.
- TelemetryClient.DisabledForTests = true;
+ public NullCurrentSessionIdFixture()
+ {
+ // We need to set this to guarantee that the telemetry logging
+ // information will not be added to the msbuild generated parameters
+ // when testing the translation between CLI params and msbuild params.
+ // This is now needed because before we set SKIP FIRST RUN in the CLI
+ // build scripts, but now we don't and we don't want to rely on scripts
+ // to make our build/tests work.
+ Telemetry.Telemetry.DisableForTests();
+ }
}
}
diff --git a/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs b/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs
index 102640211fe5..744f8bcd9856 100644
--- a/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs
+++ b/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs
@@ -3,6 +3,7 @@
using System.Runtime.CompilerServices;
using Microsoft.DotNet.Cli.Commands;
+using Xunit.Runners;
namespace Microsoft.DotNet.Cli.Package.Add.Tests
{
@@ -378,7 +379,7 @@ public void FileBasedApp_NoVersion(string[] inputVersions, string? expectedVersi
var file = Path.Join(testInstance.Path, "Program.cs");
var source = $"""
- #:property RestoreAdditionalProjectSources={restoreSources}
+ #:property RestoreSources=$(RestoreSources);{restoreSources}
Console.WriteLine();
""";
File.WriteAllText(file, source);
@@ -415,7 +416,7 @@ public void FileBasedApp_NoVersion_Prerelease(string[] inputVersions, string? _,
var file = Path.Join(testInstance.Path, "Program.cs");
var source = $"""
- #:property RestoreAdditionalProjectSources={restoreSources}
+ #:property RestoreSources=$(RestoreSources);{restoreSources}
Console.WriteLine();
""";
File.WriteAllText(file, source);
@@ -636,7 +637,7 @@ public void FileBasedApp_CentralPackageManagement_NoVersionSpecified(bool legacy
var file = Path.Join(testInstance.Path, "Program.cs");
var source = $"""
- #:property RestoreAdditionalProjectSources={restoreSources}
+ #:property RestoreSources=$(RestoreSources);{restoreSources}
Console.WriteLine();
""";
File.WriteAllText(file, source);
@@ -686,7 +687,7 @@ public void FileBasedApp_CentralPackageManagement_NoVersionSpecified_KeepExistin
var file = Path.Join(testInstance.Path, "Program.cs");
var source = $"""
- #:property RestoreAdditionalProjectSources={restoreSources}
+ #:property RestoreSources=$(RestoreSources);{restoreSources}
#:package A
Console.WriteLine();
""";
diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunIsInterrupted.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunIsInterrupted.cs
index 7ebdcd079eb9..a7a188e14648 100644
--- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunIsInterrupted.cs
+++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunIsInterrupted.cs
@@ -27,10 +27,6 @@ public void ItTerminatesWinExeAppWithCloseMainWindow()
var command = new DotnetCommand(Log, "run")
.WithWorkingDirectory(asset.Path);
- // Launch dotnet run in a new process group so that GenerateConsoleCtrlEvent
- // targets only the child group and does not propagate to the test host.
- command.CreateNewProcessGroup = true;
-
bool signaled = false;
bool sawClosingGracefully = false;
Process child = null;
diff --git a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs
index 0bfaa370f9cf..ae6425892777 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs
@@ -144,9 +144,9 @@ public void CountAdditionalProperties_CountsPropertyDirectives()
public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry()
{
// Arrange
- var events = new List<(string? eventName, IDictionary? properties)>();
+ var events = new List<(string? eventName, IDictionary? properties, IDictionary? measurements)>();
- void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties));
+ void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements));
TelemetryEventEntry.EntryPosted += handler;
@@ -171,6 +171,7 @@ public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry()
var eventData = events[0];
eventData.eventName.Should().Be("run");
eventData.properties.Should().NotBeNull();
+ eventData.measurements.Should().NotBeNull();
var props = eventData.properties!;
props["app_type"].Should().Be("file_based");
@@ -179,6 +180,12 @@ public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry()
props["used_roslyn_compiler"].Should().Be("false");
props["launch_profile_requested"].Should().Be("explicit");
props["launch_profile_is_default"].Should().Be("true");
+
+ var measurements = eventData.measurements!;
+ measurements["sdk_count"].Should().Be(2);
+ measurements["package_reference_count"].Should().Be(3);
+ measurements["project_reference_count"].Should().Be(1);
+ measurements["additional_properties_count"].Should().Be(2);
}
finally
{
@@ -191,9 +198,9 @@ public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry()
public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry()
{
// Arrange
- var events = new List<(string? eventName, IDictionary? properties)>();
+ var events = new List<(string? eventName, IDictionary? properties, IDictionary? measurements)>();
- void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties));
+ void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements));
TelemetryEventEntry.EntryPosted += handler;
@@ -215,6 +222,7 @@ public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry()
var eventData = events[0];
eventData.eventName.Should().Be("run");
eventData.properties.Should().NotBeNull();
+ eventData.measurements.Should().NotBeNull();
var props = eventData.properties!;
props["app_type"].Should().Be("project_based");
@@ -222,6 +230,12 @@ public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry()
props["launch_profile_requested"].Should().Be("none");
props.Should().NotContainKey("used_msbuild");
props.Should().NotContainKey("used_roslyn_compiler");
+
+ var measurements = eventData.measurements!;
+ measurements["sdk_count"].Should().Be(1);
+ measurements["package_reference_count"].Should().Be(5);
+ measurements["project_reference_count"].Should().Be(2);
+ measurements.Should().NotContainKey("additional_properties_count");
}
finally
{
@@ -234,9 +248,9 @@ public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry()
public void TrackRunEvent_WithDefaultLaunchProfile_MarksTelemetryCorrectly()
{
// Arrange
- var events = new List<(string? eventName, IDictionary? properties)>();
+ var events = new List<(string? eventName, IDictionary? properties, IDictionary? measurements)>();
- void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties));
+ void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements));
TelemetryEventEntry.EntryPosted += handler;
diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs
index ac056bcb2fbe..6bc213d4a4e4 100644
--- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs
+++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestfromCsproj.cs
@@ -716,9 +716,7 @@ public void EnsureOutputPathEscaped(string flag)
{
var testProjectDirectory = CopyAndRestoreVSTestDotNetCoreTestApp([flag]);
- // Use a unique subdirectory per flag to avoid conflicts between theory data rows.
- // --diag creates a file, while --output and --results-directory create directories.
- var pathWithComma = Path.Combine(AppContext.BaseDirectory, "a,b", flag.TrimStart('-'));
+ var pathWithComma = Path.Combine(AppContext.BaseDirectory, "a,b");
// Call test
CommandResult result = new DotnetTestCommand(Log, disableNewOutput: true)
diff --git a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurer.cs b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurer.cs
index 756766a8aa5f..f5b87dadeee8 100644
--- a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurer.cs
+++ b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurer.cs
@@ -259,5 +259,77 @@ public void It_does_not_add_the_tool_path_to_the_environment_if_addGlobalToolsTo
_pathAdderMock.Verify(p => p.AddPackageExecutablePathToUserPath(), Times.Never);
}
+
+ [Fact]
+ public void It_does_add_telemetry_when_all_firsttimeuse_values_run()
+ {
+
+ _firstTimeUseNoticeSentinelMock.Setup(n => n.Exists()).Returns(false);
+
+ Dictionary measurements = new();
+ var dotnetFirstTimeUseConfigurer = new DotnetFirstTimeUseConfigurer(
+ _firstTimeUseNoticeSentinelMock.Object,
+ _aspNetCertificateSentinelMock.Object,
+ _aspNetCoreCertificateGeneratorMock.Object,
+ _toolPathSentinelMock.Object,
+ new DotnetFirstRunConfiguration
+ (
+ generateAspNetCertificate: true,
+ telemetryOptout: false,
+ addGlobalToolsToPath: true,
+ nologo: false,
+ skipWorkloadIntegrityCheck: false
+ ),
+ _reporterMock.Object,
+ _pathAdderMock.Object,
+ measurements);
+
+ DateTime beforeConfigure = DateTime.Now;
+ dotnetFirstTimeUseConfigurer.Configure();
+ double configureTime = (DateTime.Now - beforeConfigure).TotalMilliseconds;
+
+ measurements.Should().HaveCount(3);
+ measurements.Should().ContainKey("AddPackageExecutablePath Time");
+ measurements.Should().ContainKey("FirstTimeUseNotice Time");
+ measurements.Should().ContainKey("GenerateAspNetCertificate Time");
+ measurements["AddPackageExecutablePath Time"].Should().BeGreaterThan(0);
+ measurements["FirstTimeUseNotice Time"].Should().BeGreaterThan(0);
+ measurements["GenerateAspNetCertificate Time"].Should().BeGreaterThan(0);
+ measurements["AddPackageExecutablePath Time"].Should().BeLessThan(configureTime);
+ measurements["FirstTimeUseNotice Time"].Should().BeLessThan(configureTime);
+ measurements["GenerateAspNetCertificate Time"].Should().BeLessThan(configureTime);
+ }
+
+ [Fact]
+ public void It_does_add_telemetry_when_no_firsttimeuse_values_run()
+ {
+
+ _firstTimeUseNoticeSentinelMock.Setup(n => n.Exists()).Returns(true);
+
+ Dictionary measurements = new();
+ var dotnetFirstTimeUseConfigurer = new DotnetFirstTimeUseConfigurer(
+ _firstTimeUseNoticeSentinelMock.Object,
+ _aspNetCertificateSentinelMock.Object,
+ _aspNetCoreCertificateGeneratorMock.Object,
+ _toolPathSentinelMock.Object,
+ new DotnetFirstRunConfiguration
+ (
+ generateAspNetCertificate: false,
+ telemetryOptout: false,
+ addGlobalToolsToPath: false,
+ nologo: false,
+ skipWorkloadIntegrityCheck: false
+ ),
+ _reporterMock.Object,
+ _pathAdderMock.Object,
+ measurements);
+
+ dotnetFirstTimeUseConfigurer.Configure();
+
+ measurements.Should().HaveCount(0);
+ measurements.Should().NotContainKey("AddPackageExecutablePath Time");
+ measurements.Should().NotContainKey("FirstTimeUseNotice Time");
+ measurements.Should().NotContainKey("GenerateAspNetCertificate Time");
+ }
}
}
diff --git a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs
index 89cc75f779b1..d694240d865b 100644
--- a/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs
+++ b/test/dotnet.Tests/ConfigurerTests/GivenADotnetFirstTimeUseConfigurerWIthStateSetup.cs
@@ -28,7 +28,7 @@ public GivenADotnetFirstTimeUseConfigurerWithStateSetup(ITestOutputHelper output
private void ResetObjectState()
{
- TelemetryClient.DisabledForTests = false;
+ Telemetry.EnableForTests();
_firstTimeUseNoticeSentinelMock = new MockBasicSentinel();
_aspNetCertificateSentinelMock = new MockBasicSentinel();
_aspNetCoreCertificateGeneratorMock = new Mock(MockBehavior.Strict);
@@ -183,6 +183,16 @@ public void Assert(ActionCalledTime expectedActionCalledTime)
}
}
+ private static ActionCalledTime GetCalledTime(bool predicate, ActionCalledTime actionCalledTime)
+ {
+ if (actionCalledTime != FirstRun && predicate)
+ {
+ actionCalledTime = SecondRun;
+ }
+
+ return actionCalledTime;
+ }
+
public enum ActionCalledTime
{
Never,
@@ -190,7 +200,7 @@ public enum ActionCalledTime
SecondRun
}
- private TelemetryClient RunConfigUsingMocks(bool isInstallerRun)
+ private Telemetry RunConfigUsingMocks(bool isInstallerRun)
{
// Assume the following objects set up are in sync with production behavior.
// subject to future refactoring to de-dup with production code.
@@ -242,7 +252,10 @@ private TelemetryClient RunConfigUsingMocks(bool isInstallerRun)
configurer.Configure();
- return new TelemetryClient("test", environmentProvider: _environmentProviderObject);
+ return new Telemetry(firstTimeUseNoticeSentinel,
+ "test",
+ environmentProvider: _environmentProviderObject,
+ senderCount: 0);
}
private class MockBasicSentinel : IFileSentinel, IFirstTimeUseNoticeSentinel, IAspNetCertificateSentinel
diff --git a/test/dotnet.Tests/FakeRecordEventNameTelemetry.cs b/test/dotnet.Tests/FakeRecordEventNameTelemetry.cs
new file mode 100644
index 000000000000..4ce548e97621
--- /dev/null
+++ b/test/dotnet.Tests/FakeRecordEventNameTelemetry.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Collections.Concurrent;
+using Microsoft.DotNet.Cli.Telemetry;
+
+namespace Microsoft.DotNet.Tests
+{
+ public class FakeRecordEventNameTelemetry : ITelemetry
+ {
+ public bool Enabled { get; set; }
+
+ public string EventName { get; set; }
+
+ public void TrackEvent(string eventName,
+ IDictionary properties,
+ IDictionary measurements)
+ {
+ LogEntries.Add(
+ new LogEntry
+ {
+ EventName = eventName,
+ Measurement = measurements,
+ Properties = properties
+ });
+ }
+
+ public void Flush()
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public ConcurrentBag LogEntries { get; set; } = new ConcurrentBag();
+
+ public class LogEntry
+ {
+ public string EventName { get; set; }
+ public IDictionary Properties { get; set; }
+ public IDictionary Measurement { get; set; }
+ }
+ }
+}
diff --git a/test/dotnet.Tests/GivenThatTheUserEnablesThePerfLog.cs b/test/dotnet.Tests/GivenThatTheUserEnablesThePerfLog.cs
new file mode 100644
index 000000000000..06a8418a6acb
--- /dev/null
+++ b/test/dotnet.Tests/GivenThatTheUserEnablesThePerfLog.cs
@@ -0,0 +1,81 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.Tracing;
+
+namespace Microsoft.DotNet.Tests
+{
+ public class GivenThatTheUserEnablesThePerfLog : SdkTest
+ {
+ public GivenThatTheUserEnablesThePerfLog(ITestOutputHelper log) : base(log)
+ {
+ }
+
+ [Fact]
+ public void WhenPerfLogDisabledDotNetDoesNotWriteToThePerfLog()
+ {
+ var dir = TestAssetsManager.CreateTestDirectory();
+
+ var result = new DotnetCommand(Log, "--help")
+ .WithEnvironmentVariable("DOTNET_PERFLOG_DIR", dir.Path)
+ .Execute();
+
+ result.ExitCode.Should().Be(0);
+ Assert.Empty(new DirectoryInfo(dir.Path).GetFiles());
+ }
+
+ [Fact]
+ public void WhenPerfLogEnabledDotNetWritesToThePerfLog()
+ {
+ var dir = TestAssetsManager.CreateTestDirectory();
+
+ var result = new DotnetCommand(Log, "--help")
+ .WithEnvironmentVariable("DOTNET_CLI_PERF_LOG", "1")
+ .WithEnvironmentVariable("DOTNET_PERFLOG_DIR", dir.Path)
+ .Execute();
+
+ result.ExitCode.Should().Be(0);
+
+ DirectoryInfo logDir = new(dir.Path);
+ FileInfo[] logFiles = logDir.GetFiles();
+ Assert.NotEmpty(logFiles);
+ Assert.All(logFiles, f => Assert.StartsWith("perf-", f.Name));
+ Assert.All(logFiles, f => Assert.NotEqual(0, f.Length));
+ }
+
+ [Fact]
+ public void WhenPerfLogEnabledDotNetBuildWritesAPerfLog()
+ {
+ using (PerfLogTestEventListener listener = new())
+ {
+ int exitCode = Cli.Program.Main(new string[] { "--help" });
+ Assert.Equal(0, exitCode);
+ Assert.NotEqual(0, listener.EventCount);
+ }
+ }
+ }
+
+ internal sealed class PerfLogTestEventListener : EventListener
+ {
+ private const string PerfLogEventSourceName = "Microsoft-Dotnet-CLI-Performance";
+
+ public int EventCount
+ {
+ get; private set;
+ }
+
+ protected override void OnEventSourceCreated(EventSource eventSource)
+ {
+ if (eventSource.Name.Equals(PerfLogEventSourceName))
+ {
+ EnableEvents(eventSource, EventLevel.Verbose);
+ }
+ }
+
+ protected override void OnEventWritten(EventWrittenEventArgs eventData)
+ {
+ Assert.Equal(PerfLogEventSourceName, eventData.EventSource.Name);
+ EventCount++;
+ }
+ }
+}
diff --git a/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs b/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs
index f0950fe9b9fd..5e9191d076f0 100644
--- a/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs
+++ b/test/dotnet.Tests/GivenThatTheUserIsRunningDotNetForTheFirstTime.cs
@@ -165,7 +165,8 @@ public void ItDoesNotCreateAFirstUseSentinelFileNorAnAspNetCertificateSentinelFi
var command = dotnetFirstTime.Setup(Log, TestAssetsManager);
- // Disable telemetry to prevent the creation of the .dotnet folder for machineid and docker cache files.
+ // Disable telemetry to prevent the creation of the .dotnet folder
+ // for machineid and docker cache files
command = command.WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "true");
command.Execute("internal-reportinstallsuccess", "test").Should().Pass();
diff --git a/test/dotnet.Tests/TelemetryCommandTest.cs b/test/dotnet.Tests/TelemetryCommandTest.cs
new file mode 100644
index 000000000000..aa180fe14930
--- /dev/null
+++ b/test/dotnet.Tests/TelemetryCommandTest.cs
@@ -0,0 +1,416 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess;
+using Microsoft.DotNet.Cli.Telemetry;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.DotNet.Utilities;
+
+namespace Microsoft.DotNet.Tests
+{
+ [Collection(TestConstants.UsesStaticTelemetryState)]
+ public class TelemetryCommandTests : SdkTest
+ {
+ private readonly FakeRecordEventNameTelemetry _fakeTelemetry;
+
+ public string EventName { get; set; }
+
+ public IDictionary Properties { get; set; }
+
+ public TelemetryCommandTests(ITestOutputHelper log) : base(log)
+ {
+ _fakeTelemetry = new FakeRecordEventNameTelemetry();
+ TelemetryEventEntry.Subscribe(_fakeTelemetry.TrackEvent);
+ TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);
+ }
+
+ [Fact]
+ public void NoTelemetryIfCommandIsInvalid()
+ {
+ string[] args = { "publish", "-r" };
+ Action a = () => { Cli.Program.ProcessArgs(args); };
+ a.Should().NotThrow();
+ }
+
+ [Fact]
+ public void NoTelemetryIfCommandIsInvalid2()
+ {
+ string[] args = { "restore", "-v" };
+ Action a = () => { Cli.Program.ProcessArgs(args); };
+ a.Should().NotThrow();
+ }
+
+ [Fact]
+ public void TopLevelCommandNameShouldBeSentToTelemetry()
+ {
+ string[] args = { "help" };
+ Cli.Program.ProcessArgs(args);
+
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("HELP"));
+ }
+
+ [Fact]
+ public void TopLevelCommandNameShouldBeSentToTelemetryWithPerformanceData()
+ {
+ string[] args = { "help" };
+ Cli.Program.ProcessArgs(args, new TimeSpan(12345));
+
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("HELP") &&
+ e.Measurement.ContainsKey("Startup Time") &&
+ e.Measurement["Startup Time"] == 1.2345 &&
+ e.Measurement.ContainsKey("Parse Time") &&
+ e.Measurement["Parse Time"] > 0);
+ }
+
+ [Fact]
+ public void TopLevelCommandNameShouldBeSentToTelemetryWithoutStartupTime()
+ {
+ string[] args = { "help" };
+ Cli.Program.ProcessArgs(args);
+
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("HELP") &&
+ !e.Measurement.ContainsKey("Startup Time") &&
+ e.Measurement.ContainsKey("Parse Time") &&
+ e.Measurement["Parse Time"] > 0);
+ }
+
+ [Fact]
+ public void TopLevelCommandNameShouldBeSentToTelemetryZeroStartupTime()
+ {
+ string[] args = { "help" };
+ Cli.Program.ProcessArgs(args, new TimeSpan(0));
+
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("HELP") &&
+ !e.Measurement.ContainsKey("Startup Time") &&
+ e.Measurement.ContainsKey("Parse Time") &&
+ e.Measurement["Parse Time"] > 0);
+ }
+
+ [Fact]
+ public void DotnetNewCommandFirstArgumentShouldBeSentToTelemetry()
+ {
+ const string argumentToSend = "console";
+ string[] args = { "new", argumentToSend };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("NEW"));
+ }
+
+ [Fact(Skip = "https://github.com/dotnet/sdk/issues/24190")]
+ public void DotnetNewCommandFirstArgumentShouldBeSentToTelemetryWithPerformanceData()
+ {
+ const string argumentToSend = "console";
+ string[] args = { "new", argumentToSend };
+ Cli.Program.ProcessArgs(args, new TimeSpan(23456));
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("NEW") &&
+ e.Measurement.ContainsKey("Startup Time") &&
+ e.Measurement["Startup Time"] == 2.3456 &&
+ e.Measurement.ContainsKey("Parse Time") &&
+ e.Measurement["Parse Time"] > 0);
+ }
+
+ [Fact]
+ public void DotnetHelpCommandFirstArgumentShouldBeSentToTelemetry()
+ {
+ const string argumentToSend = "something";
+ string[] args = { "help", argumentToSend };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("HELP"));
+ }
+
+ [Fact]
+ public void DotnetAddCommandFirstArgumentShouldBeSentToTelemetry()
+ {
+ const string argumentToSend = "package";
+ string[] args = { "add", argumentToSend, "aPackageName" };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("ADD"));
+ }
+
+ [Fact]
+ public void DotnetAddCommandFirstArgumentShouldBeSentToTelemetry2()
+ {
+ const string argumentToSend = "reference";
+ string[] args = { "add", argumentToSend, "aPackageName" };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("ADD"));
+ }
+
+ [Fact]
+ public void DotnetRemoveCommandFirstArgumentShouldBeSentToTelemetry()
+ {
+ const string argumentToSend = "package";
+ string[] args = { "remove", argumentToSend, "aPackageName" };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("REMOVE"));
+ }
+
+ [Fact]
+ public void DotnetListCommandFirstArgumentShouldBeSentToTelemetry()
+ {
+ const string argumentToSend = "reference";
+ string[] args = { "list", argumentToSend, "aPackageName" };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("LIST"));
+ }
+
+ [Fact]
+ public void DotnetSlnCommandFirstArgumentShouldBeSentToTelemetry()
+ {
+ const string argumentToSend = "list";
+ string[] args = { "sln", "aSolution", argumentToSend };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("SOLUTION"));
+ }
+
+ [Fact]
+ public void DotnetNugetCommandFirstArgumentShouldBeSentToTelemetry()
+ {
+ const string argumentToSend = "push";
+
+ string[] args = { "nuget", argumentToSend, "path" };
+
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("NUGET"));
+ }
+
+ [Fact(Skip = "https://github.com/dotnet/sdk/issues/47862")]
+ public void DotnetNewCommandLanguageOpinionShouldBeSentToTelemetry()
+ {
+ const string optionKey = "language";
+ const string optionValueToSend = "c#";
+ string[] args = { "new", "console", "--" + optionKey, optionValueToSend };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey(optionKey) &&
+ e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("NEW"));
+ }
+
+ [Fact]
+ public void AnyDotnetCommandVerbosityOpinionShouldBeSentToTelemetry()
+ {
+ const string optionKey = "verbosity";
+ const string optionValueToSend = "minimal";
+ string[] args = { "restore", "--" + optionKey, optionValueToSend };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey(optionKey) &&
+ e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("RESTORE"));
+ }
+
+ [Fact]
+ public void AnyDotnetCommandVerbosityOpinionShouldBeSentToTelemetryWithPerformanceData()
+ {
+ const string optionKey = "verbosity";
+ const string optionValueToSend = "minimal";
+ string[] args = { "restore", "--" + optionKey, optionValueToSend };
+ Cli.Program.ProcessArgs(args, new TimeSpan(34567));
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey(optionKey) &&
+ e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("RESTORE") &&
+ e.Measurement.ContainsKey("Startup Time") &&
+ e.Measurement["Startup Time"] == 3.4567 &&
+ e.Measurement.ContainsKey("Parse Time") &&
+ e.Measurement["Parse Time"] > 0);
+ }
+
+ [Fact]
+ public void DotnetBuildAndPublishCommandOpinionsShouldBeSentToTelemetry()
+ {
+ const string optionKey = "configuration";
+ const string optionValueToSend = "Debug";
+ string[] args = { "build", "--" + optionKey, optionValueToSend };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey(optionKey) &&
+ e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("BUILD"));
+ }
+
+ [Fact]
+ public void DotnetPublishCommandRuntimeOpinionsShouldBeSentToTelemetry()
+ {
+ const string optionKey = "runtime";
+ const string optionValueToSend = $"{ToolsetInfo.LatestWinRuntimeIdentifier}-x64";
+ string[] args = { "publish", "--" + optionKey, optionValueToSend };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey(optionKey) &&
+ e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("PUBLISH"));
+ }
+
+ [Fact]
+ public void DotnetBuildAndPublishCommandOpinionsShouldBeSentToTelemetryWhenThereIsMultipleOption()
+ {
+ string[] args = { "build", "--configuration", "Debug", "--runtime", $"{ToolsetInfo.LatestMacRuntimeIdentifier}-x64" };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("configuration") &&
+ e.Properties["configuration"] == Sha256Hasher.Hash("DEBUG") &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("BUILD"));
+
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("runtime") &&
+ e.Properties["runtime"] == Sha256Hasher.Hash($"{ToolsetInfo.LatestMacRuntimeIdentifier.ToUpper()}-X64") &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("BUILD"));
+ }
+
+ [Fact]
+ public void DotnetRunCleanTestCommandOpinionsShouldBeSentToTelemetryWhenThereIsMultipleOption()
+ {
+ string[] args = { "clean", "--configuration", "Debug", "--framework", ToolsetInfo.CurrentTargetFramework };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("configuration") &&
+ e.Properties["configuration"] == Sha256Hasher.Hash("DEBUG") &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("CLEAN"));
+
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("framework") &&
+ e.Properties["framework"] == Sha256Hasher.Hash(ToolsetInfo.CurrentTargetFramework.ToUpper()) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("CLEAN"));
+ }
+
+ [Fact]
+ public void DotnetUpdatePackageVulnerableOptionShouldBeSentToTelemetry()
+ {
+ const string optionKey = "vulnerable";
+ string[] args = { "package", "update", "--vulnerable" };
+ Cli.Program.ProcessArgs(args);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey(optionKey) &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("PACKAGE UPDATE"));
+ }
+
+ [WindowsOnlyFact]
+ public void InternalreportinstallsuccessCommandCollectExeNameWithEventname()
+ {
+ FakeRecordEventNameTelemetry fakeTelemetry = new();
+ string[] args = { "c:\\mypath\\dotnet-sdk-latest-win-x64.exe" };
+
+ InternalReportInstallSuccessCommand.ProcessInputAndSendTelemetry(args, fakeTelemetry);
+
+ fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "install/reportsuccess" && e.Properties.ContainsKey("exeName") &&
+ e.Properties["exeName"] == Sha256Hasher.Hash("DOTNET-SDK-LATEST-WIN-X64.EXE"));
+ }
+
+ [Fact]
+ public void ExceptionShouldBeSentToTelemetry()
+ {
+ Exception caughtException = null;
+ try
+ {
+ string[] args = { "build" };
+ Cli.Program.ProcessArgs(args);
+ throw new ArgumentException("test exception");
+ }
+ catch (Exception ex)
+ {
+ caughtException = ex;
+ TelemetryEventEntry.SendFiltered(ex);
+ }
+
+ var exception = new Exception();
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "mainCatchException/exception" &&
+ e.Properties.ContainsKey("exceptionType") &&
+ e.Properties["exceptionType"] == "System.ArgumentException" &&
+ e.Properties.ContainsKey("detail") &&
+ e.Properties["detail"].Contains(caughtException.StackTrace));
+ }
+ }
+}
diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs
new file mode 100644
index 000000000000..5939d8ffeb2c
--- /dev/null
+++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs
@@ -0,0 +1,313 @@
+// 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.Cli.Telemetry;
+using Microsoft.DotNet.Configurer;
+
+namespace Microsoft.DotNet.Tests
+{
+ public class TelemetryCommonPropertiesTests : SdkTest
+ {
+ public TelemetryCommonPropertiesTests(ITestOutputHelper log) : base(log)
+ {
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldContainIfItIsInDockerOrNot()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId").Should().ContainKey("Docker Container");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldReturnHashedPath()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(() => "ADirectory", userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Current Path Hash"].Should().NotBe("ADirectory");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldReturnHashedMachineId()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => "plaintext", userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID"].Should().NotBe("plaintext");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldReturnDevDeviceId()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getDeviceId: () => "plaintext", userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"].Should().Be("plaintext");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddress()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID"];
+
+ Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldEnsureDevDeviceIDIsCached()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache());
+ var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"];
+
+ Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid");
+ var secondAssignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"];
+
+ Guid.TryParse(secondAssignedMachineId, out var _).Should().BeTrue("it should be a guid");
+ secondAssignedMachineId.Should().Be(assignedMachineId, "it should match the previously assigned guid");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldReturnHashedMachineIdOld()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => "plaintext", userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID Old"].Should().NotBe("plaintext");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddressOld()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID Old"];
+
+ Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldReturnIsOutputRedirected()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Output Redirected"].Should().BeOneOf("True", "False");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldReturnIsCIDetection()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Continuous Integration"].Should().BeOneOf("True", "False");
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldContainKernelVersion()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Kernel Version"].Should().Be(RuntimeInformation.OSDescription);
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldContainArchitectureInformation()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["OS Architecture"].Should().Be(RuntimeInformation.OSArchitecture.ToString());
+ }
+
+ [WindowsOnlyFact]
+ public void TelemetryCommonPropertiesShouldContainWindowsInstallType()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Installation Type"].Should().NotBeEmpty();
+ }
+
+ [UnixOnlyFact]
+ public void TelemetryCommonPropertiesShouldContainEmptyWindowsInstallType()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Installation Type"].Should().BeEmpty();
+ }
+
+ [WindowsOnlyFact]
+ public void TelemetryCommonPropertiesShouldContainWindowsProductType()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Product Type"].Should().NotBeEmpty();
+ }
+
+ [UnixOnlyFact]
+ public void TelemetryCommonPropertiesShouldContainEmptyWindowsProductType()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Product Type"].Should().BeEmpty();
+ }
+
+ [WindowsOnlyFact]
+ public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().BeEmpty();
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().BeEmpty();
+ }
+
+ [MacOsOnlyFact]
+ public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion2()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().BeEmpty();
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().BeEmpty();
+ }
+
+ [LinuxOnlyFact]
+ public void TelemetryCommonPropertiesShouldContainLibcReleaseAndVersion()
+ {
+ if (!RuntimeInformation.RuntimeIdentifier.Contains("alpine", StringComparison.OrdinalIgnoreCase))
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().NotBeEmpty();
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().NotBeEmpty();
+ }
+ }
+
+ [Fact]
+ public void TelemetryCommonPropertiesShouldReturnIsLLMDetection()
+ {
+ var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
+ unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["llm"].Should().BeOneOf("claude", null);
+ }
+
+ [Theory]
+ [MemberData(nameof(CITelemetryTestCases))]
+ public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool expected)
+ {
+ try
+ {
+ foreach (var (key, value) in envVars)
+ {
+ Environment.SetEnvironmentVariable(key, value);
+ }
+ new CIEnvironmentDetectorForTelemetry().IsCIEnvironment().Should().Be(expected);
+ }
+ finally
+ {
+ foreach (var (key, value) in envVars)
+ {
+ Environment.SetEnvironmentVariable(key, null);
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(LLMTelemetryTestCases))]
+ public void CanDetectLLMStatusForEnvVars(Dictionary? envVars, string? expected)
+ {
+ try
+ {
+ if (envVars is not null)
+ {
+ foreach (var (key, value) in envVars)
+ {
+ Environment.SetEnvironmentVariable(key, value);
+ }
+ }
+ new LLMEnvironmentDetectorForTelemetry().GetLLMEnvironment().Should().Be(expected);
+ }
+ finally
+ {
+ if (envVars is not null)
+ {
+ foreach (var (key, value) in envVars)
+ {
+ Environment.SetEnvironmentVariable(key, null);
+ }
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData("dummySessionId")]
+ [InlineData(null)]
+ public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId)
+ {
+ var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache());
+ var commonProperties = unitUnderTest.GetTelemetryCommonProperties(sessionId);
+
+ commonProperties.Should().ContainKey("SessionId");
+ commonProperties["SessionId"].Should().Be(sessionId);
+ }
+
+
+ public static TheoryData?, string?> LLMTelemetryTestCases => new()
+ {
+ { new Dictionary { {"CLAUDECODE", "1" } }, "claude" },
+ { new Dictionary { {"CLAUDE_CODE_ENTRYPOINT", "some_value" } }, "claude" },
+ { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" },
+ { new Dictionary { { "CURSOR_AI", "1" } }, "cursor" },
+ { new Dictionary { { "GEMINI_CLI", "true" } }, "gemini" },
+ { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" },
+ { new Dictionary { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot" },
+ { new Dictionary { { "CODEX_CLI", "1" } }, "codex" },
+ { new Dictionary { { "CODEX_SANDBOX", "1" } }, "codex" },
+ { new Dictionary { { "OR_APP_NAME", "Aider" } }, "aider" },
+ { new Dictionary { { "OR_APP_NAME", "aider" } }, "aider" },
+ { new Dictionary { { "OR_APP_NAME", "plandex" } }, "plandex" },
+ { new Dictionary { { "OR_APP_NAME", "Plandex" } }, "plandex" },
+ { new Dictionary { { "AMP_HOME", "/path/to/amp" } }, "amp" },
+ { new Dictionary { { "QWEN_CODE", "1" } }, "qwen" },
+ { new Dictionary { { "DROID_CLI", "true" } }, "droid" },
+ { new Dictionary { { "OPENCODE_AI", "1" } }, "opencode" },
+ { new Dictionary { { "ZED_ENVIRONMENT", "1" } }, "zed" },
+ { new Dictionary { { "ZED_TERM", "1" } }, "zed" },
+ { new Dictionary { { "KIMI_CLI", "true" } }, "kimi" },
+ { new Dictionary { { "OR_APP_NAME", "OpenHands" } }, "openhands" },
+ { new Dictionary { { "OR_APP_NAME", "openhands" } }, "openhands" },
+ { new Dictionary { { "GOOSE_TERMINAL", "1" } }, "goose" },
+ { new Dictionary { { "CLINE_TASK_ID", "task123" } }, "cline" },
+ { new Dictionary { { "ROO_CODE_TASK_ID", "task456" } }, "roo" },
+ { new Dictionary { { "WINDSURF_SESSION", "session789" } }, "windsurf" },
+ { new Dictionary { { "AGENT_CLI", "true" } }, "generic_agent" },
+ // Test combinations of older tools
+ { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" },
+ { new Dictionary { { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" } }, "gemini, copilot" },
+ { new Dictionary { { "CLAUDECODE", "1" }, { "GEMINI_CLI", "true" }, { "AGENT_CLI", "true" } }, "claude, gemini, generic_agent" },
+ { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" }, { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" }, { "AGENT_CLI", "true" } }, "claude, cursor, gemini, copilot, generic_agent" },
+ // Test combinations of newer tools
+ { new Dictionary { { "OR_APP_NAME", "Aider" }, { "CLINE_TASK_ID", "task123" } }, "aider, cline" },
+ { new Dictionary { { "CODEX_CLI", "1" }, { "WINDSURF_SESSION", "session789" } }, "codex, windsurf" },
+ { new Dictionary { { "GOOSE_TERMINAL", "1" }, { "ROO_CODE_TASK_ID", "task456" } }, "goose, roo" },
+ { new Dictionary { { "GEMINI_CLI", "false" } }, null },
+ { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "false" } }, null },
+ { new Dictionary { { "AGENT_CLI", "false" } }, null },
+ { new Dictionary { { "DROID_CLI", "false" } }, null },
+ { new Dictionary { { "KIMI_CLI", "false" } }, null },
+ { new Dictionary { { "OR_APP_NAME", "SomeOtherApp" } }, null },
+ { new Dictionary(), null },
+ };
+
+ public static TheoryData, bool> CITelemetryTestCases => new()
+ {
+ { new Dictionary { { "TF_BUILD", "true" } }, true },
+ { new Dictionary { { "GITHUB_ACTIONS", "true" } }, true },
+ { new Dictionary { { "APPVEYOR", "true"} }, true },
+ { new Dictionary { { "CI", "true"} }, true },
+ { new Dictionary { { "TRAVIS", "true"} }, true },
+ { new Dictionary { { "CIRCLECI", "true"} }, true },
+ { new Dictionary { { "CODEBUILD_BUILD_ID", "hi" }, { "AWS_REGION", "hi" } }, true },
+ { new Dictionary { { "CODEBUILD_BUILD_ID", "hi" } }, false },
+ { new Dictionary { { "BUILD_ID", "hi" }, { "BUILD_URL", "hi" } }, true },
+ { new Dictionary { { "BUILD_ID", "hi" } }, false },
+ { new Dictionary { { "BUILD_ID", "hi" }, { "PROJECT_ID", "hi" } }, true },
+ { new Dictionary { { "BUILD_ID", "hi" } }, false },
+ { new Dictionary { { "TEAMCITY_VERSION", "hi" } }, true },
+ { new Dictionary { { "TEAMCITY_VERSION", "" } }, false },
+ { new Dictionary { { "JB_SPACE_API_URL", "hi" } }, true },
+ { new Dictionary { { "JB_SPACE_API_URL", "" } }, false },
+ { new Dictionary { { "SomethingElse", "hi" } }, false },
+ };
+
+ private class NothingCache : IUserLevelCacheWriter
+ {
+ public string RunWithCache(string cacheKey, Func getValueToCache)
+ {
+ return getValueToCache();
+ }
+
+ public string RunWithCacheInFilePath(string cacheFilepath, Func getValueToCache)
+ {
+ return getValueToCache();
+ }
+ }
+ }
+}
diff --git a/test/dotnet.Tests/TelemetryFilterTest.cs b/test/dotnet.Tests/TelemetryFilterTest.cs
new file mode 100644
index 000000000000..db5f835e791d
--- /dev/null
+++ b/test/dotnet.Tests/TelemetryFilterTest.cs
@@ -0,0 +1,211 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using Microsoft.DotNet.Cli.Telemetry;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.DotNet.Utilities;
+using Parser = Microsoft.DotNet.Cli.Parser;
+
+namespace Microsoft.DotNet.Tests
+{
+ ///
+ /// Only adding the performance data tests for now as the TelemetryCommandTests cover most other scenarios already
+ ///
+ public class TelemetryFilterTests : SdkTest
+ {
+ private readonly FakeRecordEventNameTelemetry _fakeTelemetry;
+
+ public string EventName { get; set; }
+
+ public IDictionary Properties { get; set; }
+
+ public TelemetryFilterTests(ITestOutputHelper log) : base(log)
+ {
+ _fakeTelemetry = new FakeRecordEventNameTelemetry();
+ TelemetryEventEntry.Subscribe(_fakeTelemetry.TrackEvent);
+ TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);
+ }
+
+ [Fact]
+ public void TopLevelCommandNameShouldBeSentToTelemetryWithoutPerformanceData()
+ {
+ var parseResult = Parser.Parse(["build"]);
+ TelemetryEventEntry.SendFiltered(parseResult);
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("BUILD") &&
+ e.Measurement == null);
+ }
+
+ [Fact]
+ public void TopLevelCommandNameShouldBeSentToTelemetryWithPerformanceData()
+ {
+ var parseResult = Parser.Parse(["build"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 12345 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("BUILD") &&
+ e.Measurement.ContainsKey("Startup Time") &&
+ e.Measurement["Startup Time"] == 12345);
+ }
+
+ [Fact]
+ public void TopLevelCommandNameShouldBeSentToTelemetryWithGlobalJsonState()
+ {
+ string globalJsonState = "invalid_data";
+ var parseResult = Parser.Parse(["build"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary(), globalJsonState));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("BUILD") &&
+ e.Measurement == null &&
+ e.Properties.ContainsKey("globalJson") &&
+ e.Properties["globalJson"] == Sha256Hasher.HashWithNormalizedCasing(globalJsonState));
+ }
+
+ [Fact]
+ public void TopLevelCommandNameShouldBeSentToTelemetryWithZeroPerformanceData()
+ {
+ var parseResult = Parser.Parse(["build"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 0 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("BUILD") &&
+ e.Measurement == null);
+ }
+
+ [Fact]
+ public void TopLevelCommandNameShouldBeSentToTelemetryWithSomeZeroPerformanceData()
+ {
+ var parseResult = Parser.Parse(["build"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("BUILD") &&
+ !e.Measurement.ContainsKey("Startup Time") &&
+ e.Measurement.ContainsKey("Parse Time") &&
+ e.Measurement["Parse Time"] == 23456);
+ }
+
+ [Fact]
+ public void SubLevelCommandNameShouldBeSentToTelemetryWithoutPerformanceData()
+ {
+ var parseResult = Parser.Parse(["new", "console"]);
+ TelemetryEventEntry.SendFiltered(parseResult);
+ _fakeTelemetry
+ .LogEntries.Should()
+ .Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("NEW") &&
+ e.Measurement == null);
+ }
+
+ [Fact]
+ public void SubLevelCommandNameShouldBeSentToTelemetryWithPerformanceData()
+ {
+ var parseResult = Parser.Parse(["new", "console"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 34567 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("NEW") &&
+ e.Measurement.ContainsKey("Startup Time") &&
+ e.Measurement["Startup Time"] == 34567);
+ }
+
+ [Fact]
+ public void SubLevelCommandNameShouldBeSentToTelemetryWithZeroPerformanceData()
+ {
+ var parseResult = Parser.Parse(["new", "console"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 0 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("NEW") &&
+ e.Measurement == null);
+ }
+
+ [Fact]
+ public void SubLevelCommandNameShouldBeSentToTelemetryWithSomeZeroPerformanceData()
+ {
+ var parseResult = Parser.Parse(["new", "console"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 45678 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("argument") &&
+ e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("NEW") &&
+ !e.Measurement.ContainsKey("Startup Time") &&
+ e.Measurement.ContainsKey("Parse Time") &&
+ e.Measurement["Parse Time"] == 45678);
+ }
+
+ [Fact]
+ public void WorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry()
+ {
+ var parseResult =
+ Parser.Parse(["workload", "install", "microsoft-ios-sdk-full"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult,
+ new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") &&
+ e.Properties["subcommand"] ==
+ Sha256Hasher.Hash("INSTALL") &&
+ e.Properties["argument"] ==
+ Sha256Hasher.Hash("MICROSOFT-IOS-SDK-FULL"));
+ }
+
+ [Fact]
+ public void ToolsSubLevelCommandNameAndArgumentShouldBeSentToTelemetry()
+ {
+ var parseResult =
+ Parser.Parse(["tool", "install", "dotnet-format"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult,
+ new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("TOOL") &&
+ e.Properties["subcommand"] ==
+ Sha256Hasher.Hash("INSTALL") &&
+ e.Properties["argument"] ==
+ Sha256Hasher.Hash("DOTNET-FORMAT"));
+ }
+
+ [Fact]
+ public void WhenCalledWithDiagnosticWorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry()
+ {
+ var parseResult =
+ Parser.Parse(["-d", "workload", "install", "microsoft-ios-sdk-full"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult,
+ new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") &&
+ e.Properties["subcommand"] ==
+ Sha256Hasher.Hash("INSTALL") &&
+ e.Properties["argument"] ==
+ Sha256Hasher.Hash("MICROSOFT-IOS-SDK-FULL"));
+ }
+
+ [Fact]
+ public void WhenCalledWithMissingArgumentWorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry()
+ {
+ var parseResult =
+ Parser.Parse(["-d", "workload", "install"]);
+ TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult,
+ new Dictionary() { { "Startup Time", 0 }, { "Parse Time", 23456 } }));
+ _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
+ e.Properties.ContainsKey("verb") &&
+ e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") &&
+ e.Properties["subcommand"] ==
+ Sha256Hasher.Hash("INSTALL"));
+ }
+ }
+}
diff --git a/test/dotnet.Tests/TelemetryTests/FakeRecordEventNameTelemetry.cs b/test/dotnet.Tests/TelemetryTests/FakeRecordEventNameTelemetry.cs
deleted file mode 100644
index 277e89826a8d..000000000000
--- a/test/dotnet.Tests/TelemetryTests/FakeRecordEventNameTelemetry.cs
+++ /dev/null
@@ -1,31 +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 Microsoft.DotNet.Cli.Telemetry;
-
-namespace Microsoft.DotNet.Tests.TelemetryTests;
-
-public class FakeRecordEventNameTelemetry : ITelemetryClient
-{
- public bool Enabled { get; set; }
-
- public string? EventName { get; set; }
-
- public void TrackEvent(string eventName, IDictionary? properties)
- {
- LogEntries.Add(new LogEntry
- {
- EventName = eventName,
- Properties = properties ?? new Dictionary()
- });
- }
-
- public ConcurrentBag LogEntries { get; set; } = [];
-
- public class LogEntry
- {
- public string? EventName { get; set; }
- public IDictionary Properties { get; set; } = new Dictionary();
- }
-}
diff --git a/test/dotnet.Tests/TelemetryTests/SenderTests.cs b/test/dotnet.Tests/TelemetryTests/SenderTests.cs
new file mode 100644
index 000000000000..6d6a4983ea23
--- /dev/null
+++ b/test/dotnet.Tests/TelemetryTests/SenderTests.cs
@@ -0,0 +1,184 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Net;
+using System.Runtime.CompilerServices;
+using Moq;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel.Tests
+{
+ public class SenderTests : SdkTest
+ {
+ private int _deleteCount;
+
+ private Mock TransmissionMock { get; }
+
+ private Mock StorageBaseMock { get; }
+
+ public SenderTests(ITestOutputHelper log) : base(log)
+ {
+ StorageBaseMock = new Mock();
+ TransmissionMock = new Mock(string.Empty, new Uri("http://some/url"), new byte[] { },
+ string.Empty, string.Empty);
+ _deleteCount = 0;
+ StorageBaseMock.Setup(storage => storage.Delete(It.IsAny()))
+ .Callback(() => _deleteCount++);
+ }
+
+ [Fact]
+ public void WhenServerReturn503TransmissionWillBeRetried()
+ {
+ var Sender = GetSenderUnderTest();
+ int peekCounts = 0;
+
+ // Setup transmission.SendAsync() to throw WebException that has 503 status Code
+ TransmissionMock.Setup(transmission => transmission.SendAsync())
+ .Throws(GenerateWebException((HttpStatusCode)503));
+
+ // Setup Storage.Peek() to return the mocked transmission, and stop the loop after 10 peeks.
+ StorageBaseMock.Setup(storage => storage.Peek())
+ .Returns(TransmissionMock.Object)
+ .Callback(() =>
+ {
+ if (peekCounts++ == 10)
+ {
+ Sender.StopAsync();
+ }
+ });
+
+ // Act
+ Sender.SendLoop();
+ _deleteCount.Should().Be(0,
+ "delete is not expected to be called on 503, request is expected to be send forever.");
+ }
+
+ [Fact]
+ public void WhenServerReturn400IntervalWillBe10Seconds()
+ {
+ var Sender = GetSenderUnderTest();
+ int peekCounts = 0;
+
+ // Setup transmission.SendAsync() to throw WebException that has 400 status Code
+ TransmissionMock.Setup(transmission => transmission.SendAsync())
+ .Throws(GenerateWebException((HttpStatusCode)400));
+
+ // Setup Storage.Peek() to return the mocked transmission, and stop the loop after 10 peeks.
+ StorageBaseMock.Setup(storage => storage.Peek())
+ .Returns(TransmissionMock.Object)
+ .Callback(() =>
+ {
+ if (peekCounts++ == 10)
+ {
+ Sender.StopAsync();
+ }
+ });
+
+ // Cache the interval (it is a parameter passed to the Send method).
+ TimeSpan intervalOnSixIteration = TimeSpan.Zero;
+ Sender.OnSend = interval => intervalOnSixIteration = interval;
+
+ // Act
+ Sender.SendLoop();
+
+ intervalOnSixIteration.TotalSeconds.Should().Be(5);
+ _deleteCount.Should().Be(10, "400 should not be retried so delete should always be called.");
+ }
+
+ [Fact]
+ public void DisposeDoesNotThrow()
+ {
+ new Sender(StorageBaseMock.Object,
+ new PersistenceTransmitter(
+ CreateStorageService(),
+ 3))
+ .Dispose();
+ }
+
+ [Fact]
+ public void WhenServerReturnDnsErrorRequestWillBeRetried()
+ {
+ var Sender = GetSenderUnderTest();
+ int peekCounts = 0;
+
+ // Setup transmission.SendAsync() to throw WebException with ProxyNameResolutionFailure failure
+ WebException webException = new(
+ string.Empty,
+ new Exception(),
+ WebExceptionStatus.ProxyNameResolutionFailure,
+ null);
+ TransmissionMock.Setup(transmission => transmission.SendAsync()).Throws(webException);
+
+ // Setup Storage.Peek() to return the mocked transmission, and stop the loop after 10 peeks.
+ StorageBaseMock.Setup(storage => storage.Peek())
+ .Returns(TransmissionMock.Object)
+ .Callback(() =>
+ {
+ if (peekCounts++ == 10)
+ {
+ Sender.StopAsync();
+ }
+ });
+
+ // Act
+ Sender.SendLoop();
+
+ _deleteCount.Should().Be(0,
+ "delete is not expected to be called on Dns errors since it , request is expected to be retried forever.");
+ }
+
+ private WebException GenerateWebException(HttpStatusCode httpStatusCode)
+ {
+ Mock httpWebResponse = new();
+ httpWebResponse.SetupGet(webResponse => webResponse.StatusCode).Returns(httpStatusCode);
+
+ WebException webException = new(string.Empty, new Exception(), WebExceptionStatus.SendFailure,
+ httpWebResponse.Object);
+
+ return webException;
+ }
+
+ ///
+ /// A class that inherits from Sender, to expose its protected methods.
+ ///
+ internal class SenderUnderTest : Sender
+ {
+ internal Action OnSend = nextSendInterval => { };
+
+ internal SenderUnderTest(BaseStorageService storage, PersistenceTransmitter transmitter)
+ : base(storage, transmitter, false)
+ {
+ }
+
+ internal AutoResetEvent IntervalAutoResetEvent => DelayHandler;
+
+ internal new void SendLoop()
+ {
+ base.SendLoop();
+ }
+
+ protected override bool Send(StorageTransmission transmission, ref TimeSpan nextSendInterval)
+ {
+ OnSend(nextSendInterval);
+ DelayHandler.Set();
+ return base.Send(transmission, ref nextSendInterval);
+ }
+ }
+
+ private StorageService CreateStorageService([CallerMemberName] string testName = null)
+ {
+ string tempPath = Path.Combine(TestAssetsManager.CreateTestDirectory("TestStorageService", identifier: testName).Path, Path.GetTempFileName());
+ StorageService storageService = new();
+ storageService.Init(tempPath);
+ return storageService;
+ }
+
+ private SenderUnderTest GetSenderUnderTest([CallerMemberName] string testName = null)
+ {
+ StorageService storageService = CreateStorageService(testName);
+ PersistenceTransmitter transmitter = new(storageService, 0);
+ return new SenderUnderTest(StorageBaseMock.Object, transmitter);
+ }
+ }
+}
diff --git a/test/dotnet.Tests/TelemetryTests/StorageTests.cs b/test/dotnet.Tests/TelemetryTests/StorageTests.cs
new file mode 100644
index 000000000000..bd56db879b8d
--- /dev/null
+++ b/test/dotnet.Tests/TelemetryTests/StorageTests.cs
@@ -0,0 +1,202 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Runtime.CompilerServices;
+using Microsoft.ApplicationInsights.Channel;
+using Microsoft.ApplicationInsights.DataContracts;
+using Microsoft.ApplicationInsights.Extensibility.Implementation;
+using IChannelTelemetry = Microsoft.ApplicationInsights.Channel.ITelemetry;
+
+namespace Microsoft.DotNet.Cli.Telemetry.PersistenceChannel.Tests
+{
+ ///
+ /// Tests for Storage.
+ ///
+ ///
+ /// To reduce complexity, there was a design decision to make Storage the file system abstraction layer.
+ /// That means that Storage knows about the file system types (e.g. IStorageFile or FileInfo).
+ /// Those types are not easy to mock (even IStorageFile is using extension methods that makes it very hard to mock).
+ /// Therefore those UnitTests just doesn't mock the file system. Every unit test in
+ /// reads and writes files to/from the disk.
+ ///
+ public class StorageTests : SdkTest
+ {
+ public StorageTests(ITestOutputHelper log) : base(log)
+ {
+ }
+
+ [Fact]
+ public async Task EnqueuedContentIsEqualToPeekedContent()
+ {
+ // Setup
+ StorageService storage = new();
+ storage.Init(GetTemporaryPath());
+ Transmission transmissionToEnqueue = CreateTransmission(new TraceTelemetry("mock_item"));
+
+ // Act
+ await storage.EnqueueAsync(transmissionToEnqueue);
+ StorageTransmission peekedTransmission = storage.Peek();
+
+ // Asserts
+ string enqueuedContent =
+ Encoding.UTF8.GetString(transmissionToEnqueue.Content, 0, transmissionToEnqueue.Content.Length);
+ string peekedContent =
+ Encoding.UTF8.GetString(peekedTransmission.Content, 0, peekedTransmission.Content.Length);
+ enqueuedContent.Should().Be(peekedContent);
+ }
+
+ [Fact]
+ public void DeletedItemIsNotReturnedInCallsToPeek()
+ {
+ // Setup - create a storage with one item
+ StorageService storage = new();
+ storage.Init(GetTemporaryPath());
+ Transmission transmissionToEnqueue = CreateTransmissionAndEnqueueIt(storage);
+
+ // Act
+ StorageTransmission firstPeekedTransmission;
+
+ // if item is not disposed,peek will not return it (regardless of the call to delete).
+ // So for this test to actually test something, using 'using' is required.
+ using (firstPeekedTransmission = storage.Peek())
+ {
+ storage.Delete(firstPeekedTransmission);
+ }
+
+ StorageTransmission secondPeekedTransmission = storage.Peek();
+
+ // Asserts
+ firstPeekedTransmission.Should().NotBeNull();
+ secondPeekedTransmission.Should().BeNull();
+ }
+
+ [Fact]
+ public void PeekedItemIsOnlyReturnedOnce()
+ {
+ // Setup - create a storage with one item
+ StorageService storage = new();
+ storage.Init(GetTemporaryPath());
+
+ Transmission transmissionToEnqueue = CreateTransmissionAndEnqueueIt(storage);
+
+ // Act
+ StorageTransmission firstPeekedTransmission = storage.Peek();
+ StorageTransmission secondPeekedTransmission = storage.Peek();
+
+ // Asserts
+ firstPeekedTransmission.Should().NotBeNull();
+ secondPeekedTransmission.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task PeekedItemIsReturnedAgainAfterTheItemInTheFirstCallToPeekIsDisposed()
+ {
+ // Setup - create a storage with one item
+ StorageService storage = new();
+ storage.Init(GetTemporaryPath());
+
+ Transmission transmissionToEnqueue = CreateTransmission(new TraceTelemetry("mock_item"));
+ await storage.EnqueueAsync(transmissionToEnqueue);
+
+ // Act
+ StorageTransmission firstPeekedTransmission;
+ using (firstPeekedTransmission = storage.Peek())
+ {
+ }
+
+ StorageTransmission secondPeekedTransmission = storage.Peek();
+
+ // Asserts
+ firstPeekedTransmission.Should().NotBeNull();
+ secondPeekedTransmission.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void WhenStorageHasTwoItemsThenTwoCallsToPeekReturns2DifferentItems()
+ {
+ // Setup - create a storage with 2 items
+ StorageService storage = new();
+ storage.Init(GetTemporaryPath());
+
+ Transmission firstTransmission = CreateTransmissionAndEnqueueIt(storage);
+ Transmission secondTransmission = CreateTransmissionAndEnqueueIt(storage);
+
+ // Act
+ StorageTransmission firstPeekedTransmission = storage.Peek();
+ StorageTransmission secondPeekedTransmission = storage.Peek();
+
+ // Asserts
+ firstPeekedTransmission.Should().NotBeNull();
+ secondPeekedTransmission.Should().NotBeNull();
+
+ string first = Encoding.UTF8.GetString(firstPeekedTransmission.Content, 0,
+ firstPeekedTransmission.Content.Length);
+ string second = Encoding.UTF8.GetString(secondPeekedTransmission.Content, 0,
+ secondPeekedTransmission.Content.Length);
+ first.Should().NotBe(second);
+ }
+
+ [Fact]
+ public void WhenMaxFilesIsOneThenSecondTransmissionIsDropped()
+ {
+ // Setup
+ StorageService storage = new();
+ storage.Init(GetTemporaryPath());
+
+ storage.MaxFiles = 1;
+
+ // Act - Enqueue twice
+ CreateTransmissionAndEnqueueIt(storage);
+ CreateTransmissionAndEnqueueIt(storage);
+
+ // Asserts - Second Peek should be null
+ storage.Peek().Should().NotBeNull();
+ storage.Peek().Should().BeNull();
+ }
+
+ [Fact]
+ public void WhenMaxSizeIsReachedThenEnqueuedTransmissionsAreDropped()
+ {
+ // Setup - create a storage with 2 items
+ StorageService storage = new();
+ storage.Init(GetTemporaryPath());
+
+ storage.CapacityInBytes = 200; // Each file enqueued in CreateTransmissionAndEnqueueIt is ~300 bytes.
+
+ // Act - Enqueue twice
+ CreateTransmissionAndEnqueueIt(storage);
+ CreateTransmissionAndEnqueueIt(storage);
+
+ // Asserts - Second Peek should be null
+ storage.Peek().Should().NotBeNull();
+ storage.Peek().Should().BeNull();
+ }
+
+ private static Transmission CreateTransmission(IChannelTelemetry telemetry)
+ {
+ byte[] data = JsonSerializer.Serialize(new[] { telemetry });
+ Transmission transmission = new(
+ new Uri(@"http://some.url"),
+ data,
+ "application/x-json-stream",
+ JsonSerializer.CompressionType);
+
+ return transmission;
+ }
+
+ private static Transmission CreateTransmissionAndEnqueueIt(StorageService storage)
+ {
+ Transmission firstTransmission = CreateTransmission(new TraceTelemetry(Guid.NewGuid().ToString()));
+ storage.EnqueueAsync(firstTransmission).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ return firstTransmission;
+ }
+
+ private string GetTemporaryPath([CallerMemberName] string callingMethod = null)
+ {
+ return TestAssetsManager.CreateTestDirectory(callingMethod).Path;
+ }
+ }
+}
diff --git a/test/dotnet.Tests/TelemetryTests/TelemetryClientTests.cs b/test/dotnet.Tests/TelemetryTests/TelemetryClientTests.cs
deleted file mode 100644
index eb64306bfa69..000000000000
--- a/test/dotnet.Tests/TelemetryTests/TelemetryClientTests.cs
+++ /dev/null
@@ -1,67 +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.Text.Json.Nodes;
-
-namespace Microsoft.DotNet.Tests.TelemetryTests;
-
-public class TelemetryClientTests(ITestOutputHelper log) : SdkTest(log)
-{
- public static TheoryData CommandsWithExitCode => new()
- {
- { new[] { "--help" }, "0" },
- { new[] { "--info" }, "0" },
- { new[] { "workload", "list" }, "0" },
- { new[] { "sdk", "check" }, "0" },
- { new[] { "build-server", "shutdown" }, "0" },
- { new[] { "solution", "list" }, "1" },
- { new[] { "clean" }, "1" },
- { new[] { "run" }, "1" },
- { new[] { "new", "details" }, "127" }
- };
-
- // Only runs on Windows because OTel libraries are only referenced on Windows builds.
- // Thus, this test that writes telemetry logs will not work on other platforms.
- [PlatformSpecificTheory(TestPlatforms.Windows)]
- [MemberData(nameof(CommandsWithExitCode))]
- public void ItProcessesTelemetryData(string[] commandArgs, string exitCodeExpected)
- {
- var testDir = TestAssetsManager.CreateTestDirectory().Path;
- var commandString = string.Join(' ', commandArgs);
- var logFile = Path.Combine(testDir, $"TelemLog_{commandString}.json");
-
- new DotnetCommand(Log, commandArgs)
- .WithWorkingDirectory(testDir)
- .WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "false")
- .WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_DISABLE_TRACE_EXPORT", "true")
- .WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_LOG_PATH", logFile)
- .Execute();
-
- var logFileInfo = new FileInfo(logFile);
- logFileInfo.Should().Exist();
-
- var telemetryJson = JsonNode.Parse(logFileInfo.ReadAllText());
- telemetryJson.Should().NotBeNull();
-
- var activities = telemetryJson["activities"]?.AsArray();
- activities.Should().NotBeNull();
-
- var mainOperation = activities.FirstOrDefault(n => n?["operationName"]?.GetValue() == "main");
- mainOperation.Should().NotBeNull();
-
- var displayName = mainOperation["displayName"]?.GetValue();
- displayName.Should().Be($"dotnet {commandString}");
-
- var events = mainOperation["events"]?.AsArray();
- events.Should().NotBeNull();
-
- var finishEvent = events.FirstOrDefault(n => n?["name"]?.GetValue() == "dotnet/cli/command/finish");
- finishEvent.Should().NotBeNull();
-
- var tags = finishEvent["tags"];
- tags.Should().NotBeNull();
-
- var exitCode = tags["exitCode"]?.GetValue();
- exitCode.Should().Be(exitCodeExpected);
- }
-}
diff --git a/test/dotnet.Tests/TelemetryTests/TelemetryCommandTest.cs b/test/dotnet.Tests/TelemetryTests/TelemetryCommandTest.cs
deleted file mode 100644
index cef125282576..000000000000
--- a/test/dotnet.Tests/TelemetryTests/TelemetryCommandTest.cs
+++ /dev/null
@@ -1,332 +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 Microsoft.DotNet.Cli.Commands.Hidden.InternalReportInstallSuccess;
-using Microsoft.DotNet.Cli.Telemetry;
-using Microsoft.DotNet.Cli.Utils;
-using Microsoft.DotNet.Utilities;
-
-namespace Microsoft.DotNet.Tests.TelemetryTests;
-
-[Collection(TestConstants.UsesStaticTelemetryState)]
-public class TelemetryCommandTests : SdkTest
-{
- private readonly FakeRecordEventNameTelemetry _fakeTelemetry;
-
- public string? EventName { get; set; }
-
- public IDictionary Properties { get; set; } = new Dictionary();
-
- public TelemetryCommandTests(ITestOutputHelper log) : base(log)
- {
- _fakeTelemetry = new FakeRecordEventNameTelemetry();
- TelemetryEventEntry.Subscribe(_fakeTelemetry.TrackEvent);
- TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);
- }
-
- [Fact]
- public void NoTelemetryIfCommandIsInvalid()
- {
- string[] args = { "publish", "-r" };
- Action a = () => { Cli.Program.ProcessArgsAndExecute(args); };
- a.Should().NotThrow();
- }
-
- [Fact]
- public void NoTelemetryIfCommandIsInvalid2()
- {
- string[] args = { "restore", "-v" };
- Action a = () => { Cli.Program.ProcessArgsAndExecute(args); };
- a.Should().NotThrow();
- }
-
- [Fact]
- public void TopLevelCommandNameShouldBeSentToTelemetry()
- {
- string[] args = { "help" };
- Cli.Program.ProcessArgsAndExecute(args);
-
- _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("HELP"));
- }
-
- [Fact]
- public void DotnetNewCommandFirstArgumentShouldBeSentToTelemetry()
- {
- const string argumentToSend = "console";
- string[] args = { "new", argumentToSend };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("argument") &&
- e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("NEW"));
- }
-
- [Fact]
- public void DotnetHelpCommandFirstArgumentShouldBeSentToTelemetry()
- {
- const string argumentToSend = "something";
- string[] args = { "help", argumentToSend };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("argument") &&
- e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("HELP"));
- }
-
- [Fact]
- public void DotnetAddCommandFirstArgumentShouldBeSentToTelemetry()
- {
- const string argumentToSend = "package";
- string[] args = { "add", argumentToSend, "aPackageName" };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("argument") &&
- e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("ADD"));
- }
-
- [Fact]
- public void DotnetAddCommandFirstArgumentShouldBeSentToTelemetry2()
- {
- const string argumentToSend = "reference";
- string[] args = { "add", argumentToSend, "aPackageName" };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("argument") &&
- e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("ADD"));
- }
-
- [Fact]
- public void DotnetRemoveCommandFirstArgumentShouldBeSentToTelemetry()
- {
- const string argumentToSend = "package";
- string[] args = { "remove", argumentToSend, "aPackageName" };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("argument") &&
- e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("REMOVE"));
- }
-
- [Fact]
- public void DotnetListCommandFirstArgumentShouldBeSentToTelemetry()
- {
- const string argumentToSend = "reference";
- string[] args = { "list", argumentToSend, "aPackageName" };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("argument") &&
- e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("LIST"));
- }
-
- [Fact]
- public void DotnetSlnCommandFirstArgumentShouldBeSentToTelemetry()
- {
- const string argumentToSend = "list";
- string[] args = { "sln", "aSolution", argumentToSend };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("argument") &&
- e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("SOLUTION"));
- }
-
- [Fact]
- public void DotnetNugetCommandFirstArgumentShouldBeSentToTelemetry()
- {
- const string argumentToSend = "push";
-
- string[] args = { "nuget", argumentToSend, "path" };
-
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("argument") &&
- e.Properties["argument"] == Sha256Hasher.Hash(argumentToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("NUGET"));
- }
-
- [Fact(Skip = "https://github.com/dotnet/sdk/issues/47862")]
- public void DotnetNewCommandLanguageOpinionShouldBeSentToTelemetry()
- {
- const string optionKey = "language";
- const string optionValueToSend = "c#";
- string[] args = { "new", "console", "--" + optionKey, optionValueToSend };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey(optionKey) &&
- e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("NEW"));
- }
-
- [Fact]
- public void AnyDotnetCommandVerbosityOpinionShouldBeSentToTelemetry()
- {
- const string optionKey = "verbosity";
- const string optionValueToSend = "minimal";
- string[] args = { "restore", "--" + optionKey, optionValueToSend };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey(optionKey) &&
- e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("RESTORE"));
- }
-
- [Fact]
- public void DotnetBuildAndPublishCommandOpinionsShouldBeSentToTelemetry()
- {
- const string optionKey = "configuration";
- const string optionValueToSend = "Debug";
- string[] args = { "build", "--" + optionKey, optionValueToSend };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey(optionKey) &&
- e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("BUILD"));
- }
-
- [Fact]
- public void DotnetPublishCommandRuntimeOpinionsShouldBeSentToTelemetry()
- {
- const string optionKey = "runtime";
- const string optionValueToSend = $"{ToolsetInfo.LatestWinRuntimeIdentifier}-x64";
- string[] args = { "publish", "--" + optionKey, optionValueToSend };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey(optionKey) &&
- e.Properties[optionKey] == Sha256Hasher.Hash(optionValueToSend.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("PUBLISH"));
- }
-
- [Fact]
- public void DotnetBuildAndPublishCommandOpinionsShouldBeSentToTelemetryWhenThereIsMultipleOption()
- {
- string[] args = { "build", "--configuration", "Debug", "--runtime", $"{ToolsetInfo.LatestMacRuntimeIdentifier}-x64" };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("configuration") &&
- e.Properties["configuration"] == Sha256Hasher.Hash("DEBUG") &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("BUILD"));
-
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("runtime") &&
- e.Properties["runtime"] == Sha256Hasher.Hash($"{ToolsetInfo.LatestMacRuntimeIdentifier.ToUpper()}-X64") &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("BUILD"));
- }
-
- [Fact]
- public void DotnetRunCleanTestCommandOpinionsShouldBeSentToTelemetryWhenThereIsMultipleOption()
- {
- string[] args = { "clean", "--configuration", "Debug", "--framework", ToolsetInfo.CurrentTargetFramework };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("configuration") &&
- e.Properties["configuration"] == Sha256Hasher.Hash("DEBUG") &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("CLEAN"));
-
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" && e.Properties.ContainsKey("framework") &&
- e.Properties["framework"] == Sha256Hasher.Hash(ToolsetInfo.CurrentTargetFramework.ToUpper()) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("CLEAN"));
- }
-
- [Fact]
- public void DotnetUpdatePackageVulnerableOptionShouldBeSentToTelemetry()
- {
- const string optionKey = "vulnerable";
- string[] args = { "package", "update", "--vulnerable" };
- Cli.Program.ProcessArgsAndExecute(args);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey(optionKey) &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("PACKAGE UPDATE"));
- }
-
- [WindowsOnlyFact]
- public void InternalreportinstallsuccessCommandCollectExeNameWithEventname()
- {
- FakeRecordEventNameTelemetry fakeTelemetry = new();
- string[] args = { "c:\\mypath\\dotnet-sdk-latest-win-x64.exe" };
-
- InternalReportInstallSuccessCommand.ProcessInputAndSendTelemetry(args, fakeTelemetry);
-
- fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "install/reportsuccess" && e.Properties.ContainsKey("exeName") &&
- e.Properties["exeName"] == Sha256Hasher.Hash("DOTNET-SDK-LATEST-WIN-X64.EXE"));
- }
-
- [Fact]
- public void ExceptionShouldBeSentToTelemetry()
- {
- Exception? caughtException = null;
- try
- {
- string[] args = { "build" };
- Cli.Program.ProcessArgsAndExecute(args);
- throw new ArgumentException("test exception");
- }
- catch (Exception ex)
- {
- caughtException = ex;
- TelemetryEventEntry.SendFiltered(ex);
- }
-
- var stackTrace = caughtException?.StackTrace ?? string.Empty;
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "mainCatchException/exception" &&
- e.Properties.ContainsKey("exceptionType") &&
- e.Properties["exceptionType"] == "System.ArgumentException" &&
- e.Properties.ContainsKey("detail") &&
- e.Properties["detail"] != null &&
- e.Properties["detail"]!.Contains(stackTrace));
- }
-}
diff --git a/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs
deleted file mode 100644
index fb4382e0ae04..000000000000
--- a/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs
+++ /dev/null
@@ -1,311 +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 Microsoft.DotNet.Cli.Telemetry;
-using Microsoft.DotNet.Configurer;
-
-namespace Microsoft.DotNet.Tests.TelemetryTests;
-
-public class TelemetryCommonPropertiesTests : SdkTest
-{
- public TelemetryCommonPropertiesTests(ITestOutputHelper log) : base(log)
- {
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldContainIfItIsInDockerOrNot()
- {
- var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId").Should().ContainKey("Docker Container");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldReturnHashedPath()
- {
- var unitUnderTest = new TelemetryCommonProperties(() => "ADirectory", userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Current Path Hash"].Should().NotBe("ADirectory");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldReturnHashedMachineId()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => "plaintext", userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID"].Should().NotBe("plaintext");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldReturnDevDeviceId()
- {
- var unitUnderTest = new TelemetryCommonProperties(getDeviceId: () => "plaintext", userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"].Should().Be("plaintext");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddress()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID"];
-
- Guid.TryParse((string?)assignedMachineId, out var _).Should().BeTrue("it should be a guid");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldEnsureDevDeviceIDIsCached()
- {
- var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache());
- var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"];
-
- Guid.TryParse((string?)assignedMachineId, out var _).Should().BeTrue("it should be a guid");
- var secondAssignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["devdeviceid"];
-
- Guid.TryParse((string?)secondAssignedMachineId, out var _).Should().BeTrue("it should be a guid");
- secondAssignedMachineId.Should().Be(assignedMachineId, "it should match the previously assigned guid");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldReturnHashedMachineIdOld()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => "plaintext", userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID Old"].Should().NotBe("plaintext");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddressOld()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Machine ID Old"];
-
- Guid.TryParse((string?)assignedMachineId, out var _).Should().BeTrue("it should be a guid");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldReturnIsOutputRedirected()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Output Redirected"].Should().BeOneOf("True", "False");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldReturnIsCIDetection()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Continuous Integration"].Should().BeOneOf("True", "False");
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldContainKernelVersion()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Kernel Version"].Should().Be(RuntimeInformation.OSDescription);
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldContainArchitectureInformation()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["OS Architecture"].Should().Be(RuntimeInformation.OSArchitecture.ToString());
- }
-
- [WindowsOnlyFact]
- public void TelemetryCommonPropertiesShouldContainWindowsInstallType()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Installation Type"].Should().NotBeEmpty();
- }
-
- [UnixOnlyFact]
- public void TelemetryCommonPropertiesShouldContainEmptyWindowsInstallType()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Installation Type"].Should().BeEmpty();
- }
-
- [WindowsOnlyFact]
- public void TelemetryCommonPropertiesShouldContainWindowsProductType()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Product Type"].Should().NotBeEmpty();
- }
-
- [UnixOnlyFact]
- public void TelemetryCommonPropertiesShouldContainEmptyWindowsProductType()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Product Type"].Should().BeEmpty();
- }
-
- [WindowsOnlyFact]
- public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().BeEmpty();
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().BeEmpty();
- }
-
- [MacOsOnlyFact]
- public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion2()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().BeEmpty();
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().BeEmpty();
- }
-
- [LinuxOnlyFact]
- public void TelemetryCommonPropertiesShouldContainLibcReleaseAndVersion()
- {
- if (!RuntimeInformation.RuntimeIdentifier.Contains("alpine", StringComparison.OrdinalIgnoreCase))
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Release"].Should().NotBeEmpty();
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["Libc Version"].Should().NotBeEmpty();
- }
- }
-
- [Fact]
- public void TelemetryCommonPropertiesShouldReturnIsLLMDetection()
- {
- var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache());
- unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["llm"].Should().BeOneOf("claude", null);
- }
-
- [Theory]
- [MemberData(nameof(CITelemetryTestCases))]
- public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool expected)
- {
- try
- {
- foreach (var (key, value) in envVars)
- {
- Environment.SetEnvironmentVariable(key, value);
- }
- new CIEnvironmentDetectorForTelemetry().IsCIEnvironment().Should().Be(expected);
- }
- finally
- {
- foreach (var (key, value) in envVars)
- {
- Environment.SetEnvironmentVariable(key, null);
- }
- }
- }
-
- [Theory]
- [MemberData(nameof(LLMTelemetryTestCases))]
- public void CanDetectLLMStatusForEnvVars(Dictionary? envVars, string? expected)
- {
- try
- {
- if (envVars is not null)
- {
- foreach (var (key, value) in envVars)
- {
- Environment.SetEnvironmentVariable(key, value);
- }
- }
- new LLMEnvironmentDetectorForTelemetry().GetLLMEnvironment().Should().Be(expected);
- }
- finally
- {
- if (envVars is not null)
- {
- foreach (var (key, value) in envVars)
- {
- Environment.SetEnvironmentVariable(key, null);
- }
- }
- }
- }
-
- [Theory]
- [InlineData("dummySessionId")]
- [InlineData(null)]
- public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId)
- {
- var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache());
- var commonProperties = unitUnderTest.GetTelemetryCommonProperties(sessionId);
-
- commonProperties.Should().ContainKey("SessionId");
- commonProperties["SessionId"].Should().Be(sessionId);
- }
-
- public static TheoryData?, string?> LLMTelemetryTestCases => new()
- {
- { new Dictionary { {"CLAUDECODE", "1" } }, "claude" },
- { new Dictionary { {"CLAUDE_CODE_ENTRYPOINT", "some_value" } }, "claude" },
- { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" },
- { new Dictionary { { "CURSOR_AI", "1" } }, "cursor" },
- { new Dictionary { { "GEMINI_CLI", "true" } }, "gemini" },
- { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" },
- { new Dictionary { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot" },
- { new Dictionary { { "CODEX_CLI", "1" } }, "codex" },
- { new Dictionary { { "CODEX_SANDBOX", "1" } }, "codex" },
- { new Dictionary { { "OR_APP_NAME", "Aider" } }, "aider" },
- { new Dictionary { { "OR_APP_NAME", "aider" } }, "aider" },
- { new Dictionary { { "OR_APP_NAME", "plandex" } }, "plandex" },
- { new Dictionary { { "OR_APP_NAME", "Plandex" } }, "plandex" },
- { new Dictionary { { "AMP_HOME", "/path/to/amp" } }, "amp" },
- { new Dictionary { { "QWEN_CODE", "1" } }, "qwen" },
- { new Dictionary { { "DROID_CLI", "true" } }, "droid" },
- { new Dictionary { { "OPENCODE_AI", "1" } }, "opencode" },
- { new Dictionary { { "ZED_ENVIRONMENT", "1" } }, "zed" },
- { new Dictionary { { "ZED_TERM", "1" } }, "zed" },
- { new Dictionary { { "KIMI_CLI", "true" } }, "kimi" },
- { new Dictionary { { "OR_APP_NAME", "OpenHands" } }, "openhands" },
- { new Dictionary { { "OR_APP_NAME", "openhands" } }, "openhands" },
- { new Dictionary { { "GOOSE_TERMINAL", "1" } }, "goose" },
- { new Dictionary { { "CLINE_TASK_ID", "task123" } }, "cline" },
- { new Dictionary { { "ROO_CODE_TASK_ID", "task456" } }, "roo" },
- { new Dictionary { { "WINDSURF_SESSION", "session789" } }, "windsurf" },
- { new Dictionary { { "AGENT_CLI", "true" } }, "generic_agent" },
- // Test combinations of older tools
- { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" },
- { new Dictionary { { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" } }, "gemini, copilot" },
- { new Dictionary { { "CLAUDECODE", "1" }, { "GEMINI_CLI", "true" }, { "AGENT_CLI", "true" } }, "claude, gemini, generic_agent" },
- { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" }, { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" }, { "AGENT_CLI", "true" } }, "claude, cursor, gemini, copilot, generic_agent" },
- // Test combinations of newer tools
- { new Dictionary { { "OR_APP_NAME", "Aider" }, { "CLINE_TASK_ID", "task123" } }, "aider, cline" },
- { new Dictionary { { "CODEX_CLI", "1" }, { "WINDSURF_SESSION", "session789" } }, "codex, windsurf" },
- { new Dictionary { { "GOOSE_TERMINAL", "1" }, { "ROO_CODE_TASK_ID", "task456" } }, "goose, roo" },
- { new Dictionary { { "GEMINI_CLI", "false" } }, null },
- { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "false" } }, null },
- { new Dictionary { { "AGENT_CLI", "false" } }, null },
- { new Dictionary { { "DROID_CLI", "false" } }, null },
- { new Dictionary { { "KIMI_CLI", "false" } }, null },
- { new Dictionary { { "OR_APP_NAME", "SomeOtherApp" } }, null },
- { new Dictionary(), null },
- };
-
- public static TheoryData, bool> CITelemetryTestCases => new()
- {
- { new Dictionary { { "TF_BUILD", "true" } }, true },
- { new Dictionary { { "GITHUB_ACTIONS", "true" } }, true },
- { new Dictionary { { "APPVEYOR", "true"} }, true },
- { new Dictionary { { "CI", "true"} }, true },
- { new Dictionary { { "TRAVIS", "true"} }, true },
- { new Dictionary { { "CIRCLECI", "true"} }, true },
- { new Dictionary { { "CODEBUILD_BUILD_ID", "hi" }, { "AWS_REGION", "hi" } }, true },
- { new Dictionary { { "CODEBUILD_BUILD_ID", "hi" } }, false },
- { new Dictionary { { "BUILD_ID", "hi" }, { "BUILD_URL", "hi" } }, true },
- { new Dictionary { { "BUILD_ID", "hi" } }, false },
- { new Dictionary { { "BUILD_ID", "hi" }, { "PROJECT_ID", "hi" } }, true },
- { new Dictionary { { "BUILD_ID", "hi" } }, false },
- { new Dictionary { { "TEAMCITY_VERSION", "hi" } }, true },
- { new Dictionary { { "TEAMCITY_VERSION", "" } }, false },
- { new Dictionary { { "JB_SPACE_API_URL", "hi" } }, true },
- { new Dictionary { { "JB_SPACE_API_URL", "" } }, false },
- { new Dictionary { { "SomethingElse", "hi" } }, false },
- };
-
- private class NothingCache : IUserLevelCacheWriter
- {
- public string RunWithCache(string cacheKey, Func getValueToCache)
- {
- return getValueToCache();
- }
-
- public string RunWithCacheInFilePath(string cacheFilepath, Func getValueToCache)
- {
- return getValueToCache();
- }
- }
-}
diff --git a/test/dotnet.Tests/TelemetryTests/TelemetryFilterTest.cs b/test/dotnet.Tests/TelemetryTests/TelemetryFilterTest.cs
deleted file mode 100644
index 300f1f90e0e1..000000000000
--- a/test/dotnet.Tests/TelemetryTests/TelemetryFilterTest.cs
+++ /dev/null
@@ -1,123 +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 Microsoft.DotNet.Cli.Telemetry;
-using Microsoft.DotNet.Cli.Utils;
-using Microsoft.DotNet.Utilities;
-using Parser = Microsoft.DotNet.Cli.Parser;
-
-namespace Microsoft.DotNet.Tests.TelemetryTests;
-
-///
-/// Only adding the performance data tests for now as the TelemetryCommandTests cover most other scenarios already
-///
-public class TelemetryFilterTests : SdkTest
-{
- private readonly FakeRecordEventNameTelemetry _fakeTelemetry;
-
- public string? EventName { get; set; }
-
- public IDictionary Properties { get; set; } = new Dictionary();
-
- public TelemetryFilterTests(ITestOutputHelper log) : base(log)
- {
- _fakeTelemetry = new FakeRecordEventNameTelemetry();
- TelemetryEventEntry.Subscribe(_fakeTelemetry.TrackEvent);
- TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);
- }
-
- [Fact]
- public void TopLevelCommandNameShouldBeSentToTelemetry()
- {
- var parseResult = Parser.Parse(["build"]);
- TelemetryEventEntry.SendFiltered(parseResult);
- _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("BUILD"));
- }
-
- [Fact]
- public void TopLevelCommandNameShouldBeSentToTelemetryWithGlobalJsonState()
- {
- string globalJsonState = "invalid_data";
- var parseResult = Parser.Parse(["build"]);
- TelemetryEventEntry.SendFiltered(new ParseResultWithGlobalJsonState(parseResult, globalJsonState));
- _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("BUILD") &&
- e.Properties.ContainsKey("globalJson") &&
- e.Properties["globalJson"] == Sha256Hasher.HashWithNormalizedCasing(globalJsonState));
- }
-
- [Fact]
- public void SubLevelCommandNameShouldBeSentToTelemetry()
- {
- var parseResult = Parser.Parse(["new", "console"]);
- TelemetryEventEntry.SendFiltered(parseResult);
- _fakeTelemetry
- .LogEntries.Should()
- .Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("argument") &&
- e.Properties["argument"] == Sha256Hasher.Hash("CONSOLE") &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("NEW"));
- }
-
- [Fact]
- public void WorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry()
- {
- var parseResult =
- Parser.Parse(["workload", "install", "microsoft-ios-sdk-full"]);
- TelemetryEventEntry.SendFiltered(parseResult);
- _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") &&
- e.Properties["subcommand"] ==
- Sha256Hasher.Hash("INSTALL") &&
- e.Properties["argument"] ==
- Sha256Hasher.Hash("MICROSOFT-IOS-SDK-FULL"));
- }
-
- [Fact]
- public void ToolsSubLevelCommandNameAndArgumentShouldBeSentToTelemetry()
- {
- var parseResult =
- Parser.Parse(["tool", "install", "dotnet-format"]);
- TelemetryEventEntry.SendFiltered(parseResult);
- _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("TOOL") &&
- e.Properties["subcommand"] ==
- Sha256Hasher.Hash("INSTALL") &&
- e.Properties["argument"] ==
- Sha256Hasher.Hash("DOTNET-FORMAT"));
- }
-
- [Fact]
- public void WhenCalledWithDiagnosticWorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry()
- {
- var parseResult =
- Parser.Parse(["-d", "workload", "install", "microsoft-ios-sdk-full"]);
- TelemetryEventEntry.SendFiltered(parseResult);
- _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") &&
- e.Properties["subcommand"] ==
- Sha256Hasher.Hash("INSTALL") &&
- e.Properties["argument"] ==
- Sha256Hasher.Hash("MICROSOFT-IOS-SDK-FULL"));
- }
-
- [Fact]
- public void WhenCalledWithMissingArgumentWorkloadSubLevelCommandNameAndArgumentShouldBeSentToTelemetry()
- {
- var parseResult =
- Parser.Parse(["-d", "workload", "install"]);
- TelemetryEventEntry.SendFiltered(parseResult);
- _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "sublevelparser/command" &&
- e.Properties.ContainsKey("verb") &&
- e.Properties["verb"] == Sha256Hasher.Hash("WORKLOAD") &&
- e.Properties["subcommand"] ==
- Sha256Hasher.Hash("INSTALL"));
- }
-}
diff --git a/test/dotnet.Tests/dotnet.Tests.csproj b/test/dotnet.Tests/dotnet.Tests.csproj
index 658735a93ee6..e5655de02a0f 100644
--- a/test/dotnet.Tests/dotnet.Tests.csproj
+++ b/test/dotnet.Tests/dotnet.Tests.csproj
@@ -89,9 +89,9 @@
-
-
-
+
+
+
diff --git a/test/xunit-runner/XUnitRunner.targets b/test/xunit-runner/XUnitRunner.targets
index dce3fb456e8d..03897ea63aab 100644
--- a/test/xunit-runner/XUnitRunner.targets
+++ b/test/xunit-runner/XUnitRunner.targets
@@ -4,20 +4,12 @@
$(SdkTargetFramework)
$(SdkTargetFramework)
- $(XUnitV3Version)
+ 2.4.1
<_SDKCustomXUnitPublishTargetsPath>$(MSBuildThisFileDirectory)XUnitPublish.targets
-nocolor
-
- <_TestPublishRidProperties Condition="'$(TargetRid)' != ''">RuntimeIdentifier=$(TargetRid);SelfContained=false;ErrorOnDuplicatePublishOutputFiles=false
-
$(ArtifactsBinDir)HelixTasks\$(Configuration)\HelixTasks.dll
@@ -45,7 +37,7 @@
Outputs="%(SDKCustomXUnitProject.Identity)%(SDKCustomXUnitProject.TargetFramework)%(SDKCustomXUnitProject.RuntimeTargetFramework)%(SDKCustomXUnitProject.AdditionalProperties)">
+ Properties="CustomAfterMicrosoftCommonTargets=$(_SDKCustomXUnitPublishTargetsPath);%(SDKCustomXUnitProject.AdditionalProperties)">
@@ -61,16 +53,7 @@
<_CurrentRuntimeTargetFramework Condition="'$(_CurrentRuntimeTargetFramework)' == ''">$(SDKCustomXUnitRuntimeTargetFramework)
<_CurrentAdditionalProperties>%(SDKCustomXUnitProject.AdditionalProperties)
-
-
-
-
+
@@ -94,7 +77,6 @@
diff --git a/test/xunit.runner.json b/test/xunit.runner.json
index 650eda816f3b..1fca20845e33 100644
--- a/test/xunit.runner.json
+++ b/test/xunit.runner.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://xunit.net/schema/v3.1/xunit.runner.schema.json",
+ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"diagnosticMessages": true,
"longRunningTestSeconds": 20,
"showLiveOutput": true,