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
    }

}
6
  • 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 . Commented Jan 6, 2016 at 7: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 ...? Commented Jan 6, 2016 at 7: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. Commented Jan 6, 2016 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. Commented Jan 6, 2016 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 Commented Jan 6, 2016 at 15:26

3 Answers 3

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
    }
'@
Sign up to request clarification or add additional context in comments.

6 Comments

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. . .
You dot-sourced the file the dot-source statement is in? Like putting ". $PSScriptRoot\Class.ps1" in a file named "Class.ps1" ?
So is this behaviour a bug, or is PowerShell's import system this terrible?
@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.
@TNT How is eval over constant string is more dangerous, then execution of arbitrary script file?
|
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.

Comments

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')
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.