Skip to content

Commit 00f50d2

Browse files
committed
- Added TestSlowStatusHandlerAsync to test slow status handling.
- Updated `TestQueueShuffleAsync` to use `RetryFact` for retries. - Upgraded `Microsoft.Testing.Platform.MSBuild` to version 1.8.2. - Introduced `SafeInvokeEvent` for safe asynchronous event handling. - Replaced direct event invocations with `SafeInvokeEvent` in multiple channels.
1 parent f251bee commit 00f50d2

File tree

10 files changed

+108
-26
lines changed

10 files changed

+108
-26
lines changed

Sharpcaster.Test/ComplicatedCasesTester.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,5 +201,35 @@ public async Task TestingJoiningMultipleTimes()
201201

202202

203203
}
204+
205+
[Fact]
206+
public async Task TestSlowStatusHandlerAsync()
207+
{
208+
var TestHelper = new TestHelper();
209+
ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(outputHelper, fixture.Receivers[0]);
210+
211+
// Register malicious handler
212+
client.MediaChannel.StatusChanged += (sender, args) => {
213+
Task.Delay(10000).Wait();
214+
};
215+
216+
var media = new Media
217+
{
218+
ContentUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/DesigningForGoogleCast.mp4"
219+
};
220+
221+
MediaStatus status = await client.MediaChannel.LoadAsync(media);
222+
Assert.Equal(PlayerStateType.Playing, status.PlayerState);
223+
224+
// Wait a bit for media to start
225+
await Task.Delay(2000, Xunit.TestContext.Current.CancellationToken);
226+
227+
// Get current media status
228+
MediaStatus currentStatus = await client.MediaChannel.GetMediaStatusAsync();
229+
Assert.NotNull(currentStatus);
230+
Assert.True(currentStatus.MediaSessionId > 0);
231+
232+
await client.DisconnectAsync();
233+
}
204234
}
205235
}

Sharpcaster.Test/QueueTester.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Sharpcaster.Test.helper;
55
using System.Linq;
66
using System.Threading.Tasks;
7+
using xRetry.v3;
78
using Xunit;
89

910
namespace Sharpcaster.Test
@@ -218,7 +219,7 @@ public async Task TestQueueUpdateAsync()
218219
}
219220
}
220221

221-
[Fact]
222+
[RetryFact]
222223
public async Task TestQueueShuffleAsync()
223224
{
224225
var testHelper = new TestHelper();

Sharpcaster.Test/Sharpcaster.Test.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
<ItemGroup>
1515
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
1616
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
17-
<PackageReference Include="Microsoft.Testing.Platform.MSBuild" Version="1.8.1" />
17+
<PackageReference Include="Microsoft.Testing.Platform.MSBuild" Version="1.8.2" />
1818
<PackageReference Include="Moq" Version="4.20.72" />
1919
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
2020
<PackageReference Include="System.Text.Json" Version="9.0.8" />
21+
<PackageReference Include="xRetry.v3" Version="0.1.0-alpha1" />
2122
<PackageReference Include="xunit.analyzers" Version="1.23.0">
2223
<PrivateAssets>all</PrivateAssets>
2324
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

Sharpcaster/Channels/ChromecastChannel.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.Extensions.Logging;
22
using Sharpcaster.Interfaces;
3+
using System;
34
using System.Threading.Tasks;
45

56
namespace Sharpcaster.Channels
@@ -78,5 +79,54 @@ public virtual Task OnMessageReceivedAsync(string messagePayload, string type)
7879
{
7980
return Task.CompletedTask;
8081
}
82+
83+
/// <summary>
84+
/// Safely invokes an event handler asynchronously to prevent blocking the receive loop
85+
/// </summary>
86+
/// <typeparam name="T">Event argument type</typeparam>
87+
/// <param name="eventHandler">Event handler to invoke</param>
88+
/// <param name="sender">Event sender</param>
89+
/// <param name="args">Event arguments</param>
90+
protected static void SafeInvokeEvent<T>(EventHandler<T>? eventHandler, object sender, T args)
91+
{
92+
if (eventHandler != null)
93+
{
94+
Task.Run(() =>
95+
{
96+
try
97+
{
98+
eventHandler.Invoke(sender, args);
99+
}
100+
catch
101+
{
102+
// Swallow exceptions from event handlers to prevent them from crashing the receive loop
103+
}
104+
});
105+
}
106+
}
107+
108+
/// <summary>
109+
/// Safely invokes an event handler asynchronously to prevent blocking the receive loop
110+
/// </summary>
111+
/// <param name="eventHandler">Event handler to invoke</param>
112+
/// <param name="sender">Event sender</param>
113+
/// <param name="args">Event arguments</param>
114+
protected static void SafeInvokeEvent(EventHandler? eventHandler, object sender, EventArgs args)
115+
{
116+
if (eventHandler != null)
117+
{
118+
Task.Run(() =>
119+
{
120+
try
121+
{
122+
eventHandler.Invoke(sender, args);
123+
}
124+
catch
125+
{
126+
// Swallow exceptions from event handlers to prevent them from crashing the receive loop
127+
}
128+
});
129+
}
130+
}
81131
}
82132
}

