| 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 | |