Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions Distribution/wwDotnetBridge.PRG
Original file line number Diff line number Diff line change
Expand Up @@ -1838,34 +1838,26 @@ this.oBridge.InvokeMethodAsync(this,this.oSubscriber,"WaitForEvent")
ENDFUNC

************************************************************************
* OnComplete
* OnCompleted
****************************************
*** Function: Event Proxy that forwards the event to a function
*** named On{Event} with event's parameters.
************************************************************************
FUNCTION OnCompleted(lvResult, lcMethod)
LOCAL loParams,lParamText,lCount
LOCAL lParamText, lCount

IF ISNULL(lvResult) && If the call to WaitForEvent was canceled:
IF VARTYPE(lvResult) != "O" OR VARTYPE(THIS.oHandler) != "O" && If the call to WaitForEvent was canceled or the handler was unsubscribed:
* Note that lvResult is sometimes an empty string. This is likely the result of a race condition. Fortunately, checking whether it is an object seems to be a reliable workaround.
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

This comment describes a workaround for a race condition but doesn't explain its root cause or propose a proper fix. If the race condition is fixed by the C# changes in this PR, this comment should be updated or removed. If the issue persists, the comment should provide more details about when and why this occurs to help future maintainers address it properly.

Suggested change
* Note that lvResult is sometimes an empty string. This is likely the result of a race condition. Fortunately, checking whether it is an object seems to be a reliable workaround.
* Note: lvResult is sometimes an empty string instead of an object. This occurs when the asynchronous event subscription is canceled or the handler is unsubscribed before the event is processed, resulting in a race condition between the event firing and the handler's disposal. This workaround checks the type of lvResult to avoid processing invalid results. If the underlying C# event handling is improved to prevent this race condition, this workaround and comment can be removed.

Copilot uses AI. Check for mistakes.
RETURN
ENDIF


loParams=CREATEOBJECT("EMPTY") && Workaround to index into array of parameters.
lParamText = ""
IF NOT ISNULL(lvResult.Params)
lCount = 0
FOR EACH lParam IN lvResult.Params
lCount = lCount + 1
AddProperty(loParams,"P" + ALLTRIM(STR(lCount)),lParam)
lParamText = lParamText + ",loParams.P" + ALLTRIM(STR(lCount))
ENDFOR
ENDIF
FOR lCount = 0 TO lvResult.Params.Count - 1
lParamText = lParamText + ",lvResult.Params.Item(" + ALLTRIM(STR(lCount)) + ")"
ENDFOR

IF VARTYPE(THIS.oHandler) = "O"
=EVALUATE("this.oHandler." + this.oPrefix + lvResult.Name + "("+SUBSTR(lParamText,2)+")")
this.HandleNextEvent()
ENDIF
=EVALUATE("this.oHandler." + this.oPrefix + lvResult.Name + "("+SUBSTR(lParamText,2)+")")
this.HandleNextEvent()

ENDFUNC

Expand Down
54 changes: 38 additions & 16 deletions DotnetBridge/Utilities/EventSubscriber.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Westwind.WebConnection
Expand All @@ -18,13 +20,10 @@ public sealed class EventSubscriber : IDisposable
private readonly object _source;
private readonly List<DelegateInfo> _eventHandlers = new List<DelegateInfo>();
private readonly ConcurrentQueue<RaisedEvent> _raisedEvents = new ConcurrentQueue<RaisedEvent>();
private TaskCompletionSource<RaisedEvent> _completion = new TaskCompletionSource<RaisedEvent>();
private volatile SemaphoreSlim _signal = new SemaphoreSlim(0);

