Skip to content
Merged
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
6 changes: 6 additions & 0 deletions src/org/rascalmpl/uri/IExternalResolverRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,10 @@ default String authority() {
default boolean supportsHost() {
return false;
}

boolean supportsGetCharset(String scheme);
boolean supportsInput(String scheme);
boolean supportsWatch(String scheme);
boolean supportsOutput(String scheme);
Comment thread
DavyLandman marked this conversation as resolved.
boolean supportsLogical(String scheme);
}
23 changes: 16 additions & 7 deletions src/org/rascalmpl/uri/URIResolverRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ public synchronized void registerRemoteResolverRegistry(int remoteResolverRegist
registry = new RemoteExternalResolverRegistry(remoteResolverRegistryPort);
}
this.externalRegistry = registry;
watchers.setExternalRegistry(registry);
if (registry.anyWatchSupported()) {
watchers.setExternalRegistry(registry, l -> this.externalRegistry.supportsWatch(l.getScheme()));
}
}
}

Expand Down Expand Up @@ -402,10 +404,13 @@ private ISourceLocation physicalLocation(ISourceLocation loc) throws IOException
loc = resolveAndFixOffsets(loc, resolver, map.values());
}

if (externalRegistry != null) {
if (externalRegistry != null && original != null) {
try {
var externalResult = resolveAndFixOffsets(loc == null ? original : loc, externalRegistry, Collections.emptyList());
return externalResult == null ? loc : externalResult;
var externalResolve = loc == null ? original : loc;
if (externalRegistry.supportsLogical(externalResolve.getScheme())) {
var externalResult = resolveAndFixOffsets(externalResolve, externalRegistry, Collections.emptyList());
return externalResult == null ? loc : externalResult;
}
} catch (IOException e) {
// Ignore remote IO errors
}
Expand Down Expand Up @@ -468,7 +473,9 @@ private ISourceLocationInput getInputResolver(String scheme) {
return result;
}
}
return externalRegistry;
if (externalRegistry != null && externalRegistry.supportsInput(scheme)) {
return externalRegistry;
}
}
return result;
}
Expand Down Expand Up @@ -499,7 +506,9 @@ private ISourceLocationOutput getOutputResolver(String scheme) {
return result;
}
}
return externalRegistry;
if (externalRegistry != null && externalRegistry.supportsOutput(scheme)) {
return externalRegistry;
}
}
return result;
}
Expand Down Expand Up @@ -990,7 +999,7 @@ public Charset getCharset(ISourceLocation uri) throws IOException {
uri = safeResolve(uri);
ISourceLocationInput resolver = getInputResolver(uri.getScheme());

if (resolver == null) {
if (resolver == null || (externalRegistry != null && resolver == externalRegistry && !externalRegistry.supportsGetCharset(uri.getScheme()))) {
throw new UnsupportedSchemeException(uri.getScheme());
}

Expand Down
13 changes: 13 additions & 0 deletions src/org/rascalmpl/uri/remote/RascalFileSystemServices.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import org.rascalmpl.uri.URIResolverRegistry;
import org.rascalmpl.uri.URIUtil;
import org.rascalmpl.uri.remote.jsonrpc.BooleanResponse;
import org.rascalmpl.uri.remote.jsonrpc.CapabilitiesResponse;
import org.rascalmpl.uri.remote.jsonrpc.Capability;
import org.rascalmpl.uri.remote.jsonrpc.CopyRequest;
import org.rascalmpl.uri.remote.jsonrpc.DirectoryEntry;
import org.rascalmpl.uri.remote.jsonrpc.DirectoryListingResponse;
Expand Down Expand Up @@ -242,4 +244,15 @@ public CompletableFuture<SourceLocationResponse> resolveLocation(ISourceLocation
return new SourceLocationResponse(resolved);
});
}

