6

Below is the exact code that I am having trouble with.

A brief description:

I am trying to set up a PowerShell class that will hold objects of different types for easy access. I've done this numerous times in C#, so I thought it would be fairly straight forward. The types wanted are [System.Printing] and WMI-Objects.

Originally I had tried to write the class directly to my PowerShell profile for easy usage, but my profile fails to load when I have to class code in it. Saying that it can’t find the type name "System.Printing.PrintServer", or any other explicitly listed types.

After that failed, I moved it to its own specific module and then set my profile to import the module on open. However, even when stored in its own module, if I explicitly list a .NET type for any of the properties, the entire module fails to load. Regardless of whether I have added or imported the type / dll.

The specific problem area is this:

    [string]$Name
    [System.Printing.PrintServer]$Server
    [System.Printing.PrintQueue]$Queue
    [System.Printing.PrintTicket]$Ticket
    [System.Management.ManagementObject]$Unit
    [bool]$IsDefault

When I have it set to this, everything "kind of" works, but then all my properties have the _Object type, which is not helpful.

    [string]$Name
    $Server
    $Queue
    $Ticket
    $Unit
    $IsDefault


Add-Type -AssemblyName System.Printing
Add-Type -AssemblyName ReachFramework
Class PrinterObject
{
    [string]$Name
    [System.Printing.PrintServer]$Server
    [System.Printing.PrintQueue]$Queue
    [System.Printing.PrintTicket]$Ticket
    [System.Management.ManagementObject]$Unit
    [bool]$IsDefault

   PrinterObject([string]$Name)
    {
        #Add-Type -AssemblyName System.Printing
        #Add-Type -AssemblyName ReachFramework
        $this.Server = New-Object System.Printing.PrintServer -ArgumentList [System.Printing.PrintSystemDesiredAccess]::AdministrateServer
        $this.Queue =  New-Object System.Printing.PrintQueue (($this.Server), ($this.Server.GetPrintQueues() |
        Where-Object {$_.Name -match $Name} | Select-Object -ExpandProperty Name))

        $this.Ticket = $this.Queue.UserPrintTicket
        $this.Unit = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Name LIKE `"%$Name%`""
    }

    PrinterObject([string]$Name, [bool]$IsNetwork)
    {
        #Add-Type -AssemblyName System.Printing
        #Add-Type -AssemblyName ReachFramework
        if($IsNetwork -eq $true) {
        $this.Server = New-Object System.Printing.PrintServer ("\\Server")
        $this.Queue =  New-Object System.Printing.PrintQueue (($this.Server), ($this.Server.GetPrintQueues() |
        Where-Object {$_.Name -match $Name} | Select-Object -ExpandProperty Name))

        $this.Ticket = $this.Queue.UserPrintTicket
        $this.Unit = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Name LIKE `"%$Name%`""
        }
        else {
        $This.Server = New-Object System.Printing.PrintServer -argumentList [System.Printing.PrintSystemDesiredAccess]::AdministrateServer
        $this.Queue =  New-Object System.Printing.PrintQueue (($this.Server), ($this.Server.GetPrintQueues() |
        Where-Object {$_.Name -match $Name} | Select-Object -ExpandProperty Name))

        $this.Ticket = $this.Queue.UserPrintTicket
        $this.Unit = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Name LIKE `"%$Name%`"" }
    }
    [void]SetPrintTicket([int]$Copies, [string]$Collation, [string]$Duplex)
    {
        $this.Ticket.CopyCount = $Copies
        $this.Ticket.Collation = $Collation
        $this.Ticket.Duplexing = $Duplex
        $this.Queue.Commit()
    }

    [Object]GetJobs($Option)
    {
            if($Option -eq 1) { return $this.Queue.GetPrintJobInfoCollection() | Sort-Object -Property JobIdentifier | Select-Object -First 1}
            else { return $this.Queue.GetPrintJobInfoCollection() }
    }
    static [Object]ShowAllPrinters()
    {
        Return Get-WmiObject -Class Win32_Printer | Select-Object -Property Name, SystemName
    }

}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dakota Lorance
  • 143
  • 3
  • 9
  • Can you be concrete/literal with "the types specified" ? The pseudo code you give means nothing. You may not be separating the namespace, class and value parts properly. No one can tell that from [System.Object.SomeDotNetObject]::Enum . – Martin Maat Jan 06 '16 at 07:07
  • Where problem type belong? Is it in standard .NET assembly, custom assembly in GAC, custom assembly loaded by path or dynamic assembly `Add-Type -TypeDefinition ...`? – user4003407 Jan 06 '16 at 07:42
  • @PetSerAl Doesn't really matter - if the namespace/type can be resolved outside the class definition, why shouldn't that also apply inside the class? Sounds like a bug to me. – Mathias R. Jessen Jan 06 '16 at 12:08
  • @MathiasR.Jessen I can not reproduce that behavior on my PC, so I ask for additional details to check if I missing something. – user4003407 Jan 06 '16 at 13:23
  • @MartinMaat I have updated with the exact code for the class that I am using. I feel like this may be an issue with types not loading properly into powershell, because if I manually load the types then manually type this class into the shell, it works perfectly fine – Dakota Lorance Jan 06 '16 at 15:26
  • @PetSerAl I have updated the code to show exactly what I am using. I do appreciate any input given on this – Dakota Lorance Jan 06 '16 at 15:27