Sharpcaster/Channels/HeartbeatChannel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public void StopTimeoutTimer()
7474
private void TimerElapsed(object sender, ElapsedEventArgs e)
7575
{
7676
if (Logger != null) LogHeartbeatTimeout(Logger, null);
77-
StatusChanged?.Invoke(this, e);
77+
SafeInvokeEvent(StatusChanged, this, e);
7878
}
7979

8080
protected virtual void Dispose(bool disposing)

Sharpcaster/Channels/MediaChannel.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,37 +100,37 @@ public override Task OnMessageReceivedAsync(string messagePayload, string type)
100100
var loadFailedMessage = JsonSerializer.Deserialize(messagePayload, SharpcasteSerializationContext.Default.LoadFailedMessage);
101101
if (loadFailedMessage != null)
102102
{
103-
LoadFailed?.Invoke(this, loadFailedMessage);
103+
SafeInvokeEvent(LoadFailed, this, loadFailedMessage);
104104
}
105105
return Task.CompletedTask;
106106
case "LOAD_CANCELLED":
107107
var loadCancelledMessage = JsonSerializer.Deserialize(messagePayload, SharpcasteSerializationContext.Default.LoadCancelledMessage);
108108
if (loadCancelledMessage != null)
109109
{
110-
LoadCancelled?.Invoke(this, loadCancelledMessage);
110+
SafeInvokeEvent(LoadCancelled, this, loadCancelledMessage);
111111
}
112112
return Task.CompletedTask;
113113
case "INVALID_REQUEST":
114114
var invalidRequestMessage = JsonSerializer.Deserialize(messagePayload, SharpcasteSerializationContext.Default.InvalidRequestMessage);
115115
if (invalidRequestMessage != null)
116116
{
117-
InvalidRequest?.Invoke(this, invalidRequestMessage);
117+
SafeInvokeEvent(InvalidRequest, this, invalidRequestMessage);
118118
}
119119
return Task.CompletedTask;
120120
case "ERROR":
121121
var errorMessage = JsonSerializer.Deserialize(messagePayload, SharpcasteSerializationContext.Default.ErrorMessage);
122122
if (errorMessage != null)
123123
{
124-
ErrorHappened?.Invoke(this, errorMessage);
124+
SafeInvokeEvent(ErrorHappened, this, errorMessage);
125125
}
126126
return Task.CompletedTask;
127127
case "MEDIA_STATUS":
128128
var mediaStatusMessage = JsonSerializer.Deserialize(messagePayload, SharpcasteSerializationContext.Default.MediaStatusMessage);
129129
mediaStatus = mediaStatusMessage?.Status.FirstOrDefault();
130130
if (MediaStatus != null)
131-
{
132-
StatusChanged?.Invoke(this, MediaStatus);
133-
}
131+
{
132+
SafeInvokeEvent(StatusChanged, this, MediaStatus);
133+
}
134134
return Task.CompletedTask;
135135
case "QUEUE_ITEMS":
136136
//{"type":"QUEUE_ITEMS","requestId":908492678,"items":[{"itemId":9,"media":{"contentId":"Aquarium","contentUrl":"https://incompetech.com/music/royalty-free/mp3-royaltyfree/Aquarium.mp3","streamType":2,"contentType":"audio/mpeg","mediaCategory":"AUDIO","duration":144.013078},"orderId":0}],"sequenceNumber":0}

Sharpcaster/Channels/MultiZoneChannel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ public override Task OnMessageReceivedAsync(string messagePayload, string type)
3939
if (multizoneStatusMessage?.Status != null)
4040
{
4141
Status = multizoneStatusMessage.Status;
42-
StatusChanged?.Invoke(this, multizoneStatusMessage.Status);
42+
SafeInvokeEvent(StatusChanged, this, multizoneStatusMessage.Status);
4343
}
4444
break;
4545
case "DEVICE_UPDATED":
4646
var deviceUpdatedMessage = JsonSerializer.Deserialize(messagePayload, SharpcasteSerializationContext.Default.DeviceUpdatedMessage);
4747
if (deviceUpdatedMessage?.Device != null)
4848
{
49-
DeviceUpdated?.Invoke(this, deviceUpdatedMessage.Device);
49+
SafeInvokeEvent(DeviceUpdated, this, deviceUpdatedMessage.Device);
5050
}
5151
break;
5252
default:

