-
Notifications
You must be signed in to change notification settings - Fork 223
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
WIP Move to runspace synchronizer from completions #980
WIP Move to runspace synchronizer from completions #980
Conversation
@@ -69,6 +71,11 @@ internal static class AstOperations | |||
return null; | |||
} | |||
|
|||
if (!RunspaceSynchronizer.IsReadyForEvents) |
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.
maybe this should go somewhere else...
Cool. Presumably an F8 run or debug session would cause Activate to be called (to catch Import-Module and Add-Type calls)? |
Very good point! I should add an Activate call to F8 and a few others |
af87237
to
767e1cc
Compare
I've added support for F8... but I wonder... do we even want our own F8 support anymore when we can just use the "Run selected text in active terminal" command pallet action (which we could map to F8) |
pwsh.AddCommand("Import-Module") | ||
.AddParameter("Name", moduleInfo.Path) | ||
.AddParameter("Force") | ||
.AddStatement(); |
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.
Wouldn't this make the psm1 run twice?
Also in Windows PowerShell, static class methods have runspace affinity to the last runspace that processed the type definition. This second import will change the runspace that static class methods run in, even when invoked in the origin runspace.
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.
This affinity issue has been fixed in PowerShell Core. See PowerShell/PowerShell#4209
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.
Yeah I did qualify it, I should have been more clear though. That said, afaik there aren't plans to drop support for it anytime soon right?
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.
Right, there is no plan to port the fix back to Windows PowerShell.
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.
What I'm getting at is PSES still has to support Windows PowerShell. So it seems to me that the choices are:
- Break static methods in Windows PowerShell
- Maintain both this implementation and the current system
- Stop supporting Windows PowerShell
Or am I missing something?
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.
Or, 4. Only apply this optimization to PowerShell Core. On Windows PowerShell, keep calling into the integrated console for intellisense operations
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.
Just to clarify, that's what I meant by item 2.
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'm currently now doing 2. If you're using PowerShell Core, you will get this optimization.
Yeah we do, PSRL custom key handlers often wreak havoc with bulk input. Plus if we ever want to support injecting support for |
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 know this is WIP but I have a couple hard stops, unfortunately I think a few of them are not fixable.
{ | ||
// If the variable is a magic variable or it's type has not changed, then skip it. | ||
if(POWERSHELL_MAGIC_VARIABLES.Contains(variable.Name) || | ||
(variableCache.TryGetValue(variable.Name, out Type value) && value == variable.Value?.GetType())) |
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.
Completion isn't just done by type, member inference of a property expression is done by value (which would also be an issue for properties with a thread static backing field). This also doesn't account for PSObject
's that may have gained additional members.
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.
Completion isn't just done by type, member inference of a property expression is done by value ... This also doesn't account for PSObject's that may have gained additional members.
True, absolutely valid concerns. Maybe check on the object equality instead of type equality here.
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.
Maybe check on the object equality instead of type equality here.
You could do that, but thread static is still a concern.
Another issue to consider is any properties that attempt to marshal the call back to their origin thread, such as CommandInfo.Parameters
.
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.
now checking object equality
pwsh.AddCommand("Import-Module") | ||
.AddParameter("Name", moduleInfo.Path) | ||
.AddParameter("Force") | ||
.AddStatement(); |
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.
This will only import the psm1
, so it will miss nested/required modules, required assemblies, and any command filtering. We also can't actually determine if the module info object was imported via the psm1
directly or via the psd1
, which could yield significantly different results.
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.
This will only import the
psm1
Can you elaborate on this? When a module is imported through a manifest, the ModuleInfo.Path
points to the psd1
file. For example,
PS:34> $s = gmo Microsoft.PowerShell.Management
PS:35> $s.Path
F:\pscore70\Modules\Microsoft.PowerShell.Management\Microsoft.PowerShell.Management.psd1
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.
@daxian-dbw for some reason... for script modules, the path points to the psm1
:( Patrick is right.
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.
Ah, I see. The Microsoft.PowerShell.Management
is a bad example. The assembly is listed as a NestedModule
, so that's literally a manifest module :) If the RootModule
or ModuleToProcess
is specified, then the path would point to the specified module file I guess.
A bit more logic is needed here to find out if there is a module manifest file.
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.
We can do something similar to:
$psmoduleinfo = Get-Module PowerShellGet
Join-Path $psmoduleinfo.ModuleBase "$($psmoduleinfo.Name).psd1"
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.
Problem is you can't tell whether they've imported the psm1
or the psd1
.
One could, for instance import a psm1
directly to get intellisense on functions that are typically filtered by the psd1
. Conversely if you always assume psm1
, you miss out on nested/required modules/assemblies and type/format files.
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.
For the psd1
vs. psm1
case, we can tell which one was used in loading by inspecting the version. If you are importing a psm1
file, the version would be 0.0
. For a psd1
, it's very unlikely 0.0
.
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.
Alternatively, we could override Import-Module in order to get the exact parameters used for Import-Module
and then run the real import-module in each runspace.
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'm now importing the module base if the version is greater than 0.0
. overriding Import-Module
was more work than it was worth.
// Set or update the variables. | ||
foreach (PSVariable variable in newOrChangedVars) | ||
{ | ||
targetEngineIntrinsics.SetVariable(variable); |
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.
If the debugger is currently stopped in a different session state (like a break point in a module function), this will throw all of the module's state into the global state for the completion runspace. Also stepping through something like Pester
will dump a lot of vars into completion for the rest of the session.
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.
Good point. Need to try out this in debugging scenario.
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.
@rjmholt and I have brainstormed about this. For starters, we will skip synchronizing while the initial runspace is debugging. The belief here is that your main focus in debugging is reading, not writing. Unfortunately, this means stuff like $TestName
and $TestDrive
ala Pester
will not be available in completions while debugging... but we think it's not critical enough for this PR.
Eventually, it would be nice to get to a point where after you finish debugging, it triggers a "resync -force" which blows away the target runspace and remakes it with a clean sync so we're back to what we would expect after debugging.
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.
The belief here is that your main focus in debugging is reading, not writing.
Writing at a breakpoint, along with $Host.EnterNestedPrompt()
in general (which will also have the same problem) are the only ways to get intellisense based on a module's session state. I use this extensively.
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've disabled syncing while debugging.
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.
Sorry for the sadness, like I said, eventually I'd like to do the "trigger resync force" - both for debugging and nested prompts but it's not the highest priority for the majority of customers and I 'm not quite sure how I would approach it just yet.
Any ideas?
pwsh.AddCommand("Import-Module") | ||
.AddParameter("Name", moduleInfo.Path) | ||
.AddParameter("Force") | ||
.AddStatement(); |
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.
This won't work for argument completers that depend on module state. Also not related to this line but argument completers not loaded by a module won't be transferred either.
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.
This will cause some modules to throw. iirc a commonly installed version of the VMWare module does not handle being imported into multiple runspaces well. There's a few modules like that.
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.
Also not related to this line but argument completers not loaded by a module won't be transferred either.
Can you elaborate the scenario for this one? The assembly that implements a argument completer not getting loaded by the module?
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.
This will cause some modules to throw. iirc a commonly installed version of the VMWare module does not handle being imported into multiple runspaces well. There's a few modules like that.
This is a valid concern. Normally a module shouldn't be made only work when importing once in a process ... but we need to evaluate how bad it would be (a black list?)
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.
Also not related to this line but argument completers not loaded by a module won't be transferred either
@rjmholt thought of the idea of overriding Register-ArgumentCompleter
which will forward the call to the target runspace. That said, perhaps we should also ask ourselves, could running argument completers for Intellisense be a potential security concern and a valid thing to skip in the editor intellisense at least.
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.
Can you elaborate the scenario for this one? The assembly that implements a argument completer not getting loaded by the module?
Sorry that was confusing in the context of this line, I mean more generally. Like someone just dot sourcing a script or manually running Register-ArgumentCompleter
.
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.
That said, perhaps we should also ask ourselves, could running argument completers for Intellisense be a potential security concern and a valid thing to skip in the editor intellisense at least.
Considering they have to run code in order to register an argument completer I don't think it's a security issue. We do automatically import modules during intellisense which could register an argument completer. That might be a security issue, but more because loading a module runs code, not necessarily related to the fact that a completer could be loaded.
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 may override this cmdlet. It's not as complicated and high profile as Import-Module
93c7df7
to
1cbeb60
Compare
I don't think this change is the right path towards a responsive PSES. I believe this will lead to WhyPowerShell's thread managementMany PowerShell related objects expect to be used on the same thread that they were created. When This is in my opinion the area that represents the most risk. Unfortunately this is also the least State based completionThere are too many peices of state that completion relies on that cannot be easily replicated. In the same vein we have things that use Double invocation of psm1In general I'm very uncomfortable with the idea of running a users code more times than they expect. Changing the session state of completionA feature request that's come up a few times is intellisense for non-public functions, classes, and function Enter-ModuleScope {
. (gmo Module) { $Host.EnterNestedPrompt() }
} And if they ran that it would all just work. I mentioned we could even build in a "Select Project" HowIt seems to be the opinion that the problems PSES face are due to the fact that we only have one Currently we have a single thread that does the majority of message processing. This is enforced with My recommendation would be to remove the sync context completely and replace it with three separate Message managementA separate thread that is dedicated entirely to reading and writing messages, and dispatching. This would also make implementing an async queue significantly easier, as well as cancellations. Pipeline controllerThis thread would be the same one create by the main runspace. The class would be very similar to Dispatch poolThis pool would be where the majority of analysis is done. Any parsing, AST analysis, file enumeration, If a task requires access to the pipeline thread then it would queue the request for the pipeline Jobs created by the message manager would be picked up by threads in this thread pool. Once completed |
Agreed. Maybe it's just me but the current design can be really hard to reason about. It does seem like at the very least a re-design of the message handling code is needed (probably more as you've scoped out). The fact that PSES just gets further and further behind processing requests that VSCode doesn't need anymore is a bit of a killer. We need to be able to process cancelRequests and avoid starting unnecessary work. |
@TylerLeonhardt, @daxian-dbw and I got a chance to discuss this at length yesterday. This is one of those tricky areas because the ideal architecture of a language server collides with PowerShell's assumed interactive experience in bad ways. The idea for separating services out into different runspaces came about because of the native readline problem, which we've been chewing on for a while as to how to fix. Considering the way PSScriptAnalyzer runs in another runspace (in part so it doesn't block anything), it seems like a valid experiment to offload completions as well, especially since they represent a core experience that users feel the most pain with. We actually discussed offloading them into another process first (and using Our main motivating factors for a direction to take are:
Moving completions to a different runspace from console execution will buy us these things because it returns the integrated console to being almost entirely an ordinary PowerShell console, the divergence from which has been the nexus of the majority of our important and hard-to-fix issues. However, the downsides you raise @SeeminglyScience are definitely legitimate and I think worth going through:
Although we could also try to move to a better-informed thread architecture to manage the pipeline thread better, there are issues with that:
Ultimately there's no way to solve the problem perfectly here; PowerShell is the only programming language I've used that has a synchronous completion ecosystem, and it doesn't work well with the asynchronous IDE ecosystem. But forced to weigh up the integrated console not working like PowerShell against breaking completion experiences, I think making the console work comes first and then we can get completions working again; we might not be able to make PowerShell's existing completion logic hook in perfectly, but most of it will work and we can make sure we provide the right mechanisms for the rest. Eventually I think we have to confront that PowerShell's native completion experience isn't always what we want for our IDE experience (we've closed numerous completion bugs/requests as wontfix because they come from PowerShell, we can only provide completions for the executing PowerShell environment) and that IDEs everywhere are confounding PowerShell's native completion model. So with all that said, we want to give this a try and get user feedback on it. We think this will solve some otherwise insoluble problems, buy us a few other things along the way, open up some possibilities to reduce our technical debt, and still offer possibilities for yak-shaving on some of the foreseeable problems it might cause. Once we've got some usage, if the issues turn out to be too much we can go back to the drawing board. Thanks for your really detailed write-up @SeeminglyScience ‐ we really value it and it's important, especially for a problem that has no obvious solution and where any resolution is likely to be contentious. Ideally that information will mean we can (1) identify and work around areas where issues might arise and (2) fail fast if we determine this is not going to work. /cc @SteveL-MSFT @SydneyhSmith @joeyaiello for awareness |
@rjmholt Thank you for your detailed response. I'd like to mention before continuing that I understand that a lot of what I'm going to quote is simply explaining the motivation behind pursuing this avenue. While I will be providing counter points, I want to make it clear that I fully understand the motivation and indeed the appeal of the idea. I in no way intend to disparage anyone who worked on it.
I can see where you're coming from, but it only makes sense with PSSA because it's meant to be static. Yes it's true that some things do indeed use state like command lookup, but it creates the state from scratch. It's not trying to mimic an existing active runspace filled with objects that will be in use.
I'm not following you here, neither of these are threading related. I've explained this before but the former is due to a flaw in our logic where we're not resetting With the latter I'm not 100% sure what you mean, but if it's a *nix specific issue then it's likely related to the above. If it's not related to the above, then it's a flaw in the really hacky async version of Are these issues no longer present with the changes in this PR?
From my perspective this accomplishes the opposite of what was intended here. The whole process of trying to clone all of the state in a runspace seems a lot harder to understand and debug than managing access to one of them. Especially once you start having to worry about proxy commands.
Again I know this is explaining original motivation but this PR breaks features we already have. Also while I agree that fixing our thread management would make it easier to add new features, I believe if it's done this way then we will encounter more things that have become impossible to do.
My concern here is that by lifting objects whole cloth from one thread to another we will be increasing the probability of this and reducing how easy it is to troubleshoot. Right now if anything like that is accessed on another thread, it's done on a thread without a default runspace. That means in most instances it's either going to silently do nothing or throw. I don't recall any instances where we do that though. Conversely, PowerShell often reacts differently when there is a default runspace for the current thread. In a lot more situations it tries to send it back. In some instances this may manifest as additional dead locks, in others it may go as far as corrupting state of something currently running. Due to PowerShell using the
That's an excellent idea and I agree 100%. I believe this fits very well with what I proposed. All we would need to do is start up the pipeline thread and the controller for it. Omnisharp's LSP base would run where ever it wants to, the pipeline thread wouldn't be on the thread pool so I doubt it would conflict at all. Queuing a job for the pipeline thread could be done from any of the registered async handlers, that whole process could easily be abstracted away. At the end of the day, the pipeline thread controller just looks like a normal synchronous class that's taking objects that represent tasks from a collection. All the handlers would need to know how to do is add to that collection, and that could all be abstracted into a few methods. I realize that I'm over simplifying it a bit but I believe it is well within the realm of feasiblity.
I'm not going to try to say that properly managing the pipeline is easy, it's not and acknowledge that. I don't agree that that challenge the nexus of our problems though. We have an architecture that was built piecemeal, mostly by one individual (as fantastic as he is, it's still a whole lot). As a result there's a lot of choices that don't make sense for the product the way it is today, and a whole lot of simple human error related bugs.
I know at the very least dynamic parameters and type inference of properties will also evaluate during completions. Those have a higher probability of causing inconsistency. There may also be more, I have not gone looking through all of completion and type inference analysis, but it wouldn't surprise me if there was more.
In this one I'm not talking about a badly written module. Here's the series of events:
It's more common with third party modules, especially those that require a connection or credentials to be established.
I'm having a hard time seeing how. Are we going to special case every instance we come across?
Not in the default rules. To be fair, if I was more involved in that project when that was being discussed I probably would have argued against it there as well, but at least you have to explicitly enable it.
Yeah that's the most well known one but I think it's more common than you'd think. I didn't realize until recently that one of my modules breaks on import in a second runspace. It's pretty easy to overlook doing something statically in a problematic way, even for someone like me who is hyper aware of the issues it can cause. Realistically, no one tests for it.
Based on the small list of issues I was able to come up with, the complexity of this change seems to be increasing exponentially. There is a significant amount of special casing and effort in general being put in to simply approach the same level of functionality we have today.
If this would solve the readline issues then so would better pipeline thread management, though it would be surprising to me if either did.
That makes sense, and I appreciate the honesty. I do think you're overestimating the expertise required, or underestimating the expertise that will be required for this change. Based on the couple of issues I've thought up off the top of my head we're already creating proxy commands and even entire dummy modules. Plus whoever is going to be troubleshooting all of the thread-specific object related issues is going to need to know a thing or two about threading and obscure internals of PowerShell. If the PowerShell team doesn't have the resources to dedicate towards a rewrite of threading, then maybe work on fixing some of individual crashes and dead locks. Maybe just focus on fixing PSRL *nix issues (which I'm pretty confident aren't really related to our threading model) so we can finally get 2.0 out the door.
I'm probably arguing semantics here but I'd consider that "Resolution-External" more than I would "wontfix". They are typically added as issues to PowerShell, though yes, completion enhancements are often left to the community to implement. Even so there has been a not insignificant amount of community contributions to completion, some even specifically targeting the scenario of an editor. For instance this incredible enhancement contributed by @powercode.
There's a few issues that I've brought up that have been deemed acceptable, and I disagree with that assessment. I believe this change is significantly more complicated than a proper fix and due to the nature of the fix any testing period would need to be pretty lengthy. I'd likely be more receptive to such an experiment if we weren't already in the middle of an incredibly long preview release. At the end of the day, if the decision is made to continue with this regardless, then it is what it is. If you run into issues or have questions I will of course continue to be available, but I can't approve this PR. |
I’m going to close this. Since .NET Core did a huge refactor to the Console APIs in .NET Core 3.0, PowerShell 7 with PSRL in PSES no longer has the native prompt canceling issue because one thing they fixed was what child processes use for termios settings. Since macOS & Linux users are small compared to Windows, and they are more likely to update their version of PowerShell, we will not attempt to get 6.1 & 6.2 working with the prompt issue. Instead, we will recommend moving to PS 7 which will most likely be GA before 2.0 of the PowerShell extension is GA. Since this PR was opened to help fix that in addition to dealing with threading issues, we’ve lost half the weight of it’s usefulness (which is a good thing!) so I’m going to close it. The path forward will be moving to Omnisharp’s LSP implementation which we think will give us a solid foundation of how we should handle messages. From that, we still need to figure out how messages will properly handle a single runspace. What’s unclear at the moment is what we will use for the debug protocol. More investigation needs to be done. |
Big shout out to @rjmholt for finding the CoreFX changes! |
BTW, here's where they changed System.Diagnostics.Process.Start() that fixed us in PS 7. One question is: do we need to make any changes to UnixConsoleEcho or our use of it to integrate with these changes better? In particular, there are some thread safety things addressed in the new corefx code that weren't there before. |
We might be able to drop Lets hope 🤞 |
Yeah that's what we were hoping too, but basically I want to understand more about the purpose of UCE in the first place, since it predates the PSReadLine integration; wasn't it also used in the old readline to address a particular issue with Up and Left keys? |
So for a long time debugging was mostly broken on *nix because you can't throw To get around that we wrote the less than ideal implementation of So initially, it's sole purpose was to disable echo. That said, it's entirely possible that the other attributes we're setting may still be required to enable Best way to test it is to just remove the calls to |
This sounds like fun. I can maybe give it a go tomorrow. |
This
RunspaceSynchronizer
is used to "sync" the state of one runspace to another.It's done by copying over variables and reimporting modules into the target runspace.
It doesn't rely on the pipeline of the source runspace at all, instead leverages Reflection to access internal properties and methods on the Runspace type.
Lastly, in order to trigger the synchronizing, you must call the
Activate
method which will trigger the syncing on the nextOnIdle
event. This is added to thePSReadLine
key handler forENTER
.Let me know what you think of the overall design. From my tests, it appears to be a bit faster 🙂
The idea is, we separate everything that uses the primary runspace (where PSRL likes to hang out)... and if we can achieve that, we can let PSRL take over the whole thread if it wants... Also, by moving the completions to another runspace, when we add async message handling, Completions wont be blocked by access to the default runspace.
TODO: add testsadded