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
35namespace app {
36namespace crash {
37
38static const char* kPidFilename = "pid"; // Process ID running the session (or non-existent if the PID was closed correctly)
39static const char* kVerFilename = "ver"; // File that indicates the Aseprite version used in the session
40static 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
42Session::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
59std::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
67Session::Session(RecoveryConfig* config,
68 const std::string& path)
69 : m_pid(0)
70 , m_path(path)
71 , m_config(config)
72{
73}
74
75Session::~Session()
76{
77}
78
79std::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
100std::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
113const 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
126bool Session::isRunning()
127{
128 loadPid();
129 if (m_pid)
130 return base::is_process_running(m_pid);
131 else
132 return false;
133}
134
135bool Session::isCrashedSession()
136{
137 loadPid();
138 return (m_pid != 0);
139}
140
141bool 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
156bool 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
165void 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
176void 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
194void 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
217class CustomWeakDocReader : public WeakDocReader
218 , public doc::CancelIO {
219public:
220 explicit CustomWeakDocReader(Doc* doc)
221 : WeakDocReader(doc) {
222 }
223
224 // CancelIO impl
225 bool isCanceled() override {
226 return !isLocked();
227 }
228};
229
230bool 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
259void 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
271Doc* 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
288Doc* Session::restoreBackupDoc(const BackupPtr& backup,
289 base::task_token* t)
290{
291 return restoreBackupDoc(backup->dir(), t);
292}
293
294Doc* 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
304Doc* 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
314Doc* 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
333void 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
344void 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
357std::string Session::pidFilename() const
358{
359 return base::join_path(m_path, kPidFilename);
360}
361
362std::string Session::verFilename() const
363{
364 return base::join_path(m_path, kVerFilename);
365}
366
367void 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
383void 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
399void 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