Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
6ec786d
Started building out RemoteFileSource plugin (#DH-20578)
bmingles Nov 26, 2025
bd985ac
Command resolver now fetches plugin (#DH-20578)
bmingles Nov 26, 2025
51bb7b5
RemoteFileSourceTicketResolverFactoryService (#DH-20578)
bmingles Nov 26, 2025
c282d40
Added stub for JsRemoteFileSourceService (#DH-20578)
bmingles Nov 26, 2025
1735733
Generated GWT bindings
niloc132 Nov 27, 2025
c051a01
Fetching plugin service working (#DH-20578)
bmingles Dec 3, 2025
ae7d97e
Wiring up messagestream (#DH-20578)
bmingles Dec 4, 2025
ef53eeb
test bidirectional communication (#DH-20578)
bmingles Dec 4, 2025
d3fe8cd
set connection id (#DH-20578)
bmingles Dec 4, 2025
f8e6dc4
Moved clientSessionId to plugin fetch instead of separate message (#D…
bmingles Dec 4, 2025
e5d8725
Cleanup (#DH-20578)
bmingles Dec 4, 2025
361019a
set execution context (#DH-20578)
bmingles Dec 4, 2025
2a1525a
Simplified execution context (#DH-20578)
bmingles Dec 5, 2025
36143fa
Basic file sourcing is working (#DH-20578)
bmingles Dec 8, 2025
8246036
Comments and cleanup (#DH-20578)
bmingles Dec 10, 2025
1998da3
Made method private (#DH-20578)
bmingles Dec 10, 2025
74e193e
Made method static (#DH-20578)
bmingles Dec 10, 2025
59afc07
onResourceRequest method (#DH-20578)
bmingles Dec 10, 2025
af3eef0
JS API types (#DH-20578)
bmingles Dec 10, 2025
07f6b8e
Refactored to use PluginMarker (#DH-20578)
bmingles Dec 11, 2025
3d7ebc7
Re-using JsWidget for message stream (#DH-20578)
bmingles Dec 11, 2025
eea7214
Added pluginType field to PluginMarker (#DH-20578)
bmingles Dec 11, 2025
bc1a90b
Removed redundant field and renamed pluginType to pluginName (#DH-20578)
bmingles Dec 12, 2025
d946963
Fixed incorrect plugin name (#DH-20578)
bmingles Dec 12, 2025
183d21c
Cleanup (#DH-20578)
bmingles Dec 16, 2025
10267c2
Renamed event (#DH-20578)
bmingles Dec 17, 2025
9a29add
Passing in relative file paths instead of top-level folder (#DH-20578)
bmingles Dec 18, 2025
13b186a
Added remotefilesource plugin to server build.gradle (#DH-20578)
bmingles Dec 18, 2025
b834ae5
Regenerate bindings
niloc132 Dec 19, 2025
5e29bdd
FIxing compile error
niloc132 Dec 19, 2025
19b0d7a
Replaced util with TextEncoder.encode (#DH-20578)
bmingles Dec 23, 2025
51e4f9d
Cleanup (#DH-20578)
bmingles Dec 24, 2025
434f5c8
Split out JsProtobufUtils (#DH-20578)
bmingles Dec 24, 2025
cf85dbd
Made canSourceResource synchronous (#DH-20578)
bmingles Dec 24, 2025
8298fef
renamed arg (#DH-20578)
bmingles Dec 24, 2025
097607b
Cleanup (#DH-20578)
bmingles Jan 2, 2026
bf47495
Sorted members (#DH-20578)
bmingles Jan 2, 2026
803a9ce
Cleanup RemoteFileSourceCommandResolver (#DH-20578)
bmingles Jan 2, 2026
47c1ca2
Cleanup RemoteFileSourceMessageStream (#DH-20578)
bmingles Jan 2, 2026
ac126af
Cleanup RemoteFileSourcePlugin (#DH-20578)
bmingles Jan 2, 2026
23b14f9
Cleanup RemoteFileSourceTicketResolverFactoryService (#DH-20578)
bmingles Jan 2, 2026
440bd23
Cleanup PluginMarker (#DH-20578)
bmingles Jan 2, 2026
9e74429
Changed back to resourceName convention to match class loaders (#DH-2…
bmingles Jan 2, 2026
140b3ac
Moved method (#DH-20578)
bmingles Jan 2, 2026
77ff6b3
Cleanup JsProtobufUtils (#DH-20578)
bmingles Jan 2, 2026
df05a62
Cleanup JsRemoteFileSourceService (#DH-20578)
bmingles Jan 2, 2026
b70f606
Applying Colin's client proto generation (#DH-20578)
bmingles Jan 5, 2026
52feab6
Changed to runtime dependencies and fixed docs links (#DH-20578)
bmingles Jan 5, 2026
f46e97d
Addressed review comments in RemoteFileSourceClassLoader (#DH-20578)
bmingles Jan 6, 2026
5cc3673
Addressed review comments in PluginMarker (#DH-20578)
bmingles Jan 6, 2026
7ad0a36
Addressed review comments in RemoteFileSourceMessageStream (#DH-20578)
bmingles Jan 6, 2026
325f688
Addressed review comments in JsRemoteFileSourceService (#DH-20578)
bmingles Jan 6, 2026
1cb84d4
Changed to getResource (#DH-20578)
bmingles Jan 13, 2026
71c58b8
Addressed review comments (#DH-20578)
bmingles Jan 14, 2026
1f775cc
Addressed review comments (#DH-20578)
bmingles Jan 14, 2026
d5ba658
Addressed review comments (#DH-20578)
bmingles Jan 14, 2026
1cdbac4
Removed timeout (#DH-20578)
bmingles Jan 14, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
//
// Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending
//
package io.deephaven.engine.util;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

/**
* A custom ClassLoader that fetches source files from remote clients via registered RemoteFileSourceProvider instances.
* This is designed to support Groovy script imports where the source files are provided by remote clients.
*
* <p>When a resource is requested (e.g., for a Groovy import), this class loader:
* <ol>
* <li>Checks registered providers to see if they can source the resource</li>
* <li>Returns a custom URL with protocol "remotefile://" if a provider can handle it</li>
* <li>When that URL is opened, fetches the resource bytes from the provider</li>
* </ol>
*/
public class RemoteFileSourceClassLoader extends ClassLoader {
private static final long RESOURCE_TIMEOUT_SECONDS = 5;

private static volatile RemoteFileSourceClassLoader instance;
private final CopyOnWriteArrayList<RemoteFileSourceProvider> providers = new CopyOnWriteArrayList<>();

/**
* Constructs a new RemoteFileSourceClassLoader with the specified parent class loader.
*
* @param parent the parent class loader for delegation
*/
private RemoteFileSourceClassLoader(ClassLoader parent) {
super(parent);
}

/**
* Initializes the singleton RemoteFileSourceClassLoader instance with the specified parent class loader.
*
* <p>This method must be called exactly once before any calls to {@link #getInstance()}. The method is
* synchronized to prevent race conditions when multiple threads attempt initialization.
*
* @param parent the parent class loader for delegation
* @return the newly created singleton instance
* @throws IllegalStateException if the instance has already been initialized
*/
public static synchronized RemoteFileSourceClassLoader initialize(ClassLoader parent) {
if (instance != null) {
throw new IllegalStateException("RemoteFileSourceClassLoader is already initialized");
}

instance = new RemoteFileSourceClassLoader(parent);
return instance;
}

/**
* Returns the singleton instance of the RemoteFileSourceClassLoader.
*
* <p>This method requires that {@link #initialize(ClassLoader)} has been called first.
*
* @return the singleton instance
* @throws IllegalStateException if the instance has not yet been initialized via {@link #initialize(ClassLoader)}
*/
public static RemoteFileSourceClassLoader getInstance() {
if (instance == null) {
throw new IllegalStateException("RemoteFileSourceClassLoader is not yet initialized");
}
return instance;
}

/**
* Registers a new provider that can source remote resources.
*
* @param provider the provider to register
*/
public void registerProvider(RemoteFileSourceProvider provider) {
providers.add(provider);
}

/**
* Unregisters a previously registered provider.
*
* @param provider the provider to unregister
*/
public void unregisterProvider(RemoteFileSourceProvider provider) {
providers.remove(provider);
}

/**
* Gets the resource with the specified name by checking registered providers.
*
* <p>This method iterates through all registered providers to see if any can source the requested resource.
* If a provider can handle the resource, a custom URL with protocol "remotefile://" is returned.
* If no provider can handle the resource, the request is delegated to the parent class loader.
*
* @param name the resource name
* @return a URL for reading the resource, or null if the resource could not be found
*/
@Override
public URL getResource(String name) {
RemoteFileSourceProvider provider = null;
for (RemoteFileSourceProvider candidate : providers) {
if (candidate.isActive() && candidate.canSourceResource(name)) {
provider = candidate;
break;
}
}

if (provider != null) {
try {
return new URL(null, "remotefile://" + name, new RemoteFileURLStreamHandler(provider, name));
} catch (MalformedURLException e) {
// Fall through to parent if URL creation fails
}
}

return super.getResource(name);
}

/**
* URLStreamHandler that delegates to a RemoteFileSourceProvider to fetch resource bytes.
*/
private static class RemoteFileURLStreamHandler extends URLStreamHandler {
private final RemoteFileSourceProvider provider;
private final String resourceName;

/**
* Constructs a new RemoteFileURLStreamHandler for the specified provider and resource.
*
* @param provider the provider that will source the resource
* @param resourceName the name of the resource to fetch
*/
RemoteFileURLStreamHandler(RemoteFileSourceProvider provider, String resourceName) {
this.provider = provider;
this.resourceName = resourceName;
}

/**
* Opens a connection to the resource referenced by this URL.
*
* @param url the URL to open a connection to
* @return a URLConnection to the specified URL
*/
@Override
protected URLConnection openConnection(URL url) {
return new RemoteFileURLConnection(url, provider, resourceName);
}
}

/**
* URLConnection that fetches resource bytes from a RemoteFileSourceProvider.
*/
private static class RemoteFileURLConnection extends URLConnection {
private final RemoteFileSourceProvider provider;
private final String resourceName;
private byte[] content;

/**
* Constructs a new RemoteFileURLConnection for the specified URL, provider, and resource.
*
* @param url the URL to connect to
* @param provider the provider that will source the resource
* @param resourceName the name of the resource to fetch
*/
RemoteFileURLConnection(URL url, RemoteFileSourceProvider provider, String resourceName) {
super(url);
this.provider = provider;
this.resourceName = resourceName;
}

/**
* Opens a connection to the resource by requesting it from the provider.
*
* <p>This method fetches the resource bytes from the provider with a timeout of
* {@value #RESOURCE_TIMEOUT_SECONDS} seconds. If already connected, this method does nothing.
*
* @throws IOException if the connection fails or times out
*/
@Override
public void connect() throws IOException {
if (!connected) {
try {
content = provider.requestResource(resourceName)
.orTimeout(RESOURCE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.get();
connected = true;
} catch (Exception e) {
throw new IOException("Failed to fetch remote resource: " + resourceName, e);
}
}
}

/**
* Returns an input stream that reads from this connection's resource.
*
* <p>This method calls {@link #connect()} to ensure the connection is established and resource bytes are
* fetched from the provider. The method then verifies that content has been successfully downloaded before
* creating the input stream.
*
* @return an input stream that reads from the fetched resource bytes
* @throws IOException if the connection or content download fails or if the resource has no content
*/
@Override
public InputStream getInputStream() throws IOException {
connect();
if (content == null || content.length == 0) {
throw new IOException("No content for resource: " + resourceName);
}
return new ByteArrayInputStream(content);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending
//
package io.deephaven.engine.util;

import java.util.concurrent.CompletableFuture;

/**
* Interface for providing remote resources to the ClassLoader.
* Plugins can implement this interface and register with RemoteFileSourceClassLoader
* to provide resources from remote sources.
*/
public interface RemoteFileSourceProvider {
/**
* Check if this provider can source the given resource.
*
* @param resourceName the name of the resource to check (e.g., "com/example/MyClass.groovy")
* @return true if this provider can handle the resource, false otherwise
*/
boolean canSourceResource(String resourceName);

/**
* Check if this provider is currently active and should be used for resource requests.
*
* @return true if this provider is active, false otherwise
*/
boolean isActive();

/**
* Request a resource from the remote source.
*
* @param resourceName the name of the resource to fetch (e.g., "com/example/MyClass.groovy")
* @return a CompletableFuture containing the resource bytes, or null if not found
*/
CompletableFuture<byte[]> requestResource(String resourceName);
}

Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public class GroovyDeephavenSession extends AbstractScriptSession<GroovySnapshot
.getBooleanForClassWithDefault(GroovyDeephavenSession.class, "allowUnknownGroovyPackageImports", false);

private static final ClassLoader STATIC_LOADER =
new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()) {
new URLClassLoader(new URL[0], RemoteFileSourceClassLoader.initialize(Thread.currentThread().getContextClassLoader())) {
final ConcurrentHashMap<String, Object> mapping = new ConcurrentHashMap<>();

@Override
Expand Down
14 changes: 14 additions & 0 deletions plugin/remotefilesource/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugins {
id 'io.deephaven.project.register'
}

dependencies {
implementation project(':plugin')
implementation project(':server')
implementation project(':proto:proto-backplane-grpc')
implementation project(':proto:proto-backplane-grpc-flight')

compileOnly libs.autoservice
compileOnly libs.jetbrains.annotations
annotationProcessor libs.autoservice.compiler
}
1 change: 1 addition & 0 deletions plugin/remotefilesource/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.deephaven.project.ProjectType=JAVA_PUBLIC
Loading
Loading