1// Aseprite
2// Copyright (C) 2018-2020 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8// Uncomment if you want to test the backup process each 5 seconds.
9//#define TEST_BACKUPS_WITH_A_SHORT_PERIOD
10
11// Uncomment if you want to check that backups are correctly saved
12// after being saved.
13//#define TEST_BACKUP_INTEGRITY
14
15#ifdef HAVE_CONFIG_H
16#include "config.h"
17#endif
18
19#include "app/crash/backup_observer.h"
20
21#include "app/app.h"
22#include "app/context.h"
23#include "app/crash/recovery_config.h"
24#include "app/crash/session.h"
25#include "app/doc.h"
26#include "app/doc_access.h"
27#include "app/doc_diff.h"
28#include "app/pref/preferences.h"
29#include "base/chrono.h"
30#include "base/remove_from_container.h"
31#include "ui/system.h"
32
33namespace app {
34namespace crash {
35
36namespace {
37
38class SwitchBackupIcon {
39public:
40 SwitchBackupIcon() {
41 ui::execute_from_ui_thread(
42 []{
43 if (App* app = App::instance())
44 app->showBackupNotification(true);
45 });
46 }
47 ~SwitchBackupIcon() {
48 ui::execute_from_ui_thread(
49 []{
50 if (App* app = App::instance())
51 app->showBackupNotification(false);
52 });
53 }
54};
55
56}
57
58BackupObserver::BackupObserver(RecoveryConfig* config,
59 Session* session,
60 Context* ctx)
61 : m_config(config)
62 , m_session(session)
63 , m_ctx(ctx)
64 , m_done(false)
65 , m_thread([this]{ backgroundThread(); })
66{
67 m_ctx->add_observer(this);
68 m_ctx->documents().add_observer(this);
69}
70
71BackupObserver::~BackupObserver()
72{
73 m_thread.join();
74 m_ctx->documents().remove_observer(this);
75 m_ctx->remove_observer(this);
76}
77
78void BackupObserver::stop()
79{
80 m_done = true;
81 m_wakeup.notify_one();
82}
83
84void BackupObserver::onAddDocument(Doc* document)
85{
86 TRACE("RECO: Observe document %p\n", document);
87
88 std::unique_lock<std::mutex> lock(m_mutex);
89 m_documents.push_back(document);
90}
91
92void BackupObserver::onRemoveDocument(Doc* doc)
93{
94 TRACE("RECO: Remove document %p\n", doc);
95 {
96 std::unique_lock<std::mutex> lock(m_mutex);
97 base::remove_from_container(m_documents, doc);
98 }
99 if (doc->needsBackup() &&
100 // If the document is already fully backed up, we don't need to
101 // add it to the background thread to create its backup
102 !doc->isFullyBackedUp() &&
103 // If the backup is disabled, we don't need it (e.g. when the
104 // document is destroyed from a script with Sprite:close(), the
105 // backup is disabled)
106 !doc->inhibitBackup()) {
107 // If m_config->keepEditedSpriteDataFor == 0 we add the document
108 // in m_closedDocs list anyway so we call markAsBackedUp(), and
109 // then it's deleted from ClosedDocs::backgroundThread()
110
111 TRACE("RECO: Adding to CLOSEDOC %p\n", doc);
112
113 std::unique_lock<std::mutex> lock(m_mutex);
114 m_closedDocs.push_back(doc);
115 }
116 else {
117 TRACE("RECO: Removing doc %p from session\n", doc);
118 m_session->removeDocument(doc);
119 }
120}
121
122void BackupObserver::backgroundThread()
123{
124 std::unique_lock<std::mutex> lock(m_mutex);
125
126 int normalPeriod = int(60.0*m_config->dataRecoveryPeriod);
127 int lockedPeriod = 5;
128#ifdef TEST_BACKUPS_WITH_A_SHORT_PERIOD
129 normalPeriod = 5;
130 lockedPeriod = 5;
131#endif
132
133 int waitFor = normalPeriod;
134
135 while (!m_done) {
136 m_wakeup.wait_for(lock, std::chrono::seconds(waitFor));
137
138 TRACE("RECO: Start backup process for %d documents\n",
139 m_documents.size() + m_closedDocs.size());
140
141 SwitchBackupIcon icon;
142 base::Chrono chrono;
143 bool somethingLocked = false;
144
145 for (Doc* doc : m_documents) {
146 if (!saveDocData(doc))
147 somethingLocked = true;
148 }
149
150 if (!m_closedDocs.empty()) {
151 for (auto it=m_closedDocs.begin(); it != m_closedDocs.end(); ) {
152 Doc* doc = *it;
153
154 TRACE("RECO: Save backup data for %p...\n", doc);
155
156 if (saveDocData(doc)) {
157 TRACE("RECO: Doc %p is fully backed up\n", doc);
158
159 it = m_closedDocs.erase(it);
160 doc->markAsBackedUp();
161 }
162 else {
163 somethingLocked = true;
164 ++it;
165 }
166 }
167 }
168
169 waitFor = (somethingLocked ? lockedPeriod: normalPeriod);
170
171 TRACE("RECO: Backup process done (%.16g)\n", chrono.elapsed());
172 }
173}
174
175// Executed from the backgroundThread() (non-UI thread)
176bool BackupObserver::saveDocData(Doc* doc)
177{
178 try {
179 if (!doc->needsBackup())
180 return true;
181
182 if (doc->inhibitBackup()) {
183 TRACE("RECO: Document '%d' backup is temporarily inhibited\n", doc->id());
184 }
185 else if (!m_session->saveDocumentChanges(doc)) {
186 TRACE("RECO: Document '%d' backup was canceled by UI\n", doc->id());
187 }
188 else {
189#ifdef TEST_BACKUP_INTEGRITY
190 DocReader reader(doc, 500);
191 std::unique_ptr<Doc> copy(
192 m_session->restoreBackupDocById(doc->id(), nullptr));
193 DocDiff diff = compare_docs(doc, copy.get());
194 if (diff.anything) {
195 TRACEARGS("RECO: Differences:",
196 diff.canvas ? "canvas": "",
197 diff.totalFrames ? "totalFrames": "",
198 diff.frameDuration ? "frameDuration": "",
199 diff.tags ? "tags": "",
200 diff.palettes ? "palettes": "",
201 diff.tilesets ? "tilesets": "",
202 diff.layers ? "layers": "",
203 diff.cels ? "cels": "",
204 diff.images ? "images": "",
205 diff.colorProfiles ? "colorProfiles": "",
206 diff.gridBounds ? "gridBounds": "");
207
208 Doc* copyDoc = copy.release();
209 ui::execute_from_ui_thread(
210 [this, copyDoc] {
211 m_ctx->documents().add(copyDoc);
212 });
213 }
214 else {
215 TRACE("RECO: No differences\n");
216 }
217#endif
218 return true;
219 }
220 }
221 catch (const std::exception&) {
222 TRACE("RECO: Document '%d' is locked\n", doc->id());
223 }
224 return false;
225}
226
227} // namespace crash
228} // namespace app
229