Updated the module interface to be less limiting

Moved away from QPluginLoader to C/C++ style QLibrary files for the
module interface. It's less limiting this way because the host can
be built statically and still beable to load non-statically built
modules. It's not tested but the i think it should now beable to
load modules built on a different version of Qt than the host but
the module would must likely need to be build statically.

Another change to the module interface makes it so the module can
can now also see the host version in CommandLoader::hostRevOk()
and decide if the host can continue loading it.

The host 'host_info' internal command now displays the version of
Qt it was built on.

All documentation related to the module interface has been
updated. No attempt will be made at the host side to support rev
2 modules so the new minimum rev is now 3.

1.0.2 --> 1.1.2
This commit is contained in:
Maurice O'Neal 2019-09-22 22:01:07 -04:00
parent 6c8867ad5c
commit 61c2f8b438
13 changed files with 144 additions and 74 deletions

View File

@ -20,17 +20,17 @@ This funtion needs to work the same way as ```cmdList()``` except the loader can
```QStringList rankExemptList()``` ```QStringList rankExemptList()```
The loader can use this function to return a ```QStringList``` of all of command names that need to be exempt from host ranking system (section [5.2](Host_Features.md)). Commands listed here will be allowed to load/run regardless of what host rank the current user is. The loader can use this function to return a ```QStringList``` of all of command names that need to be exempt from host ranking system (section [5.2](Host_Features.md)). Commands listed here will be allowed to load/run regardless of what host rank the current user is.
```bool hostRevOk(quint64 rev)``` ```bool hostRevOk(quint64 importRev, quint16 hostMajor, quint16 hostMinor, quint16 hostPatch)```
When the host calls this function, it will pass the import rev that it supports in the ```quint64``` parameter. Use this function to return if this rev is acceptable or not. The host will give up loading the module if the rev is not acceptable. When the host calls this function, it will pass the import rev that it supports in the ```quint64``` parameter along with it's own version number in the next 3 ```quint16``` parameters. Use this function to return if this rev or host version is acceptable or not. The host will give up loading the module if it is not acceptable.
```quint64 rev()``` ```quint64 rev()```
Use this function to return the import rev that this module supports. The host will decide if it is acceptable or not. Use this function to return the minimum import rev that the module supports. The host will decide if it is acceptable or not.
```QString lastError()``` ```QString lastError()```
The host will call this function if a command object fails to load or if ```hostRevOk()``` returns false so it can log the error message returned by it to the host database. The host will call this function if a command object fails to load or if ```hostRevOk()``` returns false so it can log the error message returned by it to the host database.
```void modPath(QString path)``` ```void modPath(QString path)```
The host will call this function after successfully negotiating the import rev. The ```QString``` parameter passed into this will have the absolute path to the module's install directory. You can use this path to load additional files that came bundled the module. The host will call this function after successfully negotiating the import rev. The ```QString``` parameter passed into this will have the absolute path to the module's install directory. You can use this path to load additional files that came bundled with the module.
```void aboutToDelete()``` ```void aboutToDelete()```
The host will call this function before calling ```deleteLater()```. All command objects at this point should already be deleted, use this opportunity to free any resources related to the loader itself. Unload any additional lib files that the loader may have used. The host will call this function before calling ```deleteLater()```. All command objects at this point should already be deleted, use this opportunity to free any resources related to the loader itself. Unload any additional lib files that the loader may have used.
@ -42,12 +42,16 @@ Here's a few notes to consider when using this class:
### 2.3 Modules ### ### 2.3 Modules ###
External commands are added to the host through modules based on low level [QT plugins](https://doc.qt.io/qt-5/plugins-howto.html). Each module must define a ```CommandLoader``` class in it's main library file and the file itself must be named 'main' with a library file extension that the host platform supports (main.so, main.dll, etc..). Modules are installed using the *add_mod* internal command that supports extracting the module's library files from an archive file (.zip, .tar, etc...) or just a single library file (.so, .dll, .a, etc...). External commands are added to the host through modules based on C/C++ style shared library files; see [this](https://doc.qt.io/qt-5/sharedlibrary.html) to learn how to create shared library files using the Qt API. The library must export a function named ```hostImport``` that returns a pointer to a new ```CommandLoader``` object when it is called by the host. Example:
In the case of an archive file, it extracts all of the files from the the archive file while preserving the directory tree so you can bundle additional files that your module depends on but as mentioned before, a library file named 'main' must be present on the base directory. ```extern "C" LIB_EXPORT CommandLoader *hostImport();```
The ```CommandLoader``` returned by this function must never get deleted at anytime; the host will handle it's life cycle externally. Modules are installed using the *add_mod* internal command that supports extracting the module's library files from an archive file (.zip, .tar, etc...) or just a single library file (.so, .dll, .a, etc...).
In the case of an archive file, the host will extract all of the files from it while preserving the directory tree so you can bundle additional files that your module might depend on; however, the main library file that contains the ```hostImport``` function must be named 'main' (main.so, main.dll, etc..) and must be present in the root directory of the archive.
A template and an example of a module can be found in the 'modules/Tester' directory of the source code of this project. It provides the command.cpp and command.h files that contain the ```CommandLoader``` and ```ExternCommand``` classes that are needed to create a module. Also feel free to copy the command.cpp and command.h files from 'src/commands' if you prefer. A template and an example of a module can be found in the 'modules/Tester' directory of the source code of this project. It provides the command.cpp and command.h files that contain the ```CommandLoader``` and ```ExternCommand``` classes that are needed to create a module. Also feel free to copy the command.cpp and command.h files from 'src/commands' if you prefer.
### 2.4 The Import Rev ### ### 2.4 The Import Rev ###
The import rev is a single digit versioning system for external modules that help the host determine if it is compatible with the module it is attempting to load or not. Bumps to this rev is usually triggered by significant changes to the ```CommandLoader``` class. Compatibility negotiation is a two way communication between the host ```CmdExecutor``` and the module itself using the virtual functions described in section 2.2. The import rev is a single digit versioning system for external modules that help the host determine if it is compatible with the module it is attempting to load or not. Bumps to this rev is usually triggered by significant changes to the ```CommandLoader``` class or the method at which the host imports this object. Compatibility negotiation is a two way communication between the host ```CmdExecutor``` and the module itself using the virtual functions described in section 2.2.

View File

@ -68,7 +68,7 @@ makeself
Linux_build.sh is a custom script designed to build this project from the source code using qmake, make and makeself. You can pass 2 optional arguments: Linux_build.sh is a custom script designed to build this project from the source code using qmake, make and makeself. You can pass 2 optional arguments:
1. The path to the QT bin folder in case you want to compile with a QT install not defined in PATH. 1. The path to the QT bin folder in case you want to compile with a QT install not defined in PATH.
2. Path of the output makeself file (usually has a .run extension). If not given, the outfile will be named mrci-1.0.1.run in the source code folder. 2. Path of the output makeself file (usually has a .run extension). If not given, the outfile will be named mrci-1.1.2.run in the source code folder.
Build: Build:
``` ```
@ -77,7 +77,7 @@ sh ./linux_build.sh
``` ```
Install: Install:
``` ```
chmod +x ./mrci-1.0.1.run chmod +x ./mrci-1.1.2.run
./mrci-1.0.1.run ./mrci-1.0.1.run
``` ```

View File

@ -5,7 +5,7 @@ installer_file="$2"
src_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" src_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
bin_name="mrci" bin_name="mrci"
app_version="1.0.0" app_version="1.1.2"
app_name="MRCI" app_name="MRCI"
install_dir="/opt/$bin_name" install_dir="/opt/$bin_name"
bin_dir="/usr/bin" bin_dir="/usr/bin"

View File

@ -2,20 +2,20 @@
# #
# Project created by QtCreator 2017-06-26T15:36:12 # Project created by QtCreator 2017-06-26T15:36:12
# #
# This file is part of MCI_Host. # This file is part of MRCI.
# #
# MCI_Host is free software: you can redistribute it and/or modify # MRCI is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# MCI_Host is distributed in the hope that it will be useful, # MRCI is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with MCI_Host under the GPL.txt file. If not, see # along with MRCI under the LICENSE.md file. If not, see
# <http://www.gnu.org/licenses/>. # <http://www.gnu.org/licenses/>.
# #
#------------------------------------------------- #-------------------------------------------------
@ -25,6 +25,8 @@ QT -= gui
TARGET = ModTester TARGET = ModTester
TEMPLATE = lib TEMPLATE = lib
DEFINES += MOD_TESTER
SOURCES += \ SOURCES += \
command.cpp \ command.cpp \
main.cpp main.cpp

View File

@ -152,15 +152,15 @@ public:
virtual ~CommandLoader() {} virtual ~CommandLoader() {}
virtual void modPath(const QString &) {} virtual void modPath(const QString &) {}
virtual void aboutToDelete() {} virtual void aboutToDelete() {}
virtual bool hostRevOk(quint64) {return false;} virtual bool hostRevOk(quint64, quint16, quint16, quint16) {return false;}
virtual QString lastError() {return "";} virtual QString lastError() {return "";}
virtual quint64 rev() {return 0;} virtual quint64 rev() {return 0;}
virtual QStringList pubCmdList() {return QStringList();} virtual QStringList pubCmdList() {return QStringList();}
virtual QStringList cmdList() {return QStringList();} virtual QStringList cmdList() {return QStringList();}
virtual QStringList rankExemptList() {return QStringList();} virtual QStringList rankExemptList() {return QStringList();}
virtual ExternCommand *cmdObj(const QString &) {return nullptr;} virtual ExternCommand *cmdObj(const QString &) {return nullptr;}
}; };
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE

