Powershell: Microsoft.PowerShell.SDK doesn't work in a AspNetCore Blazor WebAssembly project

Created on 7 Oct 2020  ·  14Comments  ·  Source: PowerShell/PowerShell

transferred from dotnet/aspnetcore#25844

When using the preview of AspNetCore Blazor WebAssembly and attempting to utilize the Microsoft.PowerShell.SDK package, Blazor WebAssembly is unable to load the System.Management.Automation.dll

Steps to reproduce

  • Create new Blazor WASM project. (net5-preview.7)
  • Update project file according to this guide.
  • Add Microsoft.PowerShell.SDK package reference
  • Do something with a PowerShell host Example repo

Expected behavior

it works

Actual behavior

Exceptions:

Could not load file or assembly 'System.Management.Automation, Version=7.0.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies.

Environment data

  • PowerShell 7.0.3
  • AspNetCore Blazor WebAssembly 5.0 Preview 7
Issue-Meta WG-DevEx-SDK

Most helpful comment

Microsoft.Extensions.* are the librairies from aspnetcore team, but starting net5.0, they are splitted into runtime and aspnetcore
dimportant update on dotnet/extensions

We need a layer with ILogger, and a way to specify an already ILogger to SMA in App Mode (through InitialSessionState ?).

The only one logger extension for Blazor is BlazorExtensions.Logging. It's an interesting Logger because it implements the JS API Console.Table.
https://github.com/BlazorExtensions/Logging

@SteveL-MSFT AspNetCore use the logger to write error in the console (developper tools) like Javascript. To debug the C# code, we use the Browser debugger, it's very weird, you trace C# powershell source inside the browser tools. More informations here : Blazor Client Side Debugging

GitHub
Microsoft Extension Logging implementation for Blazor - BlazorExtensions/Logging

All 14 comments

Could you test with Microsoft.PowerShell.SDK 7.1 RC1?

@iSazonov In april, I put a demo on twitter of a working prototype on Mono.Wasm/PowerShell 7 hosted on Github Pages.

I will submit a working PR soon for net5.0 RC1, but there is a lot of subjects like fileless (on startup, help system, module), trimming, preprocessor, async, sdk etc... Wasm is an embedded stack so most of the time, it's not a question, it's a behaviour : the minimal path/size/features.

For example, the PowerShell master repo is not well compatible with this kind of restriction : <ThreadPoolMaxThreads>1</ThreadPoolMaxThreads>, so the HttpClient is unusable from the API which is async only.

You can try on my previous github page demo :

if (-not $task) {
  $task = [Net.Http.HttpClient]::new().GetStringAsync("https://gist.githubusercontent.com/IISResetMe/bcbee5f504c25b166003/raw/4ad303f09088ef38aa363863a93c33969080f6ae/Get-AST.ps1")
  $task.Wait()
}
$task.Result
# 1st Execution : Exception calling "Wait" with "0" argument(s): "Cannot wait on monitors on this runtime."
# 2nd execution : OK

I will use this issue as a home to publish technical informations and read community's suggestions.

Why there is no TFM for WebAssembly

  • WebAssembly is more like an instruction set (such as x86 or x64) than like an operating system. And we generally don’t offer divergent APIs between different architectures.
  • WebAssembly’s execution model in the browser sandbox is a key differentiator, but we decided that it makes more sense to only model this as a runtime check. Similar to how you check for Windows and Linux, you can use the OperatingSystem type. Since this isn’t about instruction set, the method is called IsBrowser() rather than IsWebAssembly().
  • There are runtime identifiers (RID) for WebAssembly, called browser and browser-wasm. They allow package authors to deploy different binaries when targeting WebAssembly in a browser. This is especially useful for native code which needs to be compiled to web assembly beforehand.

New patterns

guard the call :

private static string GetLoggingDirectory()
{
    //...

    if (!OperatingSystem.IsBrowser())
    {
        string exePath = Process.GetCurrentProcess().MainModule.FileName;
        string folder = Path.GetDirectoryName(exePath);
        return Path.Combine(folder, "Logging");
    }
    else
    {
        return string.Empty;
    }
}

mark the member as being unsupported :

[UnsupportedOSPlatform("browser")]
private static string GetLoggingDirectory()
{
    //...

    string exePath = Process.GetCurrentProcess().MainModule.FileName;
    string folder = Path.GetDirectoryName(exePath);
    return Path.Combine(folder, "Logging");
}

You have to explicitly indicate that you intend to support your project in Blazor Web Assembly by adding the item to your project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <SupportedPlatform Include="browser" />
  </ItemGroup>
</Project>

Microsoft.NetCore.App APIs unsupported by Blazor WebAssembly

