main/core: changed code so that headless would run truly headless with QCoreApplication instead of QApplication

QApplication is designed for GUI/widget programs and thus fails to launch
if no window system is available. For a true headless mode, we must swap
out with QCoreApplication. However, what complicates this is that
QCommandLineParser - the class we use to determine whether we should run
in GUI mode or not - requires an active application instance to work.
This creates an unfortunate catch-22 that ultimately ended in writing a
custom command line parser.

Now we can correctly swap out with QCoreApplication, however this has
exposed other issues regarding functions that unnecessarily rely on QWidget
functions. So they'll need to be sorted out eventually.
This commit is contained in:
itsmattkc
2020-08-22 00:22:19 +10:00
parent 9aabde303e
commit 48ac8ea509
6 changed files with 450 additions and 122 deletions
+2
View File
@@ -21,6 +21,8 @@ set(OLIVE_SOURCES
common/cancelableobject.h common/cancelableobject.h
common/channellayout.h common/channellayout.h
common/clamp.h common/clamp.h
common/commandlineparser.h
common/commandlineparser.cpp
common/crashhandler.h common/crashhandler.h
common/crashhandler.cpp common/crashhandler.cpp
common/crashpadinterface.cpp common/crashpadinterface.cpp
+149
View File
@@ -0,0 +1,149 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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 3 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, see <http://www.gnu.org/licenses/>.
***/
#include "commandlineparser.h"
#include <QCoreApplication>
#include <QDebug>
CommandLineParser::~CommandLineParser()
{
foreach (const KnownOption& o, options_) {
delete o.option;
}
foreach (const KnownPositionalArgument& a, positional_args_) {
delete a.option;
}
}
const CommandLineParser::Option *CommandLineParser::AddOption(const QStringList &strings, const QString &description)
{
Option* o = new Option();
options_.append({strings, description, o});
return o;
}
const CommandLineParser::PositionalArgument *CommandLineParser::AddPositionalArgument(const QString &name, const QString &description, bool required)
{
PositionalArgument* a = new PositionalArgument();
positional_args_.append({name, description, a, required});
return a;
}
void CommandLineParser::Process(int argc, char **argv)
{
int positional_index = 0;
for (int i=1; i<argc; i++) {
if (argv[i][0] == '-') {
// Must be an option
// Skip past first dashes
const char* arg_basename = &argv[i][1];
bool matched_known = false;
for (int j=0; j<options_.size(); j++) {
KnownOption& o = options_[j];
foreach (const QString& s, o.args) {
if (!s.compare(arg_basename, Qt::CaseInsensitive)) {
// Flag discovered!
o.option->Set();
matched_known = true;
goto found_flag;
}
}
}
found_flag:
if (!matched_known) {
qWarning() << "Unknown parameter:" << argv[i];
}
} else {
// Must be a positional flag
if (positional_index < positional_args_.size()) {
positional_args_[positional_index].option->SetSetting(argv[i]);
positional_index++;
} else {
qWarning() << "Unknown parameter:" << argv[i];
}
}
}
}
void CommandLineParser::PrintHelp(const char* filename)
{
printf("%s %s\n",
QCoreApplication::applicationName().toUtf8().constData(),
QCoreApplication::applicationVersion().toUtf8().constData());
printf("Copyright (C) 2018-2020 Olive Team\n");
QString positional_args;
for (int i=0; i<positional_args_.size(); i++) {
if (i > 0) {
positional_args.append(' ');
}
positional_args.append('[');
positional_args.append(positional_args_.at(i).name);
positional_args.append(']');
}
const char* basename;
#ifdef Q_OS_WINDOWS
basename = strrchr(filename, '\\');
if (!basename) {
basename = strrchr(filename, '/');
}
#else
basename = strrchr(filename, '/');
#endif
printf("Usage: %s [options] %s\n\n", basename + 1, positional_args.toUtf8().constData());
foreach (const KnownOption& o, options_) {
QString all_args;
for (int i=0; i<o.args.size(); i++) {
if (i > 0) {
all_args.append(QStringLiteral(", "));
}
const QString& this_arg = o.args.at(i);
all_args.append('-');
all_args.append(this_arg);
}
printf(" %s\n", all_args.toUtf8().constData());
printf(" %s\n\n", o.description.toUtf8().constData());
}
printf("\n");
}
+107
View File
@@ -0,0 +1,107 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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 3 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, see <http://www.gnu.org/licenses/>.
***/
#ifndef COMMANDLINEPARSER_H
#define COMMANDLINEPARSER_H
#include <QStringList>
#include "common/define.h"
class CommandLineParser
{
public:
~CommandLineParser();
DISABLE_COPY_MOVE(CommandLineParser);
class Option {
public:
Option()
{
is_set_ = false;
}
bool IsSet() const
{
return is_set_;
}
void Set()
{
is_set_ = true;
}
private:
bool is_set_;
};
class PositionalArgument
{
public:
PositionalArgument() = default;
const QString& GetSetting() const
{
return setting_;
}
void SetSetting(const QString& s)
{
setting_ = s;
}
private:
QString setting_;
};
CommandLineParser() = default;
const Option* AddOption(const QStringList& strings, const QString& description);
const PositionalArgument* AddPositionalArgument(const QString& name, const QString& description, bool required = false);
void Process(int argc, char** argv);
void PrintHelp(const char* filename);
private:
struct KnownOption {
QStringList args;
QString description;
Option* option;
};
struct KnownPositionalArgument {
QString name;
QString description;
PositionalArgument* option;
bool required;
};
QVector<KnownOption> options_;
QVector<KnownPositionalArgument> positional_args_;
};
#endif // COMMANDLINEPARSER_H
+44 -88
View File
@@ -22,7 +22,6 @@
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QCommandLineParser>
#include <QDebug> #include <QDebug>
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo> #include <QFileInfo>
@@ -64,93 +63,23 @@
OLIVE_NAMESPACE_ENTER OLIVE_NAMESPACE_ENTER
Core Core::instance_; Core* Core::instance_ = nullptr;
Core::Core() : Core::Core(const CoreParams& params) :
main_window_(nullptr), main_window_(nullptr),
tool_(Tool::kPointer), tool_(Tool::kPointer),
addable_object_(Tool::kAddableEmpty), addable_object_(Tool::kAddableEmpty),
snapping_(true), snapping_(true),
gui_active_(false) core_params_(params)
{ {
// Store reference to this object, making the assumption that Core will only ever be made in
// main(). This will obviously break if not.
instance_ = this;
} }
Core *Core::instance() Core *Core::instance()
{ {
return &instance_; return instance_;
}
int Core::execute(QCoreApplication* a)
{
int exit_code = 1;
//
// Parse command line arguments
//
QCommandLineParser parser;
QCommandLineOption help_option = parser.addHelpOption();
QCommandLineOption version_option = parser.addVersionOption();
// Project from command line option
// FIXME: What's the correct way to make a visually "optional" positional argument, or is manually adding square
// brackets like this correct?
parser.addPositionalArgument("[project]", tr("Project to open on startup"));
// Create fullscreen option
QCommandLineOption fullscreen_option({"f", "fullscreen"}, tr("Start in full screen mode"));
parser.addOption(fullscreen_option);
// Create headless export option
QCommandLineOption headless_export_option({"x", "export"}, tr("Export project from command line"));
parser.addOption(headless_export_option);
// Parse options
parser.process(*a);
if (parser.isSet(help_option) || parser.isSet(version_option)) {
// These options don't launch any of the application proper
return a->exec();
}
// Start core
OLIVE_NAMESPACE::Core::instance()->Start();
QStringList args = parser.positionalArguments();
// Detect project to load on startup
if (!args.isEmpty()) {
startup_project_ = args.first();
}
gui_active_ = !parser.isSet(headless_export_option);
if (gui_active_) {
// Start GUI
StartGUI(parser.isSet(fullscreen_option));
// If we have a startup
QMetaObject::invokeMethod(this, "OpenStartupProject", Qt::QueuedConnection);
// Run application loop and receive exit code
exit_code = a->exec();
} else {
if (parser.isSet(headless_export_option)) {
// Start a headless export
if (StartHeadlessExport()) {
exit_code = 0;
}
}
}
// Clear core memory
OLIVE_NAMESPACE::Core::instance()->Stop();
return exit_code;
} }
void Core::DeclareTypesForQt() void Core::DeclareTypesForQt()
@@ -204,6 +133,22 @@ void Core::Start()
// //
qInfo() << "Using Qt version:" << qVersion(); qInfo() << "Using Qt version:" << qVersion();
switch (core_params_.run_mode()) {
case CoreParams::kRunNormal:
// Start GUI
StartGUI(core_params_.fullscreen());
// If we have a startup
QMetaObject::invokeMethod(this, "OpenStartupProject", Qt::QueuedConnection);
break;
case CoreParams::kHeadlessExport:
qInfo() << "Headless export is not fully implemented yet";
break;
case CoreParams::kHeadlessPreCache:
qInfo() << "Headless pre-cache is not fully implemented yet";
break;
}
} }
void Core::Stop() void Core::Stop()
@@ -571,18 +516,20 @@ void Core::ProjectWasModified(bool e)
bool Core::StartHeadlessExport() bool Core::StartHeadlessExport()
{ {
if (startup_project_.isEmpty()) { const QString& startup_project = core_params_.startup_project();
if (startup_project.isEmpty()) {
qCritical().noquote() << tr("You must specify a project file to export"); qCritical().noquote() << tr("You must specify a project file to export");
return false; return false;
} }
if (!QFileInfo::exists(startup_project_)) { if (!QFileInfo::exists(startup_project)) {
qCritical().noquote() << tr("Specified project does not exist"); qCritical().noquote() << tr("Specified project does not exist");
return false; return false;
} }
// Start a load task and try running it // Start a load task and try running it
ProjectLoadTask plm(startup_project_); ProjectLoadTask plm(startup_project);
CLITaskDialog task_dialog(&plm); CLITaskDialog task_dialog(&plm);
if (task_dialog.Run()) { if (task_dialog.Run()) {
@@ -651,21 +598,24 @@ bool Core::StartHeadlessExport()
void Core::OpenStartupProject() void Core::OpenStartupProject()
{ {
const QString& startup_project = core_params_.startup_project();
bool startup_project_exists = !startup_project.isEmpty() && QFileInfo::exists(startup_project);
// Load startup project // Load startup project
if (!startup_project_.isEmpty() && !QFileInfo::exists(startup_project_)) { if (!startup_project_exists && !startup_project.isEmpty()) {
QMessageBox::warning(main_window_, QMessageBox::warning(main_window_,
tr("Failed to open startup file"), tr("Failed to open startup file"),
tr("The project \"%1\" doesn't exist. A new project will be started instead.").arg(startup_project_), tr("The project \"%1\" doesn't exist. "
"A new project will be started instead.").arg(startup_project),
QMessageBox::Ok); QMessageBox::Ok);
startup_project_.clear();
} }
if (startup_project_.isEmpty()) { if (startup_project_exists) {
// If a startup project was set and exists, open it now
OpenProjectInternal(startup_project);
} else {
// If no load project is set, create a new one on open // If no load project is set, create a new one on open
CreateNewProject(); CreateNewProject();
} else {
OpenProjectInternal(startup_project_);
} }
} }
@@ -1228,4 +1178,10 @@ void Core::OpenProject()
} }
} }
Core::CoreParams::CoreParams() :
mode_(kRunNormal),
run_fullscreen_(false)
{
}
OLIVE_NAMESPACE_EXIT OLIVE_NAMESPACE_EXIT
+58 -13
View File
@@ -52,12 +52,62 @@ class Core : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
class CoreParams
{
public:
CoreParams();
enum RunMode {
kRunNormal,
kHeadlessExport,
kHeadlessPreCache
};
bool fullscreen() const
{
return run_fullscreen_;
}
void set_fullscreen(bool e)
{
run_fullscreen_ = e;
}
RunMode run_mode() const
{
return mode_;
}
void set_run_mode(RunMode m)
{
mode_ = m;
}
const QString startup_project() const
{
return startup_project_;
}
void set_startup_project(const QString& p)
{
startup_project_ = p;
}
private:
RunMode mode_;
QString startup_project_;
bool run_fullscreen_;
};
/** /**
* @brief Core Constructor * @brief Core Constructor
* *
* Currently empty * Currently empty
*/ */
Core(); Core(const CoreParams& params);
/** /**
* @brief Core object accessible from anywhere in the code * @brief Core object accessible from anywhere in the code
@@ -66,7 +116,10 @@ public:
*/ */
static Core* instance(); static Core* instance();
int execute(QCoreApplication *a); const CoreParams& core_params() const
{
return core_params_;
}
/** /**
* @brief Start Olive Core * @brief Start Olive Core
@@ -407,14 +460,6 @@ private:
*/ */
MainWindow* main_window_; MainWindow* main_window_;
/**
* @brief Internal startup project object
*
* If the user specifies a project file on the command line, the command line parser in Start() will write the
* project URL here to be loaded once Olive has finished initializing.
*/
QString startup_project_;
/** /**
* @brief List of currently open projects * @brief List of currently open projects
*/ */
@@ -456,14 +501,14 @@ private:
QStringList recent_projects_; QStringList recent_projects_;
/** /**
* @brief Internal variable for whether the GUI is active * @brief Parameters set up in main() determining how the program should run
*/ */
bool gui_active_; CoreParams core_params_;
/** /**
* @brief Static singleton core instance * @brief Static singleton core instance
*/ */
static Core instance_; static Core* instance_;
private slots: private slots:
void SaveAutorecovery(); void SaveAutorecovery();
+90 -21
View File
@@ -34,34 +34,23 @@ extern "C" {
#include <csignal> #include <csignal>
#include <QApplication> #include <QApplication>
#include <QCommandLineParser>
#include <QSurfaceFormat> #include <QSurfaceFormat>
#include "core.h" #include "core.h"
#include "common/commandlineparser.h"
#include "common/debug.h" #include "common/debug.h"
#ifdef USE_CRASHPAD #ifdef USE_CRASHPAD
#include "common/crashpadinterface.h" #include "common/crashpadinterface.h"
#endif // USE_CRASHPAD #endif // USE_CRASHPAD
int main(int argc, char *argv[]) { int main(int argc, char *argv[])
// Set OpenGL display profile (3.2 Core) {
QSurfaceFormat format; // Set up debug handler
format.setVersion(3, 2); qInstallMessageHandler(OLIVE_NAMESPACE::DebugHandler);
format.setDepthBufferSize(24);
format.setProfile(QSurfaceFormat::CoreProfile);
QSurfaceFormat::setDefaultFormat(format);
//QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
// Create application instance
QApplication a(argc, argv);
// Set application metadata
QCoreApplication::setOrganizationName("olivevideoeditor.org");
QCoreApplication::setOrganizationDomain("olivevideoeditor.org");
QCoreApplication::setApplicationName("Olive");
// Generate version string
QString app_version = APPVERSION; QString app_version = APPVERSION;
#ifdef GITHASH #ifdef GITHASH
// Anything after the hyphen is considered "unimportant" information. Text BEFORE the hyphen is used in version // Anything after the hyphen is considered "unimportant" information. Text BEFORE the hyphen is used in version
@@ -70,14 +59,85 @@ int main(int argc, char *argv[]) {
app_version.append(GITHASH); app_version.append(GITHASH);
#endif #endif
// Set application metadata
QCoreApplication::setOrganizationName("olivevideoeditor.org");
QCoreApplication::setOrganizationDomain("olivevideoeditor.org");
QCoreApplication::setApplicationName("Olive");
QCoreApplication::setApplicationVersion(app_version); QCoreApplication::setApplicationVersion(app_version);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
QGuiApplication::setDesktopFileName("org.olivevideoeditor.Olive"); QGuiApplication::setDesktopFileName("org.olivevideoeditor.Olive");
#endif #endif
// Set up debug handler //
qInstallMessageHandler(OLIVE_NAMESPACE::DebugHandler); // Parse command line arguments
//
OLIVE_NAMESPACE::Core::CoreParams startup_params;
CommandLineParser parser;
const CommandLineParser::Option* help_option =
parser.AddOption({QStringLiteral("h"), QStringLiteral("-help")},
QCoreApplication::translate("main", "Show this help text"));
const CommandLineParser::Option* version_option =
parser.AddOption({QStringLiteral("v"), QStringLiteral("-version")},
QCoreApplication::translate("main", "Show application version"));
const CommandLineParser::Option* fullscreen_option =
parser.AddOption({QStringLiteral("f"), QStringLiteral("-fullscreen")},
QCoreApplication::translate("main", "Start in full-screen mode"));
const CommandLineParser::Option* export_option =
parser.AddOption({QStringLiteral("x"), QStringLiteral("-export")},
QCoreApplication::translate("main", "Export only (No GUI)"));
const CommandLineParser::PositionalArgument* project_argument =
parser.AddPositionalArgument(QStringLiteral("project"),
QCoreApplication::translate("main", "Project to open on startup"));
parser.Process(argc, argv);
if (help_option->IsSet()) {
// Show help
parser.PrintHelp(argv[0]);
return 0;
}
if (version_option->IsSet()) {
// Print version
printf("%s\n", app_version.toUtf8().constData());
return 0;
}
if (export_option->IsSet()) {
startup_params.set_run_mode(OLIVE_NAMESPACE::Core::CoreParams::kHeadlessExport);
}
startup_params.set_fullscreen(fullscreen_option->IsSet());
startup_params.set_startup_project(project_argument->GetSetting());
// Set OpenGL display profile (3.2 Core)
QSurfaceFormat format;
format.setVersion(3, 2);
format.setDepthBufferSize(24);
format.setProfile(QSurfaceFormat::CoreProfile);
QSurfaceFormat::setDefaultFormat(format);
// Enable application automatically using higher resolution images from icons
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
// Create application instance
std::unique_ptr<QCoreApplication> a;
if (startup_params.run_mode() == OLIVE_NAMESPACE::Core::CoreParams::kRunNormal) {
a.reset(new QApplication(argc, argv));
} else {
a.reset(new QCoreApplication(argc, argv));
}
// Register FFmpeg codecs and filters (deprecated in 4.0+) // Register FFmpeg codecs and filters (deprecated in 4.0+)
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100) #if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
@@ -94,5 +154,14 @@ int main(int argc, char *argv[]) {
} }
#endif // USE_CRASHPAD #endif // USE_CRASHPAD
return OLIVE_NAMESPACE::Core::instance()->execute(&a); // Start core
OLIVE_NAMESPACE::Core c(startup_params);
c.Start();
int ret = a->exec();
// Clear core memory
c.Stop();
return ret;
} }