View File

@ -16,6 +16,11 @@
// along with MRCI under the LICENSE.md file. If not, see // along with MRCI under the LICENSE.md file. If not, see
// <http://www.gnu.org/licenses/>. // <http://www.gnu.org/licenses/>.
CommandLoader *hostImport()
{
return new Loader();
}
QString libName() QString libName()
{ {
return QString(LIB_NAME) + "_" + QString(LIB_VERSION); return QString(LIB_NAME) + "_" + QString(LIB_VERSION);
@ -25,9 +30,30 @@ Loader::Loader(QObject *parent) : CommandLoader(parent)
{ {
} }
bool Loader::hostRevOk(quint64 minRev) bool Loader::hostRevOk(quint64 minRev, quint16 vMajor, quint16 vMinor, quint16 vPatch)
{ {
return minRev >= IMPORT_REV; Q_UNUSED(vPatch)
bool ret = false;
if (minRev < IMPORT_REV)
{
err = "This module requires the host to supprt minimum import rev " + QString::number(IMPORT_REV) + " or higher.";
}
else if (vMajor != 1)
{
err = "Host major " + QString::number(vMajor) + " not supported. expected 1.";
}
else if (vMinor < 1)
{
err = "Host minor " + QString::number(vMinor) + " not supported. expected 1 or higher.";
}
else
{
ret = true;
}
return ret;
} }
quint64 Loader::rev() quint64 Loader::rev()
@ -40,6 +66,11 @@ QStringList Loader::cmdList()
return QStringList() << "test_text" << "test_input" << "test_loop" << "test_inherit"; return QStringList() << "test_text" << "test_input" << "test_loop" << "test_inherit";
} }
QString Loader::lastError()
{
return err;
}
ExternCommand *Loader::cmdObj(const QString &name) ExternCommand *Loader::cmdObj(const QString &name)
{ {
ExternCommand *ret = nullptr; ExternCommand *ret = nullptr;
@ -48,6 +79,10 @@ ExternCommand *Loader::cmdObj(const QString &name)
else if (name == "test_input") ret = new ModInput(this); else if (name == "test_input") ret = new ModInput(this);
else if (name == "test_loop") ret = new ModLoop(this); else if (name == "test_loop") ret = new ModLoop(this);
else if (name == "test_inherit") ret = new ModInherit(this); else if (name == "test_inherit") ret = new ModInherit(this);
else
{
err = "Command name '" + name + "' does not exists in this module. (" + QString(LIB_NAME) + ")";
}
return ret; return ret;
} }

