Files
oak-editor/app/dialog/task/task.cpp
T
Mike-Solar 01f8be7267 Fix crash when closing export dialog while export is running
TaskDialog::closeEvent() was calling deleteLater() immediately when the
user closed the window. The ExportTask was a child QObject of the dialog,
so it got destroyed while its Run() method was still executing in the
worker thread, leading to a use-after-free on encoder_->Close().

Defer deletion until the QFutureWatcher reports that the task has actually
finished. A new task_finished_ flag tracks this so closeEvent only deletes
when it is safe, and TaskFinished() closes the dialog (which now deletes
instead of calling close() again while the task is still alive).
2026-07-13 10:19:30 +08:00

117 lines
3.0 KiB
C++

/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "task.h"
#include <QFutureWatcher>
#include <QtConcurrent>
namespace olive
{
#define super ProgressDialog
TaskDialog::TaskDialog(Task *task, const QString &title, QWidget *parent)
: super(task->GetTitle(), title, parent)
, task_(task)
, destroy_on_close_(true)
, already_shown_(false)
, task_finished_(false)
{
// Clear task when this dialog is destroyed
task_->setParent(this);
// Connect the save manager progress signal to the progress bar update on the dialog
connect(task_, &Task::ProgressChanged, this, &TaskDialog::SetProgress,
Qt::QueuedConnection);
// Connect cancel signal (must be a direct connection or it'll be queued after the task has
// already finished)
connect(this, &TaskDialog::Cancelled, task_, &Task::Cancel,
Qt::DirectConnection);
}
void TaskDialog::showEvent(QShowEvent *e)
{
super::showEvent(e);
if (!already_shown_) {
// Create watcher for when the task finishes
QFutureWatcher<bool> *task_watcher = new QFutureWatcher<bool>();
// Listen for when the task finishes
connect(task_watcher, &QFutureWatcher<bool>::finished, this,
&TaskDialog::TaskFinished, Qt::QueuedConnection);
// Run task in another thread with QtConcurrent
task_watcher->setFuture(
#if QT_VERSION_MAJOR >= 6
QtConcurrent::run(&Task::Start, task_)
#else
QtConcurrent::run(task_, &Task::Start)
#endif
);
already_shown_ = true;
}
}
void TaskDialog::closeEvent(QCloseEvent *e)
{
// Cancel task if it is running
task_->Cancel();
// Standard close function
super::closeEvent(e);
// Reset shown
already_shown_ = false;
// Clean up this task and dialog, but only if the task has actually finished.
// If the user closes the window while the task is still running, deleting now
// would destroy the Task object out from under the worker thread and crash
// when the task later touches its own members (e.g. ExportTask::encoder_).
if (destroy_on_close_ && task_finished_) {
deleteLater();
}
}
void TaskDialog::TaskFinished()
{
QFutureWatcher<bool> *task_watcher =
static_cast<QFutureWatcher<bool> *>(sender());
task_finished_ = true;
if (task_watcher->result()) {
emit TaskSucceeded(task_);
} else {
ShowErrorMessage(tr("Task Failed"), task_->GetError());
emit TaskFailed(task_);
}
task_watcher->deleteLater();
close();
}
}