Badgerati / Pode.Web

Web template framework for use with the Pode PowerShell web server
MIT License
195 stars 26 forks source link

Warning JSON is truncated... after creating a file #605

Open icabraja opened 3 months ago

icabraja commented 3 months ago

I have created a page for adding my PXE deployments in the queue and everything works fine. Now I need to create a log file with new deployment but for some reason when I add

New-Item -ItemType File -Force -Path "$PSScriptRoot\public\logs\deployment\$($WebEvent.Data.MACAddress).log"

and click the button to add the deployment in the queue I get a warning in the console:

 WARNING: Resulting JSON is truncated as serialization has exceeded the set depth of 10

The page hangs, file gets created but button animation is in the loop and modal windows doesn't popup. Commenting out "New-Item", everything works fine. Here is a page snippet. Some data is redacted.

New-PodeWebCard -Name "Add deployment" -Content @(
    New-PodeWebForm -Name "Add PXE Deployment" -SubmitText "Add Deployment" -ShowReset -ResetText "Reset Form" -Content @(
        New-PodeWebTextbox -Name "Hostname" -DisplayName "Host Name" -Required -Width 500px -PrependIcon "desktop-classic" -HelpText "Hostname must follow REDACTED naming convention"
        New-PodeWebTextbox -Name "MACAddress" -DisplayName "MAC Address" -Required -Width 500px -PrependIcon "lan" -HelpText "MAC address needs to be in hypen format: aa-bb-cc-11-22-33"
        New-PodeWebLine
    ) -ScriptBlock {
        $DBPath = (Get-PodeConfig).DBPath
        $DBQuery = "SELECT MACAddress, IPAddress, Hostname FROM Deployments"
        $DBData = Invoke-SqliteQuery -Query $DBQuery -DataSource $DBPath
        $Hostname = $WebEvent.Data.Hostname.ToUpper()
        $MACAddress = $WebEvent.Data.MACAddress.ToUpper()

        $invalid = $false

        if ($Hostname -notmatch "^(REDACTED-|READECTED-)[A-Z]{4}-[0-9]{2}$") {
            Out-PodeWebValidation -Name "Hostname" -Message "Invalid Hostname format."
            $invalid = $true
        }
        if ($MACAddress -notmatch "^(([A-F0-9]{2}[-]){5}[A-F0-9]{2})$") {
            Out-PodeWebValidation -Name "MACAddress" -Message "Invalid MAC address or format. MAC address must be entered in hyphen format."
            $invalid = $true
        }
        if ($MACAddress -eq $DBData.MACAddress) {
            Out-PodeWebValidation -Name "MACAddress" -Message "MAC address already exists in deployment."
            $invalid = $true
        }
        if ($Hostname -eq $DBData.Hostname) {
            Out-PodeWebValidation -Name "Hostname" -Message "Hostname already exists in deployment."
            $invalid = $true
        }
        if ($invalid) {

            return
        }

        $DBPath = (Get-PodeConfig).DBPath
        $DBQuery = "SELECT AESKey, AESIV FROM Security"
        $DBData = Invoke-SqliteQuery -Query $DBQuery -DataSource $DBPath

        $EncryptUsername = Invoke-AESStringEncrypt -PlainTextString "$($WebEvent.Auth.User.Domain)\$($WebEvent.Auth.User.Username)" -Key $DBData.AESKey -InitVector $DBData.AESIV
        $PlainPassword = ConvertFrom-SecureString -SecureString $WebEvent.Auth.User.Credential.Password -AsPlainText
        $EncryptPassword = Invoke-AESStringEncrypt -PlainTextString $PlainPassword -Key $DBData.AESKey -InitVector $DBData.AESIV
        Clear-Variable -Name "PlainPassword"

        $DBPath = (Get-PodeConfig).DBPath
        $DBQuery = "INSERT INTO Deployments (MACAddress, Hostname, IPAddress, Username, Password, DeployStatus, VNCStatus) VALUES ('$MACAddress', '$Hostname', 'Waiting on PE', '$EncryptUsername', '$EncryptPassword', 'Waiting on PE', 'Waiting on PE')"
        Invoke-SqliteQuery -Query $DBQuery -DataSource $DBPath
        Show-PodeWebModal -Name "Add PXE Deployment"
        Sync-PodeWebTable -Name "PXE Deployment Status"
        Reset-PodeWebForm -Name "Add PXE Deployment"
        New-LogEntry -LogLevel '[INFO]' -LogLine "Deployment added to the queue by $($WebEvent.Auth.User.Username)" -Hostname "$Hostname" -MACAddress "$MACAddress" -LogFile "deployment.log"
        New-Item -ItemType File -Force -Path "$PSScriptRoot\public\logs\deployment\$($WebEvent.Data.MACAddress).log"
    }
)

