External modules with same cmdlet names as Psake (such as Resolve-Error), stomp out their Psake namesakes, resulting in unexpected behaviors #33

Closed
Iristyle opened this Issue May 15, 2012 · 6 comments

2 participants

@Iristyle

I'm just guessing here as to the root cause (imported module), but I'm getting an error without proper stacktrace information. I have seen other errors from Psake resolve correctly, so I'm not sure what makes this particular error different. It's purely speculation on my part that it's due to the module being imported -- but it's at least something to go on. I'll see if I can debug this if necessary.

5/15/2012 10:47:24 AM: An Error Occurred. See Error Details Below: 
----------------------------------------------------------------------
Microsoft.PowerShell.Commands.Internal.Format.FormatStartData Microsoft.PowerShell.Commands.Internal.Format.GroupStartData Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData Microsoft.PowerShell.Commands.Internal.Format.GroupEndData Microsoft.PowerShell.Commands.Internal.Format.FormatEndData Microsoft.PowerShell.Commands.Internal.Format.FormatStartData Microsoft.PowerShell.Commands.Internal.Format.GroupStartData Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData Microsoft.PowerShell.C
ommands.Internal.Format.GroupEndData Microsoft.PowerShell.Commands.Internal.Format.FormatEndData 11111111111111111111111111111111111111111111111111111111111111111111111111111111 Microsoft.PowerShell.Commands.Internal.Format.FormatStartData Microsoft.PowerShell.Commands.Internal.Format.GroupStartData Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData Microsoft.PowerShell.Commands.Internal.Format.GroupEndData Microsoft.PowerShell.Commands.Internal.Format.FormatEndData 22222222222222222222222222222
222222222222222222222222222222222222222222222222222 Microsoft.PowerShell.Commands.Internal.Format.FormatStartData Microsoft.PowerShell.Commands.Internal.Format.GroupStartData Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData Microsoft.PowerShell.Commands.Internal.Format.GroupEndData Microsoft.PowerShell.Commands.Internal.Format.FormatEndData 33333333333333333333333333333333333333333333333333333333333333333333333333333333 Microsoft.PowerShell.Commands.Internal.Format.FormatStartData Microsoft.Po
werShell.Commands.Internal.Format.GroupStartData Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData Microsoft.PowerShell.Commands.Internal.Format.GroupEndData Microsoft.PowerShell.Commands.Internal.Format.FormatEndData 44444444444444444444444444444444444444444444444444444444444444444444444444444444 Microsoft.PowerShell.Commands.Internal.Format.FormatStartData Microsoft.PowerShell.Commands.Internal.Format.GroupStartData Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData Microsoft.Power
Shell.Commands.Internal.Format.GroupEndData Microsoft.PowerShell.Commands.Internal.Format.FormatEndData----------------------------------------------------------------------
Script Variables
----------------------------------------------------------------------

