From 8269a0953ee9ccbdc422433fc37184e60f94b178 Mon Sep 17 00:00:00 2001 From: Gavin Andresen Date: Mon, 11 Feb 2013 18:52:30 -0500 Subject: [PATCH] Reimplement click-to-pay links. Add OSX support. Switch to using Qt's QLocalServer/QLocalSocket to handle bitcoin payment links (bitcoin:... URIs) Reason for switch: the boost::interprocess mechanism seemed flaky, and doesn't mesh as well with "The Qt Way" qtipcserver.cpp/h is replaced by paymentserver.cpp/h Click-to-pay now also works on OSX, with a custom Info.plist that registers Bitcoin-Qt as a handler for bitcoin: URLs and an event listener on the main QApplication that handles QFileOpenEvents (Qt translates 'url clicked' AppleEvents into QFileOpenEvents automagically). --- bitcoin-qt.pro | 6 +- share/qt/Info.plist | 31 ++++++++ src/qt/bitcoin.cpp | 33 +++----- src/qt/paymentserver.cpp | 159 +++++++++++++++++++++++++++++++++++++ src/qt/paymentserver.h | 66 ++++++++++++++++ src/qt/qtipcserver.cpp | 165 --------------------------------------- src/qt/qtipcserver.h | 16 ---- 7 files changed, 272 insertions(+), 204 deletions(-) create mode 100644 share/qt/Info.plist create mode 100644 src/qt/paymentserver.cpp create mode 100644 src/qt/paymentserver.h delete mode 100644 src/qt/qtipcserver.cpp delete mode 100644 src/qt/qtipcserver.h diff --git a/bitcoin-qt.pro b/bitcoin-qt.pro index 7bab4a497..89b9f338b 100644 --- a/bitcoin-qt.pro +++ b/bitcoin-qt.pro @@ -2,6 +2,7 @@ TEMPLATE = app TARGET = bitcoin-qt VERSION = 0.8.0 INCLUDEPATH += src src/json src/qt +QT += network DEFINES += QT_GUI BOOST_THREAD_USE_LIB BOOST_SPIRIT_THREADSAFE CONFIG += no_include_pwd CONFIG += thread @@ -195,7 +196,7 @@ HEADERS += src/qt/bitcoingui.h \ src/qt/askpassphrasedialog.h \ src/protocol.h \ src/qt/notificator.h \ - src/qt/qtipcserver.h \ + src/qt/paymentserver.h \ src/allocators.h \ src/ui_interface.h \ src/qt/rpcconsole.h \ @@ -264,7 +265,7 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/askpassphrasedialog.cpp \ src/protocol.cpp \ src/qt/notificator.cpp \ - src/qt/qtipcserver.cpp \ + src/qt/paymentserver.cpp \ src/qt/rpcconsole.cpp \ src/noui.cpp \ src/leveldb.cpp \ @@ -385,6 +386,7 @@ macx:TARGET = "Bitcoin-Qt" macx:QMAKE_CFLAGS_THREAD += -pthread macx:QMAKE_LFLAGS_THREAD += -pthread macx:QMAKE_CXXFLAGS_THREAD += -pthread +macx:QMAKE_INFO_PLIST = share/qt/Info.plist # Set libraries and includes at end, to use platform-defined defaults if not overridden INCLUDEPATH += $$BOOST_INCLUDE_PATH $$BDB_INCLUDE_PATH $$OPENSSL_INCLUDE_PATH $$QRENCODE_INCLUDE_PATH diff --git a/share/qt/Info.plist b/share/qt/Info.plist new file mode 100644 index 000000000..58b2152e9 --- /dev/null +++ b/share/qt/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleIconFile + bitcoin.icns + CFBundlePackageType + APPL + CFBundleGetInfoString + Bitcoin-Qt + CFBundleSignature + ???? + CFBundleExecutable + Bitcoin-Qt + CFBundleIdentifier + org.bitcoinfoundation.Bitcoin-Qt + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + org.bitcoinfoundation.BitcoinPayment + CFBundleURLSchemes + + bitcoin + + + + + diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index e5526a6c0..1b5ef28ba 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -9,12 +9,13 @@ #include "guiconstants.h" #include "init.h" #include "ui_interface.h" -#include "qtipcserver.h" +#include "paymentserver.h" #include #include #include #include +#include #include #include #include @@ -70,15 +71,6 @@ static bool ThreadSafeAskFee(int64 nFeeRequired) return payFee; } -static void ThreadSafeHandleURI(const std::string& strURI) -{ - if(!guiref) - return; - - QMetaObject::invokeMethod(guiref, "handleURI", GUIUtil::blockingGUIThreadConnection(), - Q_ARG(QString, QString::fromStdString(strURI))); -} - static void InitMessage(const std::string &message) { if(splashref) @@ -117,14 +109,6 @@ int main(int argc, char *argv[]) // Command-line options take precedence: ParseParameters(argc, argv); - if(GetBoolArg("-testnet")) // Separate message queue name for testnet - strBitcoinURIQueueName = BITCOINURI_QUEUE_NAME_TESTNET; - else - strBitcoinURIQueueName = BITCOINURI_QUEUE_NAME_MAINNET; - - // Do this early as we don't want to bother initializing if we are just calling IPC - ipcScanRelay(argc, argv); - // Internal string conversion is all UTF-8 QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8")); QTextCodec::setCodecForCStrings(QTextCodec::codecForTr()); @@ -132,6 +116,12 @@ int main(int argc, char *argv[]) Q_INIT_RESOURCE(bitcoin); QApplication app(argc, argv); + // Do this early as we don't want to bother initializing if we are just calling IPC + // ... but do it after creating app, so QCoreApplication::arguments is initialized: + if (PaymentServer::ipcSendCommandLine()) + exit(0); + PaymentServer* paymentServer = new PaymentServer(&app); + // Install global event filter that makes sure that long tooltips can be word-wrapped app.installEventFilter(new GUIUtil::ToolTipToRichTextFilter(TOOLTIP_WRAP_THRESHOLD, &app)); @@ -188,7 +178,6 @@ int main(int argc, char *argv[]) // Subscribe to global signals from core uiInterface.ThreadSafeMessageBox.connect(ThreadSafeMessageBox); uiInterface.ThreadSafeAskFee.connect(ThreadSafeAskFee); - uiInterface.ThreadSafeHandleURI.connect(ThreadSafeHandleURI); uiInterface.InitMessage.connect(InitMessage); uiInterface.QueueShutdown.connect(QueueShutdown); uiInterface.Translate.connect(Translate); @@ -249,8 +238,10 @@ int main(int argc, char *argv[]) window.show(); } - // Place this here as guiref has to be defined if we don't want to lose URIs - ipcInit(argc, argv); + // Now that initialization/startup is done, process any command-line + // bitcoin: URIs + QObject::connect(paymentServer, SIGNAL(receivedURI(QString)), &window, SLOT(handleURI(QString))); + QTimer::singleShot(100, paymentServer, SLOT(uiReady())); app.exec(); diff --git a/src/qt/paymentserver.cpp b/src/qt/paymentserver.cpp new file mode 100644 index 000000000..05f2ac10e --- /dev/null +++ b/src/qt/paymentserver.cpp @@ -0,0 +1,159 @@ +// Copyright (c) 2009-2012 The Bitcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "paymentserver.h" +#include "guiconstants.h" +#include "ui_interface.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace boost; + +const int BITCOIN_IPC_CONNECT_TIMEOUT = 1000; // milliseconds +const QString BITCOIN_IPC_PREFIX("bitcoin:"); + +// +// Create a name that is unique for: +// testnet / non-testnet +// data directory +// +static QString ipcServerName() +{ + QString name("BitcoinQt"); + + // Append a simple hash of the datadir + // Note that GetDataDir(true) returns a different path + // for -testnet versus main net + QString ddir(GetDataDir(true).string().c_str()); + name.append(QString::number(qHash(ddir))); + + return name; +} + +// +// This stores payment requests received before +// the main GUI window is up and ready to ask the user +// to send payment. +// +static QStringList savedPaymentRequests; + +// +// Sending to the server is done synchronously, at startup. +// If the server isn't already running, startup continues, +// and the items in savedPaymentRequest will be handled +// when uiReady() is called. +// +bool PaymentServer::ipcSendCommandLine() +{ + bool fResult = false; + + const QStringList& args = QCoreApplication::arguments(); + for (int i = 1; i < args.size(); i++) + { + if (!args[i].startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive)) + continue; + savedPaymentRequests.append(args[i]); + } + + foreach (const QString& arg, savedPaymentRequests) + { + QLocalSocket* socket = new QLocalSocket(); + socket->connectToServer(ipcServerName(), QIODevice::WriteOnly); + if (!socket->waitForConnected(BITCOIN_IPC_CONNECT_TIMEOUT)) + return false; + + QByteArray block; + QDataStream out(&block, QIODevice::WriteOnly); + out.setVersion(QDataStream::Qt_4_0); + out << arg; + out.device()->seek(0); + socket->write(block); + socket->flush(); + + socket->waitForBytesWritten(BITCOIN_IPC_CONNECT_TIMEOUT); + socket->disconnectFromServer(); + delete socket; + fResult = true; + } + return fResult; +} + +PaymentServer::PaymentServer(QApplication* parent) : QObject(parent), saveURIs(true) +{ + // Install global event filter to catch QFileOpenEvents on the mac (sent when you click bitcoin: links) + parent->installEventFilter(this); + + QString name = ipcServerName(); + + // Clean up old socket leftover from a crash: + QLocalServer::removeServer(name); + + uriServer = new QLocalServer(this); + + if (!uriServer->listen(name)) + qDebug() << tr("Cannot start bitcoin: click-to-pay handler"); + else + connect(uriServer, SIGNAL(newConnection()), this, SLOT(handleURIConnection())); +} + +bool PaymentServer::eventFilter(QObject *object, QEvent *event) +{ + // clicking on bitcoin: URLs creates FileOpen events on the Mac: + if (event->type() == QEvent::FileOpen) + { + QFileOpenEvent* fileEvent = static_cast(event); + if (!fileEvent->url().isEmpty()) + { + if (saveURIs) // Before main window is ready: + savedPaymentRequests.append(fileEvent->url().toString()); + else + emit receivedURI(fileEvent->url().toString()); + return true; + } + } + return false; +} + +void PaymentServer::uiReady() +{ + saveURIs = false; + foreach (const QString& s, savedPaymentRequests) + emit receivedURI(s); + savedPaymentRequests.clear(); +} + +void PaymentServer::handleURIConnection() +{ + QLocalSocket *clientConnection = uriServer->nextPendingConnection(); + + while (clientConnection->bytesAvailable() < (int)sizeof(quint32)) + clientConnection->waitForReadyRead(); + + connect(clientConnection, SIGNAL(disconnected()), + clientConnection, SLOT(deleteLater())); + + QDataStream in(clientConnection); + in.setVersion(QDataStream::Qt_4_0); + if (clientConnection->bytesAvailable() < (int)sizeof(quint16)) { + return; + } + QString message; + in >> message; + + if (saveURIs) + savedPaymentRequests.append(message); + else + emit receivedURI(message); +} diff --git a/src/qt/paymentserver.h b/src/qt/paymentserver.h new file mode 100644 index 000000000..cfc48afb3 --- /dev/null +++ b/src/qt/paymentserver.h @@ -0,0 +1,66 @@ +#ifndef PAYMENTSERVER_H +#define PAYMENTSERVER_H + +// +// This class handles payment requests from clicking on +// bitcoin: URIs +// +// This is somewhat tricky, because we have to deal with +// the situation where the user clicks on a link during +// startup/initialization, when the splash-screen is up +// but the main window (and the Send Coins tab) is not. +// +// So, the strategy is: +// +// Create the server, and register the event handler, +// when the application is created. Save any URIs +// received at or during startup in a list. +// +// When startup is finished and the main window is +// show, a signal is sent to slot uiReady(), which +// emits a receivedURL() signal for any payment +// requests that happened during startup. +// +// After startup, receivedURL() happens as usual. +// +// This class has one more feature: a static +// method that finds URIs passed in the command line +// and, if a server is running in another process, +// sends them to the server. +// +#include +#include + +class QApplication; +class QLocalServer; + +class PaymentServer : public QObject +{ + Q_OBJECT +private: + bool saveURIs; + QLocalServer* uriServer; + +public: + // Returns true if there were URIs on the command line + // which were successfully sent to an already-running + // process. + static bool ipcSendCommandLine(); + + PaymentServer(QApplication* parent); + + bool eventFilter(QObject *object, QEvent *event); + +signals: + void receivedURI(QString); + +public slots: + // Signal this when the main window's UI is ready + // to display payment requests to the user + void uiReady(); + +private slots: + void handleURIConnection(); +}; + +#endif // PAYMENTSERVER_H diff --git a/src/qt/qtipcserver.cpp b/src/qt/qtipcserver.cpp deleted file mode 100644 index 2777fab85..000000000 --- a/src/qt/qtipcserver.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) 2009-2012 The Bitcoin developers -// Distributed under the MIT/X11 software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -#include -#if defined(WIN32) && BOOST_VERSION == 104900 -#define BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME -#define BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME -#endif - -#include "qtipcserver.h" -#include "guiconstants.h" -#include "ui_interface.h" -#include "util.h" - -#include -#include -#include -#include - -#if defined(WIN32) && (!defined(BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME) || !defined(BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME) || BOOST_VERSION < 104900) -#warning Compiling without BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME and BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME uncommented in boost/interprocess/detail/tmp_dir_helpers.hpp or using a boost version before 1.49 may have unintended results see svn.boost.org/trac/boost/ticket/5392 -#endif - -using namespace boost; -using namespace boost::interprocess; -using namespace boost::posix_time; - -// holds Bitcoin-Qt message queue name (initialized in bitcoin.cpp) -std::string strBitcoinURIQueueName; - -#if defined MAC_OSX || defined __FreeBSD__ -// URI handling not implemented on OSX yet - -void ipcScanRelay(int argc, char *argv[]) { } -void ipcInit(int argc, char *argv[]) { } - -#else - -static void ipcThread2(void* pArg); - -static bool ipcScanCmd(int argc, char *argv[], bool fRelay) -{ - // Check for URI in argv - bool fSent = false; - for (int i = 1; i < argc; i++) - { - if (boost::algorithm::istarts_with(argv[i], "bitcoin:")) - { - const char *strURI = argv[i]; - try { - boost::interprocess::message_queue mq(boost::interprocess::open_only, strBitcoinURIQueueName.c_str()); - if (mq.try_send(strURI, strlen(strURI), 0)) - fSent = true; - else if (fRelay) - break; - } - catch (boost::interprocess::interprocess_exception &ex) { - // don't log the "file not found" exception, because that's normal for - // the first start of the first instance - if (ex.get_error_code() != boost::interprocess::not_found_error || !fRelay) - { - printf("main() - boost interprocess exception #%d: %s\n", ex.get_error_code(), ex.what()); - break; - } - } - } - } - return fSent; -} - -void ipcScanRelay(int argc, char *argv[]) -{ - if (ipcScanCmd(argc, argv, true)) - exit(0); -} - -static void ipcThread(void* pArg) -{ - // Make this thread recognisable as the GUI-IPC thread - RenameThread("bitcoin-gui-ipc"); - - try - { - ipcThread2(pArg); - } - catch (std::exception& e) { - PrintExceptionContinue(&e, "ipcThread()"); - } catch (...) { - PrintExceptionContinue(NULL, "ipcThread()"); - } - printf("ipcThread exited\n"); -} - -static void ipcThread2(void* pArg) -{ - printf("ipcThread started\n"); - - message_queue* mq = (message_queue*)pArg; - char buffer[MAX_URI_LENGTH + 1] = ""; - size_t nSize = 0; - unsigned int nPriority = 0; - - loop - { - ptime d = boost::posix_time::microsec_clock::universal_time() + millisec(100); - if (mq->timed_receive(&buffer, sizeof(buffer), nSize, nPriority, d)) - { - uiInterface.ThreadSafeHandleURI(std::string(buffer, nSize)); - Sleep(1000); - } - - if (fShutdown) - break; - } - - // Remove message queue - message_queue::remove(strBitcoinURIQueueName.c_str()); - // Cleanup allocated memory - delete mq; -} - -void ipcInit(int argc, char *argv[]) -{ - message_queue* mq = NULL; - char buffer[MAX_URI_LENGTH + 1] = ""; - size_t nSize = 0; - unsigned int nPriority = 0; - - try { - mq = new message_queue(open_or_create, strBitcoinURIQueueName.c_str(), 2, MAX_URI_LENGTH); - - // Make sure we don't lose any bitcoin: URIs - for (int i = 0; i < 2; i++) - { - ptime d = boost::posix_time::microsec_clock::universal_time() + millisec(1); - if (mq->timed_receive(&buffer, sizeof(buffer), nSize, nPriority, d)) - { - uiInterface.ThreadSafeHandleURI(std::string(buffer, nSize)); - } - else - break; - } - - // Make sure only one bitcoin instance is listening - message_queue::remove(strBitcoinURIQueueName.c_str()); - delete mq; - - mq = new message_queue(open_or_create, strBitcoinURIQueueName.c_str(), 2, MAX_URI_LENGTH); - } - catch (interprocess_exception &ex) { - printf("ipcInit() - boost interprocess exception #%d: %s\n", ex.get_error_code(), ex.what()); - return; - } - - if (!NewThread(ipcThread, mq)) - { - delete mq; - return; - } - - ipcScanCmd(argc, argv, false); -} - -#endif diff --git a/src/qt/qtipcserver.h b/src/qt/qtipcserver.h deleted file mode 100644 index f775f272c..000000000 --- a/src/qt/qtipcserver.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef QTIPCSERVER_H -#define QTIPCSERVER_H - -#include - -// Define Bitcoin-Qt message queue name for mainnet -#define BITCOINURI_QUEUE_NAME_MAINNET "BitcoinURI" -// Define Bitcoin-Qt message queue name for testnet -#define BITCOINURI_QUEUE_NAME_TESTNET "BitcoinURI-testnet" - -extern std::string strBitcoinURIQueueName; - -void ipcScanRelay(int argc, char *argv[]); -void ipcInit(int argc, char *argv[]); - -#endif // QTIPCSERVER_H