Error when running a dot-sourced script after executing it - powershell

I've got very weird behaviour which I suppose is related to dot-sourcing somehow, but I cannot wrap my head around it. Here's what I have:
A script sourced.ps1 which contains the two functions and a class:
class MyData {
[string] $Name
}
function withClass() {
$initialData = #{
Name1 = "1";
Name2 = "2";
}
$list = New-Object Collections.Generic.List[MyData]
foreach ($item in $initialData.Keys) {
$d = [MyData]::new()
$d.Name = $item
$list.Add($d)
}
}
function withString() {
$initialData = #{
Name1 = "1";
Name2 = "2";
}
$list = New-Object Collections.Generic.List[string]
foreach ($item in $initialData.Keys) {
$list.Add($item)
}
}
I also have a script caller.ps1 which dot-sources the one above and calls the function:
$ErrorActionPreference = 'Stop'
. ".\sourced.ps1"
withClass
I then call the caller.ps1 by executing .\caller.ps1 in the shell (Win terminal with PS Core).
Here's the behaviour I cannot explain: if I call .\caller.ps1, then .\sourced.ps1 and then caller.ps1 again, I get the error:
Line |
14 | $list.Add($d)
| ~~~~~~~~~~~~~
| Cannot find an overload for "Add" and the argument count: "1".
However, if I change the caller.ps1 to call withString function instead, everything works fine no matter how many times I call caller.ps1 and sourced.ps1.
Furthermore, if I first call caller.ps1 with withString, then change it to withClass, there is no error whatsoever.
I suppose using modules would be more correct, but I'm interested in the reason for such weird behaviour in the first place.

Written as of PowerShell 7.2.1
A given script file that is both dot-sourced and directly executed (in either order, irrespective of how often) creates successive versions of the class definitions in it - these are distinct .NET types, even though their structure is identical. Arguably, there's no good reason to do this, and the behavior may be a bug.
These versions, which have the same full name (PowerShell class definitions created in the top-level scope of scripts have only a name, no namespace) but are housed in different dynamic (in-memory) assemblies that differ by the last component of their version number, shadow each other, and which one is effect depends on the context:
Other scripts that dot-source such a script consistently see the new version.
Inside the script itself, irrespective of whether it is itself executed directly or dot-sourced:
In PowerShell code, the original version stays in effect.
Inside binary cmdlets, notably New-Object, the new version takes effect.
If you mix these two ways to access the class inside the script, type mismatches can occur, which is what happened in your case - see sample code below.
While you can technically avoid such errors by consistently using ::new() or New-Object to reference the class, it is better to avoid performing both direct execution and dot-sourcing of script files that contain class definitions to begin with.
Sample code:
Save the code to a script file, say, demo.ps1
Execute it twice.
First, by direct execution: .\demo.ps1
Then, via dot-sourcing: . .\demo.ps1
The type-mismatch error that you saw will occur during that second execution.
Note: The error message, Cannot find an overload for "Add" and the argument count: "1", is a bit obscure; what it is trying to express that is that the .Add() method cannot be called with the argument of the given type, because it expects an instance of the new version of [MyData], whereas ::new() created an instance of the original version.
# demo.ps1
# Define a class
class MyData { }
# Use New-Object to instantiate a generic list based on that class.
# This passes the type name as a *string*, and instantiation of the
# type happens *inside the cmdlet*.
# On the second execution, this will use the *new* [MyData] version.
Write-Verbose -Verbose 'Constructing list via New-Object'
$list = New-Object System.Collections.Generic.List[MyData]
# Use ::new() to create an instance of [MyData]
# Even on the second execution this will use the *original* [MyData] version
$myDataInstance = [MyData]::new()
# Try to add the instance to the list.
# On the second execution this will *fail*, because the [MyData] used
# by the list and the one that $myDataInstance is an instance of differ.
$list.Add($myDataInstance)
Note that if you used $myDataInstance = New-Object MyData, the type mismatch would go away.
Similarly, it would also go away if you stuck with ::new() and also used it to instantiate the list: $list = [Collections.Generic.List[MyData]]::new()

