Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/changelog/v0.2.10.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Follow-up patch release for v0.2.9 addressing auto-updater regressions, adding a
- Fix pre-update backup failing when a Serilog-held log file is locked — `UpdateBackupService` now enumerates files manually, skips the `logs/` directory and `.log` files, and logs-and-continues on `IOException`/`UnauthorizedAccessException` instead of aborting the whole backup
- Simplify update progress dispatch — remove redundant `Application.Invoke` wrappers around progress updates that are already called from the UI thread (introduced while fixing the freeze above)
- Fix cursor position being reset to the start of the line when auto-completing commands in the CLI app — insertion point is now moved to the end of the completed text
- Fix notification sounds crashing or being silently dropped when several arrive in quick succession — playback is now serialized through a semaphore that's held for the duration of each sound (using `PlaybackFinished` with a 10s safety timeout) and always released in `finally`, so back-to-back notifications queue up and play in order instead of racing the underlying audio player (fixes #20)

## Refactoring

Expand Down
23 changes: 20 additions & 3 deletions src/EchoHub.Client/Services/NotificationSoundService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ namespace EchoHub.Client.Services;

public class NotificationSoundService
{
// Safety net: if PlaybackFinished never fires we don't want to block future notifications forever.
private static readonly TimeSpan PlaybackTimeout = TimeSpan.FromSeconds(10);

private readonly Player _player = new();
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly NotificationConfig _config;
private string? _resolvedSoundPath;

Expand Down Expand Up @@ -41,18 +45,31 @@ public async Task PlayTestAsync()

private async Task PlayInternal()
{
await _lock.WaitAsync();

// _player.Play returns as soon as playback starts, so we wait on PlaybackFinished
// to hold the lock for the duration of the sound. A one-shot handler + timeout
// keeps the finally release robust: never-fires → timeout; fires twice → ignored
// (TrySetResult); handler throws → caller's catch still runs finally.
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
void OnFinished(object? s, EventArgs e) => completion.TrySetResult();
_player.PlaybackFinished += OnFinished;

try
{
if (_player.Playing)
await _player.Stop();

await _player.SetVolume(_config.Volume);
await _player.Play(_resolvedSoundPath!);
await Task.WhenAny(completion.Task, Task.Delay(PlaybackTimeout));
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to play notification sound");
}
finally
{
_player.PlaybackFinished -= OnFinished;
_lock.Release();
}
}

private void ResolveSoundPath()
Expand Down
Loading