PowerShell cmdlet development - How to avoid retaining handles to output objects - powershell

Microsoft's "Required Development Guidelines" for Cmdlet development here states that a Cmdlet should not retain handles to output objects.
However when developing a cmdlet which performs a side effect on pipeline input, which may or may not modify the state of the input object, one can pipe the input object to the output pipeline via WriteObject method to promote further chaining of commands. Since pipeline input parameter is an array of input object type, it will still be retaining a reference to the object.
Will this behavior break the development contract and cause problems?

In your case, I would say you aren't actually keeping a reference to the object. PowerShell is using the InputObject property (the InputObject parameter) to pass the object to you, but PowerShell is maintaining that reference, not you.
If the rule you referred to is firing on your code, you could open a suggestion to improve the detection of this error condition.
To rephrase the rule somewhat - once you call WriteObject, you shouldn't use that object any longer. If you have a reference, you could refer to it. There are several interesting ways you might keep a reference:
A local variable
A non-static field
A static field
Local variables are fine if the last reference to the object is your call to WriteObject.
Non-static fields should be fine if they are used to implement a parameter to the cmdlet.
Static fields are probably a problem.

Related

Why do powershell class properties require this within their methods?

PSVersion: 5.1.17134.858
I haven't done a whole lot of work with Powershell's classes but here's a simple example of what I'm running into:
class xNode{
[uint64]$MyID=0
static [uint64]$ClassID=0
xNode(){
$MyID = [xNode]::ClassID++
}
[String] ToString(){return "xNode: $MyID"}
}
doesn't parse it gives the two errors:line 6 $MyID..., "Cannot assign property, use '$this.MyID'."line 9 ..$MyID", "Variable is not assigned in the method."
I'm trying to use the classes property named $MyID, and this usage appears to be in line with the example given in the help documentation get-help about_Classes, and when I copied their whole example at the end out to a file then tried to run it I was getting the same errors as well for $Head, $Body, $Title,... Of course I can force it to work by adding this.
class xNode{
[uint64]$MyID=0
static [uint64]$ClassID=0
xNode(){
$this.MyID = [xNode]::ClassID++
}
[String] ToString(){return "xNode: $($this.MyID)"}
}
However I'd rather not have to keep typing this. all over the place, is there maybe some environment setting or something I've overlooked?
(Note: to get it to work at the commandline, I also needed to remove all of the blank lines)
However I'd rather not have to keep typing this. all over the place, is there maybe some environment setting or something I've overlooked?
No, it's working as advertised, you can't reference instance properties inside the class without the $this variable reference.
Why do powershell class properties require this within their methods?
(The following is what I'd call "qualified speculation", ie. not an officially sourced explanation)
PowerShell classes are a bit tricky from an implementers point of view, because a .NET property behaves differently than, say, a parameter in a powershell scriptblock or a regular variable.
This means that when the language engine parses, analyzes and compiles the code that makes up your PowerShell Class, extra care has to be taken as to which rules and constraints apply to either.
By requiring all "instance-plumbing" to be routed through $this, this problem of observing one set of rules for class members vs everything else becomes much smaller in scope, and existing editor tools can continue to (sort of) work with very little change.
From a user perspective, requiring $this also helps prevent accidental mishaps, like overwriting instance members when creating new local variables.

How do I pass all parameters between cmdlets easily?

Using CmdletBinding, is there an easy way to regurgitate the exact parameters that a cmdlet was called with, so I can call another cmdlet with the exact same parameters?
I'm writing Powershell Cmdlets in Powershell. I'm using advanced functions. I have a cmdlet called Get-Environment, with several optional parameters like [string]EnvironmentName and [switch]Active. I have another cmdlet, called Get-Machine, with all of the same optional parameters; it calls Get-Environment. Originally, before I added the [switch]Active parameter, I simply called Get-Environment with all variables explicitly (see below).
I can't do the same thing now, because if I add "active" then it will be set. I don't want to have make a test in Get-Machine to see if Active is true and have two different versions of the Get-Environment call. I'd prefer to not have to trawl through the $PSBoundParameters hashtable and reconstruct the original strings, but that looks like the only feasible way forward (unless I'm missing something.)
Original code inside get-machine:
$environments = get-Environment -EnvironmentName $EnvironmentName
Oh for Pete's sake. I found it. I was missing the big stupid easy thing. I'll leave this up for others, and in case someone has an even better answer.
https://ss64.com/ps/psboundparameters.html
$PSBoundParameters can be used to call a subordinate function or cmdlet passing the same parameters - PowerShell will automatically splat the hash table's values instead of having to type each of the parameters:
get-otherthing #PSBoundParameters