Name                           Value                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
----                           -----                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
_                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
args                           {}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
Error                          {}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
false                          False                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
input                          System.Management.Automation.Runspaces.PipelineReader`1+<GetReadEnumerator>d__0[System.Object]                                                                                                                                                                                                                                                                                                                                                                                                  
MaximumAliasCount              4096                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
MaximumDriveCount              4096                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
MaximumErrorCount              256                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
MaximumFunctionCount           4096                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
MaximumVariableCount           4096                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
msgs                           {error_no_framework_install_dir_found, error_task_name_does_not_exist, error_build_file_not_found, error_circular_reference...}                                                                                                                                                                                                                                                                                                                                                                 
MyInvocation                   System.Management.Automation.InvocationInfo                                                                                                                                                                                                                                                                                                                                                                                                                                                     
null                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
psake                          {run_by_psake_build_tester, version, build_script_dir, config_default...}                                                                                                                                                                                                                                                                                                                                                                                                                       
PSScriptRoot                   D:\JenkinsHome\workspace\FooBar9000\src\Packages\psake.4.1.0\tools                                                                                                                                                                                                                                                                                                                                                                                                                  
this                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
true                           True  
@Iristyle

I have some code that I've written for another project that builds on the Resolve-Error concept, but makes it a little more foolproof -- I found limitations under certain circumstances (like WinRM). I also found that in some cases (i.e. for HipChat notifications) I wanted a shortened version of the error. It also gives special treatment to SqlException type errors, where the actual SQL source line / error is buried at the end of the InnerException chain. You may find this useful.

function Select-ObjectWithDefault
{
  <#
  .Synopsis
    A safe, non-error generating replacement for Select-Object where you
    wish to return the value of a member OR a default value if the member
    does not exist or is $null
  .Description
    Will accept a series of objects from the pipeline. If the member is
    not found on the original class and there is a member available as a
    key in a hashtable, that value will be returned.
  .Parameter InputObject
    The object to examine
  .Parameter Name
    The name of the member on the object
  .Parameter Value
    The default value to use if the member does not exist on the object
  .Example
    @{ Bar = 'baz' } |Select-ObjectWithDefault -Name 'Count' -Value 'Foo'
    #outputs 1

    Description
    -----------
    Retrieves the Count property from the given Hashtable object.
  .Example
    @{ Bar = 'baz' } | Select-ObjectWithDefault -Name 'Bar' -Value 'ABC'
    #outputs baz

    Description
    -----------
    Retrieves the Bar property from the given Hashtable object.
  .Example
    @{ Bar = 'baz'; } | Select-ObjectWithDefault -Name 'Genie' `
      -Value 'Bottle'
    #outputs Bottle

    Description
    -----------
    Since the Genie property does not exist on the Hashtable object, the
    default value of Bottle is returned.
  #>
  [CmdletBinding()]
  param(
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
    [AllowNull()]
    [PSObject]
    $InputObject,

    [Parameter(Mandatory=$true)]
    [string]
    $Name,

    [Parameter(Mandatory=$true)]
    [AllowNull()]
    $Value
  )

  process
  { 
    if ($_ -eq $null) { $Value }    
    elseif ($_ | Get-Member -Name $Name)
    {
      $_.$Name
    }
    elseif (($_ -is [Hashtable]) -and ($_.Keys -contains $Name))
    {
      $_.$Name
    }
    else { $Value }
  }
}

function Resolve-Error
{
  <#
  .Synopsis
    A means of recursively writing data from the Error object (or a given
    ErrorRecord) instance, to a string.
  .Description
    Will accept a series of objects from the pipeline.  Will default to
    reading the first error from the global $Error object.  ShortView
    treats SQL errors specially.
  .Parameter ErrorRecord
    Optional value that would specify an ErrorRecord instance
  .Parameter ShortView
    A switch that will limit the input to a short version, suitable for
    short notifications such as HipChat.  Special handling is given to
    SqlExceptions where the last InnerException typically contains the
    source line of the error.
  .Example
    $msg = Resolve-Error

    Description
    -----------
    Reads the single ErrorRecord object at $Error[0] and writes it to the
    string $msg
  .Example
    $msg = $Error | Resolve-Error

    Description
    -----------
    Writes all ErrorRecord objects to the string $msg
  .Example
    $msg = Resolve-Error -ShortView

    Description
    -----------
    Reads all of the $Error objects and writes them to the string $msg in
    a short view.
  .Example
    Write-Host -ForegroundColor -Magenta `
      (Resolve-Error -ErrorRecord $Error[3])

    Description
    -----------
    Will write the 4th ErrorRecord from the global $Error object to the
    host in Magenta colored text.
  #>
  [CmdletBinding()]
  param(
    [Parameter(Mandatory=$false, ValueFromPipeline=$true)]
    $ErrorRecord=$Error[0],

    [Parameter(Mandatory=$false)]
    [Switch]
    $ShortView
  )

  process
  {
    $ex = $ErrorRecord.Exception

    if (-not $ShortView)
    {
      $ErrorRecord | Format-List * -Force
      $ErrorRecord.InvocationInfo | Format-List *
      $i = 0
      while ($ex -ne $null)
      {
        $i++; "$i" * 80
        $ex | Format-List * -Force
        $ex = $ex | Select-ObjectWithDefault -Name 'InnerException' -Value $null
      }
      return
    }

    $lastException = @()
    while ($ex -ne $null)
    {
      $lastMessage = $ex | Select-ObjectWithDefault -Name 'Message' -Value ''
      $lastException += ($lastMessage -replace "`n", '')
      if ($ex -is [Data.SqlClient.SqlException])
      {
        $lastException += "(Line [$($ex.LineNumber)] " +
          "Procedure [$($ex.Procedure)] Class [$($ex.Class)] " +
          " Number [$($ex.Number)] State [$($ex.State)] )"
      }
      $ex = $ex | Select-ObjectWithDefault -Name 'InnerException' -Value $null
    }
    $shortException = $lastException -join ' --> '

    $header = $null
    $header = (($ErrorRecord.InvocationInfo | 
      Select-ObjectWithDefault -Name 'PositionMessage' -Value '') -replace "`n", ' '),
      ($ErrorRecord | Select-ObjectWithDefault -Name 'Message' -Value ''),
      ($ErrorRecord | Select-ObjectWithDefault -Name 'Exception' -Value '') |
        ? { -not [String]::IsNullOrEmpty($_) } |
        Select -First 1

    $delimiter = ''
    if ((-not [String]::IsNullOrEmpty($header)) -and 
      (-not [String]::IsNullOrEmpty($shortException)))
      { $delimiter = ' [<<==>>] ' }

    '[ERROR] : ' + $header + $delimiter + $shortException
  }
}
@Iristyle

