1// Aseprite
2// Copyright (C) 2019 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/data_recovery.h"
13
14#include "app/crash/backup_observer.h"
15#include "app/crash/session.h"
16#include "app/pref/preferences.h"
17#include "app/resource_finder.h"
18#include "base/fs.h"
19#include "base/time.h"
20#include "ui/system.h"
21
22#include <algorithm>
23#include <chrono>
24#include <thread>
25
26#define RECO_TRACE(...) // TRACE
27
28namespace app {
29namespace crash {
30
31// Flag used to avoid calling SessionsListIsReady() signal after
32// DataRecovery() instance is deleted.
33static bool g_stillAliveFlag = false;
34
35DataRecovery::DataRecovery(Context* ctx)
36 : m_inProgress(nullptr)
37 , m_backup(nullptr)
38 , m_searching(false)
39{
40 auto& pref = Preferences::instance();
41 m_config.dataRecoveryPeriod = pref.general.dataRecoveryPeriod();
42 if (pref.general.keepEditedSpriteData())
43 m_config.keepEditedSpriteDataFor = pref.general.keepEditedSpriteDataFor();
44 else
45 m_config.keepEditedSpriteDataFor = 0;
46
47 ResourceFinder rf;
48 rf.includeUserDir(base::join_path("sessions", ".").c_str());
49 m_sessionsDir = rf.getFirstOrCreateDefault();
50
51 // Create a new session
52 base::pid pid = base::get_current_process_id();
53 std::string newSessionDir;
54
55 do {
56 base::Time time = base::current_time();
57
58 char buf[1024];
59 sprintf(buf, "%04d%02d%02d-%02d%02d%02d-%d",
60 time.year, time.month, time.day,
61 time.hour, time.minute, time.second, pid);
62
63 newSessionDir = base::join_path(m_sessionsDir, buf);
64
65 if (!base::is_directory(newSessionDir))
66 base::make_directory(newSessionDir);
67 else {
68 std::this_thread::sleep_for(std::chrono::seconds(1));
69 newSessionDir.clear();
70 }
71 } while (newSessionDir.empty());
72
73 m_inProgress.reset(new Session(&m_config, newSessionDir));
74 m_inProgress->create(pid);
75 RECO_TRACE("RECO: Session in progress '%s'\n", newSessionDir.c_str());
76
77 m_backup = new BackupObserver(&m_config, m_inProgress.get(), ctx);
78
79 g_stillAliveFlag = true;
80}
81
82DataRecovery::~DataRecovery()
83{
84 g_stillAliveFlag = false;
85 m_thread.join();
86
87 m_backup->stop();
88 delete m_backup;
89
90 // We just close the session on progress. The session is not
91 // deleted just in case that the user want to recover some files
92 // from this session in the future.
93 if (m_inProgress)
94 m_inProgress->close();
95
96 m_inProgress.reset();
97}
98
99void DataRecovery::launchSearch()
100{
101 if (m_searching)
102 return;
103
104 // Search current sessions in a background thread
105 if (m_thread.joinable())
106 m_thread.join();
107
108 ASSERT(!m_searching);
109 m_searching = true;
110
111 m_thread = std::thread(
112 [this]{
113 searchForSessions();
114 m_searching = false;
115 });
116}
117
118bool DataRecovery::hasRecoverySessions() const
119{
120 std::unique_lock<std::mutex> lock(m_sessionsMutex);
121
122 for (const auto& session : m_sessions)
123 if (session->isCrashedSession())
124 return true;
125 return false;
126}
127
128DataRecovery::Sessions DataRecovery::sessions()
129{
130 Sessions copy;
131 {
132 std::unique_lock<std::mutex> lock(m_sessionsMutex);
133 copy = m_sessions;
134 }
135 return copy;
136}
137
138void DataRecovery::searchForSessions()
139{
140 Sessions sessions;
141
142 // Existent sessions
143 RECO_TRACE("RECO: Listing sessions from '%s'\n", m_sessionsDir.c_str());
144 for (auto& itemname : base::list_files(m_sessionsDir)) {
145 std::string itempath = base::join_path(m_sessionsDir, itemname);
146 if (base::is_directory(itempath)) {
147 RECO_TRACE("RECO: Session '%s'\n", itempath.c_str());
148
149 SessionPtr session(new Session(&m_config, itempath));
150 if (!session->isRunning()) {
151 if ((session->isEmpty()) ||
152 (!session->isCrashedSession() && session->isOldSession())) {
153 RECO_TRACE("RECO: - to be deleted (%s)\n",
154 session->isEmpty() ? "is empty":
155 (session->isOldSession() ? "is old":
156 "unknown reason"));
157 session->removeFromDisk();
158 }
159 else {
160 RECO_TRACE("RECO: - to be loaded\n");
161 sessions.push_back(session);
162 }
163 }
164 else
165 RECO_TRACE("is running\n");
166 }
167 }
168
169 // Sort sessions from the most recent one to the oldest one
170 std::sort(sessions.begin(), sessions.end(),
171 [](const SessionPtr& a, const SessionPtr& b) {
172 return a->name() > b->name();
173 });
174
175 // Assign m_sessions=sessions
176 {
177 std::unique_lock<std::mutex> lock(m_sessionsMutex);
178 std::swap(m_sessions, sessions);
179 }
180
181 ui::execute_from_ui_thread(
182 [this]{
183 if (g_stillAliveFlag)
184 SessionsListIsReady();
185 });
186}
187
188} // namespace crash
189} // namespace app
190