Skip to content

Commit 7e51231

Browse files
committed
Implement IDisposable pattern for music players and improve shutdown handling
1 parent a21bfed commit 7e51231

8 files changed

Lines changed: 293 additions & 34 deletions

File tree

HomeSpeaker.Server2/ChattyMusicPlayer.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
namespace HomeSpeaker.Server;
44

5-
public class ChattyMusicPlayer : IMusicPlayer
5+
public class ChattyMusicPlayer : IMusicPlayer, IDisposable
66
{
77
private readonly IMusicPlayer actualPlayer;
8+
private bool disposed = false;
89

910
public ChattyMusicPlayer(IMusicPlayer actualPlayer)
1011
{
@@ -73,11 +74,27 @@ public void Stop()
7374
{
7475
actualPlayer.Stop();
7576
PlayerEvent?.Invoke(this, "Stopped playing.");
76-
}
77-
78-
public void UpdateQueue(IEnumerable<string> songs)
77+
} public void UpdateQueue(IEnumerable<string> songs)
7978
{
8079
actualPlayer.UpdateQueue(songs);
8180
PlayerEvent?.Invoke(this, "Updated queue.");
8281
}
82+
83+
public void Dispose()
84+
{
85+
Dispose(true);
86+
GC.SuppressFinalize(this);
87+
}
88+
89+
protected virtual void Dispose(bool disposing)
90+
{
91+
if (!disposed)
92+
{
93+
if (disposing)
94+
{
95+
actualPlayer?.Dispose();
96+
}
97+
disposed = true;
98+
}
99+
}
83100
}

HomeSpeaker.Server2/IMusicPlayer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace HomeSpeaker.Server;
44

5-
public interface IMusicPlayer
5+
public interface IMusicPlayer : IDisposable
66
{
77
void PlaySong(Song song);
88
void PlayStream(string streamUrl);

HomeSpeaker.Server2/LifecycleEvents.cs

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,50 @@ public async Task StartAsync(CancellationToken cancellationToken)
4343
public async Task StopAsync(CancellationToken cancellationToken)
4444
{
4545
logger.LogInformation("Application Stopping event raised!");
46-
if (player.Status.StillPlaying)
46+
try
4747
{
48-
logger.LogInformation("Still playing music...saving current song and queue");
49-
var lastState = new LastState
48+
if (player.Status.StillPlaying)
5049
{
51-
CurrentSong = player.Status.CurrentSong,
52-
Queue = player.SongQueue
53-
};
54-
var json = JsonSerializer.Serialize(lastState);
55-
await File.WriteAllTextAsync(LastStatePath, json);
56-
logger.LogInformation("Saved {LastStatePath} with {LastState}", LastStatePath, lastState);
50+
logger.LogInformation("Still playing music...saving current song and queue");
51+
var lastState = new LastState
52+
{
53+
CurrentSong = player.Status.CurrentSong,
54+
Queue = player.SongQueue
55+
};
56+
var json = JsonSerializer.Serialize(lastState);
57+
await File.WriteAllTextAsync(LastStatePath, json, cancellationToken);
58+
logger.LogInformation("Saved {LastStatePath} with {LastState}", LastStatePath, lastState);
59+
}
60+
else //if we're not playing anything right now
61+
{
62+
logger.LogInformation("Not playing anything, no state to save.");
63+
if (File.Exists(LastStatePath)) //don't leave behind a file as if we were.
64+
File.Delete(LastStatePath);
65+
}
66+
}
67+
catch (OperationCanceledException)
68+
{
69+
logger.LogWarning("Shutdown was cancelled before state could be saved");
70+
}
71+
catch (Exception ex)
72+
{
73+
logger.LogError(ex, "Error saving application state during shutdown");
5774
}
58-
else //if we're not playing anything right now
75+
finally
5976
{
60-
logger.LogInformation("Not playing anything, no state to save.");
61-
if (File.Exists(LastStatePath)) //don't leave behind a file as if we were.
62-
File.Delete(LastStatePath);
77+
// Ensure music player is properly disposed
78+
if (player is IDisposable disposablePlayer)
79+
{
80+
try
81+
{
82+
disposablePlayer.Dispose();
83+
logger.LogInformation("Music player disposed successfully");
84+
}
85+
catch (Exception ex)
86+
{
87+
logger.LogError(ex, "Error disposing music player");
88+
}
89+
}
6390
}
6491
}
6592

