fix some crashes, mostly related to network requests (#6187)

This commit is contained in:
pajlada
2025-05-17 14:22:56 +02:00
committed by GitHub
parent 46f3299a25
commit 6b968a199c
17 changed files with 259 additions and 81 deletions
+1
View File
@@ -28,6 +28,7 @@
- Bugfix: Fixed an issue where Splits could get lost by dragging it onto your Recycle Bin. (#6147)
- Bugfix: Fixed some Twitch commands not getting tab-completed correctly. (#6143)
- Bugfix: Fixed shared chat badges displaying pixelated when Chatterino is scaled too much. (#6146)
- Bugfix: Fixed a few crashes that could occur when Chatterino was shutting down, some related to network tasks still firing despite us shutting down. (#6187)
- Bugfix: Fixed a crash that could occur when eventsub was enabled and Chatterino was attached to a conhost on Windows that was later gone. (#6161)
- Bugfix: Fixed a crash that could occur an eventsub connection's keepalive timer would run after the connection was dead, causing the keepalive timer to use-itself-after-free. (#6204)
- Bugfix: Fixed a crash that could occur when an image started loading mid app shutdown. (#6213)
+58 -3
View File
@@ -70,6 +70,9 @@ using namespace chatterino;
const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws";
const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
std::atomic<bool> STOPPED{false};
std::atomic<bool> ABOUT_TO_QUIT{false};
ISoundController *makeSoundController(Settings &settings)
{
SoundBackend soundBackend = settings.soundBackend;
@@ -200,8 +203,6 @@ Application::Application(Settings &_settings, const Paths &paths,
Application::~Application()
{
this->eventSub->setQuitting();
// we do this early to ensure getApp isn't used in any dtors
INSTANCE = nullptr;
}
@@ -594,12 +595,60 @@ eventsub::IController *Application::getEventSub()
return this->eventSub.get();
}
void Application::save()
void Application::aboutToQuit()
{
ABOUT_TO_QUIT.store(true);
this->eventSub->setQuitting();
this->twitch->aboutToQuit();
this->hotkeys->save();
this->windows->save();
}
void Application::stop()
{
#ifdef CHATTERINO_HAVE_PLUGINS
this->plugins.reset();
#endif
this->pronouns.reset();
this->twitchUsers.reset();
this->streamerMode.reset();
this->linkResolver.reset();
this->seventvEventAPI.reset();
this->seventvEmotes.reset();
this->ffzEmotes.reset();
this->bttvLiveUpdates.reset();
this->bttvEmotes.reset();
this->chatterinoBadges.reset();
this->twitchBadges.reset();
this->twitchPubSub.reset();
this->twitchLiveController.reset();
this->sound.reset();
this->userData.reset();
this->seventvBadges.reset();
this->ffzBadges.reset();
this->twitch.reset();
this->highlights.reset();
this->notifications.reset();
this->commands.reset();
this->crashHandler.reset();
this->seventvAPI.reset();
this->imageUploader.reset();
this->toasts.reset();
this->windows.reset();
this->hotkeys.reset();
this->eventSub.reset();
this->accounts.reset();
this->emotes.reset();
this->logging.reset();
this->fonts.reset();
this->themes.reset();
STOPPED.store(true);
}
void Application::initNm(const Paths &paths)
{
(void)paths;
@@ -613,6 +662,7 @@ void Application::initNm(const Paths &paths)
IApplication *getApp()
{
assert(INSTANCE != nullptr);
assert(STOPPED.load() == false);
return INSTANCE;
}
@@ -622,4 +672,9 @@ IApplication *tryGetApp()
return INSTANCE;
}
bool isAppAboutToQuit()
{
return ABOUT_TO_QUIT.load();
}
} // namespace chatterino
+5 -2
View File
@@ -139,7 +139,8 @@ public:
void initialize(Settings &settings, const Paths &paths);
void load();
void save();
void aboutToQuit();
void stop();
int run();
@@ -148,7 +149,7 @@ public:
private:
std::unique_ptr<Theme> themes;
std::unique_ptr<Fonts> fonts;
const std::unique_ptr<Logging> logging;
std::unique_ptr<Logging> logging;
std::unique_ptr<Emotes> emotes;
std::unique_ptr<AccountController> accounts;
std::unique_ptr<eventsub::IController> eventSub;
@@ -246,4 +247,6 @@ IApplication *getApp();
/// Might return `nullptr` if the app is being destroyed
IApplication *tryGetApp();
bool isAppAboutToQuit();
} // namespace chatterino
+4 -4
View File
@@ -282,13 +282,13 @@ void runGui(QApplication &a, const Paths &paths, Settings &settings,
QObject::connect(qApp, &QApplication::aboutToQuit, [] {
auto *app = dynamic_cast<Application *>(tryGetApp());
if (app)
{
app->save();
}
assert(app != nullptr);
app->aboutToQuit();
getSettings()->requestSave();
getSettings()->disableSave();
app->stop();
});
Application app(settings, paths, args, updates);
+27 -2
View File
@@ -131,6 +131,14 @@ void NetworkData::emitSuccess(NetworkResult &&result)
return;
}
if (isAppAboutToQuit())
{
qCDebug(chatterinoHTTP)
<< "Success callback for" << url.toString()
<< "skipped because we're about to quit";
return;
}
QElapsedTimer timer;
timer.start();
cb(result);
@@ -153,12 +161,21 @@ void NetworkData::emitError(NetworkResult &&result)
runCallback(this->executeConcurrently,
[cb = std::move(this->onError), result = std::move(result),
hasCaller = this->hasCaller, caller = this->caller]() {
url = this->request.url(), hasCaller = this->hasCaller,
caller = this->caller]() {
if (hasCaller && caller.isNull())
{
return;
}
if (isAppAboutToQuit())
{
qCDebug(chatterinoHTTP)
<< "Error callback for" << url.toString()
<< "skipped because we're about to quit";
return;
}
cb(result);
});
}
@@ -172,12 +189,20 @@ void NetworkData::emitFinally()
runCallback(this->executeConcurrently,
[cb = std::move(this->finally), hasCaller = this->hasCaller,
caller = this->caller]() {
url = this->request.url(), caller = this->caller]() {
if (hasCaller && caller.isNull())
{
return;
}
if (isAppAboutToQuit())
{
qCDebug(chatterinoHTTP)
<< "Finally callback for" << url.toString()
<< "skipped because we're about to quit";
return;
}
cb();
});
}
+11
View File
@@ -282,6 +282,15 @@ Image::~Image()
return;
}
if (isAppAboutToQuit())
{
if (this->frames_)
{
std::ignore = this->frames_.release();
}
return;
}
// Ensure the destructor for our frames is called in the GUI thread
// If the Image destructor is called outside of the GUI thread, move the
// ownership of the frames to the GUI thread, otherwise the frames will be
@@ -488,6 +497,8 @@ void Image::actuallyLoad()
return;
}
assert(!isAppAboutToQuit());
QBuffer buffer;
buffer.setData(result.getData());
QImageReader reader(&buffer);
+35 -23
View File
@@ -1,5 +1,6 @@
#include "providers/recentmessages/Api.hpp"
#include "Application.hpp"
#include "common/network/NetworkRequest.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/QLogging.hpp"
@@ -33,8 +34,15 @@ void load(
const long delayMs = jitter ? std::rand() % 100 : 0;
QTimer::singleShot(delayMs, [=] {
if (isAppAboutToQuit())
{
return;
}
NetworkRequest(url)
.onSuccess([channelPtr, onLoaded](const auto &result) {
assert(!isAppAboutToQuit());
auto shared = channelPtr.lock();
if (!shared)
{
@@ -51,30 +59,33 @@ void load(
auto builtMessages =
buildRecentMessages(parsedMessages, shared.get());
postToThread([shared = std::move(shared),
root = std::move(root),
messages = std::move(builtMessages),
onLoaded]() mutable {
// Notify user about a possible gap in logs if it returned some messages
// but isn't currently joined to a channel
const auto errorCode = root.value("error_code").toString();
if (!errorCode.isEmpty())
{
qCDebug(LOG)
<< QString("Got error from API: error_code=%1, "
"channel=%2")
.arg(errorCode, shared->getName());
if (errorCode == "channel_not_joined" &&
!messages.empty())
{
shared->addSystemMessage(
"Message history service recovering, there may "
"be gaps in the message history.");
}
}
postToThread(
[shared = std::move(shared), root = std::move(root),
messages = std::move(builtMessages), onLoaded]() mutable {
assert(!isAppAboutToQuit());
onLoaded(messages);
});
// Notify user about a possible gap in logs if it returned some messages
// but isn't currently joined to a channel
const auto errorCode =
root.value("error_code").toString();
if (!errorCode.isEmpty())
{
qCDebug(LOG)
<< QString("Got error from API: error_code=%1, "
"channel=%2")
.arg(errorCode, shared->getName());
if (errorCode == "channel_not_joined" &&
!messages.empty())
{
shared->addSystemMessage(
"Message history service recovering, there "
"may "
"be gaps in the message history.");
}
}
onLoaded(messages);
});
})
.onError([channelPtr, onError](const NetworkResult &result) {
auto shared = channelPtr.lock();
@@ -82,6 +93,7 @@ void load(
{
return;
}
assert(!isAppAboutToQuit());
qCDebug(LOG) << "Failed to load recent messages for"
<< shared->getName();
+2
View File
@@ -440,6 +440,8 @@ void SeventvEmotes::getEmoteSet(
getApp()->getSeventvAPI()->getEmoteSet(
emoteSetId,
[callback = std::move(successCallback), emoteSetId](const auto &json) {
assert(!isAppAboutToQuit());
auto parsedEmotes = json["emotes"].toArray();
auto emoteMap = parseEmotes(parsedEmotes, false);
+2 -2
View File
@@ -106,13 +106,13 @@ ChannelPointReward::ChannelPointReward(const QJsonObject &redemption)
}
else
{
static const ImageSet defaultImage{
static const ImageSet *defaultImage = new ImageSet{
Image::fromUrl({twitchChannelPointRewardUrl("1.png")}, 1, baseSize),
Image::fromUrl({twitchChannelPointRewardUrl("2.png")}, 0.5,
baseSize * 2),
Image::fromUrl({twitchChannelPointRewardUrl("4.png")}, 0.25,
baseSize * 4)};
this->image = defaultImage;
this->image = *defaultImage;
}
}
+1
View File
@@ -1381,6 +1381,7 @@ void TwitchChannel::loadRecentMessages()
recentmessages::load(
this->getName(), weak,
[weak](const auto &messages) {
assert(!isAppAboutToQuit());
auto shared = weak.lock();
if (!shared)
{
+66 -42
View File
@@ -254,6 +254,11 @@ void TwitchIrcServer::initialize()
auto reward = ChannelPointReward(data);
postToThread([chan, reward] {
if (isAppAboutToQuit())
{
return;
}
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->addChannelPointReward(reward);
@@ -262,6 +267,13 @@ void TwitchIrcServer::initialize()
});
}
void TwitchIrcServer::aboutToQuit()
{
this->signalHolder.clear();
this->channels.clear();
}
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
ConnectionType type)
{
@@ -550,8 +562,8 @@ std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
return this->automodChannel;
}
static auto getTimer = [](ChannelPtr channel, int msBetweenMessages,
bool addInitialMessages) {
static auto getTimer = [this](ChannelPtr channel, int msBetweenMessages,
bool addInitialMessages) {
if (addInitialMessages)
{
for (auto i = 0; i < 1000; i++)
@@ -561,7 +573,7 @@ std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
}
auto *timer = new QTimer;
QObject::connect(timer, &QTimer::timeout, [channel] {
QObject::connect(timer, &QTimer::timeout, this, [channel] {
channel->addSystemMessage(QTime::currentTime().toString());
});
timer->start(msBetweenMessages);
@@ -841,37 +853,43 @@ void TwitchIrcServer::initEventAPIs(BttvLiveUpdates *bttvLiveUpdates,
bttvLiveUpdates->signals_.emoteAdded, [&](const auto &data) {
auto chan = this->getChannelOrEmptyByID(data.channelID);
postToThread([chan, data] {
if (auto *channel =
dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->addBttvEmote(data);
}
});
postToThread(
[chan, data] {
if (auto *channel =
dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->addBttvEmote(data);
}
},
this);
});
this->signalHolder.managedConnect(
bttvLiveUpdates->signals_.emoteUpdated, [&](const auto &data) {
auto chan = this->getChannelOrEmptyByID(data.channelID);
postToThread([chan, data] {
if (auto *channel =
dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->updateBttvEmote(data);
}
});
postToThread(
[chan, data] {
if (auto *channel =
dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->updateBttvEmote(data);
}
},
this);
});
this->signalHolder.managedConnect(
bttvLiveUpdates->signals_.emoteRemoved, [&](const auto &data) {
auto chan = this->getChannelOrEmptyByID(data.channelID);
postToThread([chan, data] {
if (auto *channel =
dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->removeBttvEmote(data);
}
});
postToThread(
[chan, data] {
if (auto *channel =
dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->removeBttvEmote(data);
}
},
this);
});
bttvLiveUpdates->start();
@@ -886,30 +904,36 @@ void TwitchIrcServer::initEventAPIs(BttvLiveUpdates *bttvLiveUpdates,
{
this->signalHolder.managedConnect(
seventvEventAPI->signals_.emoteAdded, [this](const auto &data) {
postToThread([this, data] {
this->forEachSeventvEmoteSet(data.emoteSetID,
[data](TwitchChannel &chan) {
chan.addSeventvEmote(data);
});
});
postToThread(
[this, data] {
this->forEachSeventvEmoteSet(
data.emoteSetID, [data](TwitchChannel &chan) {
chan.addSeventvEmote(data);
});
},
this);
});
this->signalHolder.managedConnect(
seventvEventAPI->signals_.emoteUpdated, [this](const auto &data) {
postToThread([this, data] {
this->forEachSeventvEmoteSet(
data.emoteSetID, [data](TwitchChannel &chan) {
chan.updateSeventvEmote(data);
});
});
postToThread(
[this, data] {
this->forEachSeventvEmoteSet(
data.emoteSetID, [data](TwitchChannel &chan) {
chan.updateSeventvEmote(data);
});
},
this);
});
this->signalHolder.managedConnect(
seventvEventAPI->signals_.emoteRemoved, [this](const auto &data) {
postToThread([this, data] {
this->forEachSeventvEmoteSet(
data.emoteSetID, [data](TwitchChannel &chan) {
chan.removeSeventvEmote(data);
});
});
postToThread(
[this, data] {
this->forEachSeventvEmoteSet(
data.emoteSetID, [data](TwitchChannel &chan) {
chan.removeSeventvEmote(data);
});
},
this);
});
this->signalHolder.managedConnect(
seventvEventAPI->signals_.userUpdated, [this](const auto &data) {
+2
View File
@@ -94,6 +94,8 @@ public:
void initialize();
void aboutToQuit();
void forEachChannelAndSpecialChannels(
std::function<void(ChannelPtr)> func) override;
@@ -75,6 +75,11 @@ void Connection::onNotification(const lib::messages::Metadata &metadata,
void Connection::onClose(std::unique_ptr<lib::Listener> self,
const std::optional<std::string> &reconnectURL)
{
if (isAppAboutToQuit())
{
return;
}
auto *app = tryGetApp();
if (!app)
{
+19 -2
View File
@@ -104,7 +104,10 @@ Controller::~Controller()
connection->close();
}
this->subscriptions.clear();
{
std::lock_guard lock(this->subscriptionsMutex);
this->subscriptions.clear();
}
this->work.reset();
@@ -341,7 +344,7 @@ void Controller::subscribe(const SubscriptionRequest &request, bool isRetry)
qCDebug(LOG) << "Make helix request for" << request;
getHelix()->createEventSubSubscription(
request, listener->getSessionID(),
[this, request, connection,
[this, request,
weakConnection{std::weak_ptr<lib::Session>(connection)}](
const auto &res) {
qCDebug(LOG) << "Subscription success" << request;
@@ -507,6 +510,12 @@ void Controller::registerConnection(std::weak_ptr<lib::Session> &&connection)
void Controller::retrySubscription(const SubscriptionRequest &request)
{
if (isAppAboutToQuit())
{
qCDebug(LOG) << "retrySubscription, but app is quitting" << request;
return;
}
std::lock_guard lock(this->subscriptionsMutex);
auto &subscription = this->subscriptions[request];
@@ -536,6 +545,14 @@ void Controller::retrySubscription(const SubscriptionRequest &request)
std::make_unique<boost::asio::system_timer>(this->ioContext);
retryTimer->expires_after(subscription.backoff.next() + jitter);
retryTimer->async_wait([this, request](const auto &ec) {
if (isAppAboutToQuit())
{
qCDebug(LOG)
<< "Retry was going to fire, but app is quitting so we won't."
<< request;
return;
}
if (!ec)
{
qCDebug(LOG) << "Firing retry" << request;
+8 -1
View File
@@ -396,7 +396,14 @@ void removeLastQS(QString &str)
void writeProviderEmotesCache(const QString &id, const QString &provider,
const QByteArray &bytes)
{
QThreadPool::globalInstance()->start([bytes, id, provider]() {
auto *threadPool = QThreadPool::globalInstance();
if (threadPool == nullptr)
{
// Must be exiting - do nothing
return;
}
threadPool->start([bytes, id, provider]() {
auto cacheKey = id % "." % provider;
QFile responseCache(getApp()->getPaths().cacheFilePath(cacheKey));
+11
View File
@@ -1,5 +1,6 @@
#include "widgets/helper/NotebookButton.hpp"
#include "Application.hpp"
#include "common/QLogging.hpp"
#include "singletons/Theme.hpp"
#include "widgets/helper/Button.hpp"
@@ -213,11 +214,21 @@ void NotebookButton::dropEvent(QDropEvent *event)
void NotebookButton::hideEvent(QHideEvent *)
{
if (isAppAboutToQuit())
{
return;
}
this->parent_->refresh();
}
void NotebookButton::showEvent(QShowEvent *)
{
if (isAppAboutToQuit())
{
return;
}
this->parent_->refresh();
}
+2
View File
@@ -848,6 +848,8 @@ void SplitHeader::updateChannelText()
NetworkRequest(url, NetworkRequestType::Get)
.caller(this)
.onSuccess([this](auto result) {
assert(!isAppAboutToQuit());
// NOTE: We do not follow the redirects, so we need to make sure we only treat code 200 as a valid image
if (result.status() == 200)
{