IPC and custom protocol handler for monero://

This commit is contained in:
dsc 2019-03-22 21:02:08 +01:00 committed by xmrdsc
parent ff6ce6294b
commit 18f2accc7f
13 changed files with 354 additions and 15 deletions

View file

@ -43,6 +43,13 @@ filter::filter(QObject *parent) :
}
bool filter::eventFilter(QObject *obj, QEvent *ev) {
// macOS sends fileopen signal for incoming uri handlers
if (ev->type() == QEvent::FileOpen) {
QFileOpenEvent *openEvent = static_cast<QFileOpenEvent *>(ev);
QUrl scheme = openEvent->url();
emit uriHandler(scheme);
}
if(ev->type() == QEvent::KeyPress || ev->type() == QEvent::MouseButtonRelease){
emit userActivity();
}

View file

@ -49,6 +49,7 @@ signals:
void mousePressed(const QVariant &o, const QVariant &x, const QVariant &y);
void mouseReleased(const QVariant &o, const QVariant &x, const QVariant &y);
void userActivity();
void uriHandler(const QUrl &url);
};
#endif // FILTER_H

View file

@ -32,6 +32,7 @@
#include <QStandardPaths>
#include <QIcon>
#include <QDebug>
#include <QDesktopServices>
#include <QObject>
#include <QDesktopWidget>
#include <QScreen>
@ -60,6 +61,9 @@
#include "wallet/api/wallet2_api.h"
#include "Logger.h"
#include "MainApp.h"
#include "qt/ipc.h"
#include "qt/utils.h"
#include "qt/mime.h"
// IOS exclusions
#ifndef Q_OS_IOS
@ -82,6 +86,8 @@ int main(int argc, char *argv[])
// platform dependant settings
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
bool isDesktop = true;
#elif defined(Q_OS_LINUX)
bool isLinux = true;
#elif defined(Q_OS_ANDROID)
bool isAndroid = true;
#elif defined(Q_OS_IOS)
@ -127,6 +133,7 @@ int main(int argc, char *argv[])
QCommandLineOption logPathOption(QStringList() << "l" << "log-file",
QCoreApplication::translate("main", "Log to specified file"),
QCoreApplication::translate("main", "file"));
parser.addOption(logPathOption);
parser.addHelpOption();
parser.process(app);
@ -138,11 +145,33 @@ int main(int argc, char *argv[])
Monero::Wallet::init(argv[0], "monero-wallet-gui", logPath.toStdString().c_str(), true);
qInstallMessageHandler(messageHandler);
// Get default account name
QString accountName = getAccountName();
// loglevel is configured in main.qml. Anything lower than
// qWarning is not shown here.
qWarning().noquote() << "app startd" << "(log: " + logPath + ")";
#ifdef Q_OS_LINUX
registerXdgMime(app);
#endif
IPC *ipc = new IPC(&app);
QStringList posArgs = parser.positionalArguments();
for(int i = 0; i != posArgs.count(); i++){
QString arg = QString(posArgs.at(i));
if(arg.isEmpty() || arg.length() >= 512) continue;
if(arg.contains(reURI)){
if(!ipc->saveCommand(arg)){
return 0;
}
}
}
// start listening
QTimer::singleShot(0, ipc, SLOT(bind()));
// screen settings
// Mobile is designed on 128dpi
qreal ref_dpi = 128;
@ -252,6 +281,8 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty("mainApp", &app);
engine.rootContext()->setContextProperty("IPC", ipc);
engine.rootContext()->setContextProperty("qtRuntimeVersion", qVersion());
engine.rootContext()->setContextProperty("walletLogPath", logPath);
@ -295,14 +326,6 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty("moneroAccountsDir", moneroAccountsDir);
}
// Get default account name
QString accountName = qgetenv("USER"); // mac/linux
if (accountName.isEmpty())
accountName = qgetenv("USERNAME"); // Windows
if (accountName.isEmpty())
accountName = "My monero Account";
engine.rootContext()->setContextProperty("defaultAccountName", accountName);
engine.rootContext()->setContextProperty("applicationDirectory", QApplication::applicationDirPath());
engine.rootContext()->setContextProperty("idealThreadCount", QThread::idealThreadCount());
@ -312,7 +335,6 @@ int main(int argc, char *argv[])
builtWithScanner = true;
#endif
engine.rootContext()->setContextProperty("builtWithScanner", builtWithScanner);
// Load main window (context properties needs to be defined obove this line)
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
if (engine.rootObjects().isEmpty())
@ -345,5 +367,6 @@ int main(int argc, char *argv[])
QObject::connect(eventFilter, SIGNAL(mousePressed(QVariant,QVariant,QVariant)), rootObject, SLOT(mousePressed(QVariant,QVariant,QVariant)));
QObject::connect(eventFilter, SIGNAL(mouseReleased(QVariant,QVariant,QVariant)), rootObject, SLOT(mouseReleased(QVariant,QVariant,QVariant)));
QObject::connect(eventFilter, SIGNAL(userActivity()), rootObject, SLOT(userActivity()));
QObject::connect(eventFilter, SIGNAL(uriHandler(QUrl)), ipc, SLOT(parseCommand(QUrl)));
return app.exec();
}