View File

@ -23,7 +23,7 @@
#include "command.h" #include "command.h"
#define IMPORT_REV 2 #define IMPORT_REV 3
// the import revision is a module compatibility version number // the import revision is a module compatibility version number
// used by the host to determine if it can successfully load and // used by the host to determine if it can successfully load and
@ -36,20 +36,31 @@
// the versioning system for the library itself can be completely // the versioning system for the library itself can be completely
// different from the host import revision. // different from the host import revision.
#if defined(MOD_TESTER)
# define MOD_TESTER_EXPORT Q_DECL_EXPORT
#else
# define MOD_TESTER_EXPORT Q_DECL_IMPORT
#endif
extern "C" MOD_TESTER_EXPORT CommandLoader *hostImport();
QString libName(); QString libName();
class Loader : public CommandLoader class Loader : public CommandLoader
{ {
Q_OBJECT Q_OBJECT
Q_PLUGIN_METADATA(IID "MRCI.host.module")
Q_INTERFACES(CommandLoader) private:
QString err;
public: public:
bool hostRevOk(quint64 minRev); bool hostRevOk(quint64 minRev, quint16 vMajor, quint16 vMinor, quint16 vPatch);
quint64 rev(); quint64 rev();
ExternCommand *cmdObj(const QString &name); ExternCommand *cmdObj(const QString &name);
QStringList cmdList(); QStringList cmdList();
QString lastError();
explicit Loader(QObject *parent = nullptr); explicit Loader(QObject *parent = nullptr);
}; };

