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