Powershell: Dynamically gather data types for IIS configuration element validation

I'm attempting to dynamically use the Microsoft.Web.Administration namespace within a powershell cmdlet.
Is there a way to add a variable into a Namespace.Class declaration.
I'm using the namespace [Microsoft.Web.Administration] and it's class [Microsoft.Web.Administration.ApplicationPool], it has a property under it 'Recycling' that you can access using a GetProperty method.
[Microsoft.Web.Administration.ApplicationPool].GetProperty("Recycling")
If you spit out the PropertyType of the above using this
[Microsoft.Web.Administration.ApplicationPool].GetProperty("Recycling").PropertyType.FullName
You get this result,
Microsoft.Web.Administration.ApplicationPoolRecycling
which is another class. I now want to access this class dynamically within the cmdlet. How do access this class from within the code, I want the code to dynamically discover the new class of the object and then access that class. But I can't find a way to accomplish this.
Psuedocode for what I'm trying
[System.Reflection.Assembly]::LoadFrom( "C:\windows\system32\inetsrv\Microsoft.Web.Administration.dll" )
$appPoolProperty = [Microsoft.Web.Administration.ApplicationPool].GetProperty($Property)
$subClassName = $appPoolProperty.PropertyType.FullName
#This is the step I'm lost on
$subClass = New-Object $subClassName
So I'm not sure if there's a way to have a TypeName for an object put in as a string value and I can't seem to find a way to cast the variable as anything else. Sorry if this is remedial, I'm a basement grown coder and just learn things as I go.
EDIT: As requested by Mathias below in the comments, an overview of what I'm trying to achieve.
I have a script that automates the configuration of many IIS components. At this time I'm attempting to add validation to the cmdlet Set-AppPoolConfiguration that I've created that allows a configuration to be fed into the cmdlet that configures an AppPool (this is used to deploy websites/weapplications throughout a distributed web environment). Utilizing the details inside the Microsoft.Web.Administration I'm able to get enum values, as well as types of the many configuration components of an AppPool. This is saving me time to where I don't have to hard code in the data types and can just dynamically discover them and do validation on the values when the specific configuration element is chosen.
For example. If an AppPool Recycle schedule is need to be configured, I need to validate it against a TimeSpan data type. If it is not a TimeSpan data type issues will arise when being added to the ScheduleCollection. So I'm looking to validate the value provided before attempting to add it.
Since there are many AppPool configuration elements, I don't want to have to create a massive switch or if/elseif chain that checks each configuration element and statically dictate what data type it is for validation. I want the class itself to dynamically provide this information to simplify the code.
I can get the majority of these data types by simply accessing the property chain within the namespace. For example, if you want to know what type is required for the QueueLength, use this:
[Microsoft.Web.Administration.ApplicationPool].GetProperty("QueueLength").PropertyType.Name
And you'll get Int64.
It's also extremely useful for getting enums.
[Microsoft.Web.Administration.ApplicationPool].GetProperty("ManagedPipelineMode").PropertyType.GetEnumNames()
However attempting this with Schedule and you run into a small issue as it returns ScheduleCollection. This is true of any of the configuration elements that are part of a collection.
[Microsoft.Web.Administration.ApplicationPool].GetProperty("Recycling").PropertyType.GetProperty('PeriodicRestart').PropertyType.GetProperty('Schedule').PropertyType.Name
However the knowledge that the schedule item inside the ScheduleCollection is only accessible from within the Schedule class. My code currently checks to see if it is a collection, and then if it is, it is attempting to access that collection/class and get the information that is required. To find out that schedule is a TimeSpan you have to access it's specific class instance:
[Microsoft.Web.Administration.Schedule].GetProperty('Time').PropertyType.Name
Now AppPools are simple, there's only a single collection group that is normally edited, so hard coding in that if you're attempting to set a new recycle schedule it will be a TimeSpan isn't that big of a deal, however when we move over to WebSite/WebApplication configurations, it becomes more tedious to statically declare data types for each configuration element that is part of a collection, and becomes more useful to try and discover these dynamically based on the configuration element selected.
This was my initial approach, I just included the above for clarity. I'm going to step back and take another look at how to attack this as this does not appear to be as easy as I had hoped, I'll post my solution here.
You can retrieve the constructor from the type literal and invoke it like so:
$type = [Microsoft.Web.Administration.ApplicationPoolRecycling]
$ctor = $type.GetConstructor('NonPublic,Instance',$null,#(),$null)
$appPoolRecyclingInstance = $ctor.Invoke($null)
Though there may be a way to do the above, in order to complete the updates to my cmdlet and proceed forward with my project I went a hybrid route.
The reason why I started exploring the [Microsoft.Web.Administration] namespace was that it provided information on the data types where the typical way I was manipulating IIS settings failed using Get/Set/Add-WebConfigurationProperty.
The specific failure is in reporting back valid data types for Enums. Take for instance ProcessModel.IdentityTypes. There is a set of valid entries for an IdentityType. However the following doesn't provide you with those valid types, so you either have to create static instances of them inside your cmdlet, or some other external data source, whereas I wanted Microsoft to provide them either through IIS itself, or through the classes attached to these configuration elements so the cmdlet would need minimal updating as IIS versions/configuration elements change.
This code returns Boolean
(Get-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.applicationHost/applicationPools/add[#name='$AppPool']" -name "AutoStart").Value.GetType().Name
However, this code returns string, which is true, but I needed to know that it is an enum and I needed to know the proper values.
(Get-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' -filter "system.applicationHost/applicationPools/add[#name='$AppPool']" -name "processModel").identityType.GetType().Name
Using a mix of both the Namespace and the traditional Get-WebConfigurationProperty commands I can successfully now test for data types, as well as gather proper values for enums dynamically in code. If there is interest I can post the entire validation code here that I'm using for this cmdlet.

Object type changes on import/export in Powershell

I have been banging up against this for several hours now and I'm hoping that someone can help point me in the right direction.
I'm developing a few custom PowerShell cmdlets, and one of the supporting classes is a User object. Several of my cmdlets either emit or consume List.
This has worked very well so far, but I hit a serious snag when I tried to serialize one of the lists. The export seems to work fine; I look at the file (csv, clixml, etc.) and it looks the way I expect it to with type User. However, when I re-import it, the type seems to change to CSV:Class.User or Deserialized.Class.User. Obviously, this causes a problem when it's fed into a cmdlet that expects the standard User class.
If there a good way to fix this? I suspected that changing my cmdlets to expect some Interface instead of List would probably do the trick, but I can't figure out what interface that should be. And I can find no switch to the import methods to specify class names.
Any help would be greatly appreciated.
Welcome to PowerShell's extended type system. :-) BTW you will also get back state-only deserialized objects when your objects are passed across a remoting session. You can query the PSObject's TypeNames collection looking for Deserialized.Class.User to determine if you have a deserialized version of your type. Sames goes for the CSV version. You could create a couple of factory methods or clone style constructors on your User class that takes a PSObject that is some type of User (CSV or Deserialized) and then create a regular Class.User object. Just be aware that certain operations may not make sense in the deserialization case. For instance, using a Process object as an example, you can call Kill on a Process object and if the object came from the same machine that would work (assuming correct privs). However, if you were to call Kill on a process object from another machine, that's not going to work - hence the special deserialized objects that are primarily just data (property) containers.

How do I get around PowerShell not binding pipeline parameters until after BeginProcessing is called?

I'm writing a Cmdlet that can be called in the middle of a pipeline. With this Cmdlet, there are parameters that have the ValueFromPipelineByPropertyName attribute defined so that the Cmdlet can use parameters with the same names that are defined earlier in the pipeline.
The paradox that I've run into is in the overridden BeginProcessing() method, I utilize one of the parameters that can get its value bound from the pipeline. According to the Cmdlet Processing Lifecycle, the binding of pipeline parameters does not occur until after BeginProcessing() is called. Therefore, it seems that I'm unable to rely on pipeline bound parameters if they're attempting to be used in BeginProcessing().
I've thought about moving things to the ProcessRecord() method. Unfortunately, there is a one time, relatively expensive operation that needs to occur. The best place for this to happen seems to be in the BeginProcessing() method to help ensure that it only happens once in the pipeline.
A few questions question surrounding this:
Is there a good way around this?
These same parameters also have the Mandatory attribute set on them. How can I even get this far without PowerShell complaining about not having these required parameters?
Thanks in advance for your thoughts.
Update
I took out the second part of the question as I realized I just didn't understand pipeline bound parameters well enough. I mistakingly thought that pipeline bound parameters came from the previous Cmdlet that executed in the pipeline. The actually come from the object being passed through the pipeline! I referenced a post by Keith Hill to help understand this.
You could set an instance field bool (Init) to false in BeginProcessing. Then check to see if the parameter is set in BeginProcessing. If it is then call a method that does the one time init (InitMe). In ProcessRecord check the value of Init and if it is false, then call InitMe. InitMe should set Init to true before returning.
Regarding your second question, if you've marked the parameter as mandatory then it must be supplied either as a parameter or via the pipeline. Are you using multiple parameter sets? If so, then even if a parameter is marked as mandatory, it is only mandatory if the associated parameter set is the one that is determined by PowerShell to be in use for a particular invocation of the cmdlet.