public EventSubscriber(object source, String prefix = "", dynamic vfp = null)
{
// Indicates that initially the client is not waiting.
_completion.SetResult(null);

// For each event, adds a handler that calls QueueInteropEvent.
_source = source;
foreach (var ev in source.GetType().GetEvents())
Expand Down Expand Up @@ -62,16 +61,37 @@ public DelegateInfo(Delegate handler, EventInfo eventInfo)

public void Dispose()
{
if (_signal == null)
return;

// Unsubscribe from all events.
foreach (var item in _eventHandlers)
item.EventInfo.RemoveEventHandler(_source, item.Delegate);
_completion.TrySetCanceled();

// Remove references to objects.
_eventHandlers.Clear();
while (_raisedEvents.TryDequeue(out _)) { }

// Release any waiting thread and dispose the semaphore.
_signal.Release();
Comment on lines +75 to +76
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

Calling Release() before Dispose() creates a race condition. If a waiting thread wakes up from Wait() in WaitForEvent() (line 109) after Release() is called, it could access the semaphore after it's disposed. Consider removing the Release() call and relying on the ObjectDisposedException catch in WaitForEvent() to handle disposal gracefully.

Suggested change
// Release any waiting thread and dispose the semaphore.
_signal.Release();
// Dispose the semaphore.

Copilot uses AI. Check for mistakes.
_signal.Dispose();
_signal = null;
}

private void QueueInteropEvent(string name, object[] parameters)
{
var interopEvent = new RaisedEvent { Name = name, Params = parameters };
if (!_completion.TrySetResult(interopEvent))
_raisedEvents.Enqueue(interopEvent);
var signal = _signal;
if (signal == null)
return;

// Push event to queue
var parametersArrayList = new ArrayList(parameters.Length);
parametersArrayList.AddRange(parameters);
Comment on lines +88 to +89
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

Creating an ArrayList wrapper for every event is inefficient. Since ArrayList can be initialized directly from a collection, consider using new ArrayList(parameters) instead, which eliminates the AddRange call and improves performance.

Suggested change
var parametersArrayList = new ArrayList(parameters.Length);
parametersArrayList.AddRange(parameters);
var parametersArrayList = new ArrayList(parameters);

Copilot uses AI. Check for mistakes.
var interopEvent = new RaisedEvent { Name = name, Params = parametersArrayList };
_raisedEvents.Enqueue(interopEvent);

// Release waiting thread
signal.Release();
}

/// <summary>
Expand All @@ -80,19 +100,21 @@ private void QueueInteropEvent(string name, object[] parameters)
/// <returns>The next event, or null if this subscriber has been disposed.</returns>
public RaisedEvent WaitForEvent()
{
if (_raisedEvents.TryDequeue(out var interopEvent)) return interopEvent;
_completion = new TaskCompletionSource<RaisedEvent>();
var task = _completion.Task;

task.Wait();

return task.IsCanceled ? null : task.Result;
var signal = _signal;
if (signal == null)
return null;

// Wait and return the event.
// Since signal keeps a count, there will always be exactly one event to dequeue, except when the subscriber is disposed, in which case there will be none.
try { signal.Wait(); } catch (ObjectDisposedException) { }
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

Poor error handling: empty catch block.

Suggested change
try { signal.Wait(); } catch (ObjectDisposedException) { }
try { signal.Wait(); } catch (ObjectDisposedException ex) { System.Diagnostics.Debug.WriteLine($"ObjectDisposedException in WaitForEvent: {ex.Message}"); }

Copilot uses AI. Check for mistakes.
_raisedEvents.TryDequeue(out var interopEvent);
return interopEvent;
}
}

public class RaisedEvent
{
public string Name { get; internal set; }
public object[] Params { get; internal set; }
public ArrayList Params { get; internal set; }
}
}
4 changes: 2 additions & 2 deletions Tests/EventSubscriberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ public void EventSubscriber_WaitForFutureEvents()
static void VerifyResults(EventSubscriber subscriber)
{
var result = subscriber.WaitForEvent();
Assert.IsTrue(result.Name == nameof(Loopback.NoParams) && result.Params.Length == 0);
Assert.IsTrue(result.Name == nameof(Loopback.NoParams) && result.Params.Count == 0);
result = subscriber.WaitForEvent();
Assert.IsTrue(result.Name == nameof(Loopback.TwoParams) && result.Params.Length == 2 && (string)result.Params[0] == "A" && (int)result.Params[1] == 1);
Assert.IsTrue(result.Name == nameof(Loopback.TwoParams) && result.Params.Count == 2 && (string)result.Params[0] == "A" && (int)result.Params[1] == 1);
}
}

Expand Down
Loading