Skip to content

Compile += / -= to bind & handle event (de)registration directly#12930

Closed
vexx32 wants to merge 4 commits intoPowerShell:masterfrom
vexx32:AddEqualEventHandler
Closed

Compile += / -= to bind & handle event (de)registration directly#12930
vexx32 wants to merge 4 commits intoPowerShell:masterfrom
vexx32:AddEqualEventHandler

Conversation

@vexx32
Copy link
Copy Markdown
Collaborator

@vexx32 vexx32 commented Jun 10, 2020

PR Summary

Permits the following syntax:

$scriptblock = { Write-Host 'event triggered' }
$ps = [powershell]::Create()
# Add event handler
$ps.InvocationStateChanged += $scriptblock

# Remove event handler
$ps.InvocationStateChanged -= $scriptblock

Changes

  • Expose PSEvent static and instance members of classes & enable tab completion for them.
  • Add handling in the compiler to bind event (de)registration for -= or += only if the LHS is a MemberExpressionAst, combined with a runtime check for the LHS value being PSEvent.
  • Add a new binder type which will map the event member to its add/remove method as appropriate and then offload to PSInvokeMemberBinder with that method name.

PR Context

See #12926 for some additional context, but my thoughts are essentially: events are currently not discoverable and very annoying to work with in PowerShell, sometimes requiring reliance on implementation details.

I do not believe this can be considered a breaking change, since the only place it applies is specifically for event members, which were previously pretty much entirely inaccessible prior to this change.

Resolves #12926

With respect to tooling, there shouldn't be any breaking points, but event members will be offered in tab completion results with these changes, which may be considered unexpected in some circumstances.

PR Checklist

@vexx32 vexx32 requested a review from daxian-dbw as a code owner June 10, 2020 04:01
@ghost ghost assigned iSazonov Jun 10, 2020
@vexx32
Copy link
Copy Markdown
Collaborator Author

vexx32 commented Jun 10, 2020

@TravisEz13 is it intentional that test results do not appear to be publicly viewable anywhere? Azure Pipelines reports a test pass percentage, but all the links lead to a blank page, and the Tests tab is missing from those pipeline jobs.

The few tests I could find failing in the log don't appear related at all, but I couldn't find all of them (and I'm not sure if there's a different way I need to be re-running failed jobs at the moment, too).

@iSazonov iSazonov added the CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log label Jun 10, 2020
@iSazonov iSazonov requested a review from PaulHigin June 10, 2020 04:51
@iSazonov
Copy link
Copy Markdown
Collaborator

@vexx32 There is a reference to test results
image

@TravisEz13
Copy link
Copy Markdown
Member

@vexx32 There seems to be some change to Azure DevOps. The actual pipeline was not changed at all. The Azure DevOps app was just installed to allow integration with more GitHub bots. I suggest opening feedback in Azure DevOps, filing an issue with the link and mentioning me. I can follow up from there.

@vexx32
Copy link
Copy Markdown
Collaborator Author

vexx32 commented Jun 10, 2020

@iSazonov yep! But I can only see the log files, which lack any kind of summary as to exactly which failed. The logs are thousands of lines long, surely we're not expected to painfully read through all of them for a few tiny - characters and attempt to infer which tests might have failed. 🤔

Oh, hold on... okay on one of the result pages I can see it, but the Azure Pipelines pages themselves don't seem to have a proper summary of results from what I can see.

@TravisEz13
Copy link
Copy Markdown
Member

TravisEz13 commented Jun 10, 2020

@vexx32 I can definitely see the logs though, in an incognito window. Just the test tab is gone.

@TravisEz13
Copy link
Copy Markdown
Member

@vexx32 we have moved to stage build for windows a long time ago... We moved the rest of the pipelines to stages... You may just be seeing the stage GUI and not be used to it.
image
You have to click into the failed stage to see the results.

@vexx32
Copy link
Copy Markdown
Collaborator Author

vexx32 commented Jun 10, 2020

@TravisEz13 filed here: https://developercommunity.visualstudio.com/content/problem/1072124/individual-test-results-failures-not-shown-in-buil.html

Didn't see any links pointing to a GH issues thing or anything, hopefully that's sufficient for now.

Yeah, that's about what I'm seeing. Makes it a bit tricky since our test output is very condensed and at most I just see rows of +++++++ / +++-+ marking individual tests passing/failing. Anyway, located the failing test for now... should have expected that one. 😁

I can see the stages and the logs individually, but... not super useful; tests tab just inexplicably gone 🙂