There is actually something a bit more nefarious at work here -- I have a module that defines / exports Resolve-Error already that's being imported into Psake... and mine (which works a little differently in terms of returning output) is getting called instead of the one in Psake, which is resulting in the errors.

This is strictly a dynamic scoping / import order type of issue. Because Load-Modules is being called in Psake after the Psake script itself has loaded functions into scope, the Psake functions are getting blown away.

I could have sworn I saw a means to scope / use local functions that may collide with cmdlet names brought into scope later, but I'm having trouble finding the mechanism at the moment. It could have had something to do with PSDrive / Providers - i.e. create a local drive and stash / pull functions from there in an effort to prevent this sort of thing from happening. I'm pretty sure another PowerShell tool uses a mechanims like this -- could have been Posh or Pester or something. Will dig around a little more.

I have the error recreated outside of Psake -- I'm going to experiment a little and see if I can come up with a solution that imposes minimal impact to Psake.

function Get-Content
{
    return "foo"
}

. (Join-Path (Split-Path $MyInvocation.MyCommand.Path) test2.ps1)

function Test
{
    script:Get-Content
}

Export-ModuleMember -Function Test
function Get-Content
{
    return "bar"
}

First snip can be saved / imported as test-module.psm1. Second snippet can be saved as test2.ps1.

After importing the module, and calling the Test function, the output is: bar
If the dot sourcing is test2 is moved to the first line, the output is: foo

@Iristyle

Looks like you could set readonly on the internal functions. This will stop the build dead in its track if someone redefines internal functions... but is a pretty heavy-handed approach IMHO.

Set-Item Function:Resolve-Error -Option ReadOnly

Another option would be to store the internal functions in the psake hash that you have defined. The problem with this is that you could be stuck with scriptblock style param parsing - i.e. by order and not by name. Yuck.

${psake:Resolve-Error} = { #code }
@Iristyle

OK... thinking about this some more, I have a general solution.

There's are lots of meta-programming-ish ways you could do things to try work around the fact that Powershell is dynamically scoped and doesn't have namespaces, but I don't know it's worth the ultimate effort.

The safest approach is to assign scriptblocks to an internal hash as mentioned above. That way the likelihood of local scriptblocks being overwritten is almost non-existent. You could also use nested functions like in JavaScript.

Given that this would be a sizable restructure of the existing code, I think that there's a better option in this case.

Proper PS convention states all modules should export only public cmdlets and that they should be named in standard Verb-Noun fashion. Import-Module does have a switch to bring in modules with a prefix should you really need it. I think anything internal to the module should not follow Verb-Noun naming conventions... which is an overall minor change. I will submit a pull request shortly with a few minor tweaks along these lines.

This is a pretty reasonable safeguard against internal functions getting stomped out.

@Iristyle

http://www.simple-talk.com/dotnet/.net-tools/further-down-the-rabbit-hole-powershell-modules-and-encapsulation/#twelfth

I finally found a ref as to how to get to cmdlets hidden by importing cmdlets of the same name... you can use a fully qualified name to get at them based on the module. This is another approach Psake could take if you really want to retain Verb-Noun naming.

IMHO, losing the - as I've implemented in my pull request is a simpler method of hiding private module methods.

@damianh damianh pushed a commit to damianh/psake that referenced this issue Dec 2, 2013
@Iristyle Iristyle Internal function names have been changed from Verb-Noun to VerbNoun …
…(without dash) in an effort to reduce potential for external collisions. Since external modules may be loaded with Verb-Noun naming after these internal functions are brought into scope, common functions like Resolve-Error are prone to get wiped out.. and to further lead to difficult to diagnose build problems.

See discussion psake#33
7585dcb
@damianh
psake member

I'm cleaning up psake issues, it looks like this has been addressed via #34. If not, please open a new issue.

@damianh damianh closed this Mar 31, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment