| 1 | // Aseprite | 
|---|
| 2 | // Copyright (C) 2019-2022  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 | #ifdef HAVE_CONFIG_H | 
|---|
| 9 | #include "config.h" | 
|---|
| 10 | #endif | 
|---|
| 11 |  | 
|---|
| 12 | #include "app/crash/session.h" | 
|---|
| 13 |  | 
|---|
| 14 | #include "app/console.h" | 
|---|
| 15 | #include "app/context.h" | 
|---|
| 16 | #include "app/crash/read_document.h" | 
|---|
| 17 | #include "app/crash/recovery_config.h" | 
|---|
| 18 | #include "app/crash/write_document.h" | 
|---|
| 19 | #include "app/doc.h" | 
|---|
| 20 | #include "app/doc_access.h" | 
|---|
| 21 | #include "app/file/file.h" | 
|---|
| 22 | #include "app/ui_context.h" | 
|---|
| 23 | #include "base/convert_to.h" | 
|---|
| 24 | #include "base/fs.h" | 
|---|
| 25 | #include "base/fstream_path.h" | 
|---|
| 26 | #include "base/process.h" | 
|---|
| 27 | #include "base/split_string.h" | 
|---|
| 28 | #include "base/string.h" | 
|---|
| 29 | #include "base/thread.h" | 
|---|
| 30 | #include "base/time.h" | 
|---|
| 31 | #include "doc/cancel_io.h" | 
|---|
| 32 | #include "fmt/format.h" | 
|---|
| 33 | #include "ver/info.h" | 
|---|
| 34 |  | 
|---|
| 35 | namespace app { | 
|---|
| 36 | namespace crash { | 
|---|
| 37 |  | 
|---|
| 38 | static const char* kPidFilename = "pid";   // Process ID running the session (or non-existent if the PID was closed correctly) | 
|---|
| 39 | static const char* kVerFilename = "ver";   // File that indicates the Aseprite version used in the session | 
|---|
| 40 | static const char* kOpenFilename = "open"; // File that indicates if the document is/was open in the session (or non-existent if the document was closed correctly) | 
|---|
| 41 |  | 
|---|
| 42 | Session::Backup::Backup(const std::string& dir) | 
|---|
| 43 | : m_dir(dir) | 
|---|
| 44 | { | 
|---|
| 45 | DocumentInfo info; | 
|---|
| 46 | read_document_info(dir, info); | 
|---|
| 47 |  | 
|---|
| 48 | m_fn = info.filename; | 
|---|
| 49 | m_desc = | 
|---|
| 50 | fmt::format( "{} Sprite {}x{}, {} {}", | 
|---|
| 51 | info.mode == ColorMode::RGB ? "RGB": | 
|---|
| 52 | info.mode == ColorMode::GRAYSCALE ? "Grayscale": | 
|---|
| 53 | info.mode == ColorMode::INDEXED ? "Indexed": | 
|---|
| 54 | info.mode == ColorMode::BITMAP ? "Bitmap": "Unknown", | 
|---|
| 55 | info.width, info.height, info.frames, | 
|---|
| 56 | info.frames == 1 ? "frame": "frames"); | 
|---|
| 57 | } | 
|---|
| 58 |  | 
|---|
| 59 | std::string Session::Backup::description(const bool withFullPath) const | 
|---|
| 60 | { | 
|---|
| 61 | return fmt::format( "{}: {}", | 
|---|
| 62 | m_desc, | 
|---|
| 63 | withFullPath ? m_fn: | 
|---|
| 64 | base::get_file_name(m_fn)); | 
|---|
| 65 | } | 
|---|
| 66 |  | 
|---|
| 67 | Session::Session(RecoveryConfig* config, | 
|---|
| 68 | const std::string& path) | 
|---|
| 69 | : m_pid(0) | 
|---|
| 70 | , m_path(path) | 
|---|
| 71 | , m_config(config) | 
|---|
| 72 | { | 
|---|
| 73 | } | 
|---|
| 74 |  | 
|---|
| 75 | Session::~Session() | 
|---|
| 76 | { | 
|---|
| 77 | } | 
|---|
| 78 |  | 
|---|
| 79 | std::string Session::name() const | 
|---|
| 80 | { | 
|---|
| 81 | std::string name = base::get_file_title(m_path); | 
|---|
| 82 | std::vector<std::string> parts; | 
|---|
| 83 | base::split_string(name, parts, "-"); | 
|---|
| 84 |  | 
|---|
| 85 | if (parts.size() == 3) { | 
|---|
| 86 | if (parts[0].size() == 4+2+2) { // YYYYMMDD -> YYYY-MM-DD | 
|---|
| 87 | parts[0].insert(6, 1, '-'); | 
|---|
| 88 | parts[0].insert(4, 1, '-'); | 
|---|
| 89 | } | 
|---|
| 90 | if (parts[1].size() == 2+2+2) { // HHMMSS -> HH:MM.SS | 
|---|
| 91 | parts[1].insert(4, 1, '.'); | 
|---|
| 92 | parts[1].insert(2, 1, ':'); | 
|---|
| 93 | } | 
|---|
| 94 | return "Session date: "+ parts[0] + " time: "+ parts[1] + " (PID "+ parts[2] + ")"; | 
|---|
| 95 | } | 
|---|
| 96 | else | 
|---|
| 97 | return name; | 
|---|
| 98 | } | 
|---|
| 99 |  | 
|---|
| 100 | std::string Session::version() | 
|---|
| 101 | { | 
|---|
| 102 | if (m_version.empty()) { | 
|---|
| 103 | std::string verfile = verFilename(); | 
|---|
| 104 | if (base::is_file(verfile)) { | 
|---|
| 105 | std::ifstream pf(FSTREAM_PATH(verfile)); | 
|---|
| 106 | if (pf) | 
|---|
| 107 | pf >> m_version; | 
|---|
| 108 | } | 
|---|
| 109 | } | 
|---|
| 110 | return m_version; | 
|---|
| 111 | } | 
|---|
| 112 |  | 
|---|
| 113 | const Session::Backups& Session::backups() | 
|---|
| 114 | { | 
|---|
| 115 | if (m_backups.empty()) { | 
|---|
| 116 | for (auto& item : base::list_files(m_path)) { | 
|---|
| 117 | std::string docDir = base::join_path(m_path, item); | 
|---|
| 118 | if (base::is_directory(docDir)) { | 
|---|
| 119 | m_backups.push_back(std::make_shared<Backup>(docDir)); | 
|---|
| 120 | } | 
|---|
| 121 | } | 
|---|
| 122 | } | 
|---|
| 123 | return m_backups; | 
|---|
| 124 | } | 
|---|
| 125 |  | 
|---|
| 126 | bool Session::isRunning() | 
|---|
| 127 | { | 
|---|
| 128 | loadPid(); | 
|---|
| 129 | if (m_pid) | 
|---|
| 130 | return base::is_process_running(m_pid); | 
|---|
| 131 | else | 
|---|
| 132 | return false; | 
|---|
| 133 | } | 
|---|
| 134 |  | 
|---|
| 135 | bool Session::isCrashedSession() | 
|---|
| 136 | { | 
|---|
| 137 | loadPid(); | 
|---|
| 138 | return (m_pid != 0); | 
|---|
| 139 | } | 
|---|
| 140 |  | 
|---|
| 141 | bool Session::isOldSession() | 
|---|
| 142 | { | 
|---|
| 143 | if (m_config->keepEditedSpriteDataFor <= 0) | 
|---|
| 144 | return true; | 
|---|
| 145 |  | 
|---|
| 146 | std::string verfile = verFilename(); | 
|---|
| 147 | if (!base::is_file(verfile)) | 
|---|
| 148 | return true; | 
|---|
| 149 |  | 
|---|
| 150 | int lifespanDays = m_config->keepEditedSpriteDataFor; | 
|---|
| 151 | base::Time sessionTime = base::get_modification_time(verfile); | 
|---|
| 152 |  | 
|---|
| 153 | return (sessionTime.addDays(lifespanDays) < base::current_time()); | 
|---|
| 154 | } | 
|---|
| 155 |  | 
|---|
| 156 | bool Session::isEmpty() | 
|---|
| 157 | { | 
|---|
| 158 | for (auto& item : base::list_files(m_path)) { | 
|---|
| 159 | if (base::is_directory(base::join_path(m_path, item))) | 
|---|
| 160 | return false; | 
|---|
| 161 | } | 
|---|
| 162 | return true; | 
|---|
| 163 | } | 
|---|
| 164 |  | 
|---|
| 165 | void Session::create(base::pid pid) | 
|---|
| 166 | { | 
|---|
| 167 | m_pid = pid; | 
|---|
| 168 |  | 
|---|
| 169 | std::ofstream pidf(FSTREAM_PATH(pidFilename())); | 
|---|
| 170 | std::ofstream verf(FSTREAM_PATH(verFilename())); | 
|---|
| 171 |  | 
|---|
| 172 | pidf << m_pid; | 
|---|
| 173 | verf << get_app_version(); | 
|---|
| 174 | } | 
|---|
| 175 |  | 
|---|
| 176 | void Session::close() | 
|---|
| 177 | { | 
|---|
| 178 | try { | 
|---|
| 179 | // Just remove the PID file to indicate that this session was | 
|---|
| 180 | // correctly closed | 
|---|
| 181 | if (base::is_file(pidFilename())) | 
|---|
| 182 | base::delete_file(pidFilename()); | 
|---|
| 183 |  | 
|---|
| 184 | // If we don't have to keep the sprite data, just remove it from | 
|---|
| 185 | // the disk. | 
|---|
| 186 | if (m_config->keepEditedSpriteDataFor == 0) | 
|---|
| 187 | removeFromDisk(); | 
|---|
| 188 | } | 
|---|
| 189 | catch (const std::exception&) { | 
|---|
| 190 | // TODO Log this error | 
|---|
| 191 | } | 
|---|
| 192 | } | 
|---|
| 193 |  | 
|---|
| 194 | void Session::removeFromDisk() | 
|---|
| 195 | { | 
|---|
| 196 | try { | 
|---|
| 197 | // Remove all backups from disk | 
|---|
| 198 | Backups baks = backups(); | 
|---|
| 199 | for (const BackupPtr& bak : baks) | 
|---|
| 200 | deleteBackup(bak); | 
|---|
| 201 |  | 
|---|
| 202 | if (base::is_file(pidFilename())) | 
|---|
| 203 | base::delete_file(pidFilename()); | 
|---|
| 204 |  | 
|---|
| 205 | if (base::is_file(verFilename())) | 
|---|
| 206 | base::delete_file(verFilename()); | 
|---|
| 207 |  | 
|---|
| 208 | base::remove_directory(m_path); | 
|---|
| 209 | } | 
|---|
| 210 | catch (const std::exception& ex) { | 
|---|
| 211 | (void)ex; | 
|---|
| 212 | LOG(ERROR, "RECO: Session directory cannot be removed, it's not empty.\n" | 
|---|
| 213 | "      Error: %s\n", ex.what()); | 
|---|
| 214 | } | 
|---|
| 215 | } | 
|---|
| 216 |  | 
|---|
| 217 | class CustomWeakDocReader : public WeakDocReader | 
|---|
| 218 | , public doc::CancelIO { | 
|---|
| 219 | public: | 
|---|
| 220 | explicit CustomWeakDocReader(Doc* doc) | 
|---|
| 221 | : WeakDocReader(doc) { | 
|---|
| 222 | } | 
|---|
| 223 |  | 
|---|
| 224 | // CancelIO impl | 
|---|
| 225 | bool isCanceled() override { | 
|---|
| 226 | return !isLocked(); | 
|---|
| 227 | } | 
|---|
| 228 | }; | 
|---|
| 229 |  | 
|---|
| 230 | bool Session::saveDocumentChanges(Doc* doc) | 
|---|
| 231 | { | 
|---|
| 232 | CustomWeakDocReader reader(doc); | 
|---|
| 233 | if (!reader.isLocked()) | 
|---|
| 234 | return false; | 
|---|
| 235 |  | 
|---|
| 236 | app::Context ctx; | 
|---|
| 237 | std::string dir = base::join_path(m_path, | 
|---|
| 238 | base::convert_to<std::string>(doc->id())); | 
|---|
| 239 | TRACE( "RECO: Saving document '%s'...\n", dir.c_str()); | 
|---|
| 240 |  | 
|---|
| 241 | // Create directory for document | 
|---|
| 242 | if (!base::is_directory(dir)) | 
|---|
| 243 | base::make_directory(dir); | 
|---|
| 244 |  | 
|---|
| 245 | // Create "open" file to indicate that the document is open in this session | 
|---|
| 246 | { | 
|---|
| 247 | std::string openfile = base::join_path(dir, kOpenFilename); | 
|---|
| 248 | if (!base::is_file(openfile)) { | 
|---|
| 249 | std::ofstream of(FSTREAM_PATH(openfile)); | 
|---|
| 250 | if (of) | 
|---|
| 251 | of << "open"; | 
|---|
| 252 | } | 
|---|
| 253 | } | 
|---|
| 254 |  | 
|---|
| 255 | // Save document information | 
|---|
| 256 | return write_document(dir, doc, &reader); | 
|---|
| 257 | } | 
|---|
| 258 |  | 
|---|
| 259 | void Session::removeDocument(Doc* doc) | 
|---|
| 260 | { | 
|---|
| 261 | try { | 
|---|
| 262 | delete_document_internals(doc); | 
|---|
| 263 |  | 
|---|
| 264 | markDocumentAsCorrectlyClosed(doc); | 
|---|
| 265 | } | 
|---|
| 266 | catch (const std::exception& ex) { | 
|---|
| 267 | LOG(FATAL, "Exception deleting document %s\n", ex.what()); | 
|---|
| 268 | } | 
|---|
| 269 | } | 
|---|
| 270 |  | 
|---|
| 271 | Doc* Session::restoreBackupDoc(const std::string& backupDir, | 
|---|
| 272 | base::task_token* t) | 
|---|
| 273 | { | 
|---|
| 274 | Console console; | 
|---|
| 275 | try { | 
|---|
| 276 | Doc* doc = read_document(backupDir, t); | 
|---|
| 277 | if (doc) { | 
|---|
| 278 | fixFilename(doc); | 
|---|
| 279 | return doc; | 
|---|
| 280 | } | 
|---|
| 281 | } | 
|---|
| 282 | catch (const std::exception& ex) { | 
|---|
| 283 | Console::showException(ex); | 
|---|
| 284 | } | 
|---|
| 285 | return nullptr; | 
|---|
| 286 | } | 
|---|
| 287 |  | 
|---|
| 288 | Doc* Session::restoreBackupDoc(const BackupPtr& backup, | 
|---|
| 289 | base::task_token* t) | 
|---|
| 290 | { | 
|---|
| 291 | return restoreBackupDoc(backup->dir(), t); | 
|---|
| 292 | } | 
|---|
| 293 |  | 
|---|
| 294 | Doc* Session::restoreBackupById(const doc::ObjectId id, | 
|---|
| 295 | base::task_token* t) | 
|---|
| 296 | { | 
|---|
| 297 | std::string docDir = base::join_path(m_path, base::convert_to<std::string>(int(id))); | 
|---|
| 298 | if (base::is_directory(docDir)) | 
|---|
| 299 | return restoreBackupDoc(docDir, t); | 
|---|
| 300 | else | 
|---|
| 301 | return nullptr; | 
|---|
| 302 | } | 
|---|
| 303 |  | 
|---|
| 304 | Doc* Session::restoreBackupDocById(const doc::ObjectId id, | 
|---|
| 305 | base::task_token* t) | 
|---|
| 306 | { | 
|---|
| 307 | std::string docDir = base::join_path(m_path, base::convert_to<std::string>(int(id))); | 
|---|
| 308 | if (!base::is_directory(docDir)) | 
|---|
| 309 | return nullptr; | 
|---|
| 310 |  | 
|---|
| 311 | return restoreBackupDoc(docDir, t); | 
|---|
| 312 | } | 
|---|
| 313 |  | 
|---|
| 314 | Doc* Session::restoreBackupRawImages(const BackupPtr& backup, | 
|---|
| 315 | const RawImagesAs as, | 
|---|
| 316 | base::task_token* t) | 
|---|
| 317 | { | 
|---|
| 318 | Console console; | 
|---|
| 319 | try { | 
|---|
| 320 | Doc* doc = read_document_with_raw_images(backup->dir(), as, t); | 
|---|
| 321 | if (doc) { | 
|---|
| 322 | if (isCrashedSession()) | 
|---|
| 323 | fixFilename(doc); | 
|---|
| 324 | } | 
|---|
| 325 | return doc; | 
|---|
| 326 | } | 
|---|
| 327 | catch (const std::exception& ex) { | 
|---|
| 328 | Console::showException(ex); | 
|---|
| 329 | } | 
|---|
| 330 | return nullptr; | 
|---|
| 331 | } | 
|---|
| 332 |  | 
|---|
| 333 | void Session::deleteBackup(const BackupPtr& backup) | 
|---|
| 334 | { | 
|---|
| 335 | auto it = std::find(m_backups.begin(), m_backups.end(), backup); | 
|---|
| 336 | ASSERT(it != m_backups.end()); | 
|---|
| 337 | if (it != m_backups.end()) | 
|---|
| 338 | m_backups.erase(it); | 
|---|
| 339 |  | 
|---|
| 340 | if (base::is_directory(backup->dir())) | 
|---|
| 341 | deleteDirectory(backup->dir()); | 
|---|
| 342 | } | 
|---|
| 343 |  | 
|---|
| 344 | void Session::loadPid() | 
|---|
| 345 | { | 
|---|
| 346 | if (m_pid) | 
|---|
| 347 | return; | 
|---|
| 348 |  | 
|---|
| 349 | std::string pidfile = pidFilename(); | 
|---|
| 350 | if (base::is_file(pidfile)) { | 
|---|
| 351 | std::ifstream pf(FSTREAM_PATH(pidfile)); | 
|---|
| 352 | if (pf) | 
|---|
| 353 | pf >> m_pid; | 
|---|
| 354 | } | 
|---|
| 355 | } | 
|---|
| 356 |  | 
|---|
| 357 | std::string Session::pidFilename() const | 
|---|
| 358 | { | 
|---|
| 359 | return base::join_path(m_path, kPidFilename); | 
|---|
| 360 | } | 
|---|
| 361 |  | 
|---|
| 362 | std::string Session::verFilename() const | 
|---|
| 363 | { | 
|---|
| 364 | return base::join_path(m_path, kVerFilename); | 
|---|
| 365 | } | 
|---|
| 366 |  | 
|---|
| 367 | void Session::markDocumentAsCorrectlyClosed(app::Doc* doc) | 
|---|
| 368 | { | 
|---|
| 369 | std::string dir = base::join_path( | 
|---|
| 370 | m_path, base::convert_to<std::string>(doc->id())); | 
|---|
| 371 |  | 
|---|
| 372 | ASSERT(!dir.empty()); | 
|---|
| 373 | if (dir.empty() || !base::is_directory(dir)) | 
|---|
| 374 | return; | 
|---|
| 375 |  | 
|---|
| 376 | std::string openFn = base::join_path(dir, kOpenFilename); | 
|---|
| 377 | if (base::is_file(openFn)) { | 
|---|
| 378 | TRACE( "RECO: Document was closed correctly, deleting file '%s'\n", openFn.c_str()); | 
|---|
| 379 | base::delete_file(openFn); | 
|---|
| 380 | } | 
|---|
| 381 | } | 
|---|
| 382 |  | 
|---|
| 383 | void Session::deleteDirectory(const std::string& dir) | 
|---|
| 384 | { | 
|---|
| 385 | ASSERT(!dir.empty()); | 
|---|
| 386 | if (dir.empty()) | 
|---|
| 387 | return; | 
|---|
| 388 |  | 
|---|
| 389 | for (auto& item : base::list_files(dir)) { | 
|---|
| 390 | std::string objfn = base::join_path(dir, item); | 
|---|
| 391 | if (base::is_file(objfn)) { | 
|---|
| 392 | TRACE( "RECO: Deleting file '%s'\n", objfn.c_str()); | 
|---|
| 393 | base::delete_file(objfn); | 
|---|
| 394 | } | 
|---|
| 395 | } | 
|---|
| 396 | base::remove_directory(dir); | 
|---|
| 397 | } | 
|---|
| 398 |  | 
|---|
| 399 | void Session::fixFilename(Doc* doc) | 
|---|
| 400 | { | 
|---|
| 401 | std::string fn = doc->filename(); | 
|---|
| 402 | if (fn.empty()) | 
|---|
| 403 | return; | 
|---|
| 404 |  | 
|---|
| 405 | std::string ext = base::get_file_extension(fn); | 
|---|
| 406 | if (!ext.empty()) | 
|---|
| 407 | ext = "."+ ext; | 
|---|
| 408 |  | 
|---|
| 409 | doc->setFilename( | 
|---|
| 410 | base::join_path( | 
|---|
| 411 | base::get_file_path(fn), | 
|---|
| 412 | base::get_file_title(fn) + "-Recovered"+ ext)); | 
|---|
| 413 | } | 
|---|
| 414 |  | 
|---|
| 415 | } // namespace crash | 
|---|
| 416 | } // namespace app | 
|---|
| 417 |  | 
|---|