Entire assemblies :

  • System.Diagnostics.FileVersionInfo
  • System.Diagnostics.Process
  • System.IO.Compression.Brotli
  • System.IO.FileSystem.Watcher
  • System.IO.IsolatedStorage
  • System.IO.Pipes
  • System.Net.Mail
  • System.Net.NameResolution
  • System.Net.NetworkInformation
  • System.Net.Ping
  • System.Net.Requests
  • System.Net.Security
  • System.Net.Sockets
  • System.Net.WebClient
  • System.Security.Cryptography.Csp
  • System.Security.Cryptography.Encoding
  • System.Security.Cryptography.Primitives
  • System.Security.Cryptography.X509Certificates

Partial / Limited / Restrictions :

  • System.Console
  • System.ComponentModel
  • System.Net
  • System.Net.Http
  • System.Net.WebSockets
  • System.Security.Authentication.ExtendedProtection
  • System.Security.Cryptography
  • System.Threading

Source :

General Trimming Options

MSBuild Property Name | Default Value | Type | Description
-|-|-|-
PublishTrimmed | false | bool | Enable/Disable Trimming
TrimMode | CopyUsed | enum:CopyUsed/Link | CopyUsed: Assembly trimming, Link: Member trimming
SuppressTrimAnalysisWarnings | true| bool | Suppress trim analysis warnings
ILLinkWarningLevel | 999 | int | ILLink Warning Level
ILLinkTreatWarningsAsErrors | false | bool | ILLink treat warnings as errors

Framework Feature Switches

MSBuild Property Name | Description | Trimmed when | Blazor Specific
-- | -- | -- | --
DebuggerSupport | Any dependency that enables better debugging experience | False | No
EnableUnsafeUTF7Encoding | Insecure UTF-7 encoding | False | No
EnableUnsafeBinaryFormatterSerialization | BinaryFormatter serialization | False | No
EventSourceSupport | Any EventSource related code or logic | False | No
InvariantGlobalization | All globalization specific code and data | True | No
UseSystemResourceKeys| Any localized resources for system assemblies, such as error messages | True | No
HttpActivityPropagationSupport | Any dependency related to diagnostics support for System.Net.Http | False | No
TrimmerRemoveSymbols | Remove Symbols | False | No
BlazorEnableTimeZoneSupport | Remove TimeZone Support | False | Yes

MSBuild Property Name | AppContext Setting
-- | --
DebuggerSupport | System.Diagnostics.Debugger.IsSupported
EnableUnsafeUTF7Encoding | System.Text.Encoding.EnableUnsafeUTF7Encoding
EnableUnsafeBinaryFormatterSerialization | System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization
EventSourceSupport | System.Diagnostics.Tracing.EventSource.IsSupported
InvariantGlobalization | System.Globalization.Invariant
UseSystemResourceKeys | System.Resources.UseSystemResourceKeys
HttpActivityPropagationSupport | System.Net.Http.EnableActivityPropagation
BlazorEnableTimeZoneSupport | ?

source :

@SteveL-MSFT I will need some advices/directions about LogProvider. It's maybe a good candidat for your plugin system ?
With the new features in msbuild, we could add an external logger library, mark the consumer with UnsupportedOSPlatform and trim the library out to the other platforms when publish.( I will ask again for Android on PWSH7.2/net6.0 timeline, logprovider is a wall in the boot startup)

https://github.com/PowerShell/PowerShell/blob/16cc9aaaf825db79eeba65fd0fdc3f43c0c6a8d8/src/System.Management.Automation/utils/tracing/PSEtwLog.cs#L13-L31

Temporary Workaround

internal class PSSysLogProvider : LogProvider
 {
        private static bool isBrowser;

        private static SysLogProvider s_provider;

        static PSSysLogProvider()
        {
            if (OperatingSystem.IsBrowser())
            {
                isBrowser = true;
            }
            else 
            {
                s_provider = new SysLogProvider(PowerShellConfig.Instance.GetSysLogIdentity(),
                                                PowerShellConfig.Instance.GetLogLevel(),
                                                PowerShellConfig.Instance.GetLogKeywords(),
                                                PowerShellConfig.Instance.GetLogChannels());
            }
        }

        internal void WriteEvent(PSEventId id, PSChannel channel, PSOpcode opcode, PSTask task, LogContext logContext, string payLoad)
        {
            if (isBrowser)
            {
                Console.WriteLine($"Id:       {id}\n" + 
                                  $"Channel:  {channel}\n" + 
                                  $"Task:     {task}\n" + 
                                  $"OpCode:   {opcode}\n" +
                                  $"Severity: {GetPSLevelFromSeverity(logContext.Severity)}\n" +
                                  $"Context: \n{LogContextToString(logContext)}" +
                                  $"PayLoad:  {payLoad}"  +
                                  $"UserData: {GetPSLogUserData(logContext.ExecutionContext)}\n");
            }
            else
            {
                s_provider.Log(id, channel, task, opcode, GetPSLevelFromSeverity(logContext.Severity), DefaultKeywords,
                           LogContextToString(logContext),
                           GetPSLogUserData(logContext.ExecutionContext),
                           payLoad);
            }
        }
}