View File

@ -305,6 +305,7 @@ void CmdExecutor::close()
for (auto cmdLoader : cmdLoaders.values()) for (auto cmdLoader : cmdLoaders.values())
{ {
cmdLoader->aboutToDelete(); cmdLoader->aboutToDelete();
cmdLoader->deleteLater();
} }
for (auto plugin : plugins.values()) for (auto plugin : plugins.values())
@ -395,49 +396,58 @@ QString CmdExecutor::getModFile(const QString &modName)
} }
void CmdExecutor::loadModFile(const QString &modName) void CmdExecutor::loadModFile(const QString &modName)
{ {
bool modOk = false; bool modOk = false;
QString path = getModFile(modName); QStringList ver = QCoreApplication::applicationVersion().split('.');
auto *pluginLoader = new QPluginLoader(path); QString mainFilePath = getModFile(modName);
QObject *cmdLoaderObj = pluginLoader->instance(); QString path = QFileInfo(mainFilePath).path();
CommandLoader *cmdLoader = qobject_cast<CommandLoader*>(cmdLoaderObj); auto *lib = new QLibrary(mainFilePath, this);
if (!pluginLoader->isLoaded()) lib->load();
ModImportFunc importFunc = reinterpret_cast<ModImportFunc>(lib->resolve(MOD_IMPORT_FUNC));
if (!importFunc)
{ {
qDebug() << "CmdExecutor::loadModFile() err: failed to load mod lib file: " << path << " reason: " << pluginLoader->errorString(); qDebug() << "CmdExecutor::loadModFile() err: failed to load mod lib file: " << mainFilePath << " reason: " << lib->errorString();
}
else if (!cmdLoaderObj)
{
qDebug() << "CmdExecutor::loadModFile() err: failed to load mod lib file: " << path << " reason: the root component object could not be instantiated.";
}
else if (!cmdLoader)
{
qDebug() << "CmdExecutor::loadModFile() err: failed to load mod lib file: " << path << " reason: the ModCommandLoader object could not be instantiated.";
}
else if (cmdLoader->rev() < IMPORT_REV)
{
qDebug() << "CmdExecutor::loadModFile() err: failed to load mod lib file: " << path << " reason: module import rev " << cmdLoader->rev() << " not compatible with host rev " << IMPORT_REV << ".";
}
else if (!cmdLoader->hostRevOk(IMPORT_REV))
{
qDebug() << "CmdExecutor::loadModFile() err: failed to load mod lib file: " << path << " the module rejected the host import rev. reason: " << cmdLoader->lastError() << ".";
} }
else else
{ {
modOk = true; wrCrashDebugInfo(" exe func: loadModFile()\n path: " + mainFilePath + " \nmod name: " + modName + " \nnote: calling the module's import function.");
wrCrashDebugInfo(" exe func: loadModFile()\n path: " + path + " \nmod name: " + modName + " \nnote: calling the module's modPath() function."); CommandLoader *cmdLoader = importFunc();
cmdLoader->modPath(QFileInfo(path).path()); wrCrashDebugInfo(" exe func: loadModFile()\n path: " + mainFilePath + " \nmod name: " + modName + " \nnote: running import rev negotiations.");
cmdLoaders.insert(modName, cmdLoader); if (!cmdLoader)
plugins.insert(modName, pluginLoader); {
qDebug() << "CmdExecutor::loadModFile() err: failed to load mod lib file: " << mainFilePath << " reason: the CommandLoader object returned by the import function is null.";
}
else if (cmdLoader->rev() < IMPORT_REV)
{
qDebug() << "CmdExecutor::loadModFile() err: failed to load mod lib file: " << mainFilePath << " reason: module import rev " << cmdLoader->rev() << " not compatible with host rev " << IMPORT_REV << ".";
}
else if (!cmdLoader->hostRevOk(IMPORT_REV, ver[0].toUShort(), ver[1].toUShort(), ver[2].toUShort()))
{
qDebug() << "CmdExecutor::loadModFile() err: failed to load mod lib file: " << mainFilePath << " the module rejected the host rev/version. reason: " << cmdLoader->lastError() << ".";
}
else
{
modOk = true;
wrCrashDebugInfo(" exe func: loadModFile()\n path: " + mainFilePath + " \nmod name: " + modName + " \nnote: calling the module's modPath() function.");
cmdLoader->modPath(path);
cmdLoaders.insert(modName, cmdLoader);
plugins.insert(modName, lib);
}
} }
if (!modOk) if (!modOk)
{ {
pluginLoader->unload(); lib->unload();
pluginLoader->deleteLater(); lib->deleteLater();
} }
} }
@ -447,9 +457,13 @@ void CmdExecutor::unloadModFile(const QString &modName)
{ {
termCommandsInList(cmdIdsByModName[modName], true); termCommandsInList(cmdIdsByModName[modName], true);
wrCrashDebugInfo(" exe func: unloadModFile()\n mod name: " + modName + "\n note: calling the modules's aboutToDelete()"); wrCrashDebugInfo(" exe func: unloadModFile()\n mod name: " + modName + "\n note: calling the modules's aboutToDelete().");
cmdLoaders[modName]->aboutToDelete(); cmdLoaders[modName]->aboutToDelete();
cmdLoaders[modName]->deleteLater();
wrCrashDebugInfo(" exe func: unloadModFile()\n mod name: " + modName + "\n note: calling the modules's library unload function.");
plugins[modName]->unload(); plugins[modName]->unload();
plugins[modName]->deleteLater(); plugins[modName]->deleteLater();

View File

@ -20,6 +20,8 @@
#include "common.h" #include "common.h"
#include "int_loader.h" #include "int_loader.h"
typedef CommandLoader *(*ModImportFunc)();
class CmdExecutor : public QObject class CmdExecutor : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -31,7 +33,7 @@ private:
InternalCommandLoader *internalCmds; InternalCommandLoader *internalCmds;
QSharedMemory *exeDebugInfo; QSharedMemory *exeDebugInfo;
QHash<QString, CommandLoader*> cmdLoaders; QHash<QString, CommandLoader*> cmdLoaders;
QHash<QString, QPluginLoader*> plugins; QHash<QString, QLibrary*> plugins;
QHash<QString, QList<quint16> > cmdIdsByModName; QHash<QString, QList<quint16> > cmdIdsByModName;
QList<quint16> moreInputCmds; QList<quint16> moreInputCmds;
QList<quint16> activeLoopCmds; QList<quint16> activeLoopCmds;

View File

@ -152,15 +152,15 @@ public:
virtual ~CommandLoader() {} virtual ~CommandLoader() {}
virtual void modPath(const QString &) {} virtual void modPath(const QString &) {}
virtual void aboutToDelete() {} virtual void aboutToDelete() {}
virtual bool hostRevOk(quint64) {return false;} virtual bool hostRevOk(quint64, quint16, quint16, quint16) {return false;}
virtual QString lastError() {return "";} virtual QString lastError() {return "";}
virtual quint64 rev() {return 0;} virtual quint64 rev() {return 0;}
virtual QStringList pubCmdList() {return QStringList();} virtual QStringList pubCmdList() {return QStringList();}
virtual QStringList cmdList() {return QStringList();} virtual QStringList cmdList() {return QStringList();}
virtual QStringList rankExemptList() {return QStringList();} virtual QStringList rankExemptList() {return QStringList();}
virtual ExternCommand *cmdObj(const QString &) {return nullptr;} virtual ExternCommand *cmdObj(const QString &) {return nullptr;}
}; };
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE

