The Win32 API to monitor smart card readers is SCardGetStatusChange. However its usage is sufficiently complex and arcane that a post like this is warranted to show usage.

SCardGetStatusChange blocks until a change takes place at which point it returns. An array containing state for each reader is passed in to indicate the state as understood by the caller and when the function detects a difference between the actual state and the state in the array the function updates the array with the new state and returns. The caller then processes the changes and then calls back with its new understanding.


template<
typename SetContext,
typename ClearContext,
typename Wait,
typename Report
>
unique_winerror monitor_smartcard_readers(
SetContext && setContext,
ClearContext && clearContext,
Wait && wait,
Report && report
)
{
unique_winerror winerror;

These hold the output of SCardListReaders and the input/output for SCardGetStatusChange. In fact the readers elements contain pointers into the readernames array. This is why readernames is first so that its lifetime exceeds the lifetime of readers. There is additional code to maintain this lifetime contract further down.


std::vector<wchar_t> readernames;
std::vector<SCARD_READERSTATE> readers;

while (winerror) {

make sure that the smart card service has started and that the loop has not been cancelled



if (!std::forward<Wait>(wait)()) {
return winerror_cast(SCARD_E_CANCELLED);
}

monitor_error_contract(
[&]() {

unique_close_scardcontext context;
ON_UNWIND_AUTO(
[&] {
std::forward<ClearContext>(clearContext)();
}
);

need a fresh context whenever we start over. lots of system changes could have caused this restart including the smart card service stopping.



winerror.reset(
SCardEstablishContext(
SCARD_SCOPE_USER,
NULL,
NULL,
context.replace()
)
);
if (!winerror || !context) {
return;
}

std::forward<SetContext>(setContext)(context.get());

make sure that the loop has not been cancelled. without this second wait there is a race where the new context is not cancelled because the caller cancelled the context at a time when there was no context.



if (!std::forward<Wait>(wait)()) {
winerror = winerror_cast(SCARD_E_CANCELLED);
return;
}

if (readers.empty()) {

Windows allows smart card readers to be inserted and removed from the machine. In order to support these Plug-and-Play events a static reader name was defined so that SCardGetStatusChange can report PnP changes. the make function defaults the initial state to unaware which causes SCardGetStatusChange to return immediately with the actual PnP state.


readers.push_back(
make(L"\\\\?PnP?\\Notification")
);
}

for (;;) {
auto readersstaterange = lib::rng::make_range_raw(
readers
);

winerror.reset(
SCardGetStatusChange(
context.get(),
INFINITE,
readersstaterange.begin(),
lib::rng::size_cast<DWORD>(
readersstaterange.size()
)
)
);
if (!winerror) {
// exit
return;
}

report the smart card reader state


auto readersrange = lib::rng::make_range_raw(
readers,
0,
-1
);
if (!readersrange.empty()) {
std::forward<Report>(report)(readersrange);
}

record the changes now that they have been reported. setting dwCurrentState = dwEventState is required before calling SCardGetStatusChange again or the function will just return immediately to report the same changes.


for (auto & state : readers) {
state.dwCurrentState = state.dwEventState;
}

if ((
readers.back().dwEventState &
SCARD_STATE_CHANGED
) == SCARD_STATE_CHANGED
) {
// Pnp event - list readers.
break;
}
}

A PnP event occured. must keep the existing allocations for use while building the new list.


std::vector<wchar_t> oldreadernames(
std::move(readernames)
);
std::vector<SCARD_READERSTATE> oldreaders(
std::move(readers)
);

create a range of the existing reader states that excludes the static pnp state


auto oldreaderssortedrange = lib::rng::make_range(
oldreaders,
0,
-1
);

LPWSTR concatreaderstrings = nullptr;
ON_UNWIND_AUTO(
[&] {
if (concatreaderstrings) {
SCardFreeMemory(
context.get(),
concatreaderstrings
);
};
}
);
DWORD totallength = SCARD_AUTOALLOCATE;

winerror.reset(
SCardListReaders(
context.get(),
nullptr,
reinterpret_cast<LPWSTR>(&concatreaderstrings),
&totallength
)
);

no readers is not an error, just loop around to wait for a reader to be connected


if (winerror ==
winerror_cast(SCARD_E_NO_READERS_AVAILABLE)
) {
winerror.suppress().release();
return;
} else if (!winerror) {
return;
}

save a copy of the reader names because the state array will have pointers into them.


readernames.assign(
concatreaderstrings,
concatreaderstrings + totallength
);

auto readerstateless = [](
const SCARD_READERSTATE & lhs,
const SCARD_READERSTATE & rhs
) -> bool {
return _wcsicmp(lhs.szReader, rhs.szReader) < 0;
};

all the reader names are concatenated in this array with embedded nulls for each and two nulls to mark the end


auto cursorreadernames = lib::rng::make_range_raw(
readernames
);
while (
!cursorreadernames.empty() &&
cursorreadernames.front() != L'\0'
) {
// access the current name
auto namerange = lib::rng::make_range(
cursorreadernames,
0,
wcslen(
cursorreadernames.begin()
) - cursorreadernames.size()
);
// skip to the next name
cursorreadernames = lib::rng::make_range(
namerange,
namerange.size() + 1,
0
);

find this reader in the old list of readers. This list is kept sorted so that std::equal_range can be used.


auto oldreader = std::equal_range(
oldreaderssortedrange.begin(),
oldreaderssortedrange.end(),
make(namerange.begin()),
readerstateless
);
if (oldreader.first != oldreader.second) {
// keep the old state for this reader
readers.push_back(*oldreader.first);

// must use the new string allocation,
// the old one will be gone soon
readers.back().szReader = namerange.begin();
} else {
// this reader has not been seen yet.
// make defaults to setting the state to
// unaware, so that the next call to
// SCardGetStatusChange will return
// immediately with the actual state.
readers.push_back(make(namerange.begin()));
}
}

keeping the reader states sorted makes the updates more stable and allows the std::equal_range above instead of a linear find.


std::sort(
readers.begin(),
readers.end(),
readerstateless
);

add PnP state query to the new list. keep the existing state, and keep it at the end of the list, out of the sorted area.


readers.push_back(oldreaders.back());
}
);
}
return winerror;
}

Tracking the readers state is only the first step. Future posts will explore tracking cards, certificates and subjects/users