diff --git a/build.gradle.kts b/build.gradle.kts index 55084bb..2ac0624 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,7 +16,7 @@ plugins { dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) implementation("org.eclipse.keyple:keyple-common-java-api:2.0.2") - implementation("org.eclipse.keyple:keyple-plugin-java-api:2.3.2") + implementation("org.eclipse.keyple:keyple-plugin-java-api:3.0.0-SNAPSHOT") { isChanging = true } implementation("org.eclipse.keyple:keyple-util-java-lib:2.4.1") implementation("net.java.dev.jna:jna:5.15.0") compileOnly("org.slf4j:slf4j-api:1.7.36") diff --git a/gradle.properties b/gradle.properties index c300172..2e009a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ group = org.eclipse.keyple title = Keyple Plugin PCSC Java Lib description = Keyple add-on to manage PC/SC readers -version = 2.6.3-SNAPSHOT +version = 3.0.0-SNAPSHOT # Java Configuration javaSourceLevel = 1.8 diff --git a/src/main/java/org/eclipse/keyple/plugin/pcsc/PcscReader.java b/src/main/java/org/eclipse/keyple/plugin/pcsc/PcscReader.java index d87d587..4848b29 100644 --- a/src/main/java/org/eclipse/keyple/plugin/pcsc/PcscReader.java +++ b/src/main/java/org/eclipse/keyple/plugin/pcsc/PcscReader.java @@ -210,14 +210,21 @@ enum DisconnectionMode { PcscReader setIsoProtocol(IsoProtocol isoProtocol); /** - * Changes the action to be taken after disconnection (default value {@link - * DisconnectionMode#RESET}). + * Changes the action to be taken when the internal {@code closePhysicalChannel()} method is + * called (default value {@link DisconnectionMode#RESET}). * - *
The card is either reset or left as is. + *
This setting applies to the forced-close path (e.g. non-observable mode, or abnormal + * termination). In observable mode, the channel is closed by {@code deselectCard()} (always + * SCARD_UNPOWER_CARD) before card-removal detection, so the internal {@code + * closePhysicalChannel()} method is typically a no-op and this setting has no effect. + * + *
{@link DisconnectionMode#UNPOWER} and {@link DisconnectionMode#EJECT} require the default + * jnasmartcardio provider; they silently fall back to {@link DisconnectionMode#RESET} with other + * providers. * * @param disconnectionMode The {@link DisconnectionMode} to use (must be not null). * @return This instance. - * @throws IllegalArgumentException If disconnectionMode is null + * @throws IllegalArgumentException If disconnectionMode is null. * @since 2.0.0 */ PcscReader setDisconnectionMode(DisconnectionMode disconnectionMode); diff --git a/src/main/java/org/eclipse/keyple/plugin/pcsc/PcscReaderAdapter.java b/src/main/java/org/eclipse/keyple/plugin/pcsc/PcscReaderAdapter.java index 832ef69..cd12d49 100644 --- a/src/main/java/org/eclipse/keyple/plugin/pcsc/PcscReaderAdapter.java +++ b/src/main/java/org/eclipse/keyple/plugin/pcsc/PcscReaderAdapter.java @@ -49,17 +49,19 @@ final class PcscReaderAdapter private final PcscPluginAdapter pluginAdapter; private final boolean isWindows; private final int cardMonitoringCycleDuration; - private final byte[] pingApdu = HexUtil.toByteArray("00C0000000"); // GET RESPONSE private Card card; private CardChannel channel; private Boolean isContactless; private String protocol = IsoProtocol.ANY.getValue(); - private boolean isModeExclusive = false; + private boolean isModeExclusive; private DisconnectionMode disconnectionMode = DisconnectionMode.RESET; - private final AtomicBoolean loopWaitCard = new AtomicBoolean(); + private boolean isPhysicalChannelOpen; + private String powerOnData = ""; + private final AtomicBoolean loopWaitCard = new AtomicBoolean(); private final AtomicBoolean loopWaitCardRemoval = new AtomicBoolean(); private boolean isObservationActive; + private boolean isProtocolInnovatronBPrime; /** * Constructor. @@ -232,8 +234,9 @@ public boolean isCurrentProtocol(String readerProtocol) { String protocolRule = pluginAdapter.getProtocolRule(readerProtocol); boolean isCurrentProtocol; if (protocolRule != null && !protocolRule.isEmpty()) { - String atr = HexUtil.toHex(card.getATR().getBytes()); - isCurrentProtocol = Pattern.compile(protocolRule).matcher(atr).matches(); + isCurrentProtocol = Pattern.compile(protocolRule).matcher(powerOnData).matches(); + isProtocolInnovatronBPrime = + readerProtocol.equals(PcscCardCommunicationProtocol.INNOVATRON_B_PRIME.name()); } else { isCurrentProtocol = false; } @@ -260,6 +263,46 @@ public void onStopDetection() { isObservationActive = false; } + /** + * {@inheritDoc} + * + *
Sends S(DESELECT) to put the PICC in HALT state (via SCARD_UNPOWER_CARD). The ATR cache is + * preserved so the framework can still log it after deselection while the card remains physically + * present. + * + * @since 3.0.0 + */ + @Override + public void deselectCard() { + if (!isPhysicalChannelOpen) { + return; + } + try { + if (card instanceof Smartcardio.JnaCard) { + ((Smartcardio.JnaCard) card).disconnect(getDisposition(DisconnectionMode.UNPOWER)); + // reset the driver state to avoid stale reader state after UNPOWER on some drivers + try { + communicationTerminal.connect("*").disconnect(false); + } catch (CardException ignored) { + // NOP + } + } else { + card.disconnect(true); + } + } catch (CardException e) { + // Card already removed before deselect: treat silently (spec ยง4.3 pt 5) + if (logger.isDebugEnabled()) { + logger.debug( + "[readerExt={}] deselectCard: card already removed [reason={}]", name, e.getMessage()); + } + } finally { + // powerOnData is intentionally kept: card is physically present in HALT state + isPhysicalChannelOpen = false; + card = null; + channel = null; + } + } + /** * {@inheritDoc} * @@ -273,20 +316,23 @@ public String getName() { /** * {@inheritDoc} * + *
No-op if the channel was already opened by {@link #checkCardPresence()} during + * anti-collision. + * * @since 2.0.0 */ @Override public void openPhysicalChannel() throws ReaderIOException, CardIOException { - if (card != null) { + if (isPhysicalChannelOpen) { return; } - /* init of the card physical channel: if not yet established, opening of a new physical channel */ + isProtocolInnovatronBPrime = false; try { if (logger.isDebugEnabled()) { logger.debug( "[readerExt={}] Opening card physical channel [protocol={}]", getName(), protocol); } - card = this.communicationTerminal.connect(protocol); + card = communicationTerminal.connect(protocol); if (isModeExclusive) { card.beginExclusive(); if (logger.isDebugEnabled()) { @@ -298,6 +344,8 @@ public void openPhysicalChannel() throws ReaderIOException, CardIOException { } } channel = card.getBasicChannel(); + powerOnData = HexUtil.toHex(card.getATR().getBytes()); + isPhysicalChannelOpen = true; } catch (CardNotPresentException e) { throw new CardIOException("Card removed. Reader: " + name, e); } catch (CardException e) { @@ -308,56 +356,39 @@ public void openPhysicalChannel() throws ReaderIOException, CardIOException { /** * {@inheritDoc} * + *
Forced close using the configured {@link DisconnectionMode}. No-op if the channel is already + * closed. For contactless readers, {@link #deselectCard()} (SCARD_UNPOWER_CARD) should be called + * first for a protocol-clean HALT transition; this method is then a no-op in normal flow. + * + *
UNPOWER and EJECT modes require jnasmartcardio; they silently fall back to RESET with other + * providers. + * * @since 2.0.0 */ @Override public void closePhysicalChannel() throws ReaderIOException { - // If the reader is observed, the actual disconnection will be done in the card removal sequence - if (!isObservationActive) { - disconnect(); + if (!isPhysicalChannelOpen) { + return; } - } - - /** - * Disconnects the current card and resets the context and reader state. - * - *
This method handles the disconnection of a card, taking into account the specific - * disconnection mode. If the card uses the {@code INNOVATRON_B_PRIME} protocol, the disconnection - * mode is unconditionally overridden to {@link DisconnectionMode#UNPOWER}, regardless of the - * configured mode. If the card is an instance of {@link Smartcardio.JnaCard}, it disconnects - * using the extended mode specified by {@link #getDisposition(DisconnectionMode)} and resets the - * reader state to avoid incorrect card detection in subsequent operations. For other card types, - * it disconnects using the effective disconnection mode directly. - * - *
If a {@link CardException} occurs during the operation, a {@link ReaderIOException} is - * thrown with the associated error message. - * - *
Once the disconnection is handled, the method ensures that the context is reset. - * - * @throws ReaderIOException If an error occurs while closing the physical channel. - */ - private void disconnect() throws ReaderIOException { try { - if (card != null) { - DisconnectionMode effectiveMode = - isCurrentProtocol(PcscCardCommunicationProtocol.INNOVATRON_B_PRIME.name()) - ? DisconnectionMode.UNPOWER - : disconnectionMode; - if (card instanceof Smartcardio.JnaCard) { - // disconnect using the extended mode allowing UNPOWER - ((Smartcardio.JnaCard) card).disconnect(getDisposition(effectiveMode)); - // reset the reader state to avoid bad card detection next time - resetReaderState(effectiveMode); - } else { - card.disconnect( - effectiveMode == DisconnectionMode.UNPOWER - || effectiveMode == DisconnectionMode.RESET); - } + if (card instanceof Smartcardio.JnaCard) { + ((Smartcardio.JnaCard) card).disconnect(getDisposition(disconnectionMode)); + } else { + // UNPOWER and EJECT are not available outside jnasmartcardio: fall back to RESET + card.disconnect(disconnectionMode != DisconnectionMode.LEAVE); } } catch (CardException e) { - throw new ReaderIOException("Failed to close the physical channel. Reader: " + name, e); + String msg = e.getMessage() != null ? e.getMessage() : ""; + if (!msg.contains("SCARD_E_NO_SMARTCARD") + && !msg.contains("REMOVED") + && !msg.contains("NO_SMARTCARD")) { + throw new ReaderIOException("Failed to close the physical channel. Reader: " + name, e); + } } finally { - resetContext(); + isPhysicalChannelOpen = false; + card = null; + channel = null; + powerOnData = ""; } } @@ -382,29 +413,6 @@ private static int getDisposition(DisconnectionMode mode) { } } - /** - * Resets the state of the card reader. - * - *
This method attempts to reset the reader state based on the effective disconnection mode. If - * the effective mode is {@link DisconnectionMode#UNPOWER} (either configured or forced by the - * {@code INNOVATRON_B_PRIME} protocol), it reconnects to the terminal and then disconnects - * without powering off the reader. If any {@link CardException} occurs during this process, it is - * handled silently. - * - * @param effectiveMode The disconnection mode actually applied, which may differ from the - * configured {@link #disconnectionMode} when the card uses the {@code INNOVATRON_B_PRIME} - * protocol. - */ - private void resetReaderState(DisconnectionMode effectiveMode) { - try { - if (effectiveMode == DisconnectionMode.UNPOWER) { - communicationTerminal.connect("*").disconnect(false); - } - } catch (CardException e) { - // NOP - } - } - /** * {@inheritDoc} * @@ -412,46 +420,63 @@ private void resetReaderState(DisconnectionMode effectiveMode) { */ @Override public boolean isPhysicalChannelOpen() { - return card != null; + return isPhysicalChannelOpen; } /** * {@inheritDoc} * + *
When the channel is closed, attempts a full {@code SCardConnect()} to perform anti-collision + * for contactless readers. On success the channel is marked open and a subsequent call to {@link + * #openPhysicalChannel()} is a no-op. When the channel is open, checks physical presence via + * {@code SCardGetStatusChange} and calls {@link #closePhysicalChannel()} internally if the card + * is no longer present. + * * @since 2.0.0 */ @Override public boolean checkCardPresence() throws ReaderIOException { try { - boolean isCardPresent = communicationTerminal.isCardPresent(); - closePhysicalChannelSafely(); - return isCardPresent; + if (!isPhysicalChannelOpen) { + // channel closed: attempt connection (performs anti-collision for contactless readers) + try { + isProtocolInnovatronBPrime = false; + card = communicationTerminal.connect(protocol); + if (isModeExclusive) { + card.beginExclusive(); + } + channel = card.getBasicChannel(); + powerOnData = HexUtil.toHex(card.getATR().getBytes()); + isPhysicalChannelOpen = true; + return true; + } catch (CardNotPresentException e) { + return false; + } + } else { + // channel open: verify card still responds + boolean isPresent = communicationTerminal.isCardPresent(); + if (!isPresent) { + try { + closePhysicalChannel(); + } catch (ReaderIOException ignored) { + // card already gone; flags are reset in closePhysicalChannel finally block + } + } + return isPresent; + } } catch (CardException e) { throw new ReaderIOException("Failed to check card presence. Reader: " + name, e); } } - private void closePhysicalChannelSafely() { - try { - disconnect(); - } catch (Exception e) { - // NOP - } - } - - private void resetContext() { - card = null; - channel = null; - } - /** * {@inheritDoc} * - * @since 2.0.0 + *
* @since 2.0.0 */ @Override public String getPowerOnData() { - return HexUtil.toHex(card.getATR().getBytes()); + return powerOnData; } /** @@ -525,8 +550,7 @@ public void onUnregister() { @Override public void monitorCardPresenceDuringProcessing() throws ReaderIOException, TaskCanceledException { - doWaitForCardRemoval( - !isCurrentProtocol(PcscCardCommunicationProtocol.INNOVATRON_B_PRIME.name())); + doWaitForCardRemoval(!isProtocolInnovatronBPrime); } /** @@ -555,23 +579,10 @@ private void doWaitForCardRemoval(boolean allowPolling) logger.trace("[readerExt={}] Starting waiting card removal", name); } loopWaitCardRemoval.set(true); - try { - if (allowPolling && disconnectionMode == DisconnectionMode.UNPOWER) { - waitForCardRemovalByPolling(); - } else { - waitForCardRemovalStandard(); - } - } finally { - if (loopWaitCardRemoval.get()) { - try { - disconnect(); - } catch (Exception e) { - logger.warn( - "[readerExt={}] Failed to disconnect card during card removal sequence [reason={}]", - name, - e.getMessage()); - } - } + if (allowPolling && isProtocolInnovatronBPrime) { + waitForCardRemovalByPolling(); + } else { + waitForCardRemovalStandard(); } if (logger.isTraceEnabled()) { if (!loopWaitCardRemoval.get()) { @@ -589,16 +600,18 @@ private void doWaitForCardRemoval(boolean allowPolling) private void waitForCardRemovalByPolling() { try { while (loopWaitCardRemoval.get()) { - transmitApdu(pingApdu); + if (!monitoringTerminal.isCardPresent()) { + return; + } Thread.sleep(25); if (Thread.interrupted()) { return; } } - } catch (CardIOException | ReaderIOException e) { + } catch (CardException e) { if (logger.isTraceEnabled()) { logger.trace( - "[readerExt={}] Expected IOException received while waiting for card removal [reason={}]", + "[readerExt={}] ReaderIOException received while waiting for card removal [reason={}]", getName(), e.getMessage()); }