@Override
public CompletableFuture<CapabilitiesResponse> serverCapabilities() {
return async(() ->
new CapabilitiesResponse(
Capability.full(), Capability.full(),
Capability.full(), Capability.full(),
Capability.full()
)
);
}
}
100 changes: 100 additions & 0 deletions src/org/rascalmpl/uri/remote/RemoteExternalResolverRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -60,6 +61,8 @@
import org.rascalmpl.uri.IExternalResolverRegistry;
import org.rascalmpl.uri.ISourceLocationWatcher;
import org.rascalmpl.uri.URIUtil;
import org.rascalmpl.uri.remote.jsonrpc.CapabilitiesResponse;
import org.rascalmpl.uri.remote.jsonrpc.Capability;
import org.rascalmpl.uri.remote.jsonrpc.CopyRequest;
import org.rascalmpl.uri.remote.jsonrpc.DirectoryEntry;
import org.rascalmpl.uri.remote.jsonrpc.ISourceLocationRequest;
Expand Down Expand Up @@ -90,6 +93,7 @@
*/
public class RemoteExternalResolverRegistry implements IExternalResolverRegistry, IRemoteResolverRegistryClient {
private final AtomicReference<IRemoteResolverRegistryServer> remote = new AtomicReference<>(null);
private final AtomicReference<CompletableFuture<CapabilitiesResponse>> currentCapabilities = new AtomicReference<>(null);
Comment thread
DavyLandman marked this conversation as resolved.
private static final ExecutorService exec = NamedThreadPool.cachedDaemon("rascal-remote-resolver-registry");

private final Map<WatchSubscriptionKey, Watchers> watchers = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -118,8 +122,11 @@ private void connect(Duration nextTimeout) {
var newClient = startClient();
if (!remote.compareAndSet(null, newClient.getRight())) {
newClient.getLeft().close();
return;
}
currentCapabilities.set(newClient.getRight().serverCapabilities());
firstConnectionEstablished = true;

} catch (RuntimeException | IOException e) {
CompletableFuture.delayedExecutor(nextTimeout.toMillis(), TimeUnit.MILLISECONDS, exec).execute(() -> {
var newTimeout = nextTimeout.plusMillis(10);
Expand All @@ -133,6 +140,7 @@ private void connect(Duration nextTimeout) {

private void scheduleReconnect(IRemoteResolverRegistryServer oldServer) {
if (remote.compareAndSet(oldServer, null)) {
currentCapabilities.set(null);
CompletableFuture.runAsync(() -> connect(Duration.ofMillis(10)), exec);
}
}
Expand Down Expand Up @@ -308,11 +316,103 @@ private Pair<Socket, IRemoteResolverRegistryServer> startClient() throws Runtime
clientLauncher.startListening();
var remote = clientLauncher.getRemoteProxy();


inputStream.connect(remote);
outputStream.connect(remote);
return Pair.of(socket, remote);
}

@Override
public boolean supportsGetCharset(String scheme) {
return supportsCapability(CapabilitiesResponse::getGetCharset, scheme);
}

@Override
public boolean supportsLogical(String scheme) {
return supportsCapability(CapabilitiesResponse::getLogical, scheme);
}

@Override
public boolean supportsWatch(String scheme) {
return supportsCapability(CapabilitiesResponse::getWatch, scheme);
}

@Override
public boolean supportsOutput(String scheme) {
return supportsCapability(CapabilitiesResponse::getOutput, scheme);
}

@Override
public boolean supportsInput(String scheme) {
return supportsCapability(CapabilitiesResponse::getInput, scheme);
}

/**
* Check if the remote server supports a watch, even if it's partial
* @return
*/
public boolean anyWatchSupported() {
return !getCapability(CapabilitiesResponse::getWatch).isUnsupported();
}


private boolean supportsCapability(Function<CapabilitiesResponse, Capability> field, String scheme) {
var cap = getCapability(field);
if (cap.isUnsupported()) {
return false;
}
if (cap.isFullySupported()) {
return true;
}
// we only care for the first part of the `possible+chained+scheme`
// the nested schemes are for the downstream consumer.
return cap.getOnlyForSchemes().contains(getFirstSchemePart(scheme));
Comment thread
DavyLandman marked this conversation as resolved.
}

private static String getFirstSchemePart(String scheme) {
var end = scheme.indexOf('+');
if (end > 0) {
return scheme.substring(0, end);
}
return scheme;
}


private Capability getCapability(Function<CapabilitiesResponse, Capability> field) {
var caps = currentCapabilities.get();
if (caps == null) {
var stop = System.currentTimeMillis() + Duration.ofMinutes(1).toMillis();
while (System.currentTimeMillis() <= stop) {
caps = currentCapabilities.get();
if (caps != null) {
break;
}
try {
Thread.sleep(1);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (caps == null) {
return Capability.unsupported();
}
}
try {
return caps.thenApply(field).get(1, TimeUnit.MINUTES);
}
catch (TimeoutException e) {
return Capability.unsupported();
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Capability.unsupported();
}
catch (ExecutionException e) {
return Capability.unsupported();
}
}

private static <T, U> U call(ThrowingFunction<T, CompletableFuture<U>, IOException> function, T argument) throws IOException {
try {
return function.apply(argument).get(1, TimeUnit.MINUTES);
Expand Down
106 changes: 106 additions & 0 deletions src/org/rascalmpl/uri/remote/jsonrpc/CapabilitiesResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2018-2026, NWO-I CWI and Swat.engineering
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.rascalmpl.uri.remote.jsonrpc;

import java.util.Objects;

import org.checkerframework.checker.nullness.qual.Nullable;


public class CapabilitiesResponse {
/**
* Are the `/input/` APIs supported, null is a shorthand a capability of level for{@link CapabilityLevel#UNSUPPORTED}.
*/
private final @Nullable Capability input;
/**
* Are the `/output/` APIs supported, null is a shorthand a capability of level for{@link CapabilityLevel#UNSUPPORTED}.
*/
private final @Nullable Capability output;
/**
* Are the `/watch/` APIs supported, null is a shorthand a capability of level for{@link CapabilityLevel#UNSUPPORTED}
*/
private final @Nullable Capability watch;
/**
* Are the `/logical/` APIs supported, null is a shorthand a capability of level for{@link CapabilityLevel#UNSUPPORTED}
*/
private final @Nullable Capability logical;
/**
* Is the `/getCharset` API supported, null is a shorthand a capability of level for{@link CapabilityLevel#UNSUPPORTED}
*/
private final @Nullable Capability getCharset;

public CapabilitiesResponse(@Nullable Capability input, @Nullable Capability watch, @Nullable Capability output, @Nullable Capability logical,
@Nullable Capability getCharset) {
this.input = input;
this.watch = watch;
this.output = output;
this.logical = logical;
this.getCharset = getCharset;
}

/** as GSON will set null fields, but we don't want this in our code, we replace them by unsupported in the getter */
private static Capability replaceNull(@Nullable Capability cap) {
Comment thread
DavyLandman marked this conversation as resolved.
return cap == null ? Capability.unsupported() : cap;
}


public Capability getGetCharset() {
return replaceNull(getCharset);
}

public Capability getLogical() {
return replaceNull(logical);
}
public Capability getWatch() {
return replaceNull(watch);
}

public Capability getInput() {
return replaceNull(input);
}

public Capability getOutput() {
Comment thread
DavyLandman marked this conversation as resolved.
return replaceNull(output);
}

@Override
public int hashCode() {
Comment thread
DavyLandman marked this conversation as resolved.
return Objects.hash(input, output, watch, logical, getCharset);
}

@Override
public boolean equals(Object obj) {
if (!(obj instanceof CapabilitiesResponse)) {
return false;
}
CapabilitiesResponse other = (CapabilitiesResponse) obj;
return Objects.equals(input, other.input) && Objects.equals(output, other.output)
&& Objects.equals(watch, other.watch) && Objects.equals(logical, other.logical)
&& Objects.equals(getCharset, other.getCharset);
}

}
Loading
Loading