Sharpcaster/Channels/ReceiverChannel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,14 @@ public override Task OnMessageReceivedAsync(string messagePayload, string type)
9090
{
9191
case "LAUNCH_STATUS":
9292
var launchStatusMessage = JsonSerializer.Deserialize(messagePayload, SharpcasteSerializationContext.Default.LaunchStatusMessage);
93-
LaunchStatusChanged?.Invoke(this, launchStatusMessage);
93+
SafeInvokeEvent(LaunchStatusChanged, this, launchStatusMessage);
9494
break;
9595
case "RECEIVER_STATUS":
9696
var receiverStatusMessage = JsonSerializer.Deserialize(messagePayload, SharpcasteSerializationContext.Default.ReceiverStatusMessage);
9797
if (receiverStatusMessage?.Status != null)
9898
{
9999
receiverStatus = receiverStatusMessage.Status;
100-
ReceiverStatusChanged?.Invoke(this, ReceiverStatus);
100+
SafeInvokeEvent(ReceiverStatusChanged, this, ReceiverStatus);
101101
}
102102
break;
103103

Sharpcaster/Channels/SpotifyChannel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ public override Task OnMessageReceivedAsync(string messagePayload, string type)
3535
if (getInfoResponseMessage?.Payload != null)
3636
{
3737
SpotifyStatus = getInfoResponseMessage.Payload;
38-
SpotifyStatusUpdated?.Invoke(this, getInfoResponseMessage.Payload);
38+
SafeInvokeEvent(SpotifyStatusUpdated, this, getInfoResponseMessage.Payload);
3939
}
4040
break;
4141
case "addUserResponse":
4242
var addUserResponseMessage = JsonSerializer.Deserialize(messagePayload, SharpcasteSerializationContext.Default.AddUserResponseMessage);
4343
if (addUserResponseMessage?.Payload != null)
4444
{
45-
AddUserResponseReceived?.Invoke(this, addUserResponseMessage.Payload);
45+
SafeInvokeEvent(AddUserResponseReceived, this, addUserResponseMessage.Payload);
4646
}
4747
break;
4848
}

Sharpcaster/ChromeCastClient.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ public ChromecastClient() : this(null)
9999
public ChromecastClient(ILogger<ChromecastClient>? logger)
100100
{
101101
_logger = logger;
102-
102+
103103
var serviceCollection = new ServiceCollection();
104104
RegisterChannels(serviceCollection, logger);
105105
RegisterMessages(serviceCollection);
106-
106+
107107
_serviceProvider = serviceCollection.BuildServiceProvider();
108108
InitializeClient();
109109
}
@@ -171,7 +171,7 @@ private void InitializeClient()
171171
}
172172

173173

174-
public async Task<ChromecastStatus> ConnectChromecast(ChromecastReceiver chromecastReceiver)
174+
public async Task<ChromecastStatus?> ConnectChromecast(ChromecastReceiver chromecastReceiver)
175175
{
176176
if (chromecastReceiver?.DeviceUri == null)
177177
{
@@ -288,7 +288,7 @@ private async Task SendAsync(ILogger? logger, CastMessage castMessage)
288288
await SendSemaphoreSlim.WaitAsync().ConfigureAwait(false);
289289
try
290290
{
291-
if (logger != null) LogSentMessage(logger, castMessage.Namespace, castMessage.DestinationId, castMessage.PayloadUtf8, null);
291+
if (logger != null) LogSentMessage(logger, castMessage.Namespace, castMessage.DestinationId, castMessage.PayloadUtf8, null);
292292
#if NETSTANDARD2_0
293293
byte[] message = castMessage.ToProto();
294294
#else
@@ -344,10 +344,10 @@ public async Task DisconnectAsync()
344344
HeartbeatChannel.Dispose();
345345
}
346346

347-
347+
348348
// Recreate HeartbeatChannel since it's been disposed
349349
RecreateHeartbeatChannel();
350-
350+
351351
_cancellationTokenSource.Cancel(true);
352352
await Task.Delay(100).ConfigureAwait(false);
353353
await Dispose().ConfigureAwait(false);
@@ -405,13 +405,13 @@ private void RecreateHeartbeatChannel()
405405
if (disposedHeartbeat != null)
406406
{
407407
channelsList.Remove(disposedHeartbeat);
408-
408+
409409
// Create a new HeartbeatChannel instance using the service provider
410-
var newHeartbeat = _serviceProvider.GetService<HeartbeatChannel>() ??
410+
var newHeartbeat = _serviceProvider.GetService<HeartbeatChannel>() ??
411411
new HeartbeatChannel(_serviceProvider.GetService<ILogger<HeartbeatChannel>>());
412412
newHeartbeat.Client = this;
413413
channelsList.Add(newHeartbeat);
414-
414+
415415
// Update the channels collection
416416
Channels = channelsList;
417417
}
@@ -493,7 +493,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
493493

494494
var originalMessage = formatter(state, exception);
495495
var prefixedMessage = $"[{_categoryName}] {originalMessage}";
496-
496+
497497
_baseLogger.Log(logLevel, eventId, prefixedMessage, exception, (msg, ex) => msg);
498498
}
499499
}

0 commit comments

Comments
 (0)