1 #include "coreapplication.h"
2 #include "debug.h"
3 #include "translator.h"
4 #include "utils.h"
5 #include "widgets/setupdialog.h"
6
7 #include <QFontDatabase>
8 #include <QMessageBox>
9
10 #define UPDATED_LAUNCHD_PATH_LONG \
11 "The OS X launchd scheduling service contained an out-of-date link to " \
12 "Tarsnap GUI (did you upgrade it recently?).\n\nThis has been updated to " \
13 "point to the current Tarsnap GUI."
14
15 #define UPDATED_LAUNCHD_PATH_SHORT "Updated launchd path to Tarsnap GUI"
16
17 #define UPDATED_LAUNCHD_PATH_ERROR \
18 "An error occurred while attempting to " \
19 "update the OS X launchd path."
20
21 #if defined(Q_OS_OSX)
22 struct cmdinfo
23 {
24 int exit_code;
25 QByteArray stderr_msg;
26 QByteArray stdout_msg;
27 };
28
runCmd(QString cmd,QStringList args,const QByteArray * stdin_msg=nullptr)29 static struct cmdinfo runCmd(QString cmd, QStringList args,
30 const QByteArray *stdin_msg = nullptr)
31 {
32 QProcess proc;
33 struct cmdinfo info;
34 proc.start(cmd, args);
35
36 // We can want stdin_msg to be empty but non-null; for example if we're
37 // disabling the scheduling and therefore writing an empty crontab file.
38 if(stdin_msg != nullptr)
39 {
40 proc.write(*stdin_msg);
41 proc.closeWriteChannel();
42 }
43 proc.waitForFinished(-1);
44
45 // Get exit code, working around QProcess not having a valid exitCode()
46 // if there's a crash.
47 info.exit_code = proc.exitCode();
48 if(proc.exitStatus() != QProcess::NormalExit)
49 info.exit_code = -1;
50
51 info.stderr_msg = proc.readAllStandardError();
52 info.stdout_msg = proc.readAllStandardOutput();
53
54 return (info);
55 }
56
57 // This is an awkward hack which is an intermediate step towards separating
58 // the front-end and back-end code. Return values:
59 // 0: everything ok
60 // 1: failed to load
61 // 2: failed to start
launchdLoad()62 static int launchdLoad()
63 {
64 struct cmdinfo pinfo;
65 QString launchdPlistFileName =
66 QDir::homePath() + "/Library/LaunchAgents/com.tarsnap.gui.plist";
67
68 pinfo = runCmd("launchctl", QStringList() << "load" << launchdPlistFileName);
69 if(pinfo.exit_code != 0)
70 return (1);
71
72 pinfo = runCmd("launchctl", QStringList() << "start"
73 << "com.tarsnap.gui");
74 if(pinfo.exit_code != 0)
75 return (2);
76
77 return (0);
78 }
79
80 // Return values:
81 // 0: everything ok
82 // 1: failed to unload
launchdUnload()83 static int launchdUnload()
84 {
85 struct cmdinfo pinfo;
86 QString launchdPlistFileName =
87 QDir::homePath() + "/Library/LaunchAgents/com.tarsnap.gui.plist";
88
89 pinfo =
90 runCmd("launchctl", QStringList() << "unload" << launchdPlistFileName);
91 if(pinfo.exit_code != 0)
92 return (1);
93
94 return (0);
95 }
96
launchdLoaded()97 static bool launchdLoaded()
98 {
99 struct cmdinfo pinfo;
100
101 pinfo = runCmd("launchctl", QStringList() << "list"
102 << "com.tarsnap.gui");
103 if(pinfo.exit_code != 0)
104 return (false);
105
106 return (true);
107 }
108 #endif
109
110 // Returns:
111 // -1: no change
112 // 0: changed successfully
113 // 1: an error occurred
114 static
correctedSchedulingPath()115 int correctedSchedulingPath()
116 {
117 #if defined(Q_OS_OSX)
118 QSettings launchdPlist(QDir::homePath()
119 + "/Library/LaunchAgents/com.tarsnap.gui.plist",
120 QSettings::NativeFormat);
121
122 // Bail if the file doesn't exist
123 if(!launchdPlist.contains("ProgramArguments"))
124 return (-1);
125
126 // Get path, bail if it still exists (we assume it's still executable)
127 QStringList args =
128 launchdPlist.value("ProgramArguments").value<QStringList>();
129 if(QFile::exists(args.at(0)))
130 return (-1);
131
132 // Update the path
133 args.replace(0, QCoreApplication::applicationFilePath().toLatin1());
134 launchdPlist.setValue("ProgramArguments", args);
135 launchdPlist.sync();
136
137 // Stop launchd script if it's loaded
138 if(launchdLoaded())
139 {
140 if(launchdUnload() != 0)
141 return (1);
142 }
143
144 // Load (and start) new program
145 if(launchdLoad() != 0)
146 return (1);
147
148 return (0);
149 #else
150 return (-1);
151 #endif
152 }
153
CoreApplication(int & argc,char ** argv)154 CoreApplication::CoreApplication(int &argc, char **argv)
155 : QApplication(argc, argv),
156 _mainWindow(nullptr),
157 _notification(),
158 _jobsOption(false)
159 {
160 setQuitOnLastWindowClosed(false);
161 setQuitLockEnabled(false);
162 setAttribute(Qt::AA_UseHighDpiPixmaps);
163
164 qRegisterMetaType<TaskStatus>("TaskStatus");
165 qRegisterMetaType<QList<QUrl>>("QList<QUrl>");
166 qRegisterMetaType<BackupTaskPtr>("BackupTaskPtr");
167 qRegisterMetaType<QList<ArchivePtr>>("QList<ArchivePtr >");
168 qRegisterMetaType<ArchivePtr>("ArchivePtr");
169 qRegisterMetaType<ArchiveRestoreOptions>("ArchiveRestoreOptions");
170 qRegisterMetaType<QSqlQuery>("QSqlQuery");
171 qRegisterMetaType<JobPtr>("JobPtr");
172 qRegisterMetaType<QMap<QString, JobPtr>>("QMap<QString, JobPtr>");
173 qRegisterMetaType<QSystemTrayIcon::ActivationReason>(
174 "QSystemTrayIcon::ActivationReason");
175 qRegisterMetaType<TarsnapError>("TarsnapError");
176 qRegisterMetaType<LogEntry>("LogEntry");
177 qRegisterMetaType<QVector<LogEntry>>("QVector<LogEntry>");
178 qRegisterMetaType<QVector<File>>("QVector<File>");
179
180 QCoreApplication::setOrganizationName(QLatin1String("Tarsnap Backup Inc."));
181 QCoreApplication::setOrganizationDomain(QLatin1String("tarsnap.com"));
182 QCoreApplication::setApplicationName(QLatin1String("Tarsnap"));
183 QCoreApplication::setApplicationVersion(APP_VERSION);
184 }
185
~CoreApplication()186 CoreApplication::~CoreApplication()
187 {
188 if(_mainWindow)
189 delete _mainWindow;
190 _managerThread.quit();
191 _managerThread.wait();
192 }
193
parseArgs()194 void CoreApplication::parseArgs()
195 {
196 QCommandLineParser parser;
197 parser.setApplicationDescription(
198 tr("Tarsnap GUI - Online backups for the truly lazy"));
199 parser.addHelpOption();
200 parser.addVersionOption();
201 QCommandLineOption jobsOption(QStringList() << "j"
202 << "jobs",
203 tr("Executes all Jobs sequentially that have "
204 "the \'Automatic backup schedule\' "
205 "option enabled."
206 " The application runs headless and "
207 "useful information is printed to "
208 "standard out and error."));
209 QCommandLineOption appDataOption(QStringList() << "a"
210 << "appdata",
211 tr("Use the specified app data directory."
212 " Useful for multiple configurations "
213 "on the same machine (INI format is "
214 "implied)."),
215 tr("directory"));
216 QCommandLineOption checkOption(QStringList() << "check",
217 tr("Check that Tarsnap GUI is correctly "
218 "installed"));
219
220 parser.addOption(jobsOption);
221 parser.addOption(appDataOption);
222 parser.addOption(checkOption);
223
224 parser.process(arguments());
225 _jobsOption = parser.isSet(jobsOption);
226 _appDataDir = parser.value(appDataOption);
227 _checkOption = parser.isSet(checkOption);
228 }
229
initialize()230 bool CoreApplication::initialize()
231 {
232 parseArgs();
233
234 QSettings settings;
235
236 if(!_appDataDir.isEmpty())
237 {
238 settings.setPath(QSettings::IniFormat, QSettings::UserScope, _appDataDir);
239 settings.setDefaultFormat(QSettings::IniFormat);
240 }
241
242 Translator &translator = Translator::instance();
243 translator.translateApp(this,
244 settings.value("app/language", LANG_AUTO).toString());
245
246 bool wizardDone = settings.value("app/wizard_done", false).toBool();
247 if(!wizardDone)
248 {
249 // Show the first time setup dialog
250 SetupDialog wizard;
251 connect(&wizard, &SetupDialog::getTarsnapVersion, &_taskManager,
252 &TaskManager::getTarsnapVersion);
253 connect(&_taskManager, &TaskManager::tarsnapVersion, &wizard,
254 &SetupDialog::setTarsnapVersion);
255 connect(&wizard, &SetupDialog::requestRegisterMachine, &_taskManager,
256 &TaskManager::registerMachine);
257 connect(&_taskManager, &TaskManager::registerMachineStatus, &wizard,
258 &SetupDialog::registerMachineStatus);
259 connect(&wizard, &SetupDialog::initializeCache, &_taskManager,
260 &TaskManager::initializeCache);
261 connect(&_taskManager, &TaskManager::idle, &wizard,
262 &SetupDialog::updateLoadingAnimation);
263
264 if(QDialog::Rejected == wizard.exec())
265 {
266 quit(); // if we're running in the loop
267 return false; // if called from main
268 }
269 }
270
271 if(settings.value("tarsnap/dry_run", false).toBool())
272 {
273 QMessageBox::warning(nullptr, tr("Tarsnap warning"),
274 tr("Simulation mode is enabled. Archives will not"
275 " be uploaded to the Tarsnap server. Disable"
276 " in Settings -> Backup."));
277 }
278
279 // Initialize the PersistentStore early
280 PersistentStore::instance();
281
282 connect(&_taskManager, &TaskManager::displayNotification, &_notification,
283 &Notification::displayNotification, QUEUED);
284 connect(&_taskManager, &TaskManager::message, &_journal, &Journal::log,
285 QUEUED);
286
287 QMetaObject::invokeMethod(&_journal, "load", QUEUED);
288
289 // Make sure we have the path to the current Tarsnap-GUI binary
290 int correctedPath = correctedSchedulingPath();
291
292 if(_jobsOption || _checkOption)
293 {
294 if(correctedPath == 0)
295 DEBUG << tr(UPDATED_LAUNCHD_PATH_SHORT);
296 else if(correctedPath == 1)
297 DEBUG << tr(UPDATED_LAUNCHD_PATH_ERROR);
298
299 // We don't have anything else to do
300 if(_checkOption)
301 return false;
302 }
303
304 if(_jobsOption)
305 {
306 setQuitLockEnabled(true);
307 connect(&_notification, &Notification::activated, this,
308 &CoreApplication::showMainWindow, QUEUED);
309 connect(&_notification, &Notification::messageClicked, this,
310 &CoreApplication::showMainWindow, QUEUED);
311 QMetaObject::invokeMethod(&_taskManager, "runScheduledJobs", QUEUED);
312 }
313 else
314 {
315 if(correctedPath == 0)
316 QMessageBox::information(nullptr, tr("Updated OS X launchd path"),
317 tr(UPDATED_LAUNCHD_PATH_LONG));
318 else if(correctedPath == 1)
319 QMessageBox::information(nullptr,
320 tr("Failed to updated OS X launchd path"),
321 tr(UPDATED_LAUNCHD_PATH_ERROR));
322
323 showMainWindow();
324 }
325
326 return true;
327 }
328
showMainWindow()329 void CoreApplication::showMainWindow()
330 {
331 if(_mainWindow != nullptr)
332 return;
333
334 setQuitLockEnabled(false);
335 disconnect(&_notification, &Notification::activated, this,
336 &CoreApplication::showMainWindow);
337 disconnect(&_notification, &Notification::messageClicked, this,
338 &CoreApplication::showMainWindow);
339
340 _mainWindow = new MainWindow();
341 Q_ASSERT(_mainWindow != nullptr);
342
343 connect(_mainWindow, &MainWindow::getTarsnapVersion, &_taskManager,
344 &TaskManager::getTarsnapVersion, QUEUED);
345 connect(&_taskManager, &TaskManager::tarsnapVersion, _mainWindow,
346 &MainWindow::updateTarsnapVersion, QUEUED);
347 connect(_mainWindow, &MainWindow::backupNow, &_taskManager,
348 &TaskManager::backupNow, QUEUED);
349 connect(_mainWindow, &MainWindow::getArchives, &_taskManager,
350 &TaskManager::getArchives, QUEUED);
351 connect(&_taskManager, &TaskManager::archiveList, _mainWindow,
352 &MainWindow::archiveList, QUEUED);
353 connect(&_taskManager, &TaskManager::addArchive, _mainWindow,
354 &MainWindow::addArchive, QUEUED);
355 connect(_mainWindow, &MainWindow::deleteArchives, &_taskManager,
356 &TaskManager::deleteArchives, QUEUED);
357 connect(_mainWindow, &MainWindow::loadArchiveStats, &_taskManager,
358 &TaskManager::getArchiveStats, QUEUED);
359 connect(_mainWindow, &MainWindow::loadArchiveContents, &_taskManager,
360 &TaskManager::getArchiveContents, QUEUED);
361 connect(&_taskManager, &TaskManager::idle, _mainWindow,
362 &MainWindow::updateLoadingAnimation, QUEUED);
363 connect(_mainWindow, &MainWindow::getOverallStats, &_taskManager,
364 &TaskManager::getOverallStats, QUEUED);
365 connect(&_taskManager, &TaskManager::overallStats, _mainWindow,
366 &MainWindow::overallStatsChanged, QUEUED);
367 connect(_mainWindow, &MainWindow::repairCache, &_taskManager,
368 &TaskManager::fsck, QUEUED);
369 connect(_mainWindow, &MainWindow::nukeArchives, &_taskManager,
370 &TaskManager::nuke, QUEUED);
371 connect(_mainWindow, &MainWindow::restoreArchive, &_taskManager,
372 &TaskManager::restoreArchive, QUEUED);
373 connect(_mainWindow, &MainWindow::runSetupWizard, this,
374 &CoreApplication::reinit, QUEUED);
375 connect(_mainWindow, &MainWindow::stopTasks, &_taskManager,
376 &TaskManager::stopTasks, QUEUED);
377 connect(&_taskManager, &TaskManager::jobsList, _mainWindow,
378 &MainWindow::jobsList, QUEUED);
379 connect(_mainWindow, &MainWindow::deleteJob, &_taskManager,
380 &TaskManager::deleteJob, QUEUED);
381 connect(_mainWindow, &MainWindow::getTaskInfo, &_taskManager,
382 &TaskManager::getTaskInfo, QUEUED);
383 connect(&_taskManager, &TaskManager::taskInfo, _mainWindow,
384 &MainWindow::displayStopTasksDialog, QUEUED);
385 connect(_mainWindow, &MainWindow::jobAdded, &_taskManager,
386 &TaskManager::addJob, QUEUED);
387 connect(_mainWindow, &MainWindow::getKeyId, &_taskManager,
388 &TaskManager::getKeyId, QUEUED);
389 connect(&_taskManager, &TaskManager::keyId, _mainWindow,
390 &MainWindow::saveKeyId, QUEUED);
391 connect(&_taskManager, &TaskManager::message, _mainWindow,
392 &MainWindow::updateStatusMessage, QUEUED);
393 connect(&_taskManager, &TaskManager::error, _mainWindow,
394 &MainWindow::tarsnapError, QUEUED);
395 connect(&_notification, &Notification::activated, _mainWindow,
396 &MainWindow::notificationRaise, QUEUED);
397 connect(&_notification, &Notification::messageClicked, _mainWindow,
398 &MainWindow::notificationRaise, QUEUED);
399 connect(_mainWindow, &MainWindow::displayNotification, &_notification,
400 &Notification::displayNotification, QUEUED);
401 connect(&_journal, &Journal::journal, _mainWindow, &MainWindow::setJournal,
402 QUEUED);
403 connect(&_journal, &Journal::logEntry, _mainWindow,
404 &MainWindow::appendToJournalLog, QUEUED);
405 connect(_mainWindow, &MainWindow::clearJournal, &_journal, &Journal::purge,
406 QUEUED);
407 connect(_mainWindow, &MainWindow::findMatchingArchives, &_taskManager,
408 &TaskManager::findMatchingArchives, QUEUED);
409 connect(&_taskManager, &TaskManager::matchingArchives, _mainWindow,
410 &MainWindow::matchingArchives, QUEUED);
411
412 QMetaObject::invokeMethod(_mainWindow, "initialize", QUEUED);
413 QMetaObject::invokeMethod(&_taskManager, "loadArchives", QUEUED);
414 QMetaObject::invokeMethod(&_taskManager, "loadJobs", QUEUED);
415 QMetaObject::invokeMethod(&_journal, "getJournal", QUEUED);
416
417 _mainWindow->show();
418 }
419
reinit()420 bool CoreApplication::reinit()
421 {
422 disconnect(&_taskManager, &TaskManager::displayNotification, &_notification,
423 &Notification::displayNotification);
424 disconnect(&_taskManager, &TaskManager::message, &_journal, &Journal::log);
425
426 if(_mainWindow)
427 {
428 delete _mainWindow;
429 _mainWindow = nullptr;
430 }
431
432 // reset existing persistent store and app settings
433 PersistentStore &store = PersistentStore::instance();
434 store.purge();
435
436 QSettings settings;
437 settings.setDefaultFormat(QSettings::NativeFormat);
438 QSettings defaultSettings;
439 if(defaultSettings.contains("app/wizard_done"))
440 {
441 defaultSettings.clear();
442 defaultSettings.sync();
443 }
444
445 return initialize();
446 }
447