Skip to content

Commit c9035b4

Browse files
fix: resolve PC/SC context contention on Linux (#30)
1 parent 3e499f6 commit c9035b4

File tree

2 files changed

+68
-8
lines changed

2 files changed

+68
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Fixed
9+
- Fixed PC/SC context contention on Linux by implementing separate contexts for monitoring and communication operations,
10+
preventing `SCARD_E_SHARING_VIOLATION` errors and thread blocking when using card presence detection concurrently with
11+
APDU transmission (especially with `waitForCardRemoval()` in a separate thread).
812

913
## [2.5.3] - 2025-10-22
1014
### Fixed

src/main/java/org/eclipse/keyple/plugin/pcsc/PcscReaderAdapter.java

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ final class PcscReaderAdapter
4343

4444
private static final Logger logger = LoggerFactory.getLogger(PcscReaderAdapter.class);
4545

46-
private final CardTerminal terminal;
46+
private final CardTerminal communicationTerminal; // For connect/transmit operations
47+
private final CardTerminal monitoringTerminal; // For waitForCardPresent/Absent operations
4748
private final String name;
4849
private final PcscPluginAdapter pluginAdapter;
4950
private final boolean isWindows;
@@ -67,11 +68,66 @@ final class PcscReaderAdapter
6768
*/
6869
PcscReaderAdapter(
6970
CardTerminal terminal, PcscPluginAdapter pluginAdapter, int cardMonitoringCycleDuration) {
70-
this.terminal = terminal;
71+
this.communicationTerminal = terminal;
7172
this.pluginAdapter = pluginAdapter;
7273
this.name = terminal.getName();
7374
this.isWindows = System.getProperty("os.name").toLowerCase().contains("win");
7475
this.cardMonitoringCycleDuration = cardMonitoringCycleDuration;
76+
77+
// Create a separate PC/SC context for monitoring operations to avoid contention under Linux
78+
// This is critical because Linux pcsc-lite does not handle concurrent access to a single
79+
// SCARDCONTEXT as robustly as Windows (see threading differences documentation)
80+
this.monitoringTerminal = createMonitoringTerminal(terminal.getName());
81+
}
82+
83+
/**
84+
* Creates a separate CardTerminal instance for monitoring operations using a dedicated PC/SC
85+
* context.
86+
*
87+
* <p>Under Linux with pcsc-lite, sharing the same SCARDCONTEXT between blocking monitoring calls
88+
* (waitForCardPresent/Absent) and communication operations (transmit) can cause thread contention
89+
* and SCARD_E_SHARING_VIOLATION errors due to the self-pipe trick mechanism used for
90+
* cancellation.
91+
*
92+
* <p>This method attempts to create a new TerminalFactory instance to obtain a separate context.
93+
* If this fails (e.g., on older JRE versions or with certain security providers), it falls back
94+
* to using the same terminal, which may cause issues on Linux but will still work on Windows.
95+
*
96+
* @param terminalName The name of the terminal to create a monitoring instance for.
97+
* @return A CardTerminal instance for monitoring, either with a separate context or the same one.
98+
*/
99+
private CardTerminal createMonitoringTerminal(String terminalName) {
100+
try {
101+
// Attempt to create a new TerminalFactory instance to get a separate PC/SC context
102+
TerminalFactory monitoringFactory = TerminalFactory.getDefault();
103+
CardTerminals monitoringTerminals = monitoringFactory.terminals();
104+
105+
// Find the terminal with the same name in the new context
106+
for (CardTerminal t : monitoringTerminals.list()) {
107+
if (t.getName().equals(terminalName)) {
108+
if (logger.isDebugEnabled()) {
109+
logger.debug(
110+
"Reader [{}]: created separate monitoring context for improved Linux compatibility",
111+
terminalName);
112+
}
113+
return t;
114+
}
115+
}
116+
117+
// Terminal not found in new context, fall back to same terminal
118+
logger.warn(
119+
"Reader [{}]: could not find terminal in separate context, using shared context (may cause issues on Linux)",
120+
terminalName);
121+
return communicationTerminal;
122+
123+
} catch (Exception e) {
124+
// Failed to create separate context, fall back to same terminal
125+
logger.warn(
126+
"Reader [{}]: could not create separate monitoring context ({}), using shared context (may cause issues on Linux)",
127+
terminalName,
128+
e.getMessage());
129+
return communicationTerminal;
130+
}
75131
}
76132

77133
/**
@@ -94,7 +150,7 @@ public void waitForCardInsertion() throws TaskCanceledException, ReaderIOExcepti
94150

95151
try {
96152
while (loopWaitCard.get()) {
97-
if (terminal.waitForCardPresent(cardMonitoringCycleDuration)) {
153+
if (monitoringTerminal.waitForCardPresent(cardMonitoringCycleDuration)) {
98154
// card inserted
99155
if (logger.isTraceEnabled()) {
100156
logger.trace("Reader [{}]: card inserted", getName());
@@ -227,7 +283,7 @@ public void openPhysicalChannel() throws ReaderIOException, CardIOException {
227283
logger.debug(
228284
"Reader [{}]: open card physical channel for protocol [{}]", getName(), protocol);
229285
}
230-
card = this.terminal.connect(protocol);
286+
card = this.communicationTerminal.connect(protocol);
231287
if (isModeExclusive) {
232288
card.beginExclusive();
233289
if (logger.isDebugEnabled()) {
@@ -326,7 +382,7 @@ private static int getDisposition(DisconnectionMode mode) {
326382
private void resetReaderState() {
327383
try {
328384
if (disconnectionMode == DisconnectionMode.UNPOWER) {
329-
terminal.connect("*").disconnect(false);
385+
communicationTerminal.connect("*").disconnect(false);
330386
}
331387
} catch (CardException e) {
332388
// NOP
@@ -351,7 +407,7 @@ public boolean isPhysicalChannelOpen() {
351407
@Override
352408
public boolean checkCardPresence() throws ReaderIOException {
353409
try {
354-
boolean isCardPresent = terminal.isCardPresent();
410+
boolean isCardPresent = communicationTerminal.isCardPresent();
355411
closePhysicalChannelSafely();
356412
return isCardPresent;
357413
} catch (CardException e) {
@@ -517,7 +573,7 @@ private void waitForCardRemovalByPolling() {
517573
private void waitForCardRemovalStandard() throws ReaderIOException {
518574
try {
519575
while (loopWaitCardRemoval.get()) {
520-
if (terminal.waitForCardAbsent(cardMonitoringCycleDuration)) {
576+
if (monitoringTerminal.waitForCardAbsent(cardMonitoringCycleDuration)) {
521577
return;
522578
}
523579
if (Thread.interrupted()) {
@@ -623,7 +679,7 @@ public byte[] transmitControlCommand(int commandId, byte[] command) {
623679
if (card != null) {
624680
response = card.transmitControlCommand(controlCode, command);
625681
} else {
626-
Card virtualCard = terminal.connect("DIRECT");
682+
Card virtualCard = communicationTerminal.connect("DIRECT");
627683
response = virtualCard.transmitControlCommand(controlCode, command);
628684
virtualCard.disconnect(false);
629685
}

0 commit comments

Comments
 (0)