Fix null reference when Microsoft.PowerShell.Utility is loaded as a snapin in hosting scenarios (#9404)

This commit is contained in:
Aditya Patwardhan 2019-05-03 15:16:41 -07:00 committed by Dongbo Wang
parent 3bfca6d0fa
commit 2737e74d86
3 changed files with 152 additions and 40 deletions

View file

@ -66,12 +66,7 @@ namespace Microsoft.PowerShell.Commands
/// </summary>
protected override void BeginProcessing()
{
_mdOption = this.CommandInfo.Module.SessionState.PSVariable.GetValue("PSMarkdownOptionInfo", new PSMarkdownOptionInfo()) as PSMarkdownOptionInfo;
if (_mdOption == null)
{
throw new InvalidOperationException();
}
_mdOption = PSMarkdownOptionInfoCache.Get(this.CommandInfo);
bool? supportsVT100 = this.Host?.UI.SupportsVirtualTerminal;

View file

@ -2,11 +2,13 @@
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Management.Automation.Runspaces;
using System.Threading.Tasks;
using Microsoft.PowerShell.MarkdownRender;
@ -124,7 +126,6 @@ namespace Microsoft.PowerShell.Commands
private const string IndividualSetting = "IndividualSetting";
private const string InputObjectParamSet = "InputObject";
private const string ThemeParamSet = "Theme";
private const string MarkdownOptionInfoVariableName = "PSMarkdownOptionInfo";
private const string LightThemeName = "Light";
private const string DarkThemeName = "Dark";
@ -173,11 +174,11 @@ namespace Microsoft.PowerShell.Commands
break;
}
this.CommandInfo.Module.SessionState.PSVariable.Set(MarkdownOptionInfoVariableName, mdOptionInfo);
var setOption = PSMarkdownOptionInfoCache.Set(this.CommandInfo, mdOptionInfo);
if (PassThru.IsPresent)
{
WriteObject(mdOptionInfo);
WriteObject(setOption);
}
}
@ -256,7 +257,61 @@ namespace Microsoft.PowerShell.Commands
/// </summary>
protected override void EndProcessing()
{
WriteObject(this.CommandInfo.Module.SessionState.PSVariable.GetValue(MarkdownOptionInfoVariableName, new PSMarkdownOptionInfo()));
WriteObject(PSMarkdownOptionInfoCache.Get(this.CommandInfo));
}
}
/// <summary>
/// The class manages whether we should use a module scope variable or concurrent dictionary for storing the set PSMarkdownOptions.
/// When we have a moduleInfo available we use the module scope variable.
/// In case of built-in modules, they are loaded as snapins when we are hosting PowerShell.
/// We use runspace Id as the key for the concurrent dictionary to have the functionality of separate settings per runspace.
/// Force loading the module does not unload the nested modules and hence we cannot use IModuleAssemblyCleanup to remove items from the dictionary.
/// Because of these reason, we continue using module scope variable when moduleInfo is available.
/// </summary>
internal static class PSMarkdownOptionInfoCache
{
private static ConcurrentDictionary<Guid, PSMarkdownOptionInfo> markdownOptionInfoCache;
private const string MarkdownOptionInfoVariableName = "PSMarkdownOptionInfo";
static PSMarkdownOptionInfoCache()
{
markdownOptionInfoCache = new ConcurrentDictionary<Guid, PSMarkdownOptionInfo>();
}
internal static PSMarkdownOptionInfo Get(CommandInfo command)
{
// If we have the moduleInfo then store are module scope variable
if (command.Module != null)
{
return command.Module.SessionState.PSVariable.GetValue(MarkdownOptionInfoVariableName, new PSMarkdownOptionInfo()) as PSMarkdownOptionInfo;
}
// If we don't have a moduleInfo, like in PowerShell hosting scenarios, use a concurrent dictionary.
if (markdownOptionInfoCache.TryGetValue(Runspace.DefaultRunspace.InstanceId, out PSMarkdownOptionInfo cachedOption))
{
// return the cached options for the runspaceId
return cachedOption;
}
else
{
// no option cache so cache and return the default PSMarkdownOptionInfo
var newOptionInfo = new PSMarkdownOptionInfo();
return markdownOptionInfoCache.GetOrAdd(Runspace.DefaultRunspace.InstanceId, newOptionInfo);
}
}
internal static PSMarkdownOptionInfo Set(CommandInfo command, PSMarkdownOptionInfo optionInfo)
{
// If we have the moduleInfo then store are module scope variable
if (command.Module != null)
{
command.Module.SessionState.PSVariable.Set(MarkdownOptionInfoVariableName, optionInfo);
return optionInfo;
}
// If we don't have a moduleInfo, like in PowerShell hosting scenarios with modules loaded as snapins, use a concurrent dictionary.
return markdownOptionInfoCache.AddOrUpdate(Runspace.DefaultRunspace.InstanceId, optionInfo, (key, oldvalue) => optionInfo);
}
}
}

