Running a Dashboard in IIS with Pwsh.exe instead of Windows Powershell?

@adamdriscoll @leeberg
Continuing the discussion from:

  1. How do i run dashboard.ps1 in powershell Core 6.1 on a windows machine with IIS
  2. Unable to force dashboard to run on powershell core 6.1 vs windows powershell when hosting in IIS

Has there been any movement on this? I’m currently experimenting with trying to get a dashboard running in IIS using pwsh, but I have noticed that UniversalDashboard.Server.exe seems to only use powershell.exe. There are several advantages that come with pwsh (Most of which save me from needing to write a bunch of custom code for data transformation), not to mention cross-platform compatibility. :wink:

I tried following the guidance to point the server to use the netcore dll instead of the net472 exe, but still receive the error(s) at the bottom of the page. I’m also adding the Windows Error Log Entries it produces (with necessary data scrubbed).


Potential Solution for Some Folks

It *seems* that running an IIS server with pwsh.exe as the executable...

It seems that running an IIS server with pwsh.exe as the executable, and specifying the dashboard.ps1 file as an argument works to launch a dashboard… for each session (which doesn’t work for my needs – retrieving & transforming a lot of data) however, this might work for some other people so… If you’re trying to get this working, in that manner try changing the aspNetCore line in the web.config file to match (with your install path & Logging info):

<aspNetCore processPath="C:\Program Files\PowerShell\7-preview\pwsh.exe" arguments=".\Dashboard.ps1" stdoutLogEnabled="true" stdoutLogFile="C:\TEMP\WEB_Out.log" forwardWindowsAuthToken="false" />

Windows Error Log Entries

This is the error thrown by both server.dll and server.exe
Application: universaldashboard.server.dll
Framework Version: v4.0.30319
Description: The process was terminated due to an unhandled exception.
Exception Info: System.Exception
   at UniversalDashboard.DashboardManager.Start()
   at UniversalDashboard.Program.Main(System.String[])
Faulting application name: universaldashboard.server.dll..
Faulting application name: universaldashboard.server.dll, version: 1.0.0.0, time stamp: 0xee577b5c
Faulting module name: KERNELBASE.dll, version: 6.2.9200.22753, time stamp: 0x5ccba6bc
Exception code: 0xe0434352
Fault offset: 0x000000000002c978
Faulting process id: 0x235c
Faulting application start time: 0x01d5c567ffb3a0c8
Faulting application path: D:\Inetpub\VisualCronDashboard\netstandard2.0\universaldashboard.server.dll
Faulting module path: C:\Windows\system32\KERNELBASE.dll
Report Id: <OBFUSCATED>
Faulting package full name: 
Faulting package-relative application ID: 
Application 'MACHINE/WEBROOT/APPHOST/..
Application 'MACHINE/WEBROOT/APPHOST/<OBFUSCATED>' with physical root 'D:\Inetpub\<OBFUSCATED>\' failed to start process with commandline 'D:\Inetpub\<OBFUSCATED>\netstandard2.0\universaldashboard.server.dll ', ErrorCode = '0x80004005 : e0434352.
1 Like

You are correct the UD.Server.exe is only using the Windows PowerShell SDK. I was going to suggest the use of pwsh.exe in the web.config but it looks like you’ve experimented with that and it doesn’t fit your needs. Can you explain what you mean by transforming lots of data etc? If we updated UD.Server.exe to support PWSH it would likely just be calling PWSH.exe anyways.

@adam
i was able to test the method but i have realized that custom component i have added to my UD no longer recognized this way.

How do you have the component installed?

yes and when i change the web.config file back to work with net472 everything back to normal.

@adamdriscoll I’m pulling a bunch of data out of another application’s API, then building a custom object to store specific representations of the data ( realistically should use a PS Class or a DB, but this just isn’t there yet). I could directly share the code since there isn’t anything proprietary in it, I just don’t want to drop massive chunks of PS code here unless needed.

@wsl2001 & @adamdriscoll, I also am seeing an error like this with New-UDEndpoint (not recognized as a cmdlet…) now when attempting to use the pwsh.exe method discussed above. I think it’s worth noting that I have both the full version, and Community edition installed (from the gallery). When testing in pwsh, it seems it may be an issue of explicit importing.

I’m just trying figure out what the UD.Server.exe would provide aside from loading the UD module and automatically finding the dashboard.ps1. You should be able to do anything you can do with UD.Server.exe that you can do with pwsh.exe so I’m just trying to see if there is a use-case I’m missing.

You can see here that the server implementation is very minimal: https://github.com/ironmansoftware/universal-dashboard/blob/master/src/UniversalDashboard.Server/DashboardManager.cs#L20

