Skip to content

Commit 5cf6e87

Browse files
authored
Merge pull request #3506 from ControlSystemStudio/pva_acl
PVA access permissions info
2 parents 41c7b2c + ff2c2c4 commit 5cf6e87

18 files changed

+342
-48
lines changed

core/pv-pva/src/main/java/org/phoebus/pv/pva/PVA_PV.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2019-2022 Oak Ridge National Laboratory.
2+
* Copyright (c) 2019-2025 Oak Ridge National Laboratory.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -44,7 +44,9 @@ public PVA_PV(final String name, final String base_name) throws Exception
4444
// Analyze base_name, determine channel and request
4545
name_helper = PVNameHelper.forName(base_name);
4646
logger.log(Level.FINE, () -> "PVA '" + base_name + "' -> " + name_helper);
47-
channel = PVA_Context.getInstance().getClient().getChannel(name_helper.getChannel(), this::channelStateChanged);
47+
channel = PVA_Context.getInstance().getClient().getChannel(name_helper.getChannel(),
48+
this::channelStateChanged,
49+
this::accessRightsChanged);
4850
}
4951

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

72+
private void accessRightsChanged(final PVAChannel channel, final boolean is_writable)
73+
{
74+
notifyListenersOfPermissions(! is_writable);
75+
}
76+
7077
private void handleMonitor(final PVAChannel channel,
7178
final BitSet changes,
7279
final BitSet overruns,

core/pva/src/main/java/org/epics/pva/PVASettings.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ public class PVASettings
219219
public static int EPICS_PVA_TCP_SOCKET_TMO = 5;
220220

221221
/** Maximum number of array elements shown when printing data */
222-
public static int EPICS_PVA_MAX_ARRAY_FORMATTING = 256;
222+
public static int EPICS_PVA_MAX_ARRAY_FORMATTING = 50;
223223

224224
/** Range of beacon periods in seconds recognized as "fast, new" beacons
225225
* that re-start searches for disconnected channels.
@@ -275,6 +275,11 @@ public class PVASettings
275275
EPICS_PVAS_TLS_OPTIONS = get("EPICS_PVAS_TLS_OPTIONS", EPICS_PVAS_TLS_OPTIONS);
276276
require_client_cert = EPICS_PVAS_TLS_OPTIONS.contains("client_cert=require");
277277
EPICS_PVA_TLS_KEYCHAIN = get("EPICS_PVA_TLS_KEYCHAIN", EPICS_PVA_TLS_KEYCHAIN);
278+
if (EPICS_PVA_TLS_KEYCHAIN.isEmpty() && !EPICS_PVAS_TLS_KEYCHAIN.isEmpty())
279+
{
280+
EPICS_PVA_TLS_KEYCHAIN = EPICS_PVAS_TLS_KEYCHAIN;
281+
logger.log(Level.CONFIG, "EPICS_PVA_TLS_KEYCHAIN (empty) updated from EPICS_PVAS_TLS_KEYCHAIN");
282+
}
278283
EPICS_PVA_SEND_BUFFER_SIZE = get("EPICS_PVA_SEND_BUFFER_SIZE", EPICS_PVA_SEND_BUFFER_SIZE);
279284
EPICS_PVA_FAST_BEACON_MIN = get("EPICS_PVA_FAST_BEACON_MIN", EPICS_PVA_FAST_BEACON_MIN);
280285
EPICS_PVA_FAST_BEACON_MAX = get("EPICS_PVA_FAST_BEACON_MAX", EPICS_PVA_FAST_BEACON_MAX);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Oak Ridge National Laboratory.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v10.html
7+
******************************************************************************/
8+
package org.epics.pva.client;
9+
10+
import static org.epics.pva.PVASettings.logger;
11+
12+
import java.nio.ByteBuffer;
13+
import java.util.logging.Level;
14+
15+
import org.epics.pva.common.AccessRightsChange;
16+
import org.epics.pva.common.CommandHandler;
17+
import org.epics.pva.common.PVAHeader;
18+
19+
/** Handle a server's CMD_ACL_CHANGE message
20+
* @author Kay Kasemir
21+
*/
22+
class AccessRightsChangeHandler implements CommandHandler<ClientTCPHandler>
23+
{
24+
@Override
25+
public byte getCommand()
26+
{
27+
return PVAHeader.CMD_ACL_CHANGE;
28+
}
29+
30+
@Override
31+
public void handleCommand(final ClientTCPHandler tcp, final ByteBuffer buffer) throws Exception
32+
{
33+
final AccessRightsChange acl = AccessRightsChange.decode(tcp.getRemoteAddress(), buffer.remaining(), buffer);
34+
if (acl == null)
35+
return;
36+
final PVAChannel channel = tcp.getClient().getChannel(acl.cid);
37+
if (channel == null)
38+
{
39+
logger.log(Level.WARNING, this + " received CMD_ACL_CHANGE for unknown channel ID " + acl.cid);
40+
return;
41+
}
42+
43+
logger.log(Level.FINE, () -> "Received '" + channel.getName() + "' " + acl);
44+
channel.updateAccessRights(acl.havePUTaccess());
45+
}
46+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Oak Ridge National Laboratory.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v10.html
7+
******************************************************************************/
8+
package org.epics.pva.client;
9+
10+
/** Listener to a {@link PVAChannel} access rights
11+
* @author Kay Kasemir
12+
*/
13+
@FunctionalInterface
14+
public interface ClientAccessRightsListener
15+
{
16+
/** Invoked when the channel access rights change
17+
*
18+
* <p>Will be called as soon as possible, i.e. within
19+
* the thread that handles the network communication.
20+
*
21+
* <p>Client code <b>must not</b> block.
22+
*
23+
* @param channel Channel with updated permissions
24+
* @param is_writable May we write to the channel?
25+
*/
26+
public void channelAccessRightsChanged(PVAChannel channel, boolean is_writable);
27+
}

core/pva/src/main/java/org/epics/pva/client/ClientChannelListener.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
/*******************************************************************************
2-
* Copyright (c) 2019 Oak Ridge National Laboratory.
2+
* Copyright (c) 2019-2025 Oak Ridge National Laboratory.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
66
* http://www.eclipse.org/legal/epl-v10.html
77
******************************************************************************/
88
package org.epics.pva.client;
99

10-
/** Listener to a {@link PVAChannel}
11-
*
10+
/** Listener to a {@link PVAChannel} state
1211
* @author Kay Kasemir
1312
*/
13+
@FunctionalInterface
1414
public interface ClientChannelListener
1515
{
1616
/** Invoked when the channel state changes

core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class ClientTCPHandler extends TCPHandler
4848
new ValidatedHandler(),
4949
new EchoHandler(),
5050
new CreateChannelHandler(),
51+
new AccessRightsChangeHandler(),
5152
new DestroyChannelHandler(),
5253
new GetHandler(),
5354
new PutHandler(),

core/pva/src/main/java/org/epics/pva/client/PVAChannel.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.concurrent.CompletableFuture;
1313
import java.util.concurrent.CopyOnWriteArrayList;
1414
import java.util.concurrent.TimeoutException;
15+
import java.util.concurrent.atomic.AtomicBoolean;
1516
import java.util.concurrent.atomic.AtomicInteger;
1617
import java.util.concurrent.atomic.AtomicReference;
1718
import java.util.logging.Level;
@@ -38,13 +39,13 @@
3839
*
3940
* <p>When no longer in use, the channel should be {@link #close()}d.
4041
*
41-
*
42+
*
4243
* Note that several methods return a CompletableFuture.
4344
* This has been done because at this time the Futures used internally are indeed CompletableFutures
4445
* and this type offers an extensive API for composition and chaining of futures.
4546
* But note that user code must never call 'complete(..)' nor 'completeExceptionally()'
4647
* on the provided CompletableFutures.
47-
*
48+
*
4849
* @author Kay Kasemir
4950
*/
5051
@SuppressWarnings("nls")
@@ -63,7 +64,11 @@ public class PVAChannel extends SearchRequest.Channel implements AutoCloseable
6364

6465
private final PVAClient client;
6566
private final ClientChannelListener listener;
67+
private final ClientAccessRightsListener access_rights_listener;
6668
private volatile int sid = -1;
69+
// For compatibility with earlier implementation,
70+
// assume channels are writable until access_rights_listener tells otherwise
71+
private AtomicBoolean is_writable = new AtomicBoolean(true);
6772

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

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

82-
PVAChannel(final PVAClient client, final String name, final ClientChannelListener listener)
87+
PVAChannel(final PVAClient client, final String name,
88+
final ClientChannelListener listener,
89+
final ClientAccessRightsListener access_rights_listener)
8390
{
8491
super(CID_Provider.incrementAndGet(), name);
8592
this.client = client;
8693
this.listener = listener;
94+
this.access_rights_listener = access_rights_listener;
8795
}
8896

8997
PVAClient getClient()
@@ -121,6 +129,21 @@ public boolean isConnected()
121129
return getState() == ClientChannelState.CONNECTED;
122130
}
123131

132+
/** @return <code>true</code> if channel has write permissions */
133+
public boolean isWritable()
134+
{
135+
return is_writable.get();
136+
}
137+
138+
/** Called by AccessRightsChangeHandler
139+
* @param may_write Is channel writable?
140+
*/
141+
void updateAccessRights(final boolean may_write)
142+
{
143+
if (is_writable.getAndSet(may_write) != may_write)
144+
access_rights_listener.channelAccessRightsChanged(this, may_write);
145+
}
146+
124147
/** Wait for channel to connect
125148
* @return {@link CompletableFuture} to await connection.
126149
* <code>true</code> on success,

core/pva/src/main/java/org/epics/pva/client/PVAClient.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@
4040
*
4141
* @author Kay Kasemir
4242
*/
43-
@SuppressWarnings("nls")
4443
public class PVAClient implements AutoCloseable
4544
{
46-
/** Default channel listener logs state changes */
45+
/** Default channel state listener logs state changes */
4746
private static final ClientChannelListener DEFAULT_CHANNEL_LISTENER = (ch, state) -> logger.log(Level.INFO, ch.toString());
4847

48+
/** Default channel access rights listener does nothing */
49+
private static final ClientAccessRightsListener DEFAULT_ACCESS_RIGHTS_LISTENER = (ch, write) -> {};
50+
4951
private final ClientUDPHandler udp;
5052

5153
private final BeaconTracker beacons = new BeaconTracker();
@@ -166,7 +168,23 @@ public PVAChannel getChannel(final String channel_name)
166168
*/
167169
public PVAChannel getChannel(final String channel_name, final ClientChannelListener listener)
168170
{
169-
final PVAChannel channel = new PVAChannel(this, channel_name, listener);
171+
return getChannel(channel_name, listener, DEFAULT_ACCESS_RIGHTS_LISTENER);
172+
}
173+
174+
/** Create channel by name
175+
*
176+
* <p>Starts search.
177+
*
178+
* @param channel_name PVA channel name
179+
* @param state_listener {@link ClientChannelListener} that will be invoked with connection state updates
180+
* @param access_rights_listener {@link ClientAccessRightsListener} that will be invoked with access rights updates
181+
* @return {@link PVAChannel}
182+
*/
183+
public PVAChannel getChannel(final String channel_name,
184+
final ClientChannelListener state_listener,
185+
final ClientAccessRightsListener access_rights_listener)
186+
{
187+
final PVAChannel channel = new PVAChannel(this, channel_name, state_listener, access_rights_listener);
170188
channels_by_id.putIfAbsent(channel.getCID(), channel);
171189

172190
// Register with search

core/pva/src/main/java/org/epics/pva/client/PVAClientMain.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ private static void help()
7171

7272
private static void setLogLevel(final Level level)
7373
{
74-
PVASettings.logger.setLevel(level);
74+
// Cannot use PVASettings.logger here because that would
75+
// construct it and log CONFIG messages before we might be
76+
// able to disable them
77+
Logger.getLogger("org.epics.pva").setLevel(level);
7578
Logger.getLogger("jdk.event.security").setLevel(level);
7679
}
7780

@@ -97,8 +100,8 @@ private static void info(final List<String> names) throws Exception
97100
final PVAChannel pv = iter.next();
98101
if (pv.getState() == ClientChannelState.CONNECTED)
99102
{
100-
PVASettings.logger.log(Level.INFO, "Server: " + pv.getTCP().getServerX509Name());
101-
PVASettings.logger.log(Level.INFO, "Client: " + pv.getTCP().getClientX509Name());
103+
PVASettings.logger.log(Level.INFO, "Server X509 Name: " + pv.getTCP().getServerX509Name());
104+
PVASettings.logger.log(Level.INFO, "Client X509 Name: " + pv.getTCP().getClientX509Name());
102105

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

148151
final PVAData data = pv.read(request).get(timeout_ms, TimeUnit.MILLISECONDS);
149152
System.out.println(pv.getName() + " = " + data);
@@ -188,8 +191,8 @@ private static void monitor(final List<String> names) throws Exception
188191
{
189192
try
190193
{
191-
PVASettings.logger.log(Level.INFO, "Server: " + ch.getTCP().getServerX509Name());
192-
PVASettings.logger.log(Level.INFO, "Client: " + ch.getTCP().getClientX509Name());
194+
PVASettings.logger.log(Level.INFO, "Server X509 Name: " + ch.getTCP().getServerX509Name());
195+
PVASettings.logger.log(Level.INFO, "Client X509 Name: " + ch.getTCP().getClientX509Name());
193196
ch.subscribe(request, listener);
194197
}
195198
catch (Exception ex)

0 commit comments

Comments
 (0)