mirror of
https://github.com/apple/pkl.git
synced 2026-04-23 08:48:36 +02:00
Implement SPICE-0009 External Readers (#660)
This adds a new feature, which allows Pkl to read resources and modules from external processes. Follows the design laid out in SPICE-0009. Also, this moves most of the messaging API into pkl-core
This commit is contained in:
@@ -20,6 +20,9 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import org.pkl.core.SecurityManagers.StandardBuilder;
|
||||
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcess;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessImpl;
|
||||
import org.pkl.core.http.HttpClient;
|
||||
import org.pkl.core.module.ModuleKeyFactories;
|
||||
import org.pkl.core.module.ModuleKeyFactory;
|
||||
@@ -478,6 +481,25 @@ public final class EvaluatorBuilder {
|
||||
} else if (settings.moduleCacheDir() != null) {
|
||||
setModuleCacheDir(settings.moduleCacheDir());
|
||||
}
|
||||
|
||||
// this isn't ideal as project and non-project ExternalProcessImpl instances can be dupes
|
||||
var procs = new HashMap<ExternalReader, ExternalReaderProcess>();
|
||||
if (settings.externalModuleReaders() != null) {
|
||||
for (var entry : settings.externalModuleReaders().entrySet()) {
|
||||
addModuleKeyFactory(
|
||||
ModuleKeyFactories.externalProcess(
|
||||
entry.getKey(),
|
||||
procs.computeIfAbsent(entry.getValue(), ExternalReaderProcessImpl::new)));
|
||||
}
|
||||
}
|
||||
if (settings.externalResourceReaders() != null) {
|
||||
for (var entry : settings.externalResourceReaders().entrySet()) {
|
||||
addResourceReader(
|
||||
ResourceReaders.externalProcess(
|
||||
entry.getKey(),
|
||||
procs.computeIfAbsent(entry.getValue(), ExternalReaderProcessImpl::new)));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.pkl.core.ast.internal.ToStringNodeGen;
|
||||
import org.pkl.core.ast.lambda.ApplyVmFunction1NodeGen;
|
||||
import org.pkl.core.ast.member.*;
|
||||
import org.pkl.core.ast.type.*;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.module.ModuleKeys;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
@@ -1847,6 +1848,12 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
.withHint(e.getHint())
|
||||
.withSourceSection(createSourceSection(importUriCtx))
|
||||
.build();
|
||||
} catch (ExternalReaderProcessException e) {
|
||||
throw exceptionBuilder()
|
||||
.evalError("externalReaderFailure")
|
||||
.withCause(e.getCause())
|
||||
.withSourceSection(createSourceSection(importUriCtx))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (!resolvedUri.isAbsolute()) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
@@ -75,6 +76,8 @@ public abstract class AbstractReadNode extends UnaryExpressionNode {
|
||||
.build();
|
||||
} catch (PackageLoadError | SecurityManagerException e) {
|
||||
throw exceptionBuilder().withCause(e).build();
|
||||
} catch (ExternalReaderProcessException e) {
|
||||
throw exceptionBuilder().evalError("externalReaderFailure").withCause(e).build();
|
||||
}
|
||||
|
||||
if (!resolvedUri.isAbsolute()) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.ast.member.SharedMemberNode;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
@@ -104,6 +105,8 @@ public class ImportGlobNode extends AbstractImportNode {
|
||||
.evalError("invalidGlobPattern", globPattern)
|
||||
.withHint(e.getMessage())
|
||||
.build();
|
||||
} catch (ExternalReaderProcessException e) {
|
||||
throw exceptionBuilder().evalError("externalReaderFailure").withCause(e).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.net.URISyntaxException;
|
||||
import org.graalvm.collections.EconomicMap;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.ast.member.SharedMemberNode;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
@@ -103,6 +104,8 @@ public abstract class ReadGlobNode extends AbstractReadNode {
|
||||
.evalError("invalidGlobPattern", globPattern)
|
||||
.withHint(e.getMessage())
|
||||
.build();
|
||||
} catch (ExternalReaderProcessException e) {
|
||||
throw exceptionBuilder().evalError("externalReaderFailure").withCause(e).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@ import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import org.pkl.core.Duration;
|
||||
import org.pkl.core.PNull;
|
||||
import org.pkl.core.PObject;
|
||||
@@ -43,7 +45,9 @@ public record PklEvaluatorSettings(
|
||||
@Nullable List<Path> modulePath,
|
||||
@Nullable Duration timeout,
|
||||
@Nullable Path rootDir,
|
||||
@Nullable Http http) {
|
||||
@Nullable Http http,
|
||||
@Nullable Map<String, ExternalReader> externalModuleReaders,
|
||||
@Nullable Map<String, ExternalReader> externalResourceReaders) {
|
||||
|
||||
/** Initializes a {@link PklEvaluatorSettings} from a raw object representation. */
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -80,6 +84,24 @@ public record PklEvaluatorSettings(
|
||||
var rootDirStr = (String) pSettings.get("rootDir");
|
||||
var rootDir = rootDirStr == null ? null : pathNormalizer.apply(rootDirStr, "rootDir");
|
||||
|
||||
var externalModuleReadersRaw = (Map<String, Value>) pSettings.get("externalModuleReaders");
|
||||
var externalModuleReaders =
|
||||
externalModuleReadersRaw == null
|
||||
? null
|
||||
: externalModuleReadersRaw.entrySet().stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
Entry::getKey, entry -> ExternalReader.parse(entry.getValue())));
|
||||
|
||||
var externalResourceReadersRaw = (Map<String, Value>) pSettings.get("externalResourceReaders");
|
||||
var externalResourceReaders =
|
||||
externalResourceReadersRaw == null
|
||||
? null
|
||||
: externalResourceReadersRaw.entrySet().stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
Entry::getKey, entry -> ExternalReader.parse(entry.getValue())));
|
||||
|
||||
return new PklEvaluatorSettings(
|
||||
(Map<String, String>) pSettings.get("externalProperties"),
|
||||
(Map<String, String>) pSettings.get("env"),
|
||||
@@ -90,7 +112,9 @@ public record PklEvaluatorSettings(
|
||||
modulePath,
|
||||
(Duration) pSettings.get("timeout"),
|
||||
rootDir,
|
||||
Http.parse((Value) pSettings.get("http")));
|
||||
Http.parse((Value) pSettings.get("http")),
|
||||
externalModuleReaders,
|
||||
externalResourceReaders);
|
||||
}
|
||||
|
||||
public record Http(@Nullable Proxy proxy) {
|
||||
@@ -133,6 +157,18 @@ public record PklEvaluatorSettings(
|
||||
}
|
||||
}
|
||||
|
||||
public record ExternalReader(String executable, @Nullable List<String> arguments) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static ExternalReader parse(Value input) {
|
||||
if (input instanceof PObject externalReader) {
|
||||
var executable = (String) externalReader.getProperty("executable");
|
||||
var arguments = (List<String>) externalReader.get("arguments");
|
||||
return new ExternalReader(executable, arguments);
|
||||
}
|
||||
throw PklBugException.unreachableCode();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean arePatternsEqual(
|
||||
@Nullable List<Pattern> thesePatterns, @Nullable List<Pattern> thosePatterns) {
|
||||
if (thesePatterns == null) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.externalreader;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Map;
|
||||
import org.msgpack.core.MessagePack;
|
||||
import org.msgpack.core.MessageUnpacker;
|
||||
import org.msgpack.value.Value;
|
||||
import org.pkl.core.externalreader.ExternalReaderMessages.*;
|
||||
import org.pkl.core.messaging.BaseMessagePackDecoder;
|
||||
import org.pkl.core.messaging.DecodeException;
|
||||
import org.pkl.core.messaging.Message;
|
||||
import org.pkl.core.messaging.Message.Type;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public class ExternalReaderMessagePackDecoder extends BaseMessagePackDecoder {
|
||||
|
||||
public ExternalReaderMessagePackDecoder(MessageUnpacker unpacker) {
|
||||
super(unpacker);
|
||||
}
|
||||
|
||||
public ExternalReaderMessagePackDecoder(InputStream inputStream) {
|
||||
this(MessagePack.newDefaultUnpacker(inputStream));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Message decodeMessage(Type msgType, Map<Value, Value> map)
|
||||
throws DecodeException, URISyntaxException {
|
||||
return switch (msgType) {
|
||||
case INITIALIZE_MODULE_READER_REQUEST ->
|
||||
new InitializeModuleReaderRequest(
|
||||
unpackLong(map, "requestId"), unpackString(map, "scheme"));
|
||||
case INITIALIZE_RESOURCE_READER_REQUEST ->
|
||||
new InitializeResourceReaderRequest(
|
||||
unpackLong(map, "requestId"), unpackString(map, "scheme"));
|
||||
case INITIALIZE_MODULE_READER_RESPONSE ->
|
||||
new InitializeModuleReaderResponse(
|
||||
unpackLong(map, "requestId"), unpackModuleReaderSpec(getNullable(map, "spec")));
|
||||
case INITIALIZE_RESOURCE_READER_RESPONSE ->
|
||||
new InitializeResourceReaderResponse(
|
||||
unpackLong(map, "requestId"), unpackResourceReaderSpec(getNullable(map, "spec")));
|
||||
case CLOSE_EXTERNAL_PROCESS -> new CloseExternalProcess();
|
||||
default -> super.decodeMessage(msgType, map);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.externalreader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import org.msgpack.core.MessagePack;
|
||||
import org.msgpack.core.MessagePacker;
|
||||
import org.pkl.core.externalreader.ExternalReaderMessages.*;
|
||||
import org.pkl.core.messaging.BaseMessagePackEncoder;
|
||||
import org.pkl.core.messaging.Message;
|
||||
import org.pkl.core.messaging.ProtocolException;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public class ExternalReaderMessagePackEncoder extends BaseMessagePackEncoder {
|
||||
|
||||
public ExternalReaderMessagePackEncoder(MessagePacker packer) {
|
||||
super(packer);
|
||||
}
|
||||
|
||||
public ExternalReaderMessagePackEncoder(OutputStream outputStream) {
|
||||
this(MessagePack.newDefaultPacker(outputStream));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable void encodeMessage(Message msg) throws ProtocolException, IOException {
|
||||
switch (msg.type()) {
|
||||
case INITIALIZE_MODULE_READER_REQUEST -> {
|
||||
var m = (InitializeModuleReaderRequest) msg;
|
||||
packer.packMapHeader(2);
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("scheme", m.scheme());
|
||||
}
|
||||
case INITIALIZE_RESOURCE_READER_REQUEST -> {
|
||||
var m = (InitializeResourceReaderRequest) msg;
|
||||
packer.packMapHeader(2);
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("scheme", m.scheme());
|
||||
}
|
||||
case INITIALIZE_MODULE_READER_RESPONSE -> {
|
||||
var m = (InitializeModuleReaderResponse) msg;
|
||||
packMapHeader(1, m.spec());
|
||||
packKeyValue("requestId", m.requestId());
|
||||
if (m.spec() != null) {
|
||||
packer.packString("spec");
|
||||
packModuleReaderSpec(m.spec());
|
||||
}
|
||||
}
|
||||
case INITIALIZE_RESOURCE_READER_RESPONSE -> {
|
||||
var m = (InitializeResourceReaderResponse) msg;
|
||||
packMapHeader(1, m.spec());
|
||||
packKeyValue("requestId", m.requestId());
|
||||
if (m.spec() != null) {
|
||||
packer.packString("spec");
|
||||
packResourceReaderSpec(m.spec());
|
||||
}
|
||||
}
|
||||
case CLOSE_EXTERNAL_PROCESS -> packer.packMapHeader(0);
|
||||
default -> super.encodeMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.externalreader;
|
||||
|
||||
import org.pkl.core.messaging.Message.*;
|
||||
import org.pkl.core.messaging.Messages.ModuleReaderSpec;
|
||||
import org.pkl.core.messaging.Messages.ResourceReaderSpec;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public class ExternalReaderMessages {
|
||||
|
||||
public record InitializeModuleReaderRequest(long requestId, String scheme)
|
||||
implements Server.Request {
|
||||
public Type type() {
|
||||
return Type.INITIALIZE_MODULE_READER_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
public record InitializeResourceReaderRequest(long requestId, String scheme)
|
||||
implements Server.Request {
|
||||
public Type type() {
|
||||
return Type.INITIALIZE_RESOURCE_READER_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
public record InitializeModuleReaderResponse(long requestId, @Nullable ModuleReaderSpec spec)
|
||||
implements Client.Response {
|
||||
public Type type() {
|
||||
return Type.INITIALIZE_MODULE_READER_RESPONSE;
|
||||
}
|
||||
}
|
||||
|
||||
public record InitializeResourceReaderResponse(long requestId, @Nullable ResourceReaderSpec spec)
|
||||
implements Client.Response {
|
||||
public Type type() {
|
||||
return Type.INITIALIZE_RESOURCE_READER_RESPONSE;
|
||||
}
|
||||
}
|
||||
|
||||
public record CloseExternalProcess() implements Server.OneWay {
|
||||
public Type type() {
|
||||
return Type.CLOSE_EXTERNAL_PROCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.externalreader;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.pkl.core.messaging.MessageTransport;
|
||||
import org.pkl.core.messaging.Messages.ModuleReaderSpec;
|
||||
import org.pkl.core.messaging.Messages.ResourceReaderSpec;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
/** An interface for interacting with external module/resource processes. */
|
||||
public interface ExternalReaderProcess extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* Obtain the process's underlying {@link MessageTransport} for sending reader-specific message
|
||||
*
|
||||
* <p>May allocate resources upon first call, including spawning a child process. Must not be
|
||||
* called after {@link ExternalReaderProcess#close} has been called.
|
||||
*/
|
||||
MessageTransport getTransport() throws ExternalReaderProcessException;
|
||||
|
||||
/** Retrieve the spec, if available, of the process's module reader with the given scheme. */
|
||||
@Nullable
|
||||
ModuleReaderSpec getModuleReaderSpec(String scheme) throws IOException;
|
||||
|
||||
/** Retrieve the spec, if available, of the process's resource reader with the given scheme. */
|
||||
@Nullable
|
||||
ResourceReaderSpec getResourceReaderSpec(String scheme) throws IOException;
|
||||
|
||||
/**
|
||||
* Close the external process, cleaning up any resources.
|
||||
*
|
||||
* <p>The {@link MessageTransport} is sent the {@link ExternalReaderMessages.CloseExternalProcess}
|
||||
* message to request a graceful stop. A bespoke (empty) message type is used here instead of an
|
||||
* OS mechanism like signals to avoid forcing external reader implementers needing to handle many
|
||||
* OS-specific mechanisms. Implementations may then forcibly clean up resources after a timeout.
|
||||
* Must be safe to call multiple times.
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.externalreader;
|
||||
|
||||
public final class ExternalReaderProcessException extends Exception {
|
||||
public ExternalReaderProcessException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public ExternalReaderProcessException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.externalreader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ProcessBuilder.Redirect;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import org.pkl.core.Duration;
|
||||
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader;
|
||||
import org.pkl.core.externalreader.ExternalReaderMessages.*;
|
||||
import org.pkl.core.messaging.MessageTransport;
|
||||
import org.pkl.core.messaging.MessageTransports;
|
||||
import org.pkl.core.messaging.Messages.ModuleReaderSpec;
|
||||
import org.pkl.core.messaging.Messages.ResourceReaderSpec;
|
||||
import org.pkl.core.messaging.ProtocolException;
|
||||
import org.pkl.core.util.LateInit;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public class ExternalReaderProcessImpl implements ExternalReaderProcess {
|
||||
|
||||
private static final Duration CLOSE_TIMEOUT = Duration.ofSeconds(3);
|
||||
|
||||
private final ExternalReader spec;
|
||||
private final @Nullable String logPrefix;
|
||||
private final Map<String, Future<@Nullable ModuleReaderSpec>> initializeModuleReaderResponses =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<String, Future<@Nullable ResourceReaderSpec>>
|
||||
initializeResourceReaderResponses = new ConcurrentHashMap<>();
|
||||
|
||||
private @GuardedBy("this") boolean closed = false;
|
||||
|
||||
@LateInit
|
||||
@GuardedBy("this")
|
||||
private Process process;
|
||||
|
||||
@LateInit
|
||||
@GuardedBy("this")
|
||||
private MessageTransport transport;
|
||||
|
||||
private void log(String msg) {
|
||||
if (logPrefix != null) {
|
||||
System.err.println(logPrefix + msg);
|
||||
}
|
||||
}
|
||||
|
||||
public ExternalReaderProcessImpl(ExternalReader spec) {
|
||||
this.spec = spec;
|
||||
logPrefix =
|
||||
Objects.equals(System.getenv("PKL_DEBUG"), "1")
|
||||
? "[pkl-core][external-process][" + spec.executable() + "] "
|
||||
: null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized MessageTransport getTransport() throws ExternalReaderProcessException {
|
||||
if (closed) {
|
||||
throw new ExternalReaderProcessException("ExternalProcessImpl has already been closed");
|
||||
}
|
||||
if (process != null) {
|
||||
if (!process.isAlive()) {
|
||||
throw new ExternalReaderProcessException("ExternalProcessImpl process is no longer alive");
|
||||
}
|
||||
|
||||
return transport;
|
||||
}
|
||||
|
||||
// This relies on Java/OS behavior around PATH resolution, absolute/relative paths, etc.
|
||||
var command = new ArrayList<String>();
|
||||
command.add(spec.executable());
|
||||
command.addAll(spec.arguments());
|
||||
|
||||
var builder = new ProcessBuilder(command);
|
||||
builder.redirectError(Redirect.INHERIT); // inherit stderr from this pkl process
|
||||
try {
|
||||
process = builder.start();
|
||||
} catch (IOException e) {
|
||||
throw new ExternalReaderProcessException(e);
|
||||
}
|
||||
transport =
|
||||
MessageTransports.stream(
|
||||
new ExternalReaderMessagePackDecoder(process.getInputStream()),
|
||||
new ExternalReaderMessagePackEncoder(process.getOutputStream()),
|
||||
this::log);
|
||||
|
||||
var rxThread = new Thread(this::runTransport, "ExternalProcessImpl rxThread for " + spec);
|
||||
rxThread.setDaemon(true);
|
||||
rxThread.start();
|
||||
|
||||
return transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the underlying message transport so it can receive responses from the child process.
|
||||
*
|
||||
* <p>Blocks until the underlying transport is closed.
|
||||
*/
|
||||
private void runTransport() {
|
||||
try {
|
||||
transport.start(
|
||||
(msg) -> {
|
||||
throw new ProtocolException("Unexpected incoming one-way message: " + msg);
|
||||
},
|
||||
(msg) -> {
|
||||
throw new ProtocolException("Unexpected incoming request message: " + msg);
|
||||
});
|
||||
} catch (ProtocolException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
closed = true;
|
||||
if (process == null || !process.isAlive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (transport != null) {
|
||||
transport.send(new CloseExternalProcess());
|
||||
transport.close();
|
||||
}
|
||||
|
||||
// forcefully stop the process after the timeout
|
||||
// note that both transport.close() and process.destroy() are safe to call multiple times
|
||||
new Timer()
|
||||
.schedule(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (process != null) {
|
||||
transport.close();
|
||||
process.destroyForcibly();
|
||||
}
|
||||
}
|
||||
},
|
||||
CLOSE_TIMEOUT.inWholeMillis());
|
||||
|
||||
// block on process exit
|
||||
process.onExit().get();
|
||||
} catch (Exception e) {
|
||||
transport.close();
|
||||
process.destroyForcibly();
|
||||
} finally {
|
||||
process = null;
|
||||
transport = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ModuleReaderSpec getModuleReaderSpec(String uriScheme) throws IOException {
|
||||
return MessageTransports.resolveFuture(
|
||||
initializeModuleReaderResponses.computeIfAbsent(
|
||||
uriScheme,
|
||||
(scheme) -> {
|
||||
var future = new CompletableFuture<@Nullable ModuleReaderSpec>();
|
||||
var request = new InitializeModuleReaderRequest(new Random().nextLong(), scheme);
|
||||
try {
|
||||
getTransport()
|
||||
.send(
|
||||
request,
|
||||
(response) -> {
|
||||
if (response instanceof InitializeModuleReaderResponse resp) {
|
||||
future.complete(resp.spec());
|
||||
} else {
|
||||
future.completeExceptionally(
|
||||
new ProtocolException("unexpected response"));
|
||||
}
|
||||
});
|
||||
} catch (ProtocolException | IOException | ExternalReaderProcessException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
return future;
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ResourceReaderSpec getResourceReaderSpec(String uriScheme) throws IOException {
|
||||
return MessageTransports.resolveFuture(
|
||||
initializeResourceReaderResponses.computeIfAbsent(
|
||||
uriScheme,
|
||||
(scheme) -> {
|
||||
var future = new CompletableFuture<@Nullable ResourceReaderSpec>();
|
||||
var request = new InitializeResourceReaderRequest(new Random().nextLong(), scheme);
|
||||
try {
|
||||
getTransport()
|
||||
.send(
|
||||
request,
|
||||
(response) -> {
|
||||
log(response.toString());
|
||||
if (response instanceof InitializeResourceReaderResponse resp) {
|
||||
future.complete(resp.spec());
|
||||
} else {
|
||||
future.completeExceptionally(
|
||||
new ProtocolException("unexpected response"));
|
||||
}
|
||||
});
|
||||
} catch (ProtocolException | IOException | ExternalReaderProcessException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
return future;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NonnullByDefault
|
||||
package org.pkl.core.externalreader;
|
||||
|
||||
import org.pkl.core.util.NonnullByDefault;
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.msgpack.core.MessagePack;
|
||||
import org.msgpack.core.MessageTypeException;
|
||||
import org.msgpack.core.MessageUnpacker;
|
||||
import org.msgpack.value.Value;
|
||||
import org.msgpack.value.impl.ImmutableStringValueImpl;
|
||||
import org.pkl.core.messaging.Message.Type;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public abstract class AbstractMessagePackDecoder implements MessageDecoder {
|
||||
|
||||
protected final MessageUnpacker unpacker;
|
||||
|
||||
public AbstractMessagePackDecoder(MessageUnpacker unpacker) {
|
||||
this.unpacker = unpacker;
|
||||
}
|
||||
|
||||
public AbstractMessagePackDecoder(InputStream stream) {
|
||||
this(MessagePack.newDefaultUnpacker(stream));
|
||||
}
|
||||
|
||||
protected abstract @Nullable Message decodeMessage(Type msgType, Map<Value, Value> map)
|
||||
throws DecodeException, URISyntaxException;
|
||||
|
||||
@Override
|
||||
public @Nullable Message decode() throws IOException, DecodeException {
|
||||
if (!unpacker.hasNext()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int code;
|
||||
try {
|
||||
var arraySize = unpacker.unpackArrayHeader();
|
||||
if (arraySize != 2) {
|
||||
throw new DecodeException(ErrorMessages.create("malformedMessageHeaderLength", arraySize));
|
||||
}
|
||||
code = unpacker.unpackInt();
|
||||
} catch (MessageTypeException e) {
|
||||
throw new DecodeException(ErrorMessages.create("malformedMessageHeaderException"), e);
|
||||
}
|
||||
|
||||
Type msgType;
|
||||
try {
|
||||
msgType = Type.fromInt(code);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new DecodeException(
|
||||
ErrorMessages.create("malformedMessageHeaderUnrecognizedCode", Integer.toHexString(code)),
|
||||
e);
|
||||
}
|
||||
|
||||
try {
|
||||
var map = unpacker.unpackValue().asMapValue().map();
|
||||
var decoded = decodeMessage(msgType, map);
|
||||
if (decoded != null) {
|
||||
return decoded;
|
||||
}
|
||||
throw new DecodeException(
|
||||
ErrorMessages.create("unhandledMessageCode", Integer.toHexString(code)));
|
||||
} catch (MessageTypeException | URISyntaxException e) {
|
||||
throw new DecodeException(ErrorMessages.create("malformedMessageBody", code), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected static @Nullable Value getNullable(Map<Value, Value> map, String key) {
|
||||
return map.get(new ImmutableStringValueImpl(key));
|
||||
}
|
||||
|
||||
protected static Value get(Map<Value, Value> map, String key) throws DecodeException {
|
||||
var value = map.get(new ImmutableStringValueImpl(key));
|
||||
if (value == null) {
|
||||
throw new DecodeException(ErrorMessages.create("missingMessageParameter", key));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected static String unpackString(Map<Value, Value> map, String key) throws DecodeException {
|
||||
return get(map, key).asStringValue().asString();
|
||||
}
|
||||
|
||||
protected static @Nullable String unpackStringOrNull(Map<Value, Value> map, String key) {
|
||||
var value = getNullable(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.asStringValue().asString();
|
||||
}
|
||||
|
||||
protected static <T> @Nullable T unpackStringOrNull(
|
||||
Map<Value, Value> map, String key, Function<String, T> mapper) {
|
||||
var value = getNullable(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return mapper.apply(value.asStringValue().asString());
|
||||
}
|
||||
|
||||
protected static byte @Nullable [] unpackByteArray(Map<Value, Value> map, String key) {
|
||||
var value = getNullable(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.asBinaryValue().asByteArray();
|
||||
}
|
||||
|
||||
protected static boolean unpackBoolean(Map<Value, Value> map, String key) throws DecodeException {
|
||||
return get(map, key).asBooleanValue().getBoolean();
|
||||
}
|
||||
|
||||
protected static int unpackInt(Map<Value, Value> map, String key) throws DecodeException {
|
||||
return get(map, key).asIntegerValue().asInt();
|
||||
}
|
||||
|
||||
protected static long unpackLong(Map<Value, Value> map, String key) throws DecodeException {
|
||||
return get(map, key).asIntegerValue().asLong();
|
||||
}
|
||||
|
||||
protected static @Nullable Long unpackLongOrNull(Map<Value, Value> map, String key) {
|
||||
var value = getNullable(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.asIntegerValue().asLong();
|
||||
}
|
||||
|
||||
protected static <T> @Nullable T unpackLongOrNull(
|
||||
Map<Value, Value> map, String key, Function<Long, T> mapper) {
|
||||
var value = unpackLongOrNull(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return mapper.apply(value);
|
||||
}
|
||||
|
||||
protected static @Nullable List<String> unpackStringListOrNull(
|
||||
Map<Value, Value> map, String key) {
|
||||
var value = getNullable(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.asArrayValue().list().stream().map((it) -> it.asStringValue().asString()).toList();
|
||||
}
|
||||
|
||||
protected static @Nullable Map<String, String> unpackStringMapOrNull(
|
||||
Map<Value, Value> map, String key) {
|
||||
var value = getNullable(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.asMapValue().entrySet().stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
(e) -> e.getKey().asStringValue().asString(),
|
||||
(e) -> e.getValue().asStringValue().asString()));
|
||||
}
|
||||
|
||||
protected static <T> @Nullable List<T> unpackStringListOrNull(
|
||||
Map<Value, Value> map, String key, Function<String, T> mapper) {
|
||||
var value = unpackStringListOrNull(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.stream().map(mapper).toList();
|
||||
}
|
||||
|
||||
protected static <T> @Nullable List<T> unpackListOrNull(
|
||||
Map<Value, Value> map, String key, Function<Value, T> mapper) {
|
||||
var keys = getNullable(map, key);
|
||||
if (keys == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new ArrayList<T>(keys.asArrayValue().size());
|
||||
for (Value value : keys.asArrayValue()) {
|
||||
result.add(mapper.apply(value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected static <T> @Nullable Map<String, T> unpackStringMapOrNull(
|
||||
Map<Value, Value> map, String key, Function<Map<Value, Value>, T> mapper) {
|
||||
var value = getNullable(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.asMapValue().entrySet().stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
(e) -> e.getKey().asStringValue().asString(),
|
||||
(e) -> mapper.apply(e.getValue().asMapValue().map())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import org.msgpack.core.MessagePack;
|
||||
import org.msgpack.core.MessagePacker;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public abstract class AbstractMessagePackEncoder implements MessageEncoder {
|
||||
|
||||
protected final MessagePacker packer;
|
||||
|
||||
public AbstractMessagePackEncoder(MessagePacker packer) {
|
||||
this.packer = packer;
|
||||
}
|
||||
|
||||
public AbstractMessagePackEncoder(OutputStream stream) {
|
||||
this(MessagePack.newDefaultPacker(stream));
|
||||
}
|
||||
|
||||
protected abstract @Nullable void encodeMessage(Message msg)
|
||||
throws ProtocolException, IOException;
|
||||
|
||||
@Override
|
||||
public final void encode(Message msg) throws IOException, ProtocolException {
|
||||
packer.packArrayHeader(2);
|
||||
packer.packInt(msg.type().getCode());
|
||||
encodeMessage(msg);
|
||||
packer.flush();
|
||||
}
|
||||
|
||||
protected void packMapHeader(int size, @Nullable Object value1) throws IOException {
|
||||
packer.packMapHeader(size + (value1 != null ? 1 : 0));
|
||||
}
|
||||
|
||||
protected void packMapHeader(int size, @Nullable Object value1, @Nullable Object value2)
|
||||
throws IOException {
|
||||
packer.packMapHeader(size + (value1 != null ? 1 : 0) + (value2 != null ? 1 : 0));
|
||||
}
|
||||
|
||||
protected void packMapHeader(
|
||||
int size,
|
||||
@Nullable Object value1,
|
||||
@Nullable Object value2,
|
||||
@Nullable Object value3,
|
||||
@Nullable Object value4,
|
||||
@Nullable Object value5,
|
||||
@Nullable Object value6,
|
||||
@Nullable Object value7,
|
||||
@Nullable Object value8,
|
||||
@Nullable Object value9,
|
||||
@Nullable Object valueA,
|
||||
@Nullable Object valueB,
|
||||
@Nullable Object valueC,
|
||||
@Nullable Object valueD,
|
||||
@Nullable Object valueE,
|
||||
@Nullable Object valueF)
|
||||
throws IOException {
|
||||
packer.packMapHeader(
|
||||
size
|
||||
+ (value1 != null ? 1 : 0)
|
||||
+ (value2 != null ? 1 : 0)
|
||||
+ (value3 != null ? 1 : 0)
|
||||
+ (value4 != null ? 1 : 0)
|
||||
+ (value5 != null ? 1 : 0)
|
||||
+ (value6 != null ? 1 : 0)
|
||||
+ (value7 != null ? 1 : 0)
|
||||
+ (value8 != null ? 1 : 0)
|
||||
+ (value9 != null ? 1 : 0)
|
||||
+ (valueA != null ? 1 : 0)
|
||||
+ (valueB != null ? 1 : 0)
|
||||
+ (valueC != null ? 1 : 0)
|
||||
+ (valueD != null ? 1 : 0)
|
||||
+ (valueE != null ? 1 : 0)
|
||||
+ (valueF != null ? 1 : 0));
|
||||
}
|
||||
|
||||
protected void packKeyValue(String name, @Nullable Integer value) throws IOException {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
packer.packString(name);
|
||||
packer.packInt(value);
|
||||
}
|
||||
|
||||
protected void packKeyValue(String name, @Nullable Long value) throws IOException {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
packer.packString(name);
|
||||
packer.packLong(value);
|
||||
}
|
||||
|
||||
protected <T> void packKeyValueLong(String name, @Nullable T value, Function<T, Long> mapper)
|
||||
throws IOException {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
packKeyValue(name, mapper.apply(value));
|
||||
}
|
||||
|
||||
protected void packKeyValue(String name, @Nullable String value) throws IOException {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
packer.packString(name);
|
||||
packer.packString(value);
|
||||
}
|
||||
|
||||
protected <T> void packKeyValueString(String name, @Nullable T value, Function<T, String> mapper)
|
||||
throws IOException {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
packKeyValue(name, mapper.apply(value));
|
||||
}
|
||||
|
||||
protected void packKeyValue(String name, @Nullable Collection<String> value) throws IOException {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
packer.packString(name);
|
||||
packer.packArrayHeader(value.size());
|
||||
for (String elem : value) {
|
||||
packer.packString(elem);
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> void packKeyValue(
|
||||
String name, @Nullable Collection<T> value, Function<T, String> mapper) throws IOException {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
packer.packString(name);
|
||||
packer.packArrayHeader(value.size());
|
||||
for (T elem : value) {
|
||||
packer.packString(mapper.apply(elem));
|
||||
}
|
||||
}
|
||||
|
||||
protected void packKeyValue(String name, @Nullable Map<String, String> value) throws IOException {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
packer.packString(name);
|
||||
packer.packMapHeader(value.size());
|
||||
for (Map.Entry<String, String> e : value.entrySet()) {
|
||||
packer.packString(e.getKey());
|
||||
packer.packString(e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
protected void packKeyValue(String name, byte @Nullable [] value) throws IOException {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
packer.packString(name);
|
||||
packer.packBinaryHeader(value.length);
|
||||
packer.writePayload(value);
|
||||
}
|
||||
|
||||
protected void packKeyValue(String name, boolean value) throws IOException {
|
||||
packer.packString(name);
|
||||
packer.packBoolean(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.*;
|
||||
import org.msgpack.core.MessageUnpacker;
|
||||
import org.msgpack.value.Value;
|
||||
import org.pkl.core.messaging.Message.Type;
|
||||
import org.pkl.core.messaging.Messages.*;
|
||||
import org.pkl.core.module.PathElement;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public class BaseMessagePackDecoder extends AbstractMessagePackDecoder {
|
||||
|
||||
public BaseMessagePackDecoder(MessageUnpacker unpacker) {
|
||||
super(unpacker);
|
||||
}
|
||||
|
||||
public BaseMessagePackDecoder(InputStream stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
protected @Nullable Message decodeMessage(Type msgType, Map<Value, Value> map)
|
||||
throws DecodeException, URISyntaxException {
|
||||
return switch (msgType) {
|
||||
case READ_RESOURCE_REQUEST ->
|
||||
new ReadResourceRequest(
|
||||
unpackLong(map, "requestId"),
|
||||
unpackLong(map, "evaluatorId"),
|
||||
new URI(unpackString(map, "uri")));
|
||||
case READ_RESOURCE_RESPONSE ->
|
||||
new ReadResourceResponse(
|
||||
unpackLong(map, "requestId"),
|
||||
unpackLong(map, "evaluatorId"),
|
||||
unpackByteArray(map, "contents"),
|
||||
unpackStringOrNull(map, "error"));
|
||||
case READ_MODULE_REQUEST ->
|
||||
new ReadModuleRequest(
|
||||
unpackLong(map, "requestId"),
|
||||
unpackLong(map, "evaluatorId"),
|
||||
new URI(unpackString(map, "uri")));
|
||||
case READ_MODULE_RESPONSE ->
|
||||
new ReadModuleResponse(
|
||||
unpackLong(map, "requestId"),
|
||||
unpackLong(map, "evaluatorId"),
|
||||
unpackStringOrNull(map, "contents"),
|
||||
unpackStringOrNull(map, "error"));
|
||||
case LIST_RESOURCES_REQUEST ->
|
||||
new ListResourcesRequest(
|
||||
unpackLong(map, "requestId"),
|
||||
unpackLong(map, "evaluatorId"),
|
||||
new URI(unpackString(map, "uri")));
|
||||
case LIST_RESOURCES_RESPONSE ->
|
||||
new ListResourcesResponse(
|
||||
unpackLong(map, "requestId"),
|
||||
unpackLong(map, "evaluatorId"),
|
||||
unpackPathElements(map, "pathElements"),
|
||||
unpackStringOrNull(map, "error"));
|
||||
case LIST_MODULES_REQUEST ->
|
||||
new ListModulesRequest(
|
||||
unpackLong(map, "requestId"),
|
||||
unpackLong(map, "evaluatorId"),
|
||||
new URI(unpackString(map, "uri")));
|
||||
case LIST_MODULES_RESPONSE ->
|
||||
new ListModulesResponse(
|
||||
unpackLong(map, "requestId"),
|
||||
unpackLong(map, "evaluatorId"),
|
||||
unpackPathElements(map, "pathElements"),
|
||||
unpackStringOrNull(map, "error"));
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
protected static @Nullable ModuleReaderSpec unpackModuleReaderSpec(@Nullable Value value)
|
||||
throws DecodeException {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
var map = value.asMapValue().map();
|
||||
return new ModuleReaderSpec(
|
||||
unpackString(map, "scheme"),
|
||||
unpackBoolean(map, "hasHierarchicalUris"),
|
||||
unpackBoolean(map, "isLocal"),
|
||||
unpackBoolean(map, "isGlobbable"));
|
||||
}
|
||||
|
||||
protected static @Nullable ResourceReaderSpec unpackResourceReaderSpec(@Nullable Value value)
|
||||
throws DecodeException {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
var map = value.asMapValue().map();
|
||||
return new ResourceReaderSpec(
|
||||
unpackString(map, "scheme"),
|
||||
unpackBoolean(map, "hasHierarchicalUris"),
|
||||
unpackBoolean(map, "isGlobbable"));
|
||||
}
|
||||
|
||||
protected static @Nullable List<PathElement> unpackPathElements(Map<Value, Value> map, String key)
|
||||
throws DecodeException {
|
||||
var value = getNullable(map, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new ArrayList<PathElement>(value.asArrayValue().size());
|
||||
for (Value pathElement : value.asArrayValue()) {
|
||||
var pathElementMap = pathElement.asMapValue().map();
|
||||
result.add(
|
||||
new PathElement(
|
||||
unpackString(pathElementMap, "name"), unpackBoolean(pathElementMap, "isDirectory")));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import org.msgpack.core.MessagePacker;
|
||||
import org.pkl.core.messaging.Messages.*;
|
||||
import org.pkl.core.module.PathElement;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public class BaseMessagePackEncoder extends AbstractMessagePackEncoder {
|
||||
|
||||
public BaseMessagePackEncoder(MessagePacker packer) {
|
||||
super(packer);
|
||||
}
|
||||
|
||||
public BaseMessagePackEncoder(OutputStream stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
protected void packModuleReaderSpec(ModuleReaderSpec reader) throws IOException {
|
||||
packer.packMapHeader(4);
|
||||
packKeyValue("scheme", reader.scheme());
|
||||
packKeyValue("hasHierarchicalUris", reader.hasHierarchicalUris());
|
||||
packKeyValue("isLocal", reader.isLocal());
|
||||
packKeyValue("isGlobbable", reader.isGlobbable());
|
||||
}
|
||||
|
||||
protected void packResourceReaderSpec(ResourceReaderSpec reader) throws IOException {
|
||||
packer.packMapHeader(3);
|
||||
packKeyValue("scheme", reader.scheme());
|
||||
packKeyValue("hasHierarchicalUris", reader.hasHierarchicalUris());
|
||||
packKeyValue("isGlobbable", reader.isGlobbable());
|
||||
}
|
||||
|
||||
protected void packPathElement(PathElement pathElement) throws IOException {
|
||||
packer.packMapHeader(2);
|
||||
packKeyValue("name", pathElement.getName());
|
||||
packKeyValue("isDirectory", pathElement.isDirectory());
|
||||
}
|
||||
|
||||
protected @Nullable void encodeMessage(Message msg) throws ProtocolException, IOException {
|
||||
switch (msg.type()) {
|
||||
case READ_RESOURCE_REQUEST -> {
|
||||
var m = (ReadResourceRequest) msg;
|
||||
packer.packMapHeader(3);
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("evaluatorId", m.evaluatorId());
|
||||
packKeyValue("uri", m.uri().toString());
|
||||
}
|
||||
case READ_RESOURCE_RESPONSE -> {
|
||||
var m = (ReadResourceResponse) msg;
|
||||
packMapHeader(2, m.contents(), m.error());
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("evaluatorId", m.evaluatorId());
|
||||
packKeyValue("contents", m.contents());
|
||||
packKeyValue("error", m.error());
|
||||
}
|
||||
case READ_MODULE_REQUEST -> {
|
||||
var m = (ReadModuleRequest) msg;
|
||||
packer.packMapHeader(3);
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("evaluatorId", m.evaluatorId());
|
||||
packKeyValue("uri", m.uri().toString());
|
||||
}
|
||||
case READ_MODULE_RESPONSE -> {
|
||||
var m = (ReadModuleResponse) msg;
|
||||
packMapHeader(2, m.contents(), m.error());
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("evaluatorId", m.evaluatorId());
|
||||
packKeyValue("contents", m.contents());
|
||||
packKeyValue("error", m.error());
|
||||
}
|
||||
case LIST_RESOURCES_REQUEST -> {
|
||||
var m = (ListResourcesRequest) msg;
|
||||
packer.packMapHeader(3);
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("evaluatorId", m.evaluatorId());
|
||||
packKeyValue("uri", m.uri().toString());
|
||||
}
|
||||
case LIST_RESOURCES_RESPONSE -> {
|
||||
var m = (ListResourcesResponse) msg;
|
||||
packMapHeader(2, m.pathElements(), m.error());
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("evaluatorId", m.evaluatorId());
|
||||
if (m.pathElements() != null) {
|
||||
packer.packString("pathElements");
|
||||
packer.packArrayHeader(m.pathElements().size());
|
||||
for (var pathElement : m.pathElements()) {
|
||||
packPathElement(pathElement);
|
||||
}
|
||||
}
|
||||
packKeyValue("error", m.error());
|
||||
}
|
||||
case LIST_MODULES_REQUEST -> {
|
||||
var m = (ListModulesRequest) msg;
|
||||
packer.packMapHeader(3);
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("evaluatorId", m.evaluatorId());
|
||||
packKeyValue("uri", m.uri().toString());
|
||||
}
|
||||
case LIST_MODULES_RESPONSE -> {
|
||||
var m = (ListModulesResponse) msg;
|
||||
packMapHeader(2, m.pathElements(), m.error());
|
||||
packKeyValue("requestId", m.requestId());
|
||||
packKeyValue("evaluatorId", m.evaluatorId());
|
||||
if (m.pathElements() != null) {
|
||||
packer.packString("pathElements");
|
||||
packer.packArrayHeader(m.pathElements().size());
|
||||
for (var pathElement : m.pathElements()) {
|
||||
packPathElement(pathElement);
|
||||
}
|
||||
}
|
||||
packKeyValue("error", m.error());
|
||||
}
|
||||
default ->
|
||||
throw new ProtocolException(
|
||||
ErrorMessages.create("unhandledMessageType", msg.type().toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
public final class DecodeException extends ProtocolException {
|
||||
|
||||
public DecodeException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
public DecodeException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
89
pkl-core/src/main/java/org/pkl/core/messaging/Message.java
Normal file
89
pkl-core/src/main/java/org/pkl/core/messaging/Message.java
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
public interface Message {
|
||||
|
||||
Type type();
|
||||
|
||||
enum Type {
|
||||
CREATE_EVALUATOR_REQUEST(0x20),
|
||||
CREATE_EVALUATOR_RESPONSE(0x21),
|
||||
CLOSE_EVALUATOR(0x22),
|
||||
EVALUATE_REQUEST(0x23),
|
||||
EVALUATE_RESPONSE(0x24),
|
||||
LOG_MESSAGE(0x25),
|
||||
READ_RESOURCE_REQUEST(0x26),
|
||||
READ_RESOURCE_RESPONSE(0x27),
|
||||
READ_MODULE_REQUEST(0x28),
|
||||
READ_MODULE_RESPONSE(0x29),
|
||||
LIST_RESOURCES_REQUEST(0x2a),
|
||||
LIST_RESOURCES_RESPONSE(0x2b),
|
||||
LIST_MODULES_REQUEST(0x2c),
|
||||
LIST_MODULES_RESPONSE(0x2d),
|
||||
INITIALIZE_MODULE_READER_REQUEST(0x2e),
|
||||
INITIALIZE_MODULE_READER_RESPONSE(0x2f),
|
||||
INITIALIZE_RESOURCE_READER_REQUEST(0x30),
|
||||
INITIALIZE_RESOURCE_READER_RESPONSE(0x31),
|
||||
CLOSE_EXTERNAL_PROCESS(0x32);
|
||||
|
||||
private final int code;
|
||||
|
||||
Type(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static Type fromInt(int val) throws IllegalArgumentException {
|
||||
for (Type t : Type.values()) {
|
||||
if (t.code == val) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown Message.Type code");
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
interface OneWay extends Message {}
|
||||
|
||||
interface Request extends Message {
|
||||
long requestId();
|
||||
}
|
||||
|
||||
interface Response extends Message {
|
||||
long requestId();
|
||||
}
|
||||
|
||||
interface Client extends Message {
|
||||
interface Request extends Client, Message.Request {}
|
||||
|
||||
interface Response extends Client, Message.Response {}
|
||||
|
||||
interface OneWay extends Client, Message.OneWay {}
|
||||
}
|
||||
|
||||
interface Server extends Message {
|
||||
interface Request extends Server, Message.Request {}
|
||||
|
||||
interface Response extends Server, Message.Response {}
|
||||
|
||||
interface OneWay extends Server, Message.OneWay {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
/** Decodes a stream of messages. */
|
||||
public interface MessageDecoder {
|
||||
@Nullable
|
||||
Message decode() throws IOException, DecodeException;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/** Encodes a stream of messages. */
|
||||
public interface MessageEncoder {
|
||||
void encode(Message msg) throws IOException, ProtocolException;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/** A bidirectional transport for sending and receiving messages. */
|
||||
public interface MessageTransport extends AutoCloseable {
|
||||
interface OneWayHandler {
|
||||
void handleOneWay(Message.OneWay msg) throws ProtocolException;
|
||||
}
|
||||
|
||||
interface RequestHandler {
|
||||
void handleRequest(Message.Request msg) throws ProtocolException, IOException;
|
||||
}
|
||||
|
||||
interface ResponseHandler {
|
||||
void handleResponse(Message.Response msg) throws ProtocolException;
|
||||
}
|
||||
|
||||
void start(OneWayHandler oneWayHandler, RequestHandler requestHandler)
|
||||
throws ProtocolException, IOException;
|
||||
|
||||
void send(Message.OneWay message) throws ProtocolException, IOException;
|
||||
|
||||
void send(Message.Request message, ResponseHandler responseHandler)
|
||||
throws ProtocolException, IOException;
|
||||
|
||||
void send(Message.Response message) throws ProtocolException, IOException;
|
||||
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import org.pkl.core.messaging.Message.OneWay;
|
||||
import org.pkl.core.messaging.Message.Response;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.Pair;
|
||||
|
||||
/** Factory methods for creating [MessageTransport]s. */
|
||||
public class MessageTransports {
|
||||
|
||||
public interface Logger {
|
||||
void log(String msg);
|
||||
}
|
||||
|
||||
/** Creates a message transport that reads from [inputStream] and writes to [outputStream]. */
|
||||
public static MessageTransport stream(
|
||||
MessageDecoder decoder, MessageEncoder encoder, Logger logger) {
|
||||
return new EncodingMessageTransport(decoder, encoder, logger);
|
||||
}
|
||||
|
||||
/** Creates "client" and "server" transports that are directly connected to each other. */
|
||||
public static Pair<MessageTransport, MessageTransport> direct(Logger logger) {
|
||||
var transport1 = new DirectMessageTransport(logger);
|
||||
var transport2 = new DirectMessageTransport(logger);
|
||||
transport1.setOther(transport2);
|
||||
transport2.setOther(transport1);
|
||||
return Pair.of(transport1, transport2);
|
||||
}
|
||||
|
||||
public static <T> T resolveFuture(Future<T> future) throws IOException {
|
||||
try {
|
||||
return future.get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
if (e.getCause() instanceof IOException ioExc) {
|
||||
throw ioExc;
|
||||
} else {
|
||||
throw new IOException("external read failure: " + e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static class EncodingMessageTransport extends AbstractMessageTransport {
|
||||
|
||||
private final MessageDecoder decoder;
|
||||
private final MessageEncoder encoder;
|
||||
private volatile boolean isClosed = false;
|
||||
|
||||
protected EncodingMessageTransport(
|
||||
MessageDecoder decoder, MessageEncoder encoder, Logger logger) {
|
||||
super(logger);
|
||||
this.decoder = decoder;
|
||||
this.encoder = encoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws ProtocolException, IOException {
|
||||
while (!isClosed) {
|
||||
var message = decoder.decode();
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
accept(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doClose() {
|
||||
isClosed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSend(Message message) throws ProtocolException, IOException {
|
||||
encoder.encode(message);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class DirectMessageTransport extends AbstractMessageTransport {
|
||||
|
||||
private DirectMessageTransport other;
|
||||
|
||||
protected DirectMessageTransport(Logger logger) {
|
||||
super(logger);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() {}
|
||||
|
||||
@Override
|
||||
protected void doClose() {}
|
||||
|
||||
@Override
|
||||
protected void doSend(Message message) throws ProtocolException, IOException {
|
||||
other.accept(message);
|
||||
}
|
||||
|
||||
public void setOther(DirectMessageTransport other) {
|
||||
this.other = other;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract static class AbstractMessageTransport implements MessageTransport {
|
||||
|
||||
private final Logger logger;
|
||||
private MessageTransport.OneWayHandler oneWayHandler;
|
||||
private MessageTransport.RequestHandler requestHandler;
|
||||
private final Map<Long, ResponseHandler> responseHandlers = new ConcurrentHashMap<>();
|
||||
|
||||
protected AbstractMessageTransport(Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
protected void log(String message, Object... args) {
|
||||
var formatter = new MessageFormat(message);
|
||||
logger.log(formatter.format(args));
|
||||
}
|
||||
|
||||
protected abstract void doStart() throws ProtocolException, IOException;
|
||||
|
||||
protected abstract void doClose();
|
||||
|
||||
protected abstract void doSend(Message message) throws ProtocolException, IOException;
|
||||
|
||||
protected void accept(Message message) throws ProtocolException, IOException {
|
||||
log("Received message: {0}", message);
|
||||
if (message instanceof Message.OneWay msg) {
|
||||
oneWayHandler.handleOneWay(msg);
|
||||
} else if (message instanceof Message.Request msg) {
|
||||
requestHandler.handleRequest(msg);
|
||||
} else if (message instanceof Message.Response msg) {
|
||||
var handler = responseHandlers.remove(msg.requestId());
|
||||
if (handler == null) {
|
||||
throw new ProtocolException(
|
||||
ErrorMessages.create(
|
||||
"unknownRequestId", message.getClass().getSimpleName(), msg.requestId()));
|
||||
}
|
||||
handler.handleResponse(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void start(OneWayHandler oneWayHandler, RequestHandler requestHandler)
|
||||
throws ProtocolException, IOException {
|
||||
log("Starting transport: {0}", this);
|
||||
this.oneWayHandler = oneWayHandler;
|
||||
this.requestHandler = requestHandler;
|
||||
doStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void close() {
|
||||
log("Closing transport: {0}", this);
|
||||
doClose();
|
||||
responseHandlers.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(OneWay message) throws ProtocolException, IOException {
|
||||
log("Sending message: {0}", message);
|
||||
doSend(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Message.Request message, ResponseHandler responseHandler)
|
||||
throws ProtocolException, IOException {
|
||||
log("Sending message: {0}", message);
|
||||
responseHandlers.put(message.requestId(), responseHandler);
|
||||
doSend(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Response message) throws ProtocolException, IOException {
|
||||
log("Sending message: {0}", message);
|
||||
doSend(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
126
pkl-core/src/main/java/org/pkl/core/messaging/Messages.java
Normal file
126
pkl-core/src/main/java/org/pkl/core/messaging/Messages.java
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.pkl.core.messaging.Message.*;
|
||||
import org.pkl.core.module.PathElement;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public class Messages {
|
||||
|
||||
public record ModuleReaderSpec(
|
||||
String scheme, boolean hasHierarchicalUris, boolean isLocal, boolean isGlobbable) {}
|
||||
|
||||
public record ResourceReaderSpec(
|
||||
String scheme, boolean hasHierarchicalUris, boolean isGlobbable) {}
|
||||
|
||||
public record ListResourcesRequest(long requestId, long evaluatorId, URI uri)
|
||||
implements Server.Request {
|
||||
public Type type() {
|
||||
return Type.LIST_RESOURCES_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
public record ListResourcesResponse(
|
||||
long requestId,
|
||||
long evaluatorId,
|
||||
@Nullable List<PathElement> pathElements,
|
||||
@Nullable String error)
|
||||
implements Client.Response {
|
||||
public Type type() {
|
||||
return Type.LIST_RESOURCES_RESPONSE;
|
||||
}
|
||||
}
|
||||
|
||||
public record ListModulesRequest(long requestId, long evaluatorId, URI uri)
|
||||
implements Server.Request {
|
||||
public Type type() {
|
||||
return Type.LIST_MODULES_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
public record ListModulesResponse(
|
||||
long requestId,
|
||||
long evaluatorId,
|
||||
@Nullable List<PathElement> pathElements,
|
||||
@Nullable String error)
|
||||
implements Client.Response {
|
||||
public Type type() {
|
||||
return Type.LIST_MODULES_RESPONSE;
|
||||
}
|
||||
}
|
||||
|
||||
public record ReadResourceRequest(long requestId, long evaluatorId, URI uri)
|
||||
implements Message.Request {
|
||||
public Type type() {
|
||||
return Type.READ_RESOURCE_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
public record ReadResourceResponse(
|
||||
long requestId, long evaluatorId, byte @Nullable [] contents, @Nullable String error)
|
||||
implements Client.Response {
|
||||
|
||||
// workaround for kotlin bridging issue where `byte @Nullable [] contents` isn't detected as
|
||||
// nullable
|
||||
// public ReadResourceResponse(long requestId, long evaluatorId, @Nullable String error) {
|
||||
// this(requestId, evaluatorId, null, error);
|
||||
// }
|
||||
|
||||
public Type type() {
|
||||
return Type.READ_RESOURCE_RESPONSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ReadResourceResponse that = (ReadResourceResponse) o;
|
||||
return requestId == that.requestId
|
||||
&& evaluatorId == that.evaluatorId
|
||||
&& Objects.equals(error, that.error)
|
||||
&& Arrays.equals(contents, that.contents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(requestId, evaluatorId, Arrays.hashCode(contents), error);
|
||||
}
|
||||
}
|
||||
|
||||
public record ReadModuleRequest(long requestId, long evaluatorId, URI uri)
|
||||
implements Message.Request {
|
||||
public Type type() {
|
||||
return Type.READ_MODULE_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
public record ReadModuleResponse(
|
||||
long requestId, long evaluatorId, @Nullable String contents, @Nullable String error)
|
||||
implements Client.Response {
|
||||
public Type type() {
|
||||
return Type.READ_MODULE_RESPONSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
public class ProtocolException extends Exception {
|
||||
public ProtocolException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
public ProtocolException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NonnullByDefault
|
||||
package org.pkl.core.messaging;
|
||||
|
||||
import org.pkl.core.util.NonnullByDefault;
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.module;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.messaging.MessageTransport;
|
||||
import org.pkl.core.messaging.MessageTransports;
|
||||
import org.pkl.core.messaging.Messages.ListModulesRequest;
|
||||
import org.pkl.core.messaging.Messages.ListModulesResponse;
|
||||
import org.pkl.core.messaging.Messages.ReadModuleRequest;
|
||||
import org.pkl.core.messaging.Messages.ReadModuleResponse;
|
||||
import org.pkl.core.messaging.ProtocolException;
|
||||
|
||||
public class ExternalModuleResolver {
|
||||
private final MessageTransport transport;
|
||||
private final long evaluatorId;
|
||||
private final Map<URI, Future<String>> readResponses = new ConcurrentHashMap<>();
|
||||
private final Map<URI, Future<List<PathElement>>> listResponses = new ConcurrentHashMap<>();
|
||||
|
||||
public ExternalModuleResolver(MessageTransport transport, long evaluatorId) {
|
||||
this.transport = transport;
|
||||
this.evaluatorId = evaluatorId;
|
||||
}
|
||||
|
||||
public List<PathElement> listElements(SecurityManager securityManager, URI uri)
|
||||
throws IOException, SecurityManagerException {
|
||||
securityManager.checkResolveModule(uri);
|
||||
return doListElements(uri);
|
||||
}
|
||||
|
||||
public boolean hasElement(SecurityManager securityManager, URI uri)
|
||||
throws SecurityManagerException {
|
||||
securityManager.checkResolveModule(uri);
|
||||
try {
|
||||
doReadModule(uri);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String resolveModule(SecurityManager securityManager, URI uri)
|
||||
throws IOException, SecurityManagerException {
|
||||
securityManager.checkResolveModule(uri);
|
||||
return doReadModule(uri);
|
||||
}
|
||||
|
||||
private String doReadModule(URI moduleUri) throws IOException {
|
||||
return MessageTransports.resolveFuture(
|
||||
readResponses.computeIfAbsent(
|
||||
moduleUri,
|
||||
(uri) -> {
|
||||
var future = new CompletableFuture<String>();
|
||||
var request = new ReadModuleRequest(new Random().nextLong(), evaluatorId, uri);
|
||||
try {
|
||||
transport.send(
|
||||
request,
|
||||
(response) -> {
|
||||
if (response instanceof ReadModuleResponse resp) {
|
||||
if (resp.error() != null) {
|
||||
future.completeExceptionally(new IOException(resp.error()));
|
||||
} else if (resp.contents() != null) {
|
||||
future.complete(resp.contents());
|
||||
} else {
|
||||
future.complete("");
|
||||
}
|
||||
} else {
|
||||
future.completeExceptionally(new ProtocolException("unexpected response"));
|
||||
}
|
||||
});
|
||||
} catch (ProtocolException | IOException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
return future;
|
||||
}));
|
||||
}
|
||||
|
||||
private List<PathElement> doListElements(URI baseUri) throws IOException {
|
||||
return MessageTransports.resolveFuture(
|
||||
listResponses.computeIfAbsent(
|
||||
baseUri,
|
||||
(uri) -> {
|
||||
var future = new CompletableFuture<List<PathElement>>();
|
||||
var request = new ListModulesRequest(new Random().nextLong(), evaluatorId, uri);
|
||||
try {
|
||||
transport.send(
|
||||
request,
|
||||
(response) -> {
|
||||
if (response instanceof ListModulesResponse resp) {
|
||||
if (resp.error() != null) {
|
||||
future.completeExceptionally(new IOException(resp.error()));
|
||||
} else {
|
||||
future.complete(
|
||||
Objects.requireNonNullElseGet(resp.pathElements(), List::of));
|
||||
}
|
||||
} else {
|
||||
future.completeExceptionally(new ProtocolException("unexpected response"));
|
||||
}
|
||||
});
|
||||
} catch (ProtocolException | IOException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
return future;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.pkl.core.module;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.FileSystemNotFoundException;
|
||||
@@ -25,6 +26,10 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcess;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
|
||||
/** Utilities for obtaining and using module key factories. */
|
||||
@@ -72,7 +77,27 @@ public final class ModuleKeyFactories {
|
||||
return new ClassPath(classLoader);
|
||||
}
|
||||
|
||||
/** Closes the given factories, ignoring any exceptions. */
|
||||
/**
|
||||
* Returns a factory for external reader module keys
|
||||
*
|
||||
* <p>NOTE: {@code process} needs to be {@link ExternalReaderProcess#close closed} to avoid
|
||||
* resource leaks.
|
||||
*/
|
||||
public static ModuleKeyFactory externalProcess(String scheme, ExternalReaderProcess process) {
|
||||
return new ExternalProcess(scheme, process, 0);
|
||||
}
|
||||
|
||||
public static ModuleKeyFactory externalProcess(
|
||||
String scheme, ExternalReaderProcess process, long evaluatorId) {
|
||||
return new ExternalProcess(scheme, process, evaluatorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the given factories, ignoring any exceptions.
|
||||
*
|
||||
* @deprecated Replaced by {@link org.pkl.core.util.Readers#closeQuietly}.
|
||||
*/
|
||||
@Deprecated(since = "0.27.0", forRemoval = true)
|
||||
public static void closeQuietly(Iterable<ModuleKeyFactory> factories) {
|
||||
for (ModuleKeyFactory factory : factories) {
|
||||
try {
|
||||
@@ -225,4 +250,48 @@ public final class ModuleKeyFactories {
|
||||
INSTANCE = Collections.unmodifiableList(factories);
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents a module from an external reader process. */
|
||||
private static final class ExternalProcess implements ModuleKeyFactory {
|
||||
private final String scheme;
|
||||
private final ExternalReaderProcess process;
|
||||
private final long evaluatorId;
|
||||
|
||||
@GuardedBy("this")
|
||||
private ExternalModuleResolver resolver;
|
||||
|
||||
public ExternalProcess(String scheme, ExternalReaderProcess process, long evaluatorId) {
|
||||
this.scheme = scheme;
|
||||
this.process = process;
|
||||
this.evaluatorId = evaluatorId;
|
||||
}
|
||||
|
||||
private synchronized ExternalModuleResolver getResolver()
|
||||
throws ExternalReaderProcessException {
|
||||
if (resolver != null) {
|
||||
return resolver;
|
||||
}
|
||||
|
||||
resolver = new ExternalModuleResolver(process.getTransport(), evaluatorId);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
public Optional<ModuleKey> create(URI uri)
|
||||
throws URISyntaxException, ExternalReaderProcessException, IOException {
|
||||
if (!scheme.equalsIgnoreCase(uri.getScheme())) return Optional.empty();
|
||||
|
||||
var spec = process.getModuleReaderSpec(scheme);
|
||||
if (spec == null) {
|
||||
throw new ExternalReaderProcessException(
|
||||
ErrorMessages.create("externalReaderDoesNotSupportScheme", "module", scheme));
|
||||
}
|
||||
|
||||
return Optional.of(ModuleKeys.externalResolver(uri, spec, getResolver()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
process.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
*/
|
||||
package org.pkl.core.module;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Optional;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
|
||||
/** A factory for {@link ModuleKey}s. */
|
||||
public interface ModuleKeyFactory extends AutoCloseable {
|
||||
@@ -35,7 +37,8 @@ public interface ModuleKeyFactory extends AutoCloseable {
|
||||
* @param uri an absolute normalized URI
|
||||
* @return a module key for the given URI
|
||||
*/
|
||||
Optional<ModuleKey> create(URI uri) throws URISyntaxException;
|
||||
Optional<ModuleKey> create(URI uri)
|
||||
throws URISyntaxException, ExternalReaderProcessException, IOException;
|
||||
|
||||
/**
|
||||
* Closes this factory, releasing any resources held. See the documentation of factory methods in
|
||||
|
||||
@@ -29,6 +29,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.messaging.Messages.*;
|
||||
import org.pkl.core.packages.Dependency;
|
||||
import org.pkl.core.packages.Dependency.LocalDependency;
|
||||
import org.pkl.core.packages.PackageAssetUri;
|
||||
@@ -127,6 +129,12 @@ public final class ModuleKeys {
|
||||
return new ProjectPackage(assetUri);
|
||||
}
|
||||
|
||||
/** Creates a module key for an externally read module. */
|
||||
public static ModuleKey externalResolver(
|
||||
URI uri, ModuleReaderSpec spec, ExternalModuleResolver resolver) throws URISyntaxException {
|
||||
return new ExternalResolver(uri, spec, resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a module key that behaves like {@code delegate}, except that it returns {@code text} as
|
||||
* its loaded source.
|
||||
@@ -165,7 +173,7 @@ public final class ModuleKeys {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHierarchicalUris() {
|
||||
public boolean hasHierarchicalUris() throws IOException, ExternalReaderProcessException {
|
||||
return delegate.hasHierarchicalUris();
|
||||
}
|
||||
|
||||
@@ -175,19 +183,19 @@ public final class ModuleKeys {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlobbable() {
|
||||
public boolean isGlobbable() throws IOException, ExternalReaderProcessException {
|
||||
return delegate.isGlobbable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasElement(SecurityManager securityManager, URI uri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
return delegate.hasElement(securityManager, uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PathElement> listElements(SecurityManager securityManager, URI baseUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
return delegate.listElements(securityManager, baseUri);
|
||||
}
|
||||
}
|
||||
@@ -397,7 +405,6 @@ public final class ModuleKeys {
|
||||
}
|
||||
|
||||
private static final class ClassPath implements ModuleKey {
|
||||
|
||||
final URI uri;
|
||||
|
||||
final ClassLoader classLoader;
|
||||
@@ -460,7 +467,6 @@ public final class ModuleKeys {
|
||||
}
|
||||
|
||||
private static class Http implements ModuleKey {
|
||||
|
||||
private final URI uri;
|
||||
|
||||
Http(URI uri) {
|
||||
@@ -550,7 +556,6 @@ public final class ModuleKeys {
|
||||
}
|
||||
|
||||
private abstract static class AbstractPackage implements ModuleKey {
|
||||
|
||||
protected final PackageAssetUri packageAssetUri;
|
||||
|
||||
AbstractPackage(PackageAssetUri packageAssetUri) {
|
||||
@@ -663,6 +668,7 @@ public final class ModuleKeys {
|
||||
* an internal implementation detail, and we do not expect a module to declare this.
|
||||
*/
|
||||
public static class ProjectPackage extends AbstractPackage {
|
||||
|
||||
ProjectPackage(PackageAssetUri packageAssetUri) {
|
||||
super(packageAssetUri);
|
||||
}
|
||||
@@ -712,7 +718,7 @@ public final class ModuleKeys {
|
||||
|
||||
@Override
|
||||
public List<PathElement> listElements(SecurityManager securityManager, URI baseUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
securityManager.checkResolveModule(baseUri);
|
||||
var packageAssetUri = PackageAssetUri.create(baseUri);
|
||||
var dependency =
|
||||
@@ -733,7 +739,7 @@ public final class ModuleKeys {
|
||||
|
||||
@Override
|
||||
public boolean hasElement(SecurityManager securityManager, URI elementUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
securityManager.checkResolveModule(elementUri);
|
||||
var packageAssetUri = PackageAssetUri.create(elementUri);
|
||||
var dependency =
|
||||
@@ -769,4 +775,56 @@ public final class ModuleKeys {
|
||||
return projectResolver.getResolvedDependenciesForPackage(packageUri, dependencyMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExternalResolver implements ModuleKey {
|
||||
|
||||
private final URI uri;
|
||||
private final ModuleReaderSpec spec;
|
||||
private final ExternalModuleResolver resolver;
|
||||
|
||||
public ExternalResolver(URI uri, ModuleReaderSpec spec, ExternalModuleResolver resolver) {
|
||||
this.uri = uri;
|
||||
this.spec = spec;
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLocal() {
|
||||
return spec.isLocal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHierarchicalUris() {
|
||||
return spec.hasHierarchicalUris();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlobbable() {
|
||||
return spec.isGlobbable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PathElement> listElements(SecurityManager securityManager, URI baseUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
return resolver.listElements(securityManager, baseUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedModuleKey resolve(SecurityManager securityManager)
|
||||
throws IOException, SecurityManagerException {
|
||||
var contents = resolver.resolveModule(securityManager, uri);
|
||||
return ResolvedModuleKeys.virtual(this, uri, contents, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasElement(SecurityManager securityManager, URI elementUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
return resolver.hasElement(securityManager, elementUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.pkl.core.packages;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public final class Checksums {
|
||||
private final String sha256;
|
||||
@@ -34,7 +35,7 @@ public final class Checksums {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -522,6 +522,8 @@ public final class Project {
|
||||
modulePath,
|
||||
timeout,
|
||||
rootDir,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.messaging.MessageTransport;
|
||||
import org.pkl.core.messaging.MessageTransports;
|
||||
import org.pkl.core.messaging.Messages.*;
|
||||
import org.pkl.core.messaging.ProtocolException;
|
||||
import org.pkl.core.module.PathElement;
|
||||
|
||||
public class ExternalResourceResolver {
|
||||
private final MessageTransport transport;
|
||||
private final long evaluatorId;
|
||||
private final Map<URI, Future<byte[]>> readResponses = new ConcurrentHashMap<>();
|
||||
private final Map<URI, Future<List<PathElement>>> listResponses = new ConcurrentHashMap<>();
|
||||
|
||||
public ExternalResourceResolver(MessageTransport transport, long evaluatorId) {
|
||||
this.transport = transport;
|
||||
this.evaluatorId = evaluatorId;
|
||||
}
|
||||
|
||||
public Optional<Object> read(URI uri) throws IOException {
|
||||
var result = doRead(uri);
|
||||
return Optional.of(new Resource(uri, result));
|
||||
}
|
||||
|
||||
public boolean hasElement(org.pkl.core.SecurityManager securityManager, URI elementUri)
|
||||
throws SecurityManagerException {
|
||||
securityManager.checkResolveResource(elementUri);
|
||||
try {
|
||||
doRead(elementUri);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<PathElement> listElements(SecurityManager securityManager, URI baseUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
securityManager.checkResolveResource(baseUri);
|
||||
return doListElements(baseUri);
|
||||
}
|
||||
|
||||
public List<PathElement> doListElements(URI baseUri) throws IOException {
|
||||
return MessageTransports.resolveFuture(
|
||||
listResponses.computeIfAbsent(
|
||||
baseUri,
|
||||
(uri) -> {
|
||||
var future = new CompletableFuture<List<PathElement>>();
|
||||
var request = new ListResourcesRequest(new Random().nextLong(), evaluatorId, uri);
|
||||
try {
|
||||
transport.send(
|
||||
request,
|
||||
(response) -> {
|
||||
if (response instanceof ListResourcesResponse resp) {
|
||||
if (resp.error() != null) {
|
||||
future.completeExceptionally(new IOException(resp.error()));
|
||||
} else {
|
||||
future.complete(
|
||||
Objects.requireNonNullElseGet(resp.pathElements(), List::of));
|
||||
}
|
||||
} else {
|
||||
future.completeExceptionally(new ProtocolException("unexpected response"));
|
||||
}
|
||||
});
|
||||
} catch (ProtocolException | IOException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
return future;
|
||||
}));
|
||||
}
|
||||
|
||||
public byte[] doRead(URI baseUri) throws IOException {
|
||||
return MessageTransports.resolveFuture(
|
||||
readResponses.computeIfAbsent(
|
||||
baseUri,
|
||||
(uri) -> {
|
||||
var future = new CompletableFuture<byte[]>();
|
||||
var request = new ReadResourceRequest(new Random().nextLong(), evaluatorId, uri);
|
||||
try {
|
||||
transport.send(
|
||||
request,
|
||||
(response) -> {
|
||||
if (response instanceof ReadResourceResponse resp) {
|
||||
if (resp.error() != null) {
|
||||
future.completeExceptionally(new IOException(resp.error()));
|
||||
} else if (resp.contents() != null) {
|
||||
future.complete(resp.contents());
|
||||
} else {
|
||||
future.complete(new byte[0]);
|
||||
}
|
||||
} else {
|
||||
future.completeExceptionally(new ProtocolException("unexpected response"));
|
||||
}
|
||||
});
|
||||
} catch (ProtocolException | IOException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
return future;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Optional;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.runtime.ReaderBase;
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ import org.pkl.core.runtime.ReaderBase;
|
||||
*
|
||||
* <p>See {@link ResourceReaders} for predefined resource readers.
|
||||
*/
|
||||
public interface ResourceReader extends ReaderBase {
|
||||
public interface ResourceReader extends ReaderBase, AutoCloseable {
|
||||
/** The URI scheme associated with resources read by this resource reader. */
|
||||
String getUriScheme();
|
||||
|
||||
@@ -54,5 +55,16 @@ public interface ResourceReader extends ReaderBase {
|
||||
* manager.
|
||||
* </ul>
|
||||
*/
|
||||
Optional<Object> read(URI uri) throws IOException, URISyntaxException, SecurityManagerException;
|
||||
Optional<Object> read(URI uri)
|
||||
throws IOException,
|
||||
URISyntaxException,
|
||||
SecurityManagerException,
|
||||
ExternalReaderProcessException;
|
||||
|
||||
/**
|
||||
* Closes this reader, releasing any resources held. See the documentation of factory methods in
|
||||
* {@link ResourceReaders} for which factories need to be closed.
|
||||
*/
|
||||
@Override
|
||||
default void close() {}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcess;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.messaging.Messages.*;
|
||||
import org.pkl.core.module.FileResolver;
|
||||
import org.pkl.core.module.ModulePathResolver;
|
||||
import org.pkl.core.module.PathElement;
|
||||
@@ -137,6 +140,21 @@ public final class ResourceReaders {
|
||||
return FromServiceProviders.INSTANCE;
|
||||
}
|
||||
|
||||
public static ResourceReader externalProcess(
|
||||
String scheme, ExternalReaderProcess externalReaderProcess) {
|
||||
return new ExternalProcess(scheme, externalReaderProcess, 0);
|
||||
}
|
||||
|
||||
public static ResourceReader externalProcess(
|
||||
String scheme, ExternalReaderProcess externalReaderProcess, long evaluatorId) {
|
||||
return new ExternalProcess(scheme, externalReaderProcess, evaluatorId);
|
||||
}
|
||||
|
||||
public static ResourceReader externalResolver(
|
||||
ResourceReaderSpec spec, ExternalResourceResolver resolver) {
|
||||
return new ExternalResolver(spec, resolver);
|
||||
}
|
||||
|
||||
private static final class EnvironmentVariable implements ResourceReader {
|
||||
static final ResourceReader INSTANCE = new EnvironmentVariable();
|
||||
|
||||
@@ -521,7 +539,7 @@ public final class ResourceReaders {
|
||||
|
||||
@Override
|
||||
public List<PathElement> listElements(SecurityManager securityManager, URI baseUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
securityManager.checkResolveResource(baseUri);
|
||||
var packageAssetUri = PackageAssetUri.create(baseUri);
|
||||
var dependency =
|
||||
@@ -543,7 +561,7 @@ public final class ResourceReaders {
|
||||
|
||||
@Override
|
||||
public boolean hasElement(SecurityManager securityManager, URI elementUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
securityManager.checkResolveResource(elementUri);
|
||||
var packageAssetUri = PackageAssetUri.create(elementUri);
|
||||
var dependency =
|
||||
@@ -585,6 +603,7 @@ public final class ResourceReaders {
|
||||
}
|
||||
|
||||
private static class FromServiceProviders {
|
||||
|
||||
private static final List<ResourceReader> INSTANCE;
|
||||
|
||||
static {
|
||||
@@ -594,4 +613,113 @@ public final class ResourceReaders {
|
||||
INSTANCE = Collections.unmodifiableList(readers);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ExternalProcess implements ResourceReader {
|
||||
private final String scheme;
|
||||
private final ExternalReaderProcess process;
|
||||
private final long evaluatorId;
|
||||
private ExternalResolver underlying;
|
||||
|
||||
public ExternalProcess(String scheme, ExternalReaderProcess process, long evaluatorId) {
|
||||
this.scheme = scheme;
|
||||
this.process = process;
|
||||
this.evaluatorId = evaluatorId;
|
||||
}
|
||||
|
||||
private ExternalResolver getUnderlyingReader()
|
||||
throws ExternalReaderProcessException, IOException {
|
||||
if (underlying != null) {
|
||||
return underlying;
|
||||
}
|
||||
|
||||
var spec = process.getResourceReaderSpec(scheme);
|
||||
if (spec == null) {
|
||||
throw new ExternalReaderProcessException(
|
||||
ErrorMessages.create("externalReaderDoesNotSupportScheme", "resource", scheme));
|
||||
}
|
||||
underlying =
|
||||
new ExternalResolver(
|
||||
spec, new ExternalResourceResolver(process.getTransport(), evaluatorId));
|
||||
return underlying;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUriScheme() {
|
||||
return scheme;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHierarchicalUris() throws ExternalReaderProcessException, IOException {
|
||||
return getUnderlyingReader().hasHierarchicalUris();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlobbable() throws ExternalReaderProcessException, IOException {
|
||||
return getUnderlyingReader().isGlobbable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> read(URI uri) throws IOException, ExternalReaderProcessException {
|
||||
return getUnderlyingReader().read(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasElement(SecurityManager securityManager, URI elementUri)
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
return getUnderlyingReader().hasElement(securityManager, elementUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PathElement> listElements(SecurityManager securityManager, URI baseUri)
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
return getUnderlyingReader().listElements(securityManager, baseUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
process.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ExternalResolver implements ResourceReader {
|
||||
private final ResourceReaderSpec readerSpec;
|
||||
private final ExternalResourceResolver resolver;
|
||||
|
||||
public ExternalResolver(ResourceReaderSpec readerSpec, ExternalResourceResolver resolver) {
|
||||
this.readerSpec = readerSpec;
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHierarchicalUris() {
|
||||
return readerSpec.hasHierarchicalUris();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlobbable() {
|
||||
return readerSpec.isGlobbable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUriScheme() {
|
||||
return readerSpec.scheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> read(URI uri) throws IOException {
|
||||
return resolver.read(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasElement(org.pkl.core.SecurityManager securityManager, URI elementUri)
|
||||
throws SecurityManagerException {
|
||||
return resolver.hasElement(securityManager, elementUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PathElement> listElements(SecurityManager securityManager, URI baseUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
return resolver.listElements(securityManager, baseUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
package org.pkl.core.runtime;
|
||||
|
||||
import com.oracle.truffle.api.nodes.Node;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import org.pkl.core.ModuleSource;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.module.ModuleKeyFactory;
|
||||
import org.pkl.core.module.ModuleKeys;
|
||||
@@ -82,6 +84,18 @@ public final class ModuleResolver {
|
||||
.evalError("invalidModuleUri", moduleUri)
|
||||
.withHint(e.getReason())
|
||||
.build();
|
||||
} catch (ExternalReaderProcessException e) {
|
||||
throw new VmExceptionBuilder()
|
||||
.withOptionalLocation(importNode)
|
||||
.evalError("externalReaderFailure")
|
||||
.withCause(e)
|
||||
.build();
|
||||
} catch (IOException e) {
|
||||
throw new VmExceptionBuilder()
|
||||
.withOptionalLocation(importNode)
|
||||
.evalError("ioErrorLoadingModule")
|
||||
.withCause(e)
|
||||
.build();
|
||||
}
|
||||
if (key.isPresent()) return key.get();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.net.URI;
|
||||
import java.util.List;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.module.PathElement;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
@@ -29,10 +30,10 @@ public interface ReaderBase {
|
||||
* Tells if the URIs represented by this module key or resource reader should be interpreted as <a
|
||||
* href="https://www.rfc-editor.org/rfc/rfc3986#section-1.2.3">hierarchical</a>.
|
||||
*/
|
||||
boolean hasHierarchicalUris();
|
||||
boolean hasHierarchicalUris() throws ExternalReaderProcessException, IOException;
|
||||
|
||||
/** Tells if this module key or resource reader supports globbing. */
|
||||
boolean isGlobbable();
|
||||
boolean isGlobbable() throws ExternalReaderProcessException, IOException;
|
||||
|
||||
/**
|
||||
* Tells if relative paths of this URI should be resolved from {@link URI#getFragment()}, rather
|
||||
@@ -49,7 +50,7 @@ public interface ReaderBase {
|
||||
* if either {@link #isGlobbable()} or {@link ModuleKey#isLocal()} returns true.
|
||||
*/
|
||||
default boolean hasElement(SecurityManager securityManager, URI elementUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ public interface ReaderBase {
|
||||
* this reader.
|
||||
*/
|
||||
default List<PathElement> listElements(SecurityManager securityManager, URI baseUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.http.HttpClientInitException;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.resource.Resource;
|
||||
@@ -83,7 +84,10 @@ public final class ResourceManager {
|
||||
.withHint(e.getReason())
|
||||
.withOptionalLocation(readNode)
|
||||
.build();
|
||||
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
|
||||
} catch (SecurityManagerException
|
||||
| PackageLoadError
|
||||
| HttpClientInitException
|
||||
| ExternalReaderProcessException e) {
|
||||
throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build();
|
||||
}
|
||||
return resource;
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.ast.builder.ImportsAndReadsParser;
|
||||
import org.pkl.core.ast.builder.ImportsAndReadsParser.Entry;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.util.GlobResolver;
|
||||
import org.pkl.core.util.GlobResolver.InvalidGlobPatternException;
|
||||
import org.pkl.core.util.GlobResolver.ResolvedGlobElement;
|
||||
@@ -38,7 +39,10 @@ import org.pkl.core.util.IoUtils;
|
||||
public class VmImportAnalyzer {
|
||||
@TruffleBoundary
|
||||
public static ImportGraph analyze(URI[] moduleUris, VmContext context)
|
||||
throws IOException, URISyntaxException, SecurityManagerException {
|
||||
throws IOException,
|
||||
URISyntaxException,
|
||||
SecurityManagerException,
|
||||
ExternalReaderProcessException {
|
||||
var imports = new TreeMap<URI, Set<ImportGraph.Import>>();
|
||||
var resolvedImports = new TreeMap<URI, URI>();
|
||||
for (var moduleUri : moduleUris) {
|
||||
@@ -53,7 +57,10 @@ public class VmImportAnalyzer {
|
||||
VmContext context,
|
||||
Map<URI, Set<ImportGraph.Import>> imports,
|
||||
Map<URI, URI> resolvedImports)
|
||||
throws IOException, URISyntaxException, SecurityManagerException {
|
||||
throws IOException,
|
||||
URISyntaxException,
|
||||
SecurityManagerException,
|
||||
ExternalReaderProcessException {
|
||||
var moduleResolver = context.getModuleResolver();
|
||||
var securityManager = context.getSecurityManager();
|
||||
var importsInModule = collectImports(moduleUri, moduleResolver, securityManager);
|
||||
@@ -71,7 +78,10 @@ public class VmImportAnalyzer {
|
||||
|
||||
private static Set<ImportGraph.Import> collectImports(
|
||||
URI moduleUri, ModuleResolver moduleResolver, SecurityManager securityManager)
|
||||
throws IOException, URISyntaxException, SecurityManagerException {
|
||||
throws IOException,
|
||||
URISyntaxException,
|
||||
SecurityManagerException,
|
||||
ExternalReaderProcessException {
|
||||
var moduleKey = moduleResolver.resolve(moduleUri);
|
||||
var resolvedModuleKey = moduleKey.resolve(securityManager);
|
||||
List<Entry> importsAndReads;
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.pkl.core.module.ModuleKeyFactories;
|
||||
import org.pkl.core.module.ModulePathResolver;
|
||||
import org.pkl.core.project.Project;
|
||||
import org.pkl.core.resource.ResourceReaders;
|
||||
import org.pkl.core.util.Readers;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpi;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiException;
|
||||
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
|
||||
@@ -125,7 +126,8 @@ public final class ExecutorSpiImpl implements ExecutorSpi {
|
||||
} catch (PklException e) {
|
||||
throw new ExecutorSpiException(e.getMessage(), e.getCause());
|
||||
} finally {
|
||||
ModuleKeyFactories.closeQuietly(builder.getModuleKeyFactories());
|
||||
Readers.closeQuietly(builder.getModuleKeyFactories());
|
||||
Readers.closeQuietly(builder.getResourceReaders());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.net.URISyntaxException;
|
||||
import org.pkl.core.ImportGraph;
|
||||
import org.pkl.core.ImportGraph.Import;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.runtime.AnalyzeModule;
|
||||
import org.pkl.core.runtime.VmContext;
|
||||
@@ -91,7 +92,11 @@ public final class AnalyzeNodes {
|
||||
try {
|
||||
var results = VmImportAnalyzer.analyze(uris, context);
|
||||
return importGraphFactory.create(results);
|
||||
} catch (IOException | URISyntaxException | SecurityManagerException | PackageLoadError e) {
|
||||
} catch (IOException
|
||||
| URISyntaxException
|
||||
| SecurityManagerException
|
||||
| PackageLoadError
|
||||
| ExternalReaderProcessException e) {
|
||||
throw exceptionBuilder().withCause(e).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.module.PathElement;
|
||||
import org.pkl.core.module.ResolvedModuleKey;
|
||||
@@ -108,7 +109,7 @@ public final class OutputBenchmarkNodes {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHierarchicalUris() {
|
||||
public boolean hasHierarchicalUris() throws IOException, ExternalReaderProcessException {
|
||||
return delegate.hasHierarchicalUris();
|
||||
}
|
||||
|
||||
@@ -118,19 +119,19 @@ public final class OutputBenchmarkNodes {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlobbable() {
|
||||
public boolean isGlobbable() throws IOException, ExternalReaderProcessException {
|
||||
return delegate.isGlobbable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasElement(SecurityManager securityManager, URI uri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
return delegate.hasElement(securityManager, uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PathElement> listElements(SecurityManager securityManager, URI baseUri)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
return delegate.listElements(securityManager, baseUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.util.stream.Collectors;
|
||||
import org.pkl.core.PklBugException;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.module.PathElement;
|
||||
import org.pkl.core.runtime.ReaderBase;
|
||||
@@ -260,7 +261,7 @@ public final class GlobResolver {
|
||||
URI globUri,
|
||||
Pattern pattern,
|
||||
Map<String, ResolvedGlobElement> result)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
var elements = reader.listElements(securityManager, globUri);
|
||||
for (var elem : sorted(elements)) {
|
||||
URI resolvedUri;
|
||||
@@ -318,7 +319,10 @@ public final class GlobResolver {
|
||||
boolean isGlobStar,
|
||||
boolean hasAbsoluteGlob,
|
||||
MutableLong listElementCallCount)
|
||||
throws IOException, SecurityManagerException, InvalidGlobPatternException {
|
||||
throws IOException,
|
||||
SecurityManagerException,
|
||||
InvalidGlobPatternException,
|
||||
ExternalReaderProcessException {
|
||||
var result = new ArrayList<ResolvedGlobElement>();
|
||||
doExpandHierarchicalGlobPart(
|
||||
securityManager,
|
||||
@@ -343,7 +347,10 @@ public final class GlobResolver {
|
||||
boolean hasAbsoluteGlob,
|
||||
MutableLong listElementCallCount,
|
||||
List<ResolvedGlobElement> result)
|
||||
throws IOException, SecurityManagerException, InvalidGlobPatternException {
|
||||
throws IOException,
|
||||
SecurityManagerException,
|
||||
InvalidGlobPatternException,
|
||||
ExternalReaderProcessException {
|
||||
|
||||
if (listElementCallCount.getAndIncrement() > maxListElements()) {
|
||||
throw new InvalidGlobPatternException(ErrorMessages.create("invalidGlobTooComplex"));
|
||||
@@ -384,7 +391,10 @@ public final class GlobResolver {
|
||||
boolean hasAbsoluteGlob,
|
||||
Map<String, ResolvedGlobElement> result,
|
||||
MutableLong listElementCallCount)
|
||||
throws IOException, SecurityManagerException, InvalidGlobPatternException {
|
||||
throws IOException,
|
||||
SecurityManagerException,
|
||||
InvalidGlobPatternException,
|
||||
ExternalReaderProcessException {
|
||||
var isLeaf = idx == globPatternParts.length - 1;
|
||||
var patternPart = globPatternParts[idx];
|
||||
if (isRegularPathPart(patternPart)) {
|
||||
@@ -481,7 +491,10 @@ public final class GlobResolver {
|
||||
ModuleKey enclosingModuleKey,
|
||||
URI enclosingUri,
|
||||
String globPattern)
|
||||
throws IOException, SecurityManagerException, InvalidGlobPatternException {
|
||||
throws IOException,
|
||||
SecurityManagerException,
|
||||
InvalidGlobPatternException,
|
||||
ExternalReaderProcessException {
|
||||
|
||||
var result = new LinkedHashMap<String, ResolvedGlobElement>();
|
||||
var hasAbsoluteGlob = globPattern.matches("\\w+:.*");
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.pkl.core.PklBugException;
|
||||
import org.pkl.core.Platform;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.externalreader.ExternalReaderProcessException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.runtime.ReaderBase;
|
||||
@@ -317,7 +318,7 @@ public final class IoUtils {
|
||||
|
||||
private static URI resolveTripleDotImport(
|
||||
SecurityManager securityManager, ModuleKey moduleKey, String tripleDotPath)
|
||||
throws IOException, SecurityManagerException {
|
||||
throws IOException, SecurityManagerException, ExternalReaderProcessException {
|
||||
var moduleKeyUri = moduleKey.getUri();
|
||||
if (!moduleKey.isLocal() || !moduleKey.hasHierarchicalUris()) {
|
||||
throw new VmExceptionBuilder()
|
||||
@@ -363,7 +364,8 @@ public final class IoUtils {
|
||||
return Pair.of(importPath.substring(1, idx), importPath.substring(idx));
|
||||
}
|
||||
|
||||
private static URI resolveProjectDependency(ModuleKey moduleKey, String notation) {
|
||||
private static URI resolveProjectDependency(ModuleKey moduleKey, String notation)
|
||||
throws IOException, ExternalReaderProcessException {
|
||||
var parsed = parseDependencyNotation(notation);
|
||||
var name = parsed.getFirst();
|
||||
var path = parsed.getSecond();
|
||||
@@ -395,7 +397,10 @@ public final class IoUtils {
|
||||
* dependency notation ()
|
||||
*/
|
||||
public static URI resolve(SecurityManager securityManager, ModuleKey moduleKey, URI importUri)
|
||||
throws URISyntaxException, IOException, SecurityManagerException {
|
||||
throws URISyntaxException,
|
||||
IOException,
|
||||
SecurityManagerException,
|
||||
ExternalReaderProcessException {
|
||||
if (importUri.isAbsolute()) {
|
||||
return moduleKey.resolveUri(importUri);
|
||||
}
|
||||
|
||||
28
pkl-core/src/main/java/org/pkl/core/util/Readers.java
Normal file
28
pkl-core/src/main/java/org/pkl/core/util/Readers.java
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.util;
|
||||
|
||||
public class Readers {
|
||||
/** Closes the given readers, ignoring any exceptions. */
|
||||
public static void closeQuietly(Iterable<? extends AutoCloseable> readers) {
|
||||
for (var reader : readers) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1081,3 +1081,33 @@ Malformed proxy URI (expecting `http://<host>[:<port>]`): `{0}`.
|
||||
|
||||
cannotAnalyzeBecauseSyntaxError=\
|
||||
Found a syntax error when parsing module `{0}`.
|
||||
|
||||
malformedMessageHeaderLength=\
|
||||
Malformed message header (expected size 2, but got {0}).
|
||||
|
||||
malformedMessageHeaderException=\
|
||||
Malformed message header.
|
||||
|
||||
malformedMessageHeaderUnrecognizedCode=\
|
||||
Malformed message header (unrecognized code `{0}`).
|
||||
|
||||
unhandledMessageCode=\
|
||||
Unhandled decoding message code `{0}`.
|
||||
|
||||
unhandledMessageType=\
|
||||
Unhandled encoding message type `{0}`.
|
||||
|
||||
malformedMessageBody=\
|
||||
Malformed message body for message with code `{0}`.
|
||||
|
||||
missingMessageParameter=\
|
||||
Missing message parameter `{0}`
|
||||
|
||||
unknownRequestId=\
|
||||
Received response {0} for unknown request ID `{1}`.
|
||||
|
||||
externalReaderFailure=\
|
||||
Failed to communicate with external reader process.
|
||||
|
||||
externalReaderDoesNotSupportScheme=\
|
||||
External {0} reader does not support scheme `{1}`.
|
||||
|
||||
Reference in New Issue
Block a user