diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ConvertFromMarkdownCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ConvertFromMarkdownCommand.cs
index 6a1daa24737..5df1b594391 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ConvertFromMarkdownCommand.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ConvertFromMarkdownCommand.cs
@@ -66,12 +66,7 @@ public class ConvertFromMarkdownCommand : PSCmdlet
///
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;
diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/MarkdownOptionCommands.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/MarkdownOptionCommands.cs
index 53238ecfa06..61953205445 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/MarkdownOptionCommands.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/MarkdownOptionCommands.cs
@@ -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 @@ public class SetMarkdownOptionCommand : PSCmdlet
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 @@ protected override void EndProcessing()
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 @@ public class GetMarkdownOptionCommand : PSCmdlet
///
protected override void EndProcessing()
{
- WriteObject(this.CommandInfo.Module.SessionState.PSVariable.GetValue(MarkdownOptionInfoVariableName, new PSMarkdownOptionInfo()));
+ WriteObject(PSMarkdownOptionInfoCache.Get(this.CommandInfo));
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ internal static class PSMarkdownOptionInfoCache
+ {
+ private static ConcurrentDictionary markdownOptionInfoCache;
+ private const string MarkdownOptionInfoVariableName = "PSMarkdownOptionInfo";
+
+ static PSMarkdownOptionInfoCache()
+ {
+ markdownOptionInfoCache = new ConcurrentDictionary();
+ }
+
+ 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);
}
}
}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/MarkdownCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/MarkdownCmdlets.Tests.ps1
index bbaf412d3fb..431bc492961 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/MarkdownCmdlets.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/MarkdownCmdlets.Tests.ps1
@@ -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 'test
'
+ } 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()
+ }
+ }
+ }
}