HomeSpeaker.Server2/LinuxSoxMusicPlayer.cs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
namespace HomeSpeaker.Server;
88

9-
public class LinuxSoxMusicPlayer : IMusicPlayer
9+
public class LinuxSoxMusicPlayer : IMusicPlayer, IDisposable
1010
{
1111
private readonly ILogger<LinuxSoxMusicPlayer> logger;
1212
private readonly Mp3Library library;
1313
private Process? playerProcess;
14+
private bool disposed = false;
1415

1516
public LinuxSoxMusicPlayer(ILogger<LinuxSoxMusicPlayer> logger, Mp3Library library)
1617
{
@@ -124,18 +125,65 @@ public void PlaySong(Song song)
124125
playerProcess.BeginOutputReadLine();
125126
playerProcess.BeginErrorReadLine();
126127
startedPlaying = false;
127-
}
128-
129-
private void stopPlaying()
128+
} private void stopPlaying()
130129
{
131-
if (playerProcess != null && playerProcess.HasExited is false)
130+
if (playerProcess != null)
132131
{
133-
playerProcess.Exited -= PlayerProcess_Exited;//stop listening to when the process ends.
132+
try
133+
{
134+
if (!playerProcess.HasExited)
135+
{
136+
playerProcess.Exited -= PlayerProcess_Exited; // Stop listening to when the process ends
137+
playerProcess.Kill();
138+
playerProcess.WaitForExit(5000); // Wait up to 5 seconds for clean exit
139+
}
140+
playerProcess.Dispose();
141+
}
142+
catch (Exception ex)
143+
{
144+
logger.LogWarning(ex, "Error stopping player process");
145+
}
146+
finally
147+
{
148+
playerProcess = null;
149+
}
134150
}
135151

136-
foreach (var proc in Process.GetProcessesByName("play").Union(Process.GetProcessesByName("vlc")))
152+
// Fallback: kill any remaining processes that might be hanging around
153+
try
137154
{
138-
proc.Kill();
155+
foreach (var proc in Process.GetProcessesByName("play").Union(Process.GetProcessesByName("vlc")))
156+
{
157+
if (!proc.HasExited)
158+
{
159+
proc.Kill();
160+
proc.WaitForExit(2000);
161+
}
162+
proc.Dispose();
163+
}
164+
}
165+
catch (Exception ex)
166+
{
167+
logger.LogWarning(ex, "Error cleaning up audio processes");
168+
}
169+
}
170+
171+
public void Dispose()
172+
{
173+
Dispose(true);
174+
GC.SuppressFinalize(this);
175+
}
176+
177+
protected virtual void Dispose(bool disposing)
178+
{
179+
if (!disposed)
180+
{
181+
if (disposing)
182+
{
183+
logger.LogInformation("Disposing LinuxSoxMusicPlayer");
184+
stopPlaying();
185+
}
186+
disposed = true;
139187
}
140188
}
141189

HomeSpeaker.Server2/Program.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111

1212
var builder = WebApplication.CreateBuilder(args);
1313

14+
// Configure host shutdown timeout
15+
builder.Host.ConfigureHostOptions(options =>
16+
{
17+
options.ShutdownTimeout = TimeSpan.FromSeconds(30); // 30 second timeout for graceful shutdown
18+
});
19+
1420
builder.AddServiceDefaults();
1521

1622
builder.Services.AddResponseCompression(o => o.EnableForHttps = true);

HomeSpeaker.Server2/Services/YoutubeService.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace HomeSpeaker.Server2.Services;
1313