New-PodeWebModal -Name "Add PXE Deployment" -DisplayName "Add PXE Deployment" -Icon "server-plus" -Content @(
    New-PodeWebAlert -Type Success -Content @(
        New-PodeWebText -Value "Deployment successfully added."
    )
)

EDIT

Tried with piping empty string to Out-FiIe, empty string with redirect operator and finally Set-Content with empty string as a -Value, they all worked. Bug maybe?

Badgerati commented 3 months ago

Hi @icabraja,

It's because New-Item returns a value, and this is being returned from the scriptblock to Pode.Web which tries to parse it as an "Action".

If you pipe or set the value to null that should remove the warning :)

ie:

$null = New-Item -ItemType File -Force -Path "$PSScriptRoot\public\logs\deployment\$($WebEvent.Data.MACAddress).log"
ChrisLynchHPE commented 2 weeks ago

While the output from New-Item might be causing a pipeline issue here, I'm curious if you have any references to Save-PodeState in the rest of your script. I found what I believe is a bug (#639) with PodeState not handling ScriptBlock objects within $PodeContext.Server.State.'pode.web.pages'. I've written a workaround function that should handle this unexpected behavior.

function Save-PodeStateInternal {

    # "Save-PodeStateInternal called" | Out-Host

    $PageCollection = [System.Collections.ArrayList]::new()
    $Page = @{
        Name = $null;
        Value = $null
    }

    # Need to process $PodeContext.Server.State.'pode.web.pages'.Value collection
    ForEach ($__page in $PodeContext.Server.State.'pode.web.pages'.Value.Keys) {

        # $PodeContext.Server.State.'pode.web.pages'.Value."$__page" | Format-List | Out-Host

        # "Processing '{0}'" -f $__page | Out-Host

        if ($PodeContext.Server.State.'pode.web.pages'.Value."$__page".ScriptBlock) {

            # "Processing '{0}' ScriptBlock property" -f $__page | Out-Host

            $Page.Name = $__page
            $Page.Value = $PodeContext.Server.State.'pode.web.pages'.Value."$__page".ScriptBlock

            # "Adding to collection" | Out-Host
            [void]$PageCollection.Add($Page)

            $PodeContext.Server.State.'pode.web.pages'.Value."$__page".ScriptBlock = @{}

        }

    }

    $null = Save-PodeState -Path $cache:PodeStateConfig

    # Loop back through the collection restoring the running state
    ForEach ($__page in $PageCollection) {

        # "Restoring '{0}' ScriptBlock property" -f $__page.Name | Out-Host

        $PodeContext.Server.State.'pode.web.pages'.Value.$($__page.Name).ScriptBlock = $__page.Value

    }

}

However, unless I put this function directly within the Add-PodeRoute scriptblock, the Function is not accessible anywhere else. I've declared it outside of Start-PodeServer, inside Start-PodeServer, with and without Export-ModuleMember or even Export-PodeFunction as shown in the Pode Functions documentation.

For example:

# The following code is stored in a ps1 file, and called from pwsh console
# Declaring Save-PodeStateInternal is NOT accessible within Start-PodeServer

Start-PodeServer {

    # Declaring Save-PodeStateInternal IS accessible within Start-PodeServer but NOT within Add-PodeRoute or other Pode function ScriptBlocks.
    # Get-Command -Name Save-PodeStateInternal returns the Command when declared within Start-PodeServer

    # INIT AND NETWORK BINDING LOGIC HERE

    Add-PodeRoute -Method Post -Path '/api/jobs/checkOut' -ScriptBlock {

        function Save-PodeStateInternal {

            $PageCollection = [System.Collections.ArrayList]::new()
            $Page = @{
                Name = $null;
                Value = $null
            }

            # Need to process $PodeContext.Server.State.'pode.web.pages'.Value collection
            ForEach ($__page in $PodeContext.Server.State.'pode.web.pages'.Value.Keys) {

                if ($PodeContext.Server.State.'pode.web.pages'.Value."$__page".ScriptBlock) {

                    $Page.Name = $__page
                    $Page.Value = $PodeContext.Server.State.'pode.web.pages'.Value."$__page".ScriptBlock

                    [void]$PageCollection.Add($Page)

                    $PodeContext.Server.State.'pode.web.pages'.Value."$__page".ScriptBlock = @{}

                }

            }

            $null = Save-PodeState -Path $cache:PodeStateConfig

            # Loop back through the collection restoring the running state
            ForEach ($__page in $PageCollection) {

                $PodeContext.Server.State.'pode.web.pages'.Value.$($__page.Name).ScriptBlock = $__page.Value

            }

        }

        $RequestBody = $WebEvent.Data

        if ($RequestBody.Count -eq 0 -or $null -eq $RequestBody) {

            Write-PodeJsonResponse -StatusCode 400 -Value @{ ErrorID = 'Invalid_Request_Body'; ErrorDescription = "The missing JobID and JobURL in request body." }

        }

        elseif (-not $RequestBody['JobID']) {

            Write-PodeJsonResponse -StatusCode 400 -Value @{ ErrorID = 'Invalid_Request_Body'; ErrorDescription = "The missing JobID in request body." }

        }

        elseif (-not $RequestBody['JobURL']) {

            Write-PodeJsonResponse -StatusCode 400 -Value @{ ErrorID = 'Invalid_Request_Body'; ErrorDescription = "The missing JobURL in request body." }

        }

        else {

            Lock-PodeObject -ScriptBlock {

                # LOGIC GOES HERE
                Write-PodeJsonResponse -Value $Result

                # Save the server state using the workaround
                "Saving StateConfig with Save-PodeStateInternal." | Out-Host
                Save-PodeStateInternal

            }

        }

    }

}