Related

How to create click events for Windows form generated recursively in powershell

I am trying to populate a tree structure in Powershell windows form. The only difference is my tree structure will have textboxes, which I found is not possible using TreeView. So I am using a recursive function to populate the form step by step. However, when I try to add_click on any of the form control, it throws an error saying the object is null. I am new on this and would appreciate any suggestions on how to solve this. The exact message is
Cannot bind argument to parameter 'panel' because it is null.
My functions look like this. The click event binds successfully and calls the toggleVisible function, however at runtime when the click happens it does not pass the correct value to the function.
Function handlePanelClick{
$hash | ForEach-Object{
$_.Label.Add_Click({toggleVisible $_.Panel});
}
}
Function toggleVisible{
Param(
[Parameter(Mandatory = $true, Position = 1)]
[System.Object]$panel
)
$panel.Visible = $false;
}
Assuming that $hash is a hashtable, as the name suggests, it is not enumerated in the pipeline, so that $_ in your $hash | ForEach-Object{ ... } command refers to $hash itself, not its entries.
To enumerate the entries, use $hash.GetEnumerator().
Inside a script block ({ ... }) serving as an event delegate, the automatic $_ variable is not defined.
Use the automatic $this variable to refer to the event sender (the object triggering the event).

Powershell script queues results of an if statement in a do while in a function