It’s mostly just doing the two things I mentioned above. Honestly, I think deprecating the UD.Server.exe for IIS, updating the web.config and having a script that does what the UD.Server.exe does would be more flexible in the long term. We could have a iis-startup.ps1 or something that’s included with the module that the web.config looks for and then does exactly what the UD.Server.exe is responsible for.

1 Like

(Is @adam or @adamdriscoll the correct user to tag?) That sounds reasonable to me. I was just about to go dive into the C# for the server when you sent that – after a tedious string of two-factor auths to get into the slack channel that you’re not currently logged into :rofl:.

When I get this working (and I will get this working) I’ll post my updates here, and probably send a pull request to at least include the generalized solution if you’ve not yet implemented something.

Side bar: I intend to contribute a lot more to this in the near future, and have at least a few paid licenses coming your way in the next couple of weeks. If all goes well, 50+ in the next couple of months.

Wow. Weird. Why do I have 2 user names!?

I thought I was always logging in as @adam

Sounds good man. I think a generalized solution thats just an update to the web.config with a PS1 would do the trick. It removes the magic-factor that it seems like UD.Server is doing and allows people to better customize their IIS startup.

I’m always happy to formalize it into a PR with tests if you find yourself getting something that works but don’t want to do the GitHub dance.

1 Like

i figured out my issue. i have to copy over my module to pwsh 7 path in order to use the custom components. but in this case its not using the files in iis host directory.

also there is another issue which is your dashboard.ps1 script has to be written in pwsh compatible code like for example : windows powershell get-service -computername will not work in pwsh.

so in this case you have to rewrite your whole script unless these is a solution.

Your script will have to be pwsh compatible if you’re going to use pwsh. Nothing you can do about that.

1 Like

Thats awesome.

Gotta love things like that. I think that is a pretty solid plan. I’m not a web-dev, so the mysticism was definitely there. I didn’t know what kind of magic was happening in the background. I am still a bit perplexed by the new instance of the aspNetCore Process (in my case, pwsh.exe) for each new session. I’m not sure if it’s something I need to be doing in my dashboard, or just the nature of IIS defaults that causes it to work this way. I assume the -Wait parameter for Start-UDDashboard is a sort of keep-alive for the process.

Yea, that is a given. With the windows powershell compatibility in pwsh 7+ it’s not so bad, you can use compatibility to try to force it, but some things will inevitably need to be rewritten.

I don’t know why you would be getting multiple processes with IIS. That’s strange and wasn’t aware that’s how IIS was working. As far as the -Wait parameter, yep, it’s just more a less a keep alive and calls a different method within ASP.NET Core that holds the process open and waits for the IIS proxy connection.

1 Like

Alright @adam, I finally have a very long stack trace, which might help to reveal where the issue lies for me with this one. See below:

There were Errors Building the Dashboard.
New-UDCollapsible : 
   at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception)
   at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.Interpreter.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.LightLambda.RunVoid1[T0](T0 arg0)
   at System.Management.Automation.PSScriptCmdlet.RunClause(Action`1 clause, Object dollarUnderbar, Object inputToProcess)
   at System.Management.Automation.CommandProcessorBase.Complete()
   --- End of inner exception stack trace ---
   at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception)
   at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.Interpreter.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.LightLambda.RunVoid1[T0](T0 arg0)
   at System.Management.Automation.ScriptBlock.InvokeWithPipeImpl(ScriptBlockClauseToInvoke clauseToInvoke, Boolean createLocalScope, Dictionary`2 functionsToDefine, List`1 variablesToDefine, ErrorHandlingBehavior errorHandlingBehavior, Object dollarUnder, Object input, Object scriptThis, Pipe outputPipe, InvocationInfo invocationInfo, Object[] args)
   at System.Management.Automation.ScriptBlock.InvokeWithPipe(Boolean useLocalScope, ErrorHandlingBehavior errorHandlingBehavior, Object dollarUnder, Object input, Object scriptThis, Pipe outputPipe, InvocationInfo invocationInfo, Boolean propagateAllExceptionsToTop, List`1 variablesToDefine, Dictionary`2 functionsToDefine, Object[] args)
   at System.Management.Automation.ScriptBlock.DoInvoke(Object dollarUnder, Object input, Object[] args)
   at System.Management.Automation.ScriptBlock.Invoke(Object[] args)
   at CallSite.Target(Closure , CallSite , ScriptBlock )
   --- End of inner exception stack trace ---
   at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception)
   at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.Interpreter.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.LightLambda.RunVoid1[T0](T0 arg0)
   at System.Management.Automation.PSScriptCmdlet.RunClause(Action`1 clause, Object dollarUnderbar, Object inputToProcess)
   at System.Management.Automation.CommandProcessorBase.Complete()
	 + ScriptStackTrace :
 at New-UDCollapsible, C:\Program Files\WindowsPowerShell\Modules\UniversalDashboard.Community\2.8.1\Modules\UniversalDashboard.Materialize\Scripts\collapsible.ps1: line 18

