Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin support #140

Merged
merged 61 commits into from
Aug 22, 2017
Merged

Add plugin support #140

merged 61 commits into from
Aug 22, 2017

Conversation

SamCarlberg
Copy link
Member

@SamCarlberg SamCarlberg commented Aug 11, 2017

Moves builtin widgets and data types to :plugins:base

Moves network table sources and widgets to :plugins:networktables

Autogenerates source trees for each registered source type

Features

  • Exposes a plugin API (see Plugin.java)
    • Allow custom widgets
    • Allow custom source types
    • Allow custom data types
    • Allow setting default widgets on a per-data-type basis
    • Allow custom type adapters for recording/playback
    • Define custom data playback in SourceType; nothing is hard-coded anymore in :api or :app
    • Expose custom names, descriptions, and version numbers
    • Expose custom themes
  • Allow plugins to be dynamically loaded an unloaded at runtime, not just startup
    • (This has a very nice dialog, see below)
    • Remove widgets when the plugins defining them are unloaded
    • Remove widgets sources when plugins defining those sources or data types are unloaded
    • Restore sources when the relevant plugins are reloaded
  • Read JAR files and load all plugins found
  • Read .class files and load all plugins found

Known issues

  • Startup takes about 5 seconds to load the UI due to LiveWindow tab autopopulation

Screenshots

Plugin preferences dialog

Plugin preferences dialog

@SamCarlberg SamCarlberg added api Related to the API module core work in progress A pull request that is still a work-in-progress and should not be merged labels Aug 11, 2017
@SamCarlberg
Copy link
Member Author

SamCarlberg commented Aug 11, 2017

Dialog for loading/unloading plugins

screenshot from 2017-08-11 14 53 55

Still need to make unloading plugins remove its exported widgets and replace the sources of its type with dummies

fun testFx(name: String, version: String = "4.0.+") =
create(group = "org.testfx", name = name, version = version)
testCompile(testFx(name = "testfx-core"))
testCompile(testFx(name = "testfx-junit5"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this testCompile dependency is shared now in both api and app you should put it there so you don't have duplicate version numbers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"There" being...?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build.gradle.kts in the root project.

return name;
}

public final Class<T> getJavaClass() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vs getting the Groovy class or the Kotlin class? What is this supposed to mean? A javadoc would be helpful.

return DataTypes.forName(name).orElse(DataTypes.Unknown);
@Override
public int hashCode() {
return getClass().hashCode();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't you also hashing on the name?

Copy link
Member

@JLLeitschuh JLLeitschuh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite a few things.

@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, you have a check for this immediately after the null check as an optimization:

if (this == obj) return true;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should really stop using IntelliJ's autogenerated equals/hashcode methods, they miss stuff like this

});
}

private static Comparator<Class<?>> closestTo(Class<?> target) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is needed and what it is? Also, this is a complex bit of logic, are you sure that Guava doesn't have some sort of reflection utility to do this?
https://github.com/google/guava/wiki/ReflectionExplained

Also, if you do need it, I really hope that this is unit tested.

.collect(Collectors.toSet());
}

