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 | |
33 | namespace app { |
34 | namespace crash { |
35 | |
36 | namespace { |
37 | |
38 | class SwitchBackupIcon { |
39 | public: |
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 | |
58 | BackupObserver::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 | |
71 | BackupObserver::~BackupObserver() |
72 | { |
73 | m_thread.join(); |
74 | m_ctx->documents().remove_observer(this); |
75 | m_ctx->remove_observer(this); |
76 | } |
77 | |
78 | void BackupObserver::stop() |
79 | { |
80 | m_done = true; |
81 | m_wakeup.notify_one(); |
82 | } |
83 | |
84 | void 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 | |
92 | void 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 | |
122 | void 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) |
176 | bool 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 | |