3 Answers3

12

Every PowerShell script is completely parsed before the first statement in the script is executed. An unresolvable type name token inside a class definition is considered a parse error. To solve your problem, you have to load your types before the class definition is parsed, so the class definition has to be in a separate file. For example:

Main.ps1:

Add-Type -AssemblyName System.Printing
Add-Type -AssemblyName ReachFramework

. $PSScriptRoot\Class.ps1

Class.ps1:

using namespace System.Management
using namespace System.Printing

Class PrinterObject
{
    [string]$Name
    [PrintServer]$Server
    [PrintQueue]$Queue
    [PrintTicket]$Ticket
    [ManagementObject]$Unit
    [bool]$IsDefault
}

The other possibility would be embed Class.ps1 as a string and use Invoke-Expression to execute it. This will delay parsing of class definition to time where types is available.

Add-Type -AssemblyName System.Printing
Add-Type -AssemblyName ReachFramework

Invoke-Expression @'
    using namespace System.Management
    using namespace System.Printing

    Class PrinterObject
    {
        [string]$Name
        [PrintServer]$Server
        [PrintQueue]$Queue
        [PrintTicket]$Ticket
        [ManagementObject]$Unit
        [bool]$IsDefault
    }
'@
mklement0
  • 382,024
  • 64
  • 607
  • 775
user4003407
  • 21,204
  • 4
  • 50
  • 60
  • So. . . I tried this and I guess a very weird bug just caused an infinite amount of powershell windows to pop up so fast that my system ran out of memory. . . – Dakota Lorance Jan 06 '16 at 16:41
  • You dot-sourced the file the dot-source statement is in? Like putting ". $PSScriptRoot\Class.ps1" in a file named "Class.ps1" ? – Martin Maat Jan 06 '16 at 20:08
  • 2
    So is this behaviour a bug, or is PowerShell's import system this terrible? – tyteen4a03 Nov 03 '17 at 15:51
  • 1
    @tyteen4a03 `using assembly` do not cause parse time assembly loading. Assembly loading can cause arbitrary code to be executed, which can be undesired. And extracting types from assembly metadata without loading it not yet implemented, AFAIK. – user4003407 Nov 03 '17 at 15:55
  • @PetSerAl "Assembly loading can cause arbitrary code to be executed, which can be undesired." And opposed to that `Invoke-Expression` - which is Powershell's `eval` - is the desired safe way to do things? – TNT Jun 03 '18 at 15:53
  • 1
    @TNT How is `eval` over constant string is more dangerous, then execution of arbitrary script file? – user4003407 Jun 03 '18 at 16:16
2

To complement PetSerAl's helpful answer, which explains the underlying problem and contains effective solutions, with additional background information:

To recap:

  • As of PowerShell 7.3.1, a PowerShell class definition can only reference .NET types that have already been loaded into the session before the script is invoked.

  • Because class definitions are processed at parse time of a script, rather than at runtime, Add-Type -AssemblyName calls inside a script execute too late for the referenced assemblies' types to be known to any class definitions inside the same script.

  • A using assembly statement should solve this problem, but currently doesn't:

    • using assembly should be the parse-time equivalent of an Add-Type (analogous to the relationship between using module and Import-Module), but this hasn't been implemented yet, because it requires extra work to avoid the potential for undesired execution of arbitrary code when an assembly is loaded.

    • Implementing a solution has been green-lighted in GitHub issue #3641, and the necessary work is being tracked as part of GitHub issue #6652 - but it is unclear when this will happen, given that the issue hasn't received attention in several years.

mklement0
  • 382,024
  • 64
  • 607
  • 775
0

A better solution (than just invoking the entire class in a string) would be to just create your objects and pass them to the class as parameters. For example, this runs fine:

Add-Type -AssemblyName PresentationCore,PresentationFramework

class ExampleClass {
    $object

    ExampleClass ($anotherClass) {
        $this.object = $anotherClass
    }

    [void] Show () {
        $this.object::Show('Hello')
    }
}

$y = [ExampleClass]::new([System.Windows.MessageBox])
$y.Show()

However, if you were to do something like this, you can expect Unable to find type [System.Windows.MessageBox].

Add-Type -AssemblyName PresentationCore,PresentationFramework

class ExampleClass2 {
    $object

    ExampleClass () {
        $this.object = [System.Windows.MessageBox]
    }

    [void] Show () {
        $this.object::Show('Hello')
    }
}
Jacob Colvin
  • 2,625
  • 1
  • 17
  • 36