diff --git a/debian/control b/debian/control index 5be8fc63..ead887ba 100644 --- a/debian/control +++ b/debian/control @@ -461,6 +461,22 @@ Description: guh.io plugin for senic This package will install the guh.io plugin for senic +Package: guh-plugin-snapd +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + guh-plugins-translations, +Description: guh.io plugin for snapd + The guh daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the guh.io plugin for snapd + + + Package: guh-plugins-translations Section: misc diff --git a/debian/guh-plugin-snapd.install.in b/debian/guh-plugin-snapd.install.in new file mode 100644 index 00000000..281b705a --- /dev/null +++ b/debian/guh-plugin-snapd.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/guh/plugins/libguh_devicepluginsnapd.so diff --git a/guh-plugins.pro b/guh-plugins.pro index 2540de02..3e46678f 100644 --- a/guh-plugins.pro +++ b/guh-plugins.pro @@ -31,6 +31,7 @@ PLUGIN_DIRS = \ denon \ avahimonitor \ gpio \ + snapd \ CONFIG+=all diff --git a/snapd/devicepluginsnapd.cpp b/snapd/devicepluginsnapd.cpp new file mode 100644 index 00000000..60ed3802 --- /dev/null +++ b/snapd/devicepluginsnapd.cpp @@ -0,0 +1,259 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2017 Simon Stürz . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "devicepluginsnapd.h" +#include "plugin/device.h" +#include "plugininfo.h" +#include "network/networkaccessmanager.h" + +DevicePluginSnapd::DevicePluginSnapd() +{ + +} + +void DevicePluginSnapd::init() +{ + // Check advanced mode + m_advancedMode = configValue(advancedModeParamTypeId).toBool(); + connect(this, &DevicePluginSnapd::configValueChanged, this, &DevicePluginSnapd::onPluginConfigurationChanged); + + // Setup timers + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_refreshTimer, &PluginTimer::timeout, this, &DevicePluginSnapd::onRefreshTimer); + + // Check all 5 min if there is an update available + m_updateTimer = hardwareManager()->pluginTimerManager()->registerTimer(300); + connect(m_refreshTimer, &PluginTimer::timeout, this, &DevicePluginSnapd::onUpdateTimer); +} + +void DevicePluginSnapd::startMonitoringAutoDevices() +{ + // Check if we already have a controller and if snapd is available + if (m_snapdControl && !m_snapdControl->available()) { + return; + } + + // Check if we already have a device for the snapd control + bool deviceAlreadyExists = false; + foreach (Device *device, myDevices()) { + if (device->deviceClassId() == snapdControlDeviceClassId) { + deviceAlreadyExists = true; + } + } + + // Add device if there isn't already one + if (!deviceAlreadyExists) { + DeviceDescriptor descriptor(snapdControlDeviceClassId, "Update manager"); + emit autoDevicesAppeared(snapdControlDeviceClassId, QList() << descriptor); + } +} + +void DevicePluginSnapd::postSetupDevice(Device *device) +{ + if (m_snapdControl && m_snapdControl->device() == device) + m_snapdControl->update(); + +} + +void DevicePluginSnapd::deviceRemoved(Device *device) +{ + if (device->deviceClassId() == snapdControlDeviceClassId) { + qCWarning(dcSnapd()) << "User deleted snapd control."; + + // Clean up old device + if (m_snapdControl) { + delete m_snapdControl; + m_snapdControl = nullptr; + } + + // Respawn device immediately + startMonitoringAutoDevices(); + + } else if (device->deviceClassId() == snapDeviceClassId) { + if (m_snapDevices.values().contains(device)) { + QString snapId = m_snapDevices.key(device); + m_snapDevices.remove(snapId); + } + } +} + +DeviceManager::DeviceSetupStatus DevicePluginSnapd::setupDevice(Device *device) +{ + qCDebug(dcSnapd()) << "Setup" << device->name() << device->params(); + + if (device->deviceClassId() == snapdControlDeviceClassId) { + if (m_snapdControl) { + delete m_snapdControl; + m_snapdControl = nullptr; + } + + m_snapdControl = new SnapdControl(device, this); + connect(m_snapdControl, &SnapdControl::snapListUpdated, this, &DevicePluginSnapd::onSnapListUpdated); + + } else if (device->deviceClassId() == snapDeviceClassId) { + device->setName(QString("%1").arg(device->paramValue(snapNameParamTypeId).toString())); + m_snapDevices.insert(device->paramValue(snapIdParamTypeId).toString(), device); + } + + return DeviceManager::DeviceSetupStatusSuccess; +} + +DeviceManager::DeviceError DevicePluginSnapd::executeAction(Device *device, const Action &action) { + + if (device->deviceClassId() == snapdControlDeviceClassId) { + + if (!m_snapdControl) { + qCDebug(dcSnapd()) << "There is currently no snapd controller."; + return DeviceManager::DeviceErrorHardwareFailure; + } + + if (!m_snapdControl->connected()) { + qCDebug(dcSnapd()) << "Snapd controller not connected to to backend."; + return DeviceManager::DeviceErrorHardwareFailure; + } + + if (action.actionTypeId() == startUpdateActionTypeId) { + m_snapdControl->snapRefresh(); + return DeviceManager::DeviceErrorNoError; + } else if (action.actionTypeId() == checkUpdatesActionTypeId) { + m_snapdControl->checkForUpdates(); + return DeviceManager::DeviceErrorNoError; + } + + return DeviceManager::DeviceErrorActionTypeNotFound; + + } else if (device->deviceClassId() == snapDeviceClassId) { + + if (!m_snapdControl) { + qCDebug(dcSnapd()) << "There is currently no snapd controller."; + return DeviceManager::DeviceErrorHardwareFailure; + } + + if (!m_snapdControl->connected()) { + qCDebug(dcSnapd()) << "Snapd controller not connected to to backend."; + return DeviceManager::DeviceErrorHardwareFailure; + } + + if (action.actionTypeId() == snapChannelActionTypeId) { + QString snapName = device->paramValue(snapNameParamTypeId).toString(); + m_snapdControl->changeSnapChannel(snapName, action.param(snapChannelStateParamTypeId).value().toString()); + return DeviceManager::DeviceErrorNoError; + } else if (action.actionTypeId() == snapRevertActionTypeId) { + QString snapName = device->paramValue(snapNameParamTypeId).toString(); + m_snapdControl->snapRevert(snapName); + return DeviceManager::DeviceErrorNoError; + } + + return DeviceManager::DeviceErrorActionTypeNotFound; + } + + return DeviceManager::DeviceErrorDeviceClassNotFound; +} + +void DevicePluginSnapd::onPluginConfigurationChanged(const ParamTypeId ¶mTypeId, const QVariant &value) +{ + if (paramTypeId == advancedModeParamTypeId) { + qCDebug(dcSnapd()) << "Advanced mode" << (value.toBool() ? "enabled." : "disabled."); + m_advancedMode = value.toBool(); + + // If advanced mode disabled, clean up all snap devices + if (!m_advancedMode) { + foreach (const QString deviceSnapId, m_snapDevices.keys()) { + Device *device = m_snapDevices.take(deviceSnapId); + qCDebug(dcSnapd()) << "Remove device for snap" << device->paramValue(snapNameParamTypeId).toString(); + emit autoDeviceDisappeared(device->id()); + } + } else { + if (!m_snapdControl) + return; + + m_snapdControl->update(); + } + } +} + +void DevicePluginSnapd::onRefreshTimer() +{ + if (!m_snapdControl) { + startMonitoringAutoDevices(); + return; + } + + m_snapdControl->update(); +} + +void DevicePluginSnapd::onUpdateTimer() +{ + if (!m_snapdControl) + return; + + m_snapdControl->checkForUpdates(); +} + +void DevicePluginSnapd::onSnapListUpdated(const QVariantList &snapList) +{ + // Check for new snap devices only in advanced mode + if (!m_advancedMode) + return; + + // Collect list of snap id's from snapList for remove checking + QStringList snapIdList; + + // Check if we have to create a new device + foreach (const QVariant &snapVariant, snapList) { + QVariantMap snapMap = snapVariant.toMap(); + snapIdList.append(snapMap.value("id").toString()); + // If there is no device for this snap yet + if (!m_snapDevices.contains(snapMap.value("id").toString())) { + DeviceDescriptor descriptor(snapDeviceClassId, QString("Snap %1").arg(snapMap.value("name").toString())); + ParamList params; + params.append(Param(snapNameParamTypeId, snapMap.value("name"))); + params.append(Param(snapIdParamTypeId, snapMap.value("id"))); + params.append(Param(snapSummaryParamTypeId, snapMap.value("summary"))); + params.append(Param(snapDescriptionParamTypeId, snapMap.value("description"))); + params.append(Param(snapDeveloperParamTypeId, snapMap.value("developer"))); + descriptor.setParams(params); + + emit autoDevicesAppeared(snapDeviceClassId, QList() << descriptor); + } else { + // Update the states + Device *device = m_snapDevices.value(snapMap.value("id").toString(), nullptr); + if (!device) { + qCWarning(dcSnapd()) << "Holding invalid snap device. This should never happen. Please report a bug if you see this message."; + continue; + } + + device->setStateValue(snapChannelStateTypeId, snapMap.value("channel").toString()); + device->setStateValue(snapVersionStateTypeId, snapMap.value("version").toString()); + device->setStateValue(snapRevisionStateTypeId, snapMap.value("revision").toString()); + } + } + + // Check if we have to remove a device + foreach (const QString deviceSnapId, m_snapDevices.keys()) { + if (!snapIdList.contains(deviceSnapId)) { + Device *device = m_snapDevices.take(deviceSnapId); + qCDebug(dcSnapd()) << "The snap" << device->paramValue(snapNameParamTypeId).toString() << "is not installed any more."; + emit autoDeviceDisappeared(device->id()); + } + } +} diff --git a/snapd/devicepluginsnapd.h b/snapd/devicepluginsnapd.h new file mode 100644 index 00000000..e0b2d001 --- /dev/null +++ b/snapd/devicepluginsnapd.h @@ -0,0 +1,71 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2017 Simon Stürz . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DEVICEPLUGINSNAPD_H +#define DEVICEPLUGINSNAPD_H + +#include "devicemanager.h" +#include "plugin/deviceplugin.h" +#include "plugintimer.h" + +#include +#include +#include +#include + +#include "snapdcontrol.h" + + +class DevicePluginSnapd: public DevicePlugin { + Q_OBJECT + Q_PLUGIN_METADATA(IID "guru.guh.DevicePlugin" FILE "devicepluginsnapd.json") + Q_INTERFACES(DevicePlugin) + +public: + explicit DevicePluginSnapd(); + void init() override; + void startMonitoringAutoDevices() override; + void postSetupDevice(Device *device) override; + void deviceRemoved(Device *device) override; + + DeviceManager::DeviceSetupStatus setupDevice(Device *device) override; + DeviceManager::DeviceError executeAction(Device *device, const Action &action) override; + +private: + SnapdControl *m_snapdControl = nullptr; + PluginTimer *m_refreshTimer = nullptr; + PluginTimer *m_updateTimer = nullptr; + + bool m_advancedMode = false; + + // Snap list for faster access (snap id, device) + QHash m_snapDevices; + +private slots: + void onPluginConfigurationChanged(const ParamTypeId ¶mTypeId, const QVariant &value); + void onRefreshTimer(); + void onUpdateTimer(); + void onSnapListUpdated(const QVariantList &snapList); + +}; + +#endif // DEVICEPLUGINSNAPD_H diff --git a/snapd/devicepluginsnapd.json b/snapd/devicepluginsnapd.json new file mode 100644 index 00000000..308fb7db --- /dev/null +++ b/snapd/devicepluginsnapd.json @@ -0,0 +1,196 @@ +{ + "name": "Snapd", + "idName": "Snapd", + "id": "b82bce59-59bf-48b3-b781-54a6f45800f3", + "paramTypes": [ + { + "id": "017fe4c5-fc41-41fe-8e67-08fdaccb89ea", + "idName": "advancedMode", + "name": "Advanced mode", + "type": "bool", + "defaultValue": false + } + ], + "vendors": [ + { + "name": "Canonical", + "idName": "canonical", + "id": "60582ddf-32ea-4fcd-a6f2-f3beaaf21517", + "deviceClasses": [ + { + "id": "d90cda58-4d8c-4b7f-a982-38e56a95b72a", + "idName": "snapdControl", + "name": "Update manager", + "createMethods": [ "auto" ], + "basicTags": [ "Gateway" ], + "deviceIcon": "Gateway", + "paramTypes": [ ], + "actionTypes": [ + { + "id": "45626b75-f09d-4dd1-b6c4-ee33201b47b0", + "idName": "startUpdate", + "name": "Start update", + "paramTypes": [ ] + }, + { + "id": "4738f2c9-666e-45b9-91d3-7bcbf722b669", + "idName": "checkUpdates", + "name": "Check for updates", + "paramTypes": [ ] + } + ], + "stateTypes": [ + { + "id": "6b662b3e-fd12-4f24-be77-aec066f16d8c", + "idName": "snapdAvailable", + "name": "Update manager available", + "eventTypeName": "Update manager available changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "a6b1d24b-d523-4516-9bce-5b467e5e09b2", + "idName": "updateAvailable", + "name": "System update available", + "eventTypeName": "System update available changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "01ca7a22-5607-4c5e-a465-a2ae7e8b529c", + "idName": "updateRunning", + "name": "System update running", + "eventTypeName": "System update running changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "c671545a-6bde-4c08-8e37-0d256841a3a5", + "idName": "lastUpdateTime", + "name": "Last automatic system update", + "eventTypeName": "Last automatic system update time changed", + "unit": "UnixTime", + "type": "int", + "eventRuleRelevant": false, + "stateRuleRelevant": false, + "defaultValue": 0 + }, + { + "id": "122c2423-a1d9-400f-80f8-b1f798975914", + "idName": "nextUpdateTime", + "name": "Next automatic system update", + "eventTypeName": "Next automatic system update time changed", + "unit": "UnixTime", + "type": "int", + "eventRuleRelevant": false, + "stateRuleRelevant": false, + "defaultValue": 0 + }, + { + "id": "4987aca3-3916-4cb3-938f-df6c99d04dbf", + "idName": "status", + "name": "Status", + "eventTypeName": "Status changed", + "type": "QString", + "eventRuleRelevant": false, + "stateRuleRelevant": false, + "defaultValue": "-" + } + ] + }, + { + "id": "ff0840d7-fcfc-4403-9d9f-301610d5a437", + "idName": "snap", + "name": "Snap", + "createMethods": [ "auto" ], + "basicTags": [ "Gateway" ], + "deviceIcon": "Network", + "paramTypes": [ + { + "id": "4f38614d-8be0-48dc-a24d-cee9ff1f2a89", + "idName": "snapName", + "name": "Name", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "9afb98fb-f717-4f4c-8009-1a6514054c5f", + "idName": "snapId", + "name": "ID", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "12b9a65f-970b-49b5-b1d0-1625fc6d8758", + "idName": "snapSummary", + "name": "Summary", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "fe24c61b-e154-4259-b7ca-6f0602e9d1c3", + "idName": "snapDescription", + "name": "Description", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "76ead9c5-0a18-40a2-b31d-f6bb6dfea0a5", + "idName": "snapDeveloper", + "name": "Developer", + "type": "QString", + "defaultValue": "-" + } + ], + "actionTypes": [ + { + "id": "e061dee6-62fc-45cc-9c9f-403c2be52939", + "idName": "snapRevert", + "name": "Rollback to previous version" + } + ], + "stateTypes": [ + { + "id": "7be2b61e-3f59-4b92-b2bb-50d027bb92ff", + "idName": "snapChannel", + "name": "Channel", + "eventTypeName": "Channel changed", + "type": "QString", + "eventRuleRelevant": false, + "stateRuleRelevant": false, + "actionTypeName": "Set channel", + "defaultValue": "stable", + "writable": true, + "possibleValues": [ + "stable", + "candidate", + "beta", + "edge" + ] + }, + { + "id": "532a95f3-db29-427e-bb32-d5a22029e586", + "idName": "snapVersion", + "name": "Version", + "eventTypeName": "Version changed", + "type": "QString", + "eventRuleRelevant": false, + "stateRuleRelevant": false, + "defaultValue": "-" + }, + { + "id": "f26a6404-e011-11e7-9224-2350048461eb", + "idName": "snapRevision", + "name": "Revision", + "eventTypeName": "Revision changed", + "type": "QString", + "eventRuleRelevant": false, + "stateRuleRelevant": false, + "defaultValue": "-" + } + ] + } + ] + } + ] +} diff --git a/snapd/snapd.pro b/snapd/snapd.pro new file mode 100644 index 00000000..c48b8661 --- /dev/null +++ b/snapd/snapd.pro @@ -0,0 +1,21 @@ +TRANSLATIONS = translations/en_US.ts \ + translations/de_DE.ts + +# Note: include after the TRANSLATIONS definition +include(../plugins.pri) + +TARGET = $$qtLibraryTarget(guh_devicepluginsnapd) + +QT += network + +SOURCES += \ + devicepluginsnapd.cpp \ + snapdcontrol.cpp \ + snapdconnection.cpp \ + snapdreply.cpp + +HEADERS += \ + devicepluginsnapd.h \ + snapdcontrol.h \ + snapdconnection.h \ + snapdreply.h diff --git a/snapd/snapdconnection.cpp b/snapd/snapdconnection.cpp new file mode 100644 index 00000000..6140019a --- /dev/null +++ b/snapd/snapdconnection.cpp @@ -0,0 +1,327 @@ +#include "snapdconnection.h" +#include "extern-plugininfo.h" + +#include +#include + + +SnapdConnection::SnapdConnection(QObject *parent) : + QLocalSocket(parent) +{ + connect(this, &QLocalSocket::connected, this, &SnapdConnection::onConnected); + connect(this, &QLocalSocket::disconnected, this, &SnapdConnection::onDisconnected); + connect(this, &QLocalSocket::readyRead, this, &SnapdConnection::onReadyRead); + connect(this, &QLocalSocket::stateChanged, this, &SnapdConnection::onStateChanged); + connect(this, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(onError(QLocalSocket::LocalSocketError))); +} + +SnapdReply *SnapdConnection::get(const QString &path) +{ + SnapdReply *reply = new SnapdReply(this); + reply->setRequestPath(path); + reply->setRequestMethod("GET"); + reply->setRequestRawMessage(createRequestHeader("GET", path)); + + // Check if currently a reply is running + if (m_currentReply) { + m_replyQueue.enqueue(reply); + } else { + // Send request + m_currentReply = reply; + if (m_debug) + qCDebug(dcSnapd()) << "-->" << reply->requestMethod() << reply->requestPath(); + + if (write(reply->requestRawMessage()) <= 0) { + m_currentReply = nullptr; + reply->setFinished(false); + sendNextRequest(); + } + } + + // Note: the caller owns the object now + return reply; +} + +SnapdReply *SnapdConnection::post(const QString &path, const QByteArray &payload) +{ + SnapdReply *reply = new SnapdReply(this); + reply->setRequestPath(path); + reply->setRequestMethod("POST"); + QByteArray header = createRequestHeader("POST", path, payload); + reply->setRequestRawMessage(header.append(payload)); + + // Check if currently a reply is running + if (m_currentReply) { + m_replyQueue.enqueue(reply); + } else { + // Send request + m_currentReply = reply; + if (m_debug) + qCDebug(dcSnapd()) << "-->" << reply->requestMethod() << reply->requestPath() << payload; + + if (write(reply->requestRawMessage()) <= 0) { + m_currentReply = nullptr; + reply->setFinished(false); + sendNextRequest(); + } + } + + // Note: the caller owns the object now + return reply; +} + +bool SnapdConnection::isConnected() const +{ + return m_connected; +} + +void SnapdConnection::setConnected(const bool &connected) +{ + if (m_connected == connected) + return; + + qCDebug(dcSnapd()) << "Connected"; + + m_connected = connected; + emit connectedChanged(m_connected); +} + +QByteArray SnapdConnection::createRequestHeader(const QString &method, const QString &path, const QByteArray &payload) +{ + QByteArray request; + request.append(QString("%1 %2 HTTP/1.1\r\n").arg(method).arg(path).toUtf8()); + request.append("Host: http\r\n"); + request.append("Accept: *\r\n"); + if (!payload.isEmpty()) { + request.append("Content-Type: application/json\r\n"); + request.append(QString("Content-Length: %1\r\n").arg(payload.count()).toUtf8()); + } + + request.append("\r\n"); + return request; +} + +QByteArray SnapdConnection::getChunckedPayload(const QByteArray &payload) +{ + // Read line by line + QStringList payloadLines = QString::fromUtf8(payload).split(QRegExp("\r\n")); + if (payloadLines.count() < 4) { + qCWarning(dcSnapd()) << "Chuncked payload invalid linecount" << payloadLines.count(); + return QByteArray(); + } + + int payloadSize = payloadLines.at(2).toInt(0, 16); + + if (m_debug) + qCDebug(dcSnapd()) << "Payload size" << payloadSize; + + if (payloadLines.at(3).toUtf8().size() != payloadSize) { + qCWarning(dcSnapd()) << "Invalid payload size" << payloadLines.at(3).toUtf8().size() << "!=" << payloadSize; + return QByteArray(); + } + + // Return just the payload + return payloadLines.at(3).toUtf8(); +} + +void SnapdConnection::processData() +{ + if (!m_currentReply) { + qCWarning(dcSnapd()) << "Data received without current reply" << m_payload; + return; + } + + if (m_header.isEmpty()) { + qCWarning(dcSnapd()) << "Could not process data. There is no header."; + m_currentReply->setFinished(); + return; + } + + // Get the raw payload + QByteArray payloadData; + if (m_chuncked) { + payloadData = getChunckedPayload(m_payload); + } else { + payloadData = m_payload; + } + + // Check if there are data to process + if (m_payload.isEmpty()) { + qCWarning(dcSnapd()) << "Could not process data. There is no payload to process."; + return; + } + + // Parse header + QHash parsedHeader; + QStringList headerLines = QString::fromUtf8(m_header).split(QRegExp("\r\n")); + + // Read status line + QString statusLine = headerLines.takeFirst(); + QStringList statusLineTokens = statusLine.split(QRegExp("[ \r\n][ \r\n]*")); + if (statusLineTokens.count() < 3) { + qCWarning(dcSnapd()) << "Could not parse HTTP status line:" << statusLine; + return; + } + + bool statusCodeOk = false; + int statusCode = statusLineTokens.at(1).simplified().toInt(&statusCodeOk); + if (!statusCodeOk) { + qCWarning(dcSnapd()) << "Could not parse HTTP status code:" << statusLineTokens.at(1); + return; + } + + QString statusMessage; + for (int i = 2; i < statusLineTokens.count(); i++) { + statusMessage.append(statusLineTokens.at(i).simplified()); + if (i < statusLineTokens.count() -1) { + statusMessage.append(" "); + } + } + + // Verify header formating + foreach (const QString &line, headerLines) { + if (!line.contains(":")) { + qCWarning(dcSnapd()) << "Invalid HTTP header. Missing \":\" in line" << line; + return; + } + + int index = line.indexOf(":"); + QString key = line.left(index).toUtf8().simplified(); + QString value = line.right(line.count() - index - 1).toUtf8().simplified(); + //qCDebug(dcSnapd()) << " Key:" << key << "Value:" << value; + parsedHeader.insert(key, value); + } + + // Parse payload + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(payloadData, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcSnapd()) << "Got invalid JSON data from snapd:" << error.offset << error.errorString(); + qCWarning(dcSnapd()) << qUtf8Printable(payloadData); + return; + } + + if (m_debug) + qCDebug(dcSnapd()) << "<--" << m_currentReply->requestPath() << statusCode << statusMessage; + + // Fill reply + m_currentReply->setStatusCode(statusCode); + m_currentReply->setStatusMessage(statusMessage); + m_currentReply->setHeader(parsedHeader); + m_currentReply->setDataMap(jsonDoc.toVariant().toMap()); + m_currentReply->setFinished(); + m_currentReply = nullptr; + + sendNextRequest(); + + // Current data stream finished, reset for new messages + m_payload.clear(); + m_header.clear(); + m_chuncked = false; +} + +void SnapdConnection::sendNextRequest() +{ + if (m_replyQueue.isEmpty()) + return; + + SnapdReply *reply = m_replyQueue.dequeue(); + m_currentReply = reply; + if (m_debug) + qCDebug(dcSnapd()) << "-->" << reply->requestMethod() << reply->requestPath(); + + if (write(reply->requestRawMessage()) < 0) { + m_currentReply->setFinished(false); + m_currentReply = nullptr; + sendNextRequest(); + } +} + +void SnapdConnection::onConnected() +{ + setConnected(true); +} + +void SnapdConnection::onDisconnected() +{ + setConnected(false); +} + +void SnapdConnection::onError(const QLocalSocket::LocalSocketError &socketError) +{ + qCWarning(dcSnapd()) << "Socket error" << socketError << errorString(); +} + +void SnapdConnection::onStateChanged(const QLocalSocket::LocalSocketState &state) +{ + switch (state) { + case QLocalSocket::UnconnectedState: + qCDebug(dcSnapd()) << "Disconnected from snapd."; + break; + case QLocalSocket::ConnectingState: + qCDebug(dcSnapd()) << "Connecting to snapd..."; + break; + case QLocalSocket::ConnectedState: + qCDebug(dcSnapd()) << "Connected to snapd."; + break; + case QLocalSocket::ClosingState: + qCDebug(dcSnapd()) << "Closing connection to snapd."; + break; + default: + break; + } +} + +void SnapdConnection::onReadyRead() +{ + QByteArray data = readAll(); + + if (m_debug) + qCDebug(dcSnapd()) << "Data received:" << data; + + // If we are not appending to a reply + if (!m_chuncked) { + + // Parse header + int headerIndex = data.indexOf("\r\n\r\n"); + if (headerIndex < 0) { + qCWarning(dcSnapd()) << "Invalid response format. Could not find header/payload mark."; + return; + } + + m_header = data.left(headerIndex); + + if (m_debug) + qCDebug(dcSnapd()) << "Header:" << m_header; + + QByteArray payload = data.right(data.length() - (headerIndex)); + + if (m_debug) + qCDebug(dcSnapd()) << "Payload" << payload; + + // Check if this message is chuncked + if (m_header.contains("chunked")) { + + if (m_debug) + qCDebug(dcSnapd()) << "Chuncked message receiving"; + + m_chuncked = true; + m_payload.append(payload); + + if (m_payload.endsWith("\r\n0\r\n\r\n")) { + // Chuncked message finished + processData(); + } + } else { + // Not chucked + m_payload = payload; + processData(); + } + } else { + m_payload.append(data); + if (m_payload.endsWith("\r\n0\r\n\r\n")) { + // Chuncked message finished + processData(); + } + } +} diff --git a/snapd/snapdconnection.h b/snapd/snapdconnection.h new file mode 100644 index 00000000..d079839e --- /dev/null +++ b/snapd/snapdconnection.h @@ -0,0 +1,54 @@ +#ifndef SNAPDCONNECTION_H +#define SNAPDCONNECTION_H + +#include +#include +#include + +#include "snapdreply.h" + +class SnapdConnection : public QLocalSocket +{ + Q_OBJECT +public: + explicit SnapdConnection(QObject *parent = nullptr); + + SnapdReply *get(const QString &path); + SnapdReply *post(const QString &path, const QByteArray &payload); + + bool isConnected() const; + +private: + bool m_chuncked = false; + + QByteArray m_header; + QByteArray m_payload; + + bool m_connected = false; + bool m_debug = false; + + SnapdReply *m_currentReply = nullptr; + QQueue m_replyQueue; + + void setConnected(const bool &connected); + + // Helper methods + QByteArray createRequestHeader(const QString &method, const QString &path, const QByteArray &payload = QByteArray()); + QByteArray getChunckedPayload(const QByteArray &payload); + + void processData(); + void sendNextRequest(); + +private slots: + void onConnected(); + void onDisconnected(); + void onError(const QLocalSocket::LocalSocketError &socketError); + void onStateChanged(const QLocalSocket::LocalSocketState &state); + void onReadyRead(); + +signals: + void connectedChanged(const bool &connected); + +}; + +#endif // SNAPDCONNECTION_H diff --git a/snapd/snapdcontrol.cpp b/snapd/snapdcontrol.cpp new file mode 100644 index 00000000..706e32d7 --- /dev/null +++ b/snapd/snapdcontrol.cpp @@ -0,0 +1,424 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2017 Simon Stürz . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "snapdcontrol.h" +#include "extern-plugininfo.h" + +#include +#include +#include +#include + +SnapdControl::SnapdControl(Device *device, QObject *parent) : + QObject(parent), + m_device(device), + m_snapdSocketPath("/run/snapd.socket") +{ + m_snapConnection = new SnapdConnection(this); + connect(m_snapConnection, &SnapdConnection::connectedChanged, this, &SnapdControl::onConnectedChanged); +} + +Device *SnapdControl::device() +{ + return m_device; +} + +bool SnapdControl::available() const +{ + QFileInfo fileInfo(m_snapdSocketPath); + if (!fileInfo.exists()) { + qCWarning(dcSnapd()) << "The socket descriptor" << m_snapdSocketPath << "does not exist"; + return false; + } + + if (!fileInfo.isReadable()) { + qCWarning(dcSnapd()) << "The socket descriptor" << m_snapdSocketPath << "is not readable"; + return false; + } + + if (!fileInfo.isWritable()) { + qCWarning(dcSnapd()) << "The socket descriptor" << m_snapdSocketPath << "is not writable"; + return false; + } + + return true; +} + +bool SnapdControl::connected() const +{ + return m_snapConnection->isConnected(); +} + +bool SnapdControl::enabled() const +{ + return m_enabled; +} + +void SnapdControl::loadSystemInfo() +{ + if (!m_snapConnection) + return; + + if (!m_snapConnection->isConnected()) + return; + + SnapdReply *reply = m_snapConnection->get("/v2/system-info"); + connect(reply, &SnapdReply::finished, this, &SnapdControl::onLoadSystemInfoFinished); +} + +void SnapdControl::loadSnapList() +{ + if (!m_snapConnection) + return; + + if (!m_snapConnection->isConnected()) + return; + + SnapdReply *reply = m_snapConnection->get("/v2/snaps"); + connect(reply, &SnapdReply::finished, this, &SnapdControl::onLoadSnapListFinished); +} + +void SnapdControl::loadRunningChanges() +{ + if (!m_snapConnection) + return; + + if (!m_snapConnection->isConnected()) + return; + + SnapdReply *reply = m_snapConnection->get("/v2/changes"); + connect(reply, &SnapdReply::finished, this, &SnapdControl::onLoadRunningChangesFinished); +} + +void SnapdControl::loadChange(const int &change) +{ + if (!m_snapConnection) + return; + + if (!m_snapConnection->isConnected()) + return; + + SnapdReply *reply = m_snapConnection->get(QString("/v2/changes/%1").arg(QString::number(change))); + connect(reply, &SnapdReply::finished, this, &SnapdControl::onLoadChangeFinished); +} + +void SnapdControl::processChange(const QVariantMap &changeMap) +{ + int changeId = changeMap.value("id").toInt(); + bool changeReady = changeMap.value("ready").toBool(); + QString changeKind = changeMap.value("kind").toString(); + QString changeStatus = changeMap.value("status").toString(); + QString changeSummary = changeMap.value("summary").toString(); + + qCDebug(dcSnapd()) << changeStatus << changeKind << changeSummary; + + // Add this change if not alreade finishished or added + if (!m_watchingChanges.contains(changeId) && !changeReady) + m_watchingChanges.append(changeId); + + // If change is on Doing, update the status + if (changeStatus == "Doing") { + device()->setStateValue(statusStateTypeId, changeSummary); + } + + if (changeReady) { + qCDebug(dcSnapd()).noquote() << changeKind << (changeReady ? "finished." : "running.") << changeSummary; + m_watchingChanges.removeAll(changeId); + } +} + +bool SnapdControl::validAsyncResponse(const QVariantMap &responseMap) +{ + if (!responseMap.contains("type")) + return false; + + if (responseMap.value("type") == "error") + return false; + + return true; +} + +void SnapdControl::onConnectedChanged(const bool &connected) +{ + if (connected) { + device()->setStateValue(snapdAvailableStateTypeId, true); + update(); + } else { + device()->setStateValue(snapdAvailableStateTypeId, false); + } +} + +void SnapdControl::onLoadSystemInfoFinished() +{ + SnapdReply *reply = static_cast(sender()); + if (!reply->isValid()) { + qCDebug(dcSnapd()) << "Load system info request finished with error" << reply->requestPath(); + reply->deleteLater(); + return; + } + + QVariantMap result = reply->dataMap().value("result").toMap(); + QDateTime lastRefreshTime = QDateTime::fromString(result.value("refresh").toMap().value("last").toString(), Qt::ISODate); + QDateTime nextRefreshTime = QDateTime::fromString(result.value("refresh").toMap().value("next").toString(), Qt::ISODate); + + // Set update time information + device()->setStateValue(lastUpdateTimeStateTypeId, lastRefreshTime.toTime_t()); + device()->setStateValue(nextUpdateTimeStateTypeId, nextRefreshTime.toTime_t()); + + reply->deleteLater(); +} + +void SnapdControl::onLoadSnapListFinished() +{ + SnapdReply *reply = static_cast(sender()); + if (!reply->isValid()) { + qCDebug(dcSnapd()) << "Load system info request finished with error" << reply->requestPath(); + reply->deleteLater(); + return; + } + + emit snapListUpdated(reply->dataMap().value("result").toList()); + reply->deleteLater(); +} + +void SnapdControl::onLoadRunningChangesFinished() +{ + SnapdReply *reply = static_cast(sender()); + if (!reply->isValid()) { + qCDebug(dcSnapd()) << "Load running changes request finished with error" << reply->requestPath(); + reply->deleteLater(); + return; + } + + foreach (const QVariant &changeVariant, reply->dataMap().value("result").toList()) { + processChange(changeVariant.toMap()); + } + + if (reply->dataMap().value("result").toList().isEmpty() && m_watchingChanges.isEmpty()) { + device()->setStateValue(updateRunningStateTypeId, false); + device()->setStateValue(statusStateTypeId, "-"); + } else { + device()->setStateValue(updateRunningStateTypeId, true); + } + + reply->deleteLater(); +} + +void SnapdControl::onLoadChangeFinished() +{ + SnapdReply *reply = static_cast(sender()); + if (!reply->isValid()) { + qCDebug(dcSnapd()) << "Load change request finished with error" << reply->requestPath(); + reply->deleteLater(); + return; + } + + processChange(reply->dataMap().value("result").toMap()); + + if (m_watchingChanges.isEmpty()) { + device()->setStateValue(updateRunningStateTypeId, false); + + } + + reply->deleteLater(); +} + +void SnapdControl::onSnapRefreshFinished() +{ + SnapdReply *reply = static_cast(sender()); + if (!reply->isValid()) { + qCDebug(dcSnapd()) << "Snap refresh request finished with error" << reply->requestPath(); + reply->deleteLater(); + return; + } + + //qCDebug(dcSnapd()) << qUtf8Printable(QJsonDocument::fromVariant(reply->dataMap()).toJson(QJsonDocument::Indented)); + if (!validAsyncResponse(reply->dataMap())) { + qCWarning(dcSnapd()) << "Async change request finished with error" << reply->dataMap().value("status").toString() << reply->dataMap().value("").toString(); + } else { + loadChange(reply->dataMap().value("change").toInt()); + } + + reply->deleteLater(); +} + +void SnapdControl::onSnapRevertFinished() +{ + SnapdReply *reply = static_cast(sender()); + if (!reply->isValid()) { + qCDebug(dcSnapd()) << "Snap revert request finished with error" << reply->requestPath(); + reply->deleteLater(); + return; + } + + //qCDebug(dcSnapd()) << qUtf8Printable(QJsonDocument::fromVariant(reply->dataMap()).toJson(QJsonDocument::Indented)); + if (!validAsyncResponse(reply->dataMap())) { + qCWarning(dcSnapd()) << "Async change request finished with error" << reply->dataMap().value("status").toString() << reply->dataMap().value("").toString(); + } else { + loadChange(reply->dataMap().value("change").toInt()); + } + reply->deleteLater(); +} + +void SnapdControl::onCheckForUpdatesFinished() +{ + SnapdReply *reply = static_cast(sender()); + if (!reply->isValid()) { + qCDebug(dcSnapd()) << "Snap check for updates request finished with error" << reply->requestPath(); + reply->deleteLater(); + return; + } + + //qCDebug(dcSnapd()) << qUtf8Printable(QJsonDocument::fromVariant(reply->dataMap()).toJson(QJsonDocument::Indented)); + device()->setStateValue(updateAvailableStateTypeId, !reply->dataMap().value("result").toList().isEmpty()); + reply->deleteLater(); +} + +void SnapdControl::onChangeSnapChannelFinished() +{ + SnapdReply *reply = static_cast(sender()); + if (!reply->isValid()) { + qCDebug(dcSnapd()) << "Change snap channel request finished with error" << reply->requestPath(); + reply->deleteLater(); + return; + } + + if (!validAsyncResponse(reply->dataMap())) { + qCWarning(dcSnapd()) << "Async change request finished with error" << reply->dataMap().value("status").toString() << reply->dataMap().value("").toString(); + } else { + loadChange(reply->dataMap().value("change").toInt()); + } + reply->deleteLater(); +} + +void SnapdControl::enable() +{ + m_enabled = true; + update(); +} + +void SnapdControl::disable() +{ + m_enabled = false; + + if (m_snapConnection) { + m_snapConnection->close(); + } +} + +void SnapdControl::update() +{ + if (!m_snapConnection) + return; + + if (!available()) + return; + + if (!enabled()) + return; + + // Try to reconnect if unconnected + if (m_snapConnection->state() == QLocalSocket::UnconnectedState) { + m_snapConnection->connectToServer(m_snapdSocketPath, QLocalSocket::ReadWrite); + return; + } + + // Note: this makes sure the state is realy connected (including connection initialisation stuff) + if (!m_snapConnection->isConnected()) + return; + + // Update information + if (!m_watchingChanges.isEmpty()) { + // We are watching currently changes + foreach (const int &change, m_watchingChanges) { + loadChange(change); + } + loadRunningChanges(); + } else { + // Normal refresh + loadSystemInfo(); + loadSnapList(); + checkForUpdates(); + loadRunningChanges(); + } +} + +void SnapdControl::snapRefresh() +{ + if (!m_snapConnection) + return; + + if (!m_snapConnection->isConnected()) + return; + + QVariantMap request; + request.insert("action", "refresh"); + + qCDebug(dcSnapd()) << "Refresh all snaps"; + SnapdReply *reply = m_snapConnection->post("/v2/snaps", QJsonDocument::fromVariant(request).toJson(QJsonDocument::Compact)); + connect(reply, &SnapdReply::finished, this, &SnapdControl::onSnapRefreshFinished); +} + +void SnapdControl::changeSnapChannel(const QString &snapName, const QString &channel) +{ + if (!m_snapConnection) + return; + + if (!m_snapConnection->isConnected()) + return; + + QVariantMap request; + request.insert("action", "refresh"); + request.insert("channel", channel); + + qCDebug(dcSnapd()) << "Refresh snap" << snapName << "to channel" << channel; + SnapdReply *reply = m_snapConnection->post(QString("/v2/snaps/%1").arg(snapName), QJsonDocument::fromVariant(request).toJson(QJsonDocument::Compact)); + connect(reply, &SnapdReply::finished, this, &SnapdControl::onChangeSnapChannelFinished); +} + +void SnapdControl::checkForUpdates() +{ + if (!m_snapConnection) + return; + + if (!m_snapConnection->isConnected()) + return; + + SnapdReply *reply = m_snapConnection->get("/v2/find?select=refresh"); + connect(reply, &SnapdReply::finished, this, &SnapdControl::onCheckForUpdatesFinished); +} + +void SnapdControl::snapRevert(const QString &snapName) +{ + if (!m_snapConnection) + return; + + if (!m_snapConnection->isConnected()) + return; + + QVariantMap request; + request.insert("action", "revert"); + + qCDebug(dcSnapd()) << "Revert snap" << snapName; + SnapdReply *reply = m_snapConnection->post(QString("/v2/snaps/%1").arg(snapName), QJsonDocument::fromVariant(request).toJson(QJsonDocument::Compact)); + connect(reply, &SnapdReply::finished, this, &SnapdControl::onSnapRevertFinished); +} diff --git a/snapd/snapdcontrol.h b/snapd/snapdcontrol.h new file mode 100644 index 00000000..4b5bfc9d --- /dev/null +++ b/snapd/snapdcontrol.h @@ -0,0 +1,91 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2017 Simon Stürz . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef SNAPDCONTROL_H +#define SNAPDCONTROL_H + +#include +#include + +#include "plugin/device.h" +#include "snapdconnection.h" + +class SnapdControl : public QObject +{ + Q_OBJECT +public: + explicit SnapdControl(Device *device, QObject *parent = nullptr); + + Device *device(); + + bool available() const; + bool connected() const; + bool enabled() const; + +private: + Device *m_device = nullptr; + SnapdConnection *m_snapConnection = nullptr; + + QString m_snapdSocketPath; + bool m_enabled = true; + + QList m_watchingChanges; + + // Update calls + void loadSystemInfo(); + void loadSnapList(); + void loadRunningChanges(); + void loadChange(const int &change); + + void processChange(const QVariantMap &changeMap); + bool validAsyncResponse(const QVariantMap &responseMap); + +private slots: + void onConnectedChanged(const bool &connected); + + // Response handler for different requests + void onLoadSystemInfoFinished(); + void onLoadSnapListFinished(); + void onLoadRunningChangesFinished(); + void onLoadChangeFinished(); + + void onSnapRefreshFinished(); + void onSnapRevertFinished(); + void onCheckForUpdatesFinished(); + void onChangeSnapChannelFinished(); + +signals: + void snapListUpdated(const QVariantList &snapList); + +public slots: + void enable(); + void disable(); + + // Snapd request calls + void update(); + void snapRefresh(); + void checkForUpdates(); + void snapRevert(const QString &snapName); + void changeSnapChannel(const QString &snapName, const QString &channel); +}; + +#endif // SNAPDCONTROL_H diff --git a/snapd/snapdreply.cpp b/snapd/snapdreply.cpp new file mode 100644 index 00000000..4b0ee290 --- /dev/null +++ b/snapd/snapdreply.cpp @@ -0,0 +1,95 @@ +#include "snapdreply.h" + +QString SnapdReply::requestPath() const +{ + return m_requestPath; +} + +QString SnapdReply::requestMethod() const +{ + return m_requestMethod; +} + +QByteArray SnapdReply::requestRawMessage() const +{ + return m_requestRawMessage; +} + +int SnapdReply::statusCode() const +{ + return m_statusCode; +} + +QString SnapdReply::statusMessage() const +{ + return m_statusMessage; +} + +QHash SnapdReply::header() const +{ + return m_header; +} + +QVariantMap SnapdReply::dataMap() const +{ + return m_dataMap; +} + +bool SnapdReply::isFinished() const +{ + return m_isFinished; +} + +bool SnapdReply::isValid() const +{ + return m_valid; +} + +SnapdReply::SnapdReply(QObject *parent) : + QObject(parent) +{ + +} + +void SnapdReply::setRequestPath(const QString &requestPath) +{ + m_requestPath = requestPath; +} + +void SnapdReply::setRequestMethod(const QString &requestMethod) +{ + m_requestMethod = requestMethod; +} + +void SnapdReply::setRequestRawMessage(const QByteArray &rawMessage) +{ + m_requestRawMessage = rawMessage; +} + +void SnapdReply::setStatusCode(const int &statusCode) +{ + m_statusCode = statusCode; +} + +void SnapdReply::setStatusMessage(const QString &statusMessage) +{ + m_statusMessage = statusMessage; +} + +void SnapdReply::setHeader(const QHash header) +{ + m_header = header; +} + +void SnapdReply::setDataMap(const QVariantMap &dataMap) +{ + m_dataMap = dataMap; +} + +void SnapdReply::setFinished(const bool &valid) +{ + m_isFinished = true; + m_valid = valid; + + emit finished(); +} diff --git a/snapd/snapdreply.h b/snapd/snapdreply.h new file mode 100644 index 00000000..0251335f --- /dev/null +++ b/snapd/snapdreply.h @@ -0,0 +1,60 @@ +#ifndef SNAPDREPLY_H +#define SNAPDREPLY_H + +#include +#include +#include + +class SnapdReply : public QObject +{ + Q_OBJECT + + friend class SnapdConnection; + +public: + // Request + QString requestPath() const; + QString requestMethod() const; + QByteArray requestRawMessage() const; + + // Response + int statusCode() const; + QString statusMessage() const; + QHash header() const; + QVariantMap dataMap() const; + + bool isFinished() const; + bool isValid() const; + +private: + explicit SnapdReply(QObject *parent = nullptr); + + QString m_requestPath; + QString m_requestMethod; + QByteArray m_requestRawMessage; + + int m_statusCode; + QString m_statusMessage; + QHash m_header; + QVariantMap m_dataMap; + + bool m_isFinished = false; + bool m_valid = false; + + // Methods for SnapdConnection + void setRequestPath(const QString &requestPath); + void setRequestMethod(const QString &requestMethod); + void setRequestRawMessage(const QByteArray &rawMessage); + + void setStatusCode(const int &statusCode); + void setStatusMessage(const QString &statusMessage); + void setHeader(const QHash header); + void setDataMap(const QVariantMap &dataMap); + void setFinished(const bool &valid = true); + +signals: + void finished(); + +}; + +#endif // SNAPDREPLY_H diff --git a/snapd/translations/de_DE.ts b/snapd/translations/de_DE.ts new file mode 100644 index 00000000..21c86613 --- /dev/null +++ b/snapd/translations/de_DE.ts @@ -0,0 +1,199 @@ + + + + + Snapd + + + Snapd + The name of the plugin Snapd (b82bce59-59bf-48b3-b781-54a6f45800f3) + Snapd + + + + Advanced mode + The name of the paramType (017fe4c5-fc41-41fe-8e67-08fdaccb89ea) of Snapd + Fortgeschrittener Modus + + + + Canonical + The name of the vendor (60582ddf-32ea-4fcd-a6f2-f3beaaf21517) + Canonical + + + + System update available changed + The name of the autocreated EventType (a6b1d24b-d523-4516-9bce-5b467e5e09b2) + Systemupdate Verfügbarkeit geändert + + + + System update available + The name of the ParamType of StateType (a6b1d24b-d523-4516-9bce-5b467e5e09b2) of DeviceClass Update manager + Systemupdate verfügbar + + + + Last automatic system update time changed + The name of the autocreated EventType (c671545a-6bde-4c08-8e37-0d256841a3a5) + Letztes automatisches Systemupdate geändert + + + + Last automatic system update + The name of the ParamType of StateType (c671545a-6bde-4c08-8e37-0d256841a3a5) of DeviceClass Update manager + Letztes automatisches Systemupdate + + + + Next automatic system update time changed + The name of the autocreated EventType (122c2423-a1d9-400f-80f8-b1f798975914) + Nächstes automatisches Systemupdate geändert + + + + Next automatic system update + The name of the ParamType of StateType (122c2423-a1d9-400f-80f8-b1f798975914) of DeviceClass Update manager + Nächstes automatisches Systemupdate + + + + ID + The name of the paramType (9afb98fb-f717-4f4c-8009-1a6514054c5f) of Snap + ID + + + + Summary + The name of the paramType (12b9a65f-970b-49b5-b1d0-1625fc6d8758) of Snap + Zusammenfassung + + + + Description + The name of the paramType (fe24c61b-e154-4259-b7ca-6f0602e9d1c3) of Snap + Beschreibung + + + + Developer + The name of the paramType (76ead9c5-0a18-40a2-b31d-f6bb6dfea0a5) of Snap + Entwickler + + + + Channel + The name of the ParamType of StateType (7be2b61e-3f59-4b92-b2bb-50d027bb92ff) of DeviceClass Snap + Kanal + + + + Set channel + The name of the autocreated ActionType (7be2b61e-3f59-4b92-b2bb-50d027bb92ff) + Setze Kanal + + + + Version changed + The name of the autocreated EventType (532a95f3-db29-427e-bb32-d5a22029e586) + Version geändert + + + + Version + The name of the ParamType of StateType (532a95f3-db29-427e-bb32-d5a22029e586) of DeviceClass Snap + Version + + + + Revision changed + The name of the autocreated EventType (f26a6404-e011-11e7-9224-2350048461eb) + Revision geändert + + + + Revision + The name of the ParamType of StateType (f26a6404-e011-11e7-9224-2350048461eb) of DeviceClass Snap + Revision + + + + Rollback to previous version + The name of the ActionType e061dee6-62fc-45cc-9c9f-403c2be52939 of deviceClass Snap + Zurücksetzen auf vorhergehende Version + + + + Start update + The name of the ActionType 45626b75-f09d-4dd1-b6c4-ee33201b47b0 of deviceClass Update manager + Starte Systemupdate + + + + Update manager + The name of the DeviceClass (d90cda58-4d8c-4b7f-a982-38e56a95b72a) + Update Manager + + + + Update manager available changed + The name of the autocreated EventType (6b662b3e-fd12-4f24-be77-aec066f16d8c) + Update Manager Verfügbarkeit geändert + + + + Update manager available + The name of the ParamType of StateType (6b662b3e-fd12-4f24-be77-aec066f16d8c) of DeviceClass Update manager + Update Manager verfügbar + + + + Snap + The name of the DeviceClass (ff0840d7-fcfc-4403-9d9f-301610d5a437) + Snap + + + + System update running changed + The name of the autocreated EventType (01ca7a22-5607-4c5e-a465-a2ae7e8b529c) + Systemupdate aktiv geändert + + + + System update running + The name of the ParamType of StateType (01ca7a22-5607-4c5e-a465-a2ae7e8b529c) of DeviceClass Update manager + Systemupdate aktiv + + + + Status changed + The name of the autocreated EventType (4987aca3-3916-4cb3-938f-df6c99d04dbf) + Status geändert + + + + Status + The name of the ParamType of StateType (4987aca3-3916-4cb3-938f-df6c99d04dbf) of DeviceClass Update manager + Status + + + + Name + The name of the paramType (4f38614d-8be0-48dc-a24d-cee9ff1f2a89) of Snap + Name + + + + Channel changed + The name of the autocreated EventType (7be2b61e-3f59-4b92-b2bb-50d027bb92ff) + Kanal geändert + + + + Check for updates + The name of the ActionType 4738f2c9-666e-45b9-91d3-7bcbf722b669 of deviceClass Update manager + Nach Aktualisierungen suchen + + + diff --git a/snapd/translations/en_US.ts b/snapd/translations/en_US.ts new file mode 100644 index 00000000..872b2680 --- /dev/null +++ b/snapd/translations/en_US.ts @@ -0,0 +1,199 @@ + + + + + Snapd + + + Snapd + The name of the plugin Snapd (b82bce59-59bf-48b3-b781-54a6f45800f3) + + + + + Advanced mode + The name of the paramType (017fe4c5-fc41-41fe-8e67-08fdaccb89ea) of Snapd + + + + + Canonical + The name of the vendor (60582ddf-32ea-4fcd-a6f2-f3beaaf21517) + + + + + System update available changed + The name of the autocreated EventType (a6b1d24b-d523-4516-9bce-5b467e5e09b2) + + + + + System update available + The name of the ParamType of StateType (a6b1d24b-d523-4516-9bce-5b467e5e09b2) of DeviceClass Update manager + + + + + Last automatic system update time changed + The name of the autocreated EventType (c671545a-6bde-4c08-8e37-0d256841a3a5) + + + + + Last automatic system update + The name of the ParamType of StateType (c671545a-6bde-4c08-8e37-0d256841a3a5) of DeviceClass Update manager + + + + + Next automatic system update time changed + The name of the autocreated EventType (122c2423-a1d9-400f-80f8-b1f798975914) + + + + + Next automatic system update + The name of the ParamType of StateType (122c2423-a1d9-400f-80f8-b1f798975914) of DeviceClass Update manager + + + + + ID + The name of the paramType (9afb98fb-f717-4f4c-8009-1a6514054c5f) of Snap + + + + + Summary + The name of the paramType (12b9a65f-970b-49b5-b1d0-1625fc6d8758) of Snap + + + + + Description + The name of the paramType (fe24c61b-e154-4259-b7ca-6f0602e9d1c3) of Snap + + + + + Developer + The name of the paramType (76ead9c5-0a18-40a2-b31d-f6bb6dfea0a5) of Snap + + + + + Channel + The name of the ParamType of StateType (7be2b61e-3f59-4b92-b2bb-50d027bb92ff) of DeviceClass Snap + + + + + Set channel + The name of the autocreated ActionType (7be2b61e-3f59-4b92-b2bb-50d027bb92ff) + + + + + Version changed + The name of the autocreated EventType (532a95f3-db29-427e-bb32-d5a22029e586) + + + + + Version + The name of the ParamType of StateType (532a95f3-db29-427e-bb32-d5a22029e586) of DeviceClass Snap + + + + + Revision changed + The name of the autocreated EventType (f26a6404-e011-11e7-9224-2350048461eb) + + + + + Revision + The name of the ParamType of StateType (f26a6404-e011-11e7-9224-2350048461eb) of DeviceClass Snap + + + + + Rollback to previous version + The name of the ActionType e061dee6-62fc-45cc-9c9f-403c2be52939 of deviceClass Snap + + + + + Start update + The name of the ActionType 45626b75-f09d-4dd1-b6c4-ee33201b47b0 of deviceClass Update manager + + + + + Update manager + The name of the DeviceClass (d90cda58-4d8c-4b7f-a982-38e56a95b72a) + + + + + Update manager available changed + The name of the autocreated EventType (6b662b3e-fd12-4f24-be77-aec066f16d8c) + + + + + Update manager available + The name of the ParamType of StateType (6b662b3e-fd12-4f24-be77-aec066f16d8c) of DeviceClass Update manager + + + + + Snap + The name of the DeviceClass (ff0840d7-fcfc-4403-9d9f-301610d5a437) + + + + + System update running changed + The name of the autocreated EventType (01ca7a22-5607-4c5e-a465-a2ae7e8b529c) + + + + + System update running + The name of the ParamType of StateType (01ca7a22-5607-4c5e-a465-a2ae7e8b529c) of DeviceClass Update manager + + + + + Status changed + The name of the autocreated EventType (4987aca3-3916-4cb3-938f-df6c99d04dbf) + + + + + Status + The name of the ParamType of StateType (4987aca3-3916-4cb3-938f-df6c99d04dbf) of DeviceClass Update manager + + + + + Name + The name of the paramType (4f38614d-8be0-48dc-a24d-cee9ff1f2a89) of Snap + + + + + Channel changed + The name of the autocreated EventType (7be2b61e-3f59-4b92-b2bb-50d027bb92ff) + + + + + Check for updates + The name of the ActionType 4738f2c9-666e-45b9-91d3-7bcbf722b669 of deviceClass Update manager + + + +