Skip to content

Commit 8fae627

Browse files
committed
[java] Support disabling Preferences support for legacy dashboards
To support legacy dashboards, the Preferences code installed a listener that marked all new topics as persisted. Unfortunately, having a listener that updates new topics in the background can result in a closed NetworkTablesInstance being accessed (resulting in a SIGSEGV) when Preferences.setNetworkTableInstance() is called soon after values have been published for new topics (see #8215). It's possible this race condition is why PreferencesTest.removeAllTest() was flaky. The code to set new topics discovered under /Preferences as persistent was added back in 2015 in 87fc49c. Dashboards that want to want data for new keys to be saved should be explicitly marking the topics as persistant.
1 parent dbffe6e commit 8fae627

File tree

2 files changed

+97
-15
lines changed

2 files changed

+97
-15
lines changed

wpilibj/src/main/java/edu/wpi/first/wpilibj/Preferences.java

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public final class Preferences {
4545
private static StringPublisher m_typePublisher;
4646
private static MultiSubscriber m_tableSubscriber;
4747
private static NetworkTableListener m_listener;
48+
private static boolean m_supportLegacyDashboards = true;
4849

4950
/** Creates a preference class. */
5051
private Preferences() {}
@@ -60,7 +61,12 @@ private Preferences() {}
6061
* @param inst NetworkTable instance
6162
*/
6263
public static synchronized void setNetworkTableInstance(NetworkTableInstance inst) {
63-
m_table = inst.getTable(kTableName);
64+
NetworkTable table = inst.getTable(kTableName);
65+
if (table.equals(m_table)) {
66+
return;
67+
}
68+
m_table = table;
69+
6470
if (m_typePublisher != null) {
6571
m_typePublisher.close();
6672
}
@@ -78,23 +84,57 @@ public static synchronized void setNetworkTableInstance(NetworkTableInstance ins
7884
}
7985
m_tableSubscriber = new MultiSubscriber(inst, new String[] {m_table.getPath() + "/"});
8086

81-
// Listener to set all Preferences values to persistent
82-
// (for backwards compatibility with old dashboards).
8387
if (m_listener != null) {
8488
m_listener.close();
8589
}
86-
m_listener =
87-
NetworkTableListener.createListener(
88-
m_tableSubscriber,
89-
EnumSet.of(NetworkTableEvent.Kind.kImmediate, NetworkTableEvent.Kind.kPublish),
90-
event -> {
91-
if (event.topicInfo != null) {
92-
Topic topic = event.topicInfo.getTopic();
93-
if (!topic.equals(m_typePublisher.getTopic())) {
94-
event.topicInfo.getTopic().setPersistent(true);
95-
}
96-
}
97-
});
90+
if (m_supportLegacyDashboards) {
91+
m_listener = createTopicPersistingListener();
92+
} else {
93+
m_listener = null;
94+
}
95+
}
96+
97+
/**
98+
* Enables or disables support for dashboards that create Preferences topics without marking them
99+
* as persistent.
100+
*
101+
* <p>Legacy dashboard support is enabled by default. In future releases of WPILIb, it might
102+
* change to be disabled by default.
103+
*
104+
* @param enable Whether legacy dashboard support should be enabled
105+
* @return Whether legacy dashboard support was enabled when this method was called
106+
*/
107+
public static synchronized boolean enableLegacyDashboardSupport(boolean enable) {
108+
boolean wasEnabled = m_supportLegacyDashboards;
109+
if (wasEnabled != enable) {
110+
if (enable) {
111+
m_listener = createTopicPersistingListener();
112+
} else if (m_listener != null) {
113+
m_listener.close();
114+
m_listener = null;
115+
}
116+
m_supportLegacyDashboards = enable;
117+
}
118+
return wasEnabled;
119+
}
120+
121+
/**
122+
* Creates a listener that update all Preference topics to be persistent.
123+
*
124+
* <p>This is done for backwards compatibility with old dashboards.
125+
*/
126+
private static NetworkTableListener createTopicPersistingListener() {
127+
return NetworkTableListener.createListener(
128+
m_tableSubscriber,
129+
EnumSet.of(NetworkTableEvent.Kind.kImmediate, NetworkTableEvent.Kind.kPublish),
130+
event -> {
131+
if (event.topicInfo != null) {
132+
Topic topic = event.topicInfo.getTopic();
133+
if (!topic.equals(m_typePublisher.getTopic())) {
134+
event.topicInfo.getTopic().setPersistent(true);
135+
}
136+
}
137+
});
98138
}
99139

100140
/**

wpilibj/src/test/java/edu/wpi/first/wpilibj/PreferencesTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,21 @@
1212
import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD;
1313

1414
import edu.wpi.first.networktables.NetworkTable;
15+
import edu.wpi.first.networktables.NetworkTableEntry;
16+
import edu.wpi.first.networktables.NetworkTableEvent;
1517
import edu.wpi.first.networktables.NetworkTableInstance;
18+
import edu.wpi.first.networktables.NetworkTableListener;
1619
import edu.wpi.first.networktables.Topic;
1720
import java.io.IOException;
1821
import java.io.InputStream;
1922
import java.nio.file.Files;
2023
import java.nio.file.Path;
2124
import java.util.ArrayList;
2225
import java.util.Arrays;
26+
import java.util.EnumSet;
2327
import java.util.List;
28+
import java.util.concurrent.Semaphore;
29+
import java.util.concurrent.TimeUnit;
2430
import java.util.stream.Stream;
2531
import org.junit.jupiter.api.AfterEach;
2632
import org.junit.jupiter.api.BeforeEach;
@@ -31,6 +37,7 @@
3137
import org.junit.jupiter.api.parallel.Execution;
3238
import org.junit.jupiter.params.ParameterizedTest;
3339
import org.junit.jupiter.params.provider.MethodSource;
40+
import org.junit.jupiter.params.provider.ValueSource;
3441

3542
@Execution(SAME_THREAD)
3643
class PreferencesTest {
@@ -69,6 +76,7 @@ void setup(@TempDir Path tempDir) {
6976

7077
@AfterEach
7178
void cleanup() {
79+
m_inst.waitForListenerQueue(0.1);
7280
m_inst.close();
7381
}
7482

@@ -121,6 +129,40 @@ void defaultValueTest() {
121129
() -> assertFalse(Preferences.getBoolean("checkedValueBoolean", true)));
122130
}
123131

132+
@ParameterizedTest
133+
@ValueSource(booleans = {true, false})
134+
void enableLegacyDashboardSupportTest(boolean enable) throws InterruptedException {
135+
// Publish a value, wait until we are sure the listener would have fired, and verify that the
136+
// topic is persistant only if legacy dashboard support is enabled.
137+
boolean wasEnabled = Preferences.enableLegacyDashboardSupport(enable);
138+
Semaphore semaphore = new Semaphore(0);
139+
NetworkTableEntry entry = m_table.getEntry("legacyDashboardValueLong");
140+
try (NetworkTableListener listener =
141+
NetworkTableListener.createListener(
142+
entry,
143+
EnumSet.of(NetworkTableEvent.Kind.kImmediate, NetworkTableEvent.Kind.kValueAll),
144+
event -> {
145+
if (event.valueData != null) {
146+
semaphore.release();
147+
}
148+
})) {
149+
// Publish a value, wait for listeners to fire, and do that again. This ensures any listener
150+
// installed by Preferences would have been called at least once.
151+
for (int value = 1; value < 3; value++) {
152+
entry.setInteger(value);
153+
m_inst.waitForListenerQueue(0.5);
154+
if (!semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)) {
155+
fail("timed out waiting for event listener");
156+
}
157+
}
158+
assertEquals(enable, entry.isPersistent());
159+
} finally {
160+
if (wasEnabled != enable) {
161+
Preferences.enableLegacyDashboardSupport(wasEnabled);
162+
}
163+
}
164+
}
165+
124166
@Nested
125167
class PutGetTests {
126168
@Test

0 commit comments

Comments
 (0)