Output from executing the script, then receiving HTTPS POST request from client:

[PS] C:\temp\pode> .\Start-PodeServer2.ps1

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Function        Save-PodeStateInternal

Pode v2.11.1 (PID: 1088)
Get-Command:
Line |
   3 |      Get-Command -Name Save-PodeStateInternal | Out-Host
     |      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | The term 'Save-PodeStateInternal' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
Setting PodeStateConfigFileLocation
Setting UploadFileDirectoryLocation
Attempting to restore pode state
Binding to <IPv4>
Binding to <IPv6>
Listening on the following 2 endpoint(s) [1 thread(s)]:
        - https://<IPv4>:8443/
        - https://<IPv6>:8443/
[Verbose]: [ContextId: 13d4a2b5-5ef6-59e5-0ab6-82acd923ef2b] Opening Receive
[Verbose]: [ContextId: 13d4a2b5-5ef6-59e5-0ab6-82acd923ef2b] Starting Receive
[Verbose]: [ContextId: 13d4a2b5-5ef6-59e5-0ab6-82acd923ef2b] Receiving request
[Verbose]: [ContextId: 13d4a2b5-5ef6-59e5-0ab6-82acd923ef2b] Received request
Saving StateConfig with Save-PodeStateInternal.
[Verbose]: [ContextId: 13d4a2b5-5ef6-59e5-0ab6-82acd923ef2b] Disposing Context
[Verbose]: [ContextId: 13d4a2b5-5ef6-59e5-0ab6-82acd923ef2b] Sending response
[Verbose]: [ContextId: 13d4a2b5-5ef6-59e5-0ab6-82acd923ef2b] Response sent
[Verbose]: [ContextId: 13d4a2b5-5ef6-59e5-0ab6-82acd923ef2b] Request disposed
[Verbose]: [ContextId: 13d4a2b5-5ef6-59e5-0ab6-82acd923ef2b] Response disposed
Date: 2024-11-13 13:11:21
Level: Error
ThreadId: 1
Server: Fileserver
Category: ObjectNotFound: (Save-PodeStateInternal:String) [Invoke-PodeScriptBlock], CommandNotFoundException
Message: The term 'Save-PodeStateInternal' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
StackTrace: at <ScriptBlock>, <No file>: line 566
at Invoke-PodeScriptBlock, C:\Program Files\PowerShell\Modules\Pode\2.11.1\Public\Utilities.ps1: line 584
at Lock-PodeObject<End>, C:\Program Files\PowerShell\Modules\Pode\2.11.1\Public\Threading.ps1: line 85
at <ScriptBlock>, <No file>: line 434
at Invoke-PodeScriptBlock, C:\Program Files\PowerShell\Modules\Pode\2.11.1\Public\Utilities.ps1: line 573
at <ScriptBlock>, <No file>: line 124

Date: 2024-11-13 13:11:21
Level: Error
ThreadId: 1
Server: Fileserver
Category: ObjectNotFound: (Save-PodeStateInternal:String) [], CommandNotFoundException
Message: The term 'Save-PodeStateInternal' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
StackTrace: at <ScriptBlock>, <No file>: line 566
at Invoke-PodeScriptBlock, C:\Program Files\PowerShell\Modules\Pode\2.11.1\Public\Utilities.ps1: line 584
at Lock-PodeObject<End>, C:\Program Files\PowerShell\Modules\Pode\2.11.1\Public\Threading.ps1: line 85
at <ScriptBlock>, <No file>: line 434
at Invoke-PodeScriptBlock, C:\Program Files\PowerShell\Modules\Pode\2.11.1\Public\Utilities.ps1: line 573
at <ScriptBlock>, <No file>: line 124

Date: 2024-11-13 13:11:21
Level: Error
ThreadId: 1
Server: Fileserver
Category: System.Management.Automation
Message: The term 'Save-PodeStateInternal' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
StackTrace:    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)