main.cpp
1 //------------------------------------------------------------------------------
2 // main.cpp
3 //------------------------------------------------------------------------------
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 // 02110-1301 USA
19 //
20 //------------------------------------------------------------------------------
21 // Copyright (C) 2009 Braden "Blzut3" Obrzut <admin@maniacsvault.net>
22 //------------------------------------------------------------------------------
23 
24 #include <QApplication>
25 #include <QDir>
26 #include <QFile>
27 #include <QHashIterator>
28 #include <QLabel>
29 #include <QMainWindow>
30 #include <QMessageBox>
31 #include <QObject>
32 #include <QSslConfiguration>
33 #include <QThreadPool>
34 #include <QTimer>
35 
36 #include <cstdio>
37 
38 #include "application.h"
39 #include "cmdargshelp.h"
40 #include "commandlinetokenizer.h"
41 #include "configuration/doomseekerconfig.h"
42 #include "configuration/passwordscfg.h"
43 #include "configuration/queryspeed.h"
44 #include "connectionhandler.h"
45 #include "datapaths.h"
46 #include "doomseekerfilepaths.h"
47 #include "gui/createserverdialog.h"
48 #include "gui/mainwindow.h"
49 #include "gui/remoteconsole.h"
50 #include "ini/ini.h"
51 #include "ip2c/ip2c.h"
52 #include "irc/configuration/ircconfig.h"
53 #include "localization.h"
54 #include "log.h"
55 #include "main.h"
56 #include "plugins/engineplugin.h"
57 #include "plugins/pluginloader.h"
58 #include "refresher/refresher.h"
59 #include "serverapi/server.h"
60 #include "serverapi/server.h"
61 #include "strings.hpp"
62 #include "tests/testruns.h"
63 #include "updater/updateinstaller.h"
64 #include "versiondump.h"
65 #include "wadseeker/wadseeker.h"
66 
67 #ifdef Q_OS_OPENBSD
68 #include <unistd.h>
69 #endif
70 
71 QString Main::argDataDir;
73 
74 
75 Main::Main(int argc, char *argv[])
76  : arguments(argv), argumentsCount(argc),
77  startCreateGame(false), startRcon(false)
78 {
79  #ifdef Q_OS_OPENBSD
80  pledge ("stdio rpath wpath cpath tmppath inet mcast fattr chown flock unix "
81  "dns getpw sendfd recvfd tty proc exec prot_exec ps audio video unveil",
82  "");
83  #endif
84  bIsFirstRun = false;
85  bTestMode = false;
86  bPortableMode = false;
87  bVersionDump = false;
88  logVerbosity = LV_Default;
89  updateFailedCode = 0;
90 
91  qRegisterMetaType<ServerPtr>("ServerPtr");
92  qRegisterMetaType<ServerCPtr>("ServerCPtr");
93 }
94 
95 Main::~Main()
96 {
97  if (Application::isInit())
98  {
99  gApp->stopRunning();
100  }
101 
102  if (Refresher::isInstantiated())
103  {
104  Refresher::instance()->quit();
105  Refresher::deinstantiate();
106  }
107 
108  // We can't save a config if we haven't initalized the program!
109  if (Application::isInit())
110  {
111  gConfig.saveToFile();
112  gConfig.dispose();
113 
114  gIRCConfig.saveToFile();
115  gIRCConfig.dispose();
116  }
117 
118  IP2C::deinstantiate();
119 
122 }
123 
124 int Main::connectToServerByURL()
125 {
126  ConnectionHandler *handler = ConnectionHandler::connectByUrl(connectUrl);
127 
128  if (handler)
129  {
130  connect(handler, SIGNAL(finished(int)), gApp, SLOT(quit()));
131  handler->run();
132  int ret = gApp->exec();
133  delete handler;
134  return ret;
135  }
136  return 0;
137 }
138 
139 // This method is an exception to sorting everything in alphabetical order
140 // because it's... the main method.
142 {
143  if (!interpretCommandLineParameters())
144  {
145  return 0;
146  }
147  applyLogVerbosity();
148 
149  Application::init(argumentsCount, arguments);
150  #ifdef Q_OS_DARWIN
151  // In Mac OS X it is abnormal to have menu icons unless it's a shortcut to a file of some kind.
152  gApp->setAttribute(Qt::AA_DontShowIconsInMenus);
153  #endif
154 
155  gLog << "Starting Doomseeker. Hello World! :)";
156  gLog << "Setting up data directories.";
157 
158  if (!initDataDirectories())
159  return 0;
160 
161  initCaCerts();
162 
163  PluginLoader::init(gDefaultDataPaths->pluginSearchLocationPaths());
164  if (bVersionDump)
165  {
166  return runVersionDump();
167  }
168  PluginUrlHandler::registerAll();
169 
170  if (bTestMode)
171  {
172  return runTestMode();
173  }
174 
175  initMainConfig();
176  #ifdef WITH_AUTOUPDATES
177  // Handle pending update installations.
178  UpdateInstaller::ErrorCode updateInstallerResult
179  = (UpdateInstaller::ErrorCode)installPendingUpdates();
180  if (updateInstallerResult == UpdateInstaller::EC_Ok)
181  {
182  return 0;
183  }
184  #endif
185 
186  initLocalizationsDefinitions();
187  initIP2C();
188  initPasswordsConfig();
189  initPluginConfig();
190  initIRCConfig();
191 
192  if (startCreateGame)
193  {
194  QTimer::singleShot(0, this, SLOT(runCreateGame()));
195  }
196  else if (startRcon)
197  {
198  QTimer::singleShot(0, this, SLOT(runRemoteConsole()));
199  }
200  else if (connectUrl.isValid())
201  {
202  setupRefreshingThread();
203  return connectToServerByURL();
204  }
205  else
206  {
207  setupRefreshingThread();
208  createMainWindow();
209  #ifdef WITH_AUTOUPDATES
210  // Handle auto update: display update failure or start auto update
211  // check/download.
212  if (updateFailedCode != 0)
213  {
214  // This is when updater program failed to install the update.
215  gApp->mainWindow()->setDisplayUpdaterProcessFailure(updateFailedCode);
216  }
217  else if (updateInstallerResult != UpdateInstaller::EC_NothingToUpdate)
218  {
219  // This is when Doomseeker failed to start the updater program.
220  gApp->mainWindow()->setDisplayUpdateInstallerError(updateInstallerResult);
221  }
222  else
223  {
224  if (gConfig.autoUpdates.updateMode != DoomseekerConfig::AutoUpdates::UM_Disabled)
225  {
226  QTimer::singleShot(0, gApp->mainWindow(), SLOT(checkForUpdatesAuto()));
227  }
228  }
229  #endif
230  }
231 
232  gLog << tr("Init finished.");
233  gLog.addUnformattedEntry("================================\n");
234 
235  int returnCode = gApp->exec();
236 
237  #ifdef WITH_AUTOUPDATES
239  {
240  // Code must be reset because the install method
241  // doesn't do the actual installation if it's not equal to zero.
242  updateFailedCode = 0;
243  int installResult = installPendingUpdates();
244  if (installResult != UpdateInstaller::EC_Ok
245  && installResult != UpdateInstaller::EC_NothingToUpdate)
246  {
247  QMessageBox::critical(nullptr, tr("Doomseeker - Updates Install Failure"),
248  UpdateInstaller::errorCodeToStr((UpdateInstaller::ErrorCode)installResult));
249  }
250  }
251  #endif
252 
253  return returnCode;
254 }
255 
256 int Main::runTestMode()
257 {
258  // Setup
259  gLog << "Entering test mode.";
260  gLog << "";
261  TestCore testCore;
262 
263  // Call tests here.
264  TestRuns::pTestCore = &testCore;
265  TestRuns::callTests();
266 
267  // Summary
268  QString strSucceded = "Tests succeeded: %1";
269  QString strFailed = "Tests failed: %1";
270  QString strPercentage = "Pass percentage: %1%";
271 
272  float passPercentage = (float)testCore.numTestsSucceeded() / (float)testCore.numTests();
273  passPercentage *= 100.0f;
274 
275  gLog << "==== TESTS SUMMARY: ====";
276  gLog << strSucceded.arg(testCore.numTestsSucceeded(), 6);
277  gLog << strFailed.arg(testCore.numTestsFailed(), 6);
278  gLog << strPercentage.arg(passPercentage, 6, 'f', 2);
279  gLog << "==== Done. ====";
280 
281  return testCore.numTestsFailed();
282 }
283 
284 int Main::runVersionDump()
285 {
286  QFile outfile;
287  QString error;
288  if (!versionDumpFile.isEmpty())
289  {
290  outfile.setFileName(versionDumpFile);
291  if (!outfile.open(QIODevice::WriteOnly | QIODevice::Text))
292  {
293  error = tr("Failed to open file '%1'.").arg(versionDumpFile);
294  }
295  }
296  else
297  {
298  // Use stdout instead.
299  if (!outfile.open(stdout, QIODevice::WriteOnly))
300  {
301  error = tr("Failed to open stdout.");
302  }
303  }
304  if (!error.isEmpty())
305  {
306  gLog.setPrintingToStderr(true);
307  gLog << error;
308  return 2;
309  }
310 
311  gLog << tr("Dumping version info to file in JSON format.");
312  VersionDump::dumpJsonToIO(outfile);
313  return 0;
314 }
315 
316 void Main::applyLogVerbosity()
317 {
318  gLog.setPrintingToStderr(shouldLogToStderr());
319 }
320 
321 void Main::createMainWindow()
322 {
323  gLog << tr("Preparing GUI.");
324 
325  gApp->setMainWindow(new MainWindow(gApp));
326  gApp->mainWindow()->show();
327 
328  if (bIsFirstRun)
329  {
330  gApp->mainWindow()->notifyFirstRun();
331  }
332 }
333 
334 void Main::runCreateGame()
335 {
336  gLog << tr("Starting Create Game box.");
337  auto dialog = new CreateServerDialog(GameCreateParams::Host, nullptr);
338  dialog->setWindowIcon(Application::icon());
339  dialog->show();
340 }
341 
342 void Main::runRemoteConsole()
343 {
344  gLog << tr("Starting RCon client.");
345  if (rconPluginName.isEmpty())
346  {
347  bool canAnyEngineRcon = false;
348  for (unsigned int i = 0; i < gPlugins->numPlugins(); i++)
349  {
350  const EnginePlugin *info = gPlugins->plugin(i)->info();
351  if (info->server(QHostAddress("localhost"), 0)->hasRcon())
352  {
353  canAnyEngineRcon = true;
354  break;
355  }
356  }
357  if (!canAnyEngineRcon)
358  {
359  QString error = tr("None of the currently loaded game plugins supports RCon.");
360  gLog << error;
361  QMessageBox::critical(nullptr, tr("Doomseeker RCon"), error);
362  gApp->exit(2);
363  return;
364  }
365 
366  auto rc = new RemoteConsole();
367  rc->show();
368  }
369  else
370  {
371  // Find plugin
372  int pIndex = gPlugins->pluginIndexFromName(rconPluginName);
373  if (pIndex == -1)
374  {
375  gLog << tr("Couldn't find specified plugin: ") + rconPluginName;
376  gApp->exit(2);
377  return;
378  }
379 
380  // Check for RCon Availability.
381  const EnginePlugin *plugin = gPlugins->plugin(pIndex)->info();
382  ServerPtr server = plugin->server(QHostAddress(rconAddress), rconPort);
383  if (!server->hasRcon())
384  {
385  gLog << tr("Plugin does not support RCon.");
386  gApp->exit(2);
387  return;
388  }
389 
390  // Start it!
391  RemoteConsole *rc = new RemoteConsole(server);
392  rc->show();
393  }
394 }
395 
396 void Main::initCaCerts()
397 {
398  QString certsFilePath = DoomseekerFilePaths::cacerts();
399  QFile certsFile(certsFilePath);
400  if (!certsFilePath.isEmpty() && certsFile.exists())
401  {
402  gLog << tr("Loading extra CA certificates from '%1'.").arg(certsFilePath);
403  certsFile.open(QIODevice::ReadOnly);
404  QSslConfiguration sslConf = QSslConfiguration::defaultConfiguration();
405  QList<QSslCertificate> cacerts = sslConf.caCertificates();
406  QList<QSslCertificate> extraCerts = QSslCertificate::fromDevice(&certsFile);
407  gLog << tr("Appending %n extra CA certificate(s).", nullptr, extraCerts.size());
408  cacerts.append(extraCerts);
409  sslConf.setCaCertificates(cacerts);
410  QSslConfiguration::setDefaultConfiguration(sslConf);
411  certsFile.close();
412  }
413 }
414 
415 bool Main::initDataDirectories()
416 {
417  DataPaths::initDefault(bPortableMode);
418  DoomseekerFilePaths::pDataPaths = gDefaultDataPaths;
419  QList<DataPaths::DirErrno> failedDirsErrno = gDefaultDataPaths->createDirectories();
420  if (!failedDirsErrno.isEmpty())
421  {
422  // Inform the user which directories failed and QUIT.
423  // We give an accurate error message of what is going wrong, thanks to errno.
424  QString errorMessage = tr("Doomseeker will not run because some directories cannot be used properly.\n");
425  for (const DataPaths::DirErrno &failedDirErrno : failedDirsErrno)
426  {
427  errorMessage += "\n[" + QString::number(failedDirErrno.errnoNum) + "] ";
428  errorMessage += failedDirErrno.directory.absolutePath() + ": ";
429  errorMessage += failedDirErrno.errnoString;
430  }
431  // Prompt the errorMessage and exit.
432  QMessageBox::critical(nullptr, tr("Doomseeker startup error"), errorMessage);
433  return false;
434  }
435 
436  // I think this directory should take priority, if user, for example,
437  // wants to update the ip2country file.
438  dataDirectories << gDefaultDataPaths->localDataLocationPath();
439  dataDirectories << gDefaultDataPaths->workingDirectory();
440 
441  // Continue with standard dirs:
442  dataDirectories << "./";
443  #if defined(Q_OS_LINUX)
444  // check in /usr/local/share/doomseeker/ on Linux
445  dataDirectories << INSTALL_PREFIX "/share/doomseeker/";
446  #endif
447 
448  dataDirectories << ":/";
449  QDir::setSearchPaths("data", dataDirectories);
450 
451  return true;
452 }
453 
454 int Main::initIP2C()
455 {
456  gLog << tr("Initializing IP2C database.");
457  IP2C::instance();
458 
459  return 0;
460 }
461 
462 void Main::initIRCConfig()
463 {
464  gLog << tr("Initializing IRC configuration file.");
465 
466  // This macro initializes the Singleton.
467  gIRCConfig;
468 
469  // Now try to access the configuration stored on drive.
470  QString configPath = DoomseekerFilePaths::ircIni();
471  if (!configPath.isEmpty())
472  {
473  if (gIRCConfig.setIniFile(configPath))
474  {
475  gIRCConfig.readFromFile();
476  }
477  }
478 }
479 
480 void Main::initLocalizationsDefinitions()
481 {
482  gLog << tr("Loading translations definitions");
483  Localization::get()->loadLocalizationsList(
484  DataPaths::staticDataSearchDirs(DataPaths::TRANSLATIONS_DIR_NAME));
485 
486  LocalizationInfo bestMatchedLocalization = Localization::get()->coerceBestMatchingLocalization(
487  gConfig.doomseeker.localization);
488  if (bestMatchedLocalization.isValid() && bestMatchedLocalization != LocalizationInfo::PROGRAM_NATIVE)
489  {
490  gLog << tr("Loading translation \"%1\".").arg(bestMatchedLocalization.localeName);
491  bool bSuccess = Localization::get()->loadTranslation(bestMatchedLocalization.localeName);
492  if (bSuccess)
493  {
494  gLog << tr("Translation loaded.");
495  }
496  else
497  {
498  gLog << tr("Failed to load translation.");
499  }
500  }
501 }
502 
503 void Main::initMainConfig()
504 {
505  gLog << tr("Initializing configuration file.");
506 
507  // This macro initializes the Singleton.
508  gConfig;
509 
510  // Now try to access the configuration stored on drive.
511  QString configDirPath = gDefaultDataPaths->programsDataDirectoryPath();
512  if (configDirPath.isEmpty())
513  {
514  gLog << tr("Could not get an access to the settings directory. Configuration will not be saved.");
515  return;
516  }
517 
518  QString filePath = DoomseekerFilePaths::ini();
519 
520  // Check for first run.
521  QFileInfo iniFileInfo(filePath);
522  bIsFirstRun = !iniFileInfo.exists();
523 
524  // Init the config.
525  if (gConfig.setIniFile(filePath))
526  {
527  gConfig.readFromFile();
528  }
529 }
530 
531 void Main::initPasswordsConfig()
532 {
533  gLog << tr("Initializing passwords configuration file.");
534  // Now try to access the configuration stored on drive.
535  QString configDirPath = gDefaultDataPaths->programsDataDirectoryPath();
536  if (configDirPath.isEmpty())
537  {
538  return;
539  }
540  QString filePath = DoomseekerFilePaths::passwordIni();
541  PasswordsCfg::initIni(filePath);
542 }
543 
544 void Main::initPluginConfig()
545 {
546  gLog << tr("Initializing configuration for plugins.");
547  gPlugins->initConfig();
548 }
549 
550 int Main::installPendingUpdates()
551 {
553  if (gConfig.autoUpdates.bPerformUpdateOnNextRun)
554  {
555  gConfig.autoUpdates.bPerformUpdateOnNextRun = false;
556  gConfig.saveToFile();
557  // Update should only be attempted if program was not called
558  // with "--update-failed" arg (previous update didn't fail).
559  if (updateFailedCode == 0)
560  {
561  UpdateInstaller updateInstaller;
562  updateInstallerResult = updateInstaller.startInstallation();
563  }
564  }
565  return updateInstallerResult;
566 }
567 
568 bool Main::interpretCommandLineParameters()
569 {
570  QString failure;
571  //first argument is the command to run the program, example: ./doomseeker. better use 1 instead of 0
572  for (int i = 1; i < argumentsCount && failure.isEmpty(); ++i)
573  {
574  const char *arg = arguments[i];
575 
576  if (strcmp(arg, "--connect") == 0)
577  {
578  if (i + 1 < argumentsCount)
579  {
580  ++i;
581  connectUrl = QUrl(arguments[i]);
582  }
583  else
584  {
585  //basically prevent the program from running if there are no arguments given.
586  failure = CmdArgsHelp::missingArgs(1, arg);
587  }
588  }
589  else if (strcmp(arg, "--create-game") == 0)
590  {
591  startCreateGame = true;
592  }
593  else if (strcmp(arg, "--datadir") == 0)
594  {
595  if (i + 1 < argumentsCount)
596  {
597  ++i;
598  dataDirectories.prepend(arguments[i]);
599  argDataDir = arguments[i];
600  }
601  else
602  {
603  failure = CmdArgsHelp::missingArgs(1, arg);
604  }
605  }
606  else if (strcmp(arg, "--rcon") == 0)
607  {
608  startRcon = true;
609  if (i + 2 < argumentsCount)
610  {
611  rconPluginName = arguments[++i];
612  Strings::translateServerAddress(arguments[++i], rconAddress, rconPort, "localhost:10666");
613  }
614  }
615  else if (strcmp(arg, "--help") == 0)
616  {
617  gLog.setTimestampsEnabled(false);
618  // Print information to the log and terminate.
620  return false;
621  }
622  else if (strcmp(arg, "--update-failed") == 0)
623  {
624  ++i;
625  updateFailedCode = QString(arguments[i]).toInt();
626  }
627  else if (strcmp(arg, "--portable") == 0)
628  {
629  bPortableMode = true;
630  }
631  else if (strcmp(arg, "--quiet") == 0)
632  {
633  logVerbosity = LV_Quiet;
634  }
635  else if (strcmp(arg, "--tests") == 0)
636  {
637  bTestMode = true;
638  }
639  else if (strcmp(arg, "--verbose") == 0)
640  {
641  logVerbosity = LV_Verbose;
642  }
643  else if (strcmp(arg, "--version-json") == 0)
644  {
645  bVersionDump = true;
646  if (i + 1 < argumentsCount)
647  {
648  ++i;
649  QString filename = arguments[i];
650  if (filename != "-" && filename != "")
651  {
652  versionDumpFile = filename;
653  }
654  }
655  }
656  else
657  {
658  failure = CmdArgsHelp::unrecognizedOption(arg);
659  }
660  }
661 
662  QList<bool> exclusives;
663  exclusives << !connectUrl.isEmpty() << startCreateGame << startRcon;
664  if (exclusives.count(true) > 1)
665  failure = tr("doomseeker: `--connect`, `--create-game` and `--rcon` are mutually exclusive");
666 
667  if (!failure.isEmpty())
668  {
669  gLog.setTimestampsEnabled(false);
670  gLog << failure;
671  return false;
672  }
673  return true;
674 }
675 
676 void Main::setupRefreshingThread()
677 {
678  gLog << tr("Starting refreshing thread.");
679  gRefresher->setDelayBetweenResends(gConfig.doomseeker.querySpeed().delayBetweenSingleServerAttempts);
680  gRefresher->start();
681 }
682 
683 bool Main::shouldLogToStderr() const
684 {
685  if (bTestMode)
686  return logVerbosity != LV_Quiet;
687  if (bVersionDump)
688  return logVerbosity == LV_Verbose;
689  return logVerbosity != LV_Quiet;
690 }
691 
692 //==============================================================================
693 
694 #ifdef _MSC_VER
695  #ifdef NDEBUG
696 #define USE_WINMAIN_AS_ENTRY_POINT
697  #endif
698 #endif
699 
700 #ifdef USE_WINMAIN_AS_ENTRY_POINT
701 #include <windows.h>
702 QStringList getCommandLineArgs()
703 {
704  CommandLineTokenizer tokenizer;
705  return tokenizer.tokenize(QString::fromUtf16((const ushort *)GetCommandLineW()));
706 }
707 
708 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLine, int nCmdShow)
709 {
710  int argc = 0;
711  char **argv = nullptr;
712 
713  QStringList commandLine = getCommandLineArgs();
714 
715  // At least one is ensured to be here.
716  argc = commandLine.size();
717  argv = new char *[argc];
718 
719  for (int i = 0; i < commandLine.size(); ++i)
720  {
721  const QString &parameter = commandLine[i];
722  argv[i] = new char[parameter.toUtf8().size() + 1];
723  strcpy(argv[i], parameter.toUtf8().constData());
724  }
725 
726  Main *pMain = new Main(argc, argv);
727  int returnValue = pMain->run();
728 
729  // Cleans up after the program.
730  delete pMain;
731 
732  // On the other hand we could just ignore the fact that this array is left
733  // hanging in the memory because Windows will clean it up for us...
734  for (int i = 0; i < argc; ++i)
735  {
736  delete [] argv[i];
737  }
738  delete [] argv;
739 
740  return returnValue;
741 }
742 #else
743 int main(int argc, char *argv[])
744 {
745  Main *pMain = new Main(argc, argv);
746  int returnValue = pMain->run();
747 
748  // Cleans up after the program.
749  delete pMain;
750 
751  return returnValue;
752 }
753 #endif