$Pscx context object versus $Pscx settings provider

Topics: Developer Forum
Coordinator
Feb 13, 2008 at 9:24 PM
Edited Feb 13, 2008 at 9:25 PM
Oisin and Jachym,

I really want to get 1.2 out before the MVP summit. :-) Currently we have a $Pscx settings object. Jachym has integrated its usage into the C# cmdlet code (much thanks!) but it does require that the PSCX user execute the PscxInitialization.ps1 script in order to effectively use PSCX. Right now the "store" for user PSCX settings is in the user's profile. Note that PscxInitialization.ps1 initializes the required preferences variables to a reasonable default value. A couple of things I don't like about this approach is:
  • Having to dot source PscxInitialization.ps1
  • Usage is a bit awkward for paths e.g. "$($Pscx.Home)\Scripts\..." as compared to the old $PscxHome\Scripts\...
Oisin had a very interesting idea. Use a provider to create a PSCX settings provider. I recommended that we use a settings XML file stored in the user's WindowsPowerShell dir (or LocalAppData\Pscx\PscxConfig.xml) as the backing store for this provider. It has a couple of benefits:
  • Automatic initialization via automatic loading of that provider during snapin load
  • Paths are easier to deal with $Pscx:Home\Scripts\...
The primary con is that Jachym has already done a bunch of work to make the current $PscxContext object usable within Pscx 1.2. The secondary con is that both of you guys seem pretty busy with your day jobs so making a switchover might be difficult to do within that timeframe.

What do you guys think? I'm drawn to the settings provider idea but perhaps it will just have to wait until 1.3?
Developer
Feb 14, 2008 at 1:21 AM
That's a great idea, and I think it could be done without breaking the PscxContext interface. Let me try...
Developer
Feb 15, 2008 at 1:09 AM
Edited Feb 15, 2008 at 4:14 PM
jachym - if you want to have a stab at the provider, that's cool... I'm really finding it hard at the moment to find enough contiguous time to get any decent amount of stuff done. The goal of my design is to make it very easy to add new settings and simultaneously provide persistant storage of values and allow script.

The general plan I had (I have some skeletal code if you want it), is this:

ultimately, the pscx profile is executed with a dot source in the user's profile.ps1 file:

. pscx:profile

...where pscx is a psdrive that is automatically mounted at startup. For simplicity's sake, it's a ContainerCmdletProvider (also implements IContentCmdletProvider and IPropertyCmdletProvider), and by dot sourcing the path pscx:profile, the provider simultaneous executes initialisation script and/or "renders" into the profile. Each item in the provider is responsible for rendering its own initialisation script. In the following explanations, assume all the magic plumbing is done ;-)

Persistable settings are creatable by assigning a function to an item on the path (named parameters become settable properties and/or dynamic parameters on the path):

function smtpfunc ($port, $server) { }
 
set-item pscx:smtp (gi function:smtpfunc)
si pscx:smtp -port 25
si pscx:smtp -server smtp.server.com
or set properties using set-itemproperty

set-itemproperty pscx:smtp port 25
sp pscx:smtp server smtp.server.com
reading it back is as simple as going through the ItemProperty API, or by using dynamic parameters (in the former).

When the provider shuts down, these settings are persisted. The trick is, the next time Pscx starts up, when pscx:profile is dot sourced, it will - as a side-effect - execute the function passing the persisted properties. The function can obviously execute script using these properties, and/or it can write text to the pipeline which will be executed in the context of the profile (because it is being dot sourced). The function could also be empty, just for property persistance and nothing else.

If you want to read back a persisted property, it's as easy as:

gp pscx:smtp port
25
A more fun example:

