/** * Copyright (C) 2016, Canonical Ltd. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ // #define BUILD_FOR_BUNDLE #include #include #include #include #include #include #include #include #include #include #include #include "attachedproperties.h" #include "reactitem.h" #include "rootview.h" #include "utilities.h" #include "exceptionglobalhandler.h" Q_DECLARE_LOGGING_CATEGORY(JSSERVER) Q_DECLARE_LOGGING_CATEGORY(STATUS) Q_LOGGING_CATEGORY(JSSERVER, "jsserver") Q_LOGGING_CATEGORY(STATUS, "status") static QStringList consoleOutputStrings; static QMutex consoleOutputMutex; #ifdef BUILD_FOR_BUNDLE bool ubuntuServerStarted = false; QProcess *g_ubuntuServerProcess = nullptr; #endif const int MAIN_WINDOW_WIDTH = 1024; const int MAIN_WINDOW_HEIGHT = 768; const QString CRASH_REPORT_EXECUTABLE = QStringLiteral("reportApp"); const QString CRASH_REPORT_EXECUTABLE_RELATIVE_PATH = QStringLiteral("/../reportApp"); const char *ENABLE_LOG_FILE_ENV_VAR_NAME = "STATUS_LOG_FILE_ENABLED"; const char *LOG_FILE_PATH_ENV_VAR_NAME = "STATUS_LOG_PATH"; // TODO: some way to change while running class ReactNativeProperties : public QObject { Q_OBJECT Q_PROPERTY(bool liveReload READ liveReload WRITE setLiveReload NOTIFY liveReloadChanged) Q_PROPERTY(QUrl codeLocation READ codeLocation WRITE setCodeLocation NOTIFY codeLocationChanged) Q_PROPERTY(QString pluginsPath READ pluginsPath WRITE setPluginsPath NOTIFY pluginsPathChanged) Q_PROPERTY( QString executor READ executor WRITE setExecutor NOTIFY executorChanged) public: ReactNativeProperties(QObject *parent = nullptr) : QObject(parent) { m_codeLocation = m_packagerTemplate.arg(m_packagerHost).arg(m_packagerPort); } bool liveReload() const { return m_liveReload; } void setLiveReload(bool liveReload) { if (m_liveReload == liveReload) return; m_liveReload = liveReload; Q_EMIT liveReloadChanged(); } QUrl codeLocation() const { return m_codeLocation; } void setCodeLocation(const QUrl &codeLocation) { if (m_codeLocation == codeLocation) return; m_codeLocation = codeLocation; Q_EMIT codeLocationChanged(); } QString pluginsPath() const { return m_pluginsPath; } void setPluginsPath(const QString &pluginsPath) { if (m_pluginsPath == pluginsPath) return; m_pluginsPath = pluginsPath; Q_EMIT pluginsPathChanged(); } QString executor() const { return m_executor; } void setExecutor(const QString &executor) { if (m_executor == executor) return; m_executor = executor; Q_EMIT executorChanged(); } QString packagerHost() const { return m_packagerHost; } void setPackagerHost(const QString &packagerHost) { if (m_packagerHost == packagerHost) return; m_packagerHost = packagerHost; setCodeLocation(m_packagerTemplate.arg(m_packagerHost).arg(m_packagerPort)); } QString packagerPort() const { return m_packagerPort; } void setPackagerPort(const QString &packagerPort) { if (m_packagerPort == packagerPort) return; m_packagerPort = packagerPort; setCodeLocation(m_packagerTemplate.arg(m_packagerHost).arg(m_packagerPort)); } void setLocalSource(const QString &source) { if (m_localSource == source) return; // overrides packager* if (source.startsWith("file:")) { setCodeLocation(source); } else { QFileInfo fi(source); if (!fi.exists()) { qCWarning(STATUS) << "Attempt to set non-existent local source file"; return; } setCodeLocation(QUrl::fromLocalFile(fi.absoluteFilePath())); setLiveReload(false); } } Q_SIGNALS: void liveReloadChanged(); void codeLocationChanged(); void pluginsPathChanged(); void executorChanged(); private: bool m_liveReload = false; QString m_packagerHost = "localhost"; QString m_packagerPort = "8081"; QString m_localSource; QString m_packagerTemplate = "http://%1:%2/index.desktop.bundle?platform=desktop&dev=true"; QUrl m_codeLocation; QString m_pluginsPath; #ifdef BUILD_FOR_BUNDLE QString m_executor = "RemoteServerConnection"; #else QString m_executor = "LocalServerConnection"; #endif }; void saveMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg); void writeLogsToFile(); void writeLogFromJSServer(const QString &msg); void writeSingleLineLogFromJSServer(const QString &msg); #ifdef BUILD_FOR_BUNDLE void runUbuntuServer(); #endif void loadFontsFromResources() { QDirIterator it(":", QDirIterator::Subdirectories); while (it.hasNext()) { QString resourceFile = it.next(); if (resourceFile.endsWith(".otf", Qt::CaseInsensitive) || resourceFile.endsWith(".ttf", Qt::CaseInsensitive)) { QFontDatabase::addApplicationFont(resourceFile); } } } void exceptionPostHandledCallback() { #ifdef BUILD_FOR_BUNDLE if (g_ubuntuServerProcess) { g_ubuntuServerProcess->kill(); } #endif } bool redirectLogIntoFile() { #ifdef BUILD_FOR_BUNDLE return true; #else return qEnvironmentVariable(ENABLE_LOG_FILE_ENV_VAR_NAME, "") == QStringLiteral("1"); #endif } QString getDataStoragePath() { QString dataStoragePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); QDir dir(dataStoragePath); if (!dir.exists()) { dir.mkpath("."); } return dataStoragePath; } int main(int argc, char **argv) { QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); QCoreApplication::setApplicationName("Status"); QString appPath = QCoreApplication::applicationDirPath(); QString dataStoragePath = getDataStoragePath(); #ifndef BUILD_FOR_BUNDLE appPath.append(CRASH_REPORT_EXECUTABLE_RELATIVE_PATH); dataStoragePath = ""; #endif ExceptionGlobalHandler exceptionHandler( appPath + QDir::separator() + CRASH_REPORT_EXECUTABLE, exceptionPostHandledCallback, dataStoragePath); Q_INIT_RESOURCE(react_resources); loadFontsFromResources(); QLoggingCategory::setFilterRules(QStringLiteral("UIManager=false\nFlexbox=false\nViewManager=false\nNetworking=false\nWebSocketModule=false")); if (redirectLogIntoFile()) { qInstallMessageHandler(saveMessage); } #ifdef BUILD_FOR_BUNDLE runUbuntuServer(); app.setWindowIcon(QIcon(":/icon.png")); #endif QQuickView view; ReactNativeProperties *rnp = new ReactNativeProperties(&view); #ifdef BUILD_FOR_BUNDLE rnp->setCodeLocation("file:" + QGuiApplication::applicationDirPath() + "/assets"); #endif utilities::registerReactTypes(); QCommandLineParser p; p.setApplicationDescription("React Native host application"); p.addHelpOption(); p.addOptions({ {{"R", "live-reload"}, "Enable live reload."}, {{"H", "host"}, "Set packager host address.", rnp->packagerHost()}, {{"P", "port"}, "Set packager port number.", rnp->packagerPort()}, {{"L", "local"}, "Set path to the local packaged source", "not set"}, {{"M", "plugins-path"}, "Set path to node modules", "./plugins"}, {{"E", "executor"}, "Set Javascript executor", rnp->executor()}, }); p.process(app); rnp->setLiveReload(p.isSet("live-reload")); if (p.isSet("host")) rnp->setPackagerHost(p.value("host")); if (p.isSet("port")) rnp->setPackagerPort(p.value("port")); if (p.isSet("local")) rnp->setLocalSource(p.value("local")); if (p.isSet("plugins-path")) rnp->setPluginsPath(p.value("plugins-path")); if (p.isSet("executor")) rnp->setExecutor(p.value("executor")); view.rootContext()->setContextProperty("ReactNativeProperties", rnp); view.setSource(QUrl("qrc:///main.qml")); view.setResizeMode(QQuickView::SizeRootObjectToView); view.resize(MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT); view.show(); QTimer flushLogsToFileTimer; if (redirectLogIntoFile()) { flushLogsToFileTimer.setInterval(500); QObject::connect(&flushLogsToFileTimer, &QTimer::timeout, [=]() { writeLogsToFile(); }); flushLogsToFileTimer.start(); } return app.exec(); } QString getLogFilePath() { QString logFilePath; #ifdef BUILD_FOR_BUNDLE logFilePath = getDataStoragePath() + "/Status.log"; #else logFilePath = qEnvironmentVariable(LOG_FILE_PATH_ENV_VAR_NAME, ""); if (logFilePath.isEmpty()) { logFilePath = getDataStoragePath() + "/StatusDev.log"; } #endif return logFilePath; } void writeLogsToFile() { QMutexLocker locker(&consoleOutputMutex); QFile logFile(getLogFilePath()); if (logFile.open(QIODevice::WriteOnly | QIODevice::Append)) { for (QString message : consoleOutputStrings) { logFile.write(message.toStdString().c_str()); } consoleOutputStrings.clear(); logFile.flush(); logFile.close(); } } #ifdef BUILD_FOR_BUNDLE void runUbuntuServer() { g_ubuntuServerProcess = new QProcess(); g_ubuntuServerProcess->setWorkingDirectory(getDataStoragePath()); g_ubuntuServerProcess->setProgram(QGuiApplication::applicationDirPath() + "/ubuntu-server"); QObject::connect(g_ubuntuServerProcess, &QProcess::errorOccurred, [=](QProcess::ProcessError) { qCWarning(JSSERVER) << "process name: " << qUtf8Printable(g_ubuntuServerProcess->program()); qCWarning(JSSERVER) << "process error: " << qUtf8Printable(g_ubuntuServerProcess->errorString()); }); QObject::connect( g_ubuntuServerProcess, &QProcess::readyReadStandardOutput, [=] { writeLogFromJSServer(g_ubuntuServerProcess->readAllStandardOutput().trimmed()); }); QObject::connect( g_ubuntuServerProcess, &QProcess::readyReadStandardError, [=] { QString output = g_ubuntuServerProcess->readAllStandardError().trimmed(); writeLogFromJSServer(output); if (output.contains("Server starting")) { ubuntuServerStarted = true; } }); QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, [=]() { qCDebug(STATUS) << "Kill ubuntu server"; g_ubuntuServerProcess->kill(); }); qCDebug(STATUS) << "starting ubuntu server..."; g_ubuntuServerProcess->start(); qCDebug(STATUS) << "wait for started..."; while (!ubuntuServerStarted) { QGuiApplication::processEvents(); } qCDebug(STATUS) << "waiting finished"; } #endif void writeLogFromJSServer(const QString &msg) { if (msg.contains("\\n")) { QStringList lines = msg.split("\\n"); foreach (const QString &line, lines) { writeSingleLineLogFromJSServer(line); } } else { writeSingleLineLogFromJSServer(msg); } } QString extractJSServerMessage(const QString& msg, int prefixLength) { return msg.mid(prefixLength); } void writeSingleLineLogFromJSServer(const QString &msg) { if (msg.startsWith("TRACE ")) qCDebug(JSSERVER) << qUtf8Printable(extractJSServerMessage(msg, 6)); else if (msg.startsWith("DEBUG ")) qCDebug(JSSERVER) << qUtf8Printable(extractJSServerMessage(msg, 6)); else if (msg.startsWith("INFO ")) qCInfo(JSSERVER) << qUtf8Printable(extractJSServerMessage(msg, 5)); else if (msg.startsWith("WARN ")) qCWarning(JSSERVER) << qUtf8Printable(extractJSServerMessage(msg, 5)); else if (msg.startsWith("ERROR ")) qCWarning(JSSERVER) << qUtf8Printable(extractJSServerMessage(msg, 6)); else if (msg.startsWith("FATAL ")) qCCritical(JSSERVER) << qUtf8Printable(extractJSServerMessage(msg, 6)); else qCDebug(JSSERVER) << qUtf8Printable(msg); } void appendConsoleString(const QString &msg) { QMutexLocker locker(&consoleOutputMutex); consoleOutputStrings << msg; } void saveMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg) { Q_UNUSED(context); QByteArray localMsg = msg.toLocal8Bit(); QString message = localMsg + "\n"; QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss.zzz"); QString typeStr; switch (type) { case QtDebugMsg: typeStr = "D"; break; case QtInfoMsg: typeStr = "I"; break; case QtWarningMsg: typeStr = "W"; break; case QtCriticalMsg: typeStr = "C"; break; case QtFatalMsg: typeStr = "F"; } appendConsoleString(QString("%1 - %2 - [%3] - %4").arg(timestamp, typeStr, context.category, message)); if (type == QtFatalMsg) { writeLogsToFile(); abort(); } } #include "main.moc"