View file

@ -399,6 +399,49 @@ ApplicationWindow {
leftPanel.balanceLabelText = qsTr("Balance (#%1%2)").arg(currentWallet.currentSubaddressAccount).arg(accountLabel === "" ? "" : (" " + accountLabel));
}
function onUriHandler(uri){
if(uri.startsWith("monero://")){
var address = uri.substring("monero://".length);
var params = {}
if(address.length === 0) return;
var spl = address.split("?");
if(spl.length > 2) return;
if(spl.length >= 1) {
// parse additional params
address = spl[0];
if(spl.length === 2){
spl.shift();
var item = spl[0];
var _spl = item.split("&");
for (var param in _spl){
var _item = _spl[param];
if(!_item.indexOf("=") > 0) continue;
var __spl = _item.split("=");
if(__spl.length !== 2) continue;
params[__spl[0]] = __spl[1];
}
}
}
// Fill fields
middlePanel.transferView.sendTo(address, params["tx_payment_id"], params["tx_description"], params["tx_amount"]);
// Raise window
appWindow.raise();
appWindow.show();
// @TODO: remove after paymentID deprecation
if(params.hasOwnProperty("tx_payment_id"))
persistentSettings.showPid = true;
}
}
function onWalletConnectionStatusChanged(status){
console.log("Wallet connection status changed " + status)
middlePanel.updateStatus();
@ -489,6 +532,12 @@ ApplicationWindow {
// Force switch normal view
rootItem.state = "normal";
// Process queued IPC command
if(typeof IPC !== "undefined" && IPC.queuedCmd().length > 0){
var queuedCmd = IPC.queuedCmd();
if(/^\w+:\/\/(.*)$/.test(queuedCmd)) appWindow.onUriHandler(queuedCmd); // uri
}
}
function onWalletClosed(walletAddress) {
@ -1086,6 +1135,7 @@ ApplicationWindow {
walletManager.deviceButtonPressed.connect(onDeviceButtonPressed);
walletManager.checkUpdatesComplete.connect(onWalletCheckUpdatesComplete);
walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeeded);
IPC.uriHandler.connect(onUriHandler);
if(typeof daemonManager != "undefined") {
daemonManager.daemonStarted.connect(onDaemonStarted);

View file

@ -61,7 +61,10 @@ HEADERS += \
src/zxcvbn-c/zxcvbn.h \
src/libwalletqt/UnsignedTransaction.h \
Logger.h \
MainApp.h
MainApp.h \
src/qt/ipc.h \
src/qt/mime.h \
src/qt/utils.h
SOURCES += main.cpp \
filter.cpp \
@ -89,7 +92,10 @@ SOURCES += main.cpp \
src/zxcvbn-c/zxcvbn.c \
src/libwalletqt/UnsignedTransaction.cpp \
Logger.cpp \
MainApp.cpp
MainApp.cpp \
src/qt/ipc.cpp \
src/qt/mime.cpp \
src/qt/utils.cpp
CONFIG(DISABLE_PASS_STRENGTH_METER) {
HEADERS -= src/zxcvbn-c/zxcvbn.h

View file

@ -708,10 +708,20 @@ Rectangle {
}
// Popuplate fields from addressbook.
function sendTo(address, paymentId, description){
function sendTo(address, paymentId, description, amount){
middlePanel.state = 'Transfer';
if(typeof address !== 'undefined')
addressLine.text = address
if(typeof paymentId !== 'undefined')
setPaymentId(paymentId);
if(typeof description !== 'undefined')
setDescription(description);
if(typeof amount !== 'undefined')
amountLine.text = amount;
}
function updateSendButton(){

View file

@ -31,5 +31,25 @@
<key>NSRequiresAquaSystemAppearance</key>
<string>True</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>monero Handler</string>
<key>CFBundleURLSchemes</key>
<array>
<string>monero</string>
</array>
</dict>
<dict>
<key>CFBundleURLName</key>
<string>moneroseed Handler</string>
<key>CFBundleURLSchemes</key>
<array>
<string>moneroseed</string>
</array>
</dict>
</array>
</dict>
</plist>

106
src/qt/ipc.cpp Normal file
View file

@ -0,0 +1,106 @@
#include <QCoreApplication>
#include <QLocalSocket>
#include <QLocalServer>
#include <QtNetwork>
#include <QDebug>
#include "ipc.h"
#include "utils.h"
// Start listening for incoming IPC commands on UDS (Unix) or named pipe (Windows)
void IPC::bind(){
QString path = QString(this->m_socketFile.absoluteFilePath());
qDebug() << path;
this->m_server = new QLocalServer(this);
this->m_server->setSocketOptions(QLocalServer::UserAccessOption);
bool restarted = false;
if(!this->m_server->listen(path)){
// On Unix if the server crashes without closing listen will fail with AddressInUseError.
// To create a new server the file should be removed. On Windows two local servers can listen
// to the same pipe at the same time, but any connections will go to one of the server.
#ifdef Q_OS_UNIX
qDebug() << QString("Unable to start IPC server in \"%1\": \"%2\". Retrying.").arg(path).arg(this->m_server->errorString());
if(this->m_socketFile.exists()){
QFile file(path);
file.remove();
if(this->m_server->listen(path)){
restarted = true;
}
}
#endif
if(!restarted)
qDebug() << QString("Unable to start IPC server in \"%1\": \"%2\".").arg(path).arg(this->m_server->errorString());
}
connect(this->m_server, &QLocalServer::newConnection, this, &IPC::handleConnection);
}
// Process incoming IPC command. First check if monero-wallet-gui is
// already running. If it is, send it to that instance instead, if not,
// queue the command for later use inside our QML engine. Returns true
// when queued, false if sent to another instance, at which point we can
// kill the current process.
bool IPC::saveCommand(QString cmdString){
qDebug() << QString("saveCommand called: %1").arg(cmdString);
QLocalSocket ls;
QByteArray buffer;
buffer = buffer.append(cmdString);
QString socketFilePath = this->socketFile().filePath();
ls.connectToServer(socketFilePath, QIODevice::WriteOnly);
if(ls.waitForConnected(1000)){
ls.write(buffer);
if (!ls.waitForBytesWritten(1000)){
qDebug() << QString("Could not send command \"%1\" over IPC %2: \"%3\"").arg(cmdString, socketFilePath, ls.errorString());
return false;
}
qDebug() << QString("Sent command \"%1\" over IPC \"%2\"").arg(cmdString, socketFilePath);
return false;
}
if(ls.isOpen())
ls.disconnectFromServer();
// Queue for later
this->SetQueuedCmd(cmdString);
return true;
}
bool IPC::saveCommand(const QUrl &url){;
this->saveCommand(url.toString());
}
void IPC::handleConnection(){
QLocalSocket *clientConnection = this->m_server->nextPendingConnection();
connect(clientConnection, &QLocalSocket::disconnected,
clientConnection, &QLocalSocket::deleteLater);
clientConnection->waitForReadyRead(2);
QByteArray cmdArray = clientConnection->readAll();
QString cmdString = QTextCodec::codecForMib(106)->toUnicode(cmdArray); // UTF-8
qDebug() << cmdString;
this->parseCommand(cmdString);
clientConnection->close();
delete clientConnection;
}
void IPC::parseCommand(const QUrl &url){
this->parseCommand(url.toString());
}
void IPC::parseCommand(QString cmdString){
if(cmdString.contains(reURI)){
this->emitUriHandler(cmdString);
}
}
void IPC::emitUriHandler(QString uriString){
emit uriHandler(uriString);
}

35
src/qt/ipc.h Normal file
View file

@ -0,0 +1,35 @@
#ifndef IPC_H
#define IPC_H
#include <QtCore>
#include <QLocalServer>
#include <qt/utils.h>
class IPC : public QObject
{
Q_OBJECT
public:
IPC(QObject *parent = 0) : QObject(parent) {}
QFileInfo socketFile() const { return m_socketFile; }
Q_INVOKABLE QString queuedCmd() { return m_queuedCmd; }
void SetQueuedCmd(const QString cmdString) { m_queuedCmd = cmdString; }
public slots:
void bind();
void handleConnection();
bool saveCommand(QString cmdString);
bool saveCommand(const QUrl &url);
void parseCommand(QString cmdString);
void parseCommand(const QUrl &url);
void emitUriHandler(QString uriString);
signals:
void uriHandler(QString uriString);
private:
QLocalServer *m_server;
QString m_queuedCmd;
QFileInfo m_socketFile = QFileInfo(QString(QDir::tempPath() + "/xmr-gui_%2.sock").arg(getAccountName()));
};
#endif // IPC_H

42
src/qt/mime.cpp Normal file
View file

@ -0,0 +1,42 @@
#include <QtCore>
#include <QApplication>
#include <QFile>
#include <QTextStream>
#include "mime.h"
#include "utils.h"
void registerXdgMime(QApplication &app){
// MacOS handled via Info.plist
// Windows handled in the installer by rbrunner7
QString xdg = QString(
"[Desktop Entry]\n"
"Name=Monero GUI\n"
"GenericName=Monero-GUI\n"
"X-GNOME-FullName=Monero-GUI\n"
"Comment=Monero GUI\n"
"Keywords=Monero;\n"
"Exec=%1 %u\n"
"Terminal=false\n"
"Type=Application\n"
"Icon=monero\n"
"Categories=Network;GNOME;Qt;\n"
"MimeType=x-scheme-handler/monero;x-scheme-handler/moneroseed\n"
"StartupNotify=true\n"
"X-GNOME-Bugzilla-Bugzilla=GNOME\n"
"X-GNOME-UsesNotifications=true\n"
).arg(app.applicationFilePath());
QString appPath = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
QString filePath = QString("%1/monero-gui.desktop").arg(appPath);
qDebug() << QString("Writing %1").arg(filePath);
QFile file(filePath);
if(file.open(QIODevice::WriteOnly)){
QTextStream out(&file); out << xdg << endl;
file.close();
}
else
file.close();
}

8
src/qt/mime.h Normal file
View file

@ -0,0 +1,8 @@
#ifndef MIME_H
#define MIME_H
#include <QApplication>
void registerXdgMime(QApplication &app);
#endif // MIME_H

20
src/qt/utils.cpp Normal file
View file

@ -0,0 +1,20 @@
#include <QtCore>
#include "utils.h"
bool fileExists(QString path) {
QFileInfo check_file(path);
if (check_file.exists() && check_file.isFile())
return true;
else
return false;
}
QString getAccountName(){
QString accountName = qgetenv("USER"); // mac/linux
if (accountName.isEmpty())
accountName = qgetenv("USERNAME"); // Windows
if (accountName.isEmpty())
accountName = "My monero Account";
return accountName;
}

11
src/qt/utils.h Normal file
View file

@ -0,0 +1,11 @@
#ifndef UTILS_H
#define UTILS_H
#include <QtCore>
#include <QRegExp>
bool fileExists(QString path);
QString getAccountName();
const static QRegExp reURI = QRegExp("^\\w+:\\/\\/([\\w+\\-?\\-_\\-=\\-&]+)");
#endif // UTILS_H