Files
GoldenCheetah/src/Core/main.cpp
Ale Martinez 61b5fee924 Added Start with last opened Athlete option to config
Default is checked to preserve current behavior, when cleared
the choose athlete dialog will be presented at start.
Workout library setting was moved to Train preferences
page to reduce clutter in General settings page.
[publish binaries]
2020-12-24 11:14:32 -03:00

745 lines
24 KiB
C++

/*
* 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 "NewMainWindow.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 <QApplication>
#include <QDesktopWidget>
#include <QtGui>
#include <QFile>
#include <QMessageBox>
#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 <signal.h>
#ifdef GC_WANT_X11
#include <X11/Xlib.h>
#endif
#include <QStandardPaths>
#include <gsl/gsl_errno.h>
//
// bootstrap state
//
bool restarting = false;
static bool nogui;
static int gc_opened=0;
//
// global application
//
QString gcroot;
QApplication *application;
QDesktopWidget *desktop = NULL;
#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 <RTool.h>
// 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 errors to `home'/goldencheetah.log
// sadly, no equivalent on Windows
#ifndef WIN32
#include "stdio.h"
#include "unistd.h"
void nostderr(QString dir)
{
// redirect stderr to a file
QFile fp(QString("%1/goldencheetah.log").arg(dir));
if (fp.open(QIODevice::WriteOnly|QIODevice::Truncate) == true) {
close(STDERR_FILENO);
if(dup(fp.handle()) != STDERR_FILENO) fprintf(stderr, "GoldenCheetah: cannot redirect stderr\n");
fp.close();
} else {
fprintf(stderr, "GoldenCheetah: cannot redirect stderr\n");
}
}
#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);
}
#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<argc; i++) sargs << argv[i];
#ifdef GC_WANT_PYTHON
bool noPy=false;
#endif
#ifdef GC_WANT_R
bool noR=false;
#endif
#ifdef GC_DEBUG
bool debug = true;
#else
bool debug = false;
#endif
bool server = false;
nogui = false;
bool help = false;
bool newgui = false;
// honour command line switches
foreach (QString arg, sargs) {
// help, usage or version requested, basic information
if (arg == "--help" || arg == "--usage" || arg == "--version") {
help = true;
fprintf(stderr, "GoldenCheetah %s (%d)\n", VERSION_STRING, VERSION_LATEST);
}
// help or usage requrested, additional information
if (arg == "--help" || arg == "--usage") {
fprintf(stderr, "usage: GoldenCheetah [[directory] athlete]\n\n");
fprintf(stderr, "--help or --usage to print this message and exit\n");
fprintf(stderr, "--version to print detailed version information and exit\n");
#ifdef GC_WANT_HTTP
fprintf(stderr, "--server to run as an API server\n");
#endif
#ifdef GC_DEBUG
fprintf(stderr, "--debug to turn on redirection of messages to goldencheetah.log [debug build]\n");
#else
fprintf(stderr, "--debug to direct diagnostic messages to the terminal instead of goldencheetah.log\n");
#endif
#ifdef GC_HAS_CLOUD_DB
fprintf(stderr, "--clouddbcurator to add CloudDB curator specific functions to the menus\n");
#endif
#ifdef GC_WANT_PYTHON
fprintf(stderr, "--no-python to disable Python startup\n");
#endif
#ifdef GC_WANT_R
fprintf(stderr, "--no-r to disable R startup\n");
#endif
fprintf (stderr, "\nSpecify the folder and/or athlete to open on startup\n");
fprintf(stderr, "If no parameters are passed it will reopen the last athlete.\n\n");
// version requested, additional information
} else if (arg == "--version") {
QString html = GcCrashDialog::versionHTML();
html.replace("</td><td>", ": "); // 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 == "--clouddbcurator") {
#ifdef GC_HAS_CLOUD_DB
CloudDBCommon::addCuratorFeatures = true;
#else
fprintf(stderr, "CloudDB support not compiled in, exiting.\n");
exit(1);
#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<QString,RideMetric*> 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 Q_OS_MACX
if ( QSysInfo::MacintoshVersion > QSysInfo::MV_10_8 )
{
// fix Mac OS X 10.9 (mavericks) font issue
// https://bugreports.qt-project.org/browse/QTBUG-32789
QFont::insertSubstitution("LucidaGrande", "Lucida Grande");
}
#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();
// create the application -- only ever ONE regardless of restarts
application = new QApplication(argc, argv);
//XXXIdleEventFilter idleFilter;
//XXXapplication->installEventFilter(&idleFilter);
// read defaults
initPowerProfile();
// set default colors
GCColor::setupColors();
appsettings->migrateQSettingsSystem(); // colors must be setup before migration can take place, but reading has to be from the migrated ones
GCColor::readConfig();
// set defaultfont - may be adjusted below
QFont font;
font.fromString(appsettings->value(NULL, GC_FONT_DEFAULT, QFont().toString()).toString());
font.setPointSize(11);
// use the default before taking into account screen size
baseFont = font;
// hidpi ratios -- single desktop for now
desktop = QApplication::desktop();
// since almost all dialogs are sized for a screen resolution
// of 1280x1024, to save issues we will scale DIALOGS to the
// overall screen estate when supporting HI-DPI. For widgetry
// all code needs to be changed to set height based upon font
// sizing instead.
dpiXFactor = 1.0;
dpiYFactor = 1.0;
#ifndef Q_OS_MAC // not needed on a Mac
// We will set screen ratio factor for sizing when a screen
// is greater than the Surface Pro 3 2160x1440, since its a break
// point with resolutions above that being devices like the
// 2560x1700 chromebook pixel before we get to truly hi-dpi
// resolutions like apples MBP retina 2880x1800 up through
// UHD, 4k and 5k displays at 3840x2160, 4096x2304 and 5120 x 2160.
QRect screenSize = desktop->availableGeometry();
// if we're running with dpiawareness of 0 the screen resolution
// will be expressed taking into account the scaling applied
// so for example a 3840x2160 screen will likely be expressed as
// being 1920 x 1080 rather than the native resolution
if (desktop->screen()->devicePixelRatio() <= 1 && screenSize.width() > 2160) {
// we're on a hidpi screen - lets create a multiplier - always use smallest
dpiXFactor = screenSize.width() / 1280.0;
dpiYFactor = screenSize.height() / 1024.0;
if (dpiYFactor < dpiXFactor) dpiXFactor = dpiYFactor;
else if (dpiXFactor < dpiYFactor) dpiYFactor = dpiXFactor;
// set default font size -- all others will scale off this
// choose a font size that would allow 60 lines of text on screen
// we can include the option for the user to set a scaling factor
// in settings before we release this in v3.5
double height = screenSize.height() / 70;
// points = height in inches * dpi
double pointsize = (height / QApplication::desktop()->logicalDpiY()) * 72;
baseFont.setPointSizeF(pointsize);
// fixup some style issues from QT not adjusting defaults on hidpi displays
// initially just pushbutton and combobox spacing...
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));
//qDebug()<<"geom:"<<QApplication::desktop()->geometry()<<"default font size:"<<pointsize<<"hidpi scaling:"<<dpiXFactor<<"physcial DPI:"<<QApplication::desktop()->physicalDpiX()<<"logical DPI:"<<QApplication::desktop()->logicalDpiX();
} else {
//qDebug()<<"geom:"<<QApplication::desktop()->geometry()<<"no need for hidpi scaling"<<"physcial DPI:"<<QApplication::desktop()->physicalDpiX()<<"logical DPI:"<<QApplication::desktop()->logicalDpiX();
}
#endif
// scale up to user scale factor
double fontscale = appsettings->value(NULL, GC_FONT_SCALE, 1.0).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:"<<CloudServiceFactory::instance().serviceNames();
//
// OPEN FIRST MAINWINDOW
//
do {
// lets not restart endlessly
restarting = false;
// we reload R if we are restarting to get that, but
// only if it failed
#ifdef GC_WANT_R
// create the singleton in the main thread
// will be shared by all athletes and all charts (!!)
if (noR) {
rtool = NULL;
} else if (rtool == NULL && appsettings->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::DataLocation);
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
#ifndef WIN32
if (!debug) nostderr(home.canonicalPath());
#else
Q_UNUSED(debug)
#endif
// 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);
// Initialize metrics once the translator is installed
RideMetricFactory::instance().initialize();
// Initialize global registry once the translator is installed
GcWindowRegistry::initialize();
// 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:"<<home.absolutePath();
} else {
// switch off warnings if in gui mode
#ifndef GC_WANT_ALLDEBUG
qInstallMessageHandler(myMessageOutput);
#endif
}
QString httpini = home.absolutePath() + "/httpserver.ini";
if (!QFile(httpini).exists()) {
// read default ini file
QFile file(":webservice/httpserver.ini");
QString content;
if (file.open(QIODevice::ReadOnly)) {
content = file.readAll();
file.close();
}
// write default ini file
QFile out(httpini);
if (out.open(QIODevice::WriteOnly)) {
out.resize(0);
QTextStream stream(&out);
stream << content;
out.close();
}
}
// use the default handler (just get an error page)
QSettings* settings=new QSettings(httpini,QSettings::IniFormat,application);
if (listener) {
// when changing the Athlete Directory, there is already a listener running
// close first to avoid errors
listener->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, true);
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;
}