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