View file

@ -429,36 +429,6 @@ bool function()`n{`n}
}
$options.Link | Should -BeExactly "[4;38;5;117m"
$options.Image | Should -BeExactly "[33m"
$options.EmphasisBold | Should -BeExactly "[1m"
$options.EmphasisItalics | Should -BeExactly "[36m"
}
It "Verify PSMarkdownOptionInfo is defined in module scope" {
$PSMarkdownOptionInfo | Should -BeNullOrEmpty
$mod = Get-Module Microsoft.PowerShell.Utility
$options = & $mod { $PSMarkdownOptionInfo }
$options.Header1 | Should -BeExactly "[7m"
$options.Header2 | Should -BeExactly "[4;93m"
$options.Header3 | Should -BeExactly "[4;94m"
$options.Header4 | Should -BeExactly "[4;95m"
$options.Header5 | Should -BeExactly "[4;96m"
$options.Header6 | Should -BeExactly "[4;97m"
if($IsMacOS)
{
$options.Code | Should -BeExactly "[107;95m"
}
else
{
$options.Code | Should -BeExactly "[48;2;155;155;155;38;2;30;30;30m"
}
$options.Link | Should -BeExactly "[4;38;5;117m"
$options.Image | Should -BeExactly "[33m"
$options.EmphasisBold | Should -BeExactly "[1m"
@ -546,4 +516,96 @@ bool function()`n{`n}
}
}
}
Context "Hosted PowerShell scenario" {
It 'ConvertFrom-Markdown gets expected output when run in hosted powershell' {
try {
$pool = [runspacefactory]::CreateRunspacePool(1, 2, $Host)
$pool.Open()
$ps = [powershell]::Create()
$ps.RunspacePool = $pool
$ps.AddScript({
$output = '# test' | ConvertFrom-Markdown
$output.Html.trim()
})
$output = $ps.Invoke()
$output | Should -BeExactly '<h1 id="test">test</h1>'
} finally {
$ps.Dispose()
}
}
It 'Get-MarkdownOption gets default values when run in hosted powershell' {
try {
$ps = [powershell]::Create()
$ps.AddScript( {
Get-MarkdownOption -ErrorAction Stop
})
$options = $ps.Invoke()
$options | Should -Not -BeNullOrEmpty
$options.Header1 | Should -BeExactly "[7m"
$options.Header2 | Should -BeExactly "[4;93m"
$options.Header3 | Should -BeExactly "[4;94m"
$options.Header4 | Should -BeExactly "[4;95m"
$options.Header5 | Should -BeExactly "[4;96m"
$options.Header6 | Should -BeExactly "[4;97m"
if ($IsMacOS) {
$options.Code | Should -BeExactly "[107;95m"
} else {
$options.Code | Should -BeExactly "[48;2;155;155;155;38;2;30;30;30m"
}
$options.Link | Should -BeExactly "[4;38;5;117m"
$options.Image | Should -BeExactly "[33m"
$options.EmphasisBold | Should -BeExactly "[1m"
$options.EmphasisItalics | Should -BeExactly "[36m"
}
finally {
$ps.Dispose()
}
}
It 'Set-MarkdownOption sets values when run in hosted powershell' {
try {
$ps = [powershell]::Create()
$ps.AddScript( {
Set-MarkdownOption -Header1Color '[93m' -ErrorAction Stop -PassThru
})
$options = $ps.Invoke()
$options | Should -Not -BeNullOrEmpty
$options.Header1 | Should -BeExactly "[93m"
$options.Header2 | Should -BeExactly "[4;93m"
$options.Header3 | Should -BeExactly "[4;94m"
$options.Header4 | Should -BeExactly "[4;95m"
$options.Header5 | Should -BeExactly "[4;96m"
$options.Header6 | Should -BeExactly "[4;97m"
if ($IsMacOS) {
$options.Code | Should -BeExactly "[107;95m"
} else {
$options.Code | Should -BeExactly "[48;2;155;155;155;38;2;30;30;30m"
}
$options.Link | Should -BeExactly "[4;38;5;117m"
$options.Image | Should -BeExactly "[33m"
$options.EmphasisBold | Should -BeExactly "[1m"
$options.EmphasisItalics | Should -BeExactly "[36m"
}
finally {
$ps.Dispose()
}
}
}
}