Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions core/pv-pva/src/main/java/org/phoebus/pv/pva/PVA_PV.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2019-2022 Oak Ridge National Laboratory.
* Copyright (c) 2019-2025 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -44,7 +44,9 @@ public PVA_PV(final String name, final String base_name) throws Exception
// Analyze base_name, determine channel and request
name_helper = PVNameHelper.forName(base_name);
logger.log(Level.FINE, () -> "PVA '" + base_name + "' -> " + name_helper);
channel = PVA_Context.getInstance().getClient().getChannel(name_helper.getChannel(), this::channelStateChanged);
channel = PVA_Context.getInstance().getClient().getChannel(name_helper.getChannel(),
this::channelStateChanged,
this::accessRightsChanged);
}

private void channelStateChanged(final PVAChannel channel, final ClientChannelState state)
Expand All @@ -67,6 +69,11 @@ else if (! isDisconnected(super.read()))
}
}

private void accessRightsChanged(final PVAChannel channel, final boolean is_writable)
{
notifyListenersOfPermissions(! is_writable);
}

private void handleMonitor(final PVAChannel channel,
final BitSet changes,
final BitSet overruns,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*******************************************************************************
* Copyright (c) 2025 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
******************************************************************************/
package org.epics.pva.client;

import static org.epics.pva.PVASettings.logger;

import java.nio.ByteBuffer;
import java.util.logging.Level;

import org.epics.pva.common.AccessRightsChange;
import org.epics.pva.common.CommandHandler;
import org.epics.pva.common.PVAHeader;

/** Handle a server's CMD_ACL_CHANGE message
* @author Kay Kasemir
*/
class AccessRightsChangeHandler implements CommandHandler<ClientTCPHandler>
{
@Override
public byte getCommand()
{
return PVAHeader.CMD_ACL_CHANGE;
}

@Override
public void handleCommand(final ClientTCPHandler tcp, final ByteBuffer buffer) throws Exception
{
final AccessRightsChange acl = AccessRightsChange.decode(tcp.getRemoteAddress(), buffer.remaining(), buffer);
if (acl == null)
return;
final PVAChannel channel = tcp.getClient().getChannel(acl.cid);
if (channel == null)
{
logger.log(Level.WARNING, this + " received CMD_ACL_CHANGE for unknown channel ID " + acl.cid);
return;
}

logger.log(Level.FINE, () -> "Received '" + channel.getName() + "' " + acl);
channel.updateAccessRights(acl.havePUTaccess());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*******************************************************************************
* Copyright (c) 2025 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
******************************************************************************/
package org.epics.pva.client;

/** Listener to a {@link PVAChannel} access rights
* @author Kay Kasemir
*/
@FunctionalInterface
public interface ClientAccessRightsListener
{
/** Invoked when the channel access rights change
*
* <p>Will be called as soon as possible, i.e. within
* the thread that handles the network communication.
*
* <p>Client code <b>must not</b> block.
*
* @param channel Channel with updated permissions
* @param is_writable May we write to the channel?
*/
public void channelAccessRightsChanged(PVAChannel channel, boolean is_writable);
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/*******************************************************************************
* Copyright (c) 2019 Oak Ridge National Laboratory.
* Copyright (c) 2019-2025 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
******************************************************************************/
package org.epics.pva.client;

/** Listener to a {@link PVAChannel}
*
/** Listener to a {@link PVAChannel} state
* @author Kay Kasemir
*/
@FunctionalInterface
public interface ClientChannelListener
{
/** Invoked when the channel state changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ClientTCPHandler extends TCPHandler
new ValidatedHandler(),
new EchoHandler(),
new CreateChannelHandler(),
new AccessRightsChangeHandler(),
new DestroyChannelHandler(),
new GetHandler(),
new PutHandler(),
Expand Down
29 changes: 26 additions & 3 deletions core/pva/src/main/java/org/epics/pva/client/PVAChannel.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
Expand All @@ -38,13 +39,13 @@
*
* <p>When no longer in use, the channel should be {@link #close()}d.
*
*
*
* Note that several methods return a CompletableFuture.
* This has been done because at this time the Futures used internally are indeed CompletableFutures
* and this type offers an extensive API for composition and chaining of futures.
* But note that user code must never call 'complete(..)' nor 'completeExceptionally()'
* on the provided CompletableFutures.
*
*
* @author Kay Kasemir
*/
@SuppressWarnings("nls")
Expand All @@ -63,7 +64,11 @@ public class PVAChannel extends SearchRequest.Channel implements AutoCloseable

private final PVAClient client;
private final ClientChannelListener listener;
private final ClientAccessRightsListener access_rights_listener;
private volatile int sid = -1;
// For compatibility with earlier implementation,
// assume channels are writable until access_rights_listener tells otherwise
private AtomicBoolean is_writable = new AtomicBoolean(true);

/** State
*
Expand All @@ -79,11 +84,14 @@ public class PVAChannel extends SearchRequest.Channel implements AutoCloseable

private final CopyOnWriteArrayList<MonitorRequest> subscriptions = new CopyOnWriteArrayList<>();

PVAChannel(final PVAClient client, final String name, final ClientChannelListener listener)
PVAChannel(final PVAClient client, final String name,
final ClientChannelListener listener,
final ClientAccessRightsListener access_rights_listener)
{
super(CID_Provider.incrementAndGet(), name);
this.client = client;
this.listener = listener;
this.access_rights_listener = access_rights_listener;
}

PVAClient getClient()
Expand Down Expand Up @@ -121,6 +129,21 @@ public boolean isConnected()
return getState() == ClientChannelState.CONNECTED;
}

/** @return <code>true</code> if channel has write permissions */
public boolean isWritable()
{
return is_writable.get();
}

/** Called by AccessRightsChangeHandler
* @param may_write Is channel writable?
*/
void updateAccessRights(final boolean may_write)
{
if (is_writable.getAndSet(may_write) != may_write)
access_rights_listener.channelAccessRightsChanged(this, may_write);
}

/** Wait for channel to connect
* @return {@link CompletableFuture} to await connection.
* <code>true</code> on success,
Expand Down
24 changes: 21 additions & 3 deletions core/pva/src/main/java/org/epics/pva/client/PVAClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@
*
* @author Kay Kasemir
*/
@SuppressWarnings("nls")
public class PVAClient implements AutoCloseable
{
/** Default channel listener logs state changes */
/** Default channel state listener logs state changes */
private static final ClientChannelListener DEFAULT_CHANNEL_LISTENER = (ch, state) -> logger.log(Level.INFO, ch.toString());

/** Default channel access rights listener does nothing */
private static final ClientAccessRightsListener DEFAULT_ACCESS_RIGHTS_LISTENER = (ch, write) -> {};

private final ClientUDPHandler udp;

private final BeaconTracker beacons = new BeaconTracker();
Expand Down Expand Up @@ -166,7 +168,23 @@ public PVAChannel getChannel(final String channel_name)
*/
public PVAChannel getChannel(final String channel_name, final ClientChannelListener listener)
{
final PVAChannel channel = new PVAChannel(this, channel_name, listener);
return getChannel(channel_name, listener, DEFAULT_ACCESS_RIGHTS_LISTENER);
}

/** Create channel by name
*
* <p>Starts search.
*
* @param channel_name PVA channel name
* @param state_listener {@link ClientChannelListener} that will be invoked with connection state updates
* @param access_rights_listener {@link ClientAccessRightsListener} that will be invoked with access rights updates
* @return {@link PVAChannel}
*/
public PVAChannel getChannel(final String channel_name,
final ClientChannelListener state_listener,
final ClientAccessRightsListener access_rights_listener)
{
final PVAChannel channel = new PVAChannel(this, channel_name, state_listener, access_rights_listener);
channels_by_id.putIfAbsent(channel.getCID(), channel);

// Register with search
Expand Down
12 changes: 6 additions & 6 deletions core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ private static void info(final List<String> names) throws Exception
final PVAChannel pv = iter.next();
if (pv.getState() == ClientChannelState.CONNECTED)
{
PVASettings.logger.log(Level.INFO, "Server: " + pv.getTCP().getServerX509Name());
PVASettings.logger.log(Level.INFO, "Client: " + pv.getTCP().getClientX509Name());
PVASettings.logger.log(Level.INFO, "Server X509 Name: " + pv.getTCP().getServerX509Name());
PVASettings.logger.log(Level.INFO, "Client X509 Name: " + pv.getTCP().getClientX509Name());

final PVAData data = pv.info(request).get(timeout_ms, TimeUnit.MILLISECONDS);
System.out.println(pv.getName() + " = " + data.formatType());
Expand Down Expand Up @@ -142,8 +142,8 @@ private static void get(final List<String> names) throws Exception
final PVAChannel pv = iter.next();
if (pv.getState() == ClientChannelState.CONNECTED)
{
PVASettings.logger.log(Level.INFO, "Server: " + pv.getTCP().getServerX509Name());
PVASettings.logger.log(Level.INFO, "Client: " + pv.getTCP().getClientX509Name());
PVASettings.logger.log(Level.INFO, "Server X509 Name: " + pv.getTCP().getServerX509Name());
PVASettings.logger.log(Level.INFO, "Client X509 Name: " + pv.getTCP().getClientX509Name());

final PVAData data = pv.read(request).get(timeout_ms, TimeUnit.MILLISECONDS);
System.out.println(pv.getName() + " = " + data);
Expand Down Expand Up @@ -188,8 +188,8 @@ private static void monitor(final List<String> names) throws Exception
{
try
{
PVASettings.logger.log(Level.INFO, "Server: " + ch.getTCP().getServerX509Name());
PVASettings.logger.log(Level.INFO, "Client: " + ch.getTCP().getClientX509Name());
PVASettings.logger.log(Level.INFO, "Server X509 Name: " + ch.getTCP().getServerX509Name());
PVASettings.logger.log(Level.INFO, "Client X509 Name: " + ch.getTCP().getClientX509Name());
ch.subscribe(request, listener);
}
catch (Exception ex)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*******************************************************************************
* Copyright (c) 2025 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
******************************************************************************/
package org.epics.pva.common;

import static org.epics.pva.PVASettings.logger;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.logging.Level;

/** Helper for CMD_ACL_CHANGE
* @author Kay Kasemir
*/
public class AccessRightsChange
{
/** Size of payload */
public static final int PAYLOAD_SIZE = Integer.BYTES + 1;

/** Client channel ID */
public int cid;

/** Access rights bits */
public byte access_rights;

/** Access rights bit definitions
*
* May client write (PUT),
* perform a write with read-back (PUT-GET)
* call a remote procedure (RPC)?
*/
public static final byte READ_ONLY = 0x00,
PUT_ACCESS = (1 << 0),
PUT_GET_ACCESS = (1 << 1),
RPC_ACCESS = (1 << 2);


private AccessRightsChange(final int cid, final byte access_rights)
{
this.cid = cid;
this.access_rights = access_rights;
}

// TODO Add API for PUT_GET and RPC once PVXS has a reference implementation

/** @return Do the access rights include write ('PUT') access? */
public boolean havePUTaccess()
{
return (access_rights & PUT_ACCESS) == PUT_ACCESS;
}

/** Encode access rights change
* @param buffer Buffer into which to encode
* @param cid Client channel ID
* @param b Access rights
*/
public static void encode(final ByteBuffer buffer, final int cid, final boolean writable)
{
PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_SERVER, PVAHeader.CMD_ACL_CHANGE, PAYLOAD_SIZE);
buffer.putInt(cid);
buffer.put(writable ? PUT_ACCESS : READ_ONLY);
}

/** Decode access rights change
* @param from Peer address
* @param payload Payload size
* @param buffer Buffer positioned on payload
* @return Decoded access rights change or <code>null</code> if not a valid
*/
public static AccessRightsChange decode(final InetSocketAddress from,
final int payload, final ByteBuffer buffer)
{
if (payload < PAYLOAD_SIZE)
{
logger.log(Level.WARNING, "PVA client " + from + " sent only " + payload + " bytes for access rights change");
return null;
}
final AccessRightsChange acl = new AccessRightsChange(buffer.getInt(), buffer.get());
logger.log(Level.FINER, () -> "PVA client " + from + " sent " + acl);
return acl;
}

@Override
public String toString()
{
return String.format("CID %d access rights %s (0x%02X)",
cid,
havePUTaccess() ? "writeable" : "read-only",
access_rights);
}
}
3 changes: 3 additions & 0 deletions core/pva/src/main/java/org/epics/pva/common/PVAHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ public class PVAHeader
/** Application command: Reply to search */
public static final byte CMD_SEARCH_RESPONSE = 0x04;

/** Application command: Access Control (List) Channel */
public static final byte CMD_ACL_CHANGE = 0x06;

/** Application command: Create Channel */
public static final byte CMD_CREATE_CHANNEL = 0x07;

Expand Down
Loading