Developer Community for Visual Studio Product family

Comment on lines 969 to 970
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you refactor the code you could move the Equals in separate if block and convert the switch to a switch expression.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use s_ prefix for statics. Below too.

Suggested change
private static readonly PSEventDelegateBinder _addInstanceBinder = new PSEventDelegateBinder(
private static readonly PSEventDelegateBinder s_addInstanceBinder = new PSEventDelegateBinder(

@TravisEz13
Copy link
Copy Markdown
Member

@vexx32 to find failure search for [-]. Going offline now...

@vexx32 vexx32 force-pushed the AddEqualEventHandler branch from e6945fe to 2746cb4 Compare June 10, 2020 15:18
Comment on lines 2678 to 2684
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We think we should explicitly use InvariantCulture for formatting in the two places. It is our common pattern in Engine.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppendJoin() doesn't appear to have an overload that takes a format provider, but will do for AppendFormat().

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @iSazonov is referring to the string interpolation used in the Select delegate.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant both - AppendFormat and the string interpolation.

Comment thread src/System.Management.Automation/engine/parser/Compiler.cs Outdated
Comment thread src/System.Management.Automation/engine/runtime/Binding/Binders.cs Outdated
Comment on lines 2143 to 2144
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same about InvariantCulture.

Comment on lines 2676 to 2677
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var invokeMethod = new MethodInformation(
events[0].EventHandlerType.GetMethod("Invoke"), parametersToIgnore: 0).method;
MethodInfo invokeMethod = events[0].EventHandlerType.GetMethod(nameof(Action.Invoke));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
internal T GetDotNetEvent<T>(object obj, string methodName) where T : PSMemberInfo
internal T GetDotNetEvent<T>(object obj, string eventName) where T : PSMemberInfo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return GetDotNetEventImpl<T>(obj, methodName, predicate: null);
return GetDotNetEventImpl<T>(obj, eventName, predicate: null);

@vexx32 vexx32 force-pushed the AddEqualEventHandler branch from 0b1a232 to 4408391 Compare June 11, 2020 13:25
@vexx32
Copy link
Copy Markdown
Collaborator Author

vexx32 commented Jun 11, 2020

MacOS Failure is a known issue and not a new failure: #12935

@ghost ghost added the Review - Needed The PR is being reviewed label Jun 18, 2020
@ghost
Copy link
Copy Markdown

ghost commented Jun 18, 2020

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Mainainer, Please provide feedback and/or mark it as Waiting on Author

@vexx32
Copy link
Copy Markdown
Collaborator Author

vexx32 commented Jun 24, 2020

@daxian-dbw friendly ping for review 😊

@daxian-dbw
Copy link
Copy Markdown
Member

@daxian-dbw friendly ping for review 😊

Sorry for the delay ... reviewing this one now.

@ghost ghost removed the Review - Needed The PR is being reviewed label Aug 4, 2020
@ghost ghost added the Review - Needed The PR is being reviewed label Aug 15, 2020
@ghost
Copy link
Copy Markdown

ghost commented Aug 15, 2020

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably isn't needed -- it's probably simpler to let the GC just collect the whole string builder in one go when the method returns

Comment on lines 2675 to 2699
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some interspersal of string building and formatting in here, along with some LINQ, all inside a non-threadsafe lazy logic block embedded in a property.

I think some possibilities to consider are:

  • Move the string generation into its own method that just returns a value
  • Use a Lazy<string> like EventDefinition => _eventDefinitionLazy.Value
  • Fully embrace either the string builder or the formatter, depending on whether we're trying to optimise for performance or readability

So for example:

private Lazy<string> _eventDefinitionLazy = new Lazy<string>(CreateEventDefinition);
internal string EventDefinition => _eventDefinitionLazy.Value;

private string CreateEventDefinition()
{
    var invokeMethod = new MethodInformation(
        this.events[0].EventHandlerType.GetMethod(nameof(Action.Invoke)),
        parametersToIgnore: 0);

    var sb = new StringBuilder()
        .Append("event ")
        .Append(ToStringCodeMethods.Type(events[0].EventHandlerType))
        .Append(' ')
        .Append(events[0].Name)
        .Append('(');

    ParameterInfo[] invocationParameters = invokeMethod.GetParameters();
  
    if (invocationParameters.Length > 0)
    {
        ParameterInfo currParam = invocationParameters[0];
        sb.Append(ToStringCodeMethods.Type(currParam.ParameterType))
            .Append(' ')
            .Append(currParam.Name);

        for (int i = 1; i < invocationParameters.Length; i++)
        {
            currParam = invocationParameters[i];

            sb.Append(", ")
                .Append(ToStringCodeMethods.Type(currParam.ParameterType))
                .Append(' ')
                .Append(currParam.Name);
        }
    }

    return sb.Append(')').ToString();
}

Comment on lines 3646 to 3634
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach (var @event in eventTable.Values)
{
foreach (EventCacheEntry @event in eventTable.Values)
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var expressionType = tokenKind switch
ExpressionType expressionType = tokenKind switch

Comment on lines 985 to 986
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to make this static, if we just have to pass this back in?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not anymore! I wrote it this way initially as I wasn't sure if I'd need to callback to this from the binder or some such witchcraft, but that's definitely not required here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// GetAddMethod().This lets us handle event delegates with += or -=, but we should only make this check
// GetAddMethod(). This lets us handle event delegates with += or -=, but we should only make this check

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggests that this will only work when events are given in the form:

$x.Event += $handler

But in the comment below you give an alternate form:

$e = $x.Event
$e += $handler

Should we expect that to behave differently? If so, why? Is that consistent with other similar behaviours in PowerShell today?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's a good point! I'm not sure, I guess I'd expect that that behaves the same. 🤔

I was mostly mimicking C#'s syntax, not sure saving the event to a var works there, but I guess I'd expect that works also.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking back at this, the example you're referring to is more illustrating how the $object.EventName += $sb expression is compiled (it's saved to a temporary variable first) rather than what would literally work.

Do we want to allow this to work for PSEvent instances saved in a variable as well? My concern here was mainly that the $object.EventName += $b is likely to be the most common usage, and I didn't want to bog down every $var += $sb expression with a runtime check for $var -is [PSEvent] if I could avoid it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at Sharplab, I can see the C# compiler actually completely prevents you assigning an event handler reference to a variable in the first place:
image

I don't think we need to be quite so strict in PS, but I think folks trying to use += / -= for event handling will expect similar behaviour.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, absolutely. I actually think you're right, but wanted to ask the question. (I referred to the compilation example because I knew it was a compilation example, but it was why I thought of it).

Events are strange second-class citizens in C# (I don't believe "event handler reference" is a meaningful concept for C#). But with that said, I'm not sure how many people we help by making them first-class in PowerShell (at the cost of things like runtime performance).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for how it's currently implemented. It's consistent with C# and also with how PSMethod's are handled. Like you can $method = $obj.ToString but you can't then do $method()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the cast needed for here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't recall... I'll try it without, but I probably copied this pattern from elsewhere in the compiler.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daxian-dbw may know why it's needed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the cast needed for here?

More than likely it's because expressions don't implicitly box, the cast here will do that. Also matching expected types is super finicky (like a Expression.Block's return type, or a variable) which casting everything to object greatly simplifies.

That's why there's a ton of casting, but I don't know off the top of my head of getExpr would already be cast. (Side note, it's also why byref/likes will probably never be supported 🙁)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at how this is called, I think it does make sense the way it's currently written, but I just want to raise it for discussion.

A new possibility in one of the callers in this method now forces itself on all the other callers, leading to a number of calls where the caller must pass null in here. This seems to be common in the PowerShell codebase, but in my opinion leads to decohesion of methods, both from the perspective of the caller (which must now carefully select the appropriate subset of parameters) and from the perspective of the method itself (which must now carefully step between shared and scenario-specific logic).

In some respects this new parameter is a pseudo-boolean; it's either null or has a particular value that causes us to follow another path within the method.

That suggests to me that really we have two methods on our hands, with some shared logic best represented by breaking things up into smaller, composable methods.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely a good point. I can have a look at this and try to refactor things a bit.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have a pretty good way to do this. There's some duplication of code, but it separates the code paths much more neatly. I'll commit this change shortly; any additional suggestions you have to improve what I managed to get to are more than welcome! 😊

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
: PSObject.DotNetInstanceAdapter.GetPropertiesMethodsAndEvents(type, false);
: PSObject.DotNetInstanceAdapter.GetPropertiesMethodsAndEvents(type, @static: false);

@vexx32 vexx32 force-pushed the AddEqualEventHandler branch from 4e008a8 to 9e092d7 Compare June 27, 2021 00:29
@ghost ghost added the Review - Needed The PR is being reviewed label Jul 5, 2021
@ghost
Copy link
Copy Markdown

ghost commented Jul 5, 2021

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

@iSazonov
Copy link
Copy Markdown
Collaborator

@daxian-dbw Could you please review or make a conclusion about closing the PR?

@ghost ghost removed the Review - Needed The PR is being reviewed label Jan 19, 2022
@iSazonov
Copy link
Copy Markdown
Collaborator

/rebase

@ghost ghost added the Review - Needed The PR is being reviewed label Jan 26, 2022
@ghost
Copy link
Copy Markdown

ghost commented Jan 26, 2022

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

@ALIENQuake
Copy link
Copy Markdown

Any news on this?

@Shayan-To
Copy link
Copy Markdown

Shayan-To commented Aug 26, 2024

@vexx32 Is there any progress related to this? Do you know of any other issue/PR we can follow?

@vexx32
Copy link
Copy Markdown
Collaborator Author

vexx32 commented Aug 26, 2024

I do not know if I personally have all that much time spare to look at this in detail but I will ping WG folks and to give it a look when they have a minute and get it on the radar, and put some labels on etc. It probably needs a rebase, at the least... I don't know if the WG folks will want to take it or not, but I think it's worth it myself.

(/cc @SeeminglyScience)

@vexx32 vexx32 added WG-Engine core PowerShell engine, interpreter, and runtime WG-NeedsReview Needs a review by the labeled Working Group WG-Language parser, language semantics labels Aug 26, 2024
@vexx32 vexx32 force-pushed the AddEqualEventHandler branch from 9e092d7 to d08595f Compare August 27, 2024 01:37
@JamesWTruher
Copy link
Copy Markdown
Collaborator

it does look like a rebase is all that is needed (at least that's all i did, and the new tests were successful)

vexx32 and others added 4 commits April 3, 2025 16:33
This gives visibility into events so that users can target them.
It also vastly simplifies the necessary handling in the compiler/binders
as we can use a runtime conditional expression to determine if the
target of the += / -= op is a PSEvent and maintain some sanity in the
binders and compiler logic needed here.

Completion logic also needed to be enhanced to provide completion for
event names.
Adds a new binder to handle adding/removing event delegates with += / -=

If the LHS is a MemberAccessExpressionAst, the compiler will compile
+=/-= expressions into conditionals that first check if the LHS is a
PSEvent member. If so, it attempts to register the RHS argument as a
delegate for that event.

If the LHS isn't PSEvent, falls back to the typical compound assignment.
We can split the code path into assignments to members, and assignments
in other cases, avoiding the need to make some checks when the LHS is
not a MemberExpressionAst.

Reworked things a bit to use Lazy<string> for event definition.
Also refactored logic to use StringBuilder completely per Rob's comments

Co-authored-by: Robert Holt <rjmholt@gmail.com>
Ensure we check both instance and static members to ensure we add or
remove the event correctly for both cases.

Also added a tab completion test for event members, which we don't seem
to have explicit tests for that I could find. As this completion is
related to this PR, seemed appropriate to add tests for that behaviour
here as well.

Co-authored-by: Ilya <darpa@yandex.ru>
@vexx32 vexx32 force-pushed the AddEqualEventHandler branch from d08595f to 5121900 Compare April 3, 2025 20:34
@JamesWTruher JamesWTruher added Resolution-Declined The proposed feature is declined. and removed WG-NeedsReview Needs a review by the labeled Working Group labels Apr 21, 2025
@SeeminglyScience
Copy link
Copy Markdown
Contributor

The Engine WG discussed this today (apologies for the wait). We worry that making this syntax easier (even if marginally) would imply a greater support for this operation than what we currently have (no closure created, handler must be invoked in a thread with a default runspace, potential runspace affinity issues, etc).

Thank you for your hard work on this Rain, and again sorry for the very late response.

@vexx32 vexx32 closed this Apr 23, 2025
@microsoft-github-policy-service
Copy link
Copy Markdown
Contributor

microsoft-github-policy-service Bot commented Apr 23, 2025

📣 Hey @@vexx32, how did we do? We would love to hear your feedback with the link below! 🗣️

🔗 https://aka.ms/PSRepoFeedback

@SeeminglyScience SeeminglyScience removed their assignment Apr 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log Resolution-Declined The proposed feature is declined. Review - Needed The PR is being reviewed WG-Engine core PowerShell engine, interpreter, and runtime WG-Language parser, language semantics

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve discoverability and support for (de)registering event handlers

9 participants