Improved SCardGetStatusChange() for "\\?PnP?\Notification" special reader
SCardGetStatusChange()
works fine to detect a change in one of the
listed readers.
It is also possible to detect when a reader has been added or removed
using the special reader name "\\?PnP?\Notification"
. See
How to use SCardGetStatusChange()?.
One old problem is:
how to detect that a reader has been added between two calls to
SCardGetStatusChange()
?Even if you call
SCardGetStatusChange()
just after the previousSCardGetStatusChange()
returned you may still suffer from a race condition and miss a reader event.
In pcsc-lite version 2.3.0 I implemented a solution for that use case.
SCardGetStatusChange()
will now use the high word (high 16-bits) of the
.dwEventState
field for the special reader "\\?PnP?\Notification"
to
store the number of reader events (since the start of the pcscd daemon).
Pseudo code algorithm
The suggested way to fix the problem is to use something like that:
initial call to
SCardGetStatusChange()
get the initial value from
.dwEventState
copy this value in
.dwCurrentState
second call to
SCardGetStatusChange()
If a reader has been added before the second SCardGetStatusChange()
call then SCardGetStatusChange()
will return immediately.
Sample code
File sample.c
:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #include <winscard.h> #define CHECK(f, rv, panic) \ printf(f "(): 0x%08lX, %s\n", rv, pcsc_stringify_error(rv)); \ if ((SCARD_S_SUCCESS != rv) && panic) \ { \ return -1; \ } #define PANIC true #define DONT_PANIC false #define MAX_READERS 16 int main(void) { LONG rv; SCARDCONTEXT hContext; LPTSTR mszReaders; DWORD dwReaders = 0; char *ptr, *readers[MAX_READERS] = {}; int nb_readers; SCARD_READERSTATE rgReaderStates[MAX_READERS]; rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &hContext); CHECK("SCardEstablishContext", rv, PANIC) rv = SCardListReaders(hContext, NULL, NULL, &dwReaders); CHECK("SCardListReaders", rv, DONT_PANIC) mszReaders = calloc(dwReaders, sizeof(char)); rv = SCardListReaders(hContext, NULL, mszReaders, &dwReaders); CHECK("SCardListReaders", rv, DONT_PANIC) printf("Found readers:\n"); nb_readers = 0; for (ptr=mszReaders; *ptr; ptr += strlen(ptr) +1) { printf(" %s\n", ptr); readers[nb_readers++] = ptr; } printf("Found %d readers\n", nb_readers); /* add the special reader */ readers[nb_readers++] = "\\\\?PnP?\\Notification"; /* get the initial states */ for (int i=0; i<nb_readers; i++) { rgReaderStates[i].szReader = readers[i]; rgReaderStates[i].dwCurrentState = SCARD_STATE_UNAWARE; } rv = SCardGetStatusChange(hContext, 0, rgReaderStates, nb_readers); CHECK("SCardGetStatusChange", rv, DONT_PANIC) /* set the current state */ for (int i=0; i<nb_readers; i++) { rgReaderStates[i].dwCurrentState = rgReaderStates[i].dwEventState; printf("reader: %s, events #: %d\n", readers[i], rgReaderStates[i].dwEventState >> 16); printf("reader: %s, events state 0x%04X\n", readers[i], rgReaderStates[i].dwEventState & 0xFFFF); } /* wait for a change */ rv = SCardGetStatusChange(hContext, INFINITE, rgReaderStates, nb_readers); CHECK("SCardGetStatusChange", rv, PANIC) /* new state */ for (int i=0; i<nb_readers; i++) { printf("reader: %s, events #: %d\n", readers[i], rgReaderStates[i].dwEventState >> 16); printf("reader: %s, events state 0x%04X\n", readers[i], rgReaderStates[i].dwEventState & 0xFFFF); } free(mszReaders); rv = SCardReleaseContext(hContext); CHECK("SCardReleaseContext", rv, PANIC) return 0; }
File Makefile
:
# Linux PCSC_CFLAGS := $(shell pkg-config --cflags libpcsclite) LDLIBS := $(shell pkg-config --libs libpcsclite) CFLAGS = $(PCSC_CFLAGS) -g sample: sample.c clean: rm -f sample
Connect a reader
We start the sample code with no reader connected:
SCardEstablishContext(): 0x00000000, Command successful. SCardListReaders(): 0x8010002E, Cannot find a smart card reader. SCardListReaders(): 0x8010002E, Cannot find a smart card reader. Found readers: Found 0 readers SCardGetStatusChange(): 0x8010000A, Command timeout. reader: \\?PnP?\Notification, events #: 33 reader: \\?PnP?\Notification, events state 0x0000 >>> connection of a new reader here SCardGetStatusChange(): 0x00000000, Command successful. reader: \\?PnP?\Notification, events #: 34 reader: \\?PnP?\Notification, events state 0x0002 SCardReleaseContext(): 0x00000000, Command successful.
State value 0x0002 is SCARD_STATE_CHANGED
.
The first SCardGetStatusChange()
returns a reader events of 33.
After I connect a reader the number goes to 34.
Disconnect a reader
We restart the sample code with the reader connected:
SCardEstablishContext(): 0x00000000, Command successful. SCardListReaders(): 0x00000000, Command successful. SCardListReaders(): 0x00000000, Command successful. Found readers: Gemalto PC Twin Reader 00 00 Found 1 readers SCardGetStatusChange(): 0x00000000, Command successful. reader: Gemalto PC Twin Reader 00 00, events #: 0 reader: Gemalto PC Twin Reader 00 00, events state 0x0012 reader: \\?PnP?\Notification, events #: 34 reader: \\?PnP?\Notification, events state 0x0000 >>> removal of the reader here SCardGetStatusChange(): 0x00000000, Command successful. reader: Gemalto PC Twin Reader 00 00, events #: 0 reader: Gemalto PC Twin Reader 00 00, events state 0x000E reader: \\?PnP?\Notification, events #: 35 reader: \\?PnP?\Notification, events state 0x0002 SCardReleaseContext(): 0x00000000, Command successful.
The reader "Gemalto PC Twin Reader 00 00" has been removed. Its state value 0x000E contains the bits:
SCARD_STATE_UNKNOWN
SCARD_STATE_UNAVAILABLE
SCARD_STATE_CHANGED
Connect a second reader
We restart the sample code with the reader connected:
SCardEstablishContext(): 0x00000000, Command successful. SCardListReaders(): 0x00000000, Command successful. SCardListReaders(): 0x00000000, Command successful. Found readers: Gemalto PC Twin Reader 00 00 Found 1 readers SCardGetStatusChange(): 0x00000000, Command successful. reader: Gemalto PC Twin Reader 00 00, events #: 0 reader: Gemalto PC Twin Reader 00 00, events state 0x0012 reader: \\?PnP?\Notification, events #: 36 reader: \\?PnP?\Notification, events state 0x0000 >>> connection of a second reader SCardGetStatusChange(): 0x00000000, Command successful. reader: Gemalto PC Twin Reader 00 00, events #: 0 reader: Gemalto PC Twin Reader 00 00, events state 0x0010 reader: \\?PnP?\Notification, events #: 37 reader: \\?PnP?\Notification, events state 0x0002 SCardReleaseContext(): 0x00000000, Command successful.
The Gemalto reader event state is 0x0010 = SCARD_STATE_EMPTY
because
it does not contains a smart card. It does not have the bit
SCARD_STATE_CHANGED
set because it's state has not changed.
The PnP reader state is 0x0002 = SCARD_STATE_CHANGED
because a new
reader was connected.
Remarks
Timeout
It is important to use a timeout of 0 (parameter dwTimeout)
in the first call to SCardGetStatusChange()
to make the function
returns immediately.
Unitary Test
If you want to know if your implementation of PC/SC includes the fix or not (or test on Windows or macOS) you can use the program CardGetStatusChange_PnP_Events.py included in pcsc-lite UnitaryTests/SCardGetStatusChange/ directory.
Windows
Microsoft implemented a similar mechanism. But instead of using the number of reader events it uses the number of connected readers.
So between the 2 calls to SCardGetStatusChange()
if one reader is
added and one reader is removed you may not be notified.
macOS
Apple does not support the special reader "\\?PnP?\Notification"
.
See OS X El Capitan missing feature: SCardGetStatusChange() and "\\?PnP?\Notification".
So there is no way it supports the mechanism presented here.
Backward compatibility
To not break existing codes the new SCardGetStatusChange()
beahavior
is enabled only if the number of reader events in .dwCurrentState
is
not zero.
That is also one reason why you should use a dwTimeout
value of 0
for the first call.
.dwEventState
for normal readers
The high 16-bits of .dwEventState
for the normal readers is also
used. It contains the number of card events for this particular
reader.
This is an old behavior that is documented in the API documentation:
dwEventState
also contains a number of events in the upper 16 bits (dwEventState & 0xFFFF0000
). This number of events is incremented for each card insertion or removal in the specified reader. This can be used to detect a card removal/insertion between two calls toSCardGetStatusChange()
Conclusion
I was aware of this race issue with SCardGetStatusChange()
since a
long time (at least 2018. Thanks Maksim). But I got a new motivation
boost after an issue was reported SCardGetStatusChange: Race
condition when attaching multiple readers at the same time.
It is now the job of applications programmers to use this new feature in their codes.
If you find a regression in your application because of this change please open an issue.