aseprite/src/app/job.cpp
David Capello ec4e82bdc0 Fix crashes with SpriteJob(s) that weren't locking the doc correctly (fix #4315)
This was mainly found in SpriteSizeJob crash reports. In these reports
deleted image buffers were still used to paint the Editor canvas
because the doc was write-locked in the main thread (same thread where
the canvas is painted). This produced a re-entrant lock in the
Editor::onPaint() as we can still read-lock from the same thread where
we write-locked the doc.

With this change we write-lock the doc from the SpriteJob background
thread (not the main thread) only if it's necessary (i.e. when the doc
is not already locked in the main thread, e.g. when running a script).
This makes that the main thread (Editor::onPaint) cannot read the doc
until we finish the whole SpriteJob transaction/Tx.
2024-03-25 18:53:12 -03:00

151 lines
2.8 KiB
C++

// Aseprite
// Copyright (C) 2021-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/job.h"
#include "app/app.h"
#include "app/console.h"
#include "app/context.h"
#include "app/i18n/strings.h"
#include "fmt/format.h"
#include "ui/alert.h"
#include "ui/widget.h"
#include "ui/window.h"
#include <atomic>
static const int kMonitoringPeriod = 100;
static std::atomic<int> g_runningJobs(0);
namespace app {
// static
int Job::runningJobs()
{
return g_runningJobs;
}
Job::Job(const std::string& jobName)
{
m_last_progress = 0.0;
m_done_flag = false;
m_canceled_flag = false;
if (App::instance()->isGui()) {
m_alert_window = ui::Alert::create(
fmt::format(Strings::alerts_job_working(), jobName));
m_alert_window->addProgress();
m_timer.reset(new ui::Timer(kMonitoringPeriod, m_alert_window.get()));
m_timer->Tick.connect(&Job::onMonitoringTick, this);
m_timer->start();
}
}
Job::~Job()
{
if (App::instance()->isGui()) {
ASSERT(!m_timer->isRunning());
if (m_alert_window)
m_alert_window->closeWindow(NULL);
}
}
void Job::startJob()
{
m_thread = std::thread(&Job::thread_proc, this);
++g_runningJobs;
if (m_alert_window) {
m_alert_window->openWindowInForeground();
// The job was canceled by the user?
{
std::unique_lock<std::mutex> hold(m_mutex);
if (!m_done_flag)
m_canceled_flag = true;
}
// In case of error, take the "cancel" path (i.e. it's like the
// user canceled the operation).
if (m_error) {
m_canceled_flag = true;
try {
std::rethrow_exception(m_error);
}
catch (const std::exception& ex) {
Console::showException(ex);
}
catch (...) {
Console console;
console.printf("Unknown error performing the task");
}
}
}
}
void Job::waitJob()
{
if (m_timer && m_timer->isRunning())
m_timer->stop();
if (m_thread.joinable()) {
m_thread.join();
--g_runningJobs;
}
}
void Job::jobProgress(double f)
{
m_last_progress = f;
}
bool Job::isCanceled()
{
return m_canceled_flag;
}
void Job::onMonitoringTick()
{
std::unique_lock<std::mutex> hold(m_mutex);
// update progress
m_alert_window->setProgress(m_last_progress);
// is job done? we can close the monitor
if (m_done_flag || m_canceled_flag) {
m_timer->stop();
m_alert_window->closeWindow(NULL);
}
}
void Job::done()
{
std::unique_lock<std::mutex> hold(m_mutex);
m_done_flag = true;
}
// Called to start the worker thread.
void Job::thread_proc(Job* self)
{
try {
self->onJob();
}
catch (...) {
self->m_error = std::current_exception();
}
self->done();
}
} // namespace app