function checkpopmail ($server, $username, $password, [bool]$CheckPopMail) {
    if ($CheckPopEmail) { 
       # code to check number of emails here
    }
    # this will be executed in context of profile.ps1
    "write-host `"You have $count email(s) in your inbox.`""
}
si pscx:pop3 (gi function:checkpopmail)
If the user wants to enable pop3 check on startup, he/she performs:

sp pscx:pop3 username oising
sp pscx:pop3 password bleh
sp pscx:pop3 checkpopmail $true
On next startup, profile.ps1 dot sources our pscx:profile magic path, and this triggers our provider to call all registered functions with their associated properties. In the case of our pop3 setting, this leads to the line:

write-host "You have 7 email(s) in your inbox"

being emitted and executed. While the plumbing might seem tricky, it definitely makes it easy to add new settings and start up behaviour without having to compile up new code.

Keith expressed an interest in using a hierarchy over a single container which involves a bit more coding, but the principal remains the same.

Thoughts?

- Oisin
Developer
Feb 15, 2008 at 1:45 AM
I like that. I guess that would also nicely generalize the EyeCandy settings concept.

I've already implemented a PscxObjectProviderBase class yesterady, which creates PSDrives from arbitrary PSObjects, with each PSProperty rendered as an item in the drive. This should allow us using the good old PscxContext from C# AND having a Pscx: drive instead of the global variable.

I'll add a hashtable to store the item properties, and on shutdown, I'll enumerate all the items (PSProperties), and persist every PSNoteProperty (was created by set-item), along with its item properties. On startup, items (PSNoteProperties) and their properties will be rehydrated. Any stored ScriptBlock will be executed.

BTW, you won't even need a dummy functions, just set-item pscx:smtp and a null item is created.

I don't think there would be much benefit in a hierarchy provider, and it would be complicated to implement it on top of the existing PscxContext.

And I hope you or Keith will write the documentation. I wouldn't dare to explain this beast to someone even in my native language :-)
Developer
Feb 15, 2008 at 2:38 AM
OK, so the Pscx: drive now supports item properties. No persistence yet.

PS Pscx:\> dir | ft -a
 
Key              Value
---              -----
Preferences      {}
Session          {}
Version          1.2.0.0
WindowsAccount   JACHYMTABLET\jachymko
WindowsIdentity  System.Security.Principal.WindowsIdentity
WindowsPrincipal System.Security.Principal.WindowsPrincipal
IsAdmin          False
 
 
PS Pscx:\> si Test 42
PS Pscx:\> gi Test | ft -a
 
Key  Value
---  -----
Test    42
 
 
PS Pscx:\> $pscx:test *= 2
PS Pscx:\> $pscx:test
84
 
PS Pscx:\> sp Test HostName example.org
PS Pscx:\> sp Test Port 4242
PS Pscx:\> gp Test | fl
 
 
HostName : example.org
Port     : 4242
Coordinator
Feb 15, 2008 at 3:25 AM
Edited Feb 15, 2008 at 4:25 AM
I'm glad to see Preferences and Session items at the root. Are those still hashtables? In that case we could still set preference variables like so:

$Pscx:Preferences["Send-SmtpMail\SmtpHost"] = smtp.example.net

The hashtable approach seems easier to work with (also easier to see all the values at once). One thing I would like to see minimized the kruft at the Root of the PSCX drive. I just don't want it to become a dumping ground. I like having some key dirs there (Pscx Home, ScriptsDir), Pscx version, IsAdmin is useful. Perhaps Windows Account but I'm not sure about WindowsIdentity/Principal. I could document it. :-)
Developer
Feb 15, 2008 at 4:26 AM
Actually, I was thinking we'd get rid of them once we start using the item properties. You get two-level "hierarchy" for free and I don't find it harder to use:

$Pscx:Preferences.Send-SmtpMail_SmtpHost = 'smtp.example.net'

versus

si Pscx:Send-SmtpMail
sp Pscx:Send-SmtpMail SmtpHost smtp.example.net

I'm fine with Pscx:\ becoming a "dumping ground" for PSCX components to keep their settings. But I changed my mind about the auto-persisting and auto-loading feature. It seems to me that it goes a bit against the common pattern in PowerShell -- I think we shouldn't automagically persist anything, and settings which are to be loaded into every session should be manually added to the user's profile. E.g.: eye candy settings would be initialized like they're now, except of using Pscx:\EyeCandy instead of $Pscx.Preferences['EyeCandy']

I think it's much easier to determine the group membership, SID, etc from the Identity/Principal than from WA, which is only a shortcut for getting the account name. I'd leave them in. They're all lazily initialized after all, so there's even no startup cost.
Coordinator
Feb 15, 2008 at 6:54 AM
Edited Feb 15, 2008 at 4:29 PM
Well I don't agree with letting the PSCX drive become a dumping ground. I would like the output when viewing its contents to be usable and sane. I definitely want to keep the Session hashtable as the contained variables are of no interest to the typical PSCX user. I could go either way on the preference variables. BTW this code:

si Pscx:Send-SmtpMail
sp Pscx:Send-SmtpMail SmtpHost smtp.example.net

is a total spew IMO - sorry. :-) I'd rather just have this:

$Pscx:SmtpHostPreference = "smtp.example.net"

In this way the PSCX provider would be more akin to the Environment provider. Perhaps it is best just to keep this provider as a single level provider. Personally I never really liked the way Set/Get-ItemProperty work with providers like the Registry provider. It does not feel natural to me at all.

I'm not hung up on the backing store idea. I just want to minimize impact on existing profiles when using PSCX. Right now you have to swallow a large pill to take full advantage of PSCX. Also (and correct me if I'm wrong here) isn't this roughly what MoW does with the PowerTab settings?

At the end of the day, I would like to make the PSCX profile stuff be easily integratable into a user's existing profile. I'd even like the installer to offer that as an option to taking the PSCX profile as their non-host specific profile. BTW I'm not sure I liked the idea of having the provider somehow provide the PSCX profile script. Perhaps the backing store just saved the state of the Preference variables???

Is there a way we can somehow move the update-formatdata -prepend FileSystem.format.ps1xml code into this provider's initialization? I think we could also modify the process's PATH env var to add the PSCX home and scripts dir. I would like to move as much initialization code out of the profile as we possible can. That would leave having to only do the Add-PSSnapin and then dot source whichever function libraries folks want to use. That could also be a setting to (and it would be more discoverable):

$Pscx:DotSourceEyeCandy = $true
$Pscx:DotSourceCd = $true
$Pscx:DotSourceImport-VSVars = $true
$Pscx:DotSourceDebug = $false
...

Of course, I'm not sure you can even do this from within a provider's initialization.

Regarding "Identity/Principal", on these sort of thing we have to step back and ask if this is useful for the majority of PSCX users. If it is then OK we keep it. If it isn't then perhaps it is just something you add to your profile. :-) I'm not trying to make a judgment here on these two properties but if each of us developers put everything we wanted pre-inited into this provider then it would be harder to sift through for the typical user and it would slow PowerShell startup even more.

BTW this is a good discussion. It is good to hear everybody's point of view on this. And many thanks for helping out on this. I was beginning to wonder if we lost you (Jachym). :-)
Developer
Feb 15, 2008 at 3:22 PM
Edited Feb 15, 2008 at 4:15 PM
Keith said:

Is there a way we can somehow move the update-formatdata -prepend FileSystem.format.ps1xml code into this provider's initialization? I think we could also modify the process's PATH env var to add the PSCX home and scripts dir. I would like to move as much initialization code out of the profile as we possible can. That would leave having to only do the Add-PSSnapin and then dot source whichever function libraries folks want to use. That could also be a setting to (and it would be more discoverable):

I'm not sure what sort of state the runspace is in inside the provider's initialisation phase, but under my original design, it would work like this:

function enhanced_fsformat_func ([bool]$EnableFileSystemFormatFixes) {
  if ($EnableFileSystemFormatFixes) {
     "update-formatdata -prepend FileSystem.format.ps1xml"
  }
}
si pscx:PscxFormatData (gi function:enhanced_fsformat_func)
It's important to remember that this script is just run once, by us, in design time. Pscx does not ship with this code anywhere - this is the configuration phase. When Pscx is shipped, this is stored in the XML that the provider reads at start up.

The only initialisation code in the user's profile is:

. pscx:profile
Settings are changed imperatively, using tab completion discovery, just like MoW's PowerTab. Remember, we have full control of what is returned when a user wants to check her/her settings - they just type: gci pscx: and we can return hashtables, pscustomobjects, whatever. If we want to see the emitted profile, perform gc pscx:profile.

Now to turn this feature on, just do:

sp pscx:formatdata EnableFileSystemFormatFixes $true
or using the dynamic parameter syntax

si pscx:formatdata -EnableFileSystemFormatFixes $true
Next time powershell runs, the dot sourced pscx:profile will emit the append-formatdata command if the property has been set to true and the script runs in the correct context.

Admittedly it's a bit clunky, but it's a way of "strong typing" our profile, while still using script and keeping the user's profile free from cruft. Of course the devil is in the details, and Keith I'm with you 100% on the itemproperty implementation - it's horrible, I find the registry provider incomprehensible - but there is an alternative syntax using dynamic properties too if we like: set-item pscx:pscxformatdata -propname $true

Anyway, this is all just ideas - maybe I am trying to do too much with property persistance and startup script emit tricks, but a provider gives us plenty of things that variables do not, e.g. a mistyped preference variable (or mistyped hashtable key) is treated as a missing preference - a mistyped property in a settings provider will throw. We also get tab completion.

this is good discussion though and I'm sure we'll come to a good compromise
Developer
Feb 15, 2008 at 4:44 PM
Edited Feb 15, 2008 at 4:47 PM
just another thought - this still allows us to keep external scripts if we want, e.g. the settings provider controls the dot sourcing (like you mentioned Keith). First, at design time, we configure our provider to support an Appearance section and have EyeCandy enabled by default, and command numbering (e.g. the prefix in our prompt) is disabled:

function appearance_func ([bool]$EnableEyeCandy, [bool]$EnableCommandNumbering) {
   if ($EnableEyeCandy) {
     ". scripts\eyecandy.ps1"
   }
 
  if ($EnableCommandNumbering) {
    ". scripts\commandnumbering.ps1"
  }
}
si pscx:Appearance (gi function:appearance_func)
si pscx:Appearance -EnableEyeCandy $true 
Again, this is done by us before we ship. We only ship the persisted XML. Now, and end user checks appearance settings and decides to disable eye candy:

ps> dir pscx:appearance
Name                           Value
----                           -----
EnableEyeCandy                 True
EnableCommandNumbering         False
ps> si pscx:appearance -EnableEyeCandy $false
Now next startup, the eyecandy script will not be dot sourced and the user did not have to delve into her/her profile to play with things he/she does not want to play with. The profile is clean, and free from cruft. And more importantly, it still lets the advanced user customize the eyecandy.ps1 script though if they want to.

You may also be thinking, what about the Session item? we don't want users Tab completion picking that up. Simple: in the provider's GetChildNames override, we do not return it. This still lets us use it, but it is hidden from normal use.
Coordinator
Feb 20, 2008 at 6:15 AM
I'm leaning against the notion of dynamically generating the profile. If we encounter a problem with our profile (and we have before) then we won't be able to tell folks how to tweak a gen'd profile. So I'm now thinking that we add one line to folks existing profile (or optionally create a new profile with just this one line):

. "$((Get-ItemProperty HKLM:\SOFTWARE\Microsoft\PowerShell\1\PowerShellSnapIns\Pscx).ApplicationBase)\Profile\PscxInitialization.ps1"
Or we could pull the Add-PSSnapin command out and make it a two line init:

Add-PSSnapin Pscx
. "$Pscx:Home\Profile\PscxInitialization.ps1"
The only thing that the PSCX settings provider would hold are variables (pscx common, preference, session, dot source preferences). That is whether or not we dot source cd.ps1 would be a boolean preference. Whether or not we prepend the FileSystem formatdata would be boolean preference. These preferences would get persisted to an AppData\Pscx\1.2\Settings.xml file. We could then build a simple PscxConfig winforms apps to allow users to edit that file. The beauty of this approach is that we could launch it after install and the user could relaunch it at any time. In a winforms app, we can also provide much more information on what functionality each of the various options provide.
Developer
Feb 20, 2008 at 3:22 PM
Sounds good Keith.

Btw, from your two options, I think I prefer the second even though it is two lines.
Coordinator
Feb 20, 2008 at 11:40 PM
As long as we have reasonable default preference settings I think that should be fine too.