ui: Show news sent with update check on start screen

* News are passed as JSON string, containing an array of news items.
* Each news item has version and content (html).
* Items can be filtered by min/max version, OS and license. The first item that fits is used.
* Items can be important, which highlights the news box.
* News are not shown if older than 5 days.

fortune cookie message = You will be financially successful in case your actions are well planned.
This commit is contained in:
Eberhard Graether
2018-10-21 13:49:06 +02:00
parent 518f15d490
commit 6a6bd0fca8
17 changed files with 463 additions and 102 deletions
+21 -18
View File
@@ -29,19 +29,38 @@
border-radius: 10px;
}
#recentLabel {
#titleLabel {
color: black;
font-size: 16px;
font-weight: bold;
margin-left: 6px;
}
#versionLabel {
#textLabel {
color: black;
font-size: 12px;
}
#boldLabel {
color: black;
font-size: 12px;
font-weight: bold;
}
#textField {
background: white;
border: 1px solid lightgray;
border-radius: 12px;
color: black;
font-size: 12px;
margin-top: 5px;
min-width: 180px;
}
#textField[important=true] {
border: 2px solid #007ac3;
}
#updateButton, #upgradeButton {
margin: 1px 0 3px;
text-align: left;
@@ -57,19 +76,3 @@
color: black;
text-decoration: none;
}
#licenseHeaderLabel {
color: black;
font-size: 12px;
font-weight: bold;
}
#licenseLabel {
color: black;
font-size: 12px;
}
#welcomeLabel {
color: black;
font-size: 12px;
}
@@ -97,6 +97,7 @@
<skip_version><!-- VERSION: version to skip for update --></skip_version>
<update_url><!-- STRING: url to new version download --></update_url>
<update_version><!-- VERSION: version of last check --></update_version>
<news><!-- STRING: news in last update check --></news>
</update_check>
<seen_error_help_message><!-- BOOL: whether user has been shown help message on errors after indexing --></seen_error_help_message>
+24 -7
View File
@@ -24,7 +24,7 @@ std::shared_ptr<LicenseChecker> LicenseChecker::getInstance()
return s_instance;
}
std::string LicenseChecker::getCurrentLicenseString() const
std::string LicenseChecker::getCurrentLicenseString()
{
License license;
bool isLoaded = license.loadFromEncodedString(
@@ -38,7 +38,7 @@ std::string LicenseChecker::getCurrentLicenseString() const
return "";
}
void LicenseChecker::saveCurrentLicenseString(const std::string& licenseString) const
void LicenseChecker::saveCurrentLicenseString(const std::string& licenseString)
{
License license;
bool isLoaded = license.loadFromString(licenseString);
@@ -55,7 +55,7 @@ bool LicenseChecker::isCurrentLicenseValid()
return checkCurrentLicense() == LICENSE_VALID;
}
LicenseChecker::LicenseState LicenseChecker::checkCurrentLicense() const
LicenseChecker::LicenseState LicenseChecker::checkCurrentLicense()
{
ApplicationSettings* appSettings = ApplicationSettings::getInstance().get();
if (appSettings == nullptr)
@@ -96,7 +96,7 @@ LicenseChecker::LicenseState LicenseChecker::checkCurrentLicense() const
return checkLicense(license);
}
LicenseChecker::LicenseState LicenseChecker::checkLicenseString(const std::string& licenseString) const
LicenseChecker::LicenseState LicenseChecker::checkLicenseString(const std::string& licenseString)
{
if (licenseString.size() == 0)
{
@@ -115,7 +115,7 @@ LicenseChecker::LicenseState LicenseChecker::checkLicenseString(const std::strin
return checkLicense(license);
}
MessageEnteredLicense::LicenseType LicenseChecker::getCurrentLicenseType() const
MessageEnteredLicense::LicenseType LicenseChecker::getCurrentLicenseType()
{
License license;
bool isLoaded = license.loadFromEncodedString(
@@ -143,7 +143,7 @@ MessageEnteredLicense::LicenseType LicenseChecker::getCurrentLicenseType() const
return MessageEnteredLicense::LICENSE_COMMERCIAL;
}
MessageEnteredLicense::LicenseType LicenseChecker::getLicenseType(const std::string& licenseString) const
MessageEnteredLicense::LicenseType LicenseChecker::getLicenseType(const std::string& licenseString)
{
if (licenseString.size() == 0)
{
@@ -174,11 +174,28 @@ MessageEnteredLicense::LicenseType LicenseChecker::getLicenseType(const std::str
return MessageEnteredLicense::LICENSE_COMMERCIAL;
}
std::string LicenseChecker::getCurrentLicenseTypeString()
{
// WARNING: Don't change these string. The server API relies on them.
switch (getCurrentLicenseType())
{
case MessageEnteredLicense::LICENSE_NONE:
case MessageEnteredLicense::LICENSE_NON_COMMERCIAL:
return "private";
case MessageEnteredLicense::LICENSE_TEST:
return "test";
case MessageEnteredLicense::LICENSE_COMMERCIAL:
return "commercial";
}
return "private";
}
LicenseChecker::LicenseChecker()
{
}
LicenseChecker::LicenseState LicenseChecker::checkLicense(License& license) const
LicenseChecker::LicenseState LicenseChecker::checkLicense(License& license)
{
if (license.isExpired())
{
+10 -8
View File
@@ -23,22 +23,24 @@ public:
~LicenseChecker() = default;
std::string getCurrentLicenseString() const;
void saveCurrentLicenseString(const std::string& licenseString) const;
static std::string getCurrentLicenseString();
static void saveCurrentLicenseString(const std::string& licenseString);
bool isCurrentLicenseValid();
LicenseState checkCurrentLicense() const;
LicenseState checkLicenseString(const std::string& licenseString) const;
static bool isCurrentLicenseValid();
static LicenseState checkCurrentLicense();
static LicenseState checkLicenseString(const std::string& licenseString);
MessageEnteredLicense::LicenseType getCurrentLicenseType() const;
MessageEnteredLicense::LicenseType getLicenseType(const std::string& licenseString) const;
static MessageEnteredLicense::LicenseType getCurrentLicenseType();
static MessageEnteredLicense::LicenseType getLicenseType(const std::string& licenseString);
static std::string getCurrentLicenseTypeString();
private:
LicenseChecker();
LicenseChecker(const LicenseChecker&) = delete;
void operator=(const LicenseChecker&) = delete;
LicenseState checkLicense(License& license) const;
static LicenseState checkLicense(License& license);
static std::shared_ptr<LicenseChecker> s_instance;
};
+10
View File
@@ -597,6 +597,16 @@ Version ApplicationSettings::getUpdateVersion() const
return Version::fromString(getValue<std::string>("user/update_check/update_version", "2017.1.0"));
}
std::string ApplicationSettings::getUpdateNews() const
{
return getValue<std::string>("user/update_check/news", "");
}
void ApplicationSettings::setUpdateNews(const std::string& news)
{
setValue<std::string>("user/update_check/news", news);
}
void ApplicationSettings::setUpdateVersion(const Version& version)
{
if (version.isValid())
+3
View File
@@ -167,6 +167,9 @@ public:
Version getUpdateVersion() const;
void setUpdateVersion(const Version& version);
std::string getUpdateNews() const;
void setUpdateNews(const std::string& news);
bool getSeenErrorHelpMessage() const;
void setSeenErrorHelpMessage(bool seen);
+4
View File
@@ -45,6 +45,8 @@ add_files(
qt/element/QtListBoxItem.h
qt/element/QtLocationPicker.cpp
qt/element/QtLocationPicker.h
qt/element/QtNewsWidget.cpp
qt/element/QtNewsWidget.h
qt/element/QtPathListBox.cpp
qt/element/QtPathListBox.h
qt/element/QtPathListBoxItem.cpp
@@ -67,6 +69,8 @@ add_files(
qt/element/QtStatusBar.h
qt/element/QtTable.cpp
qt/element/QtTable.h
qt/element/QtTextEdit.cpp
qt/element/QtTextEdit.h
qt/element/QtTooltip.cpp
qt/element/QtTooltip.h
qt/element/QtUndoRedo.cpp
+227
View File
@@ -0,0 +1,227 @@
#include "QtNewsWidget.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QVBoxLayout>
#include <QStyle>
#include "ApplicationSettings.h"
#include "LicenseChecker.h"
#include "logging.h"
#include "QtTextEdit.h"
#include "TimeStamp.h"
#include "utilityApp.h"
#include "Version.h"
QtNewsWidget::QtNewsWidget(QWidget* parent)
: QWidget(parent)
{
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
m_text = new QtTextEdit();
m_text->setObjectName("textField");
m_text->setReadOnly(true);
m_text->setTabStopWidth(8 * m_text->fontMetrics().width('9'));
m_text->setViewportMargins(6, 4, 16, 4);
m_text->setOpenExternalLinks(true);
layout->addWidget(m_text);
QString placeholder = "No news available";
if (!ApplicationSettings::getInstance()->getAutomaticUpdateCheck())
{
placeholder += "\n(Enable update check)";
}
m_text->setPlaceholderText(placeholder);
updateNews();
}
/*
syntax:
[
{
// required
"version": 1,
"content": "<b>Hello World!</b>",
// optional
"conditions":
{
"license": ["test", "private", "commercial"],
"os": ["windows", "macOS", "linux"],
"min_version": "2017.2.0",
"max_version": "2018.4.2",
}
// optional
"flags":
{
"important": true
}
}
]
*/
void QtNewsWidget::updateNews()
{
const int supportedNewsItemVersion = 1;
resetFlags();
ApplicationSettings* appSettings = ApplicationSettings::getInstance().get();
std::string newsRaw = appSettings->getUpdateNews();
if (!newsRaw.size())
{
setNews("");
return;
}
if (TimeStamp::now().deltaHours(appSettings->getLastUpdateCheck()) >= 120)
{
LOG_INFO_STREAM(<< "Ignore news string, because older than 5 days.");
setNews("");
return;
}
LOG_INFO_STREAM(<< "Process news string: " << newsRaw);
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(QString::fromStdString(newsRaw).toUtf8(), &error);
if (doc.isNull() || !doc.isArray())
{
LOG_ERROR_STREAM(<< "News string couldn't be parsed as JSON: " << error.errorString().toStdString()
<< "\nJSON: " << newsRaw);
setNews("");
return;
}
size_t itemNum = 0;
for (QJsonValueRef value : doc.array())
{
itemNum++;
QJsonObject newsItem = value.toObject();
int version = newsItem.value("version").toInt();
if (version > supportedNewsItemVersion)
{
LOG_INFO_STREAM(<< "News item " << itemNum << " version " << version << " is not supported.");
continue;
}
if (!checkConditions(newsItem.value("conditions").toObject()))
{
LOG_INFO_STREAM(<< "News item " << itemNum << " conditions failed.");
continue;
}
processFlags(newsItem.value("flags").toObject());
setNews(newsItem.value("content").toString());
return;
}
setNews("");
}
void QtNewsWidget::resetFlags()
{
setImportant(false);
}
void QtNewsWidget::setNews(const QString& news)
{
m_text->setHtml(news);
}
void QtNewsWidget::setImportant(bool important)
{
m_text->setProperty("important", important);
m_text->style()->unpolish(m_text);
m_text->style()->polish(m_text);
}
bool QtNewsWidget::checkConditions(const QJsonObject& conditions) const
{
if (conditions.isEmpty())
{
return true;
}
// min_version
{
QString minVersionString = conditions.value("min_version").toString();
if (!minVersionString.isEmpty())
{
Version minVersion = Version::fromString(minVersionString.toStdString());
if (minVersion.isValid() && minVersion > Version::getApplicationVersion())
{
LOG_INFO_STREAM(<< "Failed condition min_version.");
return false;
}
}
}
// max_version
{
QString maxVersionString = conditions.value("max_version").toString();
if (!maxVersionString.isEmpty())
{
Version maxVersion = Version::fromString(maxVersionString.toStdString());
if (maxVersion.isValid() && maxVersion < Version::getApplicationVersion())
{
LOG_INFO_STREAM(<< "Failed condition max_version.");
return false;
}
}
}
// license
{
QJsonArray licenseStrings = conditions.value("license").toArray();
if (!licenseStrings.isEmpty())
{
QString licenseString = QString::fromStdString(LicenseChecker::getCurrentLicenseTypeString());
if (!licenseStrings.contains(QJsonValue(licenseString)))
{
LOG_INFO_STREAM(<< "Failed condition license.");
return false;
}
}
}
// OS
{
QJsonArray osStrings = conditions.value("os").toArray();
if (!osStrings.isEmpty())
{
QString osString = QString::fromStdString(utility::getOsTypeString());
if (!osStrings.contains(QJsonValue(osString)))
{
LOG_INFO_STREAM(<< "Failed condition os.");
return false;
}
}
}
return true;
}
void QtNewsWidget::processFlags(const QJsonObject& flags)
{
if (flags.isEmpty())
{
return;
}
// important
{
bool important = flags.value("important").toBool();
if (important)
{
LOG_INFO_STREAM(<< "Flag important enabled.");
setImportant(true);
}
}
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef QT_NEWS_WIDGET_H
#define QT_NEWS_WIDGET_H
#include <QWidget>
class QtTextEdit;
class QJsonObject;
class QtNewsWidget
: public QWidget
{
Q_OBJECT
public:
QtNewsWidget(QWidget* parent = nullptr);
virtual ~QtNewsWidget() = default;
public slots:
void updateNews();
private:
void resetFlags();
void setNews(const QString& news);
void setImportant(bool important);
bool checkConditions(const QJsonObject& conditions) const;
void processFlags(const QJsonObject& flags);
QtTextEdit* m_text;
};
#endif // QT_NEWS_WIDGET_H
+18
View File
@@ -0,0 +1,18 @@
#include "QtTextEdit.h"
QtTextEdit::QtTextEdit(QWidget* parent)
: QTextBrowser(parent)
{
document()->setDefaultStyleSheet("a { color: #007AC2; }");
}
void QtTextEdit::setViewportMargins(int left, int top, int right, int bottom)
{
QAbstractScrollArea::setViewportMargins(left, top, right, bottom);
}
void QtTextEdit::focusInEvent(QFocusEvent* event)
{
emit focus();
QTextBrowser::focusInEvent(event);
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef QT_TEXT_EDIT_H
#define QT_TEXT_EDIT_H
#include <QTextBrowser>
class QtTextEdit
: public QTextBrowser
{
Q_OBJECT
public:
QtTextEdit(QWidget* parent = nullptr);
void setViewportMargins(int left, int top, int right, int bottom);
signals:
void focus();
protected:
void focusInEvent(QFocusEvent* event);
};
#endif // QT_TEXT_EDIT_H
@@ -99,6 +99,8 @@ void QtUpdateCheckerWidget::checkUpdate(bool force)
{
setDownloadUrl(result.url);
}
emit updateReceived();
}
);
}
@@ -10,10 +10,15 @@ class QPushButton;
class QtUpdateCheckerWidget
: public QWidget
{
Q_OBJECT
public:
QtUpdateCheckerWidget(QWidget* parent = nullptr);
virtual ~QtUpdateCheckerWidget();
signals:
void updateReceived();
private:
void checkUpdate(bool force);
void setDownloadUrl(QString url);
+24 -40
View File
@@ -36,27 +36,10 @@ void QtUpdateChecker::check(bool force, std::function<void(Result)> callback)
appSettings->setUpdateDownloadUrl("");
appSettings->save();
QString urlString = "https://www.sourcetrail.com/api/v1/versions/latest";
QString urlString = "https://www.sourcetrail.com/api/v2/versions/latest";
// OS
std::string osString;
switch (utility::getOsType())
{
case OS_WINDOWS:
osString = "windows";
break;
case OS_MAC:
osString = "macOS";
break;
case OS_LINUX:
osString = "linux";
break;
default:
callback(result);
return;
}
std::string osString = utility::getOsTypeString();
urlString += ("?os=" + osString).c_str();
// architecture
@@ -68,21 +51,7 @@ void QtUpdateChecker::check(bool force, std::function<void(Result)> callback)
urlString += ("&version=" + Version::getApplicationVersion().toDisplayString()).c_str();
// license
std::string licenseString;
switch (LicenseChecker::getInstance()->getCurrentLicenseType())
{
case MessageEnteredLicense::LICENSE_NONE:
case MessageEnteredLicense::LICENSE_NON_COMMERCIAL:
licenseString = "private";
break;
case MessageEnteredLicense::LICENSE_TEST:
licenseString = "test";
break;
case MessageEnteredLicense::LICENSE_COMMERCIAL:
licenseString = "commercial";
break;
}
std::string licenseString = LicenseChecker::getCurrentLicenseTypeString();
urlString += ("&license=" + licenseString).c_str();
// user token
@@ -103,15 +72,27 @@ void QtUpdateChecker::check(bool force, std::function<void(Result)> callback)
{
Result result;
ApplicationSettings* appSettings = ApplicationSettings::getInstance().get();
bool saveAppSettings = false;
do
{
QJsonDocument doc = QJsonDocument::fromJson(bytes);
if (!doc.isObject())
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(bytes, &error);
if (doc.isNull() || !doc.isObject())
{
LOG_ERROR_STREAM(<< "Update response couldn't be parsed as JSON");
LOG_ERROR_STREAM(<< "Update response couldn't be parsed as JSON: " << error.errorString().toStdString());
break;
}
QString news = doc.object().find("news")->toString();
if (news.toStdString() != appSettings->getUpdateNews())
{
appSettings->setUpdateNews(news.toStdString());
saveAppSettings = true;
}
QString version = doc.object().find("version")->toString();
QString url = doc.object().find("url")->toString();
@@ -129,10 +110,9 @@ void QtUpdateChecker::check(bool force, std::function<void(Result)> callback)
result.version = updateVersion;
result.url = url;
ApplicationSettings* appSettings = ApplicationSettings::getInstance().get();
appSettings->setUpdateVersion(updateVersion);
appSettings->setUpdateDownloadUrl(url.toStdString());
appSettings->save();
saveAppSettings = true;
if (!force && appSettings->getSkipUpdateForVersion() == updateVersion)
{
@@ -153,7 +133,6 @@ void QtUpdateChecker::check(bool force, std::function<void(Result)> callback)
if (val == 1)
{
appSettings->setSkipUpdateForVersion(updateVersion);
appSettings->save();
}
else if (val == 2)
{
@@ -163,6 +142,11 @@ void QtUpdateChecker::check(bool force, std::function<void(Result)> callback)
}
while (false);
if (saveAppSettings)
{
appSettings->save();
}
request->deleteLater();
callback(result);
+32 -20
View File
@@ -7,17 +7,17 @@
#include <QVBoxLayout>
#include <QMessageBox>
#include "ApplicationSettings.h"
#include "AppPath.h"
#include "MessageLoadProject.h"
#include "ResourcePaths.h"
#include "Version.h"
#include "License.h"
#include "MessageLoadProject.h"
#include "ProjectSettings.h"
#include "PublicKey.h"
#include "QtUpdateCheckerWidget.h"
#include "QtNewsWidget.h"
#include "ResourcePaths.h"
#include "utilityQt.h"
#include "ApplicationSettings.h"
#include "ProjectSettings.h"
#include "Version.h"
QtRecentProjectButton::QtRecentProjectButton(QWidget* parent)
: QPushButton(parent)
@@ -93,7 +93,7 @@ QtStartScreen::QtStartScreen(QWidget *parent)
QSize QtStartScreen::sizeHint() const
{
return QSize(570, 600);
return QSize(570, 630);
}
void QtStartScreen::updateButtons()
@@ -153,39 +153,39 @@ void QtStartScreen::setupStartScreen()
addLogo();
QHBoxLayout* layout = new QHBoxLayout();
layout->setContentsMargins(15, 170, 15, 10);
layout->setContentsMargins(15, 170, 15, 0);
layout->setSpacing(1);
m_content->setLayout(layout);
{
QVBoxLayout* col = new QVBoxLayout();
layout->addLayout(col, 2);
layout->addLayout(col, 3);
QLabel* versionLabel = new QLabel(("Version " + Version::getApplicationVersion().toDisplayString()).c_str(), this);
versionLabel->setObjectName("versionLabel");
versionLabel->setObjectName("boldLabel");
col->addWidget(versionLabel);
QtUpdateCheckerWidget* checker = new QtUpdateCheckerWidget(this);
col->addWidget(checker);
col->addSpacing(30);
col->addSpacing(15);
if (license.isValid())
{
std::string licenseString = license.getLicenseInfo();
QLabel* licenseHeader = new QLabel("Licensed to:");
licenseHeader->setObjectName("licenseHeaderLabel");
licenseHeader->setObjectName("boldLabel");
col->addWidget(licenseHeader);
col->addSpacing(2);
QLabel* licenseLabel = new QLabel(licenseString.c_str());
licenseLabel->setObjectName("licenseLabel");
licenseLabel->setObjectName("textLabel");
col->addWidget(licenseLabel);
}
else
{
QLabel* licenseHeader = new QLabel("Not licensed for<br />commercial use.");
licenseHeader->setObjectName("licenseHeaderLabel");
licenseHeader->setObjectName("boldLabel");
col->addWidget(licenseHeader);
QPushButton* upgradeButton = new QPushButton("upgrade");
@@ -195,6 +195,20 @@ void QtStartScreen::setupStartScreen()
connect(upgradeButton, &QPushButton::clicked, [this](){ emit openEnterLicenseDialog(); });
}
col->addSpacing(15);
{
QLabel* newsHeader = new QLabel("News:");
newsHeader->setObjectName("boldLabel");
col->addWidget(newsHeader);
QtNewsWidget* newsWidget = new QtNewsWidget(this);
col->addWidget(newsWidget);
connect(checker, &QtUpdateCheckerWidget::updateReceived, newsWidget, &QtNewsWidget::updateNews);
}
col->addSpacing(15);
col->addStretch();
QPushButton* newProjectButton = new QPushButton("New Project", this);
@@ -212,23 +226,21 @@ void QtStartScreen::setupStartScreen()
openProjectButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
connect(openProjectButton, &QPushButton::clicked, this, &QtStartScreen::handleOpenProjectButton);
col->addWidget(openProjectButton);
layout->addStretch(2);
}
layout->addSpacing(50);
{
QVBoxLayout* col = new QVBoxLayout();
layout->addLayout(col, 1);
QLabel* recentProjectsLabel = new QLabel("Recent Projects: ", this);
recentProjectsLabel->setObjectName("recentLabel");
recentProjectsLabel->setObjectName("titleLabel");
col->addWidget(recentProjectsLabel);
col->addSpacing(20);
for (int i = 0
; i < ApplicationSettings::getInstance()->getMaxRecentProjectsCount()
; i++)
for (int i = 0; i < ApplicationSettings::getInstance()->getMaxRecentProjectsCount(); i++)
{
QtRecentProjectButton* button = new QtRecentProjectButton(this);
button->setAttribute(Qt::WA_LayoutUsesWidgetRect); // fixes layouting on Mac
+22 -5
View File
@@ -137,6 +137,16 @@ void utility::killRunningProcesses()
}
}
int utility::getIdealThreadCount()
{
int threadCount = QThread::idealThreadCount();
if (getOsType() == OS_WINDOWS)
{
threadCount -= 1;
}
return std::max(1, threadCount);
}
OsType utility::getOsType()
{
if (QSysInfo::windowsVersion() != QSysInfo::WV_None)
@@ -154,14 +164,21 @@ OsType utility::getOsType()
return OS_UNKNOWN;
}
int utility::getIdealThreadCount()
std::string utility::getOsTypeString()
{
int threadCount = QThread::idealThreadCount();
if (getOsType() == OS_WINDOWS)
// WARNING: Don't change these string. The server API relies on them.
switch (utility::getOsType())
{
threadCount -= 1;
case OS_WINDOWS:
return "windows";
case OS_MAC:
return "macOS";
case OS_LINUX:
return "linux";
default:
break;
}
return std::max(1, threadCount);
return "unknown";
}
bool utility::saveLicense(const License* license)
+4 -4
View File
@@ -15,17 +15,17 @@ namespace utility
std::string executeProcess(const std::string& command, const FilePath& workingDirectory = FilePath(), const int timeout = 30000);
std::string executeProcessUntilNoOutput(const std::string& command, const FilePath& workingDirectory, int waitTime = 10000);
int executeProcessAndGetExitCode(
const std::wstring& commandPath,
const std::vector<std::wstring>& commandArguments,
const FilePath& workingDirectory = FilePath(),
const std::wstring& commandPath,
const std::vector<std::wstring>& commandArguments,
const FilePath& workingDirectory = FilePath(),
const int timeout = 30000
);
void killRunningProcesses();
int getIdealThreadCount();
OsType getOsType();
std::string getOsTypeString();
bool saveLicense(const License* license);
}