14-
public class YoutubeService
14+
public class YoutubeService : IDisposable
1515
{
1616
public YoutubeService(IConfiguration config, ILogger<YoutubeService> logger, Mp3Library library)
1717
{
@@ -24,6 +24,7 @@ public YoutubeService(IConfiguration config, ILogger<YoutubeService> logger, Mp3
2424
private readonly IConfiguration config;
2525
private readonly ILogger<YoutubeService> logger;
2626
private readonly Mp3Library library;
27+
private bool disposed = false;
2728

2829
public async Task<IEnumerable<VideoDto>> SearchAsync(string searchTerm, int maxItems = 50)
2930
{
@@ -80,6 +81,23 @@ public async Task CacheVideoAsync(string id, string title, IProgress<double> pro
8081

8182
logger.LogInformation("Finished caching {title}. Saved to {destination}", title, destinationPath);
8283
}
84+
85+
public void Dispose()
86+
{
87+
Dispose(true);
88+
GC.SuppressFinalize(this);
89+
} protected virtual void Dispose(bool disposing)
90+
{
91+
if (!disposed)
92+
{
93+
if (disposing)
94+
{
95+
// YoutubeClient doesn't implement IDisposable, but we set it to null
96+
client = null!;
97+
}
98+
disposed = true;
99+
}
100+
}
83101
}
84102

85103
public record VideoDto(string Title, string Id, string Url, string? Thumbnail, string? Author, TimeSpan? Duration);

HomeSpeaker.Server2/WindowsMusicPlayer.cs

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace HomeSpeaker.Server;
88

9-
public class WindowsMusicPlayer : IMusicPlayer
9+
public class WindowsMusicPlayer : IMusicPlayer, IDisposable
1010
{
1111
public WindowsMusicPlayer(ILogger<WindowsMusicPlayer> logger, Mp3Library library)
1212
{
@@ -21,6 +21,7 @@ public WindowsMusicPlayer(ILogger<WindowsMusicPlayer> logger, Mp3Library library
2121
private PlayerStatus status = new();
2222
private Song? currentSong;
2323
private Song? stoppedSong;
24+
private bool disposed = false;
2425
public PlayerStatus Status => (status ?? new PlayerStatus()) with { CurrentSong = currentSong };
2526

2627
private bool startedPlaying = false;
@@ -84,17 +85,65 @@ public void PlaySong(Song song)
8485
playerProcess.BeginOutputReadLine();
8586
playerProcess.BeginErrorReadLine();
8687
startedPlaying = false;
87-
}
88-
private void stopPlaying()
88+
} private void stopPlaying()
8989
{
90-
if (playerProcess != null && playerProcess.HasExited is false)
90+
if (playerProcess != null)
91+
{
92+
try
93+
{
94+
if (!playerProcess.HasExited)
95+
{
96+
playerProcess.Exited -= PlayerProcess_Exited; // Stop listening to when the process ends
97+
playerProcess.Kill();
98+
playerProcess.WaitForExit(5000); // Wait up to 5 seconds for clean exit
99+
}
100+
playerProcess.Dispose();
101+
}
102+
catch (Exception ex)
103+
{
104+
logger.LogWarning(ex, "Error stopping player process");
105+
}
106+
finally
107+
{
108+
playerProcess = null;
109+
}
110+
}
111+
112+
// Fallback: kill any remaining VLC processes that might be hanging around
113+
try
91114
{
92-
playerProcess.Exited -= PlayerProcess_Exited;//stop listening to when the process ends.
115+
foreach (var proc in Process.GetProcessesByName("vlc"))
116+
{
117+
if (!proc.HasExited)
118+
{
119+
proc.Kill();
120+
proc.WaitForExit(2000);
121+
}
122+
proc.Dispose();
123+
}
93124
}
125+
catch (Exception ex)
126+
{
127+
logger.LogWarning(ex, "Error cleaning up VLC processes");
128+
}
129+
}
94130

95-
foreach (var proc in Process.GetProcessesByName("vlc"))
131+
public void Dispose()
132+
{
133+
Dispose(true);
134+
GC.SuppressFinalize(this);
135+
}
136+
137+
protected virtual void Dispose(bool disposing)
138+
{
139+
if (!disposed)
96140
{
97-
proc.Kill();
141+
if (disposing)
142+
{
143+
logger.LogInformation("Disposing WindowsMusicPlayer");
144+
stopPlaying();
145+
}
146+
disposed = true;
98147
}
99148
}
100149

0 commit comments

Comments
 (0)