View File

@ -130,6 +130,7 @@ void HostInfo::procBin(const SharedObjs *sharedObjs, const QByteArray &binIn, uc
QTextStream txtOut(&txt); QTextStream txtOut(&txt);
txtOut << "Application: " << QCoreApplication::applicationName() << " v" << QCoreApplication::applicationVersion() << " " << QSysInfo::WordSize << "Bit" << endl; txtOut << "Application: " << QCoreApplication::applicationName() << " v" << QCoreApplication::applicationVersion() << " " << QSysInfo::WordSize << "Bit" << endl;
txtOut << "Qt Base: " << QT_VERSION_STR << endl;
txtOut << "Import Rev: " << IMPORT_REV << endl; txtOut << "Import Rev: " << IMPORT_REV << endl;
txtOut << "Host Name: " << QSysInfo::machineHostName() << endl; txtOut << "Host Name: " << QSysInfo::machineHostName() << endl;
txtOut << "Host OS: " << QSysInfo::prettyProductName() << endl; txtOut << "Host OS: " << QSysInfo::prettyProductName() << endl;

View File

@ -40,7 +40,6 @@
#include <QSysInfo> #include <QSysInfo>
#include <QFileInfoList> #include <QFileInfoList>
#include <QTemporaryFile> #include <QTemporaryFile>
#include <QPluginLoader>
#include <QChar> #include <QChar>
#include <QtMath> #include <QtMath>
#include <QStorageInfo> #include <QStorageInfo>
@ -67,6 +66,7 @@
#include <QTcpSocket> #include <QTcpSocket>
#include <QMessageLogContext> #include <QMessageLogContext>
#include <QtGlobal> #include <QtGlobal>
#include <QLibrary>
#include "db.h" #include "db.h"
#include "shell.h" #include "shell.h"
@ -74,7 +74,7 @@
#define FRAME_HEADER_SIZE 6 #define FRAME_HEADER_SIZE 6
#define MAX_FRAME_BITS 24 #define MAX_FRAME_BITS 24
#define IMPORT_REV 2 #define IMPORT_REV 3
#define LOCAL_BUFFSIZE 16777215 #define LOCAL_BUFFSIZE 16777215
#define CLIENT_INIT_TIME 5000 #define CLIENT_INIT_TIME 5000
#define IPC_PREP_TIME 1000 #define IPC_PREP_TIME 1000
@ -84,6 +84,7 @@
#define EXE_CRASH_LIMIT 5 #define EXE_CRASH_LIMIT 5
#define EXE_DEBUG_INFO_SIZE 512 #define EXE_DEBUG_INFO_SIZE 512
#define SERVER_HEADER_TAG "MRCI" #define SERVER_HEADER_TAG "MRCI"
#define MOD_IMPORT_FUNC "hostImport"
#define ASYNC_RDY 1 #define ASYNC_RDY 1
#define ASYNC_SYS_MSG 2 #define ASYNC_SYS_MSG 2

View File

@ -37,7 +37,7 @@
#include "shell.h" #include "shell.h"
#define APP_NAME "MRCI" #define APP_NAME "MRCI"
#define APP_VER "1.0.2" #define APP_VER "1.1.2"
#define APP_TARGET "mrci" #define APP_TARGET "mrci"
#ifdef Q_OS_WIN #ifdef Q_OS_WIN