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() + } + } + } }