summaryrefslogtreecommitdiffstats
path: root/examples/applicationmanager/softwarecontainer-plugin/softwarecontainer.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'examples/applicationmanager/softwarecontainer-plugin/softwarecontainer.cpp')
-rw-r--r--examples/applicationmanager/softwarecontainer-plugin/softwarecontainer.cpp585
1 files changed, 585 insertions, 0 deletions
diff --git a/examples/applicationmanager/softwarecontainer-plugin/softwarecontainer.cpp b/examples/applicationmanager/softwarecontainer-plugin/softwarecontainer.cpp
new file mode 100644
index 00000000..7e34bd86
--- /dev/null
+++ b/examples/applicationmanager/softwarecontainer-plugin/softwarecontainer.cpp
@@ -0,0 +1,585 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 Pelagicore AG
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Pelagicore Application Manager.
+**
+** $QT_BEGIN_LICENSE:LGPL-QTAS$
+** Commercial License Usage
+** Licensees holding valid commercial Qt Automotive Suite licenses may use
+** this file in accordance with the commercial license agreement provided
+** with the Software or, alternatively, in accordance with the terms
+** contained in a written agreement between you and The Qt Company. For
+** licensing terms and conditions see https://www.qt.io/terms-conditions.
+** For further information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+** SPDX-License-Identifier: LGPL-3.0
+**
+****************************************************************************/
+
+#include <tuple>
+
+#include <QtDBus/QtDBus>
+#include <QtAppManCommon/global.h>
+#include <QJsonDocument>
+#include <QSocketNotifier>
+#include <qplatformdefs.h>
+#include <unistd.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#include <sys/fcntl.h>
+#include "softwarecontainer.h"
+
+
+QT_BEGIN_NAMESPACE
+
+QDBusArgument &operator<<(QDBusArgument &argument, const QMap<QString,QString> &map)
+{
+ argument.beginMap(QMetaType::QString, QMetaType::QString);
+ for (auto it = map.cbegin(); it != map.cend(); ++it) {
+ argument.beginMapEntry();
+ argument << it.key() << it.value();
+ argument.endMapEntry();
+ }
+ argument.endMap();
+
+ return argument;
+}
+
+const QDBusArgument &operator>>(const QDBusArgument &argument, QMap<QString,QString> &map)
+{
+ argument.beginMap();
+ while (!argument.atEnd()) {
+ argument.beginMapEntry();
+ QString key, value;
+ argument >> key >> value;
+ map.insert(key, value);
+ argument.endMapEntry();
+ }
+ argument.endMap();
+
+ return argument;
+}
+
+QT_END_NAMESPACE
+
+
+QT_USE_NAMESPACE_AM
+
+// unfortunately, this is a copy of the code from debugwrapper.cpp
+static QStringList substituteCommand(const QStringList &debugWrapperCommand, const QString &program,
+ const QStringList &arguments)
+{
+ QString stringifiedArguments = arguments.join(qL1C(' '));
+ QStringList result;
+
+ for (const QString &s : debugWrapperCommand) {
+ if (s == qSL("%arguments%")) {
+ result << arguments;
+ } else {
+ QString str(s);
+ str.replace(qL1S("%program%"), program);
+ str.replace(qL1S("%arguments%"), stringifiedArguments);
+ result << str;
+ }
+ }
+ return result;
+}
+
+SoftwareContainerManager::SoftwareContainerManager()
+{
+ static bool once = false;
+ if (!once) {
+ once = true;
+ qDBusRegisterMetaType<QMap<QString, QString>>();
+ }
+}
+
+QString SoftwareContainerManager::identifier() const
+{
+ return QStringLiteral("softwarecontainer");
+}
+
+bool SoftwareContainerManager::supportsQuickLaunch() const
+{
+ return false;
+}
+
+void SoftwareContainerManager::setConfiguration(const QVariantMap &configuration)
+{
+ m_configuration = configuration;
+}
+
+ContainerInterface *SoftwareContainerManager::create(bool isQuickLaunch, const QVector<int> &stdioRedirections,
+ const QMap<QString, QString> &debugWrapperEnvironment,
+ const QStringList &debugWrapperCommand)
+{
+ if (!m_interface) {
+ QString dbus = configuration().value(QStringLiteral("dbus")).toString();
+ QDBusConnection conn(QStringLiteral("sc-bus"));
+
+ if (dbus.isEmpty())
+ dbus = QStringLiteral("system");
+
+ if (dbus == QLatin1String("system"))
+ conn = QDBusConnection::systemBus();
+ else if (dbus == QLatin1String("session"))
+ conn = QDBusConnection::sessionBus();
+ else
+ conn = QDBusConnection::connectToBus(dbus, QStringLiteral("sc-bus"));
+
+ if (!conn.isConnected()) {
+ qWarning() << "The" << dbus << "D-Bus is not available to connect to the SoftwareContainer agent.";
+ return nullptr;
+ }
+ m_interface = new QDBusInterface(QStringLiteral("com.pelagicore.SoftwareContainerAgent"),
+ QStringLiteral("/com/pelagicore/SoftwareContainerAgent"),
+ QStringLiteral("com.pelagicore.SoftwareContainerAgent"),
+ conn, this);
+ if (m_interface->lastError().isValid()) {
+ qWarning() << "Could not connect to com.pelagicore.SoftwareContainerAgent, "
+ "/com/pelagicore/SoftwareContainerAgent on the" << dbus << "D-Bus";
+ delete m_interface;
+ m_interface = nullptr;
+ return nullptr;
+ }
+
+ if (!connect(m_interface, SIGNAL(ProcessStateChanged(int,uint,bool,uint)), this, SLOT(processStateChanged(int,uint,bool,uint)))) {
+ qWarning() << "Could not connect to the com.pelagicore.SoftwareContainerAgent.ProcessStateChanged "
+ "signal on the" << dbus << "D-Bus";
+ delete m_interface;
+ m_interface = nullptr;
+ return nullptr;
+ }
+ }
+
+ QString config = qSL("[]");
+ QVariant v = configuration().value("createConfig");
+ if (v.isValid())
+ config = QString::fromUtf8(QJsonDocument::fromVariant(v).toJson(QJsonDocument::Compact));
+
+ QDBusMessage reply = m_interface->call(QDBus::Block, "Create", config);
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qWarning() << "SoftwareContainer failed to create a new container:" << reply.errorMessage()
+ << "(config was:" << config << ")";
+ return nullptr;
+ }
+
+ int containerId = reply.arguments().at(0).toInt();
+
+ if (containerId < 0) {
+ qCritical() << "SoftwareContainer failed to create a new container. (config was:" << config << ")";
+ return nullptr;
+ }
+
+ // calculate where to dump stdout/stderr
+ int outputFd = stdioRedirections.value(STDERR_FILENO, -1);
+ if (outputFd < 0)
+ outputFd = stdioRedirections.value(STDOUT_FILENO, -1);
+ if ((::fcntl(outputFd, F_GETFD) < 0) && (errno == EBADF))
+ outputFd = STDOUT_FILENO;
+
+ SoftwareContainer *container = new SoftwareContainer(this, isQuickLaunch, containerId,
+ outputFd, debugWrapperEnvironment,
+ debugWrapperCommand);
+ m_containers.insert(containerId, container);
+ connect(container, &QObject::destroyed, this, [this, containerId]() { m_containers.remove(containerId); });
+ return container;
+}
+
+QDBusInterface *SoftwareContainerManager::interface() const
+{
+ return m_interface;
+}
+
+QVariantMap SoftwareContainerManager::configuration() const
+{
+ return m_configuration;
+}
+
+void SoftwareContainerManager::processStateChanged(int containerId, uint processId, bool isRunning, uint exitCode)
+{
+ Q_UNUSED(processId)
+
+ SoftwareContainer *container = m_containers.value(containerId);
+ if (!container) {
+ qWarning() << "Received a processStateChanged signal for unknown container" << containerId;
+ return;
+ }
+
+ if (!isRunning)
+ container->containerExited(exitCode);
+}
+
+
+
+SoftwareContainer::SoftwareContainer(SoftwareContainerManager *manager, bool isQuickLaunch, int containerId,
+ int outputFd, const QMap<QString, QString> &debugWrapperEnvironment,
+ const QStringList &debugWrapperCommand)
+ : m_manager(manager)
+ , m_isQuickLaunch(isQuickLaunch)
+ , m_id(containerId)
+ , m_outputFd(outputFd)
+ , m_debugWrapperEnvironment(debugWrapperEnvironment)
+ , m_debugWrapperCommand(debugWrapperCommand)
+{ }
+
+SoftwareContainer::~SoftwareContainer()
+{
+ if (m_fifoFd >= 0)
+ QT_CLOSE(m_fifoFd);
+ if (!m_fifoPath.isEmpty())
+ ::unlink(m_fifoPath);
+ if (m_outputFd > STDERR_FILENO)
+ QT_CLOSE(m_outputFd);
+}
+
+SoftwareContainerManager *SoftwareContainer::manager() const
+{
+ return m_manager;
+}
+
+bool SoftwareContainer::attachApplication(const QVariantMap &application)
+{
+
+ // In normal launch attachApplication is called first, then the start()
+ // method is called. During quicklaunch start() is called first and then
+ // attachApplication. In this case we need to configure the container
+ // with any extra capabilities etc.
+
+ m_state = QProcess::Starting;
+ m_application = application;
+
+ m_hostPath = application.value(qSL("codeDir")).toString();
+ if (m_hostPath.isEmpty())
+ m_hostPath = QDir::currentPath();
+
+ m_appRelativeCodePath = application.value(qSL("codeFilePath")).toString();
+ m_containerPath = qSL("/app");
+
+ // If this is a quick launch instance, we need to renew the capabilities
+ // and send the bindmounts.
+ if (m_isQuickLaunch) {
+ if (!sendCapabilities())
+ return false;
+
+ if (!sendBindMounts())
+ return false;
+ }
+
+ m_ready = true;
+ emit ready();
+ return true;
+}
+
+QString SoftwareContainer::controlGroup() const
+{
+ return QString();
+}
+
+bool SoftwareContainer::setControlGroup(const QString &groupName)
+{
+ Q_UNUSED(groupName)
+ return false;
+}
+
+bool SoftwareContainer::setProgram(const QString &program)
+{
+ m_program = program;
+ return true;
+}
+
+void SoftwareContainer::setBaseDirectory(const QString &baseDirectory)
+{
+ m_baseDir = baseDirectory;
+}
+
+bool SoftwareContainer::isReady() const
+{
+ return m_ready;
+}
+
+QString SoftwareContainer::mapContainerPathToHost(const QString &containerPath) const
+{
+ return containerPath;
+}
+
+QString SoftwareContainer::mapHostPathToContainer(const QString &hostPath) const
+{
+ return hostPath;
+}
+
+bool SoftwareContainer::sendCapabilities()
+{
+ auto iface = manager()->interface();
+ if (!iface)
+ return false;
+
+ // this is the one and only capability in io.qt.ApplicationManager.Application.json
+ static const QStringList capabilities { qSL("io.qt.ApplicationManager.Application") };
+
+ QDBusMessage reply = iface->call(QDBus::Block, "SetCapabilities", m_id, QVariant::fromValue(capabilities));
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qWarning() << "SoftwareContainer failed to set capabilities to" << capabilities << ":" << reply.errorMessage();
+ return false;
+ }
+ return true;
+}
+
+bool SoftwareContainer::sendBindMounts()
+{
+ auto iface = manager()->interface();
+ if (!iface)
+ return false;
+
+ QFileInfo fontCacheInfo(qSL("/var/cache/fontconfig"));
+
+ QVector<std::tuple<QString, QString, bool>> bindMounts; // bool == isReadOnly
+ // the private P2P D-Bus
+ bindMounts.append(std::make_tuple(m_dbusP2PInfo.absoluteFilePath(), m_dbusP2PInfo.absoluteFilePath(), false));
+
+ // we need to share the fontconfig cache - otherwise the container startup might take a long time
+ bindMounts.append(std::make_tuple(fontCacheInfo.absoluteFilePath(), fontCacheInfo.absoluteFilePath(), false));
+
+ // the actual path to the application
+ bindMounts.append(std::make_tuple(m_hostPath, m_containerPath, true));
+
+ // for development only - mount the user's $HOME dir into the container as read-only. Otherwise
+ // you would have to `make install` the AM into /usr on every rebuild
+ if (manager()->configuration().value(QStringLiteral("bindMountHome")).toBool())
+ bindMounts.append(std::make_tuple(QDir::homePath(), QDir::homePath(), true));
+
+ // do all the bind-mounts in parallel to waste as little time as possible
+ QList<QDBusPendingReply<>> bindMountResults;
+
+ for (auto it = bindMounts.cbegin(); it != bindMounts.cend(); ++it)
+ bindMountResults << iface->asyncCall("BindMount", m_id, std::get<0>(*it), std::get<1>(*it), std::get<2>(*it));
+
+ for (const auto &pending : qAsConst(bindMountResults))
+ QDBusPendingCallWatcher(pending).waitForFinished();
+
+ for (int i = 0; i < bindMounts.size(); ++i) {
+ if (bindMountResults.at(i).isError()) {
+ qWarning() << "SoftwareContainer failed to bind-mount the directory" << std::get<0>(bindMounts.at(i))
+ << "into the container at" << std::get<1>(bindMounts.at(i)) << ":" << bindMountResults.at(i).error().message();
+ return false;
+ }
+ }
+
+ return true;
+
+}
+
+bool SoftwareContainer::start(const QStringList &arguments, const QMap<QString, QString> &runtimeEnvironment)
+{
+ auto iface = manager()->interface();
+ if (!iface)
+ return false;
+
+ if (!QFile::exists(m_program))
+ return false;
+
+ // Send initial capabilities even if this is a quick-launch instance
+ if (!sendCapabilities())
+ return false;
+
+ // parse out the actual socket file name from the DBus specification
+ QString dbusP2PSocket = runtimeEnvironment.value(QStringLiteral("AM_DBUS_PEER_ADDRESS"));
+ dbusP2PSocket = dbusP2PSocket.mid(dbusP2PSocket.indexOf(QLatin1Char('=')) + 1);
+ dbusP2PSocket = dbusP2PSocket.left(dbusP2PSocket.indexOf(QLatin1Char(',')));
+ QFileInfo dbusP2PInfo(dbusP2PSocket);
+ m_dbusP2PInfo = dbusP2PInfo;
+
+ // Only send the bindmounts if this is not a quick-launch instance.
+ if (!m_isQuickLaunch && !sendBindMounts())
+ return false;
+
+ // create an unique fifo name in /tmp
+ m_fifoPath = QDir::tempPath().toLocal8Bit() + "/sc-" + QUuid::createUuid().toByteArray().mid(1,36) + ".fifo";
+ if (mkfifo(m_fifoPath, 0600) != 0) {
+ qWarning() << "Failed to create FIFO at" << m_fifoPath << "to redirect stdout/stderr out of the container:" << strerror(errno);
+ m_fifoPath.clear();
+ return false;
+ }
+
+ // open fifo for reading
+ // due to QTBUG-15261 (bytesAvailable() on a fifo always returns 0) we use a raw fd
+ m_fifoFd = QT_OPEN(m_fifoPath, O_RDONLY | O_NONBLOCK);
+ if (m_fifoFd < 0) {
+ qWarning() << "Failed to open FIFO at" << m_fifoPath << "for reading:" << strerror(errno);
+ return false;
+ }
+
+ // read from fifo and dump to message handler
+ QSocketNotifier *sn = new QSocketNotifier(m_fifoFd, QSocketNotifier::Read, this);
+ int outputFd = m_outputFd;
+ connect(sn, &QSocketNotifier::activated, this, [sn, outputFd](int fifoFd) {
+ int bytesAvailable = 0;
+ if (ioctl(fifoFd, FIONREAD, &bytesAvailable) == 0) {
+ static const int bufferSize = 4096;
+ static QByteArray buffer(bufferSize, 0);
+
+ while (bytesAvailable > 0) {
+ auto bytesRead = QT_READ(fifoFd, buffer.data(), std::min(bytesAvailable, bufferSize));
+ if (bytesRead < 0) {
+ if (errno == EINTR || errno == EAGAIN)
+ continue;
+ sn->setEnabled(false);
+ return;
+ } else if (bytesRead > 0) {
+ (void) QT_WRITE(outputFd, buffer.constData(), bytesRead);
+ bytesAvailable -= bytesRead;
+ }
+ }
+ }
+ });
+
+ // Calculate the exact command to run
+ QStringList command;
+ if (!m_debugWrapperCommand.isEmpty()) {
+ command = substituteCommand(m_debugWrapperCommand, m_program, arguments);
+ } else {
+ command = arguments;
+ command.prepend(m_program);
+ }
+
+ // SC expects a plain string instead of individual args
+ QString cmdLine;
+ for (const auto &part : qAsConst(command)) {
+ if (!cmdLine.isEmpty())
+ cmdLine.append(QLatin1Char(' '));
+ cmdLine.append(QLatin1Char('\"'));
+ cmdLine.append(part);
+ cmdLine.append(QLatin1Char('\"'));
+ }
+ //cmdLine.prepend(QStringLiteral("/usr/bin/strace ")); // useful if things go wrong...
+
+ // we start with a copy of the AM's environment, but some variables would overwrite important
+ // redirections set by SC gateways.
+ static const QStringList forbiddenVars = {
+ qSL("XDG_RUNTIME_DIR"),
+ qSL("DBUS_SESSION_BUS_ADDRESS"),
+ qSL("DBUS_SYSTEM_BUS_ADDRESS"),
+ qSL("PULSE_SERVER")
+ };
+
+ // since we have to translate between a QProcessEnvironment and a QMap<>, we cache the result
+ static QMap<QString, QString> baseEnvVars;
+ if (baseEnvVars.isEmpty()) {
+ QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+ const auto keys = env.keys();
+ for (const auto key : keys) {
+ if (!key.isEmpty() && !forbiddenVars.contains(key))
+ baseEnvVars.insert(key, env.value(key));
+ }
+ }
+
+ QMap<QString, QString> envVars = baseEnvVars;
+
+ // set the env. variables coming from the runtime
+ for (auto it = runtimeEnvironment.cbegin(); it != runtimeEnvironment.cend(); ++it) {
+ if (it.value().isEmpty())
+ envVars.remove(it.key());
+ else
+ envVars.insert(it.key(), it.value());
+ }
+ // set the env. variables coming from a debug wrapper
+ for (auto it = m_debugWrapperEnvironment.cbegin(); it != m_debugWrapperEnvironment.cend(); ++it) {
+ if (it.value().isEmpty())
+ envVars.remove(it.key());
+ else
+ envVars.insert(it.key(), it.value());
+ }
+
+ QVariant venvVars = QVariant::fromValue(envVars);
+
+ qDebug () << "SoftwareContainer is trying to launch application" << m_id
+ << "\n * command ..." << cmdLine
+ << "\n * directory ." << m_containerPath
+ << "\n * output ...." << m_fifoPath
+ << "\n * environment" << envVars;
+
+#if 0
+ qWarning() << "Attach to container now!";
+ qWarning().nospace() << " sudo lxc-attach -n SC-" << m_id;
+ sleep(10000000);
+#endif
+
+ auto reply = iface->call(QDBus::Block, "Execute", m_id, cmdLine, m_containerPath, QString::fromLocal8Bit(m_fifoPath), venvVars);
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qWarning() << "SoftwareContainer failed to execute application" << m_id << "in directory" << m_containerPath << "in the container:" << reply.errorMessage();
+ return false;
+ }
+
+ m_pid = reply.arguments().at(0).value<int>();
+
+ m_state = QProcess::Running;
+ QTimer::singleShot(0, this, [this]() {
+ emit stateChanged(m_state);
+ emit started();
+ });
+ return true;
+}
+
+qint64 SoftwareContainer::processId() const
+{
+ return m_pid;
+}
+
+QProcess::ProcessState SoftwareContainer::state() const
+{
+ return m_state;
+}
+
+void SoftwareContainer::kill()
+{
+ auto iface = manager()->interface();
+
+ if (iface) {
+ QDBusMessage reply = iface->call(QDBus::Block, "Destroy", m_id);
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qWarning() << "SoftwareContainer failed to destroy container" << reply.errorMessage();
+ }
+
+ if (!reply.arguments().at(0).toBool()) {
+ qWarning() << "SoftwareContainer failed to destroy container.";
+ }
+ }
+}
+
+void SoftwareContainer::terminate()
+{
+ //TODO: handle graceful shutdown
+ kill();
+}
+
+void SoftwareContainer::containerExited(uint exitCode)
+{
+ m_state = QProcess::NotRunning;
+ emit stateChanged(m_state);
+ emit finished(WEXITSTATUS(exitCode), WIFEXITED(exitCode) ? QProcess::NormalExit : QProcess::CrashExit);
+ deleteLater();
+}