Revision control

Copy as Markdown

// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qpcscmanager_p.h"
#include "qpcscslot_p.h"
#include "qpcsccard_p.h"
#include <QtCore/QLoggingCategory>
#include <QtCore/QThread>
#include <QtCore/QTimer>
QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(QT_NFC_PCSC)
static constexpr auto PollIntervalEnvVar = "QT_NFC_POLL_INTERVAL_MS";
static constexpr int DefaultPollIntervalMs = 100;
QPcscManager::QPcscManager(QObject *parent) : QObject(parent)
{
qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO;
int pollInterval = DefaultPollIntervalMs;
QByteArray intervalEnv = qgetenv(PollIntervalEnvVar);
if (!intervalEnv.isEmpty()) {
if (int intervalFromEnv = intervalEnv.toInt(); intervalFromEnv > 0)
pollInterval = intervalFromEnv;
else
qCWarning(QT_NFC_PCSC) << PollIntervalEnvVar << "set to an invalid value";
}
m_stateUpdateTimer = new QTimer(this);
m_stateUpdateTimer->setInterval(pollInterval);
connect(m_stateUpdateTimer, &QTimer::timeout, this, &QPcscManager::onStateUpdate);
}
QPcscManager::~QPcscManager()
{
qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO;
if (m_hasContext) {
// Destroy all card handles before destroying the PCSC context.
for (auto slot : std::as_const(m_slots))
slot->invalidateInsertedCard();
SCardReleaseContext(m_context);
}
// Stop the worker thread.
thread()->quit();
}
void QPcscManager::processSlotUpdates()
{
for (auto &state : m_slotStates) {
auto slot = static_cast<QPcscSlot *>(state.pvUserData);
Q_ASSERT(slot != nullptr);
if ((state.dwEventState & SCARD_STATE_UNKNOWN) != 0)
continue;
if (state.dwEventState == state.dwCurrentState)
continue;
qCDebug(QT_NFC_PCSC) << Qt::hex << state.dwCurrentState << "=>" << state.dwEventState << ":"
<< slot->name();
state.dwCurrentState = state.dwEventState;
slot->processStateChange(state.dwEventState, m_targetDetectionRunning);
}
}
/*
Remove slots that no longer need to be tracked.
*/
void QPcscManager::removeSlots()
{
for (auto &state : m_slotStates) {
auto slot = static_cast<QPcscSlot *>(state.pvUserData);
Q_ASSERT(slot != nullptr);
// Remove slots that no longer exist, or all slots without cards if
// target detection is stopped.
if ((state.dwEventState & SCARD_STATE_UNKNOWN) != 0
|| !(m_targetDetectionRunning || slot->hasCard())) {
qCDebug(QT_NFC_PCSC) << "Removing slot:" << slot;
state.dwEventState = SCARD_STATE_UNKNOWN;
slot->invalidateInsertedCard();
m_slots.remove(slot->name());
slot->deleteLater();
state.pvUserData = nullptr;
}
}
// Remove state tracking entries for slots that no longer exist.
m_slotStates.removeIf(
[](const auto &state) { return (state.dwEventState & SCARD_STATE_UNKNOWN) != 0; });
}
/*
Reads the system slot lists and marks slots that no longer exists, also
creates new slot entries if target detection is currently running.
*/
void QPcscManager::updateSlotList()
{
Q_ASSERT(m_hasContext);
#ifndef SCARD_AUTOALLOCATE
// macOS does not support automatic allocation. Try using a fixed-size
// buffer first, extending it if it is not sufficient.
#define LIST_READER_BUFFER_EXTRA 1024
QPcscSlotName buf(nullptr);
DWORD listSize = LIST_READER_BUFFER_EXTRA;
buf.resize(listSize);
QPcscSlotName::Ptr list = buf.ptr();
auto ret = SCardListReaders(m_context, nullptr, list, &listSize);
#else
QPcscSlotName::Ptr list;
DWORD listSize = SCARD_AUTOALLOCATE;
auto ret = SCardListReaders(m_context, nullptr, reinterpret_cast<QPcscSlotName::Ptr>(&list),
&listSize);
#endif
if (ret == LONG(SCARD_E_NO_READERS_AVAILABLE)) {
list = nullptr;
ret = SCARD_S_SUCCESS;
}
#ifndef SCARD_AUTOALLOCATE
else if (ret == LONG(SCARD_E_INSUFFICIENT_BUFFER)) {
// SCardListReaders() has set listSize to the required size. We add
// extra space to reduce possibility of failure if the reader list has
// changed since the last call.
listSize += LIST_READER_BUFFER_EXTRA;
buf.resize(listSize);
list = buf.ptr();
ret = SCardListReaders(m_context, nullptr, list, &listSize);
if (ret == LONG(SCARD_E_NO_READERS_AVAILABLE)) {
list = nullptr;
ret = SCARD_S_SUCCESS;
}
}
#undef LIST_READER_BUFFER_EXTRA
#endif
if (ret != SCARD_S_SUCCESS) {
qCDebug(QT_NFC_PCSC) << "Failed to list readers:" << QPcsc::errorMessage(ret);
return;
}
#ifdef SCARD_AUTOALLOCATE
auto freeList = qScopeGuard([this, list] {
if (list) {
Q_ASSERT(m_hasContext);
SCardFreeMemory(m_context, list);
}
});
#endif
QSet<QPcscSlotName> presentSlots;
if (list != nullptr) {
for (const auto *p = list; *p; p += QPcscSlotName::nameSize(p) + 1)
presentSlots.insert(QPcscSlotName(p));
}
// Check current state list and mark slots that are not present anymore to
// be removed later.
for (auto &state : m_slotStates) {
auto slot = static_cast<QPcscSlot *>(state.pvUserData);
Q_ASSERT(slot != nullptr);
if (presentSlots.contains(slot->name()))
presentSlots.remove(slot->name());
else
state.dwEventState = SCARD_STATE_UNKNOWN;
}
if (!m_targetDetectionRunning)
return;
// Add new slots
for (auto &&slotName : std::as_const(presentSlots)) {
QPcscSlot *slot = new QPcscSlot(slotName, this);
qCDebug(QT_NFC_PCSC) << "New slot:" << slot;
m_slots[slotName] = slot;
SCARD_READERSTATE state {};
state.pvUserData = slot;
state.szReader = slot->name().ptr();
state.dwCurrentState = SCARD_STATE_UNAWARE;
m_slotStates.append(state);
}
}
bool QPcscManager::establishContext()
{
qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO;
Q_ASSERT(!m_hasContext);
LONG ret = SCardEstablishContext(SCARD_SCOPE_USER, nullptr, nullptr, &m_context);
if (ret != SCARD_S_SUCCESS) {
qCWarning(QT_NFC_PCSC) << "Failed to establish context:" << QPcsc::errorMessage(ret);
return false;
}
m_hasContext = true;
return true;
}
void QPcscManager::onStateUpdate()
{
if (!m_hasContext) {
if (!m_targetDetectionRunning) {
m_stateUpdateTimer->stop();
return;
}
if (!establishContext())
return;
}
updateSlotList();
removeSlots();
if (m_slotStates.isEmpty()) {
if (!m_targetDetectionRunning) {
// Free the context if it is no longer needed to card tracking.
SCardReleaseContext(m_context);
m_hasContext = false;
m_stateUpdateTimer->stop();
}
return;
}
// Both Windows and PCSCLite support interruptable continuos state detection
// where SCardCancel() can be used to abort it from another thread.
// But that introduces a problem of reliable cancelling of the status change
// call and no other. Reliable use of SCardCancel() would probably require
// some form of synchronization and polling, and would make the code much
// more complicated.
// Alternatively, the state detection code could run in a yet another thread
// that will not need to be cancelled too often.
// For simplicity, the current code just checks for status changes every
// second.
LONG ret = SCardGetStatusChange(m_context, 0, m_slotStates.data(), m_slotStates.size());
if (ret == SCARD_S_SUCCESS || ret == LONG(SCARD_E_UNKNOWN_READER)) {
processSlotUpdates();
removeSlots();
} else if (ret == LONG(SCARD_E_CANCELLED) || ret == LONG(SCARD_E_UNKNOWN_READER)
|| ret == LONG(SCARD_E_TIMEOUT)) {
/* ignore */
} else {
qCWarning(QT_NFC_PCSC) << "SCardGetStatusChange failed:" << QPcsc::errorMessage(ret);
// Unknown failure. It is likely that the current context will not
// recover from it, so destroy it and try to create a new context at the
// next iteration.
Q_ASSERT(m_hasContext);
m_hasContext = false;
for (auto slot : std::as_const(m_slots)) {
slot->invalidateInsertedCard();
slot->deleteLater();
}
SCardReleaseContext(m_context);
m_slots.clear();
m_slotStates.clear();
}
}
void QPcscManager::onStartTargetDetectionRequest(QNearFieldTarget::AccessMethod accessMethod)
{
qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO;
m_requestedMethod = accessMethod;
if (m_targetDetectionRunning)
return;
m_targetDetectionRunning = true;
m_stateUpdateTimer->start();
}
void QPcscManager::onStopTargetDetectionRequest()
{
qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO;
m_targetDetectionRunning = false;
}
QPcscCard *QPcscManager::connectToCard(QPcscSlot *slot)
{
qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO;
Q_ASSERT(slot != nullptr);
Q_ASSERT(m_hasContext);
SCARDHANDLE cardHandle;
DWORD activeProtocol;
LONG ret = SCardConnect(m_context, slot->name().ptr(), SCARD_SHARE_SHARED,
SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &cardHandle, &activeProtocol);
if (ret != SCARD_S_SUCCESS) {
qCDebug(QT_NFC_PCSC) << "Failed to connect to card:" << QPcsc::errorMessage(ret);
retryCardDetection(slot);
return nullptr;
}
auto card = new QPcscCard(cardHandle, activeProtocol, this);
auto uid = card->readUid();
auto maxInputLength = card->readMaxInputLength();
QNearFieldTarget::AccessMethods accessMethods = QNearFieldTarget::TagTypeSpecificAccess;
if (card->supportsNdef())
accessMethods |= QNearFieldTarget::NdefAccess;
if (m_requestedMethod != QNearFieldTarget::UnknownAccess
&& (accessMethods & m_requestedMethod) == 0) {
qCDebug(QT_NFC_PCSC) << "Dropping card without required access support";
card->deleteLater();
return nullptr;
}
if (!card->isValid()) {
qCDebug(QT_NFC_PCSC) << "Card became invalid";
card->deleteLater();
retryCardDetection(slot);
return nullptr;
}
Q_EMIT cardInserted(card, uid, accessMethods, maxInputLength);
return card;
}
/*
Setup states list so that the card detection for the given slot will
be retried on the next iteration.
This is useful to try to get cards working after reset.
*/
void QPcscManager::retryCardDetection(const QPcscSlot *slot)
{
qCDebug(QT_NFC_PCSC) << Q_FUNC_INFO;
for (auto &state : m_slotStates) {
if (state.pvUserData == slot) {
state.dwCurrentState = SCARD_STATE_UNAWARE;
break;
}
}
}
QT_END_NAMESPACE