diff --git a/.gitignore b/.gitignore index fb19bcffa77..cbf0016dd16 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,6 @@ StyleCop.Cache # Ignore SelfSignedCertificate autogenerated files test/tools/Modules/SelfSignedCertificate/ + +# BenchmarkDotNet artifacts +test/perf/BenchmarkDotNet.Artifacts/ diff --git a/.globalconfig b/.globalconfig index c2270482dbc..1cae2c3938c 100644 --- a/.globalconfig +++ b/.globalconfig @@ -1038,7 +1038,7 @@ dotnet_diagnostic.SA1006.severity = warning dotnet_diagnostic.SA1007.severity = warning # SA1008: Opening parenthesis should be spaced correctly -dotnet_diagnostic.SA1008.severity = none +dotnet_diagnostic.SA1008.severity = warning # SA1009: Closing parenthesis should be spaced correctly dotnet_diagnostic.SA1009.severity = none diff --git a/.spelling b/.spelling index 656a4e6570a..5e7191101a3 100644 --- a/.spelling +++ b/.spelling @@ -383,6 +383,7 @@ includeusername informationrecord initializers install-packageprovider +IntelliSense interactivetesting interop interoperation @@ -1303,9 +1304,24 @@ powershell.config.json romero126 boolean rtm.20526.5 +dbaileyut +un-localized +awakecoding +bcwood +ThrowTerminatingError +DoesNotReturn +GetValueOrDefault +PSLanguageMode +adamsitnik +msixbundle +PowerShell-Native#70 +AppxManifest.xml - CHANGELOG/7.0.md codesign release-BuildJson yml centos-7 + - test/perf/benchmarks/README.md +benchmarked +BenchmarkDotNet diff --git a/.vsts-ci/linux.yml b/.vsts-ci/linux.yml index b9dcf05eb33..bed0c4fd296 100644 --- a/.vsts-ci/linux.yml +++ b/.vsts-ci/linux.yml @@ -14,6 +14,7 @@ trigger: - /.vsts-ci/misc-analysis.yml - /.github/ISSUE_TEMPLATE/* - /.dependabot/config.yml + - test/perf/* pr: branches: include: @@ -30,6 +31,7 @@ pr: - .vsts-ci/windows.yml - .vsts-ci/windows/* - test/common/markdown/* + - test/perf/* - tools/releaseBuild/* - tools/releaseBuild/azureDevOps/templates/* diff --git a/.vsts-ci/mac.yml b/.vsts-ci/mac.yml index 6b050122ff8..3cd35335bef 100644 --- a/.vsts-ci/mac.yml +++ b/.vsts-ci/mac.yml @@ -15,6 +15,7 @@ trigger: - /.vsts-ci/misc-analysis.yml - /.github/ISSUE_TEMPLATE/* - /.dependabot/config.yml + - test/perf/* pr: branches: include: @@ -31,6 +32,7 @@ pr: - /.vsts-ci/windows.yml - /.vsts-ci/windows/* - test/common/markdown/* + - test/perf/* - tools/packaging/* - tools/releaseBuild/* - tools/releaseBuild/azureDevOps/templates/* diff --git a/.vsts-ci/misc-analysis.yml b/.vsts-ci/misc-analysis.yml index 8c81c604270..d760a6e4931 100644 --- a/.vsts-ci/misc-analysis.yml +++ b/.vsts-ci/misc-analysis.yml @@ -85,7 +85,7 @@ jobs: condition: succeededOrFailed() - bash: | - mdspell '**/*.md' '!**/Pester/**/*.md' --ignore-numbers --ignore-acronyms --report --en-us; + mdspell '**/*.md' '!**/Pester/**/*.md' '!**/dotnet-tools/**/*.md' --ignore-numbers --ignore-acronyms --report --en-us; displayName: Test Spelling in Markdown condition: succeededOrFailed() workingDirectory: '$(repoPath)' diff --git a/.vsts-ci/windows.yml b/.vsts-ci/windows.yml index ac6d350afaa..e96b320d77b 100644 --- a/.vsts-ci/windows.yml +++ b/.vsts-ci/windows.yml @@ -14,6 +14,7 @@ trigger: - /.vsts-ci/misc-analysis.yml - /.github/ISSUE_TEMPLATE/* - /.dependabot/config.yml + - test/perf/* pr: branches: include: @@ -28,6 +29,7 @@ pr: - .github/ISSUE_TEMPLATE/* - .vsts-ci/misc-analysis.yml - test/common/markdown/* + - test/perf/* - tools/packaging/* - tools/releaseBuild/* - tools/releaseBuild/azureDevOps/templates/* diff --git a/CHANGELOG/preview.md b/CHANGELOG/preview.md index e4f2b3debdf..a80deabc1f8 100644 --- a/CHANGELOG/preview.md +++ b/CHANGELOG/preview.md @@ -1,5 +1,91 @@ # Current preview release +## [7.2.0-preview.6] - 2021-05-27 + +### Experimental Features + +- [Breaking Change] Update prediction interface to provide additional feedback to a predictor plugin (#15421) + +### Performance + +- Avoid collecting logs in buffer if a pipeline execution event is not going to be logged (#15350) +- Avoid allocation in `LanguagePrimitives.UpdateTypeConvertFromTypeTable` (#15168) (Thanks @xtqqczze!) +- Replace `Directory.GetDirectories` with `Directory.EnumerateDirectories` to avoid array allocations (#15167) (Thanks @xtqqczze!) +- Use `List.ConvertAll` instead of `LINQ` (#15140) (Thanks @xtqqczze!) + +### General Cmdlet Updates and Fixes + +- Use `AllocConsole` before initializing CLR to ensure codepage is correct for WinRM remoting (PowerShell/PowerShell-Native#70) (Thanks @jborean93!) +- Add completions for `#requires` statements (#14596) (Thanks @MartinGC94!) +- Add completions for comment-based help keywords (#15337) (Thanks @MartinGC94!) +- Move cross platform DSC code to a PowerShell engine subsystem (#15127) +- Fix `Minimal` progress view to handle activity that is longer than console width (#15264) +- Handle exception if ConsoleHost tries to set cursor out of bounds because screen buffer changed (#15380) +- Fix `NullReferenceException` in DSC `ClearCache()` (#15373) +- Update `ControlSequenceLength` to handle colon as a virtual terminal parameter separator (#14942) +- Update the summary comment for `StopTranscriptCmdlet.cs` (#15349) (Thanks @dbaileyut!) +- Remove the unusable alias `d` for the `-Directory` parameter from `Get-ChildItem` (#15171) (Thanks @kvprasoon!) +- Fix tab completion for un-localized `about` topics (#15265) (Thanks @MartinGC94!) +- Remove the unneeded SSH stdio handle workaround (#15308) +- Add `LoadAssemblyFromNativeMemory` API to load assemblies from memory in a native PowerShell host (#14652) (Thanks @awakecoding!) +- Re-implement `Remove-Item` OneDrive support (#15260) (Thanks @iSazonov!) +- Kill native processes in pipeline when pipeline is disposed on Unix (#15287) +- Default to MTA on Windows platforms where STA is not supported (#15106) + +### Code Cleanup + +
+ + + +

We thank the following contributors!

+

@xtqqczze, @powercode, @bcwood

+ +
+ + + +
+ +### Tools + +- Add `winget` release script (#15050) + +### Tests + +- Enable cross-runtime benchmarking to compare different .NET runtimes (#15387) (Thanks @adamsitnik!) +- Add the performance benchmark project for PowerShell performance testing (#15242) + +### Build and Packaging Improvements + +
+ + +Update .NET to version v6.0.0-preview.4 + + + + +
+ +### Documentation and Help Content + +- Add documentation comments section to coding guidelines (#14316) (Thanks @xtqqczze!) + +[7.2.0-preview.6]: https://github.com/PowerShell/PowerShell/compare/v7.2.0-preview.5...v7.2.0-preview.6 + ## [7.2.0-preview.5] - 2021-04-14 ### Breaking Changes diff --git a/DotnetRuntimeMetadata.json b/DotnetRuntimeMetadata.json index c1f96b55956..9124e01730e 100644 --- a/DotnetRuntimeMetadata.json +++ b/DotnetRuntimeMetadata.json @@ -1,9 +1,9 @@ { "sdk": { - "channel": "release/6.0.1xx-preview2", - "packageVersionPattern": "6.0.0-preview.2", + "channel": "release/6.0.1xx-preview4", + "packageVersionPattern": "6.0.0-preview.4", "sdkImageVersion": "6.0.100", - "nextChannel": "6.0.1xx-preview2/daily" + "nextChannel": "6.0.1xx-preview4/daily" }, "internalfeed" : { "url": null diff --git a/PowerShell.Common.props b/PowerShell.Common.props index 38cd13e007d..b6b627bdf34 100644 --- a/PowerShell.Common.props +++ b/PowerShell.Common.props @@ -136,6 +136,7 @@ net6.0 9.0 true + false true true diff --git a/README.md b/README.md index b469018e5d0..f99105fcd57 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,17 @@ You can download and install a PowerShell package for any of the following platf | -------------------------------------------| ------------------------| ------------------------| ----------------------| ------------------------------| | [Windows (x64)][corefx-win] | [.msi][lts-windows-64] | [.msi][rl-windows-64] | [.msi][pv-windows-64] | [Instructions][in-windows] | | [Windows (x86)][corefx-win] | [.msi][lts-windows-86] | [.msi][rl-windows-86] | [.msi][pv-windows-86] | [Instructions][in-windows] | -| [Ubuntu 20.04][corefx-linux] | | [.deb][rl-ubuntu20] | [.deb][pv-ubuntu20] | [Instructions][in-ubuntu20] | -| [Ubuntu 18.04][corefx-linux] | [.deb][lts-ubuntu18] | [.deb][rl-ubuntu18] | [.deb][pv-ubuntu18] | [Instructions][in-ubuntu18] | -| [Ubuntu 16.04][corefx-linux] | [.deb][lts-ubuntu16] | [.deb][rl-ubuntu16] | [.deb][pv-ubuntu16] | [Instructions][in-ubuntu16] | -| [Debian 9][corefx-linux] | [.deb][lts-debian9] | [.deb][rl-debian9] | [.deb][pv-debian9] | [Instructions][in-deb9] | -| [Debian 10][corefx-linux] | [.deb][lts-debian10] | [.deb][rl-debian10] | [.deb][pv-debian10] | [Instructions][in-deb9] | -| [Debian 11][corefx-linux] | | [.deb][rl-debian11] | [.deb][pv-debian11] | | -| [CentOS 7][corefx-linux] | [.rpm][lts-centos] | [.rpm][rl-centos] | [.rpm][pv-centos] | [Instructions][in-centos] | -| [CentOS 8][corefx-linux] | [.rpm][lts-centos8] | [.rpm][rl-centos8] | [.rpm][pv-centos8] | | -| [Red Hat Enterprise Linux 7][corefx-linux] | [.rpm][lts-centos] | [.rpm][rl-centos] | [.rpm][pv-centos] | [Instructions][in-rhel7] | -| [openSUSE 42.3][corefx-linux] | [.rpm][lts-centos] | [.rpm][rl-centos] | [.rpm][pv-centos] | [Instructions][in-opensuse] | -| [Fedora 30][corefx-linux] | [.rpm][lts-centos] | [.rpm][rl-centos] | [.rpm][pv-centos] | [Instructions][in-fedora] | +| [Ubuntu 20.04][corefx-linux] | | [.deb][rl-ubuntu20] | [.deb][pv-deb] | [Instructions][in-ubuntu20] | +| [Ubuntu 18.04][corefx-linux] | [.deb][lts-ubuntu18] | [.deb][rl-ubuntu18] | [.deb][pv-deb] | [Instructions][in-ubuntu18] | +| [Ubuntu 16.04][corefx-linux] | [.deb][lts-ubuntu16] | [.deb][rl-ubuntu16] | [.deb][pv-deb] | [Instructions][in-ubuntu16] | +| [Debian 9][corefx-linux] | [.deb][lts-debian9] | [.deb][rl-debian9] | [.deb][pv-deb] | [Instructions][in-deb9] | +| [Debian 10][corefx-linux] | [.deb][lts-debian10] | [.deb][rl-debian10] | [.deb][pv-deb] | [Instructions][in-deb9] | +| [Debian 11][corefx-linux] | | [.deb][rl-debian11] | [.deb][pv-deb] | | +| [CentOS 7][corefx-linux] | [.rpm][lts-centos] | [.rpm][rl-centos] | [.rpm][pv-rpm] | [Instructions][in-centos] | +| [CentOS 8][corefx-linux] | [.rpm][lts-centos8] | [.rpm][rl-centos8] | [.rpm][pv-rpm] | | +| [Red Hat Enterprise Linux 7][corefx-linux] | [.rpm][lts-centos] | [.rpm][rl-centos] | [.rpm][pv-rpm] | [Instructions][in-rhel7] | +| [openSUSE 42.3][corefx-linux] | [.rpm][lts-centos] | [.rpm][rl-centos] | [.rpm][pv-rpm] | [Instructions][in-opensuse] | +| [Fedora 30][corefx-linux] | [.rpm][lts-centos] | [.rpm][rl-centos] | [.rpm][pv-rpm] | [Instructions][in-fedora] | | [macOS 10.13+][corefx-macos] | [.pkg][lts-macos] | [.pkg][rl-macos] | [.pkg][pv-macos] | [Instructions][in-macos] | | Docker | | | | [Instructions][in-docker] | @@ -49,7 +49,7 @@ You can download and install a PowerShell package for any of the following platf | Platform | Downloads (stable) | Downloads (preview) | How to Install | | -------------------------| ------------------------| ----------------------------- | ------------------------------| | Arch Linux | | | [Instructions][in-archlinux] | -| Kali Linux | [.deb][rl-ubuntu16] | [.deb][pv-ubuntu16] | [Instructions][in-kali] | +| Kali Linux | [.deb][rl-ubuntu16] | [.deb][pv-deb] | [Instructions][in-kali] | | Many Linux distributions | [Snapcraft][rl-snap] | [Snapcraft][pv-snap] | | You can also download the PowerShell binary archives for Windows, macOS and Linux. @@ -92,24 +92,18 @@ You can also download the PowerShell binary archives for Windows, macOS and Linu [rl-arm64]: https://github.com/PowerShell/PowerShell/releases/download/v7.1.3/powershell-7.1.3-linux-arm64.tar.gz [rl-snap]: https://snapcraft.io/powershell -[pv-windows-64]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/PowerShell-7.2.0-preview.4-win-x64.msi -[pv-windows-86]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/PowerShell-7.2.0-preview.4-win-x86.msi -[pv-ubuntu20]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-preview_7.2.0-preview.4-1.ubuntu.20.04_amd64.deb -[pv-ubuntu18]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-preview_7.2.0-preview.4-1.ubuntu.18.04_amd64.deb -[pv-ubuntu16]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-preview_7.2.0-preview.4-1.ubuntu.16.04_amd64.deb -[pv-debian9]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-preview_7.2.0-preview.4-1.debian.9_amd64.deb -[pv-debian10]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-preview_7.2.0-preview.4-1.debian.10_amd64.deb -[pv-debian11]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-preview_7.2.0-preview.4-1.debian.11_amd64.deb -[pv-centos]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-preview-7.2.0_preview.4-1.rhel.7.x86_64.rpm -[pv-centos8]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-preview-7.2.0_preview.4-1.centos.8.x86_64.rpm -[pv-macos]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-7.2.0-preview.4-osx-x64.pkg -[pv-winarm64]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/PowerShell-7.2.0-preview.4-win-arm64.zip -[pv-winx86-zip]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/PowerShell-7.2.0-preview.4-win-x86.zip -[pv-winx64-zip]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/PowerShell-7.2.0-preview.4-win-x64.zip -[pv-macos-tar]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-7.2.0-preview.4-osx-x64.tar.gz -[pv-linux-tar]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-7.2.0-preview.4-linux-x64.tar.gz -[pv-arm32]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-7.2.0-preview.4-linux-arm32.tar.gz -[pv-arm64]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.4/powershell-7.2.0-preview.4-linux-arm64.tar.gz +[pv-windows-64]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/PowerShell-7.2.0-preview.5-win-x64.msi +[pv-windows-86]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/PowerShell-7.2.0-preview.5-win-x86.msi +[pv-deb]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/powershell-preview_7.2.0-preview.5-1.deb_amd64.deb +[pv-rpm]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/powershell-preview-7.2.0_preview.5-1.rh.x86_64.rpm +[pv-macos]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/powershell-7.2.0-preview.5-osx-x64.pkg +[pv-winarm64]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/PowerShell-7.2.0-preview.5-win-arm64.zip +[pv-winx86-zip]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/PowerShell-7.2.0-preview.5-win-x86.zip +[pv-winx64-zip]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/PowerShell-7.2.0-preview.5-win-x64.zip +[pv-macos-tar]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/powershell-7.2.0-preview.5-osx-x64.tar.gz +[pv-linux-tar]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/powershell-7.2.0-preview.5-linux-x64.tar.gz +[pv-arm32]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/powershell-7.2.0-preview.5-linux-arm32.tar.gz +[pv-arm64]: https://github.com/PowerShell/PowerShell/releases/download/v7.2.0-preview.5/powershell-7.2.0-preview.5-linux-arm64.tar.gz [pv-snap]: https://snapcraft.io/powershell-preview [in-windows]: https://docs.microsoft.com/powershell/scripting/install/installing-powershell-core-on-windows diff --git a/assets/AppxManifest.xml b/assets/AppxManifest.xml index 83df8c31b41..bba68ca976b 100644 --- a/assets/AppxManifest.xml +++ b/assets/AppxManifest.xml @@ -20,7 +20,7 @@ - + diff --git a/assets/wix/files.wxs b/assets/wix/files.wxs index d8f3a09589d..17f0ca4daa1 100644 --- a/assets/wix/files.wxs +++ b/assets/wix/files.wxs @@ -1678,9 +1678,6 @@ - - - @@ -3066,8 +3063,8 @@ - - + + @@ -3620,7 +3617,6 @@ - @@ -4057,7 +4053,7 @@ - + diff --git a/docs/dev-process/coding-guidelines.md b/docs/dev-process/coding-guidelines.md index 389981c3f7b..9bf46aee02d 100644 --- a/docs/dev-process/coding-guidelines.md +++ b/docs/dev-process/coding-guidelines.md @@ -86,9 +86,15 @@ We also run the [.NET code formatter tool](https://github.com/dotnet/codeformatt * Make sure the added/updated comments are meaningful, accurate and easy to understand. -* Public members must use [doc comments](https://docs.microsoft.com/dotnet/csharp/programming-guide/xmldoc/). +### Documentation comments + +* Create documentation using [XML documentation comments](https://docs.microsoft.com/dotnet/csharp/codedoc) so that Visual Studio and other IDEs can use IntelliSense to show quick information about types or members. + +* Publicly visible types and their members must be documented. Internal and private members may use doc comments but it is not required. +* Documentation text should be written using complete sentences ending with full stops. + ## Performance Considerations PowerShell has a lot of performance sensitive code as well as a lot of inefficient code. diff --git a/global.json b/global.json index 08135cb47bd..c36bb3376e9 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "6.0.100-preview.3.21202.5" + "version": "6.0.100-preview.4.21255.9" } } diff --git a/src/Microsoft.Management.Infrastructure.CimCmdlets/CimCommandBase.cs b/src/Microsoft.Management.Infrastructure.CimCmdlets/CimCommandBase.cs index 059f58a97b2..a7e75bb98c7 100644 --- a/src/Microsoft.Management.Infrastructure.CimCmdlets/CimCommandBase.cs +++ b/src/Microsoft.Management.Infrastructure.CimCmdlets/CimCommandBase.cs @@ -672,6 +672,7 @@ internal virtual CmdletOperationBase CmdletOperation /// Throw terminating error /// /// + [System.Diagnostics.CodeAnalysis.DoesNotReturn] internal void ThrowTerminatingError(Exception exception, string operation) { ErrorRecord errorRecord = new(exception, operation, ErrorCategory.InvalidOperation, this); diff --git a/src/Microsoft.Management.Infrastructure.CimCmdlets/CmdletOperation.cs b/src/Microsoft.Management.Infrastructure.CimCmdlets/CmdletOperation.cs index 3c49b1dcbb8..03db6967aef 100644 --- a/src/Microsoft.Management.Infrastructure.CimCmdlets/CmdletOperation.cs +++ b/src/Microsoft.Management.Infrastructure.CimCmdlets/CmdletOperation.cs @@ -65,6 +65,7 @@ public virtual bool ShouldProcess(string verboseDescription, string verboseWarni return cmdlet.ShouldProcess(verboseDescription, verboseWarning, caption, out shouldProcessReason); } + [System.Diagnostics.CodeAnalysis.DoesNotReturn] public virtual void ThrowTerminatingError(ErrorRecord errorRecord) { cmdlet.ThrowTerminatingError(errorRecord); @@ -115,6 +116,7 @@ public virtual void WriteWarning(string text) /// Throw terminating error /// /// + [System.Diagnostics.CodeAnalysis.DoesNotReturn] internal void ThrowTerminatingError(Exception exception, string operation) { ErrorRecord errorRecord = new(exception, operation, ErrorCategory.InvalidOperation, this); diff --git a/src/Microsoft.Management.UI.Internal/HelpWindow/HelpViewModel.cs b/src/Microsoft.Management.UI.Internal/HelpWindow/HelpViewModel.cs index 7cbe4b0c74e..de62923614d 100644 --- a/src/Microsoft.Management.UI.Internal/HelpWindow/HelpViewModel.cs +++ b/src/Microsoft.Management.UI.Internal/HelpWindow/HelpViewModel.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Management.Automation; using System.Windows.Documents; @@ -273,11 +274,13 @@ private void SetMatchesLabel() /// Property name. private void OnNotifyPropertyChanged(string propertyName) { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } + #pragma warning restore IDE1005s } } } diff --git a/src/Microsoft.Management.UI.Internal/HelpWindow/ParagraphBuilder.cs b/src/Microsoft.Management.UI.Internal/HelpWindow/ParagraphBuilder.cs index 822e4c05026..16b1be82f67 100644 --- a/src/Microsoft.Management.UI.Internal/HelpWindow/ParagraphBuilder.cs +++ b/src/Microsoft.Management.UI.Internal/HelpWindow/ParagraphBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Windows.Documents; using System.Windows.Media; @@ -107,10 +108,15 @@ internal void BuildParagraph() bool newHighlighted = false; ParagraphBuilder.MoveSpanToPosition(ref currentBoldIndex, ref currentBoldSpan, i, this.boldSpans); + + #pragma warning disable IDE0075 // IDE0075: Conditional expression can be simplified newBold = currentBoldSpan == null ? false : currentBoldSpan.Value.Contains(i); + #pragma warning restore IDE0075 ParagraphBuilder.MoveSpanToPosition(ref currentHighlightedIndex, ref currentHighlightedSpan, i, this.highlightedSpans); + #pragma warning disable IDE0075 // IDE0075: Conditional expression can be simplified newHighlighted = currentHighlightedSpan == null ? false : currentHighlightedSpan.Value.Contains(i); + #pragma warning restore IDE0075 if (newBold != currentBold || newHighlighted != currentHighlighted) { @@ -301,11 +307,13 @@ private void AddHighlight(int start, int length) /// Property name. private void OnNotifyPropertyChanged(string propertyName) { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified.s PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } + #pragma warning restore IDE1005 } /// diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/Common/DismissiblePopup.cs b/src/Microsoft.Management.UI.Internal/ManagementList/Common/DismissiblePopup.cs index 582be1c7ecd..97038e71665 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/Common/DismissiblePopup.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/Common/DismissiblePopup.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Windows; using System.Windows.Automation; using System.Windows.Controls.Primitives; @@ -79,6 +80,7 @@ protected override void OnClosed(EventArgs e) } } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private void SetFocus(UIElement element) { if (element.Focusable) diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/Common/KeyboardHelp.cs b/src/Microsoft.Management.UI.Internal/ManagementList/Common/KeyboardHelp.cs index 90684f3f6cc..0d174fb25ed 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/Common/KeyboardHelp.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/Common/KeyboardHelp.cs @@ -109,7 +109,7 @@ public static FocusNavigationDirection GetNavigationDirection(DependencyObject e /// True if a control is is pressed. public static bool IsControlPressed() { - if (ModifierKeys.Control == (Keyboard.Modifiers & ModifierKeys.Control)) + if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { return true; } diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/Common/ListOrganizerItem.cs b/src/Microsoft.Management.UI.Internal/ManagementList/Common/ListOrganizerItem.cs index 469ed5aec77..276cbba283a 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/Common/ListOrganizerItem.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/Common/ListOrganizerItem.cs @@ -35,7 +35,9 @@ public bool IsInEditMode { get { + #pragma warning disable IDE0075 // IDE0075: Conditional expression can be simplified return (this.renameButton != null) ? this.renameButton.IsChecked.Value : false; + #pragma warning restore IDE0075 } } diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/Common/ReadOnlyObservableAsyncCollection.cs b/src/Microsoft.Management.UI.Internal/ManagementList/Common/ReadOnlyObservableAsyncCollection.cs index 7734bc4903c..1c3e7bffb27 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/Common/ReadOnlyObservableAsyncCollection.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/Common/ReadOnlyObservableAsyncCollection.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.Management.UI.Internal { @@ -97,6 +98,8 @@ public Exception OperationError #endregion IAsyncProgress #region Private Methods + + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) { NotifyCollectionChangedEventHandler eh = this.CollectionChanged; @@ -117,6 +120,8 @@ private void OnPropertyChanged(PropertyChangedEventArgs args) } } + #pragma warning restore IDE1005 + // forward CollectionChanged events from the base list to our listeners private void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/CommonControls/Resizer.cs b/src/Microsoft.Management.UI.Internal/ManagementList/CommonControls/Resizer.cs index 19c0671bf21..36ed7e62e3d 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/CommonControls/Resizer.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/CommonControls/Resizer.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; @@ -202,6 +203,7 @@ private double GetNewWidth(ResizeGripLocation location, double horzDelta) return this.GetConstrainedValue(newWidth, this.MaxWidth, this.MinWidth); } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private double GetHorizontalDelta(ResizeGripLocation location, double horzDelta) { double realDelta; @@ -219,6 +221,7 @@ private double GetHorizontalDelta(ResizeGripLocation location, double horzDelta) return realDelta; } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private double GetConstrainedValue(double value, double max, double min) { return Math.Min(max, Math.Max(value, min)); diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/DefaultFilterRuleCustomizationFactory.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/DefaultFilterRuleCustomizationFactory.cs index bd5faf32d63..2fa13516bde 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/DefaultFilterRuleCustomizationFactory.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/DefaultFilterRuleCustomizationFactory.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; @@ -196,6 +197,7 @@ public override string GetErrorMessageForInvalidValue(string value, Type typeToP #region Helpers + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private bool TryGetGenericParameterForComparableValueFilterRule(FilterRule rule, out Type genericParameter) { genericParameter = null; @@ -217,6 +219,7 @@ private bool TryGetGenericParameterForComparableValueFilterRule(FilterRule rule, return true; } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private object GetValueFromValidatingValue(FilterRule rule, string propertyName) { Debug.Assert(rule != null && !string.IsNullOrEmpty(propertyName), "rule and propertyname are not null"); @@ -236,6 +239,7 @@ private object GetValueFromValidatingValue(FilterRule rule, string propertyName) return property.GetValue(validatingValue, null); } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private void SetValueOnValidatingValue(FilterRule rule, string propertyName, object value) { Debug.Assert(rule != null && !string.IsNullOrEmpty(propertyName), "rule and propertyname are not null"); diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/FilterEvaluator.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/FilterEvaluator.cs index 2b33153a983..46ada50ebef 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/FilterEvaluator.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/FilterEvaluator.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.Management.UI.Internal { @@ -173,6 +174,8 @@ public void RemoveFilterExpressionProvider(IFilterExpressionProvider provider) #region NotifyPropertyChanged + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. + /// /// Notifies listeners that a property has changed. /// @@ -214,6 +217,8 @@ protected virtual void NotifyFilterExpressionChanged() } } + #pragma warning restore IDE1005 + private void FilterProvider_FilterExpressionChanged(object sender, EventArgs e) { // Update HasFilterExpression \\ diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/FilterRules/FilterRule.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/FilterRules/FilterRule.cs index 1c2fc523e86..89b31449ba6 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/FilterRules/FilterRule.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/FilterRules/FilterRule.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.Management.UI.Internal { @@ -66,12 +67,14 @@ protected FilterRule() /// protected void NotifyEvaluationResultInvalidated() { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. var eh = this.EvaluationResultInvalidated; if (eh != null) { eh(this, new EventArgs()); } + #pragma warning restore IDE1005 } #endregion diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingSelectorValue.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingSelectorValue.cs index ed5389668e6..e86f2020ecc 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingSelectorValue.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingSelectorValue.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Windows.Data; @@ -214,12 +215,14 @@ protected override DataErrorInfoValidationResult Validate(string columnName) /// protected void NotifySelectedValueChanged(T oldValue, T newValue) { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. EventHandler> eh = this.SelectedValueChanged; if (eh != null) { eh(this, new PropertyChangedEventArgs(oldValue, newValue)); } + #pragma warning restore IDE1005 } #endregion NotifySelectedValueChanged diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingValue.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingValue.cs index cf9c553f6b4..62bb3ca7517 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingValue.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingValue.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Microsoft.Management.UI.Internal @@ -190,6 +191,7 @@ private bool TryGetCastValue(object rawValue, out T castValue) } } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private bool TryGetEnumValue(object rawValue, out T castValue) { Debug.Assert(rawValue != null, "rawValue not null"); diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingValueBase.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingValueBase.cs index f3959685349..ea2b255063f 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingValueBase.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterCore/ValidatingValueBase.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Microsoft.Management.UI.Internal @@ -260,12 +261,14 @@ protected void InvalidateValidationResult() /// protected void NotifyPropertyChanged(string propertyName) { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. PropertyChangedEventHandler eh = this.PropertyChanged; if (eh != null) { eh(this, new PropertyChangedEventArgs(propertyName)); } + #pragma warning restore IDE1005 } #endregion NotifyPropertyChanged diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/AddFilterRulePicker.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/AddFilterRulePicker.cs index d41b0a3e532..a4e13c6c9f5 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/AddFilterRulePicker.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/AddFilterRulePicker.cs @@ -4,6 +4,7 @@ using System; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Windows.Controls; namespace Microsoft.Management.UI.Internal @@ -49,9 +50,11 @@ public ObservableCollection ColumnFilterRules partial void OnOkAddFilterRulesCanExecuteImplementation(System.Windows.Input.CanExecuteRoutedEventArgs e) { + #pragma warning disable IDE0075 // IDE0075: Conditional expression can be simplified e.CanExecute = (this.AddFilterRulesCommand != null) ? CommandHelper.CanExecuteCommand(this.AddFilterRulesCommand, null, this.AddFilterRulesCommandTarget) : false; + #pragma warning restore IDE0075 } partial void OnOkAddFilterRulesExecutedImplementation(System.Windows.Input.ExecutedRoutedEventArgs e) diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/AddFilterRulePickerItem.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/AddFilterRulePickerItem.cs index 24c6eec1889..962d4a9665a 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/AddFilterRulePickerItem.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/AddFilterRulePickerItem.cs @@ -70,12 +70,14 @@ public AddFilterRulePickerItem(FilterRulePanelItem filterRule) /// protected void NotifyPropertyChanged(string propertyName) { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. PropertyChangedEventHandler eh = this.PropertyChanged; if (eh != null) { eh(this, new PropertyChangedEventArgs(propertyName)); } + #pragma warning restore IDE1005 } #endregion NotifyPropertyChanged diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanel.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanel.cs index bcc9a01a92c..35060a1b8ff 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanel.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanel.cs @@ -213,11 +213,13 @@ public void ClearContentTemplates() /// protected virtual void NotifyFilterExpressionChanged() { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. EventHandler eh = this.FilterExpressionChanged; if (eh != null) { eh(this, new EventArgs()); } + #pragma warning restore IDE1005 } private void Controller_FilterExpressionChanged(object sender, EventArgs e) diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanelController.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanelController.cs index dcec5022d98..c4956063aa3 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanelController.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanelController.cs @@ -266,11 +266,13 @@ private void UpdateFilterRulePanelItemTypes() /// protected virtual void NotifyFilterExpressionChanged() { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. EventHandler eh = this.FilterExpressionChanged; if (eh != null) { eh(this, new EventArgs()); } + #pragma warning restore IDE1005 } #endregion Notify Filter Expression Changed diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanelItem.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanelItem.cs index a1a873cdbc7..cbafae9d283 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanelItem.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/FilterRulePanelItem.cs @@ -107,11 +107,15 @@ protected void NotifyPropertyChanged(string propertyName) { Debug.Assert(!string.IsNullOrEmpty(propertyName), "not null"); + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. + PropertyChangedEventHandler eh = this.PropertyChanged; if (eh != null) { eh(this, new PropertyChangedEventArgs(propertyName)); } + + #pragma warning restore IDE1005s } #endregion Public Methods diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/InputFieldBackgroundTextConverter.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/InputFieldBackgroundTextConverter.cs index 0017f2a4340..e19eae34a3e 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/InputFieldBackgroundTextConverter.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/InputFieldBackgroundTextConverter.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; using System.Windows.Data; @@ -69,6 +70,7 @@ public object ConvertBack(object value, Type targetType, object parameter, Syste #region Helpers + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private bool IsOfTypeValidatingValue(object value) { Debug.Assert(value != null, "not null"); @@ -92,6 +94,7 @@ private Type GetGenericParameter(object value, CultureInfo culture) return value.GetType().GetGenericArguments()[0]; } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private object GetBackgroundTextForType(Type inputType) { if (typeof(DateTime) == inputType) diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/SearchBox.cs b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/SearchBox.cs index 25830150939..b6471b6f653 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/SearchBox.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/FilterProviders/SearchBox.cs @@ -60,11 +60,13 @@ public bool HasFilterExpression /// protected virtual void NotifyFilterExpressionChanged() { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. EventHandler eh = this.FilterExpressionChanged; if (eh != null) { eh(this, new EventArgs()); } + #pragma warning restore IDE1005s } #endregion diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/Innerlist.cs b/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/Innerlist.cs index 03c7acdf008..a01435b2414 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/Innerlist.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/Innerlist.cs @@ -8,6 +8,7 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; using System.Windows; @@ -355,7 +356,7 @@ protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); - if ((Key.Left == e.Key || Key.Right == e.Key) && + if ((e.Key == Key.Left || e.Key == Key.Right) && Keyboard.Modifiers == ModifierKeys.None) { // If pressing Left or Right on a column header, move the focus \\ @@ -388,8 +389,8 @@ private static void InnerList_OnViewChanged(DependencyObject obj, DependencyProp throw new NotSupportedException(string.Format( CultureInfo.InvariantCulture, InvariantResources.ViewSetWithType, - typeof(GridView).Name, - typeof(InnerListGridView).Name)); + nameof(GridView), + nameof(InnerListGridView))); } ((InnerList)obj).innerGrid = innerGrid; @@ -401,12 +402,14 @@ private static void InnerList_OnViewChanged(DependencyObject obj, DependencyProp /// The exception to be thrown when using Items. private static NotSupportedException GetItemsException() { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. return new NotSupportedException( string.Format( CultureInfo.InvariantCulture, InvariantResources.NotSupportAddingToItems, - typeof(InnerList).Name, + nameof(InnerList), ItemsControl.ItemsSourceProperty.Name)); + #pragma warning restore IDE1005s } #endregion static private methods @@ -605,6 +608,7 @@ private string GetClipboardTextLineForSelectedItem(object value) return entryText.ToString(); } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private void SetClipboardWithSelectedItemsText(string text) { if (string.IsNullOrEmpty(text)) diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/PropertyValueComparer.cs b/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/PropertyValueComparer.cs index 5c9f56daa81..996eb8b72de 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/PropertyValueComparer.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/PropertyValueComparer.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Management.Automation; namespace Microsoft.Management.UI.Internal @@ -88,6 +89,7 @@ private void GetPropertyValues(string propertyName, object a, object b, out obje } } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private int CompareData(object firstValue, object secondValue, StringComparison stringComparison) { // If both values are null, do nothing; otherwise, if one is null promote the other \\ diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/PropertyValueGetter.cs b/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/PropertyValueGetter.cs index 61a1a71938c..8ec9e5ecb7d 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/PropertyValueGetter.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/PropertyValueGetter.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Data; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace Microsoft.Management.UI.Internal @@ -110,6 +111,7 @@ private PropertyDescriptor GetPropertyDescriptor(string propertyName, object val return descriptor; } + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private bool TryGetPropertyValueInternal(PropertyDescriptor descriptor, object value, out object propertyValue) { propertyValue = null; diff --git a/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/managementlist.cs b/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/managementlist.cs index 7c36244e0f1..fab4e721bec 100644 --- a/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/managementlist.cs +++ b/src/Microsoft.Management.UI.Internal/ManagementList/ManagementList/managementlist.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Windows; using System.Windows.Controls; @@ -308,6 +309,7 @@ partial void OnClearFilterExecutedImplementation(ExecutedRoutedEventArgs e) #region View Manager Callbacks + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] partial void OnSaveViewCanExecuteImplementation(CanExecuteRoutedEventArgs e) { string viewName = (string)e.Parameter; diff --git a/src/Microsoft.Management.UI.Internal/ShowCommand/Controls/ParameterSetControl.xaml.cs b/src/Microsoft.Management.UI.Internal/ShowCommand/Controls/ParameterSetControl.xaml.cs index 08f9df29337..a38e4003af3 100644 --- a/src/Microsoft.Management.UI.Internal/ShowCommand/Controls/ParameterSetControl.xaml.cs +++ b/src/Microsoft.Management.UI.Internal/ShowCommand/Controls/ParameterSetControl.xaml.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Management.Automation; using System.Windows; @@ -344,6 +345,7 @@ private void CheckBox_Click(object sender, RoutedEventArgs e) /// Creates a RowDefinition for MainGrid. /// /// Return a RowDefinition object. + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private RowDefinition CreateNewRow() { RowDefinition row = new RowDefinition(); @@ -382,6 +384,7 @@ private void CreateAndAddLabel(ParameterViewModel parameterViewModel, int rowNum /// DataContext object. /// Row number. /// Return a Label control. + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private Label CreateLabel(ParameterViewModel parameterViewModel, int rowNumber) { Label label = new Label(); diff --git a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/AllModulesViewModel.cs b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/AllModulesViewModel.cs index 34159c8f837..8040c4cb45a 100644 --- a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/AllModulesViewModel.cs +++ b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/AllModulesViewModel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Management.Automation; @@ -79,10 +80,12 @@ public class AllModulesViewModel : INotifyPropertyChanged /// Commands to show. public AllModulesViewModel(Dictionary importedModules, IEnumerable commands) { + #pragma warning disable IDE0075 // IDE0075: Conditional expression can be simplified if (commands == null || !commands.GetEnumerator().MoveNext()) { throw new ArgumentNullException("commands"); } + #pragma warning disable IDE0075 // IDE0075: Conditional expression can be simplified this.Initialization(importedModules, commands, true); } @@ -400,11 +403,13 @@ public string GetScript() /// internal void OnRefresh() { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. EventHandler handler = this.Refresh; if (handler != null) { handler(this, new EventArgs()); } + #pragma warning restore IDE1005s } #region Private Methods @@ -593,6 +598,8 @@ private void SelectedModule_SelectedCommandNeedsImportModule(object sender, Impo this.OnSelectedCommandInSelectedModuleNeedsImportModule(e); } + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. + /// /// Triggers SelectedCommandInSelectedModuleNeedsHelp. /// @@ -654,6 +661,8 @@ private void OnNotifyPropertyChanged(string propertyName) handler(this, new PropertyChangedEventArgs(propertyName)); } } + + #pragma warning restore IDE1005s #endregion } } diff --git a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/CommandViewModel.cs b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/CommandViewModel.cs index e5eb1be800d..13f3b8507d9 100644 --- a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/CommandViewModel.cs +++ b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/CommandViewModel.cs @@ -438,12 +438,12 @@ public string GetScript() builder.Append(commandName); } - builder.Append(" "); + builder.Append(' '); if (this.SelectedParameterSet != null) { builder.Append(this.SelectedParameterSet.GetScript()); - builder.Append(" "); + builder.Append(' '); } if (this.CommonParameters != null) @@ -553,6 +553,8 @@ internal static CommandViewModel GetCommandViewModel(ModuleViewModel module, Sho return returnValue; } + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. + /// /// Called to trigger the event fired when help is needed for the command. /// @@ -628,6 +630,8 @@ private void OnNotifyPropertyChanged(string propertyName) } } + #pragma warning restore IDE1005 + /// /// Called when the PropertyChanged event is triggered on the SelectedParameterSet. /// diff --git a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ModuleViewModel.cs b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ModuleViewModel.cs index 38925e458a9..d6a2e57c051 100644 --- a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ModuleViewModel.cs +++ b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ModuleViewModel.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Management.Automation; using System.Windows; @@ -76,6 +77,8 @@ public ModuleViewModel(string name, Dictionary im this.commands = new List(); this.filteredCommands = new ObservableCollection(); + #pragma warning disable IDE0075 // IDE0075: Conditional expression can be simplified + // This check looks to see if the given module name shows up in // the set of modules that are known to be imported in the current // session. In remote PowerShell sessions, the core cmdlet module @@ -86,6 +89,8 @@ public ModuleViewModel(string name, Dictionary im importedModules == null ? true : name.Length == 0 || importedModules.ContainsKey(name) || string.Equals("Microsoft.PowerShell.Core", name, StringComparison.OrdinalIgnoreCase); + + #pragma warning restore IDE0075 } #endregion @@ -372,6 +377,8 @@ internal void RefreshFilteredCommands(string filter) } } + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. + /// /// Callled in response to a GUI event that requires the command to be run. /// @@ -527,5 +534,7 @@ private void OnNotifyPropertyChanged(string propertyName) handler(this, new PropertyChangedEventArgs(propertyName)); } } + + #pragma warning restore IDE1005 } } diff --git a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ParameterSetViewModel.cs b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ParameterSetViewModel.cs index 756f58c62c8..f599750d7f9 100644 --- a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ParameterSetViewModel.cs +++ b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ParameterSetViewModel.cs @@ -369,11 +369,13 @@ private void EvaluateAllMandatoryParametersHaveValues() /// The changed property. private void OnNotifyPropertyChanged(string propertyName) { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } + #pragma warning restore IDE1005 } /// diff --git a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ParameterViewModel.cs b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ParameterViewModel.cs index 2c6931eb08e..ccd2fd635c8 100644 --- a/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ParameterViewModel.cs +++ b/src/Microsoft.Management.UI.Internal/ShowCommand/ViewModel/ParameterViewModel.cs @@ -268,11 +268,13 @@ internal static string EvaluateTooltip(string typeName, int position, bool manda /// The changed property. private void OnNotifyPropertyChanged(string propertyName) { + #pragma warning disable IDE1005 // IDE1005: Delegate invocation can be simplified. PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } + #pragma warning restore IDE1005 } } } diff --git a/src/Microsoft.Management.UI.Internal/commandHelpers/OutGridView.cs b/src/Microsoft.Management.UI.Internal/commandHelpers/OutGridView.cs index 85ecd95a36f..2135877655a 100644 --- a/src/Microsoft.Management.UI.Internal/commandHelpers/OutGridView.cs +++ b/src/Microsoft.Management.UI.Internal/commandHelpers/OutGridView.cs @@ -213,7 +213,7 @@ private void ZoomEventHandlerPlus(object sender, ExecutedRoutedEventArgs e) if (this.zoomLevel < ZOOM_MAX) { - this.zoomLevel = this.zoomLevel + ZOOM_INCREMENT; + this.zoomLevel += ZOOM_INCREMENT; Grid g = this.gridViewWindow.Content as Grid; if (g != null) @@ -232,7 +232,7 @@ private void ZoomEventHandlerMinus(object sender, ExecutedRoutedEventArgs e) { if (this.zoomLevel >= ZOOM_MIN) { - this.zoomLevel = this.zoomLevel - ZOOM_INCREMENT; + this.zoomLevel -= ZOOM_INCREMENT; Grid g = this.gridViewWindow.Content as Grid; if (g != null) { @@ -246,6 +246,7 @@ private void ZoomEventHandlerMinus(object sender, ExecutedRoutedEventArgs e) /// /// Output mode of the out-gridview. /// A new ManagementList. + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private ManagementList CreateManagementList(string outputMode) { ManagementList newList = new ManagementList(); diff --git a/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs b/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs index 5d401fc94c6..5ac2321cded 100644 --- a/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs +++ b/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs @@ -289,7 +289,7 @@ private ShowCommandHelper() } /// - /// Finalizes an instance of the ShowCommandHelper class. + /// Finalizes an instance of the class. /// ~ShowCommandHelper() { @@ -643,7 +643,9 @@ internal static AllModulesViewModel GetNewAllModulesViewModel(AllModulesViewMode ModuleViewModel moduleToSelect = returnValue.Modules.Find( new Predicate((module) => { - return module.Name.Equals(selectedModuleNeedingImportModule, StringComparison.OrdinalIgnoreCase) ? true : false; + #pragma warning disable IDE0075 // IDE0075: Conditional expression can be simplified + return module.Name.Equals(selectedModuleNeedingImportModule, StringComparison.OrdinalIgnoreCase); + #pragma warning restore IDE0075 })); if (moduleToSelect == null) @@ -657,7 +659,7 @@ internal static AllModulesViewModel GetNewAllModulesViewModel(AllModulesViewMode new Predicate((command) => { return command.ModuleName.Equals(parentModuleNeedingImportModule, StringComparison.OrdinalIgnoreCase) && - command.Name.Equals(commandNeedingImportModule, StringComparison.OrdinalIgnoreCase) ? true : false; + command.Name.Equals(commandNeedingImportModule, StringComparison.OrdinalIgnoreCase); })); if (commandToSelect == null) @@ -750,7 +752,7 @@ private static object GetPropertyValue(Type type, object obj, string propertyNam try { - return property.GetValue(obj, new object[] { }); + return property.GetValue(obj, Array.Empty()); } catch (ArgumentException) { @@ -794,7 +796,7 @@ private static bool SetPropertyValue(Type type, object obj, string propertyName, try { - property.SetValue(obj, value, new object[] { }); + property.SetValue(obj, value, Array.Empty()); } catch (ArgumentException) { @@ -1000,7 +1002,7 @@ private void ImportModuleDone(Dictionary imported { this.window.Dispatcher.Invoke( new SendOrPostCallback( - delegate (object ignored) + delegate(object ignored) { this.allModulesViewModel = ShowCommandHelper.GetNewAllModulesViewModel( this.allModulesViewModel, @@ -1050,7 +1052,7 @@ private void DisplayHelp(Collection getHelpResults) { this.window.Dispatcher.Invoke( new SendOrPostCallback( - delegate (object ignored) + delegate(object ignored) { HelpWindow help = new HelpWindow(getHelpResults[0]); help.Owner = this.window; @@ -1244,6 +1246,7 @@ private void CloseWindow() /// Showing a MessageBox when user type a invalidate command name. /// /// Error message. + [SuppressMessage("Performance", "CA1822: Mark members as static", Justification = "Potential breaking change")] private void ShowErrorString(string errorString) { if (errorString != null && errorString.Trim().Length > 0) diff --git a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj index 26e7d4f70c6..d74f77126a7 100644 --- a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj +++ b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index 9a78fe86119..2477bdc5337 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -31,10 +31,10 @@ - - - - + + + + diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/common/GetFormatDataCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/common/GetFormatDataCommand.cs index ba8a660318d..21c078648f2 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/common/GetFormatDataCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/common/GetFormatDataCommand.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Management.Automation; using System.Management.Automation.Remoting; using System.Management.Automation.Runspaces; @@ -85,7 +84,7 @@ private static Dictionary> GetTypeGroupMap(IEnumerable typeReference.name).ToList(); + var typesInGroup = typeGroup.typeReferenceList.ConvertAll(static typeReference => typeReference.name); typeGroupMap.Add(typeGroup.name, typesInGroup); } } diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index fdefdc5c870..a63c9193345 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -477,7 +477,7 @@ internal bool StaMode } else { - return true; + return Platform.IsStaSupported; } } } @@ -929,7 +929,7 @@ private void ParseHelper(string[] args) } else if (MatchSwitch(switchKey, "sta", "sta")) { - if (!Platform.IsWindowsDesktop) + if (!Platform.IsWindowsDesktop || !Platform.IsStaSupported) { SetCommandLineError( CommandLineParameterParserStrings.STANotImplemented); diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleControl.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleControl.cs index a157964c637..3b78674d89b 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleControl.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleControl.cs @@ -2654,7 +2654,7 @@ internal static void SetConsoleTextAttribute(ConsoleHandle consoleHandle, WORD a // CSI params? '#' [{}pq] // XTPUSHSGR ('{'), XTPOPSGR ('}'), or their aliases ('p' and 'q') // // Where: - // params: digit+ (';' params)? + // params: digit+ ((';' | ':') params)? // CSI: C0_CSI | C1_CSI // C0_CSI: \x001b '[' // ESC '[' // C1_CSI: \x009b @@ -2699,7 +2699,7 @@ internal static int ControlSequenceLength(string str, ref int offset) { c = str[offset++]; } - while ((offset < str.Length) && (char.IsDigit(c) || c == ';')); + while ((offset < str.Length) && (char.IsDigit(c) || (c == ';') || (c == ':'))); // Finally, handle the command characters for the specific sequences we // handle: @@ -2818,40 +2818,6 @@ internal static bool IsCJKOutputCodePage(out uint codePage) #region Cursor - /// - /// Wraps Win32 SetConsoleCursorPosition. - /// - /// - /// handle for the console where cursor position is set - /// - /// - /// location to which the cursor will be set - /// - /// - /// If Win32's SetConsoleCursorPosition fails - /// - internal static void SetConsoleCursorPosition(ConsoleHandle consoleHandle, Coordinates cursorPosition) - { - Dbg.Assert(!consoleHandle.IsInvalid, "ConsoleHandle is not valid"); - Dbg.Assert(!consoleHandle.IsClosed, "ConsoleHandle is closed"); - - ConsoleControl.COORD c; - - c.X = (short)cursorPosition.X; - c.Y = (short)cursorPosition.Y; - - bool result = NativeMethods.SetConsoleCursorPosition(consoleHandle.DangerousGetHandle(), c); - - if (!result) - { - int err = Marshal.GetLastWin32Error(); - - HostException e = CreateHostException(err, "SetConsoleCursorPosition", - ErrorCategory.ResourceUnavailable, ConsoleControlStrings.SetConsoleCursorPositionExceptionTemplate); - throw e; - } - } - /// /// Wraps Win32 GetConsoleCursorInfo. /// @@ -3159,10 +3125,6 @@ out DWORD numberOfEventsRead [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add); - [DllImport(PinvokeDllNames.SetConsoleCursorPositionDllName, SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool SetConsoleCursorPosition(NakedWin32Handle consoleOutput, COORD cursorPosition); - [DllImport(PinvokeDllNames.SetConsoleModeDllName, SetLastError = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetConsoleMode(NakedWin32Handle consoleHandle, DWORD mode); diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostRawUserInterface.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostRawUserInterface.cs index 2a6ea89176d..bca63be834b 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostRawUserInterface.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostRawUserInterface.cs @@ -190,14 +190,15 @@ public override set { - // cursor position can't be outside the buffer area - - ConsoleControl.CONSOLE_SCREEN_BUFFER_INFO bufferInfo; - - ConsoleHandle handle = GetBufferInfo(out bufferInfo); - - CheckCoordinateWithinBuffer(ref value, ref bufferInfo, "value"); - ConsoleControl.SetConsoleCursorPosition(handle, value); + try + { + Console.SetCursorPosition(value.X, value.Y); + } + catch (ArgumentOutOfRangeException) + { + // if screen buffer has changed, we cannot set it anywhere reasonable as the screen buffer + // might change again, so we ignore this + } } } diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs index ca2694bcabc..84f2f0355f2 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs @@ -387,14 +387,26 @@ internal static bool IsMinimalProgressRenderingEnabled() maxWidth = PSStyle.Instance.Progress.MaxWidth; } + // if the activity is really long, only use up to half the width + string activity; + if (Activity.Length > maxWidth / 2) + { + activity = Activity.Substring(0, maxWidth / 2) + PSObjectHelper.Ellipsis; + } + else + { + activity = Activity; + } + // 4 is for the extra space and square brackets below and one extra space - int barWidth = maxWidth - Activity.Length - indentation - 4; + int barWidth = maxWidth - activity.Length - indentation - 4; var sb = new StringBuilder(); int padding = maxWidth + PSStyle.Instance.Progress.Style.Length + PSStyle.Instance.Reverse.Length + PSStyle.Instance.ReverseOff.Length; sb.Append(PSStyle.Instance.Reverse); - if (StatusDescription.Length > barWidth - secRemainLength) + int maxStatusLength = barWidth - secRemainLength - 1; + if (maxStatusLength > 0 && StatusDescription.Length > barWidth - secRemainLength) { sb.Append(StatusDescription.Substring(0, barWidth - secRemainLength - 1)); sb.Append(PSObjectHelper.Ellipsis); @@ -404,10 +416,15 @@ internal static bool IsMinimalProgressRenderingEnabled() sb.Append(StatusDescription); } - sb.Append(string.Empty.PadRight(barWidth + PSStyle.Instance.Reverse.Length - sb.Length - secRemainLength)); + int emptyPadLength = barWidth + PSStyle.Instance.Reverse.Length - sb.Length - secRemainLength; + if (emptyPadLength > 0) + { + sb.Append(string.Empty.PadRight(emptyPadLength)); + } + sb.Append(secRemain); - if (PercentComplete > 0 && PercentComplete < 100) + if (PercentComplete > 0 && PercentComplete < 100 && barWidth > 0) { int barLength = PercentComplete * barWidth / 100; if (barLength >= barWidth) @@ -415,7 +432,10 @@ internal static bool IsMinimalProgressRenderingEnabled() barLength = barWidth - 1; } - sb.Insert(barLength + PSStyle.Instance.Reverse.Length, PSStyle.Instance.ReverseOff); + if (barLength < sb.Length) + { + sb.Insert(barLength + PSStyle.Instance.Reverse.Length, PSStyle.Instance.ReverseOff); + } } else { @@ -427,7 +447,7 @@ internal static bool IsMinimalProgressRenderingEnabled() "{0}{1}{2} [{3}]{4}", indent, PSStyle.Instance.Progress.Style, - Activity, + activity, sb.ToString(), PSStyle.Instance.Reset) .PadRight(padding)); diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/StopTranscriptCmdlet.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/StopTranscriptCmdlet.cs index 48b392f98e7..a48fa105437 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/StopTranscriptCmdlet.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/StopTranscriptCmdlet.cs @@ -15,7 +15,7 @@ namespace Microsoft.PowerShell.Commands public sealed class StopTranscriptCommand : PSCmdlet { /// - /// Starts the transcription. + /// Stops the transcription. /// protected override void diff --git a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj index caac215b23e..3e20f19b03a 100644 --- a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj +++ b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj index 52e5cf29283..4f24cdcbbca 100644 --- a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj +++ b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj @@ -18,9 +18,9 @@ - - - + + + @@ -30,7 +30,7 @@ - + diff --git a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj index a9f4bec70ea..2bd11329423 100644 --- a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj +++ b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Modules/nuget.config b/src/Modules/nuget.config index f5a7f806a36..b0fc73009da 100644 --- a/src/Modules/nuget.config +++ b/src/Modules/nuget.config @@ -3,7 +3,6 @@ - diff --git a/src/System.Management.Automation/AssemblyInfo.cs b/src/System.Management.Automation/AssemblyInfo.cs index 1963b331ef1..3c03c22e045 100644 --- a/src/System.Management.Automation/AssemblyInfo.cs +++ b/src/System.Management.Automation/AssemblyInfo.cs @@ -6,24 +6,14 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("powershell-tests,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: InternalsVisibleTo("powershell-perf,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] -[assembly: InternalsVisibleTo("Microsoft.Test.Management.Automation.GPowershell.Analyzers,PublicKey=00240000048000009400000006020000002400005253413100040000010001003f8c902c8fe7ac83af7401b14c1bd103973b26dfafb2b77eda478a2539b979b56ce47f36336741b4ec52bbc51fecd51ba23810cec47070f3e29a2261a2d1d08e4b2b4b457beaa91460055f78cc89f21cd028377af0cc5e6c04699b6856a1e49d5fad3ef16d3c3d6010f40df0a7d6cc2ee11744b5cfb42e0f19a52b8a29dc31b0")] - -#if NOT_SIGNED -// These attributes aren't every used, it's just a hack to get VS to not complain -// about access when editing using the project files that don't actually build. -[assembly: InternalsVisibleTo(@"Microsoft.PowerShell.Commands.Utility")] -[assembly: InternalsVisibleTo(@"Microsoft.PowerShell.Commands.Management")] -[assembly: InternalsVisibleTo(@"Microsoft.PowerShell.Security")] -[assembly: InternalsVisibleTo(@"System.Management.Automation.Remoting")] -[assembly: InternalsVisibleTo(@"Microsoft.PowerShell.ConsoleHost")] -#else [assembly: InternalsVisibleTo(@"Microsoft.PowerShell.Commands.Utility" + @",PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo(@"Microsoft.PowerShell.Commands.Management" + @",PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo(@"Microsoft.PowerShell.Security" + @",PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo(@"System.Management.Automation.Remoting" + @",PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: InternalsVisibleTo(@"Microsoft.PowerShell.ConsoleHost" + @",PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] -#endif +[assembly: InternalsVisibleTo(@"Microsoft.PowerShell.DscSubsystem" + @",PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] namespace System.Management.Automation { diff --git a/src/System.Management.Automation/CoreCLR/CorePsAssemblyLoadContext.cs b/src/System.Management.Automation/CoreCLR/CorePsAssemblyLoadContext.cs index 3dcc9ebdeb5..4a38eee4576 100644 --- a/src/System.Management.Automation/CoreCLR/CorePsAssemblyLoadContext.cs +++ b/src/System.Management.Automation/CoreCLR/CorePsAssemblyLoadContext.cs @@ -587,4 +587,35 @@ public static void SetPowerShellAssemblyLoadContext([MarshalAs(UnmanagedType.LPW PowerShellAssemblyLoadContext.InitializeSingleton(basePaths); } } + + /// + /// Provides helper functions to faciliate calling managed code from a native PowerShell host. + /// + public static unsafe class PowerShellUnsafeAssemblyLoad + { + /// + /// Load an assembly in memory from unmanaged code. + /// + /// + /// This API is covered by the experimental feature 'PSLoadAssemblyFromNativeCode', + /// and it may be deprecated and removed in future. + /// + /// Unmanaged pointer to assembly data buffer. + /// Size in bytes of the assembly data buffer. + /// Returns zero on success and non-zero on failure. + [UnmanagedCallersOnly] + public static int LoadAssemblyFromNativeMemory(IntPtr data, int size) + { + try + { + using var stream = new UnmanagedMemoryStream((byte*)data, size); + AssemblyLoadContext.Default.LoadFromStream(stream); + return 0; + } + catch + { + return -1; + } + } + } } diff --git a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs index b5e657d497f..8d9035f241b 100644 --- a/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs +++ b/src/System.Management.Automation/CoreCLR/CorePsPlatform.cs @@ -16,8 +16,6 @@ namespace System.Management.Automation /// public static class Platform { - private static string _tempDirectory = null; - /// /// True if the current platform is Linux. /// @@ -140,6 +138,21 @@ public static bool IsWindowsDesktop } } + /// + /// Gets a value indicating whether the underlying system supports single-threaded apartment. + /// + public static bool IsStaSupported + { + get + { +#if UNIX + return false; +#else + return _isStaSupported.Value; +#endif + } + } + #if UNIX // Gets the location for cache and config folders. internal static readonly string CacheDirectory = Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE); @@ -148,6 +161,22 @@ public static bool IsWindowsDesktop // Gets the location for cache and config folders. internal static readonly string CacheDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Microsoft\PowerShell"; internal static readonly string ConfigDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal) + @"\PowerShell"; + private static readonly Lazy _isStaSupported = new Lazy(() => + { + // See objbase.h + const int COINIT_APARTMENTTHREADED = 0x2; + const int E_NOTIMPL = unchecked((int)0X80004001); + int result = Windows.NativeMethods.CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED); + + // If 0 is returned the thread has been initialized for the first time + // as an STA and thus supported and needs to be uninitialized. + if (result > 0) + { + Windows.NativeMethods.CoUninitialize(); + } + + return result != E_NOTIMPL; + }); private static bool? _isNanoServer = null; private static bool? _isIoT = null; @@ -170,6 +199,8 @@ public static bool IsWindowsDesktop "WSMan.format.ps1xml" }; + private static string _tempDirectory = null; + /// /// Some common environment variables used in PS have different /// names in different OS platforms. @@ -557,6 +588,21 @@ internal static int NonWindowsGetProcessParentPid(int pid) return IsMacOS ? Unix.NativeMethods.GetPPid(pid) : Unix.GetProcFSParentPid(pid); } + internal static class Windows + { + /// The native methods class. + internal static class NativeMethods + { + private const string ole32Lib = "api-ms-win-core-com-l1-1-0.dll"; + + [DllImport(ole32Lib)] + internal static extern int CoInitializeEx(IntPtr reserve, int coinit); + + [DllImport(ole32Lib)] + internal static extern void CoUninitialize(); + } + } + // Please note that `Win32Exception(Marshal.GetLastWin32Error())` // works *correctly* on Linux in that it creates an exception with // the string perror would give you for the last set value of errno. diff --git a/src/System.Management.Automation/DscSupport/JsonCimDSCParser.cs b/src/System.Management.Automation/DscSupport/JsonCimDSCParser.cs deleted file mode 100755 index c2221cb3f33..00000000000 --- a/src/System.Management.Automation/DscSupport/JsonCimDSCParser.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Management.Automation; -using System.Security; - -namespace Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform -{ - /// - /// Class that does high level Cim schema parsing. - /// - internal class CimDSCParser - { - private readonly JsonDeserializer _jsonDeserializer; - - internal CimDSCParser() - { - _jsonDeserializer = JsonDeserializer.Create(); - } - - internal IEnumerable ParseSchemaJson(string filePath, bool useNewRunspace = false) - { - try - { - string json = File.ReadAllText(filePath); - string fileNameDefiningClass = Path.GetFileNameWithoutExtension(filePath); - int dotIndex = fileNameDefiningClass.IndexOf(".schema", StringComparison.InvariantCultureIgnoreCase); - if (dotIndex != -1) - { - fileNameDefiningClass = fileNameDefiningClass.Substring(0, dotIndex); - } - - IEnumerable result = _jsonDeserializer.DeserializeClasses(json, useNewRunspace); - foreach (dynamic classObject in result) - { - string superClassName = classObject.SuperClassName; - string className = classObject.ClassName; - if (string.Equals(superClassName, "OMI_BaseResource", StringComparison.OrdinalIgnoreCase)) - { - // Get the name of the file without schema.mof/json extension - if (!className.Equals(fileNameDefiningClass, StringComparison.OrdinalIgnoreCase)) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException( - ParserStrings.ClassNameNotSameAsDefiningFile, className, fileNameDefiningClass); - throw e; - } - } - } - - return result; - } - catch (Exception exception) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException( - exception, ParserStrings.CimDeserializationError, filePath); - - e.SetErrorId("CimDeserializationError"); - throw e; - } - } - } -} diff --git a/src/System.Management.Automation/DscSupport/JsonDeserializer.cs b/src/System.Management.Automation/DscSupport/JsonDeserializer.cs deleted file mode 100755 index 319560f0a09..00000000000 --- a/src/System.Management.Automation/DscSupport/JsonDeserializer.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Management.Automation; -using System.Management.Automation.Runspaces; - -namespace Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform -{ - internal class JsonDeserializer - { - #region Constructors - - /// - /// Instantiates a default deserializer. - /// - /// Default deserializer. - public static JsonDeserializer Create() - { - return new JsonDeserializer(); - } - - #endregion Constructors - - #region Methods - - /// - /// Returns schema of Cim classes from specified json file. - /// - /// Json text to deserialize. - /// If a new runspace should be used. - /// Deserialized PSObjects. - public IEnumerable DeserializeClasses(string json, bool useNewRunspace = false) - { - if (string.IsNullOrEmpty(json)) - { - throw new ArgumentNullException(nameof(json)); - } - - System.Management.Automation.PowerShell powerShell = null; - - if (useNewRunspace) - { - // currently using RunspaceMode.NewRunspace will reset PSModulePath env var for the entire process - // this is something we want to avoid in DSC GuestConfigAgent scenario, so we use following workaround - var s_iss = InitialSessionState.CreateDefault(); - s_iss.EnvironmentVariables.Add( - new SessionStateVariableEntry( - "PSModulePath", - Environment.GetEnvironmentVariable("PSModulePath"), - description: null)); - powerShell = System.Management.Automation.PowerShell.Create(s_iss); - } - else - { - powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); - } - - using (powerShell) - { - return powerShell.AddCommand("Microsoft.PowerShell.Utility\\ConvertFrom-Json") - .AddParameter("InputObject", json) - .AddParameter("Depth", 100) // maximum supported by cmdlet - .Invoke(); - } - } - - #endregion Methods - } -} diff --git a/src/System.Management.Automation/DscSupport/JsonDscClassCache.cs b/src/System.Management.Automation/DscSupport/JsonDscClassCache.cs deleted file mode 100755 index 5c3da715ccc..00000000000 --- a/src/System.Management.Automation/DscSupport/JsonDscClassCache.cs +++ /dev/null @@ -1,2498 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Language; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Security; -using System.Text; -using System.Text.RegularExpressions; - -using Microsoft.PowerShell.Commands; - -namespace Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform -{ - /// - /// Class that defines Dsc cache entries. - /// - internal class DscClassCacheEntry - { - /// - /// Initializes a new instance of the class. - /// - public DscClassCacheEntry() - : this(DSCResourceRunAsCredential.Default, isImportedImplicitly: false, cimClassInstance: null, modulePath: string.Empty) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Run as credential value. - /// Resource is imported implicitly. - /// Class definition. - /// Path of module defining the class. - public DscClassCacheEntry(DSCResourceRunAsCredential dscResourceRunAsCredential, bool isImportedImplicitly, PSObject cimClassInstance, string modulePath) - { - DscResRunAsCred = dscResourceRunAsCredential; - IsImportedImplicitly = isImportedImplicitly; - CimClassInstance = cimClassInstance; - ModulePath = modulePath; - } - - /// - /// Gets or sets the RunAs Credentials that this DSC resource will use. - /// - public DSCResourceRunAsCredential DscResRunAsCred { get; set; } - - /// - /// Gets or sets a value indicating if we have implicitly imported this resource. - /// - public bool IsImportedImplicitly { get; set; } - - /// - /// Gets or sets CimClass instance for this resource. - /// - public PSObject CimClassInstance { get; set; } - - /// - /// Gets or sets path of the implementing module for this resource. - /// - public string ModulePath { get; set; } - } - - /// - /// DSC class cache for this runspace. - /// - [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes", - Justification = "Needed Internal use only")] - public static class DscClassCache - { - private static readonly HashSet s_reservedDynamicKeywords = new HashSet(new[] { "Synchronization", "Certificate", "IIS", "SQL" }, StringComparer.OrdinalIgnoreCase); - - private static readonly HashSet s_reservedProperties = new HashSet(new[] { "Require", "Trigger", "Notify", "Before", "After", "Subscribe" }, StringComparer.OrdinalIgnoreCase); - - /// - /// Experimental feature name for DSC v3. - /// - public const string DscExperimentalFeatureName = "PS7DscSupport"; - - private static readonly PSTraceSource s_tracer = PSTraceSource.GetTracer("DSC", "DSC Class Cache"); - - // Constants for items in the module qualified name (Module\Version\ClassName) - private const int ModuleNameIndex = 0; - private const int ModuleVersionIndex = 1; - private const int ClassNameIndex = 2; - private const int FriendlyNameIndex = 3; - - // Create a HashSet for fast lookup. According to MSDN, the time complexity of search for an element in a HashSet is O(1) - private static readonly HashSet s_hiddenResourceCache = - new HashSet(StringComparer.OrdinalIgnoreCase) { "MSFT_BaseConfigurationProviderRegistration", "MSFT_CimConfigurationProviderRegistration", "MSFT_PSConfigurationProviderRegistration" }; - - // A collection to prevent circular importing case when Import-DscResource does not have a module specified - [ThreadStatic] - private static readonly HashSet t_currentImportDscResourceInvocations = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Gets DSC class cache for this runspace. - /// Cache stores the DSCRunAsBehavior, cim class and boolean to indicate if an Inbox resource has been implicitly imported. - /// - private static Dictionary ClassCache - { - get => t_classCache ??= new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - [ThreadStatic] - private static Dictionary t_classCache; - - /// - /// Gets DSC class cache for GuestConfig; it is similar to ClassCache, but maintains values between operations. - /// - private static Dictionary GuestConfigClassCache - { - get => t_guestConfigClassCache ??= new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - [ThreadStatic] - private static Dictionary t_guestConfigClassCache; - - /// - /// DSC classname to source module mapper. - /// - private static Dictionary> ByClassModuleCache - => t_byClassModuleCache ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); - - [ThreadStatic] - private static Dictionary> t_byClassModuleCache; - - /// - /// Default ModuleName and ModuleVersion to use. - /// - private static readonly Tuple s_defaultModuleInfoForResource = new Tuple("PSDesiredStateConfiguration", new Version(3, 0)); - - /// - /// When this property is set to true, DSC Cache will cache multiple versions of a resource. - /// That means it will cache duplicate resource classes (class names for a resource in two different module versions are same). - /// NOTE: This property should be set to false for DSC compiler related methods/functionality, such as Import-DscResource, - /// because the Mof serializer does not support deserialization of classes with different versions. - /// - [ThreadStatic] - private static bool t_cacheResourcesFromMultipleModuleVersions; - - private static bool CacheResourcesFromMultipleModuleVersions - { - get - { - return t_cacheResourcesFromMultipleModuleVersions; - } - - set - { - t_cacheResourcesFromMultipleModuleVersions = value; - } - } - - [ThreadStatic] - private static bool t_newApiIsUsed = false; - - /// - /// Flag shows if PS7 DSC APIs were used. - /// - public static bool NewApiIsUsed - { - get - { - return t_newApiIsUsed; - } - - set - { - t_newApiIsUsed = value; - } - } - - /// - /// Initialize the class cache with the default classes in $ENV:SystemDirectory\Configuration. - /// - public static void Initialize() - { - Initialize(errors: null, modulePathList: null); - } - - /// - /// Initialize the class cache with default classes that come with PSDesiredStateConfiguration module. - /// - /// Collection of any errors encountered during initialization. - /// List of module path from where DSC PS modules will be loaded. - public static void Initialize(Collection errors, List modulePathList) - { - s_tracer.WriteLine("Initializing DSC class cache"); - - // Load the base schema files. - ClearCache(); - var dscConfigurationDirectory = Environment.GetEnvironmentVariable("DSC_HOME"); - if (string.IsNullOrEmpty(dscConfigurationDirectory)) - { - var moduleInfos = ModuleCmdletBase.GetModuleIfAvailable(new Microsoft.PowerShell.Commands.ModuleSpecification() - { - Name = "PSDesiredStateConfiguration", - - // Version in the next line is actually MinimumVersion - Version = new Version(3, 0, 0) - }); - - if (moduleInfos.Count > 0) - { - // to be consistent with Import-Module behavior, we use the first occurrence that we find in PSModulePath - var moduleDirectory = Path.GetDirectoryName(moduleInfos[0].Path); - dscConfigurationDirectory = Path.Join(moduleDirectory, "Configuration"); - } - else - { - // when all else has failed use location of system-wide PS module directory (i.e. /usr/local/share/powershell/Modules) as backup - dscConfigurationDirectory = Path.Join(ModuleIntrinsics.GetSharedModulePath(), "PSDesiredStateConfiguration", "Configuration"); - } - } - - if (!Directory.Exists(dscConfigurationDirectory)) - { - throw new DirectoryNotFoundException(string.Format(ParserStrings.PsDscMissingSchemaStore, dscConfigurationDirectory)); - } - - var resourceBaseFile = Path.Join(dscConfigurationDirectory, "BaseRegistration", "BaseResource.schema.json"); - ImportBaseClasses(resourceBaseFile, s_defaultModuleInfoForResource, errors, false); - var metaConfigFile = Path.Join(dscConfigurationDirectory, "BaseRegistration", "MSFT_DSCMetaConfiguration.json"); - ImportBaseClasses(metaConfigFile, s_defaultModuleInfoForResource, errors, false); - } - - /// - /// Import base classes from the given file. - /// - /// Path to schema file. - /// Module information. - /// Error collection that will be shown to the user. - /// Flag for implicitly imported resource. - /// Class objects from schema file. - public static IEnumerable ImportBaseClasses(string path, Tuple moduleInfo, Collection errors, bool importInBoxResourcesImplicitly) - { - if (string.IsNullOrEmpty(path)) - { - throw PSTraceSource.NewArgumentNullException(nameof(path)); - } - - s_tracer.WriteLine("DSC ClassCache: importing file: {0}", path); - - var parser = new CimDSCParser(); - - IEnumerable classes = null; - try - { - classes = parser.ParseSchemaJson(path); - } - catch (PSInvalidOperationException e) - { - // Ignore modules with invalid schemas. - s_tracer.WriteLine("DSC ClassCache: Error importing file '{0}', with error '{1}'. Skipping file.", path, e); - if (errors != null) - { - errors.Add(e); - } - } - - if (classes != null) - { - foreach (dynamic c in classes) - { - var className = c.ClassName; - - if (string.IsNullOrEmpty(className)) - { - // ClassName is empty - skipping class import - continue; - } - - string alias = GetFriendlyName(c); - var friendlyName = string.IsNullOrEmpty(alias) ? className : alias; - string moduleQualifiedResourceName = GetModuleQualifiedResourceName(moduleInfo.Item1, moduleInfo.Item2.ToString(), className, friendlyName); - DscClassCacheEntry cimClassInfo; - - if (ClassCache.TryGetValue(moduleQualifiedResourceName, out cimClassInfo)) - { - if (errors != null) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException( - ParserStrings.DuplicateCimClassDefinition, className, path, cimClassInfo.ModulePath); - - e.SetErrorId("DuplicateCimClassDefinition"); - errors.Add(e); - } - - continue; - } - - if (s_hiddenResourceCache.Contains(className)) - { - continue; - } - - var classCacheEntry = new DscClassCacheEntry(DSCResourceRunAsCredential.NotSupported, importInBoxResourcesImplicitly, c, path); - ClassCache[moduleQualifiedResourceName] = classCacheEntry; - GuestConfigClassCache[moduleQualifiedResourceName] = classCacheEntry; - ByClassModuleCache[className] = moduleInfo; - } - - var sb = new System.Text.StringBuilder(); - foreach (dynamic c in classes) - { - sb.Append(c.ClassName); - sb.Append(','); - } - - s_tracer.WriteLine("DSC ClassCache: loading file '{0}' added the following classes to the cache: {1}", path, sb.ToString()); - } - else - { - s_tracer.WriteLine("DSC ClassCache: loading file '{0}' added no classes to the cache."); - } - - return classes; - } - - /// - /// Get text from SecureString. - /// - /// Value of SecureString. - /// Decoded string. - public static string GetStringFromSecureString(SecureString value) - { - string passwordValueToAdd = string.Empty; - - if (value != null) - { - IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(value); - passwordValueToAdd = Marshal.PtrToStringUni(ptr); - Marshal.ZeroFreeCoTaskMemUnicode(ptr); - } - - return passwordValueToAdd; - } - - /// - /// Clear out the existing collection of CIM classes and associated keywords. - /// - public static void ClearCache() - { - if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) - { - throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); - } - - s_tracer.WriteLine("DSC class: clearing the cache and associated keywords."); - ClassCache.Clear(); - ByClassModuleCache.Clear(); - CacheResourcesFromMultipleModuleVersions = false; - t_currentImportDscResourceInvocations.Clear(); - } - - private static string GetModuleQualifiedResourceName(string moduleName, string moduleVersion, string className, string resourceName) - { - return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}\\{2}\\{3}", moduleName, moduleVersion, className, resourceName); - } - - private static List> FindResourceInCache(string moduleName, string className, string resourceName) - { - return (from cacheEntry in ClassCache - let splittedName = cacheEntry.Key.Split(Utils.Separators.Backslash) - let cachedClassName = splittedName[ClassNameIndex] - let cachedModuleName = splittedName[ModuleNameIndex] - let cachedResourceName = splittedName[FriendlyNameIndex] - where (string.Equals(cachedResourceName, resourceName, StringComparison.OrdinalIgnoreCase) - || (string.Equals(cachedClassName, className, StringComparison.OrdinalIgnoreCase) - && string.Equals(cachedModuleName, moduleName, StringComparison.OrdinalIgnoreCase))) - select cacheEntry).ToList(); - } - - /// - /// Returns class declaration from GuestConfigClassCache. - /// - /// Module name. - /// Module version. - /// Name of the class. - /// Friendly name of the resource. - /// Class declaration from cache. - public static PSObject GetGuestConfigCachedClass(string moduleName, string moduleVersion, string className, string resourceName) - { - if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) - { - throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); - } - - var moduleQualifiedResourceName = GetModuleQualifiedResourceName(moduleName, moduleVersion, className, string.IsNullOrEmpty(resourceName) ? className : resourceName); - DscClassCacheEntry classCacheEntry = null; - if (GuestConfigClassCache.TryGetValue(moduleQualifiedResourceName, out classCacheEntry)) - { - return classCacheEntry.CimClassInstance; - } - else - { - // if class was not found with current ResourceName then it may be a class with non-empty FriendlyName that caller does not know, so perform a broad search - string partialClassPath = string.Join('\\', moduleName, moduleVersion, className, string.Empty); - foreach (string key in GuestConfigClassCache.Keys) - { - if (key.StartsWith(partialClassPath)) - { - return GuestConfigClassCache[key].CimClassInstance; - } - } - - return null; - } - } - - /// - /// Clears GuestConfigClassCache. - /// - public static void ClearGuestConfigClassCache() - { - GuestConfigClassCache.Clear(); - } - - private static bool IsMagicProperty(string propertyName) - { - return System.Text.RegularExpressions.Regex.Match(propertyName, "^(ResourceId|SourceInfo|ModuleName|ModuleVersion|ConfigurationName)$", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success; - } - - private static string GetFriendlyName(dynamic cimClass) - { - return cimClass.FriendlyName; - } - - /// - /// Method to get the cached classes in the form of DynamicKeyword. - /// - /// Dynamic keyword collection. - public static Collection GetKeywordsFromCachedClasses() - { - if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) - { - throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); - } - - Collection keywords = new Collection(); - - foreach (KeyValuePair cachedClass in ClassCache) - { - string[] splittedName = cachedClass.Key.Split(Utils.Separators.Backslash); - string moduleName = splittedName[ModuleNameIndex]; - string moduleVersion = splittedName[ModuleVersionIndex]; - - var keyword = CreateKeywordFromCimClass(moduleName, Version.Parse(moduleVersion), cachedClass.Value.CimClassInstance, cachedClass.Value.DscResRunAsCred); - if (keyword is not null) - { - keywords.Add(keyword); - } - } - - return keywords; - } - - private static void CreateAndRegisterKeywordFromCimClass(string moduleName, Version moduleVersion, PSObject cimClass, Dictionary functionsToDefine, DSCResourceRunAsCredential runAsBehavior) - { - var keyword = CreateKeywordFromCimClass(moduleName, moduleVersion, cimClass, runAsBehavior); - if (keyword is null) - { - return; - } - - // keyword is already defined and we don't allow redefine it - if (!CacheResourcesFromMultipleModuleVersions && DynamicKeyword.ContainsKeyword(keyword.Keyword)) - { - var oldKeyword = DynamicKeyword.GetKeyword(keyword.Keyword); - if (oldKeyword.ImplementingModule is null || - !oldKeyword.ImplementingModule.Equals(moduleName, StringComparison.OrdinalIgnoreCase) || oldKeyword.ImplementingModuleVersion != moduleVersion) - { - var e = PSTraceSource.NewInvalidOperationException(ParserStrings.DuplicateKeywordDefinition, keyword.Keyword); - e.SetErrorId("DuplicateKeywordDefinition"); - throw e; - } - } - - // Add the dynamic keyword to the table - DynamicKeyword.AddKeyword(keyword); - - // And now define the driver functions in the current scope... - if (functionsToDefine != null) - { - functionsToDefine[moduleName + "\\" + keyword.Keyword] = CimKeywordImplementationFunction; - } - } - - private static DynamicKeyword CreateKeywordFromCimClass(string moduleName, Version moduleVersion, dynamic cimClass, DSCResourceRunAsCredential runAsBehavior) - { - var resourceName = cimClass.ClassName; - string alias = GetFriendlyName(cimClass); - var keywordString = string.IsNullOrEmpty(alias) ? resourceName : alias; - - // Skip all of the base, meta, registration and other classes that are not intended to be used directly by a script author - if (System.Text.RegularExpressions.Regex.Match(keywordString, "^OMI_Base|^OMI_.*Registration", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success) - { - return null; - } - - var keyword = new DynamicKeyword() - { - BodyMode = DynamicKeywordBodyMode.Hashtable, - Keyword = keywordString, - ResourceName = resourceName, - ImplementingModule = moduleName, - ImplementingModuleVersion = moduleVersion, - SemanticCheck = CheckMandatoryPropertiesPresent - }; - - // If it's one of reserved dynamic keyword, mark it - if (s_reservedDynamicKeywords.Contains(keywordString)) - { - keyword.IsReservedKeyword = true; - } - - // see if it's a resource type i.e. it inherits from OMI_BaseResource - bool isResourceType = false; - - // previous version of this code was the only place that referenced CimSuperClass - // so to simplify things we just check superclass to be OMI_BaseResource - // with assumption that current code will not work for multi-level class inheritance (which is never used in practice according to DSC team) - // this simplification allows us to avoid linking objects together using CimSuperClass field during deserialization - if ((!string.IsNullOrEmpty(cimClass.SuperClassName)) && string.Equals("OMI_BaseResource", cimClass.SuperClassName, StringComparison.OrdinalIgnoreCase)) - { - isResourceType = true; - } - - // If it's a resource type, then a resource name is required. - keyword.NameMode = isResourceType ? DynamicKeywordNameMode.NameRequired : DynamicKeywordNameMode.NoName; - - // Add the settable properties to the keyword object - if (cimClass.ClassProperties != null) - { - foreach (var prop in cimClass.ClassProperties) - { - // If the property has the Read qualifier, skip it. - if (string.Equals(prop.Qualifiers?.Read?.ToString(), "True", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - // If it's one of our magic properties, skip it - if (IsMagicProperty(prop.Name)) - { - continue; - } - - if (runAsBehavior == DSCResourceRunAsCredential.NotSupported) - { - if (string.Equals(prop.Name, "PsDscRunAsCredential", StringComparison.OrdinalIgnoreCase)) - { - // skip adding PsDscRunAsCredential to the dynamic word for the dsc resource. - continue; - } - } - - // If it's one of our reserved properties, save it for error reporting - if (s_reservedProperties.Contains(prop.Name)) - { - keyword.HasReservedProperties = true; - continue; - } - - // Otherwise, add it to the Keyword List. - var keyProp = new System.Management.Automation.Language.DynamicKeywordProperty(); - keyProp.Name = prop.Name; - - // Copy the type name string. If it's an embedded instance, need to grab it from the ReferenceClassName - bool referenceClassNameIsNullOrEmpty = string.IsNullOrEmpty(prop.ReferenceClassName); - if (prop.CimType == "Instance" && !referenceClassNameIsNullOrEmpty) - { - keyProp.TypeConstraint = prop.ReferenceClassName; - } - else if (prop.CimType == "InstanceArray" && !referenceClassNameIsNullOrEmpty) - { - keyProp.TypeConstraint = prop.ReferenceClassName + "[]"; - } - else - { - keyProp.TypeConstraint = prop.CimType.ToString(); - } - - // Check to see if there is a Values attribute and save the list of allowed values if so. - var values = prop.Qualifiers?.Values; - if (values is not null) - { - foreach (var val in values) - { - keyProp.Values.Add(val.ToString()); - } - } - - // Check to see if there is a ValueMap attribute and save the list of allowed values if so. - var nativeValueMap = prop.Qualifiers?.ValueMap; - List valueMap = null; - if (nativeValueMap is not null) - { - valueMap = new List(); - foreach (var val in nativeValueMap) - { - valueMap.Add(val.ToString()); - } - } - - // Check to see if this property has the Required qualifier associated with it. - if (string.Equals(prop.Qualifiers?.Required?.ToString(), "True", StringComparison.OrdinalIgnoreCase)) - { - keyProp.Mandatory = true; - } - - // Check to see if this property has the Key qualifier associated with it. - if (string.Equals(prop.Qualifiers?.Key?.ToString(), "True", StringComparison.OrdinalIgnoreCase)) - { - keyProp.Mandatory = true; - keyProp.IsKey = true; - } - - // set the property to mandatory is specified for the resource. - if (runAsBehavior == DSCResourceRunAsCredential.Mandatory) - { - if (string.Equals(prop.Name, "PsDscRunAsCredential", StringComparison.OrdinalIgnoreCase)) - { - keyProp.Mandatory = true; - } - } - - if (valueMap is not null && keyProp.Values.Count > 0) - { - if (valueMap.Count != keyProp.Values.Count) - { - s_tracer.WriteLine( - "DSC CreateDynamicKeywordFromClass: the count of values for qualifier 'Values' and 'ValueMap' doesn't match. count of 'Values': {0}, count of 'ValueMap': {1}. Skip the keyword '{2}'.", - keyProp.Values.Count, - valueMap.Count, - keyword.Keyword); - return null; - } - - for (int index = 0; index < valueMap.Count; index++) - { - string key = keyProp.Values[index]; - string value = valueMap[index]; - - if (keyProp.ValueMap.ContainsKey(key)) - { - s_tracer.WriteLine( - "DSC CreateDynamicKeywordFromClass: same string value '{0}' appears more than once in qualifier 'Values'. Skip the keyword '{1}'.", - key, - keyword.Keyword); - return null; - } - - keyProp.ValueMap.Add(key, value); - } - } - - keyword.Properties.Add(prop.Name, keyProp); - } - } - - // update specific keyword with range constraints - UpdateKnownRestriction(keyword); - - return keyword; - } - - private static void UpdateKnownRestriction(DynamicKeyword keyword) - { - const int RefreshFrequencyMin = 30; - const int RefreshFrequencyMax = 44640; - - const int ConfigurationModeFrequencyMin = 15; - const int ConfigurationModeFrequencyMax = 44640; - - if ( - string.Equals( - keyword.ResourceName, - "MSFT_DSCMetaConfigurationV2", - StringComparison.OrdinalIgnoreCase) - || - string.Equals( - keyword.ResourceName, - "MSFT_DSCMetaConfiguration", - StringComparison.OrdinalIgnoreCase)) - { - if (keyword.Properties["RefreshFrequencyMins"] is not null) - { - keyword.Properties["RefreshFrequencyMins"].Range = new Tuple(RefreshFrequencyMin, RefreshFrequencyMax); - } - - if (keyword.Properties["ConfigurationModeFrequencyMins"] != null) - { - keyword.Properties["ConfigurationModeFrequencyMins"].Range = new Tuple(ConfigurationModeFrequencyMin, ConfigurationModeFrequencyMax); - } - - if (keyword.Properties["DebugMode"] is not null) - { - keyword.Properties["DebugMode"].Values.Remove("ResourceScriptBreakAll"); - keyword.Properties["DebugMode"].ValueMap.Remove("ResourceScriptBreakAll"); - } - } - } - - /// - /// Load the default system CIM classes and create the corresponding keywords. - /// - /// Collection of any errors encountered while loading keywords. - public static void LoadDefaultCimKeywords(Collection errors) - { - LoadDefaultCimKeywords(functionsToDefine: null, errors, modulePathList: null, cacheResourcesFromMultipleModuleVersions: false); - } - - /// - /// Load the default system CIM classes and create the corresponding keywords. - /// - /// A dictionary to add the defined functions to, may be null. - public static void LoadDefaultCimKeywords(Dictionary functionsToDefine) - { - LoadDefaultCimKeywords(functionsToDefine, errors: null, modulePathList: null, cacheResourcesFromMultipleModuleVersions: false); - } - - /// - /// Load the default system CIM classes and create the corresponding keywords. - /// - /// Collection of any errors encountered while loading keywords. - /// Allow caching the resources from multiple versions of modules. - public static void LoadDefaultCimKeywords(Collection errors, bool cacheResourcesFromMultipleModuleVersions) - { - LoadDefaultCimKeywords(functionsToDefine: null, errors, modulePathList: null, cacheResourcesFromMultipleModuleVersions); - } - - /// - /// Load the default system CIM classes and create the corresponding keywords. - /// - /// A dictionary to add the defined functions to, may be null. - /// Collection of any errors encountered while loading keywords. - /// List of module path from where DSC PS modules will be loaded. - /// Allow caching the resources from multiple versions of modules. - private static void LoadDefaultCimKeywords( - Dictionary functionsToDefine, - Collection errors, - List modulePathList, - bool cacheResourcesFromMultipleModuleVersions) - { - if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) - { - Exception exception = new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); - errors.Add(exception); - return; - } - - NewApiIsUsed = true; - DynamicKeyword.Reset(); - Initialize(errors, modulePathList); - - // Initialize->ClearCache resets CacheResourcesFromMultipleModuleVersions to false, - // workaround is to set it after Initialize method call. - // Initialize method imports all the Inbox resources and internal classes which belongs to only one version - // of the module, so it is ok if this property is not set during cache initialization. - CacheResourcesFromMultipleModuleVersions = cacheResourcesFromMultipleModuleVersions; - - foreach (dynamic cimClass in ClassCache.Values) - { - var className = cimClass.CimClassInstance.ClassName; - var moduleInfo = ByClassModuleCache[className]; - CreateAndRegisterKeywordFromCimClass(moduleInfo.Item1, moduleInfo.Item2, cimClass.CimClassInstance, functionsToDefine, cimClass.DscResRunAsCred); - } - - // And add the Node keyword definitions - if (!DynamicKeyword.ContainsKeyword("Node")) - { - // Implement dispatch to the Node keyword. - var nodeKeyword = new DynamicKeyword() - { - BodyMode = DynamicKeywordBodyMode.ScriptBlock, - ImplementingModule = s_defaultModuleInfoForResource.Item1, - ImplementingModuleVersion = s_defaultModuleInfoForResource.Item2, - NameMode = DynamicKeywordNameMode.NameRequired, - Keyword = "Node", - }; - DynamicKeyword.AddKeyword(nodeKeyword); - } - - // And add the Import-DscResource keyword definitions - if (!DynamicKeyword.ContainsKeyword("Import-DscResource")) - { - // Implement dispatch to the Node keyword. - var nodeKeyword = new DynamicKeyword() - { - BodyMode = DynamicKeywordBodyMode.Command, - ImplementingModule = s_defaultModuleInfoForResource.Item1, - ImplementingModuleVersion = s_defaultModuleInfoForResource.Item2, - NameMode = DynamicKeywordNameMode.NoName, - Keyword = "Import-DscResource", - MetaStatement = true, - PostParse = ImportResourcePostParse, - SemanticCheck = ImportResourceCheckSemantics - }; - DynamicKeyword.AddKeyword(nodeKeyword); - } - } - - // This function is called after parsing the Import-DscResource keyword and it's arguments, but before parsing anything else. - private static ParseError[] ImportResourcePostParse(DynamicKeywordStatementAst ast) - { - var elements = Ast.CopyElements(ast.CommandElements); - var commandAst = new CommandAst(ast.Extent, elements, TokenKind.Unknown, null); - - const string NameParam = "Name"; - const string ModuleNameParam = "ModuleName"; - const string ModuleVersionParam = "ModuleVersion"; - - StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, false); - - var errorList = new List(); - foreach (var bindingException in bindingResult.BindingExceptions.Values) - { - errorList.Add(new ParseError(bindingException.CommandElement.Extent, "ParameterBindingException", bindingException.BindingException.Message)); - } - - ParameterBindingResult moduleNameBindingResult = null; - ParameterBindingResult resourceNameBindingResult = null; - ParameterBindingResult moduleVersionBindingResult = null; - - foreach (var binding in bindingResult.BoundParameters) - { - // Error case when positional parameter values are specified - var boundParameterName = binding.Key; - var parameterBindingResult = binding.Value; - if (boundParameterName.All(char.IsDigit)) - { - errorList.Add(new ParseError(parameterBindingResult.Value.Extent, "ImportDscResourcePositionalParamsNotSupported", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourcePositionalParamsNotSupported))); - continue; - } - - if (NameParam.StartsWith(boundParameterName, StringComparison.OrdinalIgnoreCase)) - { - resourceNameBindingResult = parameterBindingResult; - } - else if (ModuleNameParam.StartsWith(boundParameterName, StringComparison.OrdinalIgnoreCase)) - { - moduleNameBindingResult = parameterBindingResult; - } - else if (ModuleVersionParam.StartsWith(boundParameterName, StringComparison.OrdinalIgnoreCase)) - { - moduleVersionBindingResult = parameterBindingResult; - } - else - { - errorList.Add(new ParseError(parameterBindingResult.Value.Extent, "ImportDscResourceNeedParams", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); - } - } - - if (errorList.Count == 0 && moduleNameBindingResult == null && resourceNameBindingResult == null) - { - errorList.Add(new ParseError(ast.Extent, "ImportDscResourceNeedParams", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); - } - - // Check here if Version is specified but modulename is not specified - if (moduleVersionBindingResult != null && moduleNameBindingResult == null) - { - // only add this error again to the error list if resources is not null - // if resources and modules are both null we have already added this error in collection - // we do not want to do this twice. since we are giving same error ImportDscResourceNeedParams in both cases - // once we have different error messages for 2 scenarios we can remove this check - if (resourceNameBindingResult is not null) - { - errorList.Add(new ParseError(ast.Extent, "ImportDscResourceNeedModuleNameWithModuleVersion", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); - } - } - - string[] resourceNames = null; - if (resourceNameBindingResult is not null) - { - object resourceName = null; - if (!IsConstantValueVisitor.IsConstant(resourceNameBindingResult.Value, out resourceName, true, true) || - !LanguagePrimitives.TryConvertTo(resourceName, out resourceNames)) - { - errorList.Add(new ParseError(resourceNameBindingResult.Value.Extent, "RequiresInvalidStringArgument", string.Format(CultureInfo.CurrentCulture, ParserStrings.RequiresInvalidStringArgument, NameParam))); - } - } - - System.Version moduleVersion = null; - if (moduleVersionBindingResult is not null) - { - object moduleVer = null; - if (!IsConstantValueVisitor.IsConstant(moduleVersionBindingResult.Value, out moduleVer, true, true)) - { - errorList.Add(new ParseError(moduleVersionBindingResult.Value.Extent, "RequiresArgumentMustBeConstant", ParserStrings.RequiresArgumentMustBeConstant)); - } - - if (moduleVer is double) - { - // this happens in case -ModuleVersion 1.0, then use extent text for that. - // The better way to do it would be define static binding API against CommandInfo, that holds information about parameter types. - // This way, we can avoid this ugly special-casing and say that -ModuleVersion has type [System.Version]. - moduleVer = moduleVersionBindingResult.Value.Extent.Text; - } - - if (!LanguagePrimitives.TryConvertTo(moduleVer, out moduleVersion)) - { - errorList.Add(new ParseError(moduleVersionBindingResult.Value.Extent, "RequiresVersionInvalid", ParserStrings.RequiresVersionInvalid)); - } - } - - ModuleSpecification[] moduleSpecifications = null; - if (moduleNameBindingResult is not null) - { - object moduleName = null; - if (!IsConstantValueVisitor.IsConstant(moduleNameBindingResult.Value, out moduleName, true, true)) - { - errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "RequiresArgumentMustBeConstant", ParserStrings.RequiresArgumentMustBeConstant)); - } - - if (LanguagePrimitives.TryConvertTo(moduleName, out moduleSpecifications)) - { - // if resourceNames are specified then we can not specify multiple modules name - if (moduleSpecifications is not null && moduleSpecifications.Length > 1 && resourceNames is not null) - { - errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "ImportDscResourceMultipleModulesNotSupportedWithName", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceMultipleModulesNotSupportedWithName))); - } - - // if moduleversion is specified then we can not specify multiple modules name - if (moduleSpecifications is not null && moduleSpecifications.Length > 1 && moduleVersion is not null) - { - errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "ImportDscResourceMultipleModulesNotSupportedWithVersion", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); - } - - // if moduleversion is specified then we can not specify another version in modulespecification object of ModuleName - if (moduleSpecifications is not null && (moduleSpecifications[0].Version is not null || moduleSpecifications[0].MaximumVersion is not null) && moduleVersion is not null) - { - errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "ImportDscResourceMultipleModuleVersionsNotSupported", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); - } - - // If moduleVersion is specified we have only one module Name in valid scenario - // So update it's version property in module specification object that will be used to load modules - if (moduleSpecifications is not null && moduleSpecifications[0].Version is null && moduleSpecifications[0].MaximumVersion is null && moduleVersion is not null) - { - moduleSpecifications[0].Version = moduleVersion; - } - } - else - { - errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "RequiresInvalidStringArgument", string.Format(CultureInfo.CurrentCulture, ParserStrings.RequiresInvalidStringArgument, ModuleNameParam))); - } - } - - if (errorList.Count == 0) - { - // No errors, try to load the resources - LoadResourcesFromModuleInImportResourcePostParse(ast.Extent, moduleSpecifications, resourceNames, errorList); - } - - return errorList.ToArray(); - } - - // This function performs semantic checks for Import-DscResource - private static ParseError[] ImportResourceCheckSemantics(DynamicKeywordStatementAst ast) - { - List errorList = null; - - var keywordAst = Ast.GetAncestorAst(ast.Parent); - while (keywordAst is not null) - { - if (keywordAst.Keyword.Keyword.Equals("Node")) - { - if (errorList is null) - { - errorList = new List(); - } - - errorList.Add(new ParseError(ast.Extent, "ImportDscResourceInsideNode", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceInsideNode))); - break; - } - - keywordAst = Ast.GetAncestorAst(keywordAst.Parent); - } - - if (errorList is not null) - { - return errorList.ToArray(); - } - else - { - return null; - } - } - - // This function performs semantic checks for all DSC Resources keywords. - private static ParseError[] CheckMandatoryPropertiesPresent(DynamicKeywordStatementAst ast) - { - HashSet mandatoryPropertiesNames = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var pair in ast.Keyword.Properties) - { - if (pair.Value.Mandatory) - { - mandatoryPropertiesNames.Add(pair.Key); - } - } - - // by design mandatoryPropertiesNames are not empty at this point: - // every resource must have at least one Key property. - HashtableAst hashtableAst = null; - foreach (var commandElementsAst in ast.CommandElements) - { - hashtableAst = commandElementsAst as HashtableAst; - if (hashtableAst != null) - { - break; - } - } - - if (hashtableAst is null) - { - // nothing to validate - return null; - } - - foreach (var pair in hashtableAst.KeyValuePairs) - { - object evalResultObject; - if (IsConstantValueVisitor.IsConstant(pair.Item1, out evalResultObject, forAttribute: false, forRequires: false)) - { - var presentName = evalResultObject as string; - if (presentName is not null) - { - if (mandatoryPropertiesNames.Remove(presentName) && mandatoryPropertiesNames.Count == 0) - { - // optimization, once all mandatory properties are specified, we can safely exit. - return null; - } - } - } - } - - if (mandatoryPropertiesNames.Count > 0) - { - ParseError[] errors = new ParseError[mandatoryPropertiesNames.Count]; - var extent = ast.CommandElements[0].Extent; - int i = 0; - foreach (string name in mandatoryPropertiesNames) - { - errors[i] = new ParseError( - extent, - "MissingValueForMandatoryProperty", - string.Format( - CultureInfo.CurrentCulture, - ParserStrings.MissingValueForMandatoryProperty, - ast.Keyword.Keyword, - ast.Keyword.Properties.First(p => StringComparer.OrdinalIgnoreCase.Equals(p.Value.Name, name)).Value.TypeConstraint, - name)); - i++; - } - - return errors; - } - - return null; - } - - /// - /// Load DSC resources from specified module. - /// - /// Script statement loading the module, can be null. - /// Module information, can be null. - /// Name of the resource to be loaded from module. - /// List of errors reported by the method. - internal static void LoadResourcesFromModuleInImportResourcePostParse( - IScriptExtent scriptExtent, - ModuleSpecification[] moduleSpecifications, - string[] resourceNames, - List errorList) - { - // get all required modules - var modules = new Collection(); - if (moduleSpecifications is not null) - { - foreach (var moduleToImport in moduleSpecifications) - { - bool foundModule = false; - var moduleInfos = ModuleCmdletBase.GetModuleIfAvailable(moduleToImport); - - if (moduleInfos.Count >= 1 && (moduleToImport.Version is not null || moduleToImport.Guid is not null)) - { - foreach (var psModuleInfo in moduleInfos) - { - if ((moduleToImport.Guid.HasValue && moduleToImport.Guid.Equals(psModuleInfo.Guid)) || - (moduleToImport.Version is not null && - moduleToImport.Version.Equals(psModuleInfo.Version))) - { - modules.Add(psModuleInfo); - foundModule = true; - break; - } - } - } - else if (moduleInfos.Count == 1) - { - modules.Add(moduleInfos[0]); - foundModule = true; - } - - if (!foundModule) - { - if (moduleInfos.Count > 1) - { - errorList.Add( - new ParseError( - scriptExtent, - "MultipleModuleEntriesFoundDuringParse", - string.Format(CultureInfo.CurrentCulture, ParserStrings.MultipleModuleEntriesFoundDuringParse, moduleToImport.Name))); - } - else - { - string moduleString = moduleToImport.Version == null - ? moduleToImport.Name - : string.Format(CultureInfo.CurrentCulture, "<{0}, {1}>", moduleToImport.Name, moduleToImport.Version); - - errorList.Add(new ParseError(scriptExtent, "ModuleNotFoundDuringParse", string.Format(CultureInfo.CurrentCulture, ParserStrings.ModuleNotFoundDuringParse, moduleString))); - } - - return; - } - } - } - else if (resourceNames is not null) - { - // Lookup the required resources under available PowerShell modules when modulename is not specified - // Make sure that this is not a circular import/parsing - var callLocation = string.Join(':', scriptExtent.File, scriptExtent.StartLineNumber, scriptExtent.StartColumnNumber, scriptExtent.Text); - if (!t_currentImportDscResourceInvocations.Contains(callLocation)) - { - t_currentImportDscResourceInvocations.Add(callLocation); - using (var powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) - { - powerShell.AddCommand("Get-Module"); - powerShell.AddParameter("ListAvailable"); - modules = powerShell.Invoke(); - } - } - } - - // When ModuleName only specified, we need to import all resources from that module - var resourcesToImport = new List(); - if (resourceNames is null || resourceNames.Length == 0) - { - resourcesToImport.Add("*"); - } - else - { - resourcesToImport.AddRange(resourceNames); - } - - foreach (var moduleInfo in modules) - { - var resourcesFound = new List(); - var exceptionList = new System.Collections.ObjectModel.Collection(); - LoadPowerShellClassResourcesFromModule(primaryModuleInfo: moduleInfo, moduleInfo: moduleInfo, resourcesToImport: resourcesToImport, resourcesFound: resourcesFound, errorList: exceptionList, functionsToDefine: null, recurse: true, extent: scriptExtent); - foreach (Exception ex in exceptionList) - { - errorList.Add(new ParseError(scriptExtent, "ClassResourcesLoadingFailed", ex.Message)); - } - - foreach (var resource in resourcesFound) - { - resourcesToImport.Remove(resource); - } - - if (resourcesToImport.Count == 0) - { - break; - } - } - - if (resourcesToImport.Count > 0) - { - foreach (var resourceNameToImport in resourcesToImport) - { - if (!resourceNameToImport.Contains('*')) - { - errorList.Add(new ParseError(scriptExtent, "DscResourcesNotFoundDuringParsing", string.Format(CultureInfo.CurrentCulture, ParserStrings.DscResourcesNotFoundDuringParsing, resourceNameToImport))); - } - } - } - } - - private static void LoadPowerShellClassResourcesFromModule( - PSModuleInfo primaryModuleInfo, - PSModuleInfo moduleInfo, - ICollection resourcesToImport, - ICollection resourcesFound, - Collection errorList, - Dictionary functionsToDefine = null, - bool recurse = true, - IScriptExtent extent = null) - { - if (primaryModuleInfo._declaredDscResourceExports is null || primaryModuleInfo._declaredDscResourceExports.Count == 0) - { - return; - } - - if (moduleInfo.ModuleType == ModuleType.Binary) - { - throw PSTraceSource.NewArgumentException("isConfiguration", ParserStrings.ConfigurationNotSupportedInPowerShellCore); - } - else - { - string scriptPath = null; - if (moduleInfo.RootModule is not null) - { - scriptPath = Path.Join(moduleInfo.ModuleBase, moduleInfo.RootModule); - } - else if (moduleInfo.Path is not null) - { - scriptPath = moduleInfo.Path; - } - - LoadPowerShellClassResourcesFromModule(scriptPath, primaryModuleInfo, resourcesToImport, resourcesFound, functionsToDefine, errorList, extent); - } - - if (moduleInfo.NestedModules is not null && recurse) - { - foreach (var nestedModule in moduleInfo.NestedModules) - { - LoadPowerShellClassResourcesFromModule(primaryModuleInfo, nestedModule, resourcesToImport, resourcesFound, errorList, functionsToDefine, recurse: false, extent: extent); - } - } - } - - /// - /// Import class resources from module. - /// - /// Module information. - /// Collection of resources to import. - /// Functions to define. - /// List of errors to return. - /// The list of resources imported from this module. - public static List ImportClassResourcesFromModule(PSModuleInfo moduleInfo, ICollection resourcesToImport, Dictionary functionsToDefine, Collection errors) - { - if (!ExperimentalFeature.IsEnabled(DscExperimentalFeatureName)) - { - throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); - } - - var resourcesImported = new List(); - LoadPowerShellClassResourcesFromModule(moduleInfo, moduleInfo, resourcesToImport, resourcesImported, errors, functionsToDefine); - return resourcesImported; - } - - internal static PSObject[] GenerateJsonClassesForAst(TypeDefinitionAst typeAst, PSModuleInfo module, DSCResourceRunAsCredential runAsBehavior) - { - var embeddedInstanceTypes = new List(); - - var result = GenerateJsonClassesForAst(typeAst, embeddedInstanceTypes); - var visitedInstances = new List(); - visitedInstances.Add(typeAst); - var classes = ProcessEmbeddedInstanceTypes(embeddedInstanceTypes, visitedInstances); - AddEmbeddedInstanceTypesToCaches(classes, module, runAsBehavior); - - return result; - } - - private static List ProcessEmbeddedInstanceTypes(List embeddedInstanceTypes, List visitedInstances) - { - var result = new List(); - while (embeddedInstanceTypes.Count > 0) - { - var batchedTypes = embeddedInstanceTypes.Where(x => !visitedInstances.Contains(x)).ToArray(); - embeddedInstanceTypes.Clear(); - - for (int i = batchedTypes.Length - 1; i >= 0; i--) - { - visitedInstances.Add(batchedTypes[i]); - var typeAst = batchedTypes[i] as TypeDefinitionAst; - if (typeAst is not null) - { - var classes = GenerateJsonClassesForAst(typeAst, embeddedInstanceTypes); - result.AddRange(classes); - } - } - } - - return result; - } - - private static void AddEmbeddedInstanceTypesToCaches(IEnumerable classes, PSModuleInfo module, DSCResourceRunAsCredential runAsBehavior) - { - foreach (dynamic c in classes) - { - var className = c.ClassName; - string alias = GetFriendlyName(c); - var friendlyName = string.IsNullOrEmpty(alias) ? className : alias; - var moduleQualifiedResourceName = GetModuleQualifiedResourceName(module.Name, module.Version.ToString(), className, friendlyName); - var classCacheEntry = new DscClassCacheEntry(runAsBehavior, false, c, module.Path); - ClassCache[moduleQualifiedResourceName] = classCacheEntry; - GuestConfigClassCache[moduleQualifiedResourceName] = classCacheEntry; - ByClassModuleCache[className] = new Tuple(module.Name, module.Version); - } - } - - internal static string MapTypeNameToMofType(ITypeName typeName, string memberName, string className, out bool isArrayType, out string embeddedInstanceType, List embeddedInstanceTypes, ref string[] enumNames) - { - TypeName propTypeName; - var arrayTypeName = typeName as ArrayTypeName; - if (arrayTypeName is not null) - { - isArrayType = true; - propTypeName = arrayTypeName.ElementType as TypeName; - } - else - { - isArrayType = false; - propTypeName = typeName as TypeName; - } - - if (propTypeName is null || propTypeName._typeDefinitionAst is null) - { - throw new NotSupportedException(string.Format( - CultureInfo.InvariantCulture, - ParserStrings.UnsupportedPropertyTypeOfDSCResourceClass, - memberName, - typeName.FullName, - typeName)); - } - - if (propTypeName._typeDefinitionAst.IsEnum) - { - enumNames = propTypeName._typeDefinitionAst.Members.Select(m => m.Name).ToArray(); - isArrayType = false; - embeddedInstanceType = null; - return "string"; - } - - if (!embeddedInstanceTypes.Contains(propTypeName._typeDefinitionAst)) - { - embeddedInstanceTypes.Add(propTypeName._typeDefinitionAst); - } - - embeddedInstanceType = propTypeName.Name.Replace('.', '_'); - return "Instance"; - } - - private static PSObject[] GenerateJsonClassesForAst(TypeDefinitionAst typeAst, List embeddedInstanceTypes) - { - // MOF-based implementation of this used to generate MOF string representing classes/typeAst and pass it to MMI/MOF deserializer to get CimClass array - // Here we are avoiding that roundtrip by constructing the resulting PSObjects directly - var className = typeAst.Name; - - string cimSuperClassName = null; - if (typeAst.Attributes.Any(a => a.TypeName.GetReflectionAttributeType() == typeof(DscResourceAttribute))) - { - cimSuperClassName = "OMI_BaseResource"; - } - - var cimClassProperties = ProcessMembers(embeddedInstanceTypes, typeAst, className).ToArray(); - - Queue bases = new Queue(); - foreach (var b in typeAst.BaseTypes) - { - bases.Enqueue(b); - } - - while (bases.Count > 0) - { - var b = bases.Dequeue(); - var tc = b as TypeConstraintAst; - - if (tc is not null) - { - b = tc.TypeName.GetReflectionType(); - if (b is null) - { - var td = tc.TypeName as TypeName; - if (td is not null && td._typeDefinitionAst is not null) - { - ProcessMembers(embeddedInstanceTypes, td._typeDefinitionAst, className); - foreach (var b1 in td._typeDefinitionAst.BaseTypes) - { - bases.Enqueue(b1); - } - } - - continue; - } - } - } - - var result = new PSObject(); - result.Properties.Add(new PSNoteProperty("ClassName", className)); - result.Properties.Add(new PSNoteProperty("FriendlyName", className)); - result.Properties.Add(new PSNoteProperty("SuperClassName", cimSuperClassName)); - result.Properties.Add(new PSNoteProperty("ClassProperties", cimClassProperties)); - - return new[] { result }; - } - - private static List ProcessMembers(List embeddedInstanceTypes, TypeDefinitionAst typeDefinitionAst, string className) - { - List result = new List(); - - foreach (var member in typeDefinitionAst.Members) - { - var property = member as PropertyMemberAst; - - if (property == null || property.IsStatic || - property.Attributes.All(a => a.TypeName.GetReflectionAttributeType() != typeof(DscPropertyAttribute))) - { - continue; - } - - var memberType = property.PropertyType is null - ? typeof(object) - : property.PropertyType.TypeName.GetReflectionType(); - - var attributes = new List(); - for (int i = 0; i < property.Attributes.Count; i++) - { - attributes.Add(property.Attributes[i].GetAttribute()); - } - - string mofType; - bool isArrayType; - string embeddedInstanceType; - string[] enumNames = null; - - if (memberType != null) - { - mofType = MapTypeToMofType(memberType, member.Name, className, out isArrayType, out embeddedInstanceType, embeddedInstanceTypes); - if (memberType.IsEnum) - { - enumNames = Enum.GetNames(memberType); - } - } - else - { - // PropertyType can't be null, we used typeof(object) above in that case so we don't get here. - mofType = MapTypeNameToMofType(property.PropertyType.TypeName, member.Name, className, out isArrayType, out embeddedInstanceType, embeddedInstanceTypes, ref enumNames); - } - - var propertyObject = new PSObject(); - propertyObject.Properties.Add(new PSNoteProperty(@"Name", member.Name)); - propertyObject.Properties.Add(new PSNoteProperty(@"CimType", mofType + (isArrayType ? "Array" : string.Empty))); - if (!string.IsNullOrEmpty(embeddedInstanceType)) - { - propertyObject.Properties.Add(new PSNoteProperty(@"ReferenceClassName", embeddedInstanceType)); - } - - PSObject attributesPSObject = null; - foreach (var attr in attributes) - { - var dscProperty = attr as DscPropertyAttribute; - if (dscProperty is not null) - { - if (attributesPSObject is null) - { - attributesPSObject = new PSObject(); - } - - if (dscProperty.Key) - { - attributesPSObject.Properties.Add(new PSNoteProperty("Key", true)); - } - - if (dscProperty.Mandatory) - { - attributesPSObject.Properties.Add(new PSNoteProperty("Required", true)); - } - - if (dscProperty.NotConfigurable) - { - attributesPSObject.Properties.Add(new PSNoteProperty("Read", true)); - } - - continue; - } - - var validateSet = attr as ValidateSetAttribute; - if (validateSet is not null) - { - if (attributesPSObject is null) - { - attributesPSObject = new PSObject(); - } - - List valueMap = new List(validateSet.ValidValues); - List values = new List(validateSet.ValidValues); - attributesPSObject.Properties.Add(new PSNoteProperty("ValueMap", valueMap)); - attributesPSObject.Properties.Add(new PSNoteProperty("Values", values)); - } - } - - if (attributesPSObject is not null) - { - propertyObject.Properties.Add(new PSNoteProperty(@"Qualifiers", attributesPSObject)); - } - - result.Add(propertyObject); - } - - return result; - } - - private static bool GetResourceDefinitionsFromModule(string fileName, out IEnumerable resourceDefinitions, Collection errorList, IScriptExtent extent) - { - resourceDefinitions = null; - - if (string.IsNullOrEmpty(fileName)) - { - return false; - } - - if (!".psm1".Equals(Path.GetExtension(fileName), StringComparison.OrdinalIgnoreCase) && - !".ps1".Equals(Path.GetExtension(fileName), StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - Token[] tokens; - ParseError[] errors; - var ast = Parser.ParseFile(fileName, out tokens, out errors); - - if (errors is not null && errors.Length > 0) - { - if (errorList is not null && extent is not null) - { - List errorMessages = new List(); - foreach (var error in errors) - { - errorMessages.Add(error.ToString()); - } - - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.FailToParseModuleScriptFile, fileName, string.Join(Environment.NewLine, errorMessages)); - e.SetErrorId("FailToParseModuleScriptFile"); - errorList.Add(e); - } - - return false; - } - - resourceDefinitions = ast.FindAll( - n => - { - var typeAst = n as TypeDefinitionAst; - if (typeAst is not null) - { - for (int i = 0; i < typeAst.Attributes.Count; i++) - { - var a = typeAst.Attributes[i]; - if (a.TypeName.GetReflectionAttributeType() == typeof(DscResourceAttribute)) - { - return true; - } - } - } - - return false; - }, - false); - - return true; - } - - private static bool LoadPowerShellClassResourcesFromModule(string fileName, PSModuleInfo module, ICollection resourcesToImport, ICollection resourcesFound, Dictionary functionsToDefine, Collection errorList, IScriptExtent extent) - { - IEnumerable resourceDefinitions; - if (!GetResourceDefinitionsFromModule(fileName, out resourceDefinitions, errorList, extent)) - { - return false; - } - - var result = false; - - const WildcardOptions options = WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant; - IEnumerable patternList = SessionStateUtilities.CreateWildcardsFromStrings(module._declaredDscResourceExports, options); - - foreach (var r in resourceDefinitions) - { - result = true; - var resourceDefnAst = (TypeDefinitionAst)r; - - if (!SessionStateUtilities.MatchesAnyWildcardPattern(resourceDefnAst.Name, patternList, true)) - { - continue; - } - - bool skip = true; - foreach (var toImport in resourcesToImport) - { - if (WildcardPattern.Get(toImport, WildcardOptions.IgnoreCase).IsMatch(resourceDefnAst.Name)) - { - skip = false; - break; - } - } - - if (skip) - { - continue; - } - - // Parse the Resource Attribute to see if RunAs behavior is specified for the resource. - DSCResourceRunAsCredential runAsBehavior = DSCResourceRunAsCredential.Default; - foreach (var attr in resourceDefnAst.Attributes) - { - if (attr.TypeName.GetReflectionAttributeType() == typeof(DscResourceAttribute)) - { - foreach (var na in attr.NamedArguments) - { - if (na.ArgumentName.Equals("RunAsCredential", StringComparison.OrdinalIgnoreCase)) - { - var dscResourceAttribute = attr.GetAttribute() as DscResourceAttribute; - if (dscResourceAttribute != null) - { - runAsBehavior = dscResourceAttribute.RunAsCredential; - } - } - } - } - } - - var classes = GenerateJsonClassesForAst(resourceDefnAst, module, runAsBehavior); - - ProcessJsonForDynamicKeywords(module, resourcesFound, functionsToDefine, classes, runAsBehavior, errorList); - } - - return result; - } - - private static readonly Dictionary s_mapPrimitiveDotNetTypeToMof = new Dictionary() - { - { typeof(sbyte), "sint8" }, - { typeof(byte), "uint8" }, - { typeof(short), "sint16" }, - { typeof(ushort), "uint16" }, - { typeof(int), "sint32" }, - { typeof(uint), "uint32" }, - { typeof(long), "sint64" }, - { typeof(ulong), "uint64" }, - { typeof(float), "real32" }, - { typeof(double), "real64" }, - { typeof(bool), "boolean" }, - { typeof(string), "string" }, - { typeof(DateTime), "datetime" }, - { typeof(PSCredential), "string" }, - { typeof(char), "char16" }, - }; - - internal static string MapTypeToMofType(Type type, string memberName, string className, out bool isArrayType, out string embeddedInstanceType, List embeddedInstanceTypes) - { - isArrayType = false; - if (type.IsValueType) - { - type = Nullable.GetUnderlyingType(type) ?? type; - } - - if (type.IsEnum) - { - embeddedInstanceType = null; - return "string"; - } - - if (type == typeof(Hashtable)) - { - // Hashtable is obviously not an array, but in the mof, we represent - // it as string[] (really, embeddedinstance of MSFT_KeyValuePair), but - // we need an array to hold each entry in the hashtable. - isArrayType = true; - embeddedInstanceType = "MSFT_KeyValuePair"; - return "string"; - } - - if (type == typeof(PSCredential)) - { - embeddedInstanceType = "MSFT_Credential"; - return "string"; - } - - if (type.IsArray) - { - isArrayType = true; - bool temp; - var elementType = type.GetElementType(); - if (!elementType.IsArray) - { - return MapTypeToMofType(type.GetElementType(), memberName, className, out temp, out embeddedInstanceType, embeddedInstanceTypes); - } - } - else - { - string cimType; - if (s_mapPrimitiveDotNetTypeToMof.TryGetValue(type, out cimType)) - { - embeddedInstanceType = null; - return cimType; - } - } - - bool supported = false; - bool missingDefaultConstructor = false; - if (type.IsValueType) - { - if (s_mapPrimitiveDotNetTypeToMof.ContainsKey(type)) - { - supported = true; - } - } - else if (!type.IsAbstract) - { - // Must have default constructor, at least 1 public property/field, and no base classes - if (type.GetConstructor(Type.EmptyTypes) is null) - { - missingDefaultConstructor = true; - } - else if (type.BaseType == typeof(object) && - (type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Length > 0 || - type.GetFields(BindingFlags.Instance | BindingFlags.Public).Length > 0)) - { - supported = true; - } - } - - if (supported) - { - if (!embeddedInstanceTypes.Contains(type)) - { - embeddedInstanceTypes.Add(type); - } - - // The type is obviously not a string, but in the mof, we represent - // it as string (really, embeddedinstance of the class type) - embeddedInstanceType = type.FullName.Replace('.', '_'); - return "string"; - } - - if (missingDefaultConstructor) - { - throw new NotSupportedException(string.Format( - CultureInfo.InvariantCulture, - ParserStrings.DscResourceMissingDefaultConstructor, - type.Name)); - } - else - { - throw new NotSupportedException(string.Format( - CultureInfo.InvariantCulture, - ParserStrings.UnsupportedPropertyTypeOfDSCResourceClass, - memberName, - type.Name, - className)); - } - } - - private static void ProcessJsonForDynamicKeywords( - PSModuleInfo module, - ICollection resourcesFound, - Dictionary functionsToDefine, - PSObject[] classes, - DSCResourceRunAsCredential runAsBehavior, - Collection errors) - { - foreach (dynamic c in classes) - { - var className = c.ClassName; - string alias = GetFriendlyName(c); - var friendlyName = string.IsNullOrEmpty(alias) ? className : alias; - if (!CacheResourcesFromMultipleModuleVersions) - { - // Find & remove the previous version of the resource. - List> resourceList = FindResourceInCache(module.Name, className, friendlyName); - - if (resourceList.Count > 0 && !string.IsNullOrEmpty(resourceList[0].Key)) - { - ClassCache.Remove(resourceList[0].Key); - - // keyword is already defined and it is a Inbox resource, remove it - if (DynamicKeyword.ContainsKeyword(friendlyName) && resourceList[0].Value.IsImportedImplicitly) - { - DynamicKeyword.RemoveKeyword(friendlyName); - } - } - } - - var moduleQualifiedResourceName = GetModuleQualifiedResourceName(module.Name, module.Version.ToString(), className, friendlyName); - DscClassCacheEntry existingCacheEntry = null; - if (ClassCache.TryGetValue(moduleQualifiedResourceName, out existingCacheEntry)) - { - if (errors is not null) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DuplicateCimClassDefinition, className, module.Path, existingCacheEntry.ModulePath); - e.SetErrorId("DuplicateCimClassDefinition"); - errors.Add(e); - } - } - else - { - var classCacheEntry = new DscClassCacheEntry(runAsBehavior, false, c, module.Path); - ClassCache[moduleQualifiedResourceName] = classCacheEntry; - GuestConfigClassCache[moduleQualifiedResourceName] = classCacheEntry; - ByClassModuleCache[className] = new Tuple(module.Name, module.Version); - resourcesFound.Add(className); - CreateAndRegisterKeywordFromCimClass(module.Name, module.Version, c, functionsToDefine, runAsBehavior); - } - } - } - - /// - /// Returns an error record to use in the case of a malformed resource reference in the DependsOn list. - /// - /// The malformed resource. - /// The referencing resource instance. - /// Generated error record. - public static ErrorRecord GetBadlyFormedRequiredResourceIdErrorRecord(string badDependsOnReference, string definingResource) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.GetBadlyFormedRequiredResourceId, badDependsOnReference, definingResource); - e.SetErrorId("GetBadlyFormedRequiredResourceId"); - return e.ErrorRecord; - } - - /// - /// Returns an error record to use in the case of a malformed resource reference in the exclusive resources list. - /// - /// The malformed resource. - /// The referencing resource instance. - /// Generated error record. - public static ErrorRecord GetBadlyFormedExclusiveResourceIdErrorRecord(string badExclusiveResourcereference, string definingResource) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.GetBadlyFormedExclusiveResourceId, badExclusiveResourcereference, definingResource); - e.SetErrorId("GetBadlyFormedExclusiveResourceId"); - return e.ErrorRecord; - } - - /// - /// If a partial configuration is in 'Pull' Mode, it needs a configuration source. - /// - /// Resource id. - /// Generated error record. - public static ErrorRecord GetPullModeNeedConfigurationSource(string resourceId) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.GetPullModeNeedConfigurationSource, resourceId); - e.SetErrorId("GetPullModeNeedConfigurationSource"); - return e.ErrorRecord; - } - - /// - /// Refresh Mode can not be Disabled for the Partial Configurations. - /// - /// Resource id. - /// Generated error record. - public static ErrorRecord DisabledRefreshModeNotValidForPartialConfig(string resourceId) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DisabledRefreshModeNotValidForPartialConfig, resourceId); - e.SetErrorId("DisabledRefreshModeNotValidForPartialConfig"); - return e.ErrorRecord; - } - - /// - /// Returns an error record to use in the case of a malformed resource reference in the DependsOn list. - /// - /// The duplicate resource identifier. - /// The node being defined. - /// The error record to use. - public static ErrorRecord DuplicateResourceIdInNodeStatementErrorRecord(string duplicateResourceId, string nodeName) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DuplicateResourceIdInNodeStatement, duplicateResourceId, nodeName); - e.SetErrorId("DuplicateResourceIdInNodeStatement"); - return e.ErrorRecord; - } - - /// - /// Returns an error record to use in the case of a configuration name is invalid. - /// - /// Configuration name. - /// Generated error record. - public static ErrorRecord InvalidConfigurationNameErrorRecord(string configurationName) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.InvalidConfigurationName, configurationName); - e.SetErrorId("InvalidConfigurationName"); - return e.ErrorRecord; - } - - /// - /// Returns an error record to use in the case of the given value for a property is invalid. - /// - /// Property name. - /// Property value. - /// Keyword name. - /// Valid property values. - /// Generated error record. - public static ErrorRecord InvalidValueForPropertyErrorRecord(string propertyName, string value, string keywordName, string validValues) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.InvalidValueForProperty, value, propertyName, keywordName, validValues); - e.SetErrorId("InvalidValueForProperty"); - return e.ErrorRecord; - } - - /// - /// Returns an error record to use in case the given property is not valid LocalConfigurationManager property. - /// - /// Property name. - /// Valid properties. - /// Generated error record. - public static ErrorRecord InvalidLocalConfigurationManagerPropertyErrorRecord(string propertyName, string validProperties) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.InvalidLocalConfigurationManagerProperty, propertyName, validProperties); - e.SetErrorId("InvalidLocalConfigurationManagerProperty"); - return e.ErrorRecord; - } - - /// - /// Returns an error record to use in the case of the given value for a property is not supported. - /// - /// Property name. - /// Property value. - /// Keyword name. - /// Valid property values. - /// Generated error record. - public static ErrorRecord UnsupportedValueForPropertyErrorRecord(string propertyName, string value, string keywordName, string validValues) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.UnsupportedValueForProperty, value, propertyName, keywordName, validValues); - e.SetErrorId("UnsupportedValueForProperty"); - return e.ErrorRecord; - } - - /// - /// Returns an error record to use in the case of no value is provided for a mandatory property. - /// - /// Keyword name. - /// Type name. - /// Property name. - /// Generated error record. - public static ErrorRecord MissingValueForMandatoryPropertyErrorRecord(string keywordName, string typeName, string propertyName) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.MissingValueForMandatoryProperty, keywordName, typeName, propertyName); - e.SetErrorId("MissingValueForMandatoryProperty"); - return e.ErrorRecord; - } - - /// - /// Returns an error record to use in the case of more than one values are provided for DebugMode property. - /// - /// Generated error record. - public static ErrorRecord DebugModeShouldHaveOneValue() - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DebugModeShouldHaveOneValue); - e.SetErrorId("DebugModeShouldHaveOneValue"); - return e.ErrorRecord; - } - - /// - /// Return an error to indicate a value is out of range for a dynamic keyword property. - /// - /// Rroperty name. - /// Resource name. - /// Provided value. - /// Valid range lower bound. - /// Valid range upper bound. - /// Generated error record. - public static ErrorRecord ValueNotInRangeErrorRecord(string property, string name, int providedValue, int lower, int upper) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.ValueNotInRange, property, name, providedValue, lower, upper); - e.SetErrorId("ValueNotInRange"); - return e.ErrorRecord; - } - - /// - /// Returns an error record to use when composite resource and its resource instances both has PsDscRunAsCredentials value. - /// - /// ResourceId of resource. - /// Generated error record. - public static ErrorRecord PsDscRunAsCredentialMergeErrorForCompositeResources(string resourceId) - { - PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.PsDscRunAsCredentialMergeErrorForCompositeResources, resourceId); - e.SetErrorId("PsDscRunAsCredentialMergeErrorForCompositeResources"); - return e.ErrorRecord; - } - - /// - /// Routine to format a usage string from keyword. The resulting string should look like: - /// User [string] #ResourceName - /// { - /// UserName = [string] - /// [ Description = [string] ] - /// [ Disabled = [bool] ] - /// [ Ensure = [string] { Absent | Present } ] - /// [ Force = [bool] ] - /// [ FullName = [string] ] - /// [ Password = [PSCredential] ] - /// [ PasswordChangeNotAllowed = [bool] ] - /// [ PasswordChangeRequired = [bool] ] - /// [ PasswordNeverExpires = [bool] ] - /// [ DependsOn = [string[]] ] - /// } - /// - /// Dynamic keyword. - /// Usage string. - public static string GetDSCResourceUsageString(DynamicKeyword keyword) - { - StringBuilder usageString; - switch (keyword.NameMode) - { - // Name must be present and simple non-empty bare word - case DynamicKeywordNameMode.SimpleNameRequired: - usageString = new StringBuilder(keyword.Keyword + " [string] # Resource Name"); - break; - - // Name must be present but can also be an expression - case DynamicKeywordNameMode.NameRequired: - usageString = new StringBuilder(keyword.Keyword + " [string[]] # Name List"); - break; - - // Name may be optionally present, but if it is present, it must be a non-empty bare word. - case DynamicKeywordNameMode.SimpleOptionalName: - usageString = new StringBuilder(keyword.Keyword + " [ [string] ] # Optional Name"); - break; - - // Name may be optionally present, expression or bare word - case DynamicKeywordNameMode.OptionalName: - usageString = new StringBuilder(keyword.Keyword + " [ [string[]] ] # Optional NameList"); - break; - - // Does not take a name - default: - usageString = new StringBuilder(keyword.Keyword); - break; - } - - usageString.Append("\n{\n"); - - bool listKeyProperties = true; - while (true) - { - foreach (var prop in keyword.Properties.OrderBy(ob => ob.Key)) - { - if (string.Equals(prop.Key, "ResourceId", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var propVal = prop.Value; - if ((listKeyProperties && propVal.IsKey) || (!listKeyProperties && !propVal.IsKey)) - { - usageString.Append(propVal.Mandatory ? " " : " [ "); - usageString.Append(prop.Key); - usageString.Append(" = "); - usageString.Append(FormatCimPropertyType(propVal, !propVal.Mandatory)); - } - } - - if (listKeyProperties) - { - listKeyProperties = false; - } - else - { - break; - } - } - - usageString.Append('}'); - - return usageString.ToString(); - } - - /// - /// Format the type name of a CIM property in a presentable way. - /// - /// Dynamic keyword property. - /// If this is optional property or not. - /// CIM property type string. - private static StringBuilder FormatCimPropertyType(DynamicKeywordProperty prop, bool isOptionalProperty) - { - string cimTypeName = prop.TypeConstraint; - StringBuilder formattedTypeString = new StringBuilder(); - - if (string.Equals(cimTypeName, "MSFT_Credential", StringComparison.OrdinalIgnoreCase)) - { - formattedTypeString.Append("[PSCredential]"); - } - else if (string.Equals(cimTypeName, "MSFT_KeyValuePair", StringComparison.OrdinalIgnoreCase) || string.Equals(cimTypeName, "MSFT_KeyValuePair[]", StringComparison.OrdinalIgnoreCase)) - { - formattedTypeString.Append("[Hashtable]"); - } - else - { - string convertedTypeString = System.Management.Automation.LanguagePrimitives.ConvertTypeNameToPSTypeName(cimTypeName); - if (!string.IsNullOrEmpty(convertedTypeString) && !string.Equals(convertedTypeString, "[]", StringComparison.OrdinalIgnoreCase)) - { - formattedTypeString.Append(convertedTypeString); - } - else - { - formattedTypeString.Append("[" + cimTypeName + "]"); - } - } - - // Do the property values map - if (prop.ValueMap is not null && prop.ValueMap.Count > 0) - { - formattedTypeString.Append(" { " + string.Join(" | ", prop.ValueMap.Keys.OrderBy(x => x)) + " }"); - } - - // We prepend optional property with "[" so close out it here. This way it is shown with [ ] to indication optional - if (isOptionalProperty) - { - formattedTypeString.Append(']'); - } - - formattedTypeString.Append('\n'); - - return formattedTypeString; - } - - /// - /// Gets the scriptblock that implements the CIM keyword functionality. - /// - private static ScriptBlock CimKeywordImplementationFunction - { - get - { - // The scriptblock cache will handle mutual exclusion - return s_cimKeywordImplementationFunction ??= ScriptBlock.Create(CimKeywordImplementationFunctionText); - } - } - - private static ScriptBlock s_cimKeywordImplementationFunction; - - private const string CimKeywordImplementationFunctionText = @" - param ( - [Parameter(Mandatory)] - $KeywordData, - [Parameter(Mandatory)] - $Name, - [Parameter(Mandatory)] - [Hashtable] - $Value, - [Parameter(Mandatory)] - $SourceMetadata - ) - -# walk the call stack to get at all of the enclosing configuration resource IDs - $stackedConfigs = @(Get-PSCallStack | - where { ($null -ne $_.InvocationInfo.MyCommand) -and ($_.InvocationInfo.MyCommand.CommandType -eq 'Configuration') }) -# keep all but the top-most - $stackedConfigs = $stackedConfigs[0..(@($stackedConfigs).Length - 2)] -# and build the complex resource ID suffix. - $complexResourceQualifier = ( $stackedConfigs | ForEach-Object { '[' + $_.Command + ']' + $_.InvocationInfo.BoundParameters['InstanceName'] } ) -join '::' - -# -# Utility function used to validate that the DependsOn arguments are well-formed. -# The function also adds them to the define nodes resource collection. -# in the case of resources generated inside a script resource, this routine -# will also fix up the DependsOn references to '[Type]Instance::[OuterType]::OuterInstance -# - function Test-DependsOn - { - -# make sure the references are well-formed - $updatedDependsOn = foreach ($DependsOnVar in $value['DependsOn']) { -# match [ResourceType]ResourceName. ResourceName should starts with [a-z_0-9] followed by [a-z_0-9\p{Zs}\.\\-]* - if ($DependsOnVar -notmatch '^\[[a-z]\w*\][a-z_0-9][a-z_0-9\p{Zs}\.\\-]*$') - { - Update-ConfigurationErrorCount - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::GetBadlyFormedRequiredResourceIdErrorRecord($DependsOnVar, $resourceId)) - } - -# Fix up DependsOn for nested names - if ($MyTypeName -and $typeName -ne $MyTypeName -and $InstanceName) - { - ""$DependsOnVar::$complexResourceQualifier"" - } - else - { - $DependsOnVar - } - } - - $value['DependsOn']= $updatedDependsOn - - if($null -ne $DependsOn) - { -# -# Combine DependsOn with dependson from outer composite resource -# which is set as local variable $DependsOn at the composite resource context -# - $value['DependsOn']= @($value['DependsOn']) + $DependsOn - } - -# Save the resource id in a per-node dictionary to do cross validation at the end - Set-NodeResources $resourceId @( $value['DependsOn']) - -# Remove depends on because it need to be fixed up for composite resources -# We do it in ValidateNodeResource and Update-Depends on in configuration/Node function - $value.Remove('DependsOn') - } - -# A copy of the value object with correctly-cased property names - $canonicalizedValue = @{} - - $typeName = $keywordData.ResourceName # CIM type - $keywordName = $keywordData.Keyword # user-friendly alias that is used in scripts - $keyValues = '' - $debugPrefix = "" ${TypeName}:"" # set up a debug prefix string that makes it easier to track what's happening. - - Write-Debug ""${debugPrefix} RESOURCE PROCESSING STARTED [KeywordName='$keywordName'] Function='$($myinvocation.Invocationname)']"" - -# Check whether it's an old style metaconfig - $OldMetaConfig = $false - if ((-not $IsMetaConfig) -and ($keywordName -ieq 'LocalConfigurationManager')) { - $OldMetaConfig = $true - } - -# Check to see if it's a resource keyword. If so add the meta-properties to the canonical property collection. - $resourceId = $null -# todo: need to include configuration managers and partial configuration - if (($keywordData.Properties.Keys -contains 'DependsOn') -or (($KeywordData.ImplementingModule -ieq 'PSDesiredStateConfigurationEngine') -and ($KeywordData.NameMode -eq [System.Management.Automation.Language.DynamicKeywordNameMode]::NameRequired))) - { - - $resourceId = ""[$keywordName]$name"" - if ($MyTypeName -and $keywordName -ne $MyTypeName -and $InstanceName) - { - $resourceId += ""::$complexResourceQualifier"" - } - - Write-Debug ""${debugPrefix} ResourceID = $resourceId"" - -# copy the meta-properties - $canonicalizedValue['ResourceID'] = $resourceId - $canonicalizedValue['SourceInfo'] = $SourceMetadata - if(-not $IsMetaConfig) - { - $canonicalizedValue['ModuleName'] = $keywordData.ImplementingModule - $canonicalizedValue['ModuleVersion'] = $keywordData.ImplementingModuleVersion -as [string] - } - -# see if there is already a resource with this ID. - if (Test-NodeResources $resourceId) - { - Update-ConfigurationErrorCount - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::DuplicateResourceIdInNodeStatementErrorRecord($resourceId, (Get-PSCurrentConfigurationNode))) - } - else - { -# If there are prerequisite resources, validate that the references are well-formed strings -# This routine also adds the resource to the global node resources table. - Test-DependsOn - -# Check if PsDscRunCredential is being specified as Arguments to Configuration - if($null -ne $PsDscRunAsCredential) - { -# Check if resource is also trying to set the value for RunAsCred -# In that case we will generate error during compilation, this is merge error - if($null -ne $value['PsDscRunAsCredential']) - { - Update-ConfigurationErrorCount - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::PsDscRunAsCredentialMergeErrorForCompositeResources($resourceId)) - } -# Set the Value of RunAsCred to that of outer configuration - else - { - $value['PsDscRunAsCredential'] = $PsDscRunAsCredential - } - } - -# Save the resource id in a per-node dictionary to do cross validation at the end - if($keywordData.ImplementingModule -ieq ""PSDesiredStateConfigurationEngine"") - { -#$keywordName is PartialConfiguration - if($keywordName -eq 'PartialConfiguration') - { -# RefreshMode is 'Pull' and .ConfigurationSource is empty - if($value['RefreshMode'] -eq 'Pull' -and -not $value['ConfigurationSource']) - { - Update-ConfigurationErrorCount - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::GetPullModeNeedConfigurationSource($resourceId)) - } - -# Verify that RefreshMode is not Disabled for Partial configuration - if($value['RefreshMode'] -eq 'Disabled') - { - Update-ConfigurationErrorCount - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::DisabledRefreshModeNotValidForPartialConfig($resourceId)) - } - - if($null -ne $value['ConfigurationSource']) - { - Set-NodeManager $resourceId $value['ConfigurationSource'] - } - - if($null -ne $value['ResourceModuleSource']) - { - Set-NodeResourceSource $resourceId $value['ResourceModuleSource'] - } - } - - if($null -ne $value['ExclusiveResources']) - { -# make sure the references are well-formed - foreach ($ExclusiveResource in $value['ExclusiveResources']) { - if (($ExclusiveResource -notmatch '^[a-z][a-z_0-9]*\\[a-z][a-z_0-9]*$') -and ($ExclusiveResource -notmatch '^[a-z][a-z_0-9]*$') -and ($ExclusiveResource -notmatch '^[a-z][a-z_0-9]*\\\*$')) - { - Update-ConfigurationErrorCount - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::GetBadlyFormedExclusiveResourceIdErrorRecord($ExclusiveResource, $resourceId)) - } - } - -# Save the resource id in a per-node dictionary to do cross validation at the end -# Validate resource exist -# Also update the resource reference from module\friendlyname to module\name - $value['ExclusiveResources'] = @(Set-NodeExclusiveResources $resourceId @( $value['ExclusiveResources'] )) - } - } - } - } - else - { - Write-Debug ""${debugPrefix} TYPE IS NOT AS DSC RESOURCE"" - } - -# -# Copy the user-supplied values into a new collection with canonicalized property names -# - foreach ($key in $keywordData.Properties.Keys) - { - Write-Debug ""${debugPrefix} Processing property '$key' ["" - - if ($value.Contains($key)) - { - if ($OldMetaConfig -and (-not ($V1MetaConfigPropertyList -contains $key))) - { - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::InvalidLocalConfigurationManagerPropertyErrorRecord($key, ($V1MetaConfigPropertyList -join ', '))) - Update-ConfigurationErrorCount - } -# see if there is a list of allowed values for this property (similar to an enum) - $allowedValues = $keywordData.Properties[$key].Values -# If there is and user-provided value is not in that list, write an error. - if ($allowedValues) - { - if(($null -eq $value[$key]) -and ($allowedValues -notcontains $value[$key])) - { - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::InvalidValueForPropertyErrorRecord($key, ""$($value[$key])"", $keywordData.Keyword, ($allowedValues -join ', '))) - Update-ConfigurationErrorCount - } - else - { - $notAllowedValue=$null - foreach($v in $value[$key]) - { - if($allowedValues -notcontains $v) - { - $notAllowedValue +=$v.ToString() + ', ' - } - } - - if($notAllowedValue) - { - $notAllowedValue = $notAllowedValue.Substring(0, $notAllowedValue.Length -2) - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::UnsupportedValueForPropertyErrorRecord($key, $notAllowedValue, $keywordData.Keyword, ($allowedValues -join ', '))) - Update-ConfigurationErrorCount - } - } - } - -# see if a value range is defined for this property - $allowedRange = $keywordData.Properties[$key].Range - if($allowedRange) - { - $castedValue = $value[$key] -as [int] - if((($castedValue -is [int]) -and (($castedValue -lt $keywordData.Properties[$key].Range.Item1) -or ($castedValue -gt $keywordData.Properties[$key].Range.Item2))) -or ($null -eq $castedValue)) - { - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::ValueNotInRangeErrorRecord($key, $keywordName, $value[$key], $keywordData.Properties[$key].Range.Item1, $keywordData.Properties[$key].Range.Item2)) - Update-ConfigurationErrorCount - } - } - - Write-Debug ""${debugPrefix} Canonicalized property '$key' = '$($value[$key])'"" - - if ($keywordData.Properties[$key].IsKey) - { - if($null -eq $value[$key]) - { - $keyValues += ""::__NULL__"" - } - else - { - $keyValues += ""::"" + $value[$key] - } - } - -# see if ValueMap is also defined for this property (actual values) - $allowedValueMap = $keywordData.Properties[$key].ValueMap -#if it is and the ValueMap contains the user-provided value as a key, use the actual value - if ($allowedValueMap -and $allowedValueMap.ContainsKey($value[$key])) - { - $canonicalizedValue[$key] = $allowedValueMap[$value[$key]] - } - else - { - $canonicalizedValue[$key] = $value[$key] - } - } - elseif ($keywordData.Properties[$key].Mandatory) - { -# If the property was mandatory but the user didn't provide a value, write and error. - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::MissingValueForMandatoryPropertyErrorRecord($keywordData.Keyword, $keywordData.Properties[$key].TypeConstraint, $Key)) - Update-ConfigurationErrorCount - } - - Write-Debug ""${debugPrefix} Processing completed '$key' ]"" - } - - if($keyValues) - { - $keyValues = $keyValues.Substring(2) # Remove the leading '::' - Add-NodeKeys $keyValues $keywordName - Test-ConflictingResources $keywordName $canonicalizedValue $keywordData - } - -# update OMI_ConfigurationDocument - if($IsMetaConfig) - { - if($keywordData.ResourceName -eq 'OMI_ConfigurationDocument') - { - if($(Get-PSMetaConfigurationProcessed)) - { - $PSMetaConfigDocumentInstVersionInfo = Get-PSMetaConfigDocumentInstVersionInfo - $canonicalizedValue['MinimumCompatibleVersion']=$PSMetaConfigDocumentInstVersionInfo['MinimumCompatibleVersion'] - } - else - { - Set-PSMetaConfigDocInsProcessedBeforeMeta - $canonicalizedValue['MinimumCompatibleVersion']='1.0.0' - } - } - - if(($keywordData.ResourceName -eq 'MSFT_WebDownloadManager') ` - -or ($keywordData.ResourceName -eq 'MSFT_FileDownloadManager') ` - -or ($keywordData.ResourceName -eq 'MSFT_WebResourceManager') ` - -or ($keywordData.ResourceName -eq 'MSFT_FileResourceManager') ` - -or ($keywordData.ResourceName -eq 'MSFT_WebReportManager') ` - -or ($keywordData.ResourceName -eq 'MSFT_SignatureValidation') ` - -or ($keywordData.ResourceName -eq 'MSFT_PartialConfiguration')) - { - Set-PSMetaConfigVersionInfoV2 - } - } - elseif($keywordData.ResourceName -eq 'OMI_ConfigurationDocument') - { - $canonicalizedValue['MinimumCompatibleVersion']='1.0.0' - $canonicalizedValue['CompatibleVersionAdditionalProperties']=@('Omi_BaseResource:ConfigurationName') - } - - if(($keywordData.ResourceName -eq 'MSFT_DSCMetaConfiguration') -or ($keywordData.ResourceName -eq 'MSFT_DSCMetaConfigurationV2')) - { - if($canonicalizedValue['DebugMode'] -and @($canonicalizedValue['DebugMode']).Length -gt 1) - { -# we only allow one value for debug mode now. - Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::DebugModeShouldHaveOneValue()) - Update-ConfigurationErrorCount - } - } - -# Generate the MOF text for this resource instance. -# when generate mof text for OMI_ConfigurationDocument we handle below two cases: -# 1. we will add versioning related property based on meta configuration instance already process -# 2. we update the existing OMI_ConfigurationDocument instance if it already exists when process meta configuration instance - $aliasId = ConvertTo-MOFInstance $keywordName $canonicalizedValue - -# If a OMI_ConfigurationDocument is executed outside of a node statement, it becomes the default -# for all nodes that don't have an explicit OMI_ConfigurationDocument declaration - if ($keywordData.ResourceName -eq 'OMI_ConfigurationDocument' -and -not (Get-PSCurrentConfigurationNode)) - { - $data = Get-MoFInstanceText $aliasId - Write-Debug ""${debugPrefix} DEFINING DEFAULT CONFIGURATION DOCUMENT: $data"" - Set-PSDefaultConfigurationDocument $data - } - - Write-Debug ""${debugPrefix} MOF alias for this resource is '$aliasId'"" - -# always return the aliasId so the generated file will be well-formed if not valid - $aliasId - - Write-Debug ""${debugPrefix} RESOURCE PROCESSING COMPLETED. TOTAL ERROR COUNT: $(Get-ConfigurationErrorCount)"" - - "; - } -} diff --git a/src/System.Management.Automation/FormatAndOutput/common/BaseCommand.cs b/src/System.Management.Automation/FormatAndOutput/common/BaseCommand.cs index dcfb79d4aed..e59cae40146 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/BaseCommand.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/BaseCommand.cs @@ -22,6 +22,7 @@ internal TerminatingErrorContext(PSCmdlet command) _command = command; } + [System.Diagnostics.CodeAnalysis.DoesNotReturn] internal void ThrowTerminatingError(ErrorRecord errorRecord) { _command.ThrowTerminatingError(errorRecord); diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index 3f317b802a3..508decb4f6d 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -16,16 +16,16 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/src/System.Management.Automation/engine/COM/ComDispatch.cs b/src/System.Management.Automation/engine/COM/ComDispatch.cs index 3f6afa6440e..8e0417e17cc 100644 --- a/src/System.Management.Automation/engine/COM/ComDispatch.cs +++ b/src/System.Management.Automation/engine/COM/ComDispatch.cs @@ -5,6 +5,7 @@ using COM = System.Runtime.InteropServices.ComTypes; +#nullable enable namespace System.Management.Automation { /// @@ -19,7 +20,7 @@ internal interface IDispatch int GetTypeInfoCount(out int info); [PreserveSig] - int GetTypeInfo(int iTInfo, int lcid, out COM.ITypeInfo ppTInfo); + int GetTypeInfo(int iTInfo, int lcid, out COM.ITypeInfo? ppTInfo); void GetIDsOfNames( [MarshalAs(UnmanagedType.LPStruct)] Guid iid, @@ -34,7 +35,7 @@ void Invoke( int lcid, COM.INVOKEKIND wFlags, [In, Out][MarshalAs(UnmanagedType.LPArray)] COM.DISPPARAMS[] paramArray, - out object pVarResult, + out object? pVarResult, out ComInvoker.EXCEPINFO pExcepInfo, out uint puArgErr); } diff --git a/src/System.Management.Automation/engine/ComInterop/ComBinder.cs b/src/System.Management.Automation/engine/ComInterop/ComBinder.cs index 726f336ec71..37a52498e7f 100644 --- a/src/System.Management.Automation/engine/ComInterop/ComBinder.cs +++ b/src/System.Management.Automation/engine/ComInterop/ComBinder.cs @@ -310,8 +310,8 @@ internal class ComGetMemberBinder : GetMemberBinder private readonly GetMemberBinder _originalBinder; internal bool _canReturnCallables; - internal ComGetMemberBinder(GetMemberBinder originalBinder, bool canReturnCallables) : - base(originalBinder.Name, originalBinder.IgnoreCase) + internal ComGetMemberBinder(GetMemberBinder originalBinder, bool canReturnCallables) + : base(originalBinder.Name, originalBinder.IgnoreCase) { _originalBinder = originalBinder; _canReturnCallables = canReturnCallables; @@ -343,8 +343,8 @@ internal class ComInvokeMemberBinder : InvokeMemberBinder private readonly InvokeMemberBinder _originalBinder; internal bool IsPropertySet; - internal ComInvokeMemberBinder(InvokeMemberBinder originalBinder, bool isPropertySet) : - base(originalBinder.Name, originalBinder.IgnoreCase, originalBinder.CallInfo) + internal ComInvokeMemberBinder(InvokeMemberBinder originalBinder, bool isPropertySet) + : base(originalBinder.Name, originalBinder.IgnoreCase, originalBinder.CallInfo) { _originalBinder = originalBinder; this.IsPropertySet = isPropertySet; diff --git a/src/System.Management.Automation/engine/ComInterop/ComMethodDesc.cs b/src/System.Management.Automation/engine/ComInterop/ComMethodDesc.cs index 35b4c9fa25d..45932e1598a 100644 --- a/src/System.Management.Automation/engine/ComInterop/ComMethodDesc.cs +++ b/src/System.Management.Automation/engine/ComInterop/ComMethodDesc.cs @@ -95,7 +95,9 @@ public bool IsPropertyPutRef } internal int ParamCount { get; } + public Type ReturnType { get; set; } + public Type InputType { get; set; } public ParameterInformation[] ParameterInformation diff --git a/src/System.Management.Automation/engine/ComInterop/ComRuntimeHelpers.cs b/src/System.Management.Automation/engine/ComInterop/ComRuntimeHelpers.cs index c10f1424cf5..39038595abc 100644 --- a/src/System.Management.Automation/engine/ComInterop/ComRuntimeHelpers.cs +++ b/src/System.Management.Automation/engine/ComInterop/ComRuntimeHelpers.cs @@ -296,8 +296,15 @@ internal static class UnsafeMethods { #region public members - public static unsafe IntPtr ConvertInt32ByrefToPtr(ref int value) { return (IntPtr)System.Runtime.CompilerServices.Unsafe.AsPointer(ref value); } - public static unsafe IntPtr ConvertVariantByrefToPtr(ref Variant value) { return (IntPtr)System.Runtime.CompilerServices.Unsafe.AsPointer(ref value); } + public static unsafe IntPtr ConvertInt32ByrefToPtr(ref int value) + { + return (IntPtr)System.Runtime.CompilerServices.Unsafe.AsPointer(ref value); + } + + public static unsafe IntPtr ConvertVariantByrefToPtr(ref Variant value) + { + return (IntPtr)System.Runtime.CompilerServices.Unsafe.AsPointer(ref value); + } internal static Variant GetVariantForObject(object obj) { @@ -365,8 +372,7 @@ public static unsafe int IDispatchInvoke( fixed (ExcepInfo* pExcepInfo = &excepInfo) fixed (uint* pArgErr = &argErr) { - var pfnIDispatchInvoke = (delegate* unmanaged) - (*(*(void***)dispatchPointer + 6 /* IDispatch.Invoke slot */)); + var pfnIDispatchInvoke = (delegate* unmanaged)(*(*(void***)dispatchPointer + 6 /* IDispatch.Invoke slot */)); int hresult = pfnIDispatchInvoke(dispatchPointer, memberDispId, &IID_NULL, 0, (ushort)flags, pDispParams, pResult, pExcepInfo, pArgErr); diff --git a/src/System.Management.Automation/engine/ComInterop/ComTypeClassDesc.cs b/src/System.Management.Automation/engine/ComInterop/ComTypeClassDesc.cs index 1ff4fac18cf..fdf0751d85c 100644 --- a/src/System.Management.Automation/engine/ComInterop/ComTypeClassDesc.cs +++ b/src/System.Management.Automation/engine/ComInterop/ComTypeClassDesc.cs @@ -24,8 +24,7 @@ public object CreateInstance() return Activator.CreateInstance(Type.GetTypeFromCLSID(Guid)); } - internal ComTypeClassDesc(ComTypes.ITypeInfo typeInfo, ComTypeLibDesc typeLibDesc) : - base(typeInfo, typeLibDesc) + internal ComTypeClassDesc(ComTypes.ITypeInfo typeInfo, ComTypeLibDesc typeLibDesc) : base(typeInfo, typeLibDesc) { ComTypes.TYPEATTR typeAttr = ComRuntimeHelpers.GetTypeAttrForTypeInfo(typeInfo); Guid = typeAttr.guid; diff --git a/src/System.Management.Automation/engine/ComInterop/ComTypeDesc.cs b/src/System.Management.Automation/engine/ComInterop/ComTypeDesc.cs index 92e9ea8ed6f..a4b90913e9b 100644 --- a/src/System.Management.Automation/engine/ComInterop/ComTypeDesc.cs +++ b/src/System.Management.Automation/engine/ComInterop/ComTypeDesc.cs @@ -118,6 +118,7 @@ internal bool TryGetPutRef(string name, out ComMethodDesc method) method = null; return false; } + internal void AddPutRef(string name, ComMethodDesc method) { name = name.ToUpper(System.Globalization.CultureInfo.InvariantCulture); diff --git a/src/System.Management.Automation/engine/ComInterop/ComTypeEnumDesc.cs b/src/System.Management.Automation/engine/ComInterop/ComTypeEnumDesc.cs index 5752fac1e5f..1f59dc475e5 100644 --- a/src/System.Management.Automation/engine/ComInterop/ComTypeEnumDesc.cs +++ b/src/System.Management.Automation/engine/ComInterop/ComTypeEnumDesc.cs @@ -20,8 +20,7 @@ public override string ToString() return string.Format(CultureInfo.CurrentCulture, "", TypeName); } - internal ComTypeEnumDesc(ComTypes.ITypeInfo typeInfo, ComTypeLibDesc typeLibDesc) : - base(typeInfo, typeLibDesc) + internal ComTypeEnumDesc(ComTypes.ITypeInfo typeInfo, ComTypeLibDesc typeLibDesc) : base(typeInfo, typeLibDesc) { ComTypes.TYPEATTR typeAttr = ComRuntimeHelpers.GetTypeAttrForTypeInfo(typeInfo); string[] memberNames = new string[typeAttr.cVars]; diff --git a/src/System.Management.Automation/engine/ComInterop/InteropServices/Variant.cs b/src/System.Management.Automation/engine/ComInterop/InteropServices/Variant.cs index 24405957f72..84a31b26426 100644 --- a/src/System.Management.Automation/engine/ComInterop/InteropServices/Variant.cs +++ b/src/System.Management.Automation/engine/ComInterop/InteropServices/Variant.cs @@ -343,6 +343,7 @@ public sbyte AsI1 Debug.Assert(VariantType == VarEnum.VT_I1); return _typeUnion._unionTypes._i1; } + set { Debug.Assert(IsEmpty); @@ -360,6 +361,7 @@ public short AsI2 Debug.Assert(VariantType == VarEnum.VT_I2); return _typeUnion._unionTypes._i2; } + set { Debug.Assert(IsEmpty); @@ -377,6 +379,7 @@ public int AsI4 Debug.Assert(VariantType == VarEnum.VT_I4); return _typeUnion._unionTypes._i4; } + set { Debug.Assert(IsEmpty); @@ -394,6 +397,7 @@ public long AsI8 Debug.Assert(VariantType == VarEnum.VT_I8); return _typeUnion._unionTypes._i8; } + set { Debug.Assert(IsEmpty); @@ -411,6 +415,7 @@ public byte AsUi1 Debug.Assert(VariantType == VarEnum.VT_UI1); return _typeUnion._unionTypes._ui1; } + set { Debug.Assert(IsEmpty); @@ -428,6 +433,7 @@ public ushort AsUi2 Debug.Assert(VariantType == VarEnum.VT_UI2); return _typeUnion._unionTypes._ui2; } + set { Debug.Assert(IsEmpty); @@ -445,6 +451,7 @@ public uint AsUi4 Debug.Assert(VariantType == VarEnum.VT_UI4); return _typeUnion._unionTypes._ui4; } + set { Debug.Assert(IsEmpty); @@ -462,6 +469,7 @@ public ulong AsUi8 Debug.Assert(VariantType == VarEnum.VT_UI8); return _typeUnion._unionTypes._ui8; } + set { Debug.Assert(IsEmpty); @@ -479,6 +487,7 @@ public int AsInt Debug.Assert(VariantType == VarEnum.VT_INT); return _typeUnion._unionTypes._int; } + set { Debug.Assert(IsEmpty); @@ -496,6 +505,7 @@ public uint AsUint Debug.Assert(VariantType == VarEnum.VT_UINT); return _typeUnion._unionTypes._uint; } + set { Debug.Assert(IsEmpty); @@ -513,6 +523,7 @@ public bool AsBool Debug.Assert(VariantType == VarEnum.VT_BOOL); return _typeUnion._unionTypes._bool != 0; } + set { Debug.Assert(IsEmpty); @@ -532,6 +543,7 @@ public int AsError Debug.Assert(VariantType == VarEnum.VT_ERROR); return _typeUnion._unionTypes._error; } + set { Debug.Assert(IsEmpty); @@ -549,6 +561,7 @@ public float AsR4 Debug.Assert(VariantType == VarEnum.VT_R4); return _typeUnion._unionTypes._r4; } + set { Debug.Assert(IsEmpty); @@ -566,6 +579,7 @@ public double AsR8 Debug.Assert(VariantType == VarEnum.VT_R8); return _typeUnion._unionTypes._r8; } + set { Debug.Assert(IsEmpty); @@ -586,6 +600,7 @@ public decimal AsDecimal v._typeUnion._vt = 0; return v._decimal; } + set { Debug.Assert(IsEmpty); @@ -605,6 +620,7 @@ public decimal AsCy Debug.Assert(VariantType == VarEnum.VT_CY); return decimal.FromOACurrency(_typeUnion._unionTypes._cy); } + set { Debug.Assert(IsEmpty); @@ -622,6 +638,7 @@ public DateTime AsDate Debug.Assert(VariantType == VarEnum.VT_DATE); return DateTime.FromOADate(_typeUnion._unionTypes._date); } + set { Debug.Assert(IsEmpty); @@ -639,6 +656,7 @@ public string AsBstr Debug.Assert(VariantType == VarEnum.VT_BSTR); return (string)Marshal.PtrToStringBSTR(this._typeUnion._unionTypes._bstr); } + set { Debug.Assert(IsEmpty); @@ -660,6 +678,7 @@ public object? AsUnknown } return Marshal.GetObjectForIUnknown(_typeUnion._unionTypes._unknown); } + set { Debug.Assert(IsEmpty); @@ -688,6 +707,7 @@ public object? AsDispatch } return Marshal.GetObjectForIUnknown(_typeUnion._unionTypes._dispatch); } + set { Debug.Assert(IsEmpty); diff --git a/src/System.Management.Automation/engine/ComInterop/SplatCallSite.cs b/src/System.Management.Automation/engine/ComInterop/SplatCallSite.cs index 68c394033fc..ecbff91a36c 100644 --- a/src/System.Management.Automation/engine/ComInterop/SplatCallSite.cs +++ b/src/System.Management.Automation/engine/ComInterop/SplatCallSite.cs @@ -24,6 +24,7 @@ internal SplatCallSite(object callable) } public delegate object InvokeDelegate(object[] args); + internal object Invoke(object[] args) { Debug.Assert(args != null); diff --git a/src/System.Management.Automation/engine/ComInterop/TypeUtils.cs b/src/System.Management.Automation/engine/ComInterop/TypeUtils.cs index 0d4ee61dcf7..041d7ccfd07 100644 --- a/src/System.Management.Automation/engine/ComInterop/TypeUtils.cs +++ b/src/System.Management.Automation/engine/ComInterop/TypeUtils.cs @@ -38,6 +38,7 @@ internal static bool AreReferenceAssignable(Type dest, Type src) } return false; } + //CONFORMING internal static bool AreAssignable(Type dest, Type src) { diff --git a/src/System.Management.Automation/engine/ComInterop/VariantBuilder.cs b/src/System.Management.Automation/engine/ComInterop/VariantBuilder.cs index edcb0eb0f10..baabb25cd75 100644 --- a/src/System.Management.Automation/engine/ComInterop/VariantBuilder.cs +++ b/src/System.Management.Automation/engine/ComInterop/VariantBuilder.cs @@ -16,6 +16,7 @@ internal class VariantBuilder private MemberExpression _variant; private readonly ArgBuilder _argBuilder; private readonly VarEnum _targetComType; + internal ParameterExpression TempVariable { get; private set; } internal VariantBuilder(VarEnum targetComType, ArgBuilder builder) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index fcc83d21456..76be6418805 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -9,6 +9,8 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.DSC; namespace System.Management.Automation { @@ -473,7 +475,7 @@ internal List GetResultHelper(CompletionContext completionCont break; completionContext.WordToComplete = tokenAtCursor.Text; - result = CompletionCompleters.CompleteComment(completionContext); + result = CompletionCompleters.CompleteComment(completionContext, ref replacementIndex, ref replacementLength); break; case TokenKind.StringExpandable: @@ -1721,9 +1723,16 @@ private static List GetResultForIdentifierInConfiguration( foreach (var keyword in matchedResults) { - string usageString = Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache.NewApiIsUsed - ? Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache.GetDSCResourceUsageString(keyword) - : Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache.GetDSCResourceUsageString(keyword); + string usageString = string.Empty; + ICrossPlatformDsc dscSubsystem = SubsystemManager.GetSubsystem(); + if (dscSubsystem != null) + { + usageString = dscSubsystem.GetDSCResourceUsageString(keyword); + } + else + { + usageString = Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache.GetDSCResourceUsageString(keyword); + } if (results == null) { diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index b70f3fd67ca..35a7398454a 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4921,13 +4921,27 @@ private static SortedSet BuildSpecialVariablesCache() #region Comments - // Complete the history entries - internal static List CompleteComment(CompletionContext context) + internal static List CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength) { - List results = new List(); + if (context.WordToComplete.StartsWith("<#", StringComparison.Ordinal)) + { + return CompleteCommentHelp(context, ref replacementIndex, ref replacementLength); + } + + // Complete #requires statements + if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase)) + { + return CompleteRequires(context, ref replacementIndex, ref replacementLength); + } + + var results = new List(); + // Complete the history entries Match matchResult = Regex.Match(context.WordToComplete, @"^#([\w\-]*)$"); - if (!matchResult.Success) { return results; } + if (!matchResult.Success) + { + return results; + } string wordToComplete = matchResult.Groups[1].Value; Collection psobjs; @@ -4988,6 +5002,447 @@ internal static List CompleteComment(CompletionContext context return results; } + private static List CompleteRequires(CompletionContext context, ref int replacementIndex, ref int replacementLength) + { + var results = new List(); + + int cursorIndex = context.CursorPosition.ColumnNumber - 1; + string lineToCursor = context.CursorPosition.Line.Substring(0, cursorIndex); + + // RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter. + if (lineToCursor.Contains(" -RunAsAdministrator", StringComparison.OrdinalIgnoreCase)) + { + return results; + } + + // Regex to find parameter like " -Parameter1" or " -" + MatchCollection hashtableKeyMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)"); + if (hashtableKeyMatches.Count == 0) + { + return results; + } + + Group currentParameterMatch = hashtableKeyMatches[^1].Groups[1]; + + // Complete the parameter if the cursor is at a parameter + if (currentParameterMatch.Index + currentParameterMatch.Length == cursorIndex) + { + string currentParameterPrefix = currentParameterMatch.Value; + + replacementIndex = context.CursorPosition.Offset - currentParameterPrefix.Length; + replacementLength = currentParameterPrefix.Length; + + // Produce completions for all parameters that begin with the prefix we've found, + // but which haven't already been specified in the line we need to complete + foreach (KeyValuePair parameter in s_requiresParameters) + { + if (parameter.Key.StartsWith(currentParameterPrefix, StringComparison.OrdinalIgnoreCase) + && !context.CursorPosition.Line.Contains($" -{parameter.Key}", StringComparison.OrdinalIgnoreCase)) + { + results.Add(new CompletionResult(parameter.Key, parameter.Key, CompletionResultType.ParameterName, parameter.Value)); + } + } + + return results; + } + + // Regex to find parameter values (any text that appears after various delimiters) + hashtableKeyMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)"); + string currentValue; + if (hashtableKeyMatches.Count == 0) + { + currentValue = string.Empty; + } + else + { + currentValue = hashtableKeyMatches[^1].Groups[2].Value; + } + + replacementIndex = context.CursorPosition.Offset - currentValue.Length; + replacementLength = currentValue.Length; + + // Complete PSEdition parameter values + if (currentParameterMatch.Value.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) + { + foreach (KeyValuePair psEditionEntry in s_requiresPSEditions) + { + if (psEditionEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) + { + results.Add(new CompletionResult(psEditionEntry.Key, psEditionEntry.Key, CompletionResultType.ParameterValue, psEditionEntry.Value)); + } + } + + return results; + } + + // Complete Modules module specification values + if (currentParameterMatch.Value.Equals("Modules", StringComparison.OrdinalIgnoreCase)) + { + int hashtableStart = lineToCursor.LastIndexOf("@{"); + int hashtableEnd = lineToCursor.LastIndexOf('}'); + + bool insideHashtable = hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart); + + // If not inside a hashtable, try to complete a module simple name + if (!insideHashtable) + { + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); + } + + string hashtableString = lineToCursor.Substring(hashtableStart); + + // Regex to find hashtable keys with or without quotes + hashtableKeyMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); + + // Build the list of keys we might want to complete, based on what's already been provided + var moduleSpecKeysToComplete = new HashSet(s_requiresModuleSpecKeys.Keys); + bool sawModuleNameLast = false; + foreach (Match existingHashtableKeyMatch in hashtableKeyMatches) + { + string existingHashtableKey = existingHashtableKeyMatch.Value.TrimStart(s_hashtableKeyPrefixes); + + if (string.IsNullOrEmpty(existingHashtableKey)) + { + continue; + } + + // Remove the existing key we just saw + moduleSpecKeysToComplete.Remove(existingHashtableKey); + + // We need to remember later if we saw "ModuleName" as the last hashtable key, for completions + if (sawModuleNameLast = existingHashtableKey.Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // "RequiredVersion" is mutually exclusive with "ModuleVersion" and "MaximumVersion" + if (existingHashtableKey.Equals("ModuleVersion", StringComparison.OrdinalIgnoreCase) + || existingHashtableKey.Equals("MaximumVersion", StringComparison.OrdinalIgnoreCase)) + { + moduleSpecKeysToComplete.Remove("RequiredVersion"); + continue; + } + + if (existingHashtableKey.Equals("RequiredVersion", StringComparison.OrdinalIgnoreCase)) + { + moduleSpecKeysToComplete.Remove("ModuleVersion"); + moduleSpecKeysToComplete.Remove("MaximumVersion"); + continue; + } + } + + Group lastHashtableKeyPrefixGroup = hashtableKeyMatches[^1].Groups[0]; + + // If we're not completing a key for the hashtable, try to complete module names, but nothing else + bool completingHashtableKey = lastHashtableKeyPrefixGroup.Index + lastHashtableKeyPrefixGroup.Length == hashtableString.Length; + if (!completingHashtableKey) + { + if (sawModuleNameLast) + { + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); + } + + return results; + } + + // Now try to complete hashtable keys + foreach (string moduleSpecKey in moduleSpecKeysToComplete) + { + if (moduleSpecKey.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) + { + results.Add(new CompletionResult(moduleSpecKey, moduleSpecKey, CompletionResultType.ParameterValue, s_requiresModuleSpecKeys[moduleSpecKey])); + } + } + } + + return results; + } + + private static readonly IReadOnlyDictionary s_requiresParameters = new SortedList(StringComparer.OrdinalIgnoreCase) + { + { "Modules", "Specifies PowerShell modules that the script requires." }, + { "PSEdition", "Specifies a PowerShell edition that the script requires." }, + { "RunAsAdministrator", "Specifies that PowerShell must be running as administrator on Windows." }, + { "Version", "Specifies the minimum version of PowerShell that the script requires." }, + }; + + private static readonly IReadOnlyDictionary s_requiresPSEditions = new SortedList(StringComparer.OrdinalIgnoreCase) + { + { "Core", "Specifies that the script requires PowerShell Core to run." }, + { "Desktop", "Specifies that the script requires Windows PowerShell to run." }, + }; + + private static readonly IReadOnlyDictionary s_requiresModuleSpecKeys = new SortedList(StringComparer.OrdinalIgnoreCase) + { + { "ModuleName", "Required. Specifies the module name." }, + { "GUID", "Optional. Specifies the GUID of the module." }, + { "ModuleVersion", "Specifies a minimum acceptable version of the module." }, + { "RequiredVersion", "Specifies an exact, required version of the module." }, + { "MaximumVersion", "Specifies the maximum acceptable version of the module." }, + }; + + private static readonly char[] s_hashtableKeyPrefixes = new[] + { + '@', + '{', + ';', + '"', + '\'', + ' ', + }; + + private static List CompleteCommentHelp(CompletionContext context, ref int replacementIndex, ref int replacementLength) + { + // Finds comment keywords like ".DESCRIPTION" + MatchCollection usedKeywords = Regex.Matches(context.TokenAtCursor.Text, @"(?<=^\s*\.)\w*", RegexOptions.Multiline); + if (usedKeywords.Count == 0) + { + return null; + } + + // Last keyword at or before the cursor + Match lineKeyword = null; + for (int i = usedKeywords.Count - 1; i >= 0; i--) + { + Match keyword = usedKeywords[i]; + if (context.CursorPosition.Offset >= keyword.Index + context.TokenAtCursor.Extent.StartOffset) + { + lineKeyword = keyword; + break; + } + } + + if (lineKeyword is null) + { + return null; + } + + // Cursor is within or at the start/end of the keyword + if (context.CursorPosition.Offset <= lineKeyword.Index + lineKeyword.Length + context.TokenAtCursor.Extent.StartOffset) + { + replacementIndex = context.TokenAtCursor.Extent.StartOffset + lineKeyword.Index; + replacementLength = lineKeyword.Value.Length; + + var validKeywords = new HashSet(s_commentHelpKeywords.Keys, StringComparer.OrdinalIgnoreCase); + foreach (Match keyword in usedKeywords) + { + if (keyword == lineKeyword || s_commentHelpAllowedDuplicateKeywords.Contains(keyword.Value)) + { + continue; + } + + validKeywords.Remove(keyword.Value); + } + + var result = new List(); + foreach (string keyword in validKeywords) + { + if (keyword.StartsWith(lineKeyword.Value, StringComparison.OrdinalIgnoreCase)) + { + result.Add(new CompletionResult(keyword, keyword, CompletionResultType.Keyword, s_commentHelpKeywords[keyword])); + } + } + + return result.Count > 0 ? result : null; + } + + // Finds the argument for the keyword (any characters following the keyword, ignoring leading/trailing whitespace). For example "C:\New folder" + Match keywordArgument = Regex.Match(context.CursorPosition.Line, @"(?<=^\s*\.\w+\s+)\S.*(?<=\S)"); + int lineStartIndex = lineKeyword.Index - context.CursorPosition.Line.IndexOf(lineKeyword.Value) + context.TokenAtCursor.Extent.StartOffset; + int argumentIndex = keywordArgument.Success ? keywordArgument.Index : context.CursorPosition.ColumnNumber - 1; + + replacementIndex = lineStartIndex + argumentIndex; + replacementLength = keywordArgument.Value.Length; + + if (lineKeyword.Value.Equals("PARAMETER", StringComparison.OrdinalIgnoreCase)) + { + return CompleteCommentParameterValue(context, keywordArgument.Value); + } + + if (lineKeyword.Value.Equals("FORWARDHELPTARGETNAME", StringComparison.OrdinalIgnoreCase)) + { + var result = new List(CompleteCommand(keywordArgument.Value, "*", CommandTypes.All)); + return result.Count > 0 ? result : null; + } + + if (lineKeyword.Value.Equals("FORWARDHELPCATEGORY", StringComparison.OrdinalIgnoreCase)) + { + var result = new List(); + foreach (string category in s_commentHelpForwardCategories) + { + if (category.StartsWith(keywordArgument.Value, StringComparison.OrdinalIgnoreCase)) + { + result.Add(new CompletionResult(category)); + } + } + return result.Count > 0 ? result : null; + } + + if (lineKeyword.Value.Equals("REMOTEHELPRUNSPACE", StringComparison.OrdinalIgnoreCase)) + { + var result = new List(); + foreach (CompletionResult variable in CompleteVariable(keywordArgument.Value)) + { + // ListItemText is used because it excludes the "$" as expected by REMOTEHELPRUNSPACE. + result.Add(new CompletionResult(variable.ListItemText, variable.ListItemText, variable.ResultType, variable.ToolTip)); + } + return result.Count > 0 ? result : null; + } + + if (lineKeyword.Value.Equals("EXTERNALHELP", StringComparison.OrdinalIgnoreCase)) + { + context.WordToComplete = keywordArgument.Value; + var result = new List(CompleteFilename(context, containerOnly: false, (new HashSet() { ".xml" }))); + return result.Count > 0 ? result : null; + } + + return null; + } + + private static readonly IReadOnlyDictionary s_commentHelpKeywords = new SortedList(StringComparer.OrdinalIgnoreCase) + { + { "SYNOPSIS", "A brief description of the function or script. This keyword can be used only once in each topic." }, + { "DESCRIPTION", "A detailed description of the function or script. This keyword can be used only once in each topic." }, + { "PARAMETER", ".PARAMETER \nThe description of a parameter. Add a .PARAMETER keyword for each parameter in the function or script syntax." }, + { "EXAMPLE", "A sample command that uses the function or script, optionally followed by sample output and a description. Repeat this keyword for each example." }, + { "INPUTS", "The .NET types of objects that can be piped to the function or script. You can also include a description of the input objects." }, + { "OUTPUTS", "The .NET type of the objects that the cmdlet returns. You can also include a description of the returned objects." }, + { "NOTES", "Additional information about the function or script." }, + { "LINK", "The name of a related topic. Repeat the .LINK keyword for each related topic. The .Link keyword content can also include a URI to an online version of the same help topic." }, + { "COMPONENT", "The name of the technology or feature that the function or script uses, or to which it is related." }, + { "ROLE", "The name of the user role for the help topic." }, + { "FUNCTIONALITY", "The keywords that describe the intended use of the function." }, + { "FORWARDHELPTARGETNAME", ".FORWARDHELPTARGETNAME \nRedirects to the help topic for the specified command." }, + { "FORWARDHELPCATEGORY", ".FORWARDHELPCATEGORY \nSpecifies the help category of the item in .ForwardHelpTargetName" }, + { "REMOTEHELPRUNSPACE", ".REMOTEHELPRUNSPACE \nSpecifies a session that contains the help topic. Enter a variable that contains a PSSession object." }, + { "EXTERNALHELP", ".EXTERNALHELP \nThe .ExternalHelp keyword is required when a function or script is documented in XML files." } + }; + + private static readonly HashSet s_commentHelpAllowedDuplicateKeywords = new(StringComparer.OrdinalIgnoreCase) + { + "PARAMETER", + "EXAMPLE", + "LINK" + }; + + private static readonly string[] s_commentHelpForwardCategories = new string[] + { + "Alias", + "Cmdlet", + "HelpFile", + "Function", + "Provider", + "General", + "FAQ", + "Glossary", + "ScriptCommand", + "ExternalScript", + "Filter", + "All" + }; + + private static FunctionDefinitionAst GetCommentHelpFunctionTarget(CompletionContext context) + { + if (context.TokenAtCursor.Kind != TokenKind.Comment) + { + return null; + } + + Ast lastAst = context.RelatedAsts[^1]; + Ast firstAstAfterComment = lastAst.Find(ast => ast.Extent.StartOffset >= context.TokenAtCursor.Extent.EndOffset, searchNestedScriptBlocks: false); + + // Comment-based help can apply to a following function definition if it starts within 2 lines + int commentEndLine = context.TokenAtCursor.Extent.EndLineNumber + 2; + + if (lastAst is NamedBlockAst) + { + // Helpblock before function inside advanced function + if (firstAstAfterComment is not null + && firstAstAfterComment.Extent.StartLineNumber <= commentEndLine + && firstAstAfterComment is FunctionDefinitionAst outerHelpFunctionDefAst) + { + return outerHelpFunctionDefAst; + } + + // Helpblock inside function + if (lastAst.Parent.Parent is FunctionDefinitionAst innerHelpFunctionDefAst) + { + return innerHelpFunctionDefAst; + } + } + + if (lastAst is ScriptBlockAst) + { + // Helpblock before function + if (firstAstAfterComment is not null + && firstAstAfterComment.Extent.StartLineNumber <= commentEndLine + && firstAstAfterComment is NamedBlockAst block + && block.Statements.Count > 0 + && block.Statements[0] is FunctionDefinitionAst statement) + { + return statement; + } + + // Advanced function with help inside + if (lastAst.Parent is FunctionDefinitionAst advFuncDefAst) + { + return advFuncDefAst; + } + } + + return null; + } + + private static List CompleteCommentParameterValue(CompletionContext context, string wordToComplete) + { + FunctionDefinitionAst foundFunction = GetCommentHelpFunctionTarget(context); + + ReadOnlyCollection foundParameters = null; + if (foundFunction is not null) + { + foundParameters = foundFunction.Parameters ?? foundFunction.Body.ParamBlock?.Parameters; + } + else if (context.RelatedAsts[^1] is ScriptBlockAst scriptAst) + { + // The helpblock is for a script file + foundParameters = scriptAst.ParamBlock?.Parameters; + } + + if (foundParameters is null || foundParameters.Count == 0) + { + return null; + } + + var parametersToShow = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (ParameterAst parameter in foundParameters) + { + if (parameter.Name.VariablePath.UserPath.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + { + parametersToShow.Add(parameter.Name.VariablePath.UserPath); + } + } + + MatchCollection usedParameters = Regex.Matches(context.TokenAtCursor.Text, @"(?<=^\s*\.parameter\s+)\w.*(?<=\S)", RegexOptions.Multiline | RegexOptions.IgnoreCase); + foreach (Match parameter in usedParameters) + { + if (wordToComplete.Equals(parameter.Value, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + parametersToShow.Remove(parameter.Value); + } + + var result = new List(); + foreach (string parameter in parametersToShow) + { + result.Add(new CompletionResult(parameter)); + } + + return result.Count > 0 ? result : null; + } + #endregion Comments #region Members @@ -6070,35 +6525,36 @@ private static string GetNamespaceToRemove(CompletionContext context, TypeComple internal static List CompleteHelpTopics(CompletionContext context) { var results = new List(); - var searchPaths = new List(); - var currentCulture = CultureInfo.CurrentCulture.Name; - - // Add the user scope path first, since it is searched in order. - var userHelpRoot = Path.Combine(HelpUtils.GetUserHomeHelpSearchPath(), currentCulture); - - if (Directory.Exists(userHelpRoot)) - { - searchPaths.Add(userHelpRoot); - } - - var dirPath = Path.Combine(Utils.GetApplicationBase(Utils.DefaultPowerShellShellID), currentCulture); - searchPaths.Add(dirPath); - - var wordToComplete = context.WordToComplete + "*"; - var topicPattern = WildcardPattern.Get("about_*.help.txt", WildcardOptions.IgnoreCase); - List files = new List(); - + string userHelpDir = HelpUtils.GetUserHomeHelpSearchPath(); + string appHelpDir = Utils.GetApplicationBase(Utils.DefaultPowerShellShellID); + string currentCulture = CultureInfo.CurrentCulture.Name; + + //search for help files for the current culture + en-US as fallback + var searchPaths = new string[] + { + Path.Combine(userHelpDir, currentCulture), + Path.Combine(appHelpDir, currentCulture), + Path.Combine(userHelpDir, "en-US"), + Path.Combine(appHelpDir, "en-US") + }.Distinct(); + + string wordToComplete = context.WordToComplete + "*"; try { var wildcardPattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase); foreach (var dir in searchPaths) { - foreach (var file in Directory.EnumerateFiles(dir)) + var currentDir = new DirectoryInfo(dir); + if (currentDir.Exists) { - if (wildcardPattern.IsMatch(Path.GetFileName(file))) + foreach (var file in currentDir.EnumerateFiles("about_*.help.txt")) { - files.Add(file); + if (wildcardPattern.IsMatch(file.Name)) + { + string topicName = file.Name.Substring(0, file.Name.LastIndexOf(".help.txt")); + results.Add(new CompletionResult(topicName)); + } } } } @@ -6106,33 +6562,6 @@ internal static List CompleteHelpTopics(CompletionContext cont catch (Exception) { } - - if (files != null) - { - foreach (string file in files) - { - if (file == null) - { - continue; - } - - try - { - var fileName = Path.GetFileName(file); - if (fileName == null || !topicPattern.IsMatch(fileName)) - continue; - - // All topic files are ending with ".help.txt" - var completionText = fileName.Substring(0, fileName.Length - 9); - results.Add(new CompletionResult(completionText)); - } - catch (Exception) - { - continue; - } - } - } - return results; } diff --git a/src/System.Management.Automation/engine/DefaultCommandRuntime.cs b/src/System.Management.Automation/engine/DefaultCommandRuntime.cs index d45abaaffbc..48d74667dae 100644 --- a/src/System.Management.Automation/engine/DefaultCommandRuntime.cs +++ b/src/System.Management.Automation/engine/DefaultCommandRuntime.cs @@ -230,6 +230,7 @@ public PSTransactionContext CurrentPSTransaction /// if it exists, otherwise throw an invalid operation exception. /// /// The error record to throw. + [System.Diagnostics.CodeAnalysis.DoesNotReturn] public void ThrowTerminatingError(ErrorRecord errorRecord) { if (errorRecord.Exception != null) diff --git a/src/System.Management.Automation/engine/ErrorPackage.cs b/src/System.Management.Automation/engine/ErrorPackage.cs index 7d6a1f2759f..1f17a501736 100644 --- a/src/System.Management.Automation/engine/ErrorPackage.cs +++ b/src/System.Management.Automation/engine/ErrorPackage.cs @@ -1815,6 +1815,7 @@ public interface IContainsErrorRecord /// since the improved /// information about the error may help enable future scenarios. /// +#nullable enable public interface IResourceSupplier { /// diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index b0c7c3d56c7..70d69ba35b2 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -125,9 +125,6 @@ static ExperimentalFeature() new ExperimentalFeature( name: "PSNotApplyErrorActionToStderr", description: "Don't have $ErrorActionPreference affect stderr output"), - new ExperimentalFeature( - name: "PS7DscSupport", - description: "Support the cross-platform class-based DSC"), new ExperimentalFeature( name: "PSSubsystemPluginModel", description: "A plugin model for registering and un-registering PowerShell subsystems"), @@ -140,7 +137,11 @@ static ExperimentalFeature() new ExperimentalFeature( name: PSNativeCommandArgumentPassingFeatureName, description: "Use ArgumentList when invoking a native command"), + new ExperimentalFeature( + name: "PSLoadAssemblyFromNativeCode", + description: "Expose an API to allow assembly loading from native code"), }; + EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures); // Initialize the readonly dictionary 'EngineExperimentalFeatureMap'. diff --git a/src/System.Management.Automation/engine/ExternalScriptInfo.cs b/src/System.Management.Automation/engine/ExternalScriptInfo.cs index bbd49c76d36..9bf3318a2c8 100644 --- a/src/System.Management.Automation/engine/ExternalScriptInfo.cs +++ b/src/System.Management.Automation/engine/ExternalScriptInfo.cs @@ -242,7 +242,7 @@ private static ScriptBlock ParseScriptContents(Parser parser, string fileName, s // If we are in ConstrainedLanguage mode but the defining language mode is FullLanguage, then we need // to parse the script contents in FullLanguage mode context. Otherwise we will get bogus parsing errors // such as "Configuration keyword not allowed". - if (definingLanguageMode.HasValue && (definingLanguageMode == PSLanguageMode.FullLanguage)) + if (definingLanguageMode.GetValueOrDefault() == PSLanguageMode.FullLanguage) { var context = LocalPipeline.GetExecutionContextFromTLS(); if ((context != null) && (context.LanguageMode == PSLanguageMode.ConstrainedLanguage)) diff --git a/src/System.Management.Automation/engine/ICommandRuntime.cs b/src/System.Management.Automation/engine/ICommandRuntime.cs index 6f0b3ce9d57..03d983354f0 100644 --- a/src/System.Management.Automation/engine/ICommandRuntime.cs +++ b/src/System.Management.Automation/engine/ICommandRuntime.cs @@ -549,6 +549,7 @@ public interface ICommandRuntime /// if any information is to be added. It should encapsulate the /// error record into an exception and then throw that exception. /// + [System.Diagnostics.CodeAnalysis.DoesNotReturn] void ThrowTerminatingError(ErrorRecord errorRecord); #endregion ThrowTerminatingError #endregion misc diff --git a/src/System.Management.Automation/engine/LanguagePrimitives.cs b/src/System.Management.Automation/engine/LanguagePrimitives.cs index 83361fe9662..f6261b85cd1 100644 --- a/src/System.Management.Automation/engine/LanguagePrimitives.cs +++ b/src/System.Management.Automation/engine/LanguagePrimitives.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Linq; using System.Linq.Expressions; using System.Management.Automation.Internal; using System.Management.Automation.Language; @@ -339,12 +338,13 @@ internal static void UpdateTypeConvertFromTypeTable(string typeName) { lock (s_converterCache) { - var toRemove = s_converterCache.Keys.Where( - conv => string.Equals(conv.to.FullName, typeName, StringComparison.OrdinalIgnoreCase) || - string.Equals(conv.from.FullName, typeName, StringComparison.OrdinalIgnoreCase)).ToArray(); - foreach (var k in toRemove) + foreach (var key in s_converterCache.Keys) { - s_converterCache.Remove(k); + if (string.Equals(key.to.FullName, typeName, StringComparison.OrdinalIgnoreCase) + || string.Equals(key.from.FullName, typeName, StringComparison.OrdinalIgnoreCase)) + { + s_converterCache.Remove(key); + } } // Note we do not clear possibleTypeConverter even when removing. diff --git a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs index d3267ad79a6..362eb5b32e2 100644 --- a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs +++ b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs @@ -33,7 +33,7 @@ internal static class AnalysisCache // This dictionary shouldn't see much use, so low concurrency and capacity private static readonly ConcurrentDictionary s_modulesBeingAnalyzed = - new ConcurrentDictionary( /*concurrency*/1, /*capacity*/2, StringComparer.OrdinalIgnoreCase); + new(concurrencyLevel: 1, capacity: 2, StringComparer.OrdinalIgnoreCase); internal static readonly char[] InvalidCommandNameCharacters = new[] { @@ -384,8 +384,10 @@ private static ConcurrentDictionary AnalyzeScriptModule(st } } - var exportedClasses = new ConcurrentDictionary( /*concurrency*/ - 1, scriptAnalysis.DiscoveredClasses.Count, StringComparer.OrdinalIgnoreCase); + ConcurrentDictionary exportedClasses = new( + concurrencyLevel: 1, + capacity: scriptAnalysis.DiscoveredClasses.Count, + StringComparer.OrdinalIgnoreCase); foreach (var exportedClass in scriptAnalysis.DiscoveredClasses) { exportedClasses[exportedClass.Name] = exportedClass.TypeAttributes; diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index 89a8296deb3..6ae212655ab 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -1715,6 +1715,7 @@ internal enum ModuleMatchFailure NullModuleSpecification, } +#nullable enable /// /// Used by Modules/Snapins to provide a hook to the engine for startup initialization /// w.r.t compiled assembly loading. diff --git a/src/System.Management.Automation/engine/MshCmdlet.cs b/src/System.Management.Automation/engine/MshCmdlet.cs index a2a7023b2d0..2c899531866 100644 --- a/src/System.Management.Automation/engine/MshCmdlet.cs +++ b/src/System.Management.Automation/engine/MshCmdlet.cs @@ -14,6 +14,7 @@ namespace System.Management.Automation { #region Auxiliary + /// /// An interface that a /// or @@ -31,6 +32,7 @@ namespace System.Management.Automation /// /// /// +#nullable enable public interface IDynamicParameters { /// @@ -62,8 +64,10 @@ public interface IDynamicParameters /// may not be set at the time this method is called, /// even if the parameters are mandatory. /// - object GetDynamicParameters(); + object? GetDynamicParameters(); } +#nullable restore + /// /// Type used to define a parameter on a cmdlet script of function that /// can only be used as a switch. diff --git a/src/System.Management.Automation/engine/MshCommandRuntime.cs b/src/System.Management.Automation/engine/MshCommandRuntime.cs index e43e49631aa..bf8fe32fd8e 100644 --- a/src/System.Management.Automation/engine/MshCommandRuntime.cs +++ b/src/System.Management.Automation/engine/MshCommandRuntime.cs @@ -2055,6 +2055,7 @@ public PSTransactionContext CurrentPSTransaction /// . /// etc. /// + [System.Diagnostics.CodeAnalysis.DoesNotReturn] public void ThrowTerminatingError(ErrorRecord errorRecord) { ThrowIfStopping(); diff --git a/src/System.Management.Automation/engine/NativeCommandProcessor.cs b/src/System.Management.Automation/engine/NativeCommandProcessor.cs index 6899a1de0ef..32f2ee1f7ec 100644 --- a/src/System.Management.Automation/engine/NativeCommandProcessor.cs +++ b/src/System.Management.Automation/engine/NativeCommandProcessor.cs @@ -1018,9 +1018,21 @@ private void CleanUp() try { - // Dispose the process if it's already created if (_nativeProcess != null) { + // on Unix, we need to kill the process to ensure it terminates as Dispose() merely + // closes the redirected streams and the processs does not exit on macOS. However, + // on Windows, a winexe like notepad should continue running so we don't want to kill it. +#if UNIX + try + { + _nativeProcess.Kill(); + } + catch + { + // Ignore all exception since it is cleanup. + } +#endif _nativeProcess.Dispose(); } } diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs index edc8fb272a2..7a87b259314 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs @@ -10,7 +10,7 @@ using System.Threading; using System.Threading.Tasks; -namespace System.Management.Automation.Subsystem +namespace System.Management.Automation.Subsystem.Prediction { /// /// The class represents the prediction result from a predictor. @@ -59,9 +59,9 @@ public static class CommandPrediction /// The object from parsing the current command line input. /// The objects from parsing the current command line input. /// A list of objects. - public static Task?> PredictInput(string client, Ast ast, Token[] astTokens) + public static Task?> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens) { - return PredictInput(client, ast, astTokens, millisecondsTimeout: 20); + return PredictInputAsync(client, ast, astTokens, millisecondsTimeout: 20); } /// @@ -72,7 +72,7 @@ public static class CommandPrediction /// The objects from parsing the current command line input. /// The milliseconds to timeout. /// A list of objects. - public static async Task?> PredictInput(string client, Ast ast, Token[] astTokens, int millisecondsTimeout) + public static async Task?> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens, int millisecondsTimeout) { Requires.Condition(millisecondsTimeout > 0, nameof(millisecondsTimeout)); @@ -86,17 +86,13 @@ public static class CommandPrediction var tasks = new Task[predictors.Count]; using var cancellationSource = new CancellationTokenSource(); + Func callBack = GetCallBack(client, context, cancellationSource); + for (int i = 0; i < predictors.Count; i++) { ICommandPredictor predictor = predictors[i]; - tasks[i] = Task.Factory.StartNew( - state => - { - var predictor = (ICommandPredictor)state!; - SuggestionPackage pkg = predictor.GetSuggestion(client, context, cancellationSource.Token); - return pkg.SuggestionEntries?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, pkg.Session, pkg.SuggestionEntries) : null; - }, + callBack, predictor, cancellationSource.Token, TaskCreationOptions.DenyChildAttach, @@ -122,6 +118,21 @@ await Task.WhenAny( } return resultList; + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered. + static Func GetCallBack( + PredictionClient client, + PredictionContext context, + CancellationTokenSource cancellationSource) + { + return state => + { + var predictor = (ICommandPredictor)state!; + SuggestionPackage pkg = predictor.GetSuggestion(client, context, cancellationSource.Token); + return pkg.SuggestionEntries?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, pkg.Session, pkg.SuggestionEntries) : null; + }; + } } /// @@ -129,7 +140,7 @@ await Task.WhenAny( /// /// Represents the client that initiates the call. /// History command lines provided as references for prediction. - public static void OnCommandLineAccepted(string client, IReadOnlyList history) + public static void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history) { Requires.NotNull(history, nameof(history)); @@ -139,16 +150,54 @@ public static void OnCommandLineAccepted(string client, IReadOnlyList hi return; } + Action? callBack = null; + foreach (ICommandPredictor predictor in predictors) + { + if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.CommandLineAccepted)) + { + callBack ??= GetCallBack(client, history); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); + } + } + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered, or no registered predictor accepts this feedback. + static Action GetCallBack(PredictionClient client, IReadOnlyList history) + { + return predictor => predictor.OnCommandLineAccepted(client, history); + } + } + + /// + /// Allow registered predictors to know the execution result (success/failure) of the last accepted command line. + /// + /// Represents the client that initiates the call. + /// The last accepted command line. + /// Whether the execution of the last command line was successful. + public static void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) + { + var predictors = SubsystemManager.GetSubsystems(); + if (predictors.Count == 0) + { + return; + } + + Action? callBack = null; foreach (ICommandPredictor predictor in predictors) { - if (predictor.SupportEarlyProcessing) + if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.CommandLineExecuted)) { - ThreadPool.QueueUserWorkItem( - state => state.StartEarlyProcessing(client, history), - predictor, - preferLocal: false); + callBack ??= GetCallBack(client, commandLine, success); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); } } + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered, or no registered predictor accepts this feedback. + static Action GetCallBack(PredictionClient client, string commandLine, bool success) + { + return predictor => predictor.OnCommandLineExecuted(client, commandLine, success); + } } /// @@ -161,7 +210,7 @@ public static void OnCommandLineAccepted(string client, IReadOnlyList hi /// When the value is greater than 0, it's the number of displayed suggestions from the list returned in , starting from the index 0. /// When the value is less than or equal to 0, it means a single suggestion from the list got displayed, and the index is the absolute value. /// - public static void OnSuggestionDisplayed(string client, Guid predictorId, uint session, int countOrIndex) + public static void OnSuggestionDisplayed(PredictionClient client, Guid predictorId, uint session, int countOrIndex) { var predictors = SubsystemManager.GetSubsystems(); if (predictors.Count == 0) @@ -171,14 +220,24 @@ public static void OnSuggestionDisplayed(string client, Guid predictorId, uint s foreach (ICommandPredictor predictor in predictors) { - if (predictor.AcceptFeedback && predictor.Id == predictorId) + if (predictor.Id == predictorId) { - ThreadPool.QueueUserWorkItem( - state => state.OnSuggestionDisplayed(client, session, countOrIndex), - predictor, - preferLocal: false); + if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.SuggestionDisplayed)) + { + Action callBack = GetCallBack(client, session, countOrIndex); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); + } + + break; } } + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered, or no registered predictor accepts this feedback. + static Action GetCallBack(PredictionClient client, uint session, int countOrIndex) + { + return predictor => predictor.OnSuggestionDisplayed(client, session, countOrIndex); + } } /// @@ -188,7 +247,7 @@ public static void OnSuggestionDisplayed(string client, Guid predictorId, uint s /// The identifier of the predictor whose prediction result was accepted. /// The mini-session where the accepted suggestion came from. /// The accepted suggestion text. - public static void OnSuggestionAccepted(string client, Guid predictorId, uint session, string suggestionText) + public static void OnSuggestionAccepted(PredictionClient client, Guid predictorId, uint session, string suggestionText) { Requires.NotNullOrEmpty(suggestionText, nameof(suggestionText)); @@ -200,14 +259,24 @@ public static void OnSuggestionAccepted(string client, Guid predictorId, uint se foreach (ICommandPredictor predictor in predictors) { - if (predictor.AcceptFeedback && predictor.Id == predictorId) + if (predictor.Id == predictorId) { - ThreadPool.QueueUserWorkItem( - state => state.OnSuggestionAccepted(client, session, suggestionText), - predictor, - preferLocal: false); + if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.SuggestionAccepted)) + { + Action callBack = GetCallBack(client, session, suggestionText); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); + } + + break; } } + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered, or no registered predictor accepts this feedback. + static Action GetCallBack(PredictionClient client, uint session, string suggestionText) + { + return predictor => predictor.OnSuggestionAccepted(client, session, suggestionText); + } } } } diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs index d55d37a90df..bd8a48a85db 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs @@ -9,7 +9,7 @@ using System.Management.Automation.Language; using System.Threading; -namespace System.Management.Automation.Subsystem +namespace System.Management.Automation.Subsystem.Prediction { /// /// Interface for implementing a predictor plugin. @@ -26,57 +26,132 @@ public interface ICommandPredictor : ISubsystem /// SubsystemKind ISubsystem.Kind => SubsystemKind.CommandPredictor; - /// - /// Gets a value indicating whether the predictor supports early processing. - /// - bool SupportEarlyProcessing { get; } - - /// - /// Gets a value indicating whether the predictor accepts feedback about the previous suggestion. - /// - bool AcceptFeedback { get; } - - /// - /// A command line was accepted to execute. - /// The predictor can start processing early as needed with the latest history. - /// - /// Represents the client that initiates the call. - /// History command lines provided as references for prediction. - void StartEarlyProcessing(string clientId, IReadOnlyList history); - /// /// Get the predictive suggestions. It indicates the start of a suggestion rendering session. /// - /// Represents the client that initiates the call. + /// Represents the client that initiates the call. /// The object to be used for prediction. /// The cancellation token to cancel the prediction. /// An instance of . - SuggestionPackage GetSuggestion(string clientId, PredictionContext context, CancellationToken cancellationToken); + SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken); + + /// + /// Gets a value indicating whether the predictor accepts a specific kind of feedback. + /// + /// Represents the client that initiates the call. + /// A specific type of feedback. + /// True or false, to indicate whether the specific feedback is accepted. + bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback); /// /// One or more suggestions provided by the predictor were displayed to the user. /// - /// Represents the client that initiates the call. + /// Represents the client that initiates the call. /// The mini-session where the displayed suggestions came from. /// /// When the value is greater than 0, it's the number of displayed suggestions from the list returned in , starting from the index 0. /// When the value is less than or equal to 0, it means a single suggestion from the list got displayed, and the index is the absolute value. /// - void OnSuggestionDisplayed(string clientId, uint session, int countOrIndex); + void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex); /// /// The suggestion provided by the predictor was accepted. /// - /// Represents the client that initiates the call. + /// Represents the client that initiates the call. /// Represents the mini-session where the accepted suggestion came from. /// The accepted suggestion text. - void OnSuggestionAccepted(string clientId, uint session, string acceptedSuggestion); + void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion); + + /// + /// A command line was accepted to execute. + /// The predictor can start processing early as needed with the latest history. + /// + /// Represents the client that initiates the call. + /// History command lines provided as references for prediction. + void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history); + + /// + /// A command line was done execution. + /// + /// Represents the client that initiates the call. + /// The last accepted command line. + /// Shows whether the execution was successful. + void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success); + } + + /// + /// Kinds of feedback a predictor can choose to accept. + /// + public enum PredictorFeedbackKind + { + /// + /// Feedback when one or more suggestions are displayed to the user. + /// + SuggestionDisplayed, + + /// + /// Feedback when a suggestion is accepted by the user. + /// + SuggestionAccepted, + + /// + /// Feedback when a command line is accepted by the user. + /// + CommandLineAccepted, + + /// + /// Feedback when the accepted command line finishes its execution. + /// + CommandLineExecuted, + } + + /// + /// Kinds of prediction clients. + /// + public enum PredictionClientKind + { + /// + /// A terminal client, representing the command-line experience. + /// + Terminal, + + /// + /// An editor client, representing the editor experience. + /// + Editor, + } + + /// + /// The class represents a client that interacts with predictors. + /// + public sealed class PredictionClient + { + /// + /// Gets the client name. + /// + public string Name { get; } + + /// + /// Gets the client kind. + /// + public PredictionClientKind Kind { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the interactive client. + /// Kind of the interactive client. + public PredictionClient(string name, PredictionClientKind kind) + { + Name = name; + Kind = kind; + } } /// /// Context information about the user input. /// - public class PredictionContext + public sealed class PredictionContext { /// /// Gets the abstract syntax tree (AST) generated from parsing the user input. diff --git a/src/System.Management.Automation/engine/Subsystem/DscSubsystem/ICrossPlatformDsc.cs b/src/System.Management.Automation/engine/Subsystem/DscSubsystem/ICrossPlatformDsc.cs new file mode 100644 index 00000000000..f16c6f245a3 --- /dev/null +++ b/src/System.Management.Automation/engine/Subsystem/DscSubsystem/ICrossPlatformDsc.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Management.Automation.Language; + +namespace System.Management.Automation.Subsystem.DSC +{ + /// + /// Interface for implementing a cross platform desired state configuration component. + /// + public interface ICrossPlatformDsc : ISubsystem + { + /// + /// Subsystem kind. + /// + SubsystemKind ISubsystem.Kind => SubsystemKind.CrossPlatformDsc; + + /// + /// Default implementation. No function is required for this subsystem. + /// + Dictionary? ISubsystem.FunctionsToDefine => null; + + /// + /// DSC initializer function. + /// + void LoadDefaultKeywords(Collection errors); + + /// + /// Clear internal class caches. + /// + void ClearCache(); + + /// + /// Returns resource usage string. + /// + string GetDSCResourceUsageString(DynamicKeyword keyword); + + /// + /// Checks if a string is one of dynamic keywords that can be used in both configuration and meta configuration. + /// + bool IsSystemResourceName(string name); + + /// + /// Checks if a string matches default module name used for meta configuration resources. + /// + bool IsDefaultModuleNameForMetaConfigResource(string name); + } +} diff --git a/src/System.Management.Automation/engine/Subsystem/ISubsystem.cs b/src/System.Management.Automation/engine/Subsystem/ISubsystem.cs index e19f37f06c7..db842f435bb 100644 --- a/src/System.Management.Automation/engine/Subsystem/ISubsystem.cs +++ b/src/System.Management.Automation/engine/Subsystem/ISubsystem.cs @@ -17,6 +17,11 @@ public enum SubsystemKind /// Component that provides predictive suggestions to commandline input. /// CommandPredictor = 1, + + /// + /// Cross platform desired state configuration component. + /// + CrossPlatformDsc = 2, } /// diff --git a/src/System.Management.Automation/engine/Subsystem/SubsystemManager.cs b/src/System.Management.Automation/engine/Subsystem/SubsystemManager.cs index 81a27f61279..8764cf83cf5 100644 --- a/src/System.Management.Automation/engine/Subsystem/SubsystemManager.cs +++ b/src/System.Management.Automation/engine/Subsystem/SubsystemManager.cs @@ -8,6 +8,8 @@ using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation.Internal; +using System.Management.Automation.Subsystem.DSC; +using System.Management.Automation.Subsystem.Prediction; namespace System.Management.Automation.Subsystem { @@ -28,6 +30,11 @@ static SubsystemManager() SubsystemKind.CommandPredictor, allowUnregistration: true, allowMultipleRegistration: true), + + SubsystemInfo.Create( + SubsystemKind.CrossPlatformDsc, + allowUnregistration: true, + allowMultipleRegistration: false), }; var subSystemTypeMap = new Dictionary(subsystems.Length); diff --git a/src/System.Management.Automation/engine/WinRT/IInspectable.cs b/src/System.Management.Automation/engine/WinRT/IInspectable.cs index abc589b4875..28544d42af4 100644 --- a/src/System.Management.Automation/engine/WinRT/IInspectable.cs +++ b/src/System.Management.Automation/engine/WinRT/IInspectable.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Runtime.InteropServices; +#nullable enable namespace System.Management.Automation { /// diff --git a/src/System.Management.Automation/engine/cmdlet.cs b/src/System.Management.Automation/engine/cmdlet.cs index b049193fd87..ba7548f982d 100644 --- a/src/System.Management.Automation/engine/cmdlet.cs +++ b/src/System.Management.Automation/engine/cmdlet.cs @@ -1714,6 +1714,7 @@ public PSTransactionContext CurrentPSTransaction /// . /// etc. /// + [System.Diagnostics.CodeAnalysis.DoesNotReturn] public void ThrowTerminatingError(ErrorRecord errorRecord) { using (PSTransactionManager.GetEngineProtectionScope()) diff --git a/src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs b/src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs index ab430decb50..50c9064661f 100644 --- a/src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs +++ b/src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs @@ -192,7 +192,7 @@ protected override void StartPipelineExecution() } #if !UNIX - if (apartmentState != ApartmentState.Unknown && !Platform.IsNanoServer && !Platform.IsIoT) + if (apartmentState != ApartmentState.Unknown && Platform.IsStaSupported) { invokeThread.SetApartmentState(apartmentState); } @@ -1165,7 +1165,7 @@ internal PipelineThread(ApartmentState apartmentState) _closed = false; #if !UNIX - if (apartmentState != ApartmentState.Unknown && !Platform.IsNanoServer && !Platform.IsIoT) + if (apartmentState != ApartmentState.Unknown && Platform.IsStaSupported) { _worker.SetApartmentState(apartmentState); } diff --git a/src/System.Management.Automation/engine/interpreter/ILightCallSiteBinder.cs b/src/System.Management.Automation/engine/interpreter/ILightCallSiteBinder.cs index 49fbfdb0f8e..9f24d14fa14 100644 --- a/src/System.Management.Automation/engine/interpreter/ILightCallSiteBinder.cs +++ b/src/System.Management.Automation/engine/interpreter/ILightCallSiteBinder.cs @@ -13,6 +13,7 @@ * * ***************************************************************************/ +#nullable enable #if !CLR2 #else using Microsoft.Scripting.Ast; diff --git a/src/System.Management.Automation/engine/parser/AstVisitor.cs b/src/System.Management.Automation/engine/parser/AstVisitor.cs index 5c09ee78dc6..03b234cf41f 100644 --- a/src/System.Management.Automation/engine/parser/AstVisitor.cs +++ b/src/System.Management.Automation/engine/parser/AstVisitor.cs @@ -10,195 +10,197 @@ namespace System.Management.Automation.Language { /// /// +#nullable enable public interface ICustomAstVisitor { /// - object DefaultVisit(Ast ast) => null; + object? DefaultVisit(Ast ast) => null; /// - object VisitErrorStatement(ErrorStatementAst errorStatementAst) => DefaultVisit(errorStatementAst); + object? VisitErrorStatement(ErrorStatementAst errorStatementAst) => DefaultVisit(errorStatementAst); /// - object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => DefaultVisit(errorExpressionAst); + object? VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => DefaultVisit(errorExpressionAst); #region Script Blocks /// - object VisitScriptBlock(ScriptBlockAst scriptBlockAst) => DefaultVisit(scriptBlockAst); + object? VisitScriptBlock(ScriptBlockAst scriptBlockAst) => DefaultVisit(scriptBlockAst); /// [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Param")] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "param")] - object VisitParamBlock(ParamBlockAst paramBlockAst) => DefaultVisit(paramBlockAst); + object? VisitParamBlock(ParamBlockAst paramBlockAst) => DefaultVisit(paramBlockAst); /// - object VisitNamedBlock(NamedBlockAst namedBlockAst) => DefaultVisit(namedBlockAst); + object? VisitNamedBlock(NamedBlockAst namedBlockAst) => DefaultVisit(namedBlockAst); /// - object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => DefaultVisit(typeConstraintAst); + object? VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => DefaultVisit(typeConstraintAst); /// - object VisitAttribute(AttributeAst attributeAst) => DefaultVisit(attributeAst); + object? VisitAttribute(AttributeAst attributeAst) => DefaultVisit(attributeAst); /// - object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => DefaultVisit(namedAttributeArgumentAst); + object? VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => DefaultVisit(namedAttributeArgumentAst); /// - object VisitParameter(ParameterAst parameterAst) => DefaultVisit(parameterAst); + object? VisitParameter(ParameterAst parameterAst) => DefaultVisit(parameterAst); #endregion Script Blocks #region Statements /// - object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) => DefaultVisit(functionDefinitionAst); + object? VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) => DefaultVisit(functionDefinitionAst); /// - object VisitStatementBlock(StatementBlockAst statementBlockAst) => DefaultVisit(statementBlockAst); + object? VisitStatementBlock(StatementBlockAst statementBlockAst) => DefaultVisit(statementBlockAst); /// - object VisitIfStatement(IfStatementAst ifStmtAst) => DefaultVisit(ifStmtAst); + object? VisitIfStatement(IfStatementAst ifStmtAst) => DefaultVisit(ifStmtAst); /// - object VisitTrap(TrapStatementAst trapStatementAst) => DefaultVisit(trapStatementAst); + object? VisitTrap(TrapStatementAst trapStatementAst) => DefaultVisit(trapStatementAst); /// - object VisitSwitchStatement(SwitchStatementAst switchStatementAst) => DefaultVisit(switchStatementAst); + object? VisitSwitchStatement(SwitchStatementAst switchStatementAst) => DefaultVisit(switchStatementAst); /// - object VisitDataStatement(DataStatementAst dataStatementAst) => DefaultVisit(dataStatementAst); + object? VisitDataStatement(DataStatementAst dataStatementAst) => DefaultVisit(dataStatementAst); /// - object VisitForEachStatement(ForEachStatementAst forEachStatementAst) => DefaultVisit(forEachStatementAst); + object? VisitForEachStatement(ForEachStatementAst forEachStatementAst) => DefaultVisit(forEachStatementAst); /// - object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => DefaultVisit(doWhileStatementAst); + object? VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => DefaultVisit(doWhileStatementAst); /// - object VisitForStatement(ForStatementAst forStatementAst) => DefaultVisit(forStatementAst); + object? VisitForStatement(ForStatementAst forStatementAst) => DefaultVisit(forStatementAst); /// - object VisitWhileStatement(WhileStatementAst whileStatementAst) => DefaultVisit(whileStatementAst); + object? VisitWhileStatement(WhileStatementAst whileStatementAst) => DefaultVisit(whileStatementAst); /// - object VisitCatchClause(CatchClauseAst catchClauseAst) => DefaultVisit(catchClauseAst); + object? VisitCatchClause(CatchClauseAst catchClauseAst) => DefaultVisit(catchClauseAst); /// - object VisitTryStatement(TryStatementAst tryStatementAst) => DefaultVisit(tryStatementAst); + object? VisitTryStatement(TryStatementAst tryStatementAst) => DefaultVisit(tryStatementAst); /// - object VisitBreakStatement(BreakStatementAst breakStatementAst) => DefaultVisit(breakStatementAst); + object? VisitBreakStatement(BreakStatementAst breakStatementAst) => DefaultVisit(breakStatementAst); /// - object VisitContinueStatement(ContinueStatementAst continueStatementAst) => DefaultVisit(continueStatementAst); + object? VisitContinueStatement(ContinueStatementAst continueStatementAst) => DefaultVisit(continueStatementAst); /// - object VisitReturnStatement(ReturnStatementAst returnStatementAst) => DefaultVisit(returnStatementAst); + object? VisitReturnStatement(ReturnStatementAst returnStatementAst) => DefaultVisit(returnStatementAst); /// - object VisitExitStatement(ExitStatementAst exitStatementAst) => DefaultVisit(exitStatementAst); + object? VisitExitStatement(ExitStatementAst exitStatementAst) => DefaultVisit(exitStatementAst); /// - object VisitThrowStatement(ThrowStatementAst throwStatementAst) => DefaultVisit(throwStatementAst); + object? VisitThrowStatement(ThrowStatementAst throwStatementAst) => DefaultVisit(throwStatementAst); /// - object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => DefaultVisit(doUntilStatementAst); + object? VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => DefaultVisit(doUntilStatementAst); /// - object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) => DefaultVisit(assignmentStatementAst); + object? VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) => DefaultVisit(assignmentStatementAst); #endregion Statements #region Pipelines /// - object VisitPipeline(PipelineAst pipelineAst) => DefaultVisit(pipelineAst); + object? VisitPipeline(PipelineAst pipelineAst) => DefaultVisit(pipelineAst); /// - object VisitCommand(CommandAst commandAst) => DefaultVisit(commandAst); + object? VisitCommand(CommandAst commandAst) => DefaultVisit(commandAst); /// - object VisitCommandExpression(CommandExpressionAst commandExpressionAst) => DefaultVisit(commandExpressionAst); + object? VisitCommandExpression(CommandExpressionAst commandExpressionAst) => DefaultVisit(commandExpressionAst); /// - object VisitCommandParameter(CommandParameterAst commandParameterAst) => DefaultVisit(commandParameterAst); + object? VisitCommandParameter(CommandParameterAst commandParameterAst) => DefaultVisit(commandParameterAst); /// - object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => DefaultVisit(fileRedirectionAst); + object? VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => DefaultVisit(fileRedirectionAst); /// - object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => DefaultVisit(mergingRedirectionAst); + object? VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => DefaultVisit(mergingRedirectionAst); #endregion Pipelines #region Expressions /// - object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => DefaultVisit(binaryExpressionAst); + object? VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => DefaultVisit(binaryExpressionAst); /// - object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => DefaultVisit(unaryExpressionAst); + object? VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => DefaultVisit(unaryExpressionAst); /// - object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => DefaultVisit(convertExpressionAst); + object? VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => DefaultVisit(convertExpressionAst); /// - object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => DefaultVisit(constantExpressionAst); + object? VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => DefaultVisit(constantExpressionAst); /// - object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => DefaultVisit(stringConstantExpressionAst); + object? VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => DefaultVisit(stringConstantExpressionAst); /// [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "SubExpression")] [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "subExpression")] - object VisitSubExpression(SubExpressionAst subExpressionAst) => DefaultVisit(subExpressionAst); + object? VisitSubExpression(SubExpressionAst subExpressionAst) => DefaultVisit(subExpressionAst); /// - object VisitUsingExpression(UsingExpressionAst usingExpressionAst) => DefaultVisit(usingExpressionAst); + object? VisitUsingExpression(UsingExpressionAst usingExpressionAst) => DefaultVisit(usingExpressionAst); /// - object VisitVariableExpression(VariableExpressionAst variableExpressionAst) => DefaultVisit(variableExpressionAst); + object? VisitVariableExpression(VariableExpressionAst variableExpressionAst) => DefaultVisit(variableExpressionAst); /// - object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => DefaultVisit(typeExpressionAst); + object? VisitTypeExpression(TypeExpressionAst typeExpressionAst) => DefaultVisit(typeExpressionAst); /// - object VisitMemberExpression(MemberExpressionAst memberExpressionAst) => DefaultVisit(memberExpressionAst); + object? VisitMemberExpression(MemberExpressionAst memberExpressionAst) => DefaultVisit(memberExpressionAst); /// - object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => DefaultVisit(invokeMemberExpressionAst); + object? VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => DefaultVisit(invokeMemberExpressionAst); /// - object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => DefaultVisit(arrayExpressionAst); + object? VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => DefaultVisit(arrayExpressionAst); /// - object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => DefaultVisit(arrayLiteralAst); + object? VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => DefaultVisit(arrayLiteralAst); /// - object VisitHashtable(HashtableAst hashtableAst) => DefaultVisit(hashtableAst); + object? VisitHashtable(HashtableAst hashtableAst) => DefaultVisit(hashtableAst); /// - object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) => DefaultVisit(scriptBlockExpressionAst); + object? VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) => DefaultVisit(scriptBlockExpressionAst); /// [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Paren")] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "paren")] - object VisitParenExpression(ParenExpressionAst parenExpressionAst) => DefaultVisit(parenExpressionAst); + object? VisitParenExpression(ParenExpressionAst parenExpressionAst) => DefaultVisit(parenExpressionAst); /// - object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) => DefaultVisit(expandableStringExpressionAst); + object? VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) => DefaultVisit(expandableStringExpressionAst); /// - object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => DefaultVisit(indexExpressionAst); + object? VisitIndexExpression(IndexExpressionAst indexExpressionAst) => DefaultVisit(indexExpressionAst); /// - object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => DefaultVisit(attributedExpressionAst); + object? VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => DefaultVisit(attributedExpressionAst); /// - object VisitBlockStatement(BlockStatementAst blockStatementAst) => DefaultVisit(blockStatementAst); + object? VisitBlockStatement(BlockStatementAst blockStatementAst) => DefaultVisit(blockStatementAst); #endregion Expressions } +#nullable restore /// #nullable enable diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 547abd10374..d5ee729577e 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -12,6 +12,8 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.DSC; using Dsc = Microsoft.PowerShell.DesiredStateConfiguration.Internal; namespace System.Management.Automation.Language @@ -2933,7 +2935,6 @@ private StatementAst ConfigurationStatementRule(IEnumerable custom // Runspaces.Runspace localRunspace = null; bool topLevel = false; - bool useCrossPlatformSchema = false; try { // At this point, we'll need a runspace to use to hold the metadata for the parse. If there is no @@ -2996,38 +2997,13 @@ private StatementAst ConfigurationStatementRule(IEnumerable custom { // Load the default CIM keywords Collection CIMKeywordErrors = new Collection(); - if (ExperimentalFeature.IsEnabled(Dsc.CrossPlatform.DscClassCache.DscExperimentalFeatureName)) - { - // In addition to checking if experimental feature is enabled - // also check if PSDesiredStateConfiguration is already loaded - // if pre-v3 is already loaded then use old mof-based APIs - // otherwise use json-based APIs - - p.AddCommand(new CmdletInfo("Get-Module", typeof(Microsoft.PowerShell.Commands.GetModuleCommand))); - p.AddParameter("Name", "PSDesiredStateConfiguration"); - - bool prev3IsLoaded = false; - foreach (PSModuleInfo moduleInfo in p.Invoke()) - { - if (moduleInfo.Version.Major < 3) - { - prev3IsLoaded = true; - break; - } - } - - p.Commands.Clear(); - useCrossPlatformSchema = !prev3IsLoaded; - - if (useCrossPlatformSchema) - { - Dsc.CrossPlatform.DscClassCache.LoadDefaultCimKeywords(CIMKeywordErrors); - } - else - { - Dsc.DscClassCache.LoadDefaultCimKeywords(CIMKeywordErrors); - } + // DscSubsystem is auto-registered when PSDesiredStateConfiguration v3 module is loaded + // so if DscSubsystem is registered that means user intention to use v3 APIs. + ICrossPlatformDsc dscSubsystem = SubsystemManager.GetSubsystem(); + if (dscSubsystem != null) + { + dscSubsystem.LoadDefaultKeywords(CIMKeywordErrors); } else { @@ -3274,9 +3250,10 @@ private StatementAst ConfigurationStatementRule(IEnumerable custom // Clear out all of the cached classes and keywords. // They will need to be reloaded when the generated function is actually run. // - if (useCrossPlatformSchema) + ICrossPlatformDsc dscSubsystem = SubsystemManager.GetSubsystem(); + if (dscSubsystem != null) { - Dsc.CrossPlatform.DscClassCache.ClearCache(); + dscSubsystem.ClearCache(); } else { diff --git a/src/System.Management.Automation/engine/parser/Position.cs b/src/System.Management.Automation/engine/parser/Position.cs index 54f4c3eccbe..3c4281e5446 100644 --- a/src/System.Management.Automation/engine/parser/Position.cs +++ b/src/System.Management.Automation/engine/parser/Position.cs @@ -15,12 +15,13 @@ namespace System.Management.Automation.Language /// /// Represents a single point in a script. The script may come from a file or interactive input. /// +#nullable enable public interface IScriptPosition { /// /// The name of the file, or if the script did not come from a file, then null. /// - string File { get; } + string? File { get; } /// /// The line number of the position, with the value 1 being the first line. @@ -45,8 +46,9 @@ public interface IScriptPosition /// /// The complete script that this position is included in. /// - string GetFullScript(); + string? GetFullScript(); } +#nullable restore /// /// Represents the a span of text in a script. diff --git a/src/System.Management.Automation/engine/parser/SemanticChecks.cs b/src/System.Management.Automation/engine/parser/SemanticChecks.cs index b98296815d8..f6f752b3af4 100644 --- a/src/System.Management.Automation/engine/parser/SemanticChecks.cs +++ b/src/System.Management.Automation/engine/parser/SemanticChecks.cs @@ -11,6 +11,8 @@ using System.Text; using Microsoft.PowerShell; +using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.DSC; using Microsoft.PowerShell.DesiredStateConfiguration.Internal; namespace System.Management.Automation.Language @@ -1405,7 +1407,10 @@ public override AstVisitAction VisitDynamicKeywordStatement(DynamicKeywordStatem { StringConstantExpressionAst nameAst = dynamicKeywordStatementAst.CommandElements[0] as StringConstantExpressionAst; Diagnostics.Assert(nameAst != null, "nameAst should never be null"); - if (!DscClassCache.SystemResourceNames.Contains(nameAst.Extent.Text.Trim())) + var extentText = nameAst.Extent.Text.Trim(); + ICrossPlatformDsc dscSubsystem = SubsystemManager.GetSubsystem(); + var extentTextIsASystemResourceName = (dscSubsystem != null) ? dscSubsystem.IsSystemResourceName(extentText) : DscClassCache.SystemResourceNames.Contains(extentText); + if (!extentTextIsASystemResourceName) { if (configAst.ConfigurationType == ConfigurationType.Meta && !dynamicKeywordStatementAst.Keyword.IsMetaDSCResource()) { diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index 79d8fcc537e..3e1f9ce60a5 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -36,8 +36,6 @@ internal interface ISupportsAssignment IAssignableValue GetAssignableValue(); } -#nullable restore - internal interface IAssignableValue { /// @@ -45,7 +43,7 @@ internal interface IAssignableValue /// It returns the expressions that holds the value of the ast. It may append the exprs or temps lists if the return /// value relies on temps and other expressions. /// - Expression GetValue(Compiler compiler, List exprs, List temps); + Expression? GetValue(Compiler compiler, List exprs, List temps); /// /// SetValue is called to set the result of an assignment (=) or to write back the result of @@ -53,6 +51,7 @@ internal interface IAssignableValue /// Expression SetValue(Compiler compiler, Expression rhs); } +#nullable restore internal interface IParameterMetadataProvider { @@ -8139,6 +8138,7 @@ internal override object Accept(ICustomAstVisitor visitor) /// /// The name and attributes of a type. /// +#nullable enable public interface ITypeName { /// @@ -8154,7 +8154,7 @@ public interface ITypeName /// /// The name of the assembly, if specified, otherwise null. /// - string AssemblyName { get; } + string? AssemblyName { get; } /// /// Returns true if the type names an array, false otherwise. @@ -8169,20 +8169,21 @@ public interface ITypeName /// /// Returns the that this typename represents, if such a type exists, null otherwise. /// - Type GetReflectionType(); + Type? GetReflectionType(); /// /// Assuming the typename is an attribute, returns the that this typename represents. /// By convention, the typename may omit the suffix "Attribute". Lookup will attempt to resolve the type as is, /// and if that fails, the suffix "Attribute" will be appended. /// - Type GetReflectionAttributeType(); + Type? GetReflectionAttributeType(); /// /// The extent of the typename. /// IScriptExtent Extent { get; } } +#nullable restore #nullable enable internal interface ISupportsTypeCaching diff --git a/src/System.Management.Automation/engine/parser/tokenizer.cs b/src/System.Management.Automation/engine/parser/tokenizer.cs index 7f36000df13..161e53cef0c 100644 --- a/src/System.Management.Automation/engine/parser/tokenizer.cs +++ b/src/System.Management.Automation/engine/parser/tokenizer.cs @@ -12,6 +12,8 @@ using System.Text; using Microsoft.PowerShell.Commands; +using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.DSC; using Microsoft.PowerShell.DesiredStateConfiguration.Internal; namespace System.Management.Automation.Language @@ -361,7 +363,15 @@ internal static bool IsMetaDSCResource(this DynamicKeyword keyword) string implementingModule = keyword.ImplementingModule; if (implementingModule != null) { - return implementingModule.Equals(DscClassCache.DefaultModuleInfoForMetaConfigResource.Item1, StringComparison.OrdinalIgnoreCase); + ICrossPlatformDsc dscSubsystem = SubsystemManager.GetSubsystem(); + if (dscSubsystem != null) + { + dscSubsystem.IsDefaultModuleNameForMetaConfigResource(implementingModule); + } + else + { + return implementingModule.Equals(DscClassCache.DefaultModuleInfoForMetaConfigResource.Item1, StringComparison.OrdinalIgnoreCase); + } } return false; diff --git a/src/System.Management.Automation/engine/pipeline.cs b/src/System.Management.Automation/engine/pipeline.cs index 14837fd1c5c..58ea07de6ae 100644 --- a/src/System.Management.Automation/engine/pipeline.cs +++ b/src/System.Management.Automation/engine/pipeline.cs @@ -220,44 +220,33 @@ private void Log(string logElement, InvocationInfo invocation, PipelineExecution } } - if (!string.IsNullOrEmpty(logElement)) + if (_needToLog && !string.IsNullOrEmpty(logElement)) { + _eventLogBuffer ??= new List(); _eventLogBuffer.Add(logElement); } } - internal void LogToEventLog() + private void LogToEventLog() { - if (NeedToLog()) + // We check to see if there is anything in the buffer before we flush it. + // Flushing the empty buffer causes a measurable performance degradation. + if (_commands?.Count > 0 && _eventLogBuffer?.Count > 0) { - // We check to see if the command is needs writing (or if there is anything in the buffer) - // before we flush it. Flushing the empty buffer causes a measurable performance degradation. - if (_commands == null || _commands.Count == 0 || _eventLogBuffer.Count == 0) - return; - - MshLog.LogPipelineExecutionDetailEvent(_commands[0].Command.Context, - _eventLogBuffer, - _commands[0].Command.MyInvocation); - } - } - - private bool NeedToLog() - { - if (_commands == null) - return false; - - foreach (CommandProcessorBase commandProcessor in _commands) - { - MshCommandRuntime cmdRuntime = commandProcessor.Command.commandRuntime as MshCommandRuntime; - - if (cmdRuntime != null && cmdRuntime.LogPipelineExecutionDetail) - return true; + InternalCommand firstCmd = _commands[0].Command; + MshLog.LogPipelineExecutionDetailEvent( + firstCmd.Context, + _eventLogBuffer, + firstCmd.MyInvocation); } - return false; + // Clear the log buffer after writing the event. + _eventLogBuffer?.Clear(); } - private List _eventLogBuffer = new List(); + private bool _needToLog = false; + private List _eventLogBuffer; + #endregion #region public_methods @@ -273,7 +262,7 @@ private bool NeedToLog() internal int Add(CommandProcessorBase commandProcessor) { commandProcessor.CommandRuntime.PipelineProcessor = this; - return AddCommand(commandProcessor, _commands.Count, false); + return AddCommand(commandProcessor, _commands.Count, readErrorQueue: false); } internal void AddRedirectionPipe(PipelineProcessor pipelineProcessor) @@ -306,7 +295,7 @@ internal void AddRedirectionPipe(PipelineProcessor pipelineProcessor) /// PipeAlreadyTaken: the downstream pipe of command /// is already taken /// - internal int AddCommand(CommandProcessorBase commandProcessor, int readFromCommand, bool readErrorQueue) + private int AddCommand(CommandProcessorBase commandProcessor, int readFromCommand, bool readErrorQueue) { if (commandProcessor == null) { @@ -412,6 +401,9 @@ internal int AddCommand(CommandProcessorBase commandProcessor, int readFromComma _commands.Add(commandProcessor); + // We will log event(s) about the pipeline execution details if any command in the pipeline requests that. + _needToLog |= commandProcessor.CommandRuntime.LogPipelineExecutionDetail; + // We give the Command a pointer back to the // PipelineProcessor so that it can check whether the // command has been stopped. diff --git a/src/System.Management.Automation/engine/remoting/commands/ConnectPSSession.cs b/src/System.Management.Automation/engine/remoting/commands/ConnectPSSession.cs index f58aa35632e..906074a8cdd 100644 --- a/src/System.Management.Automation/engine/remoting/commands/ConnectPSSession.cs +++ b/src/System.Management.Automation/engine/remoting/commands/ConnectPSSession.cs @@ -92,7 +92,10 @@ public class ConnectPSSessionCommand : PSRunspaceCmdlet, IDisposable ParameterSetName = ConnectPSSessionCommand.ComputerNameGuidParameterSet)] public string ApplicationName { - get { return _appName; } + get + { + return _appName; + } set { @@ -117,7 +120,10 @@ public string ApplicationName ParameterSetName = ConnectPSSessionCommand.ConnectionUriGuidParameterSet)] public string ConfigurationName { - get { return _shell; } + get + { + return _shell; + } set { @@ -202,7 +208,10 @@ public override string[] Name [Credential()] public PSCredential Credential { - get { return _psCredential; } + get + { + return _psCredential; + } set { @@ -223,7 +232,10 @@ public PSCredential Credential [Parameter(ParameterSetName = ConnectPSSessionCommand.ConnectionUriGuidParameterSet)] public AuthenticationMechanism Authentication { - get { return _authentication; } + get + { + return _authentication; + } set { @@ -245,7 +257,10 @@ public AuthenticationMechanism Authentication [Parameter(ParameterSetName = ConnectPSSessionCommand.ConnectionUriGuidParameterSet)] public string CertificateThumbprint { - get { return _thumbprint; } + get + { + return _thumbprint; + } set { diff --git a/src/System.Management.Automation/engine/remoting/commands/CustomShellCommands.cs b/src/System.Management.Automation/engine/remoting/commands/CustomShellCommands.cs index a97724447a4..19c093e3687 100644 --- a/src/System.Management.Automation/engine/remoting/commands/CustomShellCommands.cs +++ b/src/System.Management.Automation/engine/remoting/commands/CustomShellCommands.cs @@ -1962,7 +1962,10 @@ public string Name [Parameter(Position = 1, Mandatory = true, ParameterSetName = PSSessionConfigurationCommandBase.AssemblyNameParameterSetName)] public string AssemblyName { - get { return assemblyName; } + get + { + return assemblyName; + } set { @@ -1984,7 +1987,10 @@ public string AssemblyName [Parameter(ParameterSetName = AssemblyNameParameterSetName)] public string ApplicationBase { - get { return applicationBase; } + get + { + return applicationBase; + } set { @@ -2004,7 +2010,10 @@ public string ApplicationBase [Parameter(Position = 2, Mandatory = true, ParameterSetName = PSSessionConfigurationCommandBase.AssemblyNameParameterSetName)] public string ConfigurationTypeName { - get { return configurationTypeName; } + get + { + return configurationTypeName; + } set { @@ -2053,7 +2062,10 @@ public ApartmentState ThreadApartmentState return ApartmentState.Unknown; } - set { threadAptState = value; } + set + { + threadAptState = value; + } } internal ApartmentState? threadAptState; @@ -2074,7 +2086,10 @@ public PSThreadOptions ThreadOptions return PSThreadOptions.UseCurrentThread; } - set { threadOptions = value; } + set + { + threadOptions = value; + } } internal PSThreadOptions? threadOptions; @@ -2085,7 +2100,10 @@ public PSThreadOptions ThreadOptions [Parameter] public PSSessionConfigurationAccessMode AccessMode { - get { return _accessMode; } + get + { + return _accessMode; + } set { @@ -2124,7 +2142,10 @@ public SwitchParameter UseSharedProcess [Parameter()] public string StartupScript { - get { return configurationScript; } + get + { + return configurationScript; + } set { @@ -2144,7 +2165,10 @@ public string StartupScript [AllowNull] public double? MaximumReceivedDataSizePerCommandMB { - get { return maxCommandSizeMB; } + get + { + return maxCommandSizeMB; + } set { @@ -2171,7 +2195,10 @@ public double? MaximumReceivedDataSizePerCommandMB [AllowNull] public double? MaximumReceivedObjectSizeMB { - get { return maxObjectSizeMB; } + get + { + return maxObjectSizeMB; + } set { @@ -2198,7 +2225,10 @@ public double? MaximumReceivedObjectSizeMB [Parameter()] public string SecurityDescriptorSddl { - get { return sddl; } + get + { + return sddl; + } set { @@ -2228,7 +2258,10 @@ public string SecurityDescriptorSddl [Parameter()] public SwitchParameter ShowSecurityDescriptorUI { - get { return _showUI; } + get + { + return _showUI; + } set { @@ -2280,7 +2313,10 @@ public SwitchParameter NoServiceRestart [ValidateNotNullOrEmpty] public Version PSVersion { - get { return psVersion; } + get + { + return psVersion; + } set { diff --git a/src/System.Management.Automation/engine/remoting/commands/NewPSSessionOptionCommand.cs b/src/System.Management.Automation/engine/remoting/commands/NewPSSessionOptionCommand.cs index 2a6dbc30679..e43e1897e37 100644 --- a/src/System.Management.Automation/engine/remoting/commands/NewPSSessionOptionCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/NewPSSessionOptionCommand.cs @@ -140,7 +140,10 @@ public int OpenTimeout RunspaceConnectionInfo.DefaultOpenTimeout; } - set { _openTimeout = value; } + set + { + _openTimeout = value; + } } private int? _openTimeout; @@ -165,7 +168,10 @@ public int CancelTimeout BaseTransportManager.ClientCloseTimeoutMs; } - set { _cancelTimeout = value; } + set + { + _cancelTimeout = value; + } } private int? _cancelTimeout; @@ -187,7 +193,10 @@ public int IdleTimeout : RunspaceConnectionInfo.DefaultIdleTimeout; } - set { _idleTimeout = value; } + set + { + _idleTimeout = value; + } } private int? _idleTimeout; @@ -294,7 +303,10 @@ public int OperationTimeout BaseTransportManager.ClientDefaultOperationTimeoutMs); } - set { _operationtimeout = value; } + set + { + _operationtimeout = value; + } } private int? _operationtimeout; @@ -308,7 +320,10 @@ public int OperationTimeout [Parameter] public SwitchParameter NoEncryption { - get { return _noencryption; } + get + { + return _noencryption; + } set { @@ -327,7 +342,10 @@ public SwitchParameter NoEncryption [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "UTF")] public SwitchParameter UseUTF16 { - get { return _useutf16; } + get + { + return _useutf16; + } set { diff --git a/src/System.Management.Automation/engine/remoting/commands/ReceivePSSession.cs b/src/System.Management.Automation/engine/remoting/commands/ReceivePSSession.cs index 12cced4ae91..98d2afa3b5f 100644 --- a/src/System.Management.Automation/engine/remoting/commands/ReceivePSSession.cs +++ b/src/System.Management.Automation/engine/remoting/commands/ReceivePSSession.cs @@ -125,7 +125,10 @@ public class ReceivePSSessionCommand : PSRemotingCmdlet ParameterSetName = ReceivePSSessionCommand.ComputerInstanceIdParameterSet)] public string ApplicationName { - get { return _appName; } + get + { + return _appName; + } set { @@ -150,7 +153,10 @@ public string ApplicationName ParameterSetName = ReceivePSSessionCommand.ConnectionUriInstanceIdParameterSet)] public string ConfigurationName { - get { return _shell; } + get + { + return _shell; + } set { @@ -257,7 +263,10 @@ public SwitchParameter AllowRedirection [Credential()] public PSCredential Credential { - get { return _psCredential; } + get + { + return _psCredential; + } set { @@ -278,7 +287,10 @@ public PSCredential Credential [Parameter(ParameterSetName = ReceivePSSessionCommand.ConnectionUriInstanceIdParameterSet)] public AuthenticationMechanism Authentication { - get { return _authentication; } + get + { + return _authentication; + } set { @@ -300,7 +312,10 @@ public AuthenticationMechanism Authentication [Parameter(ParameterSetName = ReceivePSSessionCommand.ConnectionUriInstanceIdParameterSet)] public string CertificateThumbprint { - get { return _thumbprint; } + get + { + return _thumbprint; + } set { diff --git a/src/System.Management.Automation/engine/remoting/fanin/WSManPluginFacade.cs b/src/System.Management.Automation/engine/remoting/fanin/WSManPluginFacade.cs index e2679fcdf44..f01797fd760 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/WSManPluginFacade.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/WSManPluginFacade.cs @@ -33,7 +33,7 @@ namespace System.Management.Automation.Remoting /// PCWSTR. /// WSMAN_SHELL_STARTUP_INFO*. /// WSMAN_DATA*. - internal delegate void WSMPluginShellDelegate( // TODO: Rename to WSManPluginShellDelegate once I remove the MC++ module. + internal delegate void WSManPluginShellDelegate( IntPtr pluginContext, IntPtr requestDetails, int flags, @@ -45,7 +45,7 @@ internal delegate void WSMPluginShellDelegate( // TODO: Rename to WSManPluginShe /// /// PVOID. /// PVOID. - internal delegate void WSMPluginReleaseShellContextDelegate( + internal delegate void WSManPluginReleaseShellContextDelegate( IntPtr pluginContext, IntPtr shellContext); @@ -57,7 +57,7 @@ internal delegate void WSMPluginReleaseShellContextDelegate( /// PVOID. /// PVOID optional. /// WSMAN_DATA* optional. - internal delegate void WSMPluginConnectDelegate( + internal delegate void WSManPluginConnectDelegate( IntPtr pluginContext, IntPtr requestDetails, int flags, @@ -73,7 +73,7 @@ internal delegate void WSMPluginConnectDelegate( /// PVOID. /// PCWSTR. /// WSMAN_COMMAND_ARG_SET*. - internal delegate void WSMPluginCommandDelegate( + internal delegate void WSManPluginCommandDelegate( IntPtr pluginContext, IntPtr requestDetails, int flags, @@ -85,7 +85,7 @@ internal delegate void WSMPluginCommandDelegate( /// Delegate that is passed to native layer for callback on operation shutdown notifications. /// /// IntPtr. - internal delegate void WSMPluginOperationShutdownDelegate( + internal delegate void WSManPluginOperationShutdownDelegate( IntPtr shutdownContext); /// @@ -93,7 +93,7 @@ internal delegate void WSMPluginOperationShutdownDelegate( /// PVOID. /// PVOID. /// PVOID. - internal delegate void WSMPluginReleaseCommandContextDelegate( + internal delegate void WSManPluginReleaseCommandContextDelegate( IntPtr pluginContext, IntPtr shellContext, IntPtr commandContext); @@ -107,7 +107,7 @@ internal delegate void WSMPluginReleaseCommandContextDelegate( /// PVOID. /// PCWSTR. /// WSMAN_DATA*. - internal delegate void WSMPluginSendDelegate( + internal delegate void WSManPluginSendDelegate( IntPtr pluginContext, IntPtr requestDetails, int flags, @@ -124,7 +124,7 @@ internal delegate void WSMPluginSendDelegate( /// PVOID. /// PVOID optional. /// WSMAN_STREAM_ID_SET* optional. - internal delegate void WSMPluginReceiveDelegate( + internal delegate void WSManPluginReceiveDelegate( IntPtr pluginContext, IntPtr requestDetails, int flags, @@ -140,7 +140,7 @@ internal delegate void WSMPluginReceiveDelegate( /// PVOID. /// PVOID optional. /// PCWSTR. - internal delegate void WSMPluginSignalDelegate( + internal delegate void WSManPluginSignalDelegate( IntPtr pluginContext, IntPtr requestDetails, int flags, @@ -160,7 +160,7 @@ internal delegate void WaitOrTimerCallbackDelegate( /// /// /// PVOID. - internal delegate void WSMShutdownPluginDelegate( + internal delegate void WSManShutdownPluginDelegate( IntPtr pluginContext); /// @@ -192,7 +192,7 @@ internal WSManPluginEntryDelegatesInternal UnmanagedStruct private GCHandle _pluginSignalGCHandle; private GCHandle _pluginConnectGCHandle; private GCHandle _shutdownPluginGCHandle; - private GCHandle _WSMPluginOperationShutdownGCHandle; + private GCHandle _WSManPluginOperationShutdownGCHandle; #endregion @@ -255,57 +255,57 @@ private void populateDelegates() // disposal. Using GCHandle without pinning reduces fragmentation potential // of the managed heap. { - WSMPluginShellDelegate pluginShell = new WSMPluginShellDelegate(WSManPluginManagedEntryWrapper.WSManPluginShell); + WSManPluginShellDelegate pluginShell = new WSManPluginShellDelegate(WSManPluginManagedEntryWrapper.WSManPluginShell); _pluginShellGCHandle = GCHandle.Alloc(pluginShell); // marshal the delegate to a unmanaged function pointer so that AppDomain reference is stored correctly. // Populate the outgoing structure so the caller has access to the entry points _unmanagedStruct.wsManPluginShellCallbackNative = Marshal.GetFunctionPointerForDelegate(pluginShell); } { - WSMPluginReleaseShellContextDelegate pluginReleaseShellContext = new WSMPluginReleaseShellContextDelegate(WSManPluginManagedEntryWrapper.WSManPluginReleaseShellContext); + WSManPluginReleaseShellContextDelegate pluginReleaseShellContext = new WSManPluginReleaseShellContextDelegate(WSManPluginManagedEntryWrapper.WSManPluginReleaseShellContext); _pluginReleaseShellContextGCHandle = GCHandle.Alloc(pluginReleaseShellContext); _unmanagedStruct.wsManPluginReleaseShellContextCallbackNative = Marshal.GetFunctionPointerForDelegate(pluginReleaseShellContext); } { - WSMPluginCommandDelegate pluginCommand = new WSMPluginCommandDelegate(WSManPluginManagedEntryWrapper.WSManPluginCommand); + WSManPluginCommandDelegate pluginCommand = new WSManPluginCommandDelegate(WSManPluginManagedEntryWrapper.WSManPluginCommand); _pluginCommandGCHandle = GCHandle.Alloc(pluginCommand); _unmanagedStruct.wsManPluginCommandCallbackNative = Marshal.GetFunctionPointerForDelegate(pluginCommand); } { - WSMPluginReleaseCommandContextDelegate pluginReleaseCommandContext = new WSMPluginReleaseCommandContextDelegate(WSManPluginManagedEntryWrapper.WSManPluginReleaseCommandContext); + WSManPluginReleaseCommandContextDelegate pluginReleaseCommandContext = new WSManPluginReleaseCommandContextDelegate(WSManPluginManagedEntryWrapper.WSManPluginReleaseCommandContext); _pluginReleaseCommandContextGCHandle = GCHandle.Alloc(pluginReleaseCommandContext); _unmanagedStruct.wsManPluginReleaseCommandContextCallbackNative = Marshal.GetFunctionPointerForDelegate(pluginReleaseCommandContext); } { - WSMPluginSendDelegate pluginSend = new WSMPluginSendDelegate(WSManPluginManagedEntryWrapper.WSManPluginSend); + WSManPluginSendDelegate pluginSend = new WSManPluginSendDelegate(WSManPluginManagedEntryWrapper.WSManPluginSend); _pluginSendGCHandle = GCHandle.Alloc(pluginSend); _unmanagedStruct.wsManPluginSendCallbackNative = Marshal.GetFunctionPointerForDelegate(pluginSend); } { - WSMPluginReceiveDelegate pluginReceive = new WSMPluginReceiveDelegate(WSManPluginManagedEntryWrapper.WSManPluginReceive); + WSManPluginReceiveDelegate pluginReceive = new WSManPluginReceiveDelegate(WSManPluginManagedEntryWrapper.WSManPluginReceive); _pluginReceiveGCHandle = GCHandle.Alloc(pluginReceive); _unmanagedStruct.wsManPluginReceiveCallbackNative = Marshal.GetFunctionPointerForDelegate(pluginReceive); } { - WSMPluginSignalDelegate pluginSignal = new WSMPluginSignalDelegate(WSManPluginManagedEntryWrapper.WSManPluginSignal); + WSManPluginSignalDelegate pluginSignal = new WSManPluginSignalDelegate(WSManPluginManagedEntryWrapper.WSManPluginSignal); _pluginSignalGCHandle = GCHandle.Alloc(pluginSignal); _unmanagedStruct.wsManPluginSignalCallbackNative = Marshal.GetFunctionPointerForDelegate(pluginSignal); } { - WSMPluginConnectDelegate pluginConnect = new WSMPluginConnectDelegate(WSManPluginManagedEntryWrapper.WSManPluginConnect); + WSManPluginConnectDelegate pluginConnect = new WSManPluginConnectDelegate(WSManPluginManagedEntryWrapper.WSManPluginConnect); _pluginConnectGCHandle = GCHandle.Alloc(pluginConnect); _unmanagedStruct.wsManPluginConnectCallbackNative = Marshal.GetFunctionPointerForDelegate(pluginConnect); } { - WSMShutdownPluginDelegate shutdownPlugin = new WSMShutdownPluginDelegate(WSManPluginManagedEntryWrapper.ShutdownPlugin); + WSManShutdownPluginDelegate shutdownPlugin = new WSManShutdownPluginDelegate(WSManPluginManagedEntryWrapper.ShutdownPlugin); _shutdownPluginGCHandle = GCHandle.Alloc(shutdownPlugin); _unmanagedStruct.wsManPluginShutdownPluginCallbackNative = Marshal.GetFunctionPointerForDelegate(shutdownPlugin); } if (!Platform.IsWindows) { - WSMPluginOperationShutdownDelegate pluginShutDownDelegate = new WSMPluginOperationShutdownDelegate(WSManPluginManagedEntryWrapper.WSManPSShutdown); - _WSMPluginOperationShutdownGCHandle = GCHandle.Alloc(pluginShutDownDelegate); + WSManPluginOperationShutdownDelegate pluginShutDownDelegate = new WSManPluginOperationShutdownDelegate(WSManPluginManagedEntryWrapper.WSManPSShutdown); + _WSManPluginOperationShutdownGCHandle = GCHandle.Alloc(pluginShutDownDelegate); _unmanagedStruct.wsManPluginShutdownCallbackNative = Marshal.GetFunctionPointerForDelegate(pluginShutDownDelegate); } } @@ -328,7 +328,7 @@ private void CleanUpDelegates() _shutdownPluginGCHandle.Free(); if (!Platform.IsWindows) { - _WSMPluginOperationShutdownGCHandle.Free(); + _WSManPluginOperationShutdownGCHandle.Free(); } } } diff --git a/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs b/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs index 1c2728cc6d6..06c992b842b 100644 --- a/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs +++ b/src/System.Management.Automation/engine/remoting/server/OutOfProcServerMediator.cs @@ -525,27 +525,16 @@ internal sealed class SSHProcessMediator : OutOfProcessMediatorBase private SSHProcessMediator() : base(true) { -#if !UNIX - var inputHandle = PlatformInvokes.GetStdHandle((uint)PlatformInvokes.StandardHandleId.Input); - originalStdIn = new StreamReader( - new FileStream(new SafeFileHandle(inputHandle, false), FileAccess.Read)); - - var outputHandle = PlatformInvokes.GetStdHandle((uint)PlatformInvokes.StandardHandleId.Output); - originalStdOut = new OutOfProcessTextWriter( - new StreamWriter( - new FileStream(new SafeFileHandle(outputHandle, false), FileAccess.Write))); - - var errorHandle = PlatformInvokes.GetStdHandle((uint)PlatformInvokes.StandardHandleId.Error); - originalStdErr = new OutOfProcessTextWriter( - new StreamWriter( - new FileStream(new SafeFileHandle(errorHandle, false), FileAccess.Write))); -#else originalStdIn = new StreamReader(Console.OpenStandardInput(), true); originalStdOut = new OutOfProcessTextWriter( new StreamWriter(Console.OpenStandardOutput())); originalStdErr = new OutOfProcessTextWriter( new StreamWriter(Console.OpenStandardError())); -#endif + + // Disable console from writing to the PSRP streams. + Console.SetIn(TextReader.Null); + Console.SetOut(TextWriter.Null); + Console.SetError(TextWriter.Null); } #endregion diff --git a/src/System.Management.Automation/help/HelpFileHelpProvider.cs b/src/System.Management.Automation/help/HelpFileHelpProvider.cs index 5305403fa74..4591ddbf863 100644 --- a/src/System.Management.Automation/help/HelpFileHelpProvider.cs +++ b/src/System.Management.Automation/help/HelpFileHelpProvider.cs @@ -368,7 +368,7 @@ internal Collection GetExtendedSearchPaths() { // Get all the directories under the module path // * and SearchOption.AllDirectories gets all the version directories. - string[] directories = Directory.GetDirectories(psModulePath, "*", SearchOption.AllDirectories); + IEnumerable directories = Directory.EnumerateDirectories(psModulePath, "*", SearchOption.AllDirectories); var possibleModuleDirectories = directories.Where(static directory => !ModuleUtils.IsPossibleResourceDirectory(directory)); diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index b16b63fe7e9..b37b4d10c84 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -470,10 +470,11 @@ protected override ProviderInfo Start(ProviderInfo providerInfo) // The placeholder mode management APIs Rtl(Set|Query)(Process|Thread)PlaceholderCompatibilityMode // are only supported starting with Windows 10 version 1803 (build 17134) Version minBuildForPlaceHolderAPIs = new Version(10, 0, 17134, 0); + if (Environment.OSVersion.Version >= minBuildForPlaceHolderAPIs) { // let's be safe, don't change the PlaceHolderCompatibilityMode if the current one is not what we expect - if (NativeMethods.PHCM_DISGUISE_PLACEHOLDER == NativeMethods.RtlQueryProcessPlaceholderCompatibilityMode()) + if (NativeMethods.RtlQueryProcessPlaceholderCompatibilityMode() == NativeMethods.PHCM_DISGUISE_PLACEHOLDER) { NativeMethods.RtlSetProcessPlaceholderCompatibilityMode(NativeMethods.PHCM_EXPOSE_PLACEHOLDERS); } @@ -1895,11 +1896,8 @@ private void Dir( { hidden = (recursiveDirectory.Attributes & FileAttributes.Hidden) != 0; - // Performance optimization. - // Since we have already checked Attributes for Hidden we have already did a p/invoke - // and initialized Attributes property. - // So here we can check for ReparsePoint without new p/invoke. - // If it is not a reparse point we skip one p/invoke in IsReparsePointLikeSymlink() below. + // We've already taken the expense of initializing the Attributes property here, + // so we can use that to avoid needing to call IsReparsePointLikeSymlink() later. checkReparsePoint = (recursiveDirectory.Attributes & FileAttributes.ReparsePoint) != 0; } @@ -7534,7 +7532,7 @@ internal sealed class GetChildDynamicParameters /// Gets or sets the filter directory flag. /// [Parameter] - [Alias("ad", "d")] + [Alias("ad")] public SwitchParameter Directory { get { return _attributeDirectory; } @@ -8041,7 +8039,7 @@ protected override bool ReleaseHandle() } // SetLastError is false as the use of this API doesn't not require GetLastError() to be called - [DllImport(PinvokeDllNames.FindFirstFileDllName, EntryPoint = "FindFirstFileExW", SetLastError = false, CharSet = CharSet.Unicode)] + [DllImport(PinvokeDllNames.FindFirstFileDllName, EntryPoint = "FindFirstFileExW", SetLastError = true, CharSet = CharSet.Unicode)] private static extern SafeFindHandle FindFirstFileEx(string lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, ref WIN32_FIND_DATA lpFindFileData, FINDEX_SEARCH_OPS fSearchOp, IntPtr lpSearchFilter, int dwAdditionalFlags); internal enum FINDEX_INFO_LEVELS : uint @@ -8244,16 +8242,19 @@ internal static bool IsReparsePointLikeSymlink(FileSystemInfo fileInfo) } WIN32_FIND_DATA data = default; - using (var handle = FindFirstFileEx(fileInfo.FullName, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0)) + string fullPath = Path.TrimEndingDirectorySeparator(fileInfo.FullName); + using (var handle = FindFirstFileEx(fullPath, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0)) { if (handle.IsInvalid) { - // If we can not open the file object we assume it's a symlink. - return true; + // Our handle could be invalidated by something else touching the filesystem, + // so ensure we deal with that possibility here + int lastError = Marshal.GetLastWin32Error(); + throw new Win32Exception(lastError); } - // To exclude one extra p/invoke in some scenarios - // we don't check fileInfo.FileAttributes + // We already have the file attribute information from our Win32 call, + // so no need to take the expense of the FileInfo.FileAttributes call const int FILE_ATTRIBUTE_REPARSE_POINT = 0x0400; if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) { @@ -8387,9 +8388,11 @@ private static bool WinGetInodeData(string path, out System.ValueTuple /// This interface needs to be implemented by providers that want users to see /// provider-specific help. /// +#nullable enable public interface ICmdletProviderSupportsHelp { /// @@ -37,7 +39,7 @@ public interface ICmdletProviderSupportsHelp [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Maml", Justification = "Maml is an acronym.")] string GetHelpMaml(string helpItemName, string path); } - +#nullable restore #region CmdletProvider /// @@ -1417,6 +1419,7 @@ public virtual string GetResourceString(string baseName, string resourceId) #region ThrowTerminatingError /// + [System.Diagnostics.CodeAnalysis.DoesNotReturn] public void ThrowTerminatingError(ErrorRecord errorRecord) { using (PSTransactionManager.GetEngineProtectionScope()) @@ -1824,7 +1827,7 @@ private PSObject WrapOutputInPSObject( { try { - // Use LStat because if you get a link, you want the information about the + // Use LStat because if you get a link, you want the information about the // link, not the file. var commonStat = Platform.Unix.GetLStat(path); result.AddOrSetProperty("UnixStat", commonStat); diff --git a/src/System.Management.Automation/namespaces/RegistryWrapper.cs b/src/System.Management.Automation/namespaces/RegistryWrapper.cs index 0e8e0558312..ebdbe833839 100644 --- a/src/System.Management.Automation/namespaces/RegistryWrapper.cs +++ b/src/System.Management.Automation/namespaces/RegistryWrapper.cs @@ -17,11 +17,13 @@ namespace Microsoft.PowerShell.Commands { + +#nullable enable internal interface IRegistryWrapper { - void SetValue(string name, object value); + void SetValue(string? name, object value); - void SetValue(string name, object value, RegistryValueKind valueKind); + void SetValue(string? name, object value, RegistryValueKind valueKind); string[] GetValueNames(); @@ -29,17 +31,17 @@ internal interface IRegistryWrapper string[] GetSubKeyNames(); - IRegistryWrapper CreateSubKey(string subkey); + IRegistryWrapper? CreateSubKey(string subkey); - IRegistryWrapper OpenSubKey(string name, bool writable); + IRegistryWrapper? OpenSubKey(string name, bool writable); void DeleteSubKeyTree(string subkey); - object GetValue(string name); + object? GetValue(string? name); - object GetValue(string name, object defaultValue, RegistryValueOptions options); + object? GetValue(string? name, object? defaultValue, RegistryValueOptions options); - RegistryValueKind GetValueKind(string name); + RegistryValueKind GetValueKind(string? name); object RegistryKey { get; } @@ -53,6 +55,7 @@ internal interface IRegistryWrapper int SubKeyCount { get; } } +#nullable restore internal static class RegistryWrapperUtils { diff --git a/src/System.Management.Automation/utils/PInvokeDllNames.cs b/src/System.Management.Automation/utils/PInvokeDllNames.cs index 7dd28be8cdd..bff2ca2656e 100644 --- a/src/System.Management.Automation/utils/PInvokeDllNames.cs +++ b/src/System.Management.Automation/utils/PInvokeDllNames.cs @@ -116,27 +116,26 @@ internal static class PinvokeDllNames internal const string PeekConsoleInputDllName = "api-ms-win-core-console-l2-1-0.dll"; /*103*/ internal const string GetNumberOfConsoleInputEventsDllName = "api-ms-win-core-console-l1-1-0.dll"; /*104*/ internal const string SetConsoleCtrlHandlerDllName = "api-ms-win-core-console-l1-1-0.dll"; /*105*/ - internal const string SetConsoleCursorPositionDllName = "api-ms-win-core-console-l2-1-0.dll"; /*106*/ - internal const string SetConsoleModeDllName = "api-ms-win-core-console-l1-1-0.dll"; /*107*/ - internal const string SetConsoleScreenBufferSizeDllName = "api-ms-win-core-console-l2-1-0.dll"; /*108*/ - internal const string SetConsoleTextAttributeDllName = "api-ms-win-core-console-l2-1-0.dll"; /*109*/ - internal const string SetConsoleWindowInfoDllName = "api-ms-win-core-console-l2-1-0.dll"; /*110*/ - internal const string WriteConsoleOutputDllName = "api-ms-win-core-console-l2-1-0.dll"; /*111*/ - internal const string ReadConsoleOutputDllName = "api-ms-win-core-console-l2-1-0.dll"; /*112*/ - internal const string ScrollConsoleScreenBufferDllName = "api-ms-win-core-console-l2-1-0.dll"; /*113*/ - internal const string SendInputDllName = "ext-ms-win-ntuser-keyboard-l1-2-1.dll"; /*114*/ - internal const string GetConsoleCursorInfoDllName = "api-ms-win-core-console-l2-1-0.dll"; /*115*/ - internal const string SetConsoleCursorInfoDllName = "api-ms-win-core-console-l2-1-0.dll"; /*116*/ - internal const string ReadConsoleInputDllName = "api-ms-win-core-console-l1-1-0.dll"; /*117*/ - internal const string GetVersionExDllName = "api-ms-win-core-sysinfo-l1-1-0.dll"; /*118*/ - internal const string FormatMessageDllName = "api-ms-win-core-localization-l1-2-0.dll"; /*119*/ - internal const string CreateToolhelp32SnapshotDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*120*/ - internal const string Process32FirstDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*121*/ - internal const string Process32NextDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*122*/ - internal const string GetACPDllName = "api-ms-win-core-localization-l1-2-0.dll"; /*123*/ - internal const string DeleteServiceDllName = "api-ms-win-service-management-l1-1-0.dll"; /*124*/ - internal const string QueryServiceConfigDllName = "api-ms-win-service-management-l2-1-0.dll"; /*125*/ - internal const string QueryServiceConfig2DllName = "api-ms-win-service-management-l2-1-0.dll"; /*126*/ - internal const string SetServiceObjectSecurityDllName = "api-ms-win-service-management-l2-1-0.dll"; /*127*/ + internal const string SetConsoleModeDllName = "api-ms-win-core-console-l1-1-0.dll"; /*106*/ + internal const string SetConsoleScreenBufferSizeDllName = "api-ms-win-core-console-l2-1-0.dll"; /*107*/ + internal const string SetConsoleTextAttributeDllName = "api-ms-win-core-console-l2-1-0.dll"; /*108*/ + internal const string SetConsoleWindowInfoDllName = "api-ms-win-core-console-l2-1-0.dll"; /*109*/ + internal const string WriteConsoleOutputDllName = "api-ms-win-core-console-l2-1-0.dll"; /*110*/ + internal const string ReadConsoleOutputDllName = "api-ms-win-core-console-l2-1-0.dll"; /*111*/ + internal const string ScrollConsoleScreenBufferDllName = "api-ms-win-core-console-l2-1-0.dll"; /*112*/ + internal const string SendInputDllName = "ext-ms-win-ntuser-keyboard-l1-2-1.dll"; /*113*/ + internal const string GetConsoleCursorInfoDllName = "api-ms-win-core-console-l2-1-0.dll"; /*114*/ + internal const string SetConsoleCursorInfoDllName = "api-ms-win-core-console-l2-1-0.dll"; /*115*/ + internal const string ReadConsoleInputDllName = "api-ms-win-core-console-l1-1-0.dll"; /*116*/ + internal const string GetVersionExDllName = "api-ms-win-core-sysinfo-l1-1-0.dll"; /*117*/ + internal const string FormatMessageDllName = "api-ms-win-core-localization-l1-2-0.dll"; /*118*/ + internal const string CreateToolhelp32SnapshotDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*119*/ + internal const string Process32FirstDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*120*/ + internal const string Process32NextDllName = "api-ms-win-core-toolhelp-l1-1-0"; /*121*/ + internal const string GetACPDllName = "api-ms-win-core-localization-l1-2-0.dll"; /*122*/ + internal const string DeleteServiceDllName = "api-ms-win-service-management-l1-1-0.dll"; /*123*/ + internal const string QueryServiceConfigDllName = "api-ms-win-service-management-l2-1-0.dll"; /*124*/ + internal const string QueryServiceConfig2DllName = "api-ms-win-service-management-l2-1-0.dll"; /*125*/ + internal const string SetServiceObjectSecurityDllName = "api-ms-win-service-management-l2-1-0.dll"; /*126*/ } } diff --git a/src/System.Management.Automation/utils/PlatformInvokes.cs b/src/System.Management.Automation/utils/PlatformInvokes.cs index b6d919df82e..2901f7d9b6e 100644 --- a/src/System.Management.Automation/utils/PlatformInvokes.cs +++ b/src/System.Management.Automation/utils/PlatformInvokes.cs @@ -688,24 +688,6 @@ public static extern System.IntPtr CreateFileW( UInt32 dwFlagsAndAttributes, System.IntPtr hTemplateFile); -#endif - - #endregion - - #region GetStdHandle - -#if !UNIX - - internal enum StandardHandleId : uint - { - Error = unchecked((uint)-12), - Output = unchecked((uint)-11), - Input = unchecked((uint)-10), - } - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern IntPtr GetStdHandle(uint handleId); - #endif #endregion diff --git a/src/System.Management.Automation/utils/PowerShellETWTracer.cs b/src/System.Management.Automation/utils/PowerShellETWTracer.cs index 9c5e2046c87..c22e738a35f 100644 --- a/src/System.Management.Automation/utils/PowerShellETWTracer.cs +++ b/src/System.Management.Automation/utils/PowerShellETWTracer.cs @@ -1189,7 +1189,7 @@ public void WriteMessage(string className, string methodName, Guid workflowId, J PSKeyword.UseAlwaysAnalytic, className, methodName, workflowId.ToString(), parameters == null ? message : StringUtil.Format(message, parameters), - sb.ToString(),// Job + sb.ToString(), // Job string.Empty, // Activity name string.Empty, // Activity GUID string.Empty); diff --git a/src/System.Management.Automation/utils/tracing/IMethodInvoker.cs b/src/System.Management.Automation/utils/tracing/IMethodInvoker.cs index daf52e80409..7382380bfbd 100644 --- a/src/System.Management.Automation/utils/tracing/IMethodInvoker.cs +++ b/src/System.Management.Automation/utils/tracing/IMethodInvoker.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + #if !UNIX namespace System.Management.Automation.Tracing @@ -11,7 +13,7 @@ internal interface IMethodInvoker { Delegate Invoker { get; } - object[] CreateInvokerArgs(Delegate methodToInvoke, object[] methodToInvokeArgs); + object[] CreateInvokerArgs(Delegate methodToInvoke, object?[]? methodToInvokeArgs); } } diff --git a/test/perf/benchmarks/Categories.cs b/test/perf/benchmarks/Categories.cs new file mode 100644 index 00000000000..09f71064930 --- /dev/null +++ b/test/perf/benchmarks/Categories.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace MicroBenchmarks +{ + public static class Categories + { + /// + /// Benchmarks belonging to this category are executed for CI jobs. + /// + public const string Components = "Components"; + + /// + /// Benchmarks belonging to this category are executed for CI jobs. + /// + public const string Engine = "Engine"; + + /// + /// Benchmarks belonging to this category are targeting internal APIs. + /// + public const string Internal = "Internal"; + + /// + /// Benchmarks belonging to this category are targeting public APIs. + /// + public const string Public = "Public"; + } +} diff --git a/test/perf/benchmarks/Engine.Parser.cs b/test/perf/benchmarks/Engine.Parser.cs new file mode 100644 index 00000000000..10538e3201a --- /dev/null +++ b/test/perf/benchmarks/Engine.Parser.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation.Language; +using BenchmarkDotNet.Attributes; +using MicroBenchmarks; + +namespace Engine +{ + [BenchmarkCategory(Categories.Engine, Categories.Public)] + public class Parsing + { + [Benchmark] + public Ast UsingStatement() + { + const string Script = @" + using module moduleA + using Assembly assemblyA + using namespace System.IO"; + return Parser.ParseInput(Script, out _, out _); + } + } +} diff --git a/test/perf/benchmarks/Engine.ScriptBlock.cs b/test/perf/benchmarks/Engine.ScriptBlock.cs new file mode 100644 index 00000000000..ad373dd5f99 --- /dev/null +++ b/test/perf/benchmarks/Engine.ScriptBlock.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; +using MicroBenchmarks; + +namespace Engine +{ + [BenchmarkCategory(Categories.Engine, Categories.Public)] + public class Scripting + { + private Runspace runspace; + private ScriptBlock scriptBlock; + + private void SetupRunspace() + { + // Unless you want to run commands from any built-in modules, using 'CreateDefault2' is enough. + runspace = RunspaceFactory.CreateRunspace(InitialSessionState.CreateDefault2()); + runspace.Open(); + Runspace.DefaultRunspace = runspace; + } + + #region Invoke-Method + + [ParamsSource(nameof(ValuesForScript))] + public string InvokeMethodScript { get; set; } + + public IEnumerable ValuesForScript() + { + yield return @"'String'.GetType()"; + yield return @"[System.IO.Path]::HasExtension('')"; + + // Test on COM method invocation. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + yield return @"$sh=New-Object -ComObject Shell.Application; $sh.Namespace('c:\')"; + yield return @"$fs=New-Object -ComObject scripting.filesystemobject; $fs.Drives"; + } + } + + [GlobalSetup(Target = nameof(InvokeMethod))] + public void GlobalSetup() + { + SetupRunspace(); + scriptBlock = ScriptBlock.Create(InvokeMethodScript); + + // Run it once to get the C# code jitted and the script compiled. + // The first call to this takes relatively too long, which makes the BDN's heuristic incorrectly + // believe that there is no need to run many ops in each interation. However, the subsequent runs + // of this method is much faster than the first run, and this causes 'MinIterationTime' warnings + // to our benchmarks and make the benchmark results not reliable. + // Calling this method once in 'GlobalSetup' is a workaround. + // See https://github.com/dotnet/BenchmarkDotNet/issues/837#issuecomment-828600157 + scriptBlock.Invoke(); + } + + [Benchmark] + public Collection InvokeMethod() + { + return scriptBlock.Invoke(); + } + + #endregion + + [GlobalCleanup] + public void GlobalCleanup() + { + runspace.Dispose(); + Runspace.DefaultRunspace = null; + } + } +} diff --git a/test/perf/benchmarks/Program.cs b/test/perf/benchmarks/Program.cs new file mode 100644 index 00000000000..2b3aafdb127 --- /dev/null +++ b/test/perf/benchmarks/Program.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Extensions; + +namespace MicroBenchmarks +{ + public class Program + { + public static int Main(string[] args) + { + var argsList = new List(args); + int? partitionCount; + int? partitionIndex; + List exclusionFilterValue; + List categoryExclusionFilterValue; + bool getDiffableDisasm; + + // Parse and remove any additional parameters that we need that aren't part of BDN (BenchmarkDotnet) + try + { + CommandLineOptions.ParseAndRemoveIntParameter(argsList, "--partition-count", out partitionCount); + CommandLineOptions.ParseAndRemoveIntParameter(argsList, "--partition-index", out partitionIndex); + CommandLineOptions.ParseAndRemoveStringsParameter(argsList, "--exclusion-filter", out exclusionFilterValue); + CommandLineOptions.ParseAndRemoveStringsParameter(argsList, "--category-exclusion-filter", out categoryExclusionFilterValue); + CommandLineOptions.ParseAndRemoveBooleanParameter(argsList, "--disasm-diff", out getDiffableDisasm); + + CommandLineOptions.ValidatePartitionParameters(partitionCount, partitionIndex); + } + catch (ArgumentException e) + { + Console.WriteLine("ArgumentException: {0}", e.Message); + return 1; + } + + return BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run( + argsList.ToArray(), + RecommendedConfig.Create( + artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "BenchmarkDotNet.Artifacts")), + mandatoryCategories: ImmutableHashSet.Create(Categories.Components, Categories.Engine), + partitionCount: partitionCount, + partitionIndex: partitionIndex, + exclusionFilterValue: exclusionFilterValue, + categoryExclusionFilterValue: categoryExclusionFilterValue, + getDiffableDisasm: getDiffableDisasm)) + .ToExitCode(); + } + } +} diff --git a/test/perf/benchmarks/README.md b/test/perf/benchmarks/README.md new file mode 100644 index 00000000000..f8c513a2d9a --- /dev/null +++ b/test/perf/benchmarks/README.md @@ -0,0 +1,88 @@ +## Micro Benchmarks + +This folder contains micro benchmarks that test the performance of PowerShell Engine. + +### Requirement + +1. A good suite of benchmarks + Something that measures only the thing that we are interested in and _produces accurate, stable and repeatable results_. +2. A set of machine with the same configurations. +3. Automation for regression detection. + +### Design Decision + +1. This project is internal visible to `System.Management.Automation`. + We want to be able to target some internal APIs to get measurements on specific scoped scenarios, + such as measuring the time to compile AST to a delegate by the compiler. +2. This project makes `ProjectReference` to other PowerShell assemblies. + This makes it easy to run benchmarks with the changes made in the codebase. + To run benchmarks with a specific version of PowerShell, + just replace the `ProjectReference` with a `PackageReference` to the `Microsoft.PowerShell.SDK` NuGet package of the corresponding version. + +### Quick Start + +You can run the benchmarks directly using `dotnet run` in this directory: +1. To run the benchmarks in Interactive Mode, where you will be asked which benchmark(s) to run: + ``` + dotnet run -c Release + ``` + +2. To list all available benchmarks ([read more](https://github.com/dotnet/performance/blob/main/docs/benchmarkdotnet.md#Listing-the-Benchmarks)): + ``` + dotnet run -c Release --list [flat/tree] + ``` + +3. To filter the benchmarks using a glob pattern applied to `namespace.typeName.methodName` ([read more](https://github.com/dotnet/performance/blob/main/docs/benchmarkdotnet.md#Filtering-the-Benchmarks)]): + ``` + dotnet run -c Release -f net6.0 --filter *script* --list flat + ``` + +4. To profile the benchmarked code and produce an ETW Trace file ([read more](https://github.com/dotnet/performance/blob/main/docs/benchmarkdotnet.md#Profiling)) + ``` + dotnet run -c Release -f net6.0 --filter *script* --profiler ETW + ``` + +You can also use the function `Start-Benchmarking` from the module [`perf.psm1`](../perf.psm1) to run the benchmarks: +```powershell +Start-Benchmarking [[-TargetPSVersion] ] [[-List] ] [[-Filter] ] [[-Artifacts] ] [-KeepFiles] [] +``` +Run `Get-Help Start-Benchmarking -Full` to see the description of each parameter. + +### Regression Detection + +We use the tool [`ResultsComparer`](../dotnet-tools/ResultsComparer) to compare the provided benchmark results. +See the [README.md](../dotnet-tools/ResultsComparer/README.md) for `ResultsComparer` for more details. + +The module `perf.psm1` also provides `Compare-BenchmarkResult` that wraps `ResultsComparer`. +Here is an example of using it: + +``` +## Run benchmarks targeting the current code base +PS:1> Start-Benchmarking -Filter *script* -Artifacts C:\arena\tmp\BenchmarkDotNet.Artifacts\current\ + +## Run benchmarks targeting the 7.1.3 version of PS package +PS:2> Start-Benchmarking -Filter *script* -Artifacts C:\arena\tmp\BenchmarkDotNet.Artifacts\7.1.3 -TargetPSVersion 7.1.3 + +## Compare the results using 5% threshold +PS:3> Compare-BenchmarkResult -BaseResultPath C:\arena\tmp\BenchmarkDotNet.Artifacts\7.1.3\ -DiffResultPath C:\arena\tmp\BenchmarkDotNet.Artifacts\current\ -Threshold 1% +summary: +better: 4, geomean: 1.057 +total diff: 4 + +No Slower results for the provided threshold = 1% and noise filter = 0.3ns. + +| Faster | base/diff | Base Median (ns) | Diff Median (ns) | Modality| +| -------------------------------------------------------------------------------- | ---------:| ----------------:| ----------------:| --------:| +| Engine.Scripting.InvokeMethod(Script: "$fs=New-Object -ComObject scripting.files | 1.07 | 50635.77 | 47116.42 | | +| Engine.Scripting.InvokeMethod(Script: "$sh=New-Object -ComObject Shell.Applicati | 1.07 | 1063085.23 | 991602.08 | | +| Engine.Scripting.InvokeMethod(Script: "'String'.GetType()") | 1.06 | 1329.93 | 1252.51 | | +| Engine.Scripting.InvokeMethod(Script: "[System.IO.Path]::HasExtension('')") | 1.02 | 1322.04 | 1297.72 | | + +No file given +``` + +## References + +- [Getting started with BenchmarkDotNet](https://benchmarkdotnet.org/articles/guides/getting-started.html) +- [Micro-benchmark Design Guidelines](https://github.com/dotnet/performance/blob/main/docs/microbenchmark-design-guidelines.md) +- [Adam SITNIK: Powerful benchmarking in .NET](https://www.youtube.com/watch?v=pdcrSG4tOLI&t=351s) diff --git a/test/perf/benchmarks/powershell-perf.csproj b/test/perf/benchmarks/powershell-perf.csproj new file mode 100644 index 00000000000..78608c2e96f --- /dev/null +++ b/test/perf/benchmarks/powershell-perf.csproj @@ -0,0 +1,65 @@ + + + + + + + PowerShell Performance Tests + powershell-perf + Exe + + $(NoWarn);CS8002 + true + + AnyCPU + portable + true + + + $(PERF_TARGET_VERSION) + + + + netcoreapp3.1;net5.0;net6.0 + + 7.1.3 + 7.0.6 + + + + true + ../../../src/signing/visualstudiopublic.snk + true + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj new file mode 100644 index 00000000000..1383cfc1d62 --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj @@ -0,0 +1,17 @@ + + + + Library + netstandard2.0 + + + + + + + + + + + + diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/CommandLineOptions.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/CommandLineOptions.cs new file mode 100644 index 00000000000..3c8b343fc57 --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/CommandLineOptions.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; + +namespace BenchmarkDotNet.Extensions +{ + public class CommandLineOptions + { + // Find and parse given parameter with expected int value, then remove it and its value from the list of arguments to then pass to BenchmarkDotNet + // Throws ArgumentException if the parameter does not have a value or that value is not parsable as an int + public static List ParseAndRemoveIntParameter(List argsList, string parameter, out int? parameterValue) + { + int parameterIndex = argsList.IndexOf(parameter); + parameterValue = null; + + if (parameterIndex != -1) + { + if (parameterIndex + 1 < argsList.Count && Int32.TryParse(argsList[parameterIndex+1], out int parsedParameterValue)) + { + // remove --partition-count args + parameterValue = parsedParameterValue; + argsList.RemoveAt(parameterIndex+1); + argsList.RemoveAt(parameterIndex); + } + else + { + throw new ArgumentException(String.Format("{0} must be followed by an integer", parameter)); + } + } + + return argsList; + } + + public static List ParseAndRemoveStringsParameter(List argsList, string parameter, out List parameterValue) + { + int parameterIndex = argsList.IndexOf(parameter); + parameterValue = new List(); + + if (parameterIndex + 1 < argsList.Count) + { + while (parameterIndex + 1 < argsList.Count && !argsList[parameterIndex + 1].StartsWith("-")) + { + // remove each filter string and stop when we get to the next argument flag + parameterValue.Add(argsList[parameterIndex + 1]); + argsList.RemoveAt(parameterIndex + 1); + } + } + //We only want to remove the --exclusion-filter if it exists + if (parameterIndex != -1) + { + argsList.RemoveAt(parameterIndex); + } + + return argsList; + } + + public static void ParseAndRemoveBooleanParameter(List argsList, string parameter, out bool parameterValue) + { + int parameterIndex = argsList.IndexOf(parameter); + + if (parameterIndex != -1) + { + argsList.RemoveAt(parameterIndex); + + parameterValue = true; + } + else + { + parameterValue = false; + } + } + + public static void ValidatePartitionParameters(int? count, int? index) + { + // Either count and index must both be specified or neither specified + if (!(count.HasValue == index.HasValue)) + { + throw new ArgumentException("If either --partition-count or --partition-index is specified, both must be specified"); + } + // Check values of count and index parameters + else if (count.HasValue && index.HasValue) + { + if (count < 2) + { + throw new ArgumentException("When specified, value of --partition-count must be greater than 1"); + } + else if (!(index < count)) + { + throw new ArgumentException("Value of --partition-index must be less than --partition-count"); + } + else if (index < 0) + { + throw new ArgumentException("Value of --partition-index must be greater than or equal to 0"); + } + } + } + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/DiffableDisassemblyExporter.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/DiffableDisassemblyExporter.cs new file mode 100644 index 00000000000..d45977ed5bf --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/DiffableDisassemblyExporter.cs @@ -0,0 +1,90 @@ +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Disassemblers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace BenchmarkDotNet.Extensions +{ + // a simplified copy of internal BDN type: https://github.com/dotnet/BenchmarkDotNet/blob/0445917bf93059f17cb09e7d48cdb5e27a096c37/src/BenchmarkDotNet/Disassemblers/Exporters/GithubMarkdownDisassemblyExporter.cs#L35-L80 + internal static class DiffableDisassemblyExporter + { + private static readonly Lazy> GetSource = new Lazy>(() => GetElementGetter("Source")); + private static readonly Lazy> GetTextRepresentation = new Lazy>(() => GetElementGetter("TextRepresentation")); + + private static readonly Lazy>> Prettify + = new Lazy>>(GetPrettifyMethod); + + internal static string BuildDisassemblyString(DisassemblyResult disassemblyResult, DisassemblyDiagnoserConfig config) + { + StringBuilder sb = new StringBuilder(); + + int methodIndex = 0; + foreach (var method in disassemblyResult.Methods.Where(method => string.IsNullOrEmpty(method.Problem))) + { + sb.AppendLine("```assembly"); + + sb.AppendLine($"; {method.Name}"); + + var pretty = Prettify.Value.Invoke(method, disassemblyResult, config, $"M{methodIndex++:00}"); + + ulong totalSizeInBytes = 0; + foreach (var element in pretty) + { + if (element.Source() is Asm asm) + { + checked + { + totalSizeInBytes += (uint)asm.Instruction.Length; + } + + sb.AppendLine($" {element.TextRepresentation()}"); + } + else // it's a DisassemblyPrettifier.Label (internal type..) + { + sb.AppendLine($"{element.TextRepresentation()}:"); + } + } + + sb.AppendLine($"; Total bytes of code {totalSizeInBytes}"); + sb.AppendLine("```"); + } + + return sb.ToString(); + } + + private static SourceCode Source(this object element) => GetSource.Value.Invoke(element); + + private static string TextRepresentation(this object element) => GetTextRepresentation.Value.Invoke(element); + + private static Func GetElementGetter(string name) + { + var type = typeof(DisassemblyDiagnoser).Assembly.GetType("BenchmarkDotNet.Disassemblers.Exporters.DisassemblyPrettifier"); + + type = type.GetNestedType("Element", BindingFlags.Instance | BindingFlags.NonPublic); + + var property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.NonPublic); + + var method = property.GetGetMethod(nonPublic: true); + + var generic = typeof(Func<,>).MakeGenericType(type, typeof(T)); + + var @delegate = method.CreateDelegate(generic); + + return (obj) => (T)@delegate.DynamicInvoke(obj); // cast to (Func) throws + } + + private static Func> GetPrettifyMethod() + { + var type = typeof(DisassemblyDiagnoser).Assembly.GetType("BenchmarkDotNet.Disassemblers.Exporters.DisassemblyPrettifier"); + + var method = type.GetMethod("Prettify", BindingFlags.Static | BindingFlags.NonPublic); + + var @delegate = method.CreateDelegate(typeof(Func>)); + + return (Func>)@delegate; + } + } +} diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/ExclusionFilter.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/ExclusionFilter.cs new file mode 100644 index 00000000000..b3ee453123f --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/ExclusionFilter.cs @@ -0,0 +1,52 @@ +using BenchmarkDotNet.Filters; +using BenchmarkDotNet.Running; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BenchmarkDotNet.Extensions +{ + class ExclusionFilter : IFilter + { + private readonly GlobFilter globFilter; + + public ExclusionFilter(List _filter) + { + if (_filter != null && _filter.Count != 0) + { + globFilter = new GlobFilter(_filter.ToArray()); + } + } + + public bool Predicate(BenchmarkCase benchmarkCase) + { + if(globFilter == null) + { + return true; + } + return !globFilter.Predicate(benchmarkCase); + } + } + + class CategoryExclusionFilter : IFilter + { + private readonly AnyCategoriesFilter filter; + + public CategoryExclusionFilter(List patterns) + { + if (patterns != null) + { + filter = new AnyCategoriesFilter(patterns.ToArray()); + } + } + + public bool Predicate(BenchmarkCase benchmarkCase) + { + if (filter == null) + { + return true; + } + return !filter.Predicate(benchmarkCase); + } + } +} diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/Extensions.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/Extensions.cs new file mode 100644 index 00000000000..7d0631b896b --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/Extensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Reports; + +namespace BenchmarkDotNet.Extensions +{ + public static class SummaryExtensions + { + public static int ToExitCode(this IEnumerable summaries) + { + // an empty summary means that initial filtering and validation did not allow to run + if (!summaries.Any()) + return 1; + + // if anything has failed, it's an error + if (summaries.Any(summary => summary.HasCriticalValidationErrors || summary.Reports.Any(report => !report.BuildResult.IsBuildSuccess || !report.AllMeasurements.Any()))) + return 1; + + return 0; + } + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/MandatoryCategoryValidator.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/MandatoryCategoryValidator.cs new file mode 100644 index 00000000000..7b3b2d38f3b --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/MandatoryCategoryValidator.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using BenchmarkDotNet.Validators; + +namespace BenchmarkDotNet.Extensions +{ + /// + /// this class makes sure that every benchmark belongs to a mandatory category + /// categories are used by the CI for filtering + /// + public class MandatoryCategoryValidator : IValidator + { + private readonly ImmutableHashSet _mandatoryCategories; + + public bool TreatsWarningsAsErrors => true; + + public MandatoryCategoryValidator(ImmutableHashSet categories) => _mandatoryCategories = categories; + + public IEnumerable Validate(ValidationParameters validationParameters) + => validationParameters.Benchmarks + .Where(benchmark => !benchmark.Descriptor.Categories.Any(category => _mandatoryCategories.Contains(category))) + .Select(benchmark => benchmark.Descriptor.GetFilterName()) + .Distinct() + .Select(benchmarkId => + new ValidationError( + isCritical: TreatsWarningsAsErrors, + $"{benchmarkId} does not belong to one of the mandatory categories: {string.Join(", ", _mandatoryCategories)}. Use [BenchmarkCategory(Categories.$)]") + ); + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/PartitionFilter.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/PartitionFilter.cs new file mode 100644 index 00000000000..16ae22f3167 --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/PartitionFilter.cs @@ -0,0 +1,27 @@ +using BenchmarkDotNet.Filters; +using System; +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Running; + + +public class PartitionFilter : IFilter +{ + private readonly int? _partitionsCount; + private readonly int? _partitionIndex; // indexed from 0 + private int _counter = 0; + + public PartitionFilter(int? partitionCount, int? partitionIndex) + { + _partitionsCount = partitionCount; + _partitionIndex = partitionIndex; + } + + public bool Predicate(BenchmarkCase benchmarkCase) + { + if (!_partitionsCount.HasValue || !_partitionIndex.HasValue) + return true; // the filter is not enabled so it does not filter anything out and can be added to RecommendedConfig + + return _counter++ % _partitionsCount.Value == _partitionIndex.Value; // will return true only for benchmarks that belong to it’s partition + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/PerfLabExporter.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/PerfLabExporter.cs new file mode 100644 index 00000000000..b50e408af0b --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/PerfLabExporter.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Reports; +using Reporting; +using System.Linq; + +namespace BenchmarkDotNet.Extensions +{ + internal class PerfLabExporter : ExporterBase + { + protected override string FileExtension => "json"; + protected override string FileCaption => "perf-lab-report"; + + public PerfLabExporter() + { + } + + public override void ExportToLog(Summary summary, ILogger logger) + { + var reporter = Reporter.CreateReporter(); + + DisassemblyDiagnoser disassemblyDiagnoser = summary.Reports + .FirstOrDefault()? // dissasembler was either enabled for all or none of them (so we use the first one) + .BenchmarkCase.Config.GetDiagnosers().OfType().FirstOrDefault(); + + foreach (var report in summary.Reports) + { + var test = new Test(); + test.Name = FullNameProvider.GetBenchmarkName(report.BenchmarkCase); + test.Categories = report.BenchmarkCase.Descriptor.Categories; + + var results = from result in report.AllMeasurements + where result.IterationMode == Engines.IterationMode.Workload && result.IterationStage == Engines.IterationStage.Result + orderby result.LaunchIndex, result.IterationIndex + select new { result.Nanoseconds, result.Operations}; + + var overheadResults = from result in report.AllMeasurements + where result.IsOverhead() && result.IterationStage != Engines.IterationStage.Jitting + orderby result.LaunchIndex, result.IterationIndex + select new { result.Nanoseconds, result.Operations }; + + test.Counters.Add(new Counter + { + Name = "Duration of single invocation", + TopCounter = true, + DefaultCounter = true, + HigherIsBetter = false, + MetricName = "ns", + Results = (from result in results + select result.Nanoseconds / result.Operations).ToList() + }); + test.Counters.Add(new Counter + { + Name = "Overhead invocation", + TopCounter = false, + DefaultCounter = false, + HigherIsBetter = false, + MetricName = "ns", + Results = (from result in overheadResults + select result.Nanoseconds / result.Operations).ToList() + }); + test.Counters.Add(new Counter + { + Name = "Duration", + TopCounter = false, + DefaultCounter = false, + HigherIsBetter = false, + MetricName = "ms", + Results = (from result in results + select result.Nanoseconds).ToList() + }); + + test.Counters.Add(new Counter + { + Name = "Operations", + TopCounter = false, + DefaultCounter = false, + HigherIsBetter = true, + MetricName = "Count", + Results = (from result in results + select (double)result.Operations).ToList() + }); + + foreach (var metric in report.Metrics.Keys) + { + var m = report.Metrics[metric]; + test.Counters.Add(new Counter + { + Name = m.Descriptor.DisplayName, + TopCounter = false, + DefaultCounter = false, + HigherIsBetter = m.Descriptor.TheGreaterTheBetter, + MetricName = m.Descriptor.Unit, + Results = new[] { m.Value } + }); + } + + if (disassemblyDiagnoser != null && disassemblyDiagnoser.Results.TryGetValue(report.BenchmarkCase, out var disassemblyResult)) + { + string disassembly = DiffableDisassemblyExporter.BuildDisassemblyString(disassemblyResult, disassemblyDiagnoser.Config); + test.AdditionalData["disasm"] = disassembly; + } + + reporter.AddTest(test); + } + + logger.WriteLine(reporter.GetJson()); + } + } +} diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/RecommendedConfig.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/RecommendedConfig.cs new file mode 100644 index 00000000000..40aa5f87fbe --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/RecommendedConfig.cs @@ -0,0 +1,86 @@ +using System.Collections.Immutable; +using System.IO; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters.Json; +using Perfolizer.Horology; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Reports; +using System.Collections.Generic; +using Reporting; +using BenchmarkDotNet.Loggers; +using System.Linq; +using BenchmarkDotNet.Exporters; + +namespace BenchmarkDotNet.Extensions +{ + public static class RecommendedConfig + { + public static IConfig Create( + DirectoryInfo artifactsPath, + ImmutableHashSet mandatoryCategories, + int? partitionCount = null, + int? partitionIndex = null, + List exclusionFilterValue = null, + List categoryExclusionFilterValue = null, + Job job = null, + bool getDiffableDisasm = false) + { + if (job is null) + { + job = Job.Default + .WithWarmupCount(1) // 1 warmup is enough for our purpose + .WithIterationTime(TimeInterval.FromMilliseconds(250)) // the default is 0.5s per iteration, which is slighlty too much for us + .WithMinIterationCount(15) + .WithMaxIterationCount(20) // we don't want to run more that 20 iterations + .DontEnforcePowerPlan(); // make sure BDN does not try to enforce High Performance power plan on Windows + + // See https://github.com/dotnet/roslyn/issues/42393 + job = job.WithArguments(new Argument[] { new MsBuildArgument("/p:DebugType=portable") }); + } + + var config = ManualConfig.CreateEmpty() + .AddLogger(ConsoleLogger.Default) // log output to console + .AddValidator(DefaultConfig.Instance.GetValidators().ToArray()) // copy default validators + .AddAnalyser(DefaultConfig.Instance.GetAnalysers().ToArray()) // copy default analysers + .AddExporter(MarkdownExporter.GitHub) // export to GitHub markdown + .AddColumnProvider(DefaultColumnProviders.Instance) // display default columns (method name, args etc) + .AddJob(job.AsDefault()) // tell BDN that this are our default settings + .WithArtifactsPath(artifactsPath.FullName) + .AddDiagnoser(MemoryDiagnoser.Default) // MemoryDiagnoser is enabled by default + .AddFilter(new PartitionFilter(partitionCount, partitionIndex)) + .AddFilter(new ExclusionFilter(exclusionFilterValue)) + .AddFilter(new CategoryExclusionFilter(categoryExclusionFilterValue)) + .AddExporter(JsonExporter.Full) // make sure we export to Json + .AddColumn(StatisticColumn.Median, StatisticColumn.Min, StatisticColumn.Max) + .AddValidator(TooManyTestCasesValidator.FailOnError) + .AddValidator(new UniqueArgumentsValidator()) // don't allow for duplicated arguments #404 + .AddValidator(new MandatoryCategoryValidator(mandatoryCategories)) + .WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(36)); // the default is 20 and trims too aggressively some benchmark results + + if (Reporter.CreateReporter().InLab) + { + config = config.AddExporter(new PerfLabExporter()); + } + + if (getDiffableDisasm) + { + config = config.AddDiagnoser(CreateDisassembler()); + } + + return config; + } + + private static DisassemblyDiagnoser CreateDisassembler() + => new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig( + maxDepth: 1, // TODO: is depth == 1 enough? + formatter: null, // TODO: enable diffable format + printSource: false, // we are not interested in getting C# + printInstructionAddresses: false, // would make the diffing hard, however could be useful to determine alignment + exportGithubMarkdown: false, + exportHtml: false, + exportCombinedDisassemblyReport: false, + exportDiff: false)); + } +} diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/TooManyTestCasesValidator.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/TooManyTestCasesValidator.cs new file mode 100644 index 00000000000..36c8b41f9c7 --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/TooManyTestCasesValidator.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Validators; + +namespace BenchmarkDotNet.Extensions +{ + /// + /// we need to tell our users that having more than 16 test cases per benchmark is a VERY BAD idea + /// + public class TooManyTestCasesValidator : IValidator + { + private const int Limit = 16; + + public static readonly IValidator FailOnError = new TooManyTestCasesValidator(); + + public bool TreatsWarningsAsErrors => true; + + public IEnumerable Validate(ValidationParameters validationParameters) + { + var byDescriptor = validationParameters.Benchmarks.GroupBy(benchmark => (benchmark.Descriptor, benchmark.Job)); // descriptor = type + method + + return byDescriptor.Where(benchmarkCase => benchmarkCase.Count() > Limit).Select(group => + new ValidationError( + isCritical: true, + message: $"{group.Key.Descriptor.Type.Name}.{group.Key.Descriptor.WorkloadMethod.Name} has {group.Count()} test cases. It MUST NOT have more than {Limit} test cases. We don't have inifinite amount of time to run all the benchmarks!!", + benchmarkCase: group.First())); + } + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/UniqueArgumentsValidator.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/UniqueArgumentsValidator.cs new file mode 100644 index 00000000000..7bfab8445cb --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/UniqueArgumentsValidator.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using BenchmarkDotNet.Validators; +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Running; + +namespace BenchmarkDotNet.Extensions +{ + public class UniqueArgumentsValidator : IValidator + { + public bool TreatsWarningsAsErrors => true; + + public IEnumerable Validate(ValidationParameters validationParameters) + => validationParameters.Benchmarks + .Where(benchmark => benchmark.HasArguments || benchmark.HasParameters) + .GroupBy(benchmark => (benchmark.Descriptor.Type, benchmark.Descriptor.WorkloadMethod, benchmark.Job)) + .Where(sameBenchmark => + { + int numberOfUniqueTestCases = sameBenchmark.Distinct(new BenchmarkArgumentsComparer()).Count(); + int numberOfTestCases = sameBenchmark.Count(); + + return numberOfTestCases != numberOfUniqueTestCases; + }) + .Select(duplicate => new ValidationError(true, $"Benchmark Arguments should be unique, {duplicate.Key.Type}.{duplicate.Key.WorkloadMethod} has duplicate arguments.", duplicate.First())); + + private class BenchmarkArgumentsComparer : IEqualityComparer + { + public bool Equals(BenchmarkCase x, BenchmarkCase y) + => Enumerable.SequenceEqual( + x.Parameters.Items.Select(argument => argument.Value), + y.Parameters.Items.Select(argument => argument.Value)); + + public int GetHashCode(BenchmarkCase obj) + => obj.Parameters.Items + .Where(item => item.Value != null) + .Aggregate(seed: 0, (hashCode, argument) => hashCode ^= argument.Value.GetHashCode()); + } + } +} diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/ValuesGenerator.cs b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/ValuesGenerator.cs new file mode 100644 index 00000000000..87bf6d82d8d --- /dev/null +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/ValuesGenerator.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace BenchmarkDotNet.Extensions +{ + public static class ValuesGenerator + { + private const int Seed = 12345; // we always use the same seed to have repeatable results! + + public static T GetNonDefaultValue() + { + if (typeof(T) == typeof(byte)) // we can't use ArrayOfUniqueValues for byte + return Array(byte.MaxValue).First(value => !value.Equals(default)); + else + return ArrayOfUniqueValues(2).First(value => !value.Equals(default)); + } + + /// + /// does not support byte because there are only 256 unique byte values + /// + public static T[] ArrayOfUniqueValues(int count) + { + // allocate the array first to try to take advantage of memory randomization + // as it's usually the first thing called from GlobalSetup method + // which with MemoryRandomization enabled is the first method called right after allocation + // of random-sized memory by BDN engine + T[] result = new T[count]; + + var random = new Random(Seed); + + var uniqueValues = new HashSet(); + + while (uniqueValues.Count != count) + { + T value = GenerateValue(random); + + if (!uniqueValues.Contains(value)) + uniqueValues.Add(value); + } + + uniqueValues.CopyTo(result); + + return result; + } + + public static T[] Array(int count) + { + var result = new T[count]; + + var random = new Random(Seed); + + if (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte)) + { + random.NextBytes(Unsafe.As(result)); + } + else + { + for (int i = 0; i < result.Length; i++) + { + result[i] = GenerateValue(random); + } + } + + return result; + } + + public static Dictionary Dictionary(int count) + { + var dictionary = new Dictionary(); + + var random = new Random(Seed); + + while (dictionary.Count != count) + { + TKey key = GenerateValue(random); + + if (!dictionary.ContainsKey(key)) + dictionary.Add(key, GenerateValue(random)); + } + + return dictionary; + } + + private static T GenerateValue(Random random) + { + if (typeof(T) == typeof(char)) + return (T)(object)(char)random.Next(char.MinValue, char.MaxValue); + if (typeof(T) == typeof(short)) + return (T)(object)(short)random.Next(short.MaxValue); + if (typeof(T) == typeof(ushort)) + return (T)(object)(ushort)random.Next(short.MaxValue); + if (typeof(T) == typeof(int)) + return (T)(object)random.Next(); + if (typeof(T) == typeof(uint)) + return (T)(object)(uint)random.Next(); + if (typeof(T) == typeof(long)) + return (T)(object)(long)random.Next(); + if (typeof(T) == typeof(ulong)) + return (T)(object)(ulong)random.Next(); + if (typeof(T) == typeof(float)) + return (T)(object)(float)random.NextDouble(); + if (typeof(T) == typeof(double)) + return (T)(object)random.NextDouble(); + if (typeof(T) == typeof(bool)) + return (T)(object)(random.NextDouble() > 0.5); + if (typeof(T) == typeof(string)) + return (T)(object)GenerateRandomString(random, 1, 50); + if (typeof(T) == typeof(Guid)) + return (T)(object)GenerateRandomGuid(random); + + throw new NotImplementedException($"{typeof(T).Name} is not implemented"); + } + + private static string GenerateRandomString(Random random, int minLength, int maxLength) + { + var length = random.Next(minLength, maxLength); + + var builder = new StringBuilder(length); + for (int i = 0; i < length; i++) + { + var rangeSelector = random.Next(0, 3); + + if (rangeSelector == 0) + builder.Append((char) random.Next('a', 'z')); + else if (rangeSelector == 1) + builder.Append((char) random.Next('A', 'Z')); + else + builder.Append((char) random.Next('0', '9')); + } + + return builder.ToString(); + } + + private static Guid GenerateRandomGuid(Random random) + { + byte[] bytes = new byte[16]; + random.NextBytes(bytes); + return new Guid(bytes); + } + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/README.md b/test/perf/dotnet-tools/README.md new file mode 100644 index 00000000000..745130f9d6c --- /dev/null +++ b/test/perf/dotnet-tools/README.md @@ -0,0 +1,14 @@ +## Tools + +The tools here are copied from [dotnet/performance](https://github.com/dotnet/performance), +the performance testing repository for the .NET runtime and framework libraries. + +- [BenchmarkDotNet.Extensions](https://github.com/dotnet/performance/tree/main/src/harness/BenchmarkDotNet.Extensions) + - It provides the needed extensions for running benckmarks, + such as the `RecommendedConfig` which defines the set of recommended configurations for running the dotnet benckmarks. +- [Reporting](https://github.com/dotnet/performance/tree/main/src/tools/Reporting) + - It provides additional result reporting support + which may be useful to us when running our benchmarks in lab. +- [ResultsComparer](https://github.com/dotnet/performance/tree/main/src/tools/ResultsComparer) + - It's a tool for comparing different benchmark results. + It's very useful to show the regression of new changes by comparing its benchmark results to the baseline results. diff --git a/test/perf/dotnet-tools/Reporting/Build.cs b/test/perf/dotnet-tools/Reporting/Build.cs new file mode 100644 index 00000000000..c98ac254f34 --- /dev/null +++ b/test/perf/dotnet-tools/Reporting/Build.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; + +namespace Reporting +{ + public sealed class Build + { + public string Repo { get; set; } + + public string Branch { get; set; } + + public string Architecture { get; set; } + + public string Locale { get; set; } + + public string GitHash { get; set; } + + public string BuildName { get; set; } + + public DateTime TimeStamp { get; set; } + + public Dictionary AdditionalData { get; set; } = new Dictionary(); + } +} diff --git a/test/perf/dotnet-tools/Reporting/Counter.cs b/test/perf/dotnet-tools/Reporting/Counter.cs new file mode 100644 index 00000000000..3952369c312 --- /dev/null +++ b/test/perf/dotnet-tools/Reporting/Counter.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Reporting +{ + public class Counter + { + public string Name { get; set; } + + public bool TopCounter { get; set; } + + public bool DefaultCounter { get; set; } + + public bool HigherIsBetter { get; set; } + + public string MetricName { get; set; } + + public IList Results { get; set; } + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/Reporting/EnvironmentProvider.cs b/test/perf/dotnet-tools/Reporting/EnvironmentProvider.cs new file mode 100644 index 00000000000..90d28729284 --- /dev/null +++ b/test/perf/dotnet-tools/Reporting/EnvironmentProvider.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reporting +{ + public class EnvironmentProvider : IEnvironment + { + public string GetEnvironmentVariable(string variable) => Environment.GetEnvironmentVariable(variable); + } +} diff --git a/test/perf/dotnet-tools/Reporting/IEnvironment.cs b/test/perf/dotnet-tools/Reporting/IEnvironment.cs new file mode 100644 index 00000000000..c7dbfb9b002 --- /dev/null +++ b/test/perf/dotnet-tools/Reporting/IEnvironment.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reporting +{ + public interface IEnvironment + { + string GetEnvironmentVariable(string variable); + } +} diff --git a/test/perf/dotnet-tools/Reporting/Os.cs b/test/perf/dotnet-tools/Reporting/Os.cs new file mode 100644 index 00000000000..692a75ee7c5 --- /dev/null +++ b/test/perf/dotnet-tools/Reporting/Os.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Reporting +{ + public class Os + { + public string Locale { get; set; } + + public string Architecture { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/Reporting/Reporter.cs b/test/perf/dotnet-tools/Reporting/Reporter.cs new file mode 100644 index 00000000000..9291d6f36eb --- /dev/null +++ b/test/perf/dotnet-tools/Reporting/Reporter.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; + +namespace Reporting +{ + public class Reporter + { + private Run run; + private Os os; + private Build build; + private List tests = new List(); + protected IEnvironment environment; + + private Reporter() { } + + public void AddTest(Test test) + { + if (tests.Any(t => t.Name.Equals(test.Name))) + throw new Exception($"Duplicate test name, {test.Name}"); + tests.Add(test); + } + + /// + /// Get a Reporter. Relies on environment variables. + /// + /// Optional environment variable provider + /// A Reporter instance or null if the environment is incorrect. + public static Reporter CreateReporter(IEnvironment environment = null) + { + var ret = new Reporter(); + ret.environment = environment == null ? new EnvironmentProvider() : environment; + if (ret.InLab) + { + ret.Init(); + } + + return ret; + } + + private void Init() + { + run = new Run + { + CorrelationId = environment.GetEnvironmentVariable("HELIX_CORRELATION_ID"), + PerfRepoHash = environment.GetEnvironmentVariable("PERFLAB_PERFHASH"), + Name = environment.GetEnvironmentVariable("PERFLAB_RUNNAME"), + Queue = environment.GetEnvironmentVariable("PERFLAB_QUEUE"), + }; + Boolean.TryParse(environment.GetEnvironmentVariable("PERFLAB_HIDDEN"), out bool hidden); + run.Hidden = hidden; + var configs = environment.GetEnvironmentVariable("PERFLAB_CONFIGS"); + if (!String.IsNullOrEmpty(configs)) // configs should be optional. + { + foreach (var kvp in configs.Split(';')) + { + var split = kvp.Split('='); + run.Configurations.Add(split[0], split[1]); + } + } + + os = new Os() + { + Name = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}", + Architecture = RuntimeInformation.OSArchitecture.ToString(), + Locale = CultureInfo.CurrentUICulture.ToString() + }; + + build = new Build + { + Repo = environment.GetEnvironmentVariable("PERFLAB_REPO"), + Branch = environment.GetEnvironmentVariable("PERFLAB_BRANCH"), + Architecture = environment.GetEnvironmentVariable("PERFLAB_BUILDARCH"), + Locale = environment.GetEnvironmentVariable("PERFLAB_LOCALE"), + GitHash = environment.GetEnvironmentVariable("PERFLAB_HASH"), + BuildName = environment.GetEnvironmentVariable("PERFLAB_BUILDNUM"), + TimeStamp = DateTime.Parse(environment.GetEnvironmentVariable("PERFLAB_BUILDTIMESTAMP")), + }; + build.AdditionalData["productVersion"] = environment.GetEnvironmentVariable("DOTNET_VERSION"); + } + public string GetJson() + { + if (!InLab) + { + return null; + } + var jsonobj = new + { + build, + os, + run, + tests + }; + var settings = new JsonSerializerSettings(); + var resolver = new DefaultContractResolver(); + resolver.NamingStrategy = new CamelCaseNamingStrategy() { ProcessDictionaryKeys = false }; + settings.ContractResolver = resolver; + return JsonConvert.SerializeObject(jsonobj, Formatting.Indented, settings); + } + + public string WriteResultTable() + { + StringBuilder ret = new StringBuilder(); + foreach (var test in tests) + { + var defaultCounter = test.Counters.Single(c => c.DefaultCounter); + var topCounters = test.Counters.Where(c => c.TopCounter && !c.DefaultCounter); + var restCounters = test.Counters.Where(c => !(c.TopCounter || c.DefaultCounter)); + var counterWidth = Math.Max(test.Counters.Max(c => c.Name.Length) + 1, 15); + var resultWidth = Math.Max(test.Counters.Max(c => c.Results.Max().ToString("F3").Length + c.MetricName.Length) + 2, 15); + ret.AppendLine(test.Name); + ret.AppendLine($"{LeftJustify("Metric", counterWidth)}|{LeftJustify("Average",resultWidth)}|{LeftJustify("Min", resultWidth)}|{LeftJustify("Max",resultWidth)}"); + ret.AppendLine($"{new String('-', counterWidth)}|{new String('-', resultWidth)}|{new String('-', resultWidth)}|{new String('-', resultWidth)}"); + + + ret.AppendLine(Print(defaultCounter, counterWidth, resultWidth)); + foreach(var counter in topCounters) + { + ret.AppendLine(Print(counter, counterWidth, resultWidth)); + } + foreach (var counter in restCounters) + { + ret.AppendLine(Print(counter, counterWidth, resultWidth)); + } + } + return ret.ToString(); + } + private string Print(Counter counter, int counterWidth, int resultWidth) + { + string average = $"{counter.Results.Average():F3} {counter.MetricName}"; + string max = $"{counter.Results.Max():F3} {counter.MetricName}"; + string min = $"{counter.Results.Min():F3} {counter.MetricName}"; + return $"{LeftJustify(counter.Name, counterWidth)}|{LeftJustify(average, resultWidth)}|{LeftJustify(min, resultWidth)}|{LeftJustify(max, resultWidth)}"; + } + + private string LeftJustify(string str, int width) + { + return String.Format("{0,-" + width + "}", str); + } + + public bool InLab => environment.GetEnvironmentVariable("PERFLAB_INLAB")?.Equals("1") ?? false; + } +} diff --git a/test/perf/dotnet-tools/Reporting/Reporting.csproj b/test/perf/dotnet-tools/Reporting/Reporting.csproj new file mode 100644 index 00000000000..ca56fc51efc --- /dev/null +++ b/test/perf/dotnet-tools/Reporting/Reporting.csproj @@ -0,0 +1,13 @@ + + + + Library + netstandard2.0 + + + + + + + + diff --git a/test/perf/dotnet-tools/Reporting/Run.cs b/test/perf/dotnet-tools/Reporting/Run.cs new file mode 100644 index 00000000000..d39d30e5801 --- /dev/null +++ b/test/perf/dotnet-tools/Reporting/Run.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reporting +{ + public class Run + { + public bool Hidden { get; set; } + + public string CorrelationId { get; set; } + + public string PerfRepoHash { get; set; } + + public string Name { get; set; } + + public string Queue { get; set; } + public IDictionary Configurations { get; set; } = new Dictionary(); + } +} diff --git a/test/perf/dotnet-tools/Reporting/Test.cs b/test/perf/dotnet-tools/Reporting/Test.cs new file mode 100644 index 00000000000..e22529de2b1 --- /dev/null +++ b/test/perf/dotnet-tools/Reporting/Test.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. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reporting +{ + public class Test + { + public IList Categories { get; set; } = new List(); + + public string Name { get; set; } + public Dictionary AdditionalData { get; set; } = new Dictionary(); + + public IList Counters { get; set; } = new List(); + + public void AddCounter(Counter counter) + { + if (counter.DefaultCounter && Counters.Any(c => c.DefaultCounter)) + { + throw new Exception($"Duplicate default counter, name: ${counter.Name}"); + } + + if (Counters.Any(c => c.Name.Equals(counter.Name))) + { + throw new Exception($"Duplicate counter name, name: ${counter.Name}"); + } + + Counters.Add(counter); + } + + public void AddCounter(IEnumerable counters) + { + foreach (var counter in counters) + AddCounter(counter); + } + } +} diff --git a/test/perf/dotnet-tools/ResultsComparer/CommandLineOptions.cs b/test/perf/dotnet-tools/ResultsComparer/CommandLineOptions.cs new file mode 100644 index 00000000000..d3ad01be95b --- /dev/null +++ b/test/perf/dotnet-tools/ResultsComparer/CommandLineOptions.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using CommandLine; +using CommandLine.Text; + +namespace ResultsComparer +{ + public class CommandLineOptions + { + [Option("base", HelpText = "Path to the folder/file with base results.")] + public string BasePath { get; set; } + + [Option("diff", HelpText = "Path to the folder/file with diff results.")] + public string DiffPath { get; set; } + + [Option("threshold", Required = true, HelpText = "Threshold for Statistical Test. Examples: 5%, 10ms, 100ns, 1s.")] + public string StatisticalTestThreshold { get; set; } + + [Option("noise", HelpText = "Noise threshold for Statistical Test. The difference for 1.0ns and 1.1ns is 10%, but it's just a noise. Examples: 0.5ns 1ns.", Default = "0.3ns" )] + public string NoiseThreshold { get; set; } + + [Option("top", HelpText = "Filter the diff to top/bottom N results. Optional.")] + public int? TopCount { get; set; } + + [Option("csv", HelpText = "Path to exported CSV results. Optional.")] + public FileInfo CsvPath { get; set; } + + [Option("xml", HelpText = "Path to exported XML results. Optional.")] + public FileInfo XmlPath { get; set; } + + [Option('f', "filter", HelpText = "Filter the benchmarks by name using glob pattern(s). Optional.")] + public IEnumerable Filters { get; set; } + + [Usage(ApplicationAlias = "")] + public static IEnumerable Examples + { + get + { + yield return new Example(@"Compare the results stored in 'C:\results\win' (base) vs 'C:\results\unix' (diff) using 5% threshold.", + new CommandLineOptions { BasePath = @"C:\results\win", DiffPath = @"C:\results\unix", StatisticalTestThreshold = "5%" }); + yield return new Example(@"Compare the results stored in 'C:\results\win' (base) vs 'C:\results\unix' (diff) using 5% threshold and show only top/bottom 10 results.", + new CommandLineOptions { BasePath = @"C:\results\win", DiffPath = @"C:\results\unix", StatisticalTestThreshold = "5%", TopCount = 10 }); + yield return new Example(@"Compare the results stored in 'C:\results\win' (base) vs 'C:\results\unix' (diff) using 5% threshold and 0.5ns noise filter.", + new CommandLineOptions { BasePath = @"C:\results\win", DiffPath = @"C:\results\unix", StatisticalTestThreshold = "5%", NoiseThreshold = "0.5ns" }); + yield return new Example(@"Compare the System.Math benchmark results stored in 'C:\results\ubuntu16' (base) vs 'C:\results\ubuntu18' (diff) using 5% threshold.", + new CommandLineOptions { Filters = new[] { "System.Math*" }, BasePath = @"C:\results\win", DiffPath = @"C:\results\unix", StatisticalTestThreshold = "5%" }); + } + } + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/ResultsComparer/DataTransferContracts.cs b/test/perf/dotnet-tools/ResultsComparer/DataTransferContracts.cs new file mode 100644 index 00000000000..c3399ea4333 --- /dev/null +++ b/test/perf/dotnet-tools/ResultsComparer/DataTransferContracts.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// + +using System.Collections.Generic; +using System.Linq; + +namespace DataTransferContracts // generated with http://json2csharp.com/# +{ + public class ChronometerFrequency + { + public int Hertz { get; set; } + } + + public class HostEnvironmentInfo + { + public string BenchmarkDotNetCaption { get; set; } + public string BenchmarkDotNetVersion { get; set; } + public string OsVersion { get; set; } + public string ProcessorName { get; set; } + public int? PhysicalProcessorCount { get; set; } + public int? PhysicalCoreCount { get; set; } + public int? LogicalCoreCount { get; set; } + public string RuntimeVersion { get; set; } + public string Architecture { get; set; } + public bool? HasAttachedDebugger { get; set; } + public bool? HasRyuJit { get; set; } + public string Configuration { get; set; } + public string JitModules { get; set; } + public string DotNetCliVersion { get; set; } + public ChronometerFrequency ChronometerFrequency { get; set; } + public string HardwareTimerKind { get; set; } + } + + public class ConfidenceInterval + { + public int N { get; set; } + public double Mean { get; set; } + public double StandardError { get; set; } + public int Level { get; set; } + public double Margin { get; set; } + public double Lower { get; set; } + public double Upper { get; set; } + } + + public class Percentiles + { + public double P0 { get; set; } + public double P25 { get; set; } + public double P50 { get; set; } + public double P67 { get; set; } + public double P80 { get; set; } + public double P85 { get; set; } + public double P90 { get; set; } + public double P95 { get; set; } + public double P100 { get; set; } + } + + public class Statistics + { + public int N { get; set; } + public double Min { get; set; } + public double LowerFence { get; set; } + public double Q1 { get; set; } + public double Median { get; set; } + public double Mean { get; set; } + public double Q3 { get; set; } + public double UpperFence { get; set; } + public double Max { get; set; } + public double InterquartileRange { get; set; } + public List LowerOutliers { get; set; } + public List UpperOutliers { get; set; } + public List AllOutliers { get; set; } + public double StandardError { get; set; } + public double Variance { get; set; } + public double StandardDeviation { get; set; } + public double Skewness { get; set; } + public double Kurtosis { get; set; } + public ConfidenceInterval ConfidenceInterval { get; set; } + public Percentiles Percentiles { get; set; } + } + + public class Memory + { + public int Gen0Collections { get; set; } + public int Gen1Collections { get; set; } + public int Gen2Collections { get; set; } + public long TotalOperations { get; set; } + public long BytesAllocatedPerOperation { get; set; } + } + + public class Measurement + { + public string IterationStage { get; set; } + public int LaunchIndex { get; set; } + public int IterationIndex { get; set; } + public long Operations { get; set; } + public double Nanoseconds { get; set; } + } + + public class Benchmark + { + public string DisplayInfo { get; set; } + public object Namespace { get; set; } + public string Type { get; set; } + public string Method { get; set; } + public string MethodTitle { get; set; } + public string Parameters { get; set; } + public string FullName { get; set; } + public Statistics Statistics { get; set; } + public Memory Memory { get; set; } + public List Measurements { get; set; } + + /// + /// this method was not auto-generated by a tool, it was added manually + /// + /// an array of the actual workload results (not warmup, not pilot) + internal double[] GetOriginalValues() + => Measurements + .Where(measurement => measurement.IterationStage == "Result") + .Select(measurement => measurement.Nanoseconds / measurement.Operations) + .ToArray(); + } + + public class BdnResult + { + public string Title { get; set; } + public HostEnvironmentInfo HostEnvironmentInfo { get; set; } + public List Benchmarks { get; set; } + } +} \ No newline at end of file diff --git a/test/perf/dotnet-tools/ResultsComparer/Program.cs b/test/perf/dotnet-tools/ResultsComparer/Program.cs new file mode 100644 index 00000000000..655c9f3458e --- /dev/null +++ b/test/perf/dotnet-tools/ResultsComparer/Program.cs @@ -0,0 +1,290 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml; +using Perfolizer.Mathematics.Multimodality; +using Perfolizer.Mathematics.SignificanceTesting; +using Perfolizer.Mathematics.Thresholds; +using CommandLine; +using DataTransferContracts; +using MarkdownLog; +using Newtonsoft.Json; + +namespace ResultsComparer +{ + public class Program + { + private const string FullBdnJsonFileExtension = "full.json"; + + public static void Main(string[] args) + { + // we print a lot of numbers here and we want to make it always in invariant way + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + + Parser.Default.ParseArguments(args).WithParsed(Compare); + } + + private static void Compare(CommandLineOptions args) + { + if (!Threshold.TryParse(args.StatisticalTestThreshold, out var testThreshold)) + { + Console.WriteLine($"Invalid Threshold {args.StatisticalTestThreshold}. Examples: 5%, 10ms, 100ns, 1s."); + return; + } + if (!Threshold.TryParse(args.NoiseThreshold, out var noiseThreshold)) + { + Console.WriteLine($"Invalid Noise Threshold {args.NoiseThreshold}. Examples: 0.3ns 1ns."); + return; + } + + var notSame = GetNotSameResults(args, testThreshold, noiseThreshold).ToArray(); + + if (!notSame.Any()) + { + Console.WriteLine($"No differences found between the benchmark results with threshold {testThreshold}."); + return; + } + + PrintSummary(notSame); + + PrintTable(notSame, EquivalenceTestConclusion.Slower, args); + PrintTable(notSame, EquivalenceTestConclusion.Faster, args); + + ExportToCsv(notSame, args.CsvPath); + ExportToXml(notSame, args.XmlPath); + } + + private static IEnumerable<(string id, Benchmark baseResult, Benchmark diffResult, EquivalenceTestConclusion conclusion)> GetNotSameResults(CommandLineOptions args, Threshold testThreshold, Threshold noiseThreshold) + { + foreach ((string id, Benchmark baseResult, Benchmark diffResult) in ReadResults(args) + .Where(result => result.baseResult.Statistics != null && result.diffResult.Statistics != null)) // failures + { + var baseValues = baseResult.GetOriginalValues(); + var diffValues = diffResult.GetOriginalValues(); + + var userTresholdResult = StatisticalTestHelper.CalculateTost(MannWhitneyTest.Instance, baseValues, diffValues, testThreshold); + if (userTresholdResult.Conclusion == EquivalenceTestConclusion.Same) + continue; + + var noiseResult = StatisticalTestHelper.CalculateTost(MannWhitneyTest.Instance, baseValues, diffValues, noiseThreshold); + if (noiseResult.Conclusion == EquivalenceTestConclusion.Same) + continue; + + yield return (id, baseResult, diffResult, userTresholdResult.Conclusion); + } + } + + private static void PrintSummary((string id, Benchmark baseResult, Benchmark diffResult, EquivalenceTestConclusion conclusion)[] notSame) + { + var better = notSame.Where(result => result.conclusion == EquivalenceTestConclusion.Faster); + var worse = notSame.Where(result => result.conclusion == EquivalenceTestConclusion.Slower); + var betterCount = better.Count(); + var worseCount = worse.Count(); + + // If the baseline doesn't have the same set of tests, you wind up with Infinity in the list of diffs. + // Exclude them for purposes of geomean. + worse = worse.Where(x => GetRatio(x) != double.PositiveInfinity); + better = better.Where(x => GetRatio(x) != double.PositiveInfinity); + + Console.WriteLine("summary:"); + + if (betterCount > 0) + { + var betterGeoMean = Math.Pow(10, better.Skip(1).Aggregate(Math.Log10(GetRatio(better.First())), (x, y) => x + Math.Log10(GetRatio(y))) / better.Count()); + Console.WriteLine($"better: {betterCount}, geomean: {betterGeoMean:F3}"); + } + + if (worseCount > 0) + { + var worseGeoMean = Math.Pow(10, worse.Skip(1).Aggregate(Math.Log10(GetRatio(worse.First())), (x, y) => x + Math.Log10(GetRatio(y))) / worse.Count()); + Console.WriteLine($"worse: {worseCount}, geomean: {worseGeoMean:F3}"); + } + + Console.WriteLine($"total diff: {notSame.Length}"); + Console.WriteLine(); + } + + private static void PrintTable((string id, Benchmark baseResult, Benchmark diffResult, EquivalenceTestConclusion conclusion)[] notSame, EquivalenceTestConclusion conclusion, CommandLineOptions args) + { + var data = notSame + .Where(result => result.conclusion == conclusion) + .OrderByDescending(result => GetRatio(conclusion, result.baseResult, result.diffResult)) + .Take(args.TopCount ?? int.MaxValue) + .Select(result => new + { + Id = result.id.Length > 80 ? result.id.Substring(0, 80) : result.id, + DisplayValue = GetRatio(conclusion, result.baseResult, result.diffResult), + BaseMedian = result.baseResult.Statistics.Median, + DiffMedian = result.diffResult.Statistics.Median, + Modality = GetModalInfo(result.baseResult) ?? GetModalInfo(result.diffResult) + }) + .ToArray(); + + if (!data.Any()) + { + Console.WriteLine($"No {conclusion} results for the provided threshold = {args.StatisticalTestThreshold} and noise filter = {args.NoiseThreshold}."); + Console.WriteLine(); + return; + } + + var table = data.ToMarkdownTable().WithHeaders(conclusion.ToString(), conclusion == EquivalenceTestConclusion.Faster ? "base/diff" : "diff/base", "Base Median (ns)", "Diff Median (ns)", "Modality"); + + foreach (var line in table.ToMarkdown().Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)) + Console.WriteLine($"| {line.TrimStart()}|"); // the table starts with \t and does not end with '|' and it looks bad so we fix it + + Console.WriteLine(); + } + + private static IEnumerable<(string id, Benchmark baseResult, Benchmark diffResult)> ReadResults(CommandLineOptions args) + { + var baseFiles = GetFilesToParse(args.BasePath); + var diffFiles = GetFilesToParse(args.DiffPath); + + if (!baseFiles.Any() || !diffFiles.Any()) + throw new ArgumentException($"Provided paths contained no {FullBdnJsonFileExtension} files."); + + var baseResults = baseFiles.Select(ReadFromFile); + var diffResults = diffFiles.Select(ReadFromFile); + + var filters = args.Filters.Select(pattern => new Regex(WildcardToRegex(pattern), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToArray(); + + var benchmarkIdToDiffResults = diffResults + .SelectMany(result => result.Benchmarks) + .Where(benchmarkResult => !filters.Any() || filters.Any(filter => filter.IsMatch(benchmarkResult.FullName))) + .ToDictionary(benchmarkResult => benchmarkResult.FullName, benchmarkResult => benchmarkResult); + + return baseResults + .SelectMany(result => result.Benchmarks) + .ToDictionary(benchmarkResult => benchmarkResult.FullName, benchmarkResult => benchmarkResult) // we use ToDictionary to make sure the results have unique IDs + .Where(baseResult => benchmarkIdToDiffResults.ContainsKey(baseResult.Key)) + .Select(baseResult => (baseResult.Key, baseResult.Value, benchmarkIdToDiffResults[baseResult.Key])); + } + + private static void ExportToCsv((string id, Benchmark baseResult, Benchmark diffResult, EquivalenceTestConclusion conclusion)[] notSame, FileInfo csvPath) + { + if (csvPath == null) + return; + + if (csvPath.Exists) + csvPath.Delete(); + + using (var textWriter = csvPath.CreateText()) + { + foreach (var (id, baseResult, diffResult, conclusion) in notSame) + { + textWriter.WriteLine($"\"{id.Replace("\"", "\"\"")}\";base;{conclusion};{string.Join(';', baseResult.GetOriginalValues())}"); + textWriter.WriteLine($"\"{id.Replace("\"", "\"\"")}\";diff;{conclusion};{string.Join(';', diffResult.GetOriginalValues())}"); + } + } + + Console.WriteLine($"CSV results exported to {csvPath.FullName}"); + } + + private static void ExportToXml((string id, Benchmark baseResult, Benchmark diffResult, EquivalenceTestConclusion conclusion)[] notSame, FileInfo xmlPath) + { + if (xmlPath == null) + { + Console.WriteLine("No file given"); + return; + } + + if (xmlPath.Exists) + xmlPath.Delete(); + + using (XmlWriter writer = XmlWriter.Create(xmlPath.Open(FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))) + { + writer.WriteStartElement("performance-tests"); + foreach (var (id, baseResult, diffResult, conclusion) in notSame.Where(x => x.conclusion == EquivalenceTestConclusion.Slower)) + { + writer.WriteStartElement("test"); + writer.WriteAttributeString("name", id); + writer.WriteAttributeString("type", baseResult.Type); + writer.WriteAttributeString("method", baseResult.Method); + writer.WriteAttributeString("time", "0"); + writer.WriteAttributeString("result", "Fail"); + writer.WriteStartElement("failure"); + writer.WriteAttributeString("exception-type", "Regression"); + writer.WriteElementString("message", $"{id} has regressed, was {baseResult.Statistics.Median} is {diffResult.Statistics.Median}."); + writer.WriteEndElement(); + } + + foreach (var (id, baseResult, diffResult, conclusion) in notSame.Where(x => x.conclusion == EquivalenceTestConclusion.Faster)) + { + writer.WriteStartElement("test"); + writer.WriteAttributeString("name", id); + writer.WriteAttributeString("type", baseResult.Type); + writer.WriteAttributeString("method", baseResult.Method); + writer.WriteAttributeString("time", "0"); + writer.WriteAttributeString("result", "Skip"); + writer.WriteElementString("reason", $"{id} has improved, was {baseResult.Statistics.Median} is {diffResult.Statistics.Median}."); + writer.WriteEndElement(); + } + + writer.WriteEndElement(); + writer.Flush(); + } + + Console.WriteLine($"XML results exported to {xmlPath.FullName}"); + } + + private static string[] GetFilesToParse(string path) + { + if (Directory.Exists(path)) + return Directory.GetFiles(path, $"*{FullBdnJsonFileExtension}", SearchOption.AllDirectories); + else if (File.Exists(path) || !path.EndsWith(FullBdnJsonFileExtension)) + return new[] { path }; + else + throw new FileNotFoundException($"Provided path does NOT exist or is not a {path} file", path); + } + + // code and magic values taken from BenchmarkDotNet.Analysers.MultimodalDistributionAnalyzer + // See http://www.brendangregg.com/FrequencyTrails/modes.html + private static string GetModalInfo(Benchmark benchmark) + { + if (benchmark.Statistics.N < 12) // not enough data to tell + return null; + + double mValue = MValueCalculator.Calculate(benchmark.GetOriginalValues()); + if (mValue > 4.2) + return "multimodal"; + else if (mValue > 3.2) + return "bimodal"; + else if (mValue > 2.8) + return "several?"; + + return null; + } + + private static double GetRatio((string id, Benchmark baseResult, Benchmark diffResult, EquivalenceTestConclusion conclusion) item) => GetRatio(item.conclusion, item.baseResult, item.diffResult); + + private static double GetRatio(EquivalenceTestConclusion conclusion, Benchmark baseResult, Benchmark diffResult) + => conclusion == EquivalenceTestConclusion.Faster + ? baseResult.Statistics.Median / diffResult.Statistics.Median + : diffResult.Statistics.Median / baseResult.Statistics.Median; + + private static BdnResult ReadFromFile(string resultFilePath) + { + try + { + return JsonConvert.DeserializeObject(File.ReadAllText(resultFilePath)); + } + catch (JsonSerializationException) + { + Console.WriteLine($"Exception while reading the {resultFilePath} file."); + + throw; + } + } + + // https://stackoverflow.com/a/6907849/5852046 not perfect but should work for all we need + private static string WildcardToRegex(string pattern) => $"^{Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".")}$"; + } +} diff --git a/test/perf/dotnet-tools/ResultsComparer/README.md b/test/perf/dotnet-tools/ResultsComparer/README.md new file mode 100644 index 00000000000..109ba901422 --- /dev/null +++ b/test/perf/dotnet-tools/ResultsComparer/README.md @@ -0,0 +1,41 @@ +# Results Comparer + +This simple tool allows for easy comparison of provided benchmark results. + +It can be used to compare: +* historical results (eg. before and after my changes) +* results for different OSes (eg. Windows vs Ubuntu) +* results for different CPU architectures (eg. x64 vs ARM64) +* results for different target frameworks (eg. .NET Core 3.1 vs 5.0) + +All you need to provide is: +* `--base` - path to folder/file with baseline results +* `--diff` - path to folder/file with diff results +* `--threshold` - threshold for Statistical Test. Examples: 5%, 10ms, 100ns, 1s + +Optional arguments: +* `--top` - filter the diff to top/bottom `N` results +* `--noise` - noise threshold for Statistical Test. The difference for 1.0ns and 1.1ns is 10%, but it's just a noise. Examples: 0.5ns 1ns. The default value is 0.3ns. +* `--csv` - path to exported CSV results. Optional. +* `-f|--filter` - filter the benchmarks by name using glob pattern(s). Optional. + +Sample: compare the results stored in `C:\results\windows` vs `C:\results\ubuntu` using `1%` threshold and print only TOP 10. + +```cmd +dotnet run --base "C:\results\windows" --diff "C:\results\ubuntu" --threshold 1% --top 10 +``` + +**Note**: the tool supports only `*full.json` results exported by BenchmarkDotNet. This exporter is enabled by default in this repository. + +## Sample results + +| Slower | diff/base | Base Median (ns) | Diff Median (ns) | Modality| +| --------------------------------------------------------------- | ---------:| ----------------:| ----------------:| -------:| +| PerfLabTests.BlockCopyPerf.CallBlockCopy(numElements: 100) | 1.60 | 9.22 | 14.76 | | +| System.Tests.Perf_String.Trim_CharArr(s: "Test", c: [' ', ' ']) | 1.41 | 6.18 | 8.72 | | + +| Faster | base/diff | Base Median (ns) | Diff Median (ns) | Modality| +| ----------------------------------- | ---------:| ----------------:| ----------------:| -------:| +| System.Tests.Perf_Array.ArrayCopy3D | 1.31 | 372.71 | 284.73 | | + +If there is no difference or if there is no match (we use full benchmark names to match the benchmarks), then the results are omitted. diff --git a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj new file mode 100644 index 00000000000..9934bd0df21 --- /dev/null +++ b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj @@ -0,0 +1,15 @@ + + + Exe + $(PERFLAB_TARGET_FRAMEWORKS) + net5.0 + latest + + + + + + + + + diff --git a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.sln b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.sln new file mode 100644 index 00000000000..951a4d0fb5d --- /dev/null +++ b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResultsComparer", "ResultsComparer.csproj", "{00859394-44F8-466B-8624-41578CA94009}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {00859394-44F8-466B-8624-41578CA94009}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00859394-44F8-466B-8624-41578CA94009}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00859394-44F8-466B-8624-41578CA94009}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00859394-44F8-466B-8624-41578CA94009}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/test/perf/nuget.config b/test/perf/nuget.config new file mode 100644 index 00000000000..e8b7ac6770f --- /dev/null +++ b/test/perf/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/perf/perf.psm1 b/test/perf/perf.psm1 new file mode 100644 index 00000000000..a623a2290dd --- /dev/null +++ b/test/perf/perf.psm1 @@ -0,0 +1,213 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$repoRoot = git rev-parse --show-toplevel +Import-Module "$repoRoot/build.psm1" + +function Start-Benchmarking +{ + <# + .SYNOPSIS + Start a benchmark run. + + .PARAMETER TargetPSVersion + The version of 'Microsoft.PowerShell.SDK' package that we want the benchmark to target. + The supported versions are 7.0.x and above, including preview versions. + + .PARAMETER TargetFramework + The target framework to run benchmarks against. + + .PARAMETER List + List the available benchmarks, in either 'flat' or 'tree' views. + + .PARAMETER Runtime + Run benchmarks against multiple .NET runtimes. + + .PARAMETER Filter + One or more wildcard patterns to filter the benchmarks to be executed or to be listed. + + .PARAMETER Artifacts + Path to the folder where you want to store the artifacts produced from running benchmarks. + + .PARAMETER KeepFiles + Indicates to keep all temporary files produced for running benchmarks. + #> + [CmdletBinding(DefaultParameterSetName = 'Default')] + param( + [Parameter(ParameterSetName = 'Default')] + [ValidatePattern( + '^7\.(0|1|2)\.\d+(-preview\.\d{1,2})?$', + ErrorMessage = 'The package version is invalid or not supported')] + [string] $TargetPSVersion, + + [Parameter(ParameterSetName = 'Default')] + [ValidateSet('netcoreapp3.1', 'net5.0', 'net6.0')] + [string] $TargetFramework = 'net6.0', + + [Parameter(ParameterSetName = 'Default')] + [ValidateSet('flat', 'tree')] + [string] $List, + + [Parameter(Mandatory, ParameterSetName = 'Runtimes')] + [ValidateSet('netcoreapp3.1', 'net5.0', 'net6.0')] + [string[]] $Runtime, + + [string[]] $Filter = '*', + [string] $Artifacts, + [switch] $KeepFiles + ) + + Begin { + Find-Dotnet + + if ($Artifacts) { + $Artifacts = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Artifacts) + } else { + $Artifacts = Join-Path $PSScriptRoot 'BenchmarkDotNet.Artifacts' + } + + if (Test-Path -Path $Artifacts) { + Remove-Item -Path $Artifacts -Recurse -Force -ErrorAction Stop + } + + if ($TargetPSVersion) { + ## Validate the specified 'TargetPSVersion' and 'TargetFramework' are compatible. + $TargetFramework -match '\d.\d' > $null + $targetDotNetVersion = $Matches[0] + + $minimalDotNetVersion = switch -Wildcard ($TargetPSVersion) { + '7.0.*' { '3.1' } + '7.1.*' { '5.0' } + '7.2.*' { '6.0' } + } + + if ($targetDotNetVersion -lt $minimalDotNetVersion) { + $dotnetVer = $minimalDotNetVersion -eq '3.1' ? 'netcoreapp3.1' : "net$minimalDotNetVersion" + throw "The '$TargetPSVersion' version of 'Microsoft.PowerShell.SDK' requires '$dotnetVer' as the minimal target framework." + } + } + + if ($Runtime) { + ## Remove duplicate values. + $hash = [ordered]@{} + foreach ($item in $Runtime) { + if (-not $hash.Contains($item)) { + $hash.Add($item, $null) + } + } + $Runtime = $hash.Keys + } + } + + End { + try { + Push-Location -Path "$PSScriptRoot/benchmarks" + $savedOFS = $OFS; $OFS = $null + + ## Aggregate BDN arguments. + $runArgs = @('--filter') + foreach ($entry in $Filter) { $runArgs += $entry } + $runArgs += '--artifacts', $Artifacts + $runArgs += '--envVars', 'POWERSHELL_TELEMETRY_OPTOUT:1' + + if ($List) { $runArgs += '--list', $List } + if ($KeepFiles) { $runArgs += "--keepFiles" } + + if ($PSCmdlet.ParameterSetName -eq 'Default') { + if ($TargetPSVersion) { + Write-Log -message "Run benchmarks targeting '$TargetFramework' and the 'Microsoft.PowerShell.SDK' version '$TargetPSVersion' ..." + $env:PERF_TARGET_VERSION = $TargetPSVersion + } + elseif ($TargetFramework -eq 'net6.0') { + Write-Log -message "Run benchmarks targeting the current PowerShell code base ..." + } + else { + Write-Log -message "Run benchmarks targeting '$TargetFramework' and the corresponding latest version of 'Microsoft.PowerShell.SDK' ..." + } + + ## Use 'Release' instead of 'release' (note the capital case) because BDN uses 'Release' when building the auto-generated + ## project, and MSBuild somehow recognizes 'release' and 'Release' as two different configurations and thus will rebuild + ## all dependencies unnecessarily. + dotnet run -c Release -f $TargetFramework $runArgs + } + else { + Write-Log -message "Run benchmarks targeting multiple .NET runtimes: $Runtime ..." + dotnet run -c Release -f net6.0 --runtimes $Runtime $runArgs + } + + if (Test-Path $Artifacts) { + Write-Log -message "`nBenchmark artifacts can be found at $Artifacts" + } + } + finally { + $OFS = $savedOFS + $env:PERF_TARGET_VERSION = $null + Pop-Location + } + } +} + +function Compare-BenchmarkResult +{ + <# + .SYNOPSIS + Compare two benchmark run results to find possible regressions. + + When running benchmarks with 'Start-Benchmarking', you can define the result folder + where to save the artifacts by specifying '-Artifacts'. + + To compare two benchmark run results, you need to specify the result folder paths + for both runs, one as the base and one as the diff. + + .PARAMETER BaseResultPath + Path to the benchmark result used as baseline. + + .PARAMETER DiffResultPath + Path to the benchmark result to be compared with the baseline. + + .PARAMETER Threshold + Threshold for Statistical Test. Examples: 5%, 10ms, 100ns, 1s + + .PARAMETER Noise + Noise threshold for Statistical Test. + The difference for 1.0ns and 1.1ns is 10%, but it's really just noise. Examples: 0.5ns 1ns. + The default value is 0.3ns. + + .PARAMETER Top + Filter the diff to top `N` results + #> + param( + [Parameter(Mandatory)] + [string] $BaseResultPath, + + [Parameter(Mandatory)] + [string] $DiffResultPath, + + [Parameter(Mandatory)] + [ValidatePattern('^\d{1,2}%$|^\d+(ms|ns|s)$')] + [string] $Threshold, + + [ValidatePattern('^(\d\.)?\d+(ms|ns|s)$')] + [string] $Noise, + + [ValidateRange(1, 100)] + [int] $Top + ) + + Find-Dotnet + + try { + Push-Location -Path "$PSScriptRoot/dotnet-tools/ResultsComparer" + $savedOFS = $OFS; $OFS = $null + + $runArgs = @() + if ($Noise) { $runArgs += "--noise $Noise" } + if ($Top -gt 0) { $runArgs += "--top $Top" } + + dotnet run -c Release --base $BaseResultPath --diff $DiffResultPath --threshold $Threshold "$runArgs" + } + finally { + $OFS = $savedOFS + Pop-Location + } +} diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 77667306aed..01f58f06bf6 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -908,6 +908,75 @@ Describe "TabCompletion" -Tags CI { $res.CompletionMatches[0].CompletionText | Should -BeExactly "Test history completion" } + It "Test #requires parameter completion" { + $res = TabExpansion2 -inputScript "#requires -" -cursorColumn 11 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "Modules" + } + + It "Test #requires parameter value completion" { + $res = TabExpansion2 -inputScript "#requires -PSEdition " -cursorColumn 21 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "Core" + } + + It "Test no completion after #requires -RunAsAdministrator" { + $res = TabExpansion2 -inputScript "#requires -RunAsAdministrator -" -cursorColumn 31 + $res.CompletionMatches | Should -HaveCount 0 + } + + It "Test no suggestions for already existing parameters in #requires" { + $res = TabExpansion2 -inputScript "#requires -Modules -" -cursorColumn 20 + $res.CompletionMatches.CompletionText | Should -Not -Contain "Modules" + } + + It "Test module completion in #requires without quotes" { + $res = TabExpansion2 -inputScript "#requires -Modules P" -cursorColumn 20 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Contain "Pester" + } + + It "Test module completion in #requires with quotes" { + $res = TabExpansion2 -inputScript '#requires -Modules "' -cursorColumn 20 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Contain "Pester" + } + + It "Test module completion in #requires with multiple modules" { + $res = TabExpansion2 -inputScript "#requires -Modules Pester," -cursorColumn 26 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Contain "Pester" + } + + It "Test hashtable key completion in #requires statement for modules" { + $res = TabExpansion2 -inputScript "#requires -Modules @{" -cursorColumn 21 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "GUID" + } + + It "Test no suggestions for already existing hashtable keys in #requires statement for modules" { + $res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="Pester";' -cursorColumn 41 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Not -Contain "ModuleName" + } + + It "Test no suggestions for mutually exclusive hashtable keys in #requires statement for modules" { + $res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="Pester";RequiredVersion="1.0";' -cursorColumn 63 + $res.CompletionMatches.CompletionText | Should -BeExactly "GUID" + } + + It "Test no suggestions for RequiredVersion key in #requires statement when ModuleVersion is specified" { + $res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="Pester";ModuleVersion="1.0";' -cursorColumn 61 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Not -Contain "RequiredVersion" + } + + It "Test module completion in #requires statement for hashtables" { + $res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="p' -cursorColumn 34 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Contain "Pester" + } + It "Test Attribute member completion" { $inputStr = "function bar { [parameter(]param() }" $res = TabExpansion2 -inputScript $inputStr -cursorColumn ($inputStr.IndexOf('(') + 1) @@ -1302,6 +1371,201 @@ dir -Recurse ` $res.CompletionMatches | Should -HaveCount $expectedCompletions $res.CompletionMatches[0].CompletionText | Should -BeExactly 'about_Splatting' } + It 'Should complete about help topic regardless of culture' { + try + { + ## Save original culture and temporarily set it to da-DK because there's no localized help for da-DK. + $OriginalCulture = [cultureinfo]::CurrentCulture + [cultureinfo]::CurrentCulture="da-DK" + + $res = TabExpansion2 -inputScript 'get-help about_spla' -cursorColumn 'get-help about_spla'.Length + $res.CompletionMatches | Should -HaveCount 1 + $res.CompletionMatches[0].CompletionText | Should -BeExactly 'about_Splatting' + } + finally + { + [cultureinfo]::CurrentCulture = $OriginalCulture + } + } + It '' -TestCases @( + @{ + Intent = 'Complete help keywords with minimum input' + Expected = @( + 'COMPONENT' + 'DESCRIPTION' + 'EXAMPLE' + 'EXTERNALHELP' + 'FORWARDHELPCATEGORY' + 'FORWARDHELPTARGETNAME' + 'FUNCTIONALITY' + 'INPUTS' + 'LINK' + 'NOTES' + 'OUTPUTS' + 'PARAMETER' + 'REMOTEHELPRUNSPACE' + 'ROLE' + 'SYNOPSIS' + ) + TestString = @' +<# +.^ +#> +'@ + } + @{ + Intent = 'Complete help keywords without duplicates' + Expected = $null + TestString = @' +<# +.SYNOPSIS +.S^ +#> +'@ + } + @{ + Intent = 'Complete help keywords with allowed duplicates' + Expected = 'PARAMETER' + TestString = @' +<# +.PARAMETER +.Paramet^ +#> +'@ + } + @{ + Intent = 'Complete help keyword FORWARDHELPTARGETNAME argument' + Expected = 'Get-ChildItem' + TestString = @' +<# +.FORWARDHELPTARGETNAME Get-Child^ +#> +'@ + } + @{ + Intent = 'Complete help keyword FORWARDHELPCATEGORY argument' + Expected = 'Cmdlet' + TestString = @' +<# +.FORWARDHELPCATEGORY C^ +#> +'@ + } + @{ + Intent = 'Complete help keyword REMOTEHELPRUNSPACE argument' + Expected = 'PSEdition' + TestString = @' +<# +.REMOTEHELPRUNSPACE PSEditi^ +#> +'@ + } + @{ + Intent = 'Complete help keyword EXTERNALHELP argument' + Expected = Join-Path $PSHOME "pwsh.xml" + TestString = @" +<# +.EXTERNALHELP $PSHOME\pwsh.^ +#> +"@ + } + @{ + Intent = 'Complete help keyword PARAMETER argument for script' + Expected = 'Param1' + TestString = @' +<# +.PARAMETER ^ +#> +param($Param1) +'@ + } + @{ + Intent = 'Complete help keyword PARAMETER argument for function with help inside' + Expected = 'param2' + TestString = @' +function MyFunction ($param1, $param2) +{ +<# +.PARAMETER param1 +.PARAMETER ^ +#> +} +'@ + } + @{ + Intent = 'Complete help keyword PARAMETER argument for function with help before it' + Expected = 'param1','param2' + TestString = @' +<# +.PARAMETER ^ +#> +function MyFunction ($param1, $param2) +{ +} +'@ + } + @{ + Intent = 'Complete help keyword PARAMETER argument for advanced function with help inside' + Expected = 'Param1' + TestString = @' +function Verb-Noun +{ +<# +.PARAMETER ^ +#> + [CmdletBinding()] + Param + ( + $Param1 + ) + + Begin + { + } + Process + { + } + End + { + } +} +'@ + } + @{ + Intent = 'Complete help keyword PARAMETER argument for nested function with help before it' + Expected = 'param3','param4' + TestString = @' +function MyFunction ($param1, $param2) +{ + <# + .PARAMETER ^ + #> + function MyFunction2 ($param3, $param4) + { + } +} +'@ + } + @{ + Intent = 'Not complete help keyword PARAMETER argument if following function is too far away' + Expected = $null + TestString = @' +<# +.PARAMETER ^ +#> + + +function MyFunction ($param1, $param2) +{ +} +'@ + } + ){ + param($Expected, $TestString) + $CursorIndex = $TestString.IndexOf('^') + $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1) + $res.CompletionMatches.CompletionText | Should -BeExactly $Expected + } } } diff --git a/test/powershell/Language/Scripting/NativeExecution/NativeCommandProcessor.Tests.ps1 b/test/powershell/Language/Scripting/NativeExecution/NativeCommandProcessor.Tests.ps1 index 95eb59857ae..8bef53a9523 100644 --- a/test/powershell/Language/Scripting/NativeExecution/NativeCommandProcessor.Tests.ps1 +++ b/test/powershell/Language/Scripting/NativeExecution/NativeCommandProcessor.Tests.ps1 @@ -43,6 +43,12 @@ Describe 'native commands with pipeline' -tags 'Feature' { $result[0] | Should -Match "pwsh" } } + + It 'native command should be killed when pipeline is disposed' -Skip:($IsWindows) { + $yes = (Get-Process 'yes' -ErrorAction Ignore).Count + yes | Select-Object -First 2 + (Get-Process 'yes' -ErrorAction Ignore).Count | Should -Be $yes + } } Describe "Native Command Processor" -tags "Feature" { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Remove-Item.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Remove-Item.Tests.ps1 index 3e8ccb8fa91..28c5ed6052d 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Remove-Item.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Remove-Item.Tests.ps1 @@ -1,170 +1,195 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. + Describe "Remove-Item" -Tags "CI" { - $testpath = $TestDrive - $testfile = "testfile.txt" - $testfilepath = Join-Path -Path $testpath -ChildPath $testfile + BeforeAll { + $testpath = $TestDrive + $testfile = "testfile.txt" + $testfilepath = Join-Path -Path $testpath -ChildPath $testfile + } + Context "File removal Tests" { - BeforeEach { - New-Item -Name $testfile -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force - Test-Path $testfilepath | Should -BeTrue - } + BeforeEach { + New-Item -Name $testfile -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force + Test-Path $testfilepath | Should -BeTrue + } - It "Should be able to be called on a regular file without error using the Path parameter" { - { Remove-Item -Path $testfilepath } | Should -Not -Throw + It "Should be able to be called on a regular file without error using the Path parameter" { + { Remove-Item -Path $testfilepath } | Should -Not -Throw - Test-Path $testfilepath | Should -BeFalse - } + Test-Path $testfilepath | Should -BeFalse + } - It "Should be able to be called on a file without the Path parameter" { - { Remove-Item $testfilepath } | Should -Not -Throw + It "Should be able to be called on a file without the Path parameter" { + { Remove-Item $testfilepath } | Should -Not -Throw - Test-Path $testfilepath | Should -BeFalse - } + Test-Path $testfilepath | Should -BeFalse + } - It "Should be able to call the rm alias" { - { rm $testfilepath } | Should -Not -Throw + It "Should be able to call the rm alias" { + { rm $testfilepath } | Should -Not -Throw - Test-Path $testfilepath | Should -BeFalse - } + Test-Path $testfilepath | Should -BeFalse + } - It "Should be able to call the del alias" { - { del $testfilepath } | Should -Not -Throw + It "Should be able to call the del alias" { + { del $testfilepath } | Should -Not -Throw - Test-Path $testfilepath | Should -BeFalse - } + Test-Path $testfilepath | Should -BeFalse + } - It "Should be able to call the erase alias" { - { erase $testfilepath } | Should -Not -Throw + It "Should be able to call the erase alias" { + { erase $testfilepath } | Should -Not -Throw - Test-Path $testfilepath | Should -BeFalse - } + Test-Path $testfilepath | Should -BeFalse + } + + It "Should be able to call the ri alias" { + { ri $testfilepath } | Should -Not -Throw + + Test-Path $testfilepath | Should -BeFalse + } - It "Should be able to call the ri alias" { - { ri $testfilepath } | Should -Not -Throw + It "Should not be able to remove a read-only document without using the force switch" { + # Set to read only + Set-ItemProperty -Path $testfilepath -Name IsReadOnly -Value $true - Test-Path $testfilepath | Should -BeFalse - } + # attempt to remove the file + { Remove-Item $testfilepath -ErrorAction SilentlyContinue } | Should -Not -Throw - It "Should not be able to remove a read-only document without using the force switch" { - # Set to read only - Set-ItemProperty -Path $testfilepath -Name IsReadOnly -Value $true + # validate + Test-Path $testfilepath | Should -BeTrue - # attempt to remove the file - { Remove-Item $testfilepath -ErrorAction SilentlyContinue } | Should -Not -Throw + # remove using the -force switch on the readonly object + Remove-Item $testfilepath -Force - # validate - Test-Path $testfilepath | Should -BeTrue + # Validate + Test-Path $testfilepath | Should -BeFalse + } + + It "Should be able to remove all files matching a regular expression with the include parameter" { + # Create multiple files with specific string + New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" + New-Item -Name file2.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" + New-Item -Name file3.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" + # Create a single file that does not match that string - already done in BeforeEach + + # Delete the specific string + Remove-Item (Join-Path -Path $testpath -ChildPath "*") -Include file*.txt + # validate that the string under test was deleted, and the nonmatching strings still exist + Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse + Test-Path (Join-Path -Path $testpath -ChildPath file2.txt) | Should -BeFalse + Test-Path (Join-Path -Path $testpath -ChildPath file3.txt) | Should -BeFalse + Test-Path $testfilepath | Should -BeTrue + + # Delete the non-matching strings + Remove-Item $testfilepath + + Test-Path $testfilepath | Should -BeFalse + } - # remove using the -force switch on the readonly object - Remove-Item $testfilepath -Force + It "Should be able to not remove any files matching a regular expression with the exclude parameter" { + # Create multiple files with specific string + New-Item -Name file1.wav -Path $testpath -ItemType "file" -Value "lorem ipsum" + New-Item -Name file2.wav -Path $testpath -ItemType "file" -Value "lorem ipsum" - # Validate - Test-Path $testfilepath | Should -BeFalse - } + # Create a single file that does not match that string + New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" - It "Should be able to remove all files matching a regular expression with the include parameter" { - # Create multiple files with specific string - New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" - New-Item -Name file2.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" - New-Item -Name file3.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" - # Create a single file that does not match that string - already done in BeforeEach + # Delete the specific string + Remove-Item (Join-Path -Path $testpath -ChildPath "file*") -Exclude *.wav -Include *.txt - # Delete the specific string - Remove-Item (Join-Path -Path $testpath -ChildPath "*") -Include file*.txt - # validate that the string under test was deleted, and the nonmatching strings still exist - Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse - Test-Path (Join-Path -Path $testpath -ChildPath file2.txt) | Should -BeFalse - Test-Path (Join-Path -Path $testpath -ChildPath file3.txt) | Should -BeFalse - Test-Path $testfilepath | Should -BeTrue + # validate that the string under test was deleted, and the nonmatching strings still exist + Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeTrue + Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeTrue + Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse - # Delete the non-matching strings - Remove-Item $testfilepath + # Delete the non-matching strings + Remove-Item (Join-Path -Path $testpath -ChildPath file1.wav) + Remove-Item (Join-Path -Path $testpath -ChildPath file2.wav) - Test-Path $testfilepath | Should -BeFalse - } + Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeFalse + Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeFalse + } + } - It "Should be able to not remove any files matching a regular expression with the exclude parameter" { - # Create multiple files with specific string - New-Item -Name file1.wav -Path $testpath -ItemType "file" -Value "lorem ipsum" - New-Item -Name file2.wav -Path $testpath -ItemType "file" -Value "lorem ipsum" + Context "Directory Removal Tests" { + BeforeAll { + $testdirectory = Join-Path -Path $testpath -ChildPath testdir + $testsubdirectory = Join-Path -Path $testdirectory -ChildPath subd + } - # Create a single file that does not match that string - New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum" + BeforeEach { + New-Item -Name "testdir" -Path $testpath -ItemType "directory" -Force - # Delete the specific string - Remove-Item (Join-Path -Path $testpath -ChildPath "file*") -Exclude *.wav -Include *.txt + Test-Path $testdirectory | Should -BeTrue + } - # validate that the string under test was deleted, and the nonmatching strings still exist - Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeTrue - Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeTrue - Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse + It "Should be able to remove a directory" { + { Remove-Item $testdirectory -ErrorAction Stop } | Should -Not -Throw - # Delete the non-matching strings - Remove-Item (Join-Path -Path $testpath -ChildPath file1.wav) - Remove-Item (Join-Path -Path $testpath -ChildPath file2.wav) + Test-Path $testdirectory | Should -BeFalse + } - Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeFalse - Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeFalse - } - } + It "Should be able to recursively delete subfolders" { + New-Item -Name "subd" -Path $testdirectory -ItemType "directory" + New-Item -Name $testfile -Path $testsubdirectory -ItemType "file" -Value "lorem ipsum" - Context "Directory Removal Tests" { - $testdirectory = Join-Path -Path $testpath -ChildPath testdir - $testsubdirectory = Join-Path -Path $testdirectory -ChildPath subd - BeforeEach { - New-Item -Name "testdir" -Path $testpath -ItemType "directory" -Force + $complexDirectory = Join-Path -Path $testsubdirectory -ChildPath $testfile + Test-Path $complexDirectory | Should -BeTrue - Test-Path $testdirectory | Should -BeTrue - } + { Remove-Item $testdirectory -Recurse -ErrorAction Stop } | Should -Not -Throw - It "Should be able to remove a directory" { - { Remove-Item $testdirectory } | Should -Not -Throw + Test-Path $testdirectory | Should -BeFalse + } - Test-Path $testdirectory | Should -BeFalse - } + It "Should be able to recursively delete a directory with a trailing backslash" { + New-Item -Name "subd" -Path $testdirectory -ItemType "directory" + New-Item -Name $testfile -Path $testsubdirectory -ItemType "file" -Value "lorem ipsum" - It "Should be able to recursively delete subfolders" { - New-Item -Name "subd" -Path $testdirectory -ItemType "directory" - New-Item -Name $testfile -Path $testsubdirectory -ItemType "file" -Value "lorem ipsum" + $complexDirectory = Join-Path -Path $testsubdirectory -ChildPath $testfile + Test-Path $complexDirectory | Should -BeTrue - $complexDirectory = Join-Path -Path $testsubdirectory -ChildPath $testfile - Test-Path $complexDirectory | Should -BeTrue + $testdirectoryWithBackSlash = Join-Path -Path $testdirectory -ChildPath ([IO.Path]::DirectorySeparatorChar) + Test-Path $testdirectoryWithBackSlash | Should -BeTrue - { Remove-Item $testdirectory -Recurse} | Should -Not -Throw + { Remove-Item $testdirectoryWithBackSlash -Recurse -ErrorAction Stop } | Should -Not -Throw - Test-Path $testdirectory | Should -BeFalse - } + Test-Path $testdirectoryWithBackSlash | Should -BeFalse + Test-Path $testdirectory | Should -BeFalse + } } Context "Alternate Data Streams should be supported on Windows" { - BeforeAll { - if (!$IsWindows) { - return - } - $fileName = "ADStest.txt" - $streamName = "teststream" - $dirName = "ADStestdir" - $fileContent =" This is file content." - $streamContent = "datastream content here" - $streamfile = Join-Path -Path $testpath -ChildPath $fileName - $streamdir = Join-Path -Path $testpath -ChildPath $dirName - - $null = New-Item -Path $streamfile -ItemType "File" -force - Add-Content -Path $streamfile -Value $fileContent - Add-Content -Path $streamfile -Stream $streamName -Value $streamContent - $null = New-Item -Path $streamdir -ItemType "Directory" -Force - Add-Content -Path $streamdir -Stream $streamName -Value $streamContent - } - It "Should completely remove a datastream from a file" -Skip:(!$IsWindows) { - Get-Item -Path $streamfile -Stream $streamName | Should -Not -BeNullOrEmpty - Remove-Item -Path $streamfile -Stream $streamName - Get-Item -Path $streamfile -Stream $streamName -ErrorAction SilentlyContinue | Should -BeNullOrEmpty - } - It "Should completely remove a datastream from a directory" -Skip:(!$IsWindows) { - Get-Item -Path $streamdir -Stream $streamName | Should -Not -BeNullOrEmpty - Remove-Item -Path $streamdir -Stream $streamName - Get-Item -Path $streamdir -Stream $streamname -ErrorAction SilentlyContinue | Should -BeNullOrEmpty - } + BeforeAll { + if (!$IsWindows) { + return + } + $fileName = "ADStest.txt" + $streamName = "teststream" + $dirName = "ADStestdir" + $fileContent =" This is file content." + $streamContent = "datastream content here" + $streamfile = Join-Path -Path $testpath -ChildPath $fileName + $streamdir = Join-Path -Path $testpath -ChildPath $dirName + + $null = New-Item -Path $streamfile -ItemType "File" -force + Add-Content -Path $streamfile -Value $fileContent + Add-Content -Path $streamfile -Stream $streamName -Value $streamContent + $null = New-Item -Path $streamdir -ItemType "Directory" -Force + Add-Content -Path $streamdir -Stream $streamName -Value $streamContent + } + + It "Should completely remove a datastream from a file" -Skip:(!$IsWindows) { + Get-Item -Path $streamfile -Stream $streamName | Should -Not -BeNullOrEmpty + Remove-Item -Path $streamfile -Stream $streamName + Get-Item -Path $streamfile -Stream $streamName -ErrorAction SilentlyContinue | Should -BeNullOrEmpty + } + + It "Should completely remove a datastream from a directory" -Skip:(!$IsWindows) { + Get-Item -Path $streamdir -Stream $streamName | Should -Not -BeNullOrEmpty + Remove-Item -Path $streamdir -Stream $streamName + Get-Item -Path $streamdir -Stream $streamname -ErrorAction SilentlyContinue | Should -BeNullOrEmpty + } } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Write-Progress.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Write-Progress.Tests.ps1 index f346b61f079..6e6c9ee57c4 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Write-Progress.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Write-Progress.Tests.ps1 @@ -22,4 +22,8 @@ Describe "Write-Progress DRT Unit Tests" -Tags "CI" { It "all params works" -Pending { { Write-Progress -Activity 'myactivity' -Status 'mystatus' -Id 1 -ParentId 2 -Completed:$false -current 'current' -sec 1 -percent 1 } | Should -Not -Throw } + + It 'Activity longer than console width works' { + { Write-Progress -Activity ('a' * ([console]::WindowWidth + 1)) -Status ('b' * ([console]::WindowWidth + 1)) -Id 1 } | Should -Not -Throw + } } diff --git a/test/tools/TestService/TestService.csproj b/test/tools/TestService/TestService.csproj index 54ba67d50db..da9976c3a42 100644 --- a/test/tools/TestService/TestService.csproj +++ b/test/tools/TestService/TestService.csproj @@ -13,7 +13,7 @@ - + diff --git a/test/tools/WebListener/Controllers/ResponseController.cs b/test/tools/WebListener/Controllers/ResponseController.cs index ed3d22188b0..cb014029b36 100644 --- a/test/tools/WebListener/Controllers/ResponseController.cs +++ b/test/tools/WebListener/Controllers/ResponseController.cs @@ -41,7 +41,7 @@ public string Index() } StringValues responsePhrase; - if ( Request.Query.TryGetValue("responsephrase", out responsePhrase)) + if (Request.Query.TryGetValue("responsephrase", out responsePhrase)) { Response.HttpContext.Features.Get().ReasonPhrase = responsePhrase.FirstOrDefault(); } diff --git a/test/tools/WebListener/DeflateFilter.cs b/test/tools/WebListener/DeflateFilter.cs index 7af94b23920..60d39c39d18 100644 --- a/test/tools/WebListener/DeflateFilter.cs +++ b/test/tools/WebListener/DeflateFilter.cs @@ -11,7 +11,7 @@ namespace mvc.Controllers { internal sealed class DeflateFilter : ResultFilterAttribute { - public override async Task OnResultExecutionAsync( ResultExecutingContext context, ResultExecutionDelegate next) + public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { var httpContext = context.HttpContext; using (var memoryStream = new MemoryStream()) diff --git a/test/tools/WebListener/GzipFilter.cs b/test/tools/WebListener/GzipFilter.cs index e015e0f682d..9eab3627b5c 100644 --- a/test/tools/WebListener/GzipFilter.cs +++ b/test/tools/WebListener/GzipFilter.cs @@ -11,7 +11,7 @@ namespace mvc.Controllers { internal sealed class GzipFilter : ResultFilterAttribute { - public override async Task OnResultExecutionAsync( ResultExecutingContext context, ResultExecutionDelegate next) + public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { var httpContext = context.HttpContext; using (var memoryStream = new MemoryStream()) diff --git a/test/tools/WebListener/WebListener.csproj b/test/tools/WebListener/WebListener.csproj index f874f262c86..02cf0993928 100644 --- a/test/tools/WebListener/WebListener.csproj +++ b/test/tools/WebListener/WebListener.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/test/xUnit/csharp/test_CommandLineParser.cs b/test/xUnit/csharp/test_CommandLineParser.cs index c57960797bf..41f501688f5 100644 --- a/test/xUnit/csharp/test_CommandLineParser.cs +++ b/test/xUnit/csharp/test_CommandLineParser.cs @@ -48,7 +48,15 @@ public static void TestDefaults() Assert.False(cpp.SkipProfiles); Assert.False(cpp.SocketServerMode); Assert.False(cpp.SSHServerMode); - Assert.True(cpp.StaMode); + if (Platform.IsWindows) + { + Assert.True(cpp.StaMode); + } + else + { + Assert.False(cpp.StaMode); + } + Assert.False(cpp.ThrowOnReadAndPrompt); Assert.False(cpp.WasInitialCommandEncoded); Assert.Null(cpp.WorkingDirectory); @@ -1226,7 +1234,15 @@ public static void TestParameter_LastParameterIsFileName_Exist(params string[] c Assert.False(cpp.NoExit); Assert.False(cpp.ShowShortHelp); Assert.False(cpp.ShowBanner); - Assert.True(cpp.StaMode); + if (Platform.IsWindows) + { + Assert.True(cpp.StaMode); + } + else + { + Assert.False(cpp.StaMode); + } + Assert.Equal(CommandLineParameterParser.NormalizeFilePath(commandLine[commandLine.Length - 1]), cpp.File); Assert.Null(cpp.ErrorMessage); } diff --git a/test/xUnit/csharp/test_NativeInterop.cs b/test/xUnit/csharp/test_NativeInterop.cs new file mode 100644 index 00000000000..685e8a23ce4 --- /dev/null +++ b/test/xUnit/csharp/test_NativeInterop.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using System.Management.Automation; +using Xunit; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; + +namespace PSTests.Sequential +{ + public static class NativeInterop + { + [Fact] + public static void TestLoadNativeInMemoryAssembly() + { + string tempDir = Path.Combine(Path.GetTempPath(), "TestLoadNativeInMemoryAssembly"); + string testDll = Path.Combine(tempDir, "test.dll"); + + if (!File.Exists(testDll)) + { + Directory.CreateDirectory(tempDir); + bool result = CreateTestDll(testDll); + Assert.True(result, "The call to 'CreateTestDll' should be successful and return true."); + Assert.True(File.Exists(testDll), "The test assembly should be created."); + } + + var asmName = AssemblyName.GetAssemblyName(testDll); + string asmFullName = SearchAssembly(asmName.Name); + Assert.Null(asmFullName); + + unsafe + { + int ret = LoadAssemblyTest(testDll); + Assert.Equal(0, ret); + } + + asmFullName = SearchAssembly(asmName.Name); + Assert.Equal(asmName.FullName, asmFullName); + } + + private static unsafe int LoadAssemblyTest(string assemblyPath) + { + // The 'LoadAssemblyFromNativeMemory' method is annotated with 'UnmanagedCallersOnly' attribute, + // so we have to use the 'unmanaged' function pointer to invoke it. + delegate* unmanaged funcPtr = &PowerShellUnsafeAssemblyLoad.LoadAssemblyFromNativeMemory; + + int length = 0; + IntPtr nativeMem = IntPtr.Zero; + + try + { + using (var fileStream = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read)) + { + length = (int)fileStream.Length; + nativeMem = Marshal.AllocHGlobal(length); + + using var unmanagedStream = new UnmanagedMemoryStream((byte*)nativeMem, length, length, FileAccess.Write); + fileStream.CopyTo(unmanagedStream); + } + + // Call the function pointer. + return funcPtr(nativeMem, length); + } + finally + { + // Free the native memory + Marshal.FreeHGlobal(nativeMem); + } + } + + private static string SearchAssembly(string assemblyName) + { + Assembly asm = AssemblyLoadContext.Default.Assemblies.FirstOrDefault( + assembly => assembly.FullName.StartsWith(assemblyName, StringComparison.OrdinalIgnoreCase)); + + return asm?.FullName; + } + + private static bool CreateTestDll(string dllPath) + { + var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest); + var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); + + List syntaxTrees = new(); + SourceText sourceText = SourceText.From("public class Utt { }"); + syntaxTrees.Add(CSharpSyntaxTree.ParseText(sourceText, parseOptions)); + + var refs = new List { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }; + Compilation compilation = CSharpCompilation.Create( + Path.GetRandomFileName(), + syntaxTrees: syntaxTrees, + references: refs, + options: compilationOptions); + + using var fs = new FileStream(dllPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); + EmitResult emitResult = compilation.Emit(peStream: fs, options: null); + return emitResult.Success; + } + } +} diff --git a/test/xUnit/csharp/test_Prediction.cs b/test/xUnit/csharp/test_Prediction.cs index 8f018ebb9e1..12e61dcd848 100644 --- a/test/xUnit/csharp/test_Prediction.cs +++ b/test/xUnit/csharp/test_Prediction.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Management.Automation.Language; using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.Prediction; using System.Threading; using Xunit; @@ -18,6 +19,8 @@ public class MyPredictor : ICommandPredictor public List History { get; } + public List Results { get; } + public List AcceptedSuggestions { get; } public List DisplayedSuggestions { get; } @@ -47,39 +50,30 @@ private MyPredictor(Guid id, string name, string description, bool delay) _delay = delay; History = new List(); + Results = new List(); AcceptedSuggestions = new List(); DisplayedSuggestions = new List(); } + public void Clear() + { + History.Clear(); + Results.Clear(); + AcceptedSuggestions.Clear(); + DisplayedSuggestions.Clear(); + } + + #region "Interface implementation" + public Guid Id => _id; public string Name => _name; public string Description => _description; - bool ICommandPredictor.SupportEarlyProcessing => true; + bool ICommandPredictor.CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback) => true; - bool ICommandPredictor.AcceptFeedback => true; - - public void StartEarlyProcessing(string clientId, IReadOnlyList history) - { - foreach (string item in history) - { - History.Add($"{clientId}-{item}"); - } - } - - public void OnSuggestionDisplayed(string clientId, uint session, int countOrIndex) - { - DisplayedSuggestions.Add($"{clientId}-{session}-{countOrIndex}"); - } - - public void OnSuggestionAccepted(string clientId, uint session, string acceptedSuggestion) - { - AcceptedSuggestions.Add($"{clientId}-{session}-{acceptedSuggestion}"); - } - - public SuggestionPackage GetSuggestion(string clientId, PredictionContext context, CancellationToken cancellationToken) + public SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken) { if (_delay) { @@ -92,18 +86,44 @@ public SuggestionPackage GetSuggestion(string clientId, PredictionContext contex var userInput = context.InputAst.Extent.Text; var entries = new List { - new PredictiveSuggestion($"'{userInput}' from '{clientId}' - TEST-1 from {Name}"), - new PredictiveSuggestion($"'{userInput}' from '{clientId}' - TeSt-2 from {Name}"), + new PredictiveSuggestion($"'{userInput}' from '{client.Name}' - TEST-1 from {Name}"), + new PredictiveSuggestion($"'{userInput}' from '{client.Name}' - TeSt-2 from {Name}"), }; return new SuggestionPackage(56, entries); } + + public void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex) + { + DisplayedSuggestions.Add($"{client.Name}-{session}-{countOrIndex}"); + } + + public void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion) + { + AcceptedSuggestions.Add($"{client.Name}-{session}-{acceptedSuggestion}"); + } + + public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history) + { + foreach (string item in history) + { + History.Add($"{client.Name}-{item}"); + } + } + + public void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) + { + Results.Add($"{client.Name}-{commandLine}-{success}"); + } + + #endregion } public static class CommandPredictionTests { private const string Client = "PredictionTest"; private const uint Session = 56; + private static PredictionClient predClient = new(Client, PredictionClientKind.Terminal); [Fact] public static void PredictInput() @@ -114,7 +134,7 @@ public static void PredictInput() Ast ast = Parser.ParseInput(Input, out Token[] tokens, out _); // Returns null when no predictor implementation registered - List results = CommandPrediction.PredictInput(Client, ast, tokens).Result; + List results = CommandPrediction.PredictInputAsync(predClient, ast, tokens).Result; Assert.Null(results); try @@ -127,7 +147,7 @@ public static void PredictInput() // cannot finish before the specified timeout. // The specified timeout is exaggerated to make the test reliable. // xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small. - results = CommandPrediction.PredictInput(Client, ast, tokens, millisecondsTimeout: 1000).Result; + results = CommandPrediction.PredictInputAsync(predClient, ast, tokens, millisecondsTimeout: 1000).Result; Assert.Single(results); PredictionResult res = results[0]; @@ -140,7 +160,7 @@ public static void PredictInput() // Expect the results from both 'slow' and 'fast' predictors // Same here -- the specified timeout is exaggerated to make the test reliable. // xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small. - results = CommandPrediction.PredictInput(Client, ast, tokens, millisecondsTimeout: 4000).Result; + results = CommandPrediction.PredictInputAsync(predClient, ast, tokens, millisecondsTimeout: 4000).Result; Assert.Equal(2, results.Count); PredictionResult res1 = results[0]; @@ -170,6 +190,9 @@ public static void Feedback() MyPredictor slow = MyPredictor.SlowPredictor; MyPredictor fast = MyPredictor.FastPredictor; + slow.Clear(); + fast.Clear(); + try { // Register 2 predictor implementations @@ -179,16 +202,19 @@ public static void Feedback() var history = new[] { "hello", "world" }; var ids = new HashSet { slow.Id, fast.Id }; - CommandPrediction.OnCommandLineAccepted(Client, history); - CommandPrediction.OnSuggestionDisplayed(Client, slow.Id, Session, 2); - CommandPrediction.OnSuggestionDisplayed(Client, fast.Id, Session, -1); - CommandPrediction.OnSuggestionAccepted(Client, slow.Id, Session, "Yeah"); - - // The calls to 'StartEarlyProcessing' and 'OnSuggestionAccepted' are queued in thread pool, - // so we wait a bit to make sure the calls are done. - while (slow.History.Count == 0 || slow.AcceptedSuggestions.Count == 0) + CommandPrediction.OnCommandLineAccepted(predClient, history); + CommandPrediction.OnCommandLineExecuted(predClient, "last_input", true); + CommandPrediction.OnSuggestionDisplayed(predClient, slow.Id, Session, 2); + CommandPrediction.OnSuggestionDisplayed(predClient, fast.Id, Session, -1); + CommandPrediction.OnSuggestionAccepted(predClient, slow.Id, Session, "Yeah"); + + // The feedback calls are queued in thread pool, so let's wait a bit to make sure the calls are done. + while (slow.History.Count == 0 || fast.History.Count == 0 || + slow.Results.Count == 0 || fast.Results.Count == 0 || + slow.DisplayedSuggestions.Count == 0 || fast.DisplayedSuggestions.Count == 0 || + slow.AcceptedSuggestions.Count == 0) { - Thread.Sleep(10); + Thread.Sleep(100); } Assert.Equal(2, slow.History.Count); @@ -199,6 +225,12 @@ public static void Feedback() Assert.Equal($"{Client}-{history[0]}", fast.History[0]); Assert.Equal($"{Client}-{history[1]}", fast.History[1]); + Assert.Single(slow.Results); + Assert.Equal($"{Client}-last_input-True", slow.Results[0]); + + Assert.Single(fast.Results); + Assert.Equal($"{Client}-last_input-True", fast.Results[0]); + Assert.Single(slow.DisplayedSuggestions); Assert.Equal($"{Client}-{Session}-2", slow.DisplayedSuggestions[0]); diff --git a/test/xUnit/csharp/test_Subsystem.cs b/test/xUnit/csharp/test_Subsystem.cs index f2ca308221f..d5bc3b9c728 100644 --- a/test/xUnit/csharp/test_Subsystem.cs +++ b/test/xUnit/csharp/test_Subsystem.cs @@ -4,6 +4,8 @@ using System; using System.Collections.ObjectModel; using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.DSC; +using System.Management.Automation.Subsystem.Prediction; using System.Threading; using Xunit; @@ -19,8 +21,7 @@ static SubsystemTests() predictor2 = MyPredictor.SlowPredictor; } - // This method needs to be updated when there are more than 1 subsystem defined. - private static void VerifySubsystemMetadata(SubsystemInfo ssInfo) + private static void VerifyCommandPredictorMetadata(SubsystemInfo ssInfo) { Assert.Equal(SubsystemKind.CommandPredictor, ssInfo.Kind); Assert.Equal(typeof(ICommandPredictor), ssInfo.SubsystemType); @@ -30,26 +31,51 @@ private static void VerifySubsystemMetadata(SubsystemInfo ssInfo) Assert.Empty(ssInfo.RequiredFunctions); } + private static void VerifyCrossPlatformDscMetadata(SubsystemInfo ssInfo) + { + Assert.Equal(SubsystemKind.CrossPlatformDsc, ssInfo.Kind); + Assert.Equal(typeof(ICrossPlatformDsc), ssInfo.SubsystemType); + Assert.True(ssInfo.AllowUnregistration); + Assert.False(ssInfo.AllowMultipleRegistration); + Assert.Empty(ssInfo.RequiredCmdlets); + Assert.Empty(ssInfo.RequiredFunctions); + } + [Fact] public static void GetSubsystemInfo() { - SubsystemInfo ssInfo = SubsystemManager.GetSubsystemInfo(typeof(ICommandPredictor)); + SubsystemInfo predictorInfo = SubsystemManager.GetSubsystemInfo(typeof(ICommandPredictor)); - VerifySubsystemMetadata(ssInfo); - Assert.False(ssInfo.IsRegistered); - Assert.Empty(ssInfo.Implementations); + VerifyCommandPredictorMetadata(predictorInfo); + Assert.False(predictorInfo.IsRegistered); + Assert.Empty(predictorInfo.Implementations); - SubsystemInfo ssInfo2 = SubsystemManager.GetSubsystemInfo(SubsystemKind.CommandPredictor); - Assert.Same(ssInfo2, ssInfo); + SubsystemInfo predictorInfo2 = SubsystemManager.GetSubsystemInfo(SubsystemKind.CommandPredictor); + Assert.Same(predictorInfo2, predictorInfo); - ReadOnlyCollection ssInfos = SubsystemManager.GetAllSubsystemInfo(); - Assert.Single(ssInfos); - Assert.Same(ssInfos[0], ssInfo); + SubsystemInfo crossPlatformDscInfo = SubsystemManager.GetSubsystemInfo(typeof(ICrossPlatformDsc)); - ICommandPredictor impl = SubsystemManager.GetSubsystem(); - Assert.Null(impl); - ReadOnlyCollection impls = SubsystemManager.GetSubsystems(); - Assert.Empty(impls); + VerifyCrossPlatformDscMetadata(crossPlatformDscInfo); + Assert.False(crossPlatformDscInfo.IsRegistered); + Assert.Empty(crossPlatformDscInfo.Implementations); + + SubsystemInfo crossPlatformDscInfo2 = SubsystemManager.GetSubsystemInfo(SubsystemKind.CrossPlatformDsc); + Assert.Same(crossPlatformDscInfo2, crossPlatformDscInfo); + + ReadOnlyCollection ssInfos = SubsystemManager.GetAllSubsystemInfo(); + Assert.Equal(2, ssInfos.Count); + Assert.Same(ssInfos[0], predictorInfo); + Assert.Same(ssInfos[1], crossPlatformDscInfo); + + ICommandPredictor predictorImpl = SubsystemManager.GetSubsystem(); + Assert.Null(predictorImpl); + ReadOnlyCollection predictorImpls = SubsystemManager.GetSubsystems(); + Assert.Empty(predictorImpls); + + ICrossPlatformDsc crossPlatformDscImpl = SubsystemManager.GetSubsystem(); + Assert.Null(crossPlatformDscImpl); + ReadOnlyCollection crossPlatformDscImpls = SubsystemManager.GetSubsystems(); + Assert.Empty(crossPlatformDscImpls); } [Fact] @@ -72,7 +98,8 @@ public static void RegisterSubsystem() // Now validate the SubsystemInfo of the 'ICommandPredictor' subsystem SubsystemInfo ssInfo = SubsystemManager.GetSubsystemInfo(typeof(ICommandPredictor)); - VerifySubsystemMetadata(ssInfo); + SubsystemInfo crossPlatformDscInfo = SubsystemManager.GetSubsystemInfo(typeof(ICrossPlatformDsc)); + VerifyCommandPredictorMetadata(ssInfo); Assert.True(ssInfo.IsRegistered); Assert.Single(ssInfo.Implementations); @@ -84,11 +111,6 @@ public static void RegisterSubsystem() Assert.Equal(SubsystemKind.CommandPredictor, implInfo.Kind); Assert.Same(typeof(MyPredictor), implInfo.ImplementationType); - // Now validate the all-subsystem-info collection. - ReadOnlyCollection ssInfos = SubsystemManager.GetAllSubsystemInfo(); - Assert.Single(ssInfos); - Assert.Same(ssInfos[0], ssInfo); - // Now validate the subsystem implementation itself. ICommandPredictor impl = SubsystemManager.GetSubsystem(); Assert.Same(impl, predictor1); @@ -97,8 +119,9 @@ public static void RegisterSubsystem() const string Client = "SubsystemTest"; const string Input = "Hello world"; + var predClient = new PredictionClient(Client, PredictionClientKind.Terminal); var predCxt = PredictionContext.Create(Input); - var results = impl.GetSuggestion(Client, predCxt, CancellationToken.None); + var results = impl.GetSuggestion(predClient, predCxt, CancellationToken.None); Assert.Equal($"'{Input}' from '{Client}' - TEST-1 from {impl.Name}", results.SuggestionEntries[0].SuggestionText); Assert.Equal($"'{Input}' from '{Client}' - TeSt-2 from {impl.Name}", results.SuggestionEntries[1].SuggestionText); @@ -111,7 +134,7 @@ public static void RegisterSubsystem() SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, predictor2); // Now validate the SubsystemInfo of the 'ICommandPredictor' subsystem - VerifySubsystemMetadata(ssInfo); + VerifyCommandPredictorMetadata(ssInfo); Assert.True(ssInfo.IsRegistered); Assert.Equal(2, ssInfo.Implementations.Count); @@ -156,7 +179,7 @@ public static void UnregisterSubsystem() SubsystemManager.UnregisterSubsystem(predictor1.Id); SubsystemInfo ssInfo = SubsystemManager.GetSubsystemInfo(SubsystemKind.CommandPredictor); - VerifySubsystemMetadata(ssInfo); + VerifyCommandPredictorMetadata(ssInfo); Assert.True(ssInfo.IsRegistered); Assert.Single(ssInfo.Implementations); @@ -177,7 +200,7 @@ public static void UnregisterSubsystem() // Unregister 'predictor2' SubsystemManager.UnregisterSubsystem(SubsystemKind.CommandPredictor, predictor2.Id); - VerifySubsystemMetadata(ssInfo); + VerifyCommandPredictorMetadata(ssInfo); Assert.False(ssInfo.IsRegistered); Assert.Empty(ssInfo.Implementations); diff --git a/tools/metadata.json b/tools/metadata.json index ff35ea34385..204e289b9c8 100644 --- a/tools/metadata.json +++ b/tools/metadata.json @@ -1,9 +1,9 @@ { "StableReleaseTag": "v7.1.3", - "PreviewReleaseTag": "v7.2.0-preview.4", + "PreviewReleaseTag": "v7.2.0-preview.5", "ServicingReleaseTag": "v7.0.6", "ReleaseTag": "v7.1.3", "LTSReleaseTag" : ["v7.0.6"], - "NextReleaseTag": "v7.2.0-preview.5", + "NextReleaseTag": "v7.2.0-preview.6", "LTSRelease": false } diff --git a/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index 698f3e620aa..b9ffef2c91b 100644 --- a/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -14,7 +14,7 @@ - + diff --git a/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj b/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj index cbd93691aa1..2228c06a4cc 100644 --- a/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj +++ b/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj @@ -9,7 +9,7 @@ - - + + diff --git a/tools/releaseBuild/azureDevOps/templates/SetVersionVariables.yml b/tools/releaseBuild/azureDevOps/templates/SetVersionVariables.yml index 0b8e5f42417..4376c4a1e58 100644 --- a/tools/releaseBuild/azureDevOps/templates/SetVersionVariables.yml +++ b/tools/releaseBuild/azureDevOps/templates/SetVersionVariables.yml @@ -2,8 +2,17 @@ parameters: ReleaseTagVar: v6.2.0 ReleaseTagVarName: ReleaseTagVar CreateJson: 'no' + UseJson: 'yes' steps: +- ${{ if eq(parameters['UseJson'],'yes') }}: + - task: DownloadBuildArtifacts@0 + inputs: + artifactName: 'BuildInfoJson' + itemPattern: '**/*.json' + downloadPath: '$(System.ArtifactsDirectory)' + displayName: Download Build Info Json + - powershell: | $path = "./build.psm1" @@ -25,7 +34,7 @@ steps: displayName: 'Set repo Root' - powershell: | - $createJson = ("${{ parameters.ReleaseTagVarName }}" -ne "no") + $createJson = ("${{ parameters.CreateJson }}" -ne "no") $releaseTag = & "$env:REPOROOT/tools/releaseBuild/setReleaseTag.ps1" -ReleaseTag ${{ parameters.ReleaseTagVar }} -Variable "${{ parameters.ReleaseTagVarName }}" -CreateJson:$createJson $version = $releaseTag.Substring(1) $vstsCommandString = "vso[task.setvariable variable=Version]$version" diff --git a/tools/releaseBuild/azureDevOps/templates/checkAzureContainer.yml b/tools/releaseBuild/azureDevOps/templates/checkAzureContainer.yml index ea8f1ff2abf..9000d42bd61 100644 --- a/tools/releaseBuild/azureDevOps/templates/checkAzureContainer.yml +++ b/tools/releaseBuild/azureDevOps/templates/checkAzureContainer.yml @@ -12,9 +12,12 @@ jobs: steps: - checkout: self clean: true + - template: SetVersionVariables.yml parameters: ReleaseTagVar: $(ReleaseTagVar) + CreateJson: yes + UseJson: no - task: AzurePowerShell@4 inputs: diff --git a/tools/releaseBuild/azureDevOps/templates/release-MsixBundle.yml b/tools/releaseBuild/azureDevOps/templates/release-MsixBundle.yml index bab273e96da..93bbbcc06d1 100644 --- a/tools/releaseBuild/azureDevOps/templates/release-MsixBundle.yml +++ b/tools/releaseBuild/azureDevOps/templates/release-MsixBundle.yml @@ -66,5 +66,5 @@ jobs: ## https://docs.microsoft.com/en-us/previous-versions/azure/storage/storage-use-azcopy $azcopy = "C:\Program Files (x86)\Microsoft SDKs\Azure\AzCopy\AzCopy.exe" - & $azcopy /Source:$(BundleDir) /Dest:https://$(StorageAccount).blob.core.windows.net/$(AzureVersion)-private /DestKey:$(StorageAccountKey) /Pattern:*.msixbundle + & $azcopy /Source:$(BundleDir) /Dest:https://$(StorageAccount).blob.core.windows.net/$(AzureVersion)-private /DestKey:$(StorageAccountKey) /Pattern:*.msixbundle /Y displayName: Upload MSIX Bundle package to Az Blob diff --git a/tools/releaseBuild/azureDevOps/templates/windows-packaging.yml b/tools/releaseBuild/azureDevOps/templates/windows-packaging.yml index 3f509a0e24e..83a8580ac32 100644 --- a/tools/releaseBuild/azureDevOps/templates/windows-packaging.yml +++ b/tools/releaseBuild/azureDevOps/templates/windows-packaging.yml @@ -76,8 +76,16 @@ jobs: displayName: Install packaging tools - powershell: | - $zipPath = Get-Item '$(System.ArtifactsDirectory)\Symbols\results\*$(PkgFilter).zip' - Write-Verbose -Verbose "Zip Path: $zipPath" + $zipPathString = '$(System.ArtifactsDirectory)\Symbols\results\*$(PkgFilter).zip' + Write-Verbose -Verbose "Zip Path: $zipPathString" + $zipPath = Get-Item $zipPathString + if(@($zipPath).Count -eq 0) { + throw "No files found at '$zipPathString'" + } + elseif(@($zipPath).Count -ne 1) { + $names = $zipPath.Name -join "', '" + throw "multiple files '${names}' found with '${zipPathString}'" + } $expandedFolder = $zipPath.BaseName Write-Host "sending.. vso[task.setvariable variable=SymbolsFolder]$expandedFolder" diff --git a/tools/releaseBuild/setReleaseTag.ps1 b/tools/releaseBuild/setReleaseTag.ps1 index b20307cae32..8d6a8c3eefe 100644 --- a/tools/releaseBuild/setReleaseTag.ps1 +++ b/tools/releaseBuild/setReleaseTag.ps1 @@ -84,15 +84,23 @@ if($ReleaseTag -eq 'fromBranch' -or !$ReleaseTag) { $isDaily = $true Write-Verbose "daily build" -Verbose - $metaDataJsonPath = Join-Path $PSScriptRoot -ChildPath '..\metadata.json' - $metadata = Get-Content $metaDataJsonPath | ConvertFrom-Json - $versionPart = $metadata.PreviewReleaseTag - if($versionPart -match '-.*$') - { - $versionPart = $versionPart -replace '-.*$' + $jsonPath = "${env:SYSTEM_ARTIFACTSDIRECTORY}\BuildInfoJson\daily.json" + if (test-path -Path $jsonPath) { + Write-Verbose "restoring from buildinfo json..." -Verbose + $buildInfo = Get-Content -Path $jsonPath | ConvertFrom-Json + $releaseTag = $buildInfo.ReleaseTag + } else { + Write-Verbose "creating from branch counter and metadata.json..." -Verbose + $metaDataJsonPath = Join-Path $PSScriptRoot -ChildPath '..\metadata.json' + $metadata = Get-Content $metaDataJsonPath | ConvertFrom-Json + $versionPart = $metadata.PreviewReleaseTag + if ($versionPart -match '-.*$') { + $versionPart = $versionPart -replace '-.*$' + } + + $releaseTag = "$versionPart-daily$((Get-Date).ToString('yyyyMMdd')).$($env:BRANCHCOUNTER)" } - $releaseTag = "$versionPart-daily$((Get-Date).ToString('yyyyMMdd')).$($env:BRANCHCOUNTER)" $vstsCommandString = "vso[task.setvariable variable=$Variable]$releaseTag" Write-Verbose -Message "setting $Variable to $releaseTag" -Verbose Write-Host -Object "##$vstsCommandString" diff --git a/tools/releaseToWinget.ps1 b/tools/releaseToWinget.ps1 new file mode 100644 index 00000000000..e3c1af080e3 --- /dev/null +++ b/tools/releaseToWinget.ps1 @@ -0,0 +1,166 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +param( + [Parameter(Mandatory)] + [semver] + $ReleaseVersion, + + [Parameter()] + [string] + $WingetRepoPath = "$PSScriptRoot/../../winget-pkgs", + + [Parameter()] + [string] + $FromRepository = 'rjmholt', + + [Parameter()] + [string] + $GitHubToken +) + +function GetMsiHash +{ + param( + [Parameter(Mandatory)] + [string] + $ReleaseVersion, + + [Parameter(Mandatory)] + $MsiName + ) + + $releaseParams = @{ + Tag = "v$ReleaseVersion" + OwnerName = 'PowerShell' + RepositoryName = 'PowerShell' + } + + if ($GitHubToken) { $releaseParams.AccessToken = $GitHubToken } + + $releaseDescription = (Get-GitHubRelease @releaseParams).body + + $regex = [regex]::new("powershell-$ReleaseVersion-win-x64.msi.*?([0-9A-F]{64})", 'SingleLine,IgnoreCase') + + return $regex.Match($releaseDescription).Groups[1].Value +} + +function GetThisScriptRepoUrl +{ + # Find the root of the repo + $prefix = $PSScriptRoot + while ($prefix) + { + if (Test-Path "$prefix/LICENSE.txt") + { + break + } + + $prefix = Split-Path $prefix + } + + $stem = $PSCommandPath.Substring($prefix.Length + 1).Replace('\', '/') + + return "https://github.com/PowerShell/PowerShell/blob/master/$stem" +} + +function Exec +{ + param([scriptblock]$sb) + + & $sb + + if ($LASTEXITCODE -ne 0) + { + throw "Invocation failed for '$sb'. See above errors for details" + } +} + +$ErrorActionPreference = 'Stop' + +$wingetPath = (Resolve-Path $WingetRepoPath).Path + +# Ensure we have git and PowerShellForGitHub installed +Import-Module -Name PowerShellForGitHub +$null = Get-Command git + +# Get the MSI hash from the release body +$msiName = "PowerShell-$ReleaseVersion-win-x64.msi" +$msiHash = GetMsiHash -ReleaseVersion $ReleaseVersion -MsiName $msiName + +$publisherName = 'Microsoft' + +# Create the manifest +$productName = if ($ReleaseVersion.PreReleaseLabel) +{ + 'PowerShell-Preview' +} +else +{ + 'PowerShell' +} + +$manifestDir = Join-Path $wingetPath 'manifests' 'm' $publisherName $productName $ReleaseVersion +$manifestPath = Join-Path $manifestDir "$publisherName.$productName.yaml" + +$manifestContent = @" +PackageIdentifier: $publisherName.$productName +PackageVersion: $ReleaseVersion +PackageName: $productName +Publisher: $publisherName +PackageUrl: https://microsoft.com/PowerShell +License: MIT +LicenseUrl: https://github.com/PowerShell/PowerShell/blob/master/LICENSE.txt +Moniker: $($productName.ToLower()) +ShortDescription: $publisherName.$productName +Description: PowerShell is a cross-platform (Windows, Linux, and macOS) automation and configuration tool/framework that works well with your existing tools and is optimized for dealing with structured data (e.g. JSON, CSV, XML, etc.), REST APIs, and object models. It includes a command-line shell, an associated scripting language and a framework for processing cmdlets. +Tags: +- powershell +- pwsh +Homepage: https://github.com/PowerShell/PowerShell +Installers: +- Architecture: x64 + InstallerUrl: https://github.com/PowerShell/PowerShell/releases/download/v$ReleaseVersion/$msiName + InstallerSha256: $msiHash + InstallerType: msi +PackageLocale: en-US +ManifestType: singleton +ManifestVersion: 1.0.0 + +"@ + +Push-Location $wingetPath +try +{ + $branch = "pwsh-$ReleaseVersion" + + Exec { git checkout master } + Exec { git checkout -b $branch } + + New-Item -Path $manifestDir -ItemType Directory + Set-Content -Path $manifestPath -Value $manifestContent -Encoding utf8NoBOM + + Exec { git add $manifestPath } + Exec { git commit -m "Add $productName $ReleaseVersion" } + Exec { git push origin $branch } + + $prParams = @{ + Title = "Add $productName $ReleaseVersion" + Body = "This pull request is automatically generated. See $(GetThisScriptRepoUrl)." + Head = $branch + HeadOwner = $FromRepository + Base = 'master' + Owner = 'Microsoft' + RepositoryName = 'winget-pkgs' + MaintainerCanModify = $true + } + + if ($GitHubToken) { $prParams.AccessToken = $GitHubToken } + + New-GitHubPullRequest @prParams +} +finally +{ + git checkout master + Pop-Location +}