From 02cb93d854a4fdd911a07d5316e8d15c859666dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Jul 2025 20:39:08 +0900 Subject: [PATCH] Fix leak from polling chat client being initialised too early This one is quite dumb. `OsuGame` uses [`loadComponentSingleFile`](https://github.com/ppy/osu/blob/15878f7f9fc7088494d3b66e98a7bc1004a1a06d/osu.Game/OsuGame.cs#L1228) to load the `ChannelManager`. Importantly, this process does _not_ add the component to any place in the hierarchy where it would normally be disposed - this includes `InternalChildren`, but _also_ a lesser known list of [currently-loading components](https://github.com/ppy/osu-framework/blob/cfb0d7b4b673583f0cf56273e94352769aa5bc9a/osu.Framework/Graphics/Containers/CompositeDrawable.cs#L316-L323) (those which have been sent through a `LoadComponentAsync` call). The end result of this is that, `ChannelManager` creates the `IChatClient` in its constructor, expecting to be able to dispose it, but `Dispose` is never called! And the failure case here is that `PollingChatClient` creates a background task to continuously poll the API, unfortunately keeping a reference to the rest of the world in the process. --- osu.Game/Online/Chat/ChannelManager.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index e9ca0a8ed2..fde6c4db06 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -9,6 +9,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Threading; @@ -64,7 +65,6 @@ namespace osu.Game.Online.Chat public IBindableList AvailableChannels => availableChannels; private readonly IAPIProvider api; - private readonly IChatClient chatClient; [Resolved] private UserLookupCache users { get; set; } @@ -72,6 +72,7 @@ namespace osu.Game.Online.Chat private readonly IBindable apiState = new Bindable(); private ScheduledDelegate scheduledAck; + private IChatClient chatClient = null!; private long? lastSilenceMessageId; private uint? lastSilenceId; @@ -79,14 +80,13 @@ namespace osu.Game.Online.Chat { this.api = api; - chatClient = api.GetChatClient(); - CurrentChannel.ValueChanged += currentChannelChanged; } [BackgroundDependencyLoader] private void load() { + chatClient = api.GetChatClient(); chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch)); chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false)); chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs)); @@ -282,8 +282,7 @@ namespace osu.Game.Online.Chat // Check if the user has joined the requested channel already. // This uses the channel name for comparison as the PM user's username is unavailable after a restart. - var privateChannel = JoinedChannels.FirstOrDefault( - c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase)); + var privateChannel = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase)); if (privateChannel != null) { @@ -645,7 +644,9 @@ namespace osu.Game.Online.Chat protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - chatClient?.Dispose(); + + if (chatClient.IsNotNull()) + chatClient.Dispose(); } }