-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Fix powershell class to use the current EngineSessionState for execution. #2837
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,9 +43,26 @@ internal SessionStateKeeper() | |
|
|
||
| internal void RegisterRunspace() | ||
| { | ||
| // it's not get, but really 'Add' value. | ||
| // ConditionalWeakTable.Add throw exception, when you are trying to add a value with the same key. | ||
| _stateMap.GetValue(Runspace.DefaultRunspace, runspace => runspace.ExecutionContext.EngineSessionState); | ||
| SessionStateInternal ssInMap = null; | ||
| Runspace rsToUse = Runspace.DefaultRunspace; | ||
| SessionStateInternal ssToUse = rsToUse.ExecutionContext.EngineSessionState; | ||
|
|
||
| // Different threads will operate on different key/value pairs (default-runspace/session-state pairs), | ||
| // and a ConditionalWeakTable itself is thread safe, so there won't be race condition here. | ||
| if (!_stateMap.TryGetValue(rsToUse, out ssInMap)) | ||
| { | ||
| // If the key doesn't exist yet, add it | ||
| _stateMap.Add(rsToUse, ssToUse); | ||
| } | ||
| else if (!ssInMap.Equals(ssToUse)) | ||
| { | ||
| // If the key exists but the corresponding value is not what we should use, then remove the key/value pair and add the new pair. | ||
| // This could happen when a powershell class is defined in a module and the module gets reloaded. In such case, the same TypeDefinitionAst | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder, could it happen for any other reason. I think this code is better, thank you for taking time to dig into it! 👍
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This scenario involves switching |
||
| // instance will get reused, but should be associated with the SessionState from the new module, instead of the one from the old module. | ||
| _stateMap.Remove(rsToUse); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if an instance of the class still exists? It would start executing against the new session state instead of the one in which is was created. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For me, at least, the main use-case for reloading of modules containing classes is to re-run unit tests against them after changes to the module/classes. Generally-speaking those unit tests re-create their objects anyway. I'm not sure what should happen to existing objects when their originating class definitions are changed and reloaded, but I also haven't thought of a good reason to use objects across reloads so I don't expect to rely on the behavior - whatever it is.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree this is not a primary scenario, but it will happen (probably not intentionally) so we need to decide what should happen. For example, could it be a security issue to somehow get some instance to execute attackers code?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For existing class instances, I think they will continue to run against the original session state because the session state to be used is stored in Here is what I got in such scenario with the fix: Run existing class instance after updating the You can see that
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My question is what will happen if a reload module crash?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, you won't get any new instances of the class, but existing class instances will continue to work since it's running against the old module instance.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's good!
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will still be confusing with functions that accept parameters of module class type. The old instances will not bind and the error message is not helping to understand what is happening. I think this is a fairly common use case, or at least we want it to be (i.e. interactive and iterative development of a module).
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @powercode Do you mean continue to use old class instances after reloading the module? I don't think that's a common use case in module development. For most of time, use of old instances should not be intentional after the module gets reloaded. |
||
| _stateMap.Add(rsToUse, ssToUse); | ||
| } | ||
| // If the key exists and the corresponding value is the one we should use, then do nothing. | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,3 +71,43 @@ Import-Module Random | |
| } | ||
|
|
||
| } | ||
|
|
||
| Describe 'Module reloading with Class definition' -Tags "CI" { | ||
|
|
||
| BeforeAll { | ||
| Set-Content -Path TestDrive:\TestModule.psm1 -Value @' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's sometimes handy to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The dynamic modules created by
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just tried using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @daxian-dbw I couldn't reproduce your last post. I wrote another test that creates a dynamic module with arguments, then creates (overwrites?) another identical module with different arguments, then creates another with a slightly different class definition. The results are the same with PowerShell 5.0, 5.1, and 6.0.0-alpha.13:
This behavior seems very similar to how I understand reloading of file-backed modules behaves.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It seems Yes. >$a=New-Module -Name ReloadingTest -ScriptBlock {
$passedArgs = $args
class Root { $passedIn = $passedArgs }
function Get-PassedArgsRoot { ([Root]::new()).passedIn }
function Get-PassedArgsNoRoot { $passedArgs }
} -ArgumentList 'abc'
> $a.GetExportedTypeDefinitions()
Key Value
--- -----
Root class Root { $passedIn = $passedArgs }
>[root]
Unable to find type [root].
At line:1 char:1
+ [root]
+ ~~~~~~
+ CategoryInfo : InvalidOperation: (root:TypeName) [], RuntimeException
+ FullyQualifiedErrorId : TypeNotFound
> class Root { $passedIn = $passedArgs }
> [root]
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False Root System.Object
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With regard to reload modules it seems there are differences between script, binary (assembly) and dynamic modules which require test them all.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alx9r I can reproduce it in both PSv5.1 and PSv6.0.0-alpha.13. I opened the issue #2841 for this. PSv5.1 PSv6.0.0-alpha.13
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this test loads the module from the file, would not generate it every time and put into the asserts folder?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here I thought about the test performance only. |
||
| $passedArgs = $args | ||
| class Root { $passedIn = $passedArgs } | ||
| function Get-PassedArgsRoot { [Root]::new().passedIn } | ||
| function Get-PassedArgsNoRoot { $passedArgs } | ||
| '@ | ||
| $Arg_Hello = 'Hello' | ||
| $Arg_World = 'World' | ||
| } | ||
|
|
||
| AfterEach { | ||
| Remove-Module TestModule -Force -ErrorAction SilentlyContinue | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we do import-reload-remove for modules then would it be more purely do so in individual runspaces?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. Will make the change.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On a second thought, I'm not sure if it's really worthy to run each
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. My only fear was that these tests can influence each other or be dependent on environment.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It shouldn't affect each other and pollute the runspace, but if we find so, say it causes instability to other tests, then we got another bug to fix 😄
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| } | ||
|
|
||
| It "Class execution reflects changes in module reloading with '-Force'" { | ||
| Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_Hello | ||
| Get-PassedArgsRoot | Should Be $Arg_Hello | ||
| Get-PassedArgsNoRoot | Should Be $Arg_Hello | ||
|
|
||
| Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_World -Force | ||
| Get-PassedArgsRoot | Should Be $Arg_World | ||
| Get-PassedArgsNoRoot | Should Be $Arg_World | ||
| } | ||
|
|
||
| It "Class execution reflects changes in module reloading with 'Remove-Module' and 'Import-Module'" { | ||
| Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_Hello | ||
| Get-PassedArgsRoot | Should Be $Arg_Hello | ||
| Get-PassedArgsNoRoot | Should Be $Arg_Hello | ||
|
|
||
| Remove-Module TestModule | ||
|
|
||
| Import-Module TestDrive:\TestModule.psm1 -ArgumentList $Arg_World | ||
| Get-PassedArgsRoot | Should Be $Arg_World | ||
| Get-PassedArgsNoRoot | Should Be $Arg_World | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this assignment superfluous?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's more of a coding habit. I always make variable assigned before use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:-) I always thought that Microsoft is strict in templates. It turns out that Microsoft indulge habits
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@iSazonov Actually I suggest to always assign variables before use. I believe that will help prevent bugs. Sometimes C# compiler will also make you explicitly assign a variable before using it when it cannot figure out from the code path that it will be assigned for sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! It is good habit. :-)