/* * Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "Context.h" #include "Athlete.h" #include "MainWindow.h" #include "Settings.h" #include "CloudService.h" #include "TrainDB.h" #include "Colors.h" #include "GcUpgrade.h" #include "IdleTimer.h" #include "PowerProfile.h" #include "GcCrashDialog.h" // for versionHTML #include "OverviewItems.h" #include #include #include #include #include "ChooseCyclistDialog.h" #ifdef GC_WANT_HTTP #include "httplistener.h" #include "httprequesthandler.h" #endif #ifdef GC_HAS_CLOUD_DB #include "CloudDBCommon.h" #endif #ifdef GC_WANT_PYTHON #include "PythonEmbed.h" #include "FixPySettings.h" #endif #include #ifdef GC_WANT_X11 #include #endif #include #include #ifdef Q_OS_WIN #if QT_VERSION > 0x060500 #include #endif #endif // // bootstrap state // bool restarting = false; static bool nogui; static int gc_opened=0; // // global application // QString gcroot; QApplication *application; #ifdef GC_WANT_HTTP #include "APIWebService.h" HttpListener *listener = NULL; #endif // R is not multithreaded, has a single instance that we setup at startup. #ifdef GC_WANT_R #include // All R Runtime elements encapsulated in RTool RTool *rtool = NULL; #endif // // Trap signals / termination // void terminate(int code) { #ifdef GC_WANT_HTTP if (listener) listener->close(); #endif // tidy up static stuff (our globals) that are not tied // to a mainwindow instance (which will be deleted on close) #ifdef GC_WANT_PYTHON delete fixPySettings; #endif delete appsettings; application->exit(); // because QT starts a bunch of threads (e.g. reading XcbEvents) // calling exit() during startup is a no-no. So we go nuclear and // exit without calling the static destructors via _Exit(), unless we did // actually open an athlete and start-up proper with app->exec(). if (gc_opened) exit(code); else _Exit(code); } // // redirect logging // #ifdef GC_WANT_HTTP void myMessageOutput(QtMsgType type, const QMessageLogContext &, const QString &string) { QByteArray ba = string.toLocal8Bit(); const char *msg = ba.constData(); //in this function, you can write the message to any stream! switch (type) { default: // QtInfoMsg from 5.5 would arrive here case QtDebugMsg: fprintf(stderr, "Debug: %s\n", msg); break; case QtWarningMsg: // supress warnings unless server mode if (nogui) fprintf(stderr, "Warning: %s\n", msg); break; case QtCriticalMsg: fprintf(stderr, "Critical: %s\n", msg); break; case QtFatalMsg: fprintf(stderr, "Fatal: %s\n", msg); abort(); } } void sigabort(int x) { terminate(x); } #endif // // redirect stderr to `home'/goldencheetah.log or to the file specified with --debug-file // #include #include #include #ifdef WIN32 #include #include #else #include #endif void nostderr(QString file) { int fd; int fd_stderr = 2; FILE *fp; // On Windows, stderr is not connected to fd_stderr = 2 // freopen seems the only function available to redirect stderr qDebug() << "GoldenCheetah: redirecting log messages (stderr) to file " << file; fp = freopen(file.toLocal8Bit(), "w", stderr); if (fp == NULL) { qDebug() << "GoldenCheetah: cannot redirect stderr, unable to open file " << file; return; } fd = fileno(stderr); if (fd < 0) { qDebug() << "GoldenCheetah: invalid handle obtained from stderr " << fd; return; } // We redirect fd_stderr to this new file with dup2(), in case some libraries write fd_stderr. // Normally we would close fd right after it is duplicated, but not in this case. // Indeed stderr still uses fd. Both fd and fd_stderr may be used interchangeably after dup2(). int ret = dup2(fd, fd_stderr); if (ret < 0) { qDebug() << "Goldencheetah: cannot redirect STDERR_FILENO, dup2 failed with return value " << ret; return; } // Synchronize cerr with the new stderr std::ios::sync_with_stdio(); #ifdef WIN32 // Redirect STD_ERROR_HANDLE to the new file HANDLE fileHandle = (HANDLE)_get_osfhandle(fd_stderr); if(fileHandle == INVALID_HANDLE_VALUE) qDebug() << "GoldenCheetah: cannot get Win32 HANDLE for stderr"; bool res = SetStdHandle(STD_ERROR_HANDLE, fileHandle); if (!res) qDebug() << "GoldenCheetah: cannot redirect STD_ERROR_HANDLE"; #endif } // // By default will open last athlete, but will also provide // a dialog to select an athlete if not found, and then upgrade // the athlete one selected before opening a mainwindow // // It will also respawn mainwindows when restarting for changes // to application settings (athlete folder, language) // // Also creates singleton instances prior to application launching // int main(int argc, char *argv[]) { int ret=2; // return code from qapplication, default to error #ifdef Q_OS_WIN // On Windows without console, we try to attach to the parent's console // and redirect stderr and stdout on success, to have a more Unix-like // behavior when launched from cmd or PowerShell. if (_fileno(stderr) == -2 && AttachConsole(ATTACH_PARENT_PROCESS )) { freopen("CONOUT$", "w", stderr); freopen("CONOUT$", "w", stdout); } #if QT_VERSION < 0x060000 bool angle=true; #endif #endif // // PROCESS COMMAND LINE SWITCHES // // snaffle arguments into a stringlist we can play with into sargs // and only keep non-switch args in the args string list QStringList sargs, args; for (int i=0; i", ": "); // to maintain colums in one line QString text = QTextDocumentFragment::fromHtml(html).toPlainText(); QByteArray ba = text.toLocal8Bit(); const char *c_str = ba.data(); fprintf(stderr, "\n%s\n\n", c_str); } else if (arg == "--server") { #ifdef GC_WANT_HTTP nogui = server = true; #else fprintf(stderr, "HTTP support not compiled in, exiting.\n"); exit(1); #endif #ifdef GC_WANT_PYTHON } else if (arg == "--no-python") { noPy = true; #endif #ifdef GC_WANT_R } else if (arg == "--no-r") { noR = true; #endif } else if (arg == "--debug") { #ifdef GC_DEBUG // debug, so don't redirect stderr! debug = false; #else debug = true; #endif } else if (arg == "--debug-file" && i < sargs.length()) { debugFile = QString(sargs[i]); i++; } else if (arg == "--debug-format" && i < sargs.length()) { debugFormat = QString(sargs[i]); i++; } else if (arg == "--debug-rules" && i < sargs.length()) { debugRules = QString(sargs[i]); i++; } else if (arg == "--clouddbcurator") { #ifdef GC_HAS_CLOUD_DB CloudDBCommon::addCuratorFeatures = true; #else fprintf(stderr, "CloudDB support not compiled in, exiting.\n"); exit(1); #endif #ifdef Q_OS_WIN #if QT_VERSION < 0x060000 } else if (arg == "--no-angle") { angle = false; #endif #endif } else { // not switches ! args << arg; } } #if 0 // quick hack to get list of metrics and descriptions RideMetricFactory::instance().initialize(); const RideMetricFactory &factory = RideMetricFactory::instance(); QHashIterator it(factory.metricHash()); it.toFront(); while(it.hasNext()) { it.next(); fprintf(stderr, "%s|%s\n", it.value()->name().toUtf8().data(), it.value()->description().toUtf8().data()); } exit(0); #endif // help or version printed so just exit now if (help) { exit(0); } // // INITIALISE ONE TIME OBJECTS // #ifdef GC_WANT_HTTP listener = NULL; #endif #ifdef GC_WANT_X11 XInitThreads(); #endif #ifdef GC_WANT_R rtool = NULL; #endif #ifdef GC_WANT_PYTHON python = NULL; #endif // numerous bugs related to autoscaling and opengl that have persisted since Qt5.6 on and off // now we support hidpi natively we will unset scaling factors and use our own scaling ratios #ifdef Q_OS_LINUX unsetenv("QT_SCALE_FACTOR"); #endif // we don't want program aborts when maths routines don't know // what to do. We may add our own error handler later. gsl_set_error_handler_off(); #ifdef Q_OS_WIN #if QT_VERSION < 0x060000 if (angle) { // windows we use ANGLE for opengl on top of DirectX/Direct3D // it avoids issues with bad graphics drivers QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); } #endif #endif // create the application -- only ever ONE regardless of restarts application = new QApplication(argc, argv); #ifdef Q_OS_WIN #if QT_VERSION > 0x060500 if (application->style()->name() == "windows11") { application->setStyle(QStyleFactory::create("Windows")); qDebug()<<"Replacing windows11 by Windows style to avoid a Qt bug"; } #endif #endif //XXXIdleEventFilter idleFilter; //XXXapplication->installEventFilter(&idleFilter); // read defaults initPowerProfile(); // output colors as configured so we can cut and paste into Colors.cpp // uncomment when developers working on theme colors //GCColor::dumpColors(); // get defaults in case need them later AppearanceSettings defaults = GSettings::defaultAppearanceSettings(); // set font family and size QFont font; font.fromString(appsettings->value(NULL, GC_FONT_DEFAULT, defaults.fontfamily).toString()); font.setPointSize(defaults.fontpointsize); // use the default before taking into account screen size baseFont = font; // hidpi ratios dpiXFactor = defaults.xfactor; dpiYFactor = defaults.yfactor; #ifndef Q_OS_MAC // not needed on a Mac application->setStyleSheet(QString("QPushButton { padding-left: %1px; padding-right: %1px; " " padding-top: %2px; padding-bottom: %2px; }" "QComboBox { padding-left: %1px; padding-right: %1px; }") .arg(15*dpiXFactor) .arg(3*dpiYFactor)); #endif // scale up to user scale factor double fontscale = appsettings->value(NULL, GC_FONT_SCALE, defaults.fontscale).toDouble(); font.setPointSizeF(baseFont.pointSizeF() * fontscale); // now apply ! application->setFont(font); // set default font // set application wide appsettings->setValue(GC_FONT_DEFAULT, font.toString()); appsettings->setValue(GC_FONT_CHARTLABELS, font.toString()); appsettings->setValue(GC_FONT_DEFAULT_SIZE, font.pointSizeF()); appsettings->setValue(GC_FONT_CHARTLABELS_SIZE, font.pointSizeF() * 0.8); // what filestores are registered (whilst we refactor) //qDebug()<<"Cloud services registered:"<value(NULL, GC_EMBED_R, true).toBool()) { rtool = new RTool(); if (rtool->failed == true) rtool=NULL; } #endif #ifdef GC_WANT_PYTHON bool embed = appsettings->value(NULL, GC_EMBED_PYTHON, true).toBool(); if (embed && noPy == false && python == NULL) { python = new PythonEmbed(); // initialise python in this thread ? if (python->loaded == false) python=NULL; } #endif //this is the path within the current directory where GC will look for //files to allow USB stick support QString localLibraryPath="Library/GoldenCheetah"; //this is the path that used to be used for all platforms //now different platforms will use their own path //this path is checked first to make things easier for long-time users QString oldLibraryPath=QDir::home().canonicalPath()+"/Library/GoldenCheetah"; //these are the new platform-dependent library paths #if defined(Q_OS_MACX) QString libraryPath="Library/GoldenCheetah"; #elif defined(Q_OS_WIN) QStringList paths=QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation); QString libraryPath = paths.at(0); #else // not windows or osx (must be Linux or OpenBSD) // Q_OS_LINUX et al QString libraryPath=".goldencheetah"; #endif // // or did we override in settings? QString sh; if ((sh=appsettings->value(NULL, GC_HOMEDIR, "").toString()) != QString("")) localLibraryPath = sh; // lets try the local library we've worked out... QDir home = QDir(); if(QDir(localLibraryPath).exists() || home.exists(localLibraryPath)) { home.cd(localLibraryPath); } else { // YIKES !! The directory we should be using doesn't exist! home = QDir::home(); if (home.exists(oldLibraryPath)) { // there is an old style path, lets fo there home.cd(oldLibraryPath); } else { if (!home.exists(libraryPath)) { if (!home.mkpath(libraryPath)) { // tell user why we aborted ! QMessageBox::critical(NULL, "Exiting", QString("Cannot create library directory (%1)").arg(libraryPath)); exit(0); } } home.cd(libraryPath); } } // set global root directory gcroot = home.canonicalPath(); appsettings->initializeQSettingsGlobal(gcroot); // now redirect stderr and set the log filter and format if (debugFile != QString()) nostderr(debugFile); else if (!debug) nostderr(QString("%1/%2").arg(home.canonicalPath()).arg("goldencheetah.log")); qSetMessagePattern(debugFormat); QLoggingCategory::setFilterRules(debugRules.replace(";", "\n")); // accept ; as separator like QT_LOGGING_RULES // install QT Translator to enable QT Dialogs translation // we may have restarted JUST to get this! QTranslator qtTranslator; qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); application->installTranslator(&qtTranslator); // Language setting (default to system locale) QVariant lang = appsettings->value(NULL, GC_LANG, QLocale::system().name()); // Load specific translation, try from GCROOT otherwise from binary QTranslator gcTranslator; QString translation_file = "/gc_" + lang.toString() + ".qm"; if (gcTranslator.load(gcroot + translation_file)) qDebug() << "Loaded translation from"+gcroot+translation_file; else gcTranslator.load(":translations" + translation_file); application->installTranslator(&gcTranslator); // Now the translator is installed, set default colors with translated names GCColor::setupColors(); // has a default theme been applied (first run) ? QString powercolor = appsettings->value(NULL, "COLORPOWER", "").toString(); if (powercolor == "") GCColor::applyTheme(defaults.theme); // migration appsettings->migrateQSettingsSystem(); // colors must be setup before migration can take place, but reading has to be from the migrated ones GCColor::readConfig(); // Initialize metrics once the translator is installed RideMetricFactory::instance().initialize(); // Initialize global registry once the translator is installed GcWindowRegistry::initialize(); // initialize Overview Items once the translator is installed OverviewItemConfig::registerItems(); // initialise the trainDB trainDB = new TrainDB(home); // lets do what the command line says ... QVariant lastOpened; if(args.count() == 2) { // $ ./GoldenCheetah Mark -or- ./GoldenCheetah --server ~/athletedir // athlete if (!server) lastOpened = args.at(1); else home.cd(args.at(1)); } else if (args.count() == 3) { // $ ./GoldenCheetah ~/Athletes Mark // first parameter is a folder that exists? if (QFileInfo(args.at(1)).isDir()) { home.cd(args.at(1)); } // folder and athlete lastOpened = args.at(2); } else if (appsettings->value(NULL, GC_OPENLASTATHLETE, true).toBool()) { // no parameters passed lets open the last athlete we worked with lastOpened = appsettings->value(NULL, GC_SETTINGS_LAST); // does lastopened Directory exists at all QDir lastOpenedDir(gcroot+"/"+lastOpened.toString()); if (lastOpenedDir.exists()) { // but hang on, did they crash? if so we need to open with a menu appsettings->initializeQSettingsAthlete(gcroot, lastOpened.toString()); if(appsettings->cvalue(lastOpened.toString(), GC_SAFEEXIT, true).toBool() != true) lastOpened = QVariant(); } else { lastOpened = QVariant(); } } #ifdef GC_WANT_HTTP // The API server offers webservices (default port 12021, see httpserver.ini) // This is to enable integration with R and similar if (appsettings->value(NULL, GC_START_HTTP, false).toBool() || server) { // notifications etc if (nogui) { qDebug()<<"Starting GoldenCheetah API web-services... (hit ^C to close)"; qDebug()<<"Athlete directory:"<close(); } listener=new HttpListener(settings,new APIWebService(home, application),application); // if not going on to launch a gui... if (nogui) { // catch ^C exit signal(SIGINT, sigabort); ret = application->exec(); // stop web server if running qDebug()<<"Stopping GoldenCheetah API web-services..."; listener->close(); // and done terminate(0); } } #endif // lets attempt to open as asked/remembered bool anyOpened = false; if (lastOpened != QVariant()) { QStringList list = lastOpened.toStringList(); QStringListIterator i(list); while (i.hasNext()) { QString cyclist = i.next(); QString homeDir = home.canonicalPath(); if (home.cd(cyclist)) { appsettings->initializeQSettingsAthlete(homeDir, cyclist); GcUpgrade v3; if (v3.upgradeConfirmedByUser(home)) { MainWindow *mainWindow = new MainWindow(home); mainWindow->show(); mainWindow->ridesAutoImport(); gc_opened++; home.cdUp(); anyOpened = true; } else { delete trainDB; terminate(0); } } } } // ack, didn't manage to open an athlete // and the upgradeWarning was // lets ask the user which / create a new one if (!anyOpened) { ChooseCyclistDialog d(home); d.setModal(true); // choose cancel? if ((ret=d.exec()) != QDialog::Accepted) { delete trainDB; terminate(0); } // chosen, so lets get the choice.. QString homeDir = home.canonicalPath(); home.cd(d.choice()); if (!home.exists()) { delete trainDB; terminate(0); } appsettings->initializeQSettingsAthlete(homeDir, d.choice()); // .. and open a mainwindow GcUpgrade v3; if (v3.upgradeConfirmedByUser(home)) { MainWindow *mainWindow = new MainWindow(home); mainWindow->show(); mainWindow->ridesAutoImport(); gc_opened++; } else { delete trainDB; terminate(0); } } ret=application->exec(); // close trainDB delete trainDB; // reset QSettings (global & Athlete) appsettings->clearGlobalAndAthletes(); } while (restarting); delete application; return ret; }