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).
pull/59/head
Gavin Andresen 12 years ago
parent 2f0fa79db2
commit 8269a0953e

@ -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

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
<dict>
<key>CFBundleIconFile</key>
<string>bitcoin.icns</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleGetInfoString</key>
<string>Bitcoin-Qt</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleExecutable</key>
<string>Bitcoin-Qt</string>
<key>CFBundleIdentifier</key>
<string>org.bitcoinfoundation.Bitcoin-Qt</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.bitcoinfoundation.BitcoinPayment</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bitcoin</string>
</array>
</dict>
</array>
</dict>
</plist>

@ -9,12 +9,13 @@
#include "guiconstants.h"
#include "init.h"
#include "ui_interface.h"
#include "qtipcserver.h"
#include "paymentserver.h"
#include <QApplication>
#include <QMessageBox>
#include <QTextCodec>
#include <QLocale>
#include <QTimer>
#include <QTranslator>
#include <QSplashScreen>
#include <QLibraryInfo>
@ -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();

@ -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 <QApplication>
#include <QByteArray>
#include <QCoreApplication>
#include <QDataStream>
#include <QDebug>
#include <QFileOpenEvent>
#include <QHash>
#include <QLocalServer>
#include <QLocalSocket>
#include <QStringList>
#include <QUrl>
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<QFileOpenEvent*>(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);
}

@ -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 <QObject>
#include <QString>
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

@ -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 <boost/version.hpp>
#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 <boost/algorithm/string/predicate.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/interprocess/ipc/message_queue.hpp>
#include <boost/version.hpp>
#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

@ -1,16 +0,0 @@
#ifndef QTIPCSERVER_H
#define QTIPCSERVER_H
#include <string>
// 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
Loading…
Cancel
Save