I'm using a function that I call from another script. It prompts a user for input until it gets back something that is not empty or null.
function GetUserInputValue($InputValue)
{
do{
$UserValue = Read-Host -Prompt $InputValue
if (!$UserValue) { $InputValue + ' cannot be empty' }
}while(!$UserValue)
$UserValue
return $UserValue
}
The issue is quite strange and likely a result of my lack of powershell experience. When I run the code and provide empty results, the messages from the if statement queue up and only display when I finally provide a valid input. See my console output below.
Console Results
test:
test:
test:
test:
test:
test:
test: 1
test cannot be empty
test cannot be empty
test cannot be empty
test cannot be empty
test cannot be empty
test cannot be empty
1
I can make this work however in the main file with hard coded values.
do{
$Server = Read-Host -Prompt 'Server'
if (!$Server) { 'Server cannot be empty' }
}while(!$Server)
I'm working Visual Studio Code. This is a function I have in another file I've named functions.ps1.
I call this from my main file like this,
$test = GetUserInputValue("test")
$test
When you put a naked value in a script like "here's a message" or 5 or even a variable by itself $PID what you're implicitly doing is calling Write-Output against that value.
That returns the object to the pipeline, and it gets added to the objects that that returns. So in a function, it's the return value of the function, in a ForEach-Object block it's the return value of the block, etc. This bubbles all the back up the stack / pipeline.
When it has nowhere higher to go, the host handles it.
The console host (powershell.exe) or ISE host (powershell_ise.exe) handle this by displaying the object on the console; this just happens to be the way they handle it. Another host (a custom C# application for example can host the powershell runtime) might handle it differently.
So what's happening here is that you are returning the message that you want to display, as part of the return value of your function, which is not what you want.
Instead, you should use Write-Host, as this writes directly to the host, skipping the pipeline. This is the correct command to use when you want to display a message to the user that must be shown (for other information you can use different commands like Write-Verbose, Write-Warning, Write-Error, etc.).
Doing this will give you the correct result, and prevent your informational message from being part of the return value of your function.
Speaking of which, you are returning the value twice. You don't need to do:
$UserValue
return $UserValue
The first one returns the value anyway (see the top of this answer); the second one does the same thing except that it returns immediately. Since it's at the end of the function anyway, you can use wither one, but only use one.
One more note: do not call PowerShell functions with parentheses:
$test = GetUserInputValue("test")
This works only because the function has a single parameter. If it had multiple params and you attempted to call it like a method (with parentheses and commas) it would not work correctly. You should separate arguments with spaces, and you should usually call parameters by name:
$test = GetUserInputValue "test"
# better:
$test = GetUserInputValue -InputValue "test"

Why does powershell executes my property getters

I have a C# project that I'm consuming with PowerShell.
A method returns an object that have not been fully initialized and that calls P/Invoke under the hood through get properties.
When I call the method, the script crashes because of an accessViolationException that is caused by the call of a property on that partially initialized object, but I didn't call it.
Why do Powershell act like this? is there an option to disable that "eager property evaluation"?
The original issue is the one posted here: https://github.com/ZeBobo5/Vlc.DotNet/issues/330
Add-Type -Path ".\Other\VLC\Vlc.DotNet.Core.dll"
Add-Type -Path ".\Other\VLC\Vlc.DotNet.Core.Interops.dll"
$Cameras = New-Object System.Collections.ArrayList
$Test = New-Object System.Uri("rtsp://192.168.0.50/axis-media/media.amp?camera=1")
$Cameras.Add($Test)
$VlcLibDirPath = (Get-Location).Path + ".\Other\VLC\libvlc_x64"
$VlcLibDir = New-Object System.IO.DirectoryInfo($VlcLibDirPath)
$VlcOpt = "--rtsp-user=admin", "--rtsp-pwd=12345"
$Plyr = New-Object Vlc.DotNet.Core.VlcMediaPlayer($VlcLibDir, $VlcOpt)
for ($i=0; $i -lt $Cameras.Count; $i++)
{
$Plyr.SetMedia($Cameras[$i]) #Fails here with System.AccessViolationException
$Plyr.Play
$Plyr.Stop
}
SetMedia returns a VlcMedia, which contains a Statistics property, which is automatically invoked by PowerShell.
Code for VlcMedia can be found here : https://github.com/ZeBobo5/Vlc.DotNet/blob/develop/src/Vlc.DotNet.Core/VlcMedia/VlcMedia.cs
It's difficult to tell without seeing your code, but there's tons of ways this could be happening. If the object is being displayed at all, the properties are probably all being read.
You should change those to methods, and then they won't get read without specifically being invoked.
Or, change your getters to detect an uninitialized object (you should be doing this already if it's possible for consumers to end up with such an object).
Edit:
With your code posted, it's clear:
$Plyr.SetMedia($Cameras[$i]) #Fails here with System.AccessViolationException
SetMedia returns a VlcMedia, which contains a Statistics property, which is automatically invoked by PowerShell.
Everything returned in PowerShell goes somewhere. If you don't assign it or redirect it, it gets sent to the pipeline.
It seems that you don't want or need the output from this method, so you should either assign it to a variable or dispose of the return in one of a few ways:
[null]$Plyr.SetMedia($Cameras[$i])
$null = $Plyr.SetMedia($Cameras[$i])
$Plyr.SetMedia($Cameras[$i]) | Out-Null
(note: piping to Out-Null is the least performant, which is magnified since you're doing this in a loop)
If you want to use the value later (not shown in your code), assign it and use it later.

Powershell returns wrong result

I came across this weird issue in Powershell (not in other languages). Could anyone please explain to me why this happened?
I tried to return a specified number (number 8), but the function keeps throwing everything at me. Is that a bug or by design?
Function GetNum() {
Return 10
}
Function Main() {
$Number10 = GetNum
$number10 #1 WHY NO OUTPUT HERE ??????? I don't want to use write host
$result = 8 # I WANT THIS NUMBER ONLY
PAUSE
return $result
}
do {
$again = Main
Write-Host "RESULT IS "$again # Weird Result, I only want Number 8
} While ($again -eq 10) # As the result is wrong, it loops forever
Is that a bug or by design?
By design. In PowerShell, cmdlets can return a stream of objects, much like using yield return in C# to return an IEnumerable collection.
The return keyword is not required for output values to be returned, it simply exits (or returns from) the current scope.
From Get-Help about_Return (emphasis added):
The Return keyword exits a function, script, or script block. It can be
used to exit a scope at a specific point, to return a value, or to indicate
that the end of the scope has been reached.
Users who are familiar with languages like C or C# might want to use the
Return keyword to make the logic of leaving a scope explicit.
In Windows PowerShell, the results of each statement are returned as
output, even without a statement that contains the Return keyword.
Languages like C or C# return only the value or values that are specified
by the Return keyword.
Mathias is spot on as usual.
I want to address this comment in your code:
$number10 #1 WHY NO OUTPUT HERE ??????? I don't want to use write host
Why don't you want to use Write-Host? Is it because you may have come across this very popular post from PowerShell's creator with the provocative title Write-Host Considered Harmful?
If so, I encourage you to read what I think is a great follow-up/companion piece by tby, titled Is Write-Host Really Harmful?
With this information, it should be clear that as Mathias said, you are returning objects to the pipeline, but you should also be armed with the information needed to choose an alternative, whether it's Write-Verbose, Write-Debug, or even Write-Host.
If I were going to be opinionated about it, I would go with Write-Verbose, altering your function definition slightly in order to support it:
function Main {
[CmdletBinding()]
param()
$Number10 = GetNum
Write-Verbose -Message $number10
$result = 8 # I WANT THIS NUMBER ONLY
PAUSE
$result
}
When you invoke it by just calling $again = Main you'll see nothing on the screen, and $again will have a value of 8. However if you call it this way:
$again = Main -Verbose
then $again will still have the value of 8, but on the screen you'll see:
VERBOSE: 10
likely in differently colored text.
What that gives is not only a way to show the value, but a way for the caller to control whether they see the value or not, without changing the return value of the function.
To drive some of the points in the articles home further, consider that it's not necessarily necessary to invoke your function with -Verbose to get that.
For example, let's say you stored that whole script in a file called FeelingNum.ps1.
If, in addition to the changes I made above, you also add the following to the very top of your file:
[CmdletBinding()]
param()
Then, you still invoked your function "normally" as $again = Main, you could still get the verbose output by invoking your script with -Verbose:
powershell.exe -File FeelingNum.ps1 -Verbose
What happens there is that using the -Verbose parameter sets a variable called $VerbosePreference, and that gets inherited on each function called down the stack (unless it's overridden). You can also set $VerbosePreference manually.
So what you get by using these built-in features is a lot of flexibility, both for you as the author and for anyone who uses your code, which is a good thing even if the only person using it is you.

Powershell function returning instantiated object...kind of?

I'm rather new to Powershell and am working on setting up my profile.ps1 file. I have a few managed DLLs that I use often to maintain processes throughout the day which I'd like to be able to load up with quick function calls. So I created this function in my ps1 file:
function LoadSomeDll
{
[System.Reflect.Assembly]::LoadFrom("c:\wherever\SomeLib.dll")
return new-object "SomeLib.SomeObject"
}
Then, in Powershell, I do this:
PS > $myLibInstance = LoadSomeDll
The problem is that $myLibInstance, though it appears to be loaded, doesn't behave the way I expect it to or if it would if I explicitly load it without the function. Say SomeLib.SomeObject has a public string property "ConnectionString" that loads itself (from the registry, yuck) when the object is constructed.
PS > $myLibInstance.ConnectionString
//Nothing returned
But, if I do it without the function, like this:
PS > [System.Reflect.Assembly]::LoadFrom("c:\wherever\SomeLib.dll")
PS > $myOtherLibInstance = new-object "SomeLib.SomeObject"
I get this:
PS > $myOtherLibInstance.ConnectionString
StringValueOfConnectionStringProperty
Why does this happen? Is there any way that I can return an instantiated new-object from a Powershell function?
Thanks in advance.
The problem you're running into is that your original function is returning an array of objects, not a single object.
One of the tricks in PowerShell is understanding that in a function, every statement which evaluates no a non-void value will be written to the pipeline. The return "value" of a function is simply the contents of the pipeline.
The call to LoadFrom returns an assembly. So the actual return of the function LoadSomeDll is an array containing an assembly and an instance of your object. You're actually calling ConnectionString on the type Object[] and hence it silently fails.
Try switching the function to the following. I intentionally left off the keyword return because it's confusing in the context of powershell.
function LoadSomeDll
{
[System.Reflect.Assembly]::LoadFrom("c:\wherever\SomeLib.dll") | out-null
new-object "SomeLib.SomeObject"
}