The thing is, I’m passing in code that should cause no grief… I’ve stripped out all the logging in this block, but this is the code that is causing it to fail – I’m sure this is not the most elegant way to do this, but still… ($ObjectHT is just a hashtable of values) :

($ObjectHT.RealUsers.Where{ $_.UserName.Trim() -ne 'User name not found' }.foreach{ 
        $User = $_
        $Name = $User.UserName.Trim()
        $JobCount = $ObjectHT.RealUsers.Where{ $_.UserName.Trim() -eq $Name }.JobCount
        $null = $CleanName = $Name.Replace('[AD]', '').Trim()
        Write-Verbose -Message "Creating Entry for User: $CleanName"
        @{
            Name     = $CleanName
            JobCount = $JobCount
            Active   = $User.ActiveJobs.Count
            Inactive = $User.InactiveJobs.Count
            JobIds   = New-UDCollapsible -Type Expandable -FontColor WhiteSmoke -Items {      
                New-UDCollapsibleItem -Id "$Name`_Jobs" -FontColor WhiteSmoke -Endpoint {           
                    New-UDGrid -Headers @( 'Job Name', 'Active/Inactive') -Properties @( 'JobName', 'Active') -Endpoint {
                        $ObjectHT.RealUsers.Where{ $_.UserName.Trim() -eq $Name }.JobIds | ForEach-Object {
                            $id = $_
                            $thisJob = $ObjectHT.Jobs.Where{ $_.Id -eq $id }
                            $jobName = $thisJob.Name
                            $active = $thisJob.Stats.Active
                            $url = '/{0}' -f $jobName.Replace(' ', '-')
                            [PSCustomObject]@{
                                JobName = New-UDLink -Text $jobName -Id $jobName -Url $url -OpenInNewWindow -Icon Link
                                Active  = $(if ($active) {
                                        New-UDIcon -Icon smile_o -Color Green
                                    }
                                    else {
                                        New-UDIcon -Icon frown_o -Color Red
                                    })
                            }
                        } | Out-UDGridData										 
                    }
                    Write-Verbose -Message "Creating Job Grid for $Name... DONE!"
                } -Icon plus_circle -Title 'Click to Expand'  
                Write-Verbose -Message "Creating Collapsible for $Name... DONE!"
            }
        }
    })

Alright, I think I’ve got it narrowed down… My code was importing the module at multiple points, and for whatever reason “decided” to grab community vs enterprise at one point or another. Having both loaded into memory seems to trigger this issue.

1 Like

Alright, I’ve got a working Dashboard running in IIS with pwsh!

The issue I think I was having is that ANY error would cause it to quit (which may be due to my ErrorActionPreference Setting. Here is my (cleaned up) web.config. When running this way, all you need in the website’s root folder under inetpub/sitename is the web.config. I keep the Dashboard.ps1 file there as well, just to keep things easy.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <!--
    Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
  -->
  <system.webServer>
    <security>
    </security>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
    </handlers>
	<aspNetCore processPath="C:\Program Files\PowerShell\7-preview\pwsh.exe" arguments=".\Dashboard.ps1" stdoutLogEnabled="true" stdoutLogFile="C:\Path\To\LogFile" forwardWindowsAuthToken="true" />
    <httpProtocol>
      <customHeaders>
        <remove name="X-Powered-By" />
      </customHeaders>
    </httpProtocol>  
  </system.webServer>
    <system.web>
        <compilation defaultLanguage="vb" />
    </system.web>
</configuration>

If anyone is still having issues, I’d be happy to go over the other settings configured in my environment. There are a couple of tweaks, but ultimately it works as expected with default settings for a new website.

Top Tips

  1. Build Verbose Logging Into your solution
  2. Build Verbose Error Logging AND handling into your solution
  3. Ensure that your final syntax is correct. There are some nuances that yield different results when running as a script vs interactively.
1 Like

1 Like

@adam
I’m also building functions to

  1. Keep Pwsh Up to date
  2. Install UniversalDashboard Dependencies
  3. Create Dashboard Websites
    – This one will dynamically create the web.config (maybe I’ll have it manipulate appsettings.json too? :thinking: based on passed in parameters

We plan on deploying a lot of dashboards based on dynamic content… so we’re going to need these things. :upside_down_face:

Oh, man. I’m excited to see what you come up with. If it’s shareable it sounds like something the community could really benefit from.

You shouldn’t have to update appsettings.json. I don’t think (?) we use it to manage any configuration within UD.

1 Like