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