I will need some advices/directions about LogProvider. It's maybe a good candidat for your plugin system ?

We could think about ILogger

We could think about ILogger

With a dependency injection system or by adding an internal static property Provider to PSEtwLog ?
The problem is more about this area than ILogger. Logger is the second thing in a DI after Configuration, so it's maybe an interesting point for a plugin system in SMA.

@iSazonov About trimming, I know it's an area than interest you, I need a script to benchmark size/performance/GC of each MSBuild Property (there is too much properties, I need raw data to choose). Do you know if something already exists ? I will have a look of DotNetBenchmark.

With a dependency injection system or by adding an internal static property Provider to PSEtwLog ?

We have no need to use DI. I guess it will be simple custom implementation of the ILogger.
Existing PowerShell logging/tracing looks very expensive. With migrating to ILogger PowerShell would get better performance. It also fits well with the idea of PowerShell ​​_subsystems_ that MSFT team is developing.

Do you know if something already exists ? I will have a look of DotNetBenchmark.

  • DotNetBenchmark
  • PerfView
  • WPR/WPA
  • dotnet trace
  • VS Profiler
  • experimental PowerShell script profiler (draft PR)

After reading how works this plugin system DotNetCorePlugins, I'm afraid that the compatibility with Wasm will be an extra cost to implement.

FI, this is a sample for lazy load assemblies in wasm (through the router and url browsing).

@inject Microsoft.AspNetCore.Components.WebAssembly.Services.LazyAssemblyLoader assemblyLoader

<Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="@lazyLoadedAssemblies" OnNavigateAsync="@OnNavigateAsync">...</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();

    private async Task OnNavigateAsync(NavigationContext args)
    {
            if (args.Path.EndsWith("/robot"))
            {
                LazyLoadedAssemblies.AddRange(await assemblyLoader.LoadAssembliesAsync(new List<string>() { "robot.dll" }));
            }
    }
}

source : Lazy load assemblies in ASP.NET Core Blazor WebAssembly

About ILogger, I will continue for Blazor to use Console.WriteLine inside SMA Logger code until the new logger will be available. There is only one destination to ILogger and Console : the browser console, the difference is only about formatting, so it doesn't matter now.

For browser scenario, it seems that logging should just be disabled as it's not obvious to me where the logs would go to be something you can inspect later. Sending it to the console (which presumably would show up in output) might be ok for development purposes, but not general usage.

That just tells me that we need to have a pluggable logging destination, then. It not being obvious is a reason to leave it to the person implementing it in nonstandard scenarios, not disabling it entirely.

On logging, here's the "best practice" apparently: https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/logging?view=aspnetcore-3.1#blazor-webassembly

Learn about logging in Blazor apps, including log level configuration and how to write log messages from Razor components.

Microsoft.Extensions.* are the librairies from aspnetcore team, but starting net5.0, they are splitted into runtime and aspnetcore
dimportant update on dotnet/extensions

We need a layer with ILogger, and a way to specify an already ILogger to SMA in App Mode (through InitialSessionState ?).

The only one logger extension for Blazor is BlazorExtensions.Logging. It's an interesting Logger because it implements the JS API Console.Table.
https://github.com/BlazorExtensions/Logging

@SteveL-MSFT AspNetCore use the logger to write error in the console (developper tools) like Javascript. To debug the C# code, we use the Browser debugger, it's very weird, you trace C# powershell source inside the browser tools. More informations here : Blazor Client Side Debugging

GitHub
Microsoft Extension Logging implementation for Blazor - BlazorExtensions/Logging

Working :

$PSVersionTable | Out-String
Get-ChildItem -Path / -Recurse | Out-String
Get-ChildItem env: | Out-String
Get-Variable | Out-String
Get-Module -ListAvailable | Out-String
Get-Command | Out-String
$PSVersionTable | ConvertTo-Json | Out-String
ConvertFrom-Markdown -InputObject "test" | Out-String
Get-Module | ConvertTo-Html -Fragment | Out-String
Get-Help -Name Get-Command | Out-String

Not Working : Update Help + Web Cmdlets + More

# click two times on the Run as a workaround. Need 2 invocations to handle the async task.
if (-not $task) {
    $task = [Net.Http.HttpClient]::new().GetStringAsync("https://gist.githubusercontent.com/IISResetMe/bcbee5f504c25b166003/raw/4ad303f09088ef38aa363863a93c33969080f6ae/Get-AST.ps1")
}
try {
   $task.Wait() # Cannot wait on monitors on this runtime 
}
catch {
   $_ | Out-String
}
$task.Result | Out-String
Was this page helpful?
0 / 5 - 0 ratings