private static Class<?> boxedType(Class<?> primitiveType) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.out.println("GENERATING FOR " + plugin.getName());
setup(plugin);
} else {
System.out.println("REMOVING ITEMS FOR " + plugin.getName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want to move to using a logger like slf4j

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left those in while I was debugging. We use Java's builtin Logger class for logging; this was a quick and dirty (and easy to read) way to debug

@@ -213,7 +271,7 @@ public void save() {
private void saveAs() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().setAll(
new FileChooser.ExtensionFilter("SmartDashboard Save File (.json)", "*.json"));
new FileChooser.ExtensionFilter("SmartDashboard Save File (.json)", "*.json"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatter changes?

return false;
}
}
return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log in this case too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That'll be logged a lot; one statement per non-plugin class. For example, the base plugin jar would log ~20 times here

@@ -19,11 +19,12 @@

import static org.junit.jupiter.api.Assertions.assertEquals;

@Disabled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this whole test disabled?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dependent on network tables

@@ -0,0 +1,3 @@
dependencies {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above this line add a description = "[SOME HELPFUL DESCRIPTION HERE]"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been resolved.

""".trimMargin()

dependencies {
foo
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wat?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, was trying to see why it wasn't evaluating declarations like compile and testRuntime

create(group = "org.testfx", name = name, version = version)
testCompile(testFx(name = "testfx-core"))
testCompile(testFx(name = "testfx-junit5"))
testRuntime(testFx(name = "openjfx-monocle", version = "8u76-b04"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is again duplicated here.

.filter(t -> t != All)
.sorted((t1, t2) -> classComparator.reversed().compare(t1.getJavaClass(), t2.getJavaClass()))
.collect(Collectors.toList());
return Optional.of(sorted.get(0));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just call first() on the set and get an optional?
Also, should this be Optional.ofNullable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that may work. ofNullable is pointless since elements should never be null

*/
public final void registerAll(T... items) {
Objects.requireNonNull(items, "items");
for (T item : items) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The array must not be null but you could pass registerAll(item1, null, item2, null) what would happen in that case? Should you check each of the args for null too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

register does a null check....

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay! Cool!

* @throws InvalidWidgetException if the widget is invalid
*/
private static void validate(Description description) throws InvalidWidgetException {
private void validate(Description description) throws InvalidWidgetException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this no longer static? It doesn't use any member variables.
Never mind

*/
public List<Widget> getActiveWidgets() {
// Use a copy; don't want elements in the list to be removed by GC while someone's using it
return new ArrayList<>(activeWidgets.keySet());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use an ImmutableList.copyOf?


expected = new String[]{"foo", "bar"};
assertArrayEquals(expected, Serialization.readStringArray(fooBarBytes, 0));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why were all these tests removed? Are they now irrelevant?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They were dependent on serializers that were moved to the base plugin

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

public void loadPluginJar(URI jarUri) throws IOException {
URL url = jarUri.toURL();
PrivilegedAction<URLClassLoader> getClassLoader = () -> {
return new URLClassLoader(new URL[]{url}, ClassLoader.getSystemClassLoader());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this using the class loader that the rest of the code is using?

I know there are some wacky edge cases you need to be aware of when dealing with class loaders and the garbage collector.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have to look into that. This is basically the same code that the original dashboard uses for loading jars at runtime and that has worked for the past 6+ years

plugin.getThemes().forEach(Themes.getDefault()::register);

plugin.onLoad();
plugin.setLoaded(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?


/**
* A type of data source that represents the state of another source that has been destroyed or removed as a result
* of its defining plugin being unloaded. The data source may
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete javadoc.

@@ -19,11 +19,12 @@

import static org.junit.jupiter.api.Assertions.assertEquals;

@Disabled("Depends on network tables")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a plan to re-enable this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I'll need to change the save file to use mock widgets

@Test
public void testLoadClass() {
PluginLoader loader = new PluginLoader();
boolean loaded = loader.loadPluginClass(MockPlugin.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As soon as you try doing this with more complicated classes that actually have plugin logic you are going to end up adding stuff to the Widgets singleton and other singletons which will affect other tests.

@AfterAll
public static void resetRegistries() {
Widgets.setDefault(new Widgets());
DataTypes.setDefault(new DataTypes());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where static singletons bite you in the butt. You really shouldn't need to do this.

If you have a failure in one of your tests here it can cause a ripple throughout the rest of your testing framework which can be really hard to debug. I would highly recommend you refactor your code so that you can test it without using static singletons.

@codecov-io
Copy link

Codecov Report

Merging #140 into master will decrease coverage by 29.37%.
The diff coverage is 21.84%.

@@              Coverage Diff              @@
##             master     #140       +/-   ##
=============================================
- Coverage      42.6%   13.22%   -29.38%     
+ Complexity      475      186      -289     
=============================================
  Files           125      136       +11     
  Lines          3070     3523      +453     
  Branches        236      263       +27     
=============================================
- Hits           1308      466      -842     
- Misses         1667     3026     +1359     
+ Partials         95       31       -64

@SamCarlberg SamCarlberg merged commit 1197c65 into wpilibsuite:master Aug 22, 2017
@SamCarlberg SamCarlberg deleted the plugins branch September 17, 2017 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Related to the API module core work in progress A pull request that is still a work-in-progress and should not be merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants