Initial commit

This commit is contained in:
Peter Niederwieser
2016-01-19 14:51:19 +01:00
committed by Dan Chao
commit ecad035dca
2972 changed files with 211653 additions and 0 deletions

View File

@@ -0,0 +1,252 @@
/**
* 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.server
import java.nio.file.Path
import java.time.Duration
import org.msgpack.core.MessagePacker
import org.pkl.core.*
import org.pkl.core.ast.member.ObjectMember
import org.pkl.core.module.ModuleKeyFactory
import org.pkl.core.project.DeclaredDependencies
import org.pkl.core.resource.ResourceReader
import org.pkl.core.runtime.*
internal class BinaryEvaluator(
transformer: StackFrameTransformer,
manager: SecurityManager,
logger: Logger,
factories: Collection<ModuleKeyFactory?>,
readers: Collection<ResourceReader?>,
environmentVariables: Map<String, String>,
externalProperties: Map<String, String>,
timeout: Duration?,
moduleCacheDir: Path?,
declaredDependencies: DeclaredDependencies?,
outputFormat: String?
) :
EvaluatorImpl(
transformer,
manager,
logger,
factories,
readers,
environmentVariables,
externalProperties,
timeout,
moduleCacheDir,
declaredDependencies,
outputFormat
) {
fun evaluate(moduleSource: ModuleSource, expression: String?): ByteArray {
return doEvaluate(moduleSource) { module ->
val evalResult =
expression?.let { VmUtils.evaluateExpression(module, it, securityManager, moduleResolver) }
?: module
VmValue.force(evalResult, false)
threadLocalBufferPacker
.get()
.apply {
clear()
ValueEncoder(this).visit(evalResult)
}
.toByteArray()
}
}
private class ValueEncoder(private val packer: MessagePacker) : VmValueVisitor {
companion object {
private const val CODE_OBJECT: Byte = 0x1
private const val CODE_MAP: Byte = 0x2
private const val CODE_MAPPING: Byte = 0x3
private const val CODE_LIST: Byte = 0x4
private const val CODE_LISTING: Byte = 0x5
private const val CODE_SET: Byte = 0x6
private const val CODE_DURATION: Byte = 0x7
private const val CODE_DATASIZE: Byte = 0x8
private const val CODE_PAIR: Byte = 0x9
private const val CODE_INTSEQ: Byte = 0xA
private const val CODE_REGEX: Byte = 0xB
private const val CODE_CLASS: Byte = 0xC
private const val CODE_TYPEALIAS: Byte = 0xD
private const val CODE_FUNCTION: Byte = 0xE
private const val CODE_PROPERTY: Byte = 0x10
private const val CODE_ENTRY: Byte = 0x11
private const val CODE_ELEMENT: Byte = 0x12
}
override fun visitString(value: String) {
packer.packString(value)
}
override fun visitBoolean(value: Boolean) {
packer.packBoolean(value)
}
override fun visitInt(value: Long) {
packer.packLong(value)
}
override fun visitFloat(value: Double) {
packer.packDouble(value)
}
override fun visitDuration(value: VmDuration) {
packer.packArrayHeader(3)
packer.packInt(CODE_DURATION.toInt())
packer.packDouble(value.value)
packer.packString(value.unit.toString())
}
override fun visitDataSize(value: VmDataSize) {
packer.packArrayHeader(3)
packer.packInt(CODE_DATASIZE.toInt())
packer.packDouble(value.value)
packer.packString(value.unit.toString())
}
override fun visitIntSeq(value: VmIntSeq) {
packer.packArrayHeader(4)
packer.packInt(CODE_INTSEQ.toInt())
packer.packLong(value.start)
packer.packLong(value.end)
packer.packLong(value.step)
}
private fun doVisitCollection(length: Int, value: Iterable<Any>) {
packer.packArrayHeader(length)
for (elem in value) {
visit(elem)
}
}
override fun visitList(value: VmList) {
packer.packArrayHeader(2)
packer.packInt(CODE_LIST.toInt())
doVisitCollection(value.length, value)
}
override fun visitSet(value: VmSet) {
packer.packArrayHeader(2)
packer.packInt(CODE_SET.toInt())
doVisitCollection(value.length, value)
}
override fun visitMap(value: VmMap) {
packer.packArrayHeader(2)
packer.packInt(CODE_MAP.toInt())
packer.packMapHeader(value.length)
for ((k, v) in value) {
visit(k)
visit(v)
}
}
override fun visitTyped(value: VmTyped) {
packObjectPreamble(value)
packer.packArrayHeader(value.vmClass.allRegularPropertyNames.size())
value.iterateAlreadyForcedMemberValues(this::doVisitObjectMember)
}
private fun doVisitObjectMember(key: Any, member: ObjectMember, value: Any): Boolean {
if (member.isClass || member.isTypeAlias) return true
packer.packArrayHeader(3)
when {
member.isProp -> {
packer.packInt(CODE_PROPERTY.toInt())
packer.packString(key.toString())
}
member.isEntry -> {
packer.packInt(CODE_ENTRY.toInt())
visit(key)
}
else -> {
packer.packInt(CODE_ELEMENT.toInt())
packer.packLong(key as Long)
}
}
visit(value)
return true
}
override fun visitDynamic(value: VmDynamic) {
packObjectPreamble(value)
packer.packArrayHeader(value.regularMemberCount)
value.iterateAlreadyForcedMemberValues(this::doVisitObjectMember)
}
private fun packObjectPreamble(value: VmObjectLike) {
packer.packArrayHeader(4)
packer.packInt(CODE_OBJECT.toInt())
packer.packString(value.vmClass.displayName)
packer.packString(value.vmClass.module.moduleInfo.moduleKey.uri.toString())
}
override fun visitListing(value: VmListing) {
packer.packArrayHeader(2)
packer.packInt(CODE_LISTING.toInt())
packer.packArrayHeader(value.length)
value.iterateAlreadyForcedMemberValues { _, _, memberValue ->
visit(memberValue)
true
}
}
override fun visitMapping(value: VmMapping) {
packer.packArrayHeader(2)
packer.packInt(CODE_MAPPING.toInt())
packer.packMapHeader(value.entryCount)
value.iterateAlreadyForcedMemberValues { key, _, memberValue ->
visit(key)
visit(memberValue)
true
}
}
override fun visitClass(value: VmClass) {
packer.packArrayHeader(1)
packer.packInt(CODE_CLASS.toInt())
}
override fun visitTypeAlias(value: VmTypeAlias) {
packer.packArrayHeader(1)
packer.packInt(CODE_TYPEALIAS.toInt())
}
override fun visitPair(value: VmPair) {
packer.packArrayHeader(3)
packer.packInt(CODE_PAIR.toInt())
visit(value.first)
visit(value.second)
}
override fun visitRegex(value: VmRegex) {
packer.packArrayHeader(2)
packer.packInt(CODE_REGEX.toInt())
packer.packString(value.pattern.pattern())
}
override fun visitNull(value: VmNull) {
packer.packNil()
}
override fun visitFunction(value: VmFunction) {
packer.packArrayHeader(1)
packer.packInt(CODE_FUNCTION.toInt())
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.server
import org.pkl.core.Logger
import org.pkl.core.StackFrame
internal class ClientLogger(
private val evaluatorId: Long,
private val transport: MessageTransport
) : Logger {
override fun trace(message: String, frame: StackFrame) {
transport.send(LogMessage(evaluatorId, level = 0, message, frame.moduleUri))
}
override fun warn(message: String, frame: StackFrame) {
transport.send(LogMessage(evaluatorId, level = 1, message, frame.moduleUri))
}
}

View File

@@ -0,0 +1,152 @@
/**
* 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.server
import java.io.IOException
import java.net.URI
import java.util.Optional
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Future
import kotlin.random.Random
import org.pkl.core.SecurityManager
import org.pkl.core.module.ModuleKey
import org.pkl.core.module.ModuleKeyFactory
import org.pkl.core.module.PathElement
import org.pkl.core.module.ResolvedModuleKey
import org.pkl.core.module.ResolvedModuleKeys
internal class ClientModuleKeyFactory(
private val readerSpecs: Collection<ModuleReaderSpec>,
transport: MessageTransport,
evaluatorId: Long
) : ModuleKeyFactory {
companion object {
private class ClientModuleKeyResolver(
private val transport: MessageTransport,
private val evaluatorId: Long,
) {
private val readResponses: MutableMap<URI, Future<String>> = ConcurrentHashMap()
private val listResponses: MutableMap<URI, Future<List<PathElement>>> = ConcurrentHashMap()
fun listElements(securityManager: SecurityManager, uri: URI): List<PathElement> {
securityManager.checkResolveModule(uri)
return doListElements(uri)
}
fun hasElement(securityManager: SecurityManager, uri: URI): Boolean {
securityManager.checkResolveModule(uri)
return try {
doReadModule(uri)
true
} catch (e: IOException) {
false
}
}
fun resolveModule(securityManager: SecurityManager, uri: URI): String {
securityManager.checkResolveModule(uri)
return doReadModule(uri)
}
private fun doReadModule(uri: URI): String =
readResponses
.computeIfAbsent(uri) {
CompletableFuture<String>().apply {
val request = ReadModuleRequest(Random.nextLong(), evaluatorId, uri)
transport.send(request) { response ->
when (response) {
is ReadModuleResponse -> {
if (response.error != null) {
completeExceptionally(IOException(response.error))
} else {
complete(response.contents!!)
}
}
else -> {
completeExceptionally(ProtocolException("unexpected response"))
}
}
}
}
}
.getUnderlying()
private fun doListElements(uri: URI): List<PathElement> =
listResponses
.computeIfAbsent(uri) {
CompletableFuture<List<PathElement>>().apply {
val request = ListModulesRequest(Random.nextLong(), evaluatorId, uri)
transport.send(request) { response ->
when (response) {
is ListModulesResponse -> {
if (response.error != null) {
completeExceptionally(IOException(response.error))
} else {
complete(response.pathElements!!)
}
}
else -> completeExceptionally(ProtocolException("unexpected response"))
}
}
}
}
.getUnderlying()
}
/** [ModuleKey] that delegates module reads to the client. */
private class ClientModuleKey(
private val uri: URI,
private val spec: ModuleReaderSpec,
private val resolver: ClientModuleKeyResolver,
) : ModuleKey {
override fun isLocal(): Boolean = spec.isLocal
override fun hasHierarchicalUris(): Boolean = spec.hasHierarchicalUris
override fun isGlobbable(): Boolean = spec.isGlobbable
override fun getUri(): URI = uri
override fun listElements(securityManager: SecurityManager, baseUri: URI): List<PathElement> =
resolver.listElements(securityManager, baseUri)
override fun resolve(securityManager: SecurityManager): ResolvedModuleKey {
val contents = resolver.resolveModule(securityManager, uri)
return ResolvedModuleKeys.virtual(this, uri, contents, true)
}
override fun hasElement(securityManager: SecurityManager, uri: URI): Boolean {
return resolver.hasElement(securityManager, uri)
}
}
}
private val schemes = readerSpecs.map { it.scheme }
private val resolver: ClientModuleKeyResolver = ClientModuleKeyResolver(transport, evaluatorId)
override fun create(uri: URI): Optional<ModuleKey> =
when (uri.scheme) {
in schemes -> {
val readerSpec = readerSpecs.find { it.scheme == uri.scheme }!!
val moduleKey = ClientModuleKey(uri, readerSpec, resolver)
Optional.of(moduleKey)
}
else -> Optional.empty()
}
}

View File

@@ -0,0 +1,105 @@
/**
* 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.server
import java.io.IOException
import java.net.URI
import java.util.Optional
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Future
import kotlin.random.Random
import org.pkl.core.SecurityManager
import org.pkl.core.module.PathElement
import org.pkl.core.resource.Resource
import org.pkl.core.resource.ResourceReader
/** Resource reader that delegates read logic to the client. */
internal class ClientResourceReader(
private val transport: MessageTransport,
private val evaluatorId: Long,
private val readerSpec: ResourceReaderSpec,
) : ResourceReader {
private val readResponses: MutableMap<URI, Future<ByteArray>> = ConcurrentHashMap()
private val listResources: MutableMap<URI, Future<List<PathElement>>> = ConcurrentHashMap()
override fun hasHierarchicalUris(): Boolean = readerSpec.hasHierarchicalUris
override fun isGlobbable(): Boolean = readerSpec.isGlobbable
override fun getUriScheme() = readerSpec.scheme
override fun read(uri: URI): Optional<Any> = Optional.of(Resource(uri, doRead(uri)))
override fun hasElement(securityManager: SecurityManager, elementUri: URI): Boolean {
securityManager.checkResolveResource(elementUri)
return try {
doRead(elementUri)
true
} catch (e: IOException) {
false
}
}
override fun listElements(securityManager: SecurityManager, baseUri: URI): List<PathElement> {
securityManager.checkResolveResource(baseUri)
return doListElements(baseUri)
}
private fun doListElements(baseUri: URI): List<PathElement> =
listResources
.computeIfAbsent(baseUri) {
CompletableFuture<List<PathElement>>().apply {
val request = ListResourcesRequest(Random.nextLong(), evaluatorId, baseUri)
transport.send(request) { response ->
when (response) {
is ListResourcesResponse ->
if (response.pathElements != null) {
complete(response.pathElements)
} else {
completeExceptionally(IOException(response.error))
}
else -> completeExceptionally(ProtocolException("Unexpected response"))
}
}
}
}
.getUnderlying()
private fun doRead(uri: URI): ByteArray =
readResponses
.computeIfAbsent(uri) {
CompletableFuture<ByteArray>().apply {
val request = ReadResourceRequest(Random.nextLong(), evaluatorId, uri)
transport.send(request) { response ->
when (response) {
is ReadResourceResponse -> {
if (response.error != null) {
completeExceptionally(IOException(response.error))
} else {
complete(response.contents!!)
}
}
else -> {
completeExceptionally(ProtocolException("Unexpected response: $response"))
}
}
}
}
}
.getUnderlying()
}

View File

@@ -0,0 +1,320 @@
/**
* 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.server
import java.net.URI
import java.nio.file.Path
import java.time.Duration
import java.util.*
import java.util.regex.Pattern
import org.pkl.core.module.PathElement
import org.pkl.core.packages.Checksums
sealed interface Message {
val type: MessageType
}
sealed interface OneWayMessage : Message
sealed interface RequestMessage : Message {
val requestId: Long
}
sealed interface ResponseMessage : Message {
val requestId: Long
}
sealed class ClientMessage : Message
sealed class ClientRequestMessage : ClientMessage(), RequestMessage
sealed class ClientResponseMessage : ClientMessage(), ResponseMessage
sealed class ClientOneWayMessage : ClientMessage(), OneWayMessage
sealed class ServerMessage : Message
sealed class ServerRequestMessage : ServerMessage(), RequestMessage
sealed class ServerResponseMessage : ServerMessage(), ResponseMessage
sealed class ServerOneWayMessage : ServerMessage(), OneWayMessage
enum class MessageType(val code: Int) {
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),
}
data class ModuleReaderSpec(
val scheme: String,
val hasHierarchicalUris: Boolean,
val isLocal: Boolean,
val isGlobbable: Boolean
)
data class ResourceReaderSpec(
val scheme: String,
val hasHierarchicalUris: Boolean,
val isGlobbable: Boolean,
)
private fun <T> T?.equalsNullable(other: Any?): Boolean {
return Objects.equals(this, other)
}
enum class DependencyType(val value: String) {
LOCAL("local"),
REMOTE("remote")
}
sealed interface Dependency {
val type: DependencyType
val packageUri: URI?
}
data class RemoteDependency(override val packageUri: URI, val checksums: Checksums?) : Dependency {
override val type: DependencyType = DependencyType.REMOTE
}
data class Project(
val projectFileUri: URI,
override val packageUri: URI?,
val dependencies: Map<String, Dependency>
) : Dependency {
override val type: DependencyType = DependencyType.LOCAL
}
data class CreateEvaluatorRequest(
override val requestId: Long,
val allowedModules: List<Pattern>?,
val allowedResources: List<Pattern>?,
val clientModuleReaders: List<ModuleReaderSpec>?,
val clientResourceReaders: List<ResourceReaderSpec>?,
val modulePaths: List<Path>?,
val env: Map<String, String>?,
val properties: Map<String, String>?,
val timeout: Duration?,
val rootDir: Path?,
val cacheDir: Path?,
val outputFormat: String?,
val project: Project?,
) : ClientRequestMessage() {
override val type = MessageType.CREATE_EVALUATOR_REQUEST
// need to implement this manually because [Pattern.equals] returns false for two patterns
// that have the same underlying pattern string.
override fun equals(other: Any?): Boolean {
if (other == null) return false
if (other !is CreateEvaluatorRequest) return false
return requestId == other.requestId &&
Objects.equals(
allowedModules?.map { it.pattern() },
other.allowedModules?.map { it.pattern() }
) &&
Objects.equals(
allowedResources?.map { it.pattern() },
other.allowedResources?.map { it.pattern() }
) &&
clientModuleReaders.equalsNullable(other.clientModuleReaders) &&
clientResourceReaders.equalsNullable(other.clientResourceReaders) &&
modulePaths.equalsNullable(other.modulePaths) &&
env.equalsNullable(other.env) &&
properties.equalsNullable(other.properties) &&
timeout.equalsNullable(other.timeout) &&
rootDir.equalsNullable(other.rootDir) &&
cacheDir.equalsNullable(other.cacheDir) &&
outputFormat.equalsNullable(other.outputFormat) &&
project.equalsNullable(other.project)
}
@Suppress("DuplicatedCode") // false duplicate within method
override fun hashCode(): Int {
var result = requestId.hashCode()
result = 31 * result + allowedModules?.map { it.pattern() }.hashCode()
result = 31 * result + allowedResources?.map { it.pattern() }.hashCode()
result = 31 * result + clientModuleReaders.hashCode()
result = 31 * result + clientResourceReaders.hashCode()
result = 31 * result + modulePaths.hashCode()
result = 31 * result + env.hashCode()
result = 31 * result + properties.hashCode()
result = 31 * result + timeout.hashCode()
result = 31 * result + rootDir.hashCode()
result = 31 * result + cacheDir.hashCode()
result = 31 * result + outputFormat.hashCode()
result = 31 * result + project.hashCode()
result = 31 * result + type.hashCode()
return result
}
}
data class CreateEvaluatorResponse(
override val requestId: Long,
val evaluatorId: Long?,
val error: String?,
) : ServerResponseMessage() {
override val type
get() = MessageType.CREATE_EVALUATOR_RESPONSE
}
data class ListResourcesRequest(override val requestId: Long, val evaluatorId: Long, val uri: URI) :
ServerRequestMessage() {
override val type: MessageType
get() = MessageType.LIST_RESOURCES_REQUEST
}
data class ListResourcesResponse(
override val requestId: Long,
val evaluatorId: Long,
val pathElements: List<PathElement>?,
val error: String?
) : ClientResponseMessage() {
override val type: MessageType
get() = MessageType.LIST_RESOURCES_RESPONSE
}
data class ListModulesRequest(override val requestId: Long, val evaluatorId: Long, val uri: URI) :
ServerRequestMessage() {
override val type: MessageType
get() = MessageType.LIST_MODULES_REQUEST
}
data class ListModulesResponse(
override val requestId: Long,
val evaluatorId: Long,
val pathElements: List<PathElement>?,
val error: String?
) : ClientResponseMessage() {
override val type: MessageType
get() = MessageType.LIST_MODULES_RESPONSE
}
data class CloseEvaluator(val evaluatorId: Long) : ClientOneWayMessage() {
override val type = MessageType.CLOSE_EVALUATOR
}
data class EvaluateRequest(
override val requestId: Long,
val evaluatorId: Long,
val moduleUri: URI,
val moduleText: String?,
val expr: String?
) : ClientRequestMessage() {
override val type = MessageType.EVALUATE_REQUEST
}
data class EvaluateResponse(
override val requestId: Long,
val evaluatorId: Long,
val result: ByteArray?,
val error: String?
) : ServerResponseMessage() {
override val type
get() = MessageType.EVALUATE_RESPONSE
// override to use [ByteArray.contentEquals]
@Suppress("DuplicatedCode")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EvaluateResponse) return false
return requestId == other.requestId &&
evaluatorId == other.evaluatorId &&
result.contentEquals(other.result) &&
error == other.error
}
// override to use [ByteArray.contentHashCode]
override fun hashCode(): Int {
var result1 = requestId.hashCode()
result1 = 31 * result1 + evaluatorId.hashCode()
result1 = 31 * result1 + result.contentHashCode()
result1 = 31 * result1 + error.hashCode()
return result1
}
}
data class LogMessage(
val evaluatorId: Long,
val level: Int,
val message: String,
val frameUri: String
) : ServerOneWayMessage() {
override val type
get() = MessageType.LOG_MESSAGE
}
data class ReadResourceRequest(override val requestId: Long, val evaluatorId: Long, val uri: URI) :
ServerRequestMessage() {
override val type
get() = MessageType.READ_RESOURCE_REQUEST
}
data class ReadResourceResponse(
override val requestId: Long,
val evaluatorId: Long,
val contents: ByteArray?,
val error: String?
) : ClientResponseMessage() {
override val type = MessageType.READ_RESOURCE_RESPONSE
// override to use [ByteArray.contentEquals]
@Suppress("DuplicatedCode")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ReadResourceResponse) return false
return requestId == other.requestId &&
evaluatorId == other.evaluatorId &&
contents.contentEquals(other.contents) &&
error == other.error
}
// override to use [ByteArray.contentHashCode]
override fun hashCode(): Int {
var result = requestId.hashCode()
result = 31 * result + evaluatorId.hashCode()
result = 31 * result + contents.contentHashCode()
result = 31 * result + error.hashCode()
return result
}
}
data class ReadModuleRequest(override val requestId: Long, val evaluatorId: Long, val uri: URI) :
ServerRequestMessage() {
override val type
get() = MessageType.READ_MODULE_REQUEST
}
data class ReadModuleResponse(
override val requestId: Long,
val evaluatorId: Long,
val contents: String?,
val error: String?
) : ClientResponseMessage() {
override val type = MessageType.READ_MODULE_RESPONSE
}

View File

@@ -0,0 +1,21 @@
/**
* 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.server
/** Decodes a stream of messages. */
internal interface MessageDecoder {
fun decode(): Message?
}

View File

@@ -0,0 +1,31 @@
/**
* 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.server
import java.io.InputStream
import org.msgpack.core.MessagePack
import org.msgpack.core.MessageUnpacker
/** Factory methods for creating [MessageDecoder]s. */
internal object MessageDecoders {
fun from(stream: InputStream): MessageDecoder =
MessagePackDecoder(MessagePack.newDefaultUnpacker(stream))
fun from(unpacker: MessageUnpacker): MessageDecoder = MessagePackDecoder(unpacker)
fun from(array: ByteArray): MessageDecoder =
MessagePackDecoder(MessagePack.newDefaultUnpacker(array))
}

View File

@@ -0,0 +1,21 @@
/**
* 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.server
/** Encodes a stream of messages. */
internal interface MessageEncoder {
fun encode(msg: Message)
}

View 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.server
import java.io.OutputStream
import org.msgpack.core.MessagePack
import org.msgpack.core.MessagePacker
/** Factory methods for creating [MessageEncoder]s. */
internal object MessageEncoders {
fun into(stream: OutputStream): MessageEncoder =
MessagePackEncoder(MessagePack.newDefaultPacker(stream))
fun into(packer: MessagePacker): MessageEncoder = MessagePackEncoder(packer)
}

View File

@@ -0,0 +1,276 @@
/**
* 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.server
import java.net.URI
import java.nio.file.Path
import java.time.Duration
import java.util.regex.Pattern
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.module.PathElement
import org.pkl.core.packages.Checksums
internal class MessagePackDecoder(private val unpacker: MessageUnpacker) : MessageDecoder {
override fun decode(): Message? {
if (!unpacker.hasNext()) return null
val code =
try {
val arraySize = unpacker.unpackArrayHeader()
if (arraySize != 2) {
throw DecodeException("Malformed message header (expected size 2, but got $arraySize).")
}
unpacker.unpackInt()
} catch (e: MessageTypeException) {
throw DecodeException("Malformed message header.", e)
}
return try {
val map = unpacker.unpackValue().asMapValue().map()
when (code) {
MessageType.CREATE_EVALUATOR_REQUEST.code -> {
CreateEvaluatorRequest(
requestId = map.get("requestId").asIntegerValue().asLong(),
allowedModules = map.unpackStringListOrNull("allowedModules")?.map(Pattern::compile),
allowedResources =
map.unpackStringListOrNull("allowedResources")?.map(Pattern::compile),
clientModuleReaders = map.unpackModuleReaderSpec("clientModuleReaders"),
clientResourceReaders = map.unpackResourceReaderSpec("clientResourceReaders"),
modulePaths = map.unpackStringListOrNull("modulePaths")?.map(Path::of),
env = map.unpackStringMapOrNull("env"),
properties = map.unpackStringMapOrNull("properties"),
timeout = map.unpackLongOrNull("timeoutSeconds")?.let(Duration::ofSeconds),
rootDir = map.unpackStringOrNull("rootDir")?.let(Path::of),
cacheDir = map.unpackStringOrNull("cacheDir")?.let(Path::of),
outputFormat = map.unpackStringOrNull("outputFormat"),
project = map.unpackProject("project")
)
}
MessageType.CREATE_EVALUATOR_RESPONSE.code -> {
CreateEvaluatorResponse(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLongOrNull("evaluatorId"),
error = map.unpackStringOrNull("error")
)
}
MessageType.CLOSE_EVALUATOR.code -> {
CloseEvaluator(evaluatorId = map.unpackLong("evaluatorId"))
}
MessageType.EVALUATE_REQUEST.code -> {
EvaluateRequest(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
moduleUri = map.unpackString("moduleUri").let(::URI),
moduleText = map.unpackStringOrNull("moduleText"),
expr = map.unpackStringOrNull("expr")
)
}
MessageType.EVALUATE_RESPONSE.code -> {
EvaluateResponse(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
result = map.unpackByteArrayOrNull("result"),
error = map.unpackStringOrNull("error")
)
}
MessageType.LOG_MESSAGE.code -> {
LogMessage(
evaluatorId = map.unpackLong("evaluatorId"),
level = map.unpackIntValue("level"),
message = map.unpackString("message"),
frameUri = map.unpackString("frameUri")
)
}
MessageType.READ_RESOURCE_REQUEST.code -> {
ReadResourceRequest(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
uri = map.unpackString("uri").let(::URI)
)
}
MessageType.READ_RESOURCE_RESPONSE.code -> {
ReadResourceResponse(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
contents = map.unpackByteArrayOrNull("contents"),
error = map.unpackStringOrNull("error")
)
}
MessageType.READ_MODULE_REQUEST.code -> {
ReadModuleRequest(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
uri = map.unpackString("uri").let(::URI)
)
}
MessageType.READ_MODULE_RESPONSE.code -> {
ReadModuleResponse(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
contents = map.unpackStringOrNull("contents"),
error = map.unpackStringOrNull("error")
)
}
MessageType.LIST_MODULES_REQUEST.code -> {
ListModulesRequest(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
uri = map.unpackString("uri").let(::URI)
)
}
MessageType.LIST_MODULES_RESPONSE.code -> {
ListModulesResponse(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
pathElements = map.unpackPathElements("pathElements"),
error = map.unpackStringOrNull("error")
)
}
MessageType.LIST_RESOURCES_REQUEST.code -> {
ListResourcesRequest(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
uri = map.unpackString("uri").let(::URI)
)
}
MessageType.LIST_RESOURCES_RESPONSE.code -> {
ListResourcesResponse(
requestId = map.unpackLong("requestId"),
evaluatorId = map.unpackLong("evaluatorId"),
pathElements = map.unpackPathElements("pathElements"),
error = map.unpackStringOrNull("error")
)
}
else -> throw ProtocolException("Invalid message code: $code")
}
} catch (e: MessageTypeException) {
throw DecodeException("Malformed message body for message with code `$code`.", e)
}
}
private fun Array<Value>.unpackValueOrNull(key: String): Value? {
for (i in indices.step(2)) {
val currKey = this[i].asStringValue().asString()
if (currKey == key) return this[i + 1]
}
return null
}
private fun Map<Value, Value>.getNullable(key: String): Value? =
this[ImmutableStringValueImpl(key)]
private fun Map<Value, Value>.get(key: String): Value =
getNullable(key) ?: throw DecodeException("Missing message parameter `$key`")
private fun Array<Value>.unpackValue(key: String): Value =
unpackValueOrNull(key) ?: throw DecodeException("Missing message parameter `$key`.")
private fun Map<Value, Value>.unpackStringListOrNull(key: String): List<String>? {
val value = getNullable(key) ?: return null
return value.asArrayValue().map { it.asStringValue().asString() }
}
private fun Map<Value, Value>.unpackStringMapOrNull(key: String): Map<String, String>? {
val value = getNullable(key) ?: return null
return value.asMapValue().entrySet().associate { (k, v) ->
k.asStringValue().asString() to v.asStringValue().asString()
}
}
private fun Map<Value, Value>.unpackLong(key: String): Long = get(key).asIntegerValue().asLong()
private fun Map<Value, Value>.unpackBoolean(key: String): Boolean =
get(key).asBooleanValue().boolean
private fun Map<Value, Value>.unpackBooleanOrNull(key: String): Boolean? =
getNullable(key)?.asBooleanValue()?.boolean
private fun Map<Value, Value>.unpackLongOrNull(key: String): Long? =
getNullable(key)?.asIntegerValue()?.asLong()
private fun Map<Value, Value>.unpackIntValue(key: String): Int = get(key).asIntegerValue().asInt()
private fun Map<Value, Value>.unpackString(key: String): String =
get(key).asStringValue().asString()
private fun Map<Value, Value>.unpackStringOrNull(key: String): String? =
getNullable(key)?.asStringValue()?.asString()
private fun Map<Value, Value>.unpackByteArrayOrNull(key: String): ByteArray? =
getNullable(key)?.asBinaryValue()?.asByteArray()
private fun Map<Value, Value>.unpackPathElements(key: String): List<PathElement>? =
getNullable(key)?.asArrayValue()?.map { pathElement ->
val map = pathElement.asMapValue().map()
PathElement(map.unpackString("name"), map.unpackBoolean("isDirectory"))
}
private fun Map<Value, Value>.unpackModuleReaderSpec(name: String): List<ModuleReaderSpec>? {
val keys = getNullable(name) ?: return null
return keys.asArrayValue().toList().map { value ->
val readerMap = value.asMapValue().map()
ModuleReaderSpec(
scheme = readerMap.unpackString("scheme"),
hasHierarchicalUris = readerMap.unpackBoolean("hasHierarchicalUris"),
isLocal = readerMap.unpackBoolean("isLocal"),
isGlobbable = readerMap.unpackBoolean("isGlobbable")
)
}
}
private fun Map<Value, Value>.unpackResourceReaderSpec(name: String): List<ResourceReaderSpec> {
val keys = getNullable(name) ?: return emptyList()
return keys.asArrayValue().toList().map { value ->
val readerMap = value.asMapValue().map()
ResourceReaderSpec(
scheme = readerMap.unpackString("scheme"),
hasHierarchicalUris = readerMap.unpackBoolean("hasHierarchicalUris"),
isGlobbable = readerMap.unpackBoolean("isGlobbable")
)
}
}
private fun Map<Value, Value>.unpackProject(name: String): Project? {
val projMap = getNullable(name)?.asMapValue()?.map() ?: return null
val projectFileUri = URI(projMap.unpackString("projectFileUri"))
val dependencies = projMap.unpackDependencies("dependencies")
return Project(projectFileUri, null, dependencies)
}
private fun Map<Value, Value>.unpackDependencies(name: String): Map<String, Dependency> {
val mapValue = get(name).asMapValue().map()
return mapValue.entries.associate { (key, value) ->
val dependencyName = key.asStringValue().asString()
val dependencyObj = value.asMapValue().map()
val type = dependencyObj.unpackString("type")
val packageUri = URI(dependencyObj.unpackString("packageUri"))
if (type == DependencyType.REMOTE.value) {
val checksums =
dependencyObj.getNullable("checksums")?.asMapValue()?.map()?.let { obj ->
val sha256 = obj.unpackString("sha256")
Checksums(sha256)
}
return@associate dependencyName to RemoteDependency(packageUri, checksums)
}
val dependencies = dependencyObj.unpackDependencies("dependencies")
val projectFileUri = dependencyObj.unpackString("projectFileUri")
dependencyName to Project(URI(projectFileUri), packageUri, dependencies)
}
}
}

View File

@@ -0,0 +1,303 @@
/**
* 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.server
import kotlin.io.path.pathString
import org.msgpack.core.MessagePacker
import org.pkl.core.module.PathElement
import org.pkl.core.packages.Checksums
internal class MessagePackEncoder(private val packer: MessagePacker) : MessageEncoder {
private fun MessagePacker.packModuleReaderSpec(reader: ModuleReaderSpec) {
packMapHeader(4)
packKeyValue("scheme", reader.scheme)
packKeyValue("hasHierarchicalUris", reader.hasHierarchicalUris)
packKeyValue("isLocal", reader.isLocal)
packKeyValue("isGlobbable", reader.isGlobbable)
}
private fun MessagePacker.packResourceReaderSpec(reader: ResourceReaderSpec) {
packMapHeader(3)
packKeyValue("scheme", reader.scheme)
packKeyValue("hasHierarchicalUris", reader.hasHierarchicalUris)
packKeyValue("isGlobbable", reader.isGlobbable)
}
private fun MessagePacker.packPathElement(pathElement: PathElement) {
packMapHeader(2)
packKeyValue("name", pathElement.name)
packKeyValue("isDirectory", pathElement.isDirectory)
}
private fun MessagePacker.packProject(project: Project) {
packMapHeader(2)
packKeyValue("projectFileUri", project.projectFileUri.toString())
packString("dependencies")
packDependencies(project.dependencies)
}
private fun MessagePacker.packDependencies(dependencies: Map<String, Dependency>) {
packMapHeader(dependencies.size)
for ((name, dep) in dependencies) {
packString(name)
if (dep is Project) {
packMapHeader(4)
packKeyValue("type", dep.type.value)
packKeyValue("packageUri", dep.packageUri.toString())
packKeyValue("projectFileUri", dep.projectFileUri.toString())
packString("dependencies")
packDependencies(dep.dependencies)
} else {
dep as RemoteDependency
packMapHeader(dep.checksums?.let { 3 } ?: 2)
packKeyValue("type", dep.type.value)
packKeyValue("packageUri", dep.packageUri.toString())
dep.checksums?.let { checksums ->
packString("checksums")
packChecksums(checksums)
}
}
}
}
private fun MessagePacker.packChecksums(checksums: Checksums) {
packMapHeader(1)
packKeyValue("sha256", checksums.sha256)
}
override fun encode(msg: Message) =
with(packer) {
packArrayHeader(2)
packInt(msg.type.code)
@Suppress("DuplicatedCode")
when (msg.type.code) {
MessageType.CREATE_EVALUATOR_REQUEST.code -> {
msg as CreateEvaluatorRequest
packMapHeader(8, msg.timeout, msg.rootDir, msg.cacheDir, msg.outputFormat, msg.project)
packKeyValue("requestId", msg.requestId)
packKeyValue("allowedModules", msg.allowedModules?.map { it.toString() })
packKeyValue("allowedResources", msg.allowedResources?.map { it.toString() })
if (msg.clientModuleReaders != null) {
packString("clientModuleReaders")
packArrayHeader(msg.clientModuleReaders.size)
for (moduleReader in msg.clientModuleReaders) {
packModuleReaderSpec(moduleReader)
}
}
if (msg.clientResourceReaders != null) {
packString("clientResourceReaders")
packArrayHeader(msg.clientResourceReaders.size)
for (resourceReader in msg.clientResourceReaders) {
packResourceReaderSpec(resourceReader)
}
}
packKeyValue("modulePaths", msg.modulePaths?.map { it.pathString })
packKeyValue("env", msg.env)
packKeyValue("properties", msg.properties)
packKeyValue("timeoutSeconds", msg.timeout?.toSeconds())
packKeyValue("rootDir", msg.rootDir?.pathString)
packKeyValue("cacheDir", msg.cacheDir?.pathString)
packKeyValue("outputFormat", msg.outputFormat)
if (msg.project != null) {
packString("project")
packProject(msg.project)
}
}
MessageType.CREATE_EVALUATOR_RESPONSE.code -> {
msg as CreateEvaluatorResponse
packMapHeader(1, msg.evaluatorId, msg.error)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("error", msg.error)
}
MessageType.CLOSE_EVALUATOR.code -> {
msg as CloseEvaluator
packMapHeader(1)
packKeyValue("evaluatorId", msg.evaluatorId)
}
MessageType.EVALUATE_REQUEST.code -> {
msg as EvaluateRequest
packMapHeader(3, msg.moduleText, msg.expr)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("moduleUri", msg.moduleUri.toString())
packKeyValue("moduleText", msg.moduleText)
packKeyValue("expr", msg.expr)
}
MessageType.EVALUATE_RESPONSE.code -> {
msg as EvaluateResponse
packMapHeader(2, msg.result, msg.error)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("result", msg.result)
packKeyValue("error", msg.error)
}
MessageType.LOG_MESSAGE.code -> {
msg as LogMessage
packMapHeader(4)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("level", msg.level)
packKeyValue("message", msg.message)
packKeyValue("frameUri", msg.frameUri)
}
MessageType.READ_RESOURCE_REQUEST.code -> {
msg as ReadResourceRequest
packMapHeader(3)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("uri", msg.uri.toString())
}
MessageType.READ_RESOURCE_RESPONSE.code -> {
msg as ReadResourceResponse
packMapHeader(2, msg.contents, msg.error)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("contents", msg.contents)
packKeyValue("error", msg.error)
}
MessageType.READ_MODULE_REQUEST.code -> {
msg as ReadModuleRequest
packMapHeader(3)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("uri", msg.uri.toString())
}
MessageType.READ_MODULE_RESPONSE.code -> {
msg as ReadModuleResponse
packMapHeader(2, msg.contents, msg.error)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("contents", msg.contents)
packKeyValue("error", msg.error)
}
MessageType.LIST_MODULES_REQUEST.code -> {
msg as ListModulesRequest
packMapHeader(3)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("uri", msg.uri.toString())
}
MessageType.LIST_MODULES_RESPONSE.code -> {
msg as ListModulesResponse
packMapHeader(2, msg.pathElements, msg.error)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
if (msg.pathElements != null) {
packString("pathElements")
packArrayHeader(msg.pathElements.size)
for (pathElement in msg.pathElements) {
packPathElement(pathElement)
}
}
packKeyValue("error", msg.error)
}
MessageType.LIST_RESOURCES_REQUEST.code -> {
msg as ListResourcesRequest
packMapHeader(3)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
packKeyValue("uri", msg.uri.toString())
}
MessageType.LIST_RESOURCES_RESPONSE.code -> {
msg as ListResourcesResponse
packMapHeader(2, msg.pathElements, msg.error)
packKeyValue("requestId", msg.requestId)
packKeyValue("evaluatorId", msg.evaluatorId)
if (msg.pathElements != null) {
packString("pathElements")
packArrayHeader(msg.pathElements.size)
for (pathElement in msg.pathElements) {
packPathElement(pathElement)
}
}
packKeyValue("error", msg.error)
}
else -> {
throw RuntimeException("Missing encoding for ${msg.javaClass.simpleName}")
}
}
flush()
}
private fun MessagePacker.packMapHeader(size: Int, value1: Any?, value2: Any?) =
packMapHeader(size + (if (value1 != null) 1 else 0) + (if (value2 != null) 1 else 0))
private fun MessagePacker.packMapHeader(
size: Int,
value1: Any?,
value2: Any?,
value3: Any?,
value4: Any?,
value5: Any?
) =
packMapHeader(
size +
(if (value1 != null) 1 else 0) +
(if (value2 != null) 1 else 0) +
(if (value3 != null) 1 else 0) +
(if (value4 != null) 1 else 0) +
(if (value5 != null) 1 else 0)
)
private fun MessagePacker.packKeyValue(name: String, value: Int?) {
if (value == null) return
packString(name)
packInt(value)
}
private fun MessagePacker.packKeyValue(name: String, value: Long?) {
if (value == null) return
packString(name)
packLong(value)
}
private fun MessagePacker.packKeyValue(name: String, value: String?) {
if (value == null) return
packString(name)
packString(value)
}
private fun MessagePacker.packKeyValue(name: String, value: Collection<String>?) {
if (value == null) return
packString(name)
packArrayHeader(value.size)
for (elem in value) packString(elem)
}
private fun MessagePacker.packKeyValue(name: String, value: Map<String, String>?) {
if (value == null) return
packString(name)
packMapHeader(value.size)
for ((k, v) in value) {
packString(k)
packString(v)
}
}
private fun MessagePacker.packKeyValue(name: String, value: ByteArray?) {
if (value == null) return
packString(name)
packBinaryHeader(value.size)
writePayload(value)
}
private fun MessagePacker.packKeyValue(name: String, value: Boolean) {
packString(name)
packBoolean(value)
}
}

View File

@@ -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.server
/** A bidirectional transport for sending and receiving messages. */
interface MessageTransport : AutoCloseable {
fun start(oneWayHandler: (OneWayMessage) -> Unit, requestHandler: (RequestMessage) -> Unit)
fun send(message: OneWayMessage)
fun send(message: RequestMessage, responseHandler: (ResponseMessage) -> Unit)
fun send(message: ResponseMessage)
}

View File

@@ -0,0 +1,135 @@
/**
* 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.server
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.ConcurrentHashMap
/** Factory methods for creating [MessageTransport]s. */
object MessageTransports {
/** Creates a message transport that reads from [inputStream] and writes to [outputStream]. */
fun stream(inputStream: InputStream, outputStream: OutputStream): MessageTransport {
return EncodingMessageTransport(
MessageDecoders.from(inputStream),
MessageEncoders.into(outputStream)
)
}
/** Creates "client" and "server" transports that are directly connected to each other. */
fun direct(): Pair<MessageTransport, MessageTransport> {
val transport1 = DirectMessageTransport()
val transport2 = DirectMessageTransport()
transport1.other = transport2
transport2.other = transport1
return transport1 to transport2
}
internal class EncodingMessageTransport(
private val decoder: MessageDecoder,
private val encoder: MessageEncoder,
) : AbstractMessageTransport() {
@Volatile private var isClosed: Boolean = false
override fun doStart() {
while (!isClosed) {
val message = decoder.decode() ?: return
accept(message)
}
}
override fun doClose() {
isClosed = true
}
override fun doSend(message: Message) {
encoder.encode(message)
}
}
internal class DirectMessageTransport : AbstractMessageTransport() {
lateinit var other: DirectMessageTransport
override fun doStart() {}
override fun doClose() {}
override fun doSend(message: Message) {
other.accept(message)
}
}
// TODO: clean up callbacks if evaluation fails for some reason (ThreadInterrupt, timeout, etc)
internal abstract class AbstractMessageTransport : MessageTransport {
private lateinit var oneWayHandler: (OneWayMessage) -> Unit
private lateinit var requestHandler: (RequestMessage) -> Unit
private val responseHandlers: MutableMap<Long, (ResponseMessage) -> Unit> = ConcurrentHashMap()
protected abstract fun doStart()
protected abstract fun doClose()
protected abstract fun doSend(message: Message)
protected fun accept(message: Message) {
log("Received message: $message")
when (message) {
is OneWayMessage -> oneWayHandler(message)
is RequestMessage -> requestHandler(message)
is ResponseMessage -> {
val handler =
responseHandlers.remove(message.requestId)
?: throw ProtocolException(
"Received response ${message.javaClass.simpleName} for unknown request ID `${message.requestId}`."
)
handler(message)
}
}
}
final override fun start(
oneWayHandler: (OneWayMessage) -> Unit,
requestHandler: (RequestMessage) -> Unit
) {
log("Starting transport: $this")
this.oneWayHandler = oneWayHandler
this.requestHandler = requestHandler
doStart()
}
final override fun close() {
log("Closing transport: $this")
doClose()
responseHandlers.clear()
}
override fun send(message: OneWayMessage) {
log("Sending message: $message")
doSend(message)
}
override fun send(message: RequestMessage, responseHandler: (ResponseMessage) -> Unit) {
log("Sending message: $message")
responseHandlers[message.requestId] = responseHandler
return doSend(message)
}
override fun send(message: ResponseMessage) {
log("Sending message: $message")
doSend(message)
}
}
}

View File

@@ -0,0 +1,225 @@
/**
* 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.server
import java.net.URI
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.random.Random
import org.pkl.core.*
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.module.ModuleKeyFactory
import org.pkl.core.module.ModulePathResolver
import org.pkl.core.packages.PackageUri
import org.pkl.core.project.DeclaredDependencies
import org.pkl.core.resource.ResourceReader
import org.pkl.core.resource.ResourceReaders
class Server(private val transport: MessageTransport) : AutoCloseable {
private val evaluators: MutableMap<Long, BinaryEvaluator> = ConcurrentHashMap()
// https://github.com/jano7/executor would be the perfect executor here
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
/** Starts listening to incoming messages */
fun start() {
transport.start(
{ message ->
when (message) {
is CloseEvaluator -> handleCloseEvaluator(message)
else -> throw ProtocolException("Unexpected incoming one-way message: $message")
}
},
{ message ->
when (message) {
is CreateEvaluatorRequest -> handleCreateEvaluator(message)
is EvaluateRequest -> handleEvaluate(message)
else -> throw ProtocolException("Unexpected incoming request message: $message")
}
}
)
}
/**
* Stops listening to incoming messages, cancels pending evaluation requests, and releases
* resources held by this server.
*/
override fun close() {
transport.closeQuietly()
for ((_, evaluator) in evaluators) {
// if currently in use, blocks until cancellation complete
evaluator.closeQuietly()
}
executor.shutdown()
}
private fun handleCreateEvaluator(message: CreateEvaluatorRequest) {
val evaluatorId = Random.Default.nextLong()
val baseResponse = CreateEvaluatorResponse(message.requestId, evaluatorId = null, error = null)
val evaluator =
try {
createEvaluator(message, evaluatorId)
} catch (e: ServerException) {
transport.send(baseResponse.copy(error = e.message))
return
}
evaluators[evaluatorId] = evaluator
transport.send(baseResponse.copy(evaluatorId = evaluatorId))
}
private fun handleEvaluate(msg: EvaluateRequest) {
val baseResponse = EvaluateResponse(msg.requestId, msg.evaluatorId, result = null, error = null)
val evaluator = evaluators[msg.evaluatorId]
if (evaluator == null) {
transport.send(
baseResponse.copy(error = "Evaluator with ID ${msg.evaluatorId} was not found.")
)
return
}
executor.execute {
try {
val resp = evaluator.evaluate(ModuleSource.create(msg.moduleUri, msg.moduleText), msg.expr)
transport.send(baseResponse.copy(result = resp))
} catch (e: PklBugException) {
transport.send(baseResponse.copy(error = e.toString()))
} catch (e: PklException) {
transport.send(baseResponse.copy(error = e.message))
}
}
}
private fun handleCloseEvaluator(message: CloseEvaluator) {
val evaluator = evaluators.remove(message.evaluatorId)
if (evaluator == null) {
log("Ignoring close request for unknown evaluator ID `${message.evaluatorId}`.")
return
}
evaluator.close()
}
private fun buildDeclaredDependencies(
projectFileUri: URI,
dependencies: Map<String, Dependency>,
myPackageUri: URI?
): DeclaredDependencies {
val remoteDependencies = buildMap {
for ((key, dep) in dependencies) {
if (dep is RemoteDependency) {
put(
key,
org.pkl.core.packages.Dependency.RemoteDependency(
PackageUri(dep.packageUri),
dep.checksums
)
)
}
}
}
val localDependencies = buildMap {
for ((key, dep) in dependencies) {
if (dep is Project) {
val localDep =
buildDeclaredDependencies(dep.projectFileUri, dep.dependencies, dep.packageUri)
put(key, localDep)
}
}
}
return DeclaredDependencies(
remoteDependencies,
localDependencies,
projectFileUri,
myPackageUri?.let(::PackageUri)
)
}
private fun createEvaluator(message: CreateEvaluatorRequest, evaluatorId: Long): BinaryEvaluator {
val modulePaths = message.modulePaths ?: emptyList()
val resolver = ModulePathResolver(modulePaths)
val allowedModules = message.allowedModules ?: emptyList()
val allowedResources = message.allowedResources ?: emptyList()
val rootDir = message.rootDir
val env = message.env ?: emptyMap()
val properties = message.properties ?: emptyMap()
val timeout = message.timeout
val cacheDir = message.cacheDir
val dependencies =
message.project?.let { proj ->
buildDeclaredDependencies(proj.projectFileUri, proj.dependencies, null)
}
log("Got dependencies: $dependencies")
return BinaryEvaluator(
StackFrameTransformers.defaultTransformer,
SecurityManagers.standard(
allowedModules,
allowedResources,
SecurityManagers.defaultTrustLevels,
rootDir
),
ClientLogger(evaluatorId, transport),
createModuleKeyFactories(message, evaluatorId, resolver),
createResourceReaders(message, evaluatorId, resolver),
env,
properties,
timeout,
cacheDir,
dependencies,
message.outputFormat
)
}
private fun createResourceReaders(
message: CreateEvaluatorRequest,
evaluatorId: Long,
modulePathResolver: ModulePathResolver
): List<ResourceReader> = buildList {
add(ResourceReaders.environmentVariable())
add(ResourceReaders.externalProperty())
add(ResourceReaders.file())
add(ResourceReaders.http())
add(ResourceReaders.https())
add(ResourceReaders.pkg())
add(ResourceReaders.projectpackage())
add(ResourceReaders.modulePath(modulePathResolver))
// add client-side resource readers last to ensure they win over builtin ones
for (readerSpec in message.clientResourceReaders ?: emptyList()) {
val resourceReader = ClientResourceReader(transport, evaluatorId, readerSpec)
add(resourceReader)
}
}
private fun createModuleKeyFactories(
message: CreateEvaluatorRequest,
evaluatorId: Long,
modulePathResolver: ModulePathResolver
): List<ModuleKeyFactory> = buildList {
// add client-side module key factory first to ensure it wins over builtin ones
if (message.clientModuleReaders?.isNotEmpty() == true) {
add(ClientModuleKeyFactory(message.clientModuleReaders, transport, evaluatorId))
}
add(ModuleKeyFactories.standardLibrary)
addAll(ModuleKeyFactories.fromServiceProviders())
add(ModuleKeyFactories.file)
add(ModuleKeyFactories.modulePath(modulePathResolver))
add(ModuleKeyFactories.pkg)
add(ModuleKeyFactories.projectpackage)
add(ModuleKeyFactories.genericUrl)
}
}

View File

@@ -0,0 +1,24 @@
/**
* 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.server
sealed class ServerException(msg: String, cause: Throwable?) : Exception(msg, cause)
open class ProtocolException(msg: String, cause: Throwable? = null) : ServerException(msg, cause)
class InvalidCommandException(msg: String, cause: Throwable? = null) : ServerException(msg, cause)
class DecodeException(msg: String, cause: Throwable? = null) : ProtocolException(msg, cause)

View File

@@ -0,0 +1,67 @@
/**
* 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.server
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import org.msgpack.core.MessageBufferPacker
import org.msgpack.core.MessagePack
internal fun log(msg: String) {
if (System.getenv("PKL_DEBUG") == "1") {
System.err.println("[pkl-server] $msg")
}
}
internal fun AutoCloseable.closeQuietly() {
try {
close()
} catch (e: Exception) {
log(e.message.orEmpty())
}
}
internal val threadLocalBufferPacker: ThreadLocal<MessageBufferPacker> =
ThreadLocal.withInitial { MessagePack.newDefaultBufferPacker() }
private val threadLocalEncoder: ThreadLocal<(Message) -> ByteArray> =
ThreadLocal.withInitial {
val packer = threadLocalBufferPacker.get()
val encoder = MessageEncoders.into(packer);
{ message: Message ->
packer.clear()
encoder.encode(message)
packer.toByteArray()
}
}
internal fun encode(message: Message): ByteArray {
return threadLocalEncoder.get()(message)
}
/**
* This is like [Future.get], except it throws the actual exception given to
* [CompletableFuture.completeExceptionally].
*
* [Future.get] will wrap any exception in [ExecutionException], which is kind of silly.
*/
fun <T> Future<T>.getUnderlying(): T =
try {
get()
} catch (e: ExecutionException) {
throw e.cause!!
}

View File

@@ -0,0 +1,7 @@
res1 = "bar"
res2 = ""
res3 = 1
res4 = 2.3
res5 = true
res6 = false
res7 = null

View File

@@ -0,0 +1,25 @@
module com.foo.bar.MyModule
class Person {
firstName: String
lastName: String
age: Int
}
barnOwl: Person = new {
firstName = "Barn Owl"
lastName = "Bird"
age = 38
}
pigeon: Person = new {
firstName = "Pigeon"
lastName = "Bird"
age = 41
}
typealias MyPerson = Person
personClass = Person
personTypeAlias = MyPerson

View File

@@ -0,0 +1,11 @@
res1: DataSize = 1.b
res2: DataSize = 2.kb
res3: DataSize = 3.kib
res4: DataSize = 4.mb
res5: DataSize = 5.mib
res6: DataSize = 6.gb
res7: DataSize = 7.gib
res8: DataSize = 8.tb
res9: DataSize = 9.tib
res10: DataSize = 10.pb
res11: DataSize = 11.pib

View File

@@ -0,0 +1,7 @@
res1 = 1.ns
res2 = 2.us
res3 = 3.ms
res4 = 4.s
res5 = 5.min
res6 = 6.h
res7 = 7.d

View File

@@ -0,0 +1,2 @@
res1 = IntSeq(1, 3)
res2 = IntSeq(1, 4).step(5)

View File

@@ -0,0 +1,6 @@
res1: List<Int> = List(1, 3, 5, 7)
res2: Listing<Int> = new { 2; 4; 6; 8 }
res3: List<Int> = List()
res4: Listing<Int> = new {}
res5: List<List<Int>> = List(List(1, 2))
res6: Listing<Listing<Int>> = new { new { 1; 2 } }

View File

@@ -0,0 +1,15 @@
res1: Map = Map("foo", 1, "bar", 2)
res2: Mapping = new {
["foo"] = 1
["bar"] = 2
}
res3: Mapping = new {
["childMap"] = new Mapping {
["childFoo"] = 3
}
}
res4: Mapping = new {
[Map("foo", 1)] = new Mapping {
["bar"] = 2
}
}

View File

@@ -0,0 +1,2 @@
res1 = Pair(1, 2)
res2 = Pair("foo", "bar")

View File

@@ -0,0 +1,3 @@
res1 = Regex("abc")
res2 = Regex("")
res3 = Regex("(?m)^abc$")

View File

@@ -0,0 +1,3 @@
res1: Set<Int> = Set(1, 3, 5, 7)
res2: Set<Int> = Set()
res3: Set<Any> = Set(1, true, "", null)

View File

@@ -0,0 +1,32 @@
- 1
- basic
- file:///$snippetsDir/input/basic.pkl
-
-
- 16
- res1
- bar
-
- 16
- res2
- ''
-
- 16
- res3
- 1
-
- 16
- res4
- 2.3
-
- 16
- res5
- true
-
- 16
- res6
- false
-
- 16
- res7
- null

View File

@@ -0,0 +1,54 @@
- 1
- com.foo.bar.MyModule
- file:///$snippetsDir/input/classes.pkl
-
-
- 16
- barnOwl
-
- 1
- com.foo.bar.MyModule#Person
- file:///$snippetsDir/input/classes.pkl
-
-
- 16
- firstName
- Barn Owl
-
- 16
- lastName
- Bird
-
- 16
- age
- 38
-
- 16
- pigeon
-
- 1
- com.foo.bar.MyModule#Person
- file:///$snippetsDir/input/classes.pkl
-
-
- 16
- firstName
- Pigeon
-
- 16
- lastName
- Bird
-
- 16
- age
- 41
-
- 16
- personClass
-
- 12
-
- 16
- personTypeAlias
-
- 13

View File

@@ -0,0 +1,81 @@
- 1
- datasize
- file:///$snippetsDir/input/datasize.pkl
-
-
- 16
- res1
-
- 8
- 1.0
- b
-
- 16
- res2
-
- 8
- 2.0
- kb
-
- 16
- res3
-
- 8
- 3.0
- kib
-
- 16
- res4
-
- 8
- 4.0
- mb
-
- 16
- res5
-
- 8
- 5.0
- mib
-
- 16
- res6
-
- 8
- 6.0
- gb
-
- 16
- res7
-
- 8
- 7.0
- gib
-
- 16
- res8
-
- 8
- 8.0
- tb
-
- 16
- res9
-
- 8
- 9.0
- tib
-
- 16
- res10
-
- 8
- 10.0
- pb
-
- 16
- res11
-
- 8
- 11.0
- pib

View File

@@ -0,0 +1,53 @@
- 1
- duration
- file:///$snippetsDir/input/duration.pkl
-
-
- 16
- res1
-
- 7
- 1.0
- ns
-
- 16
- res2
-
- 7
- 2.0
- us
-
- 16
- res3
-
- 7
- 3.0
- ms
-
- 16
- res4
-
- 7
- 4.0
- s
-
- 16
- res5
-
- 7
- 5.0
- min
-
- 16
- res6
-
- 7
- 6.0
- h
-
- 16
- res7
-
- 7
- 7.0
- d

View File

@@ -0,0 +1,20 @@
- 1
- intseq
- file:///$snippetsDir/input/intseq.pkl
-
-
- 16
- res1
-
- 10
- 1
- 3
- 1
-
- 16
- res2
-
- 10
- 1
- 4
- 5

View File

@@ -0,0 +1,58 @@
- 1
- list
- file:///$snippetsDir/input/list.pkl
-
-
- 16
- res1
-
- 4
-
- 1
- 3
- 5
- 7
-
- 16
- res2
-
- 5
-
- 2
- 4
- 6
- 8
-
- 16
- res3
-
- 4
- []
-
- 16
- res4
-
- 5
- []
-
- 16
- res5
-
- 4
-
-
- 4
-
- 1
- 2
-
- 16
- res6
-
- 5
-
-
- 5
-
- 1
- 2

View File

@@ -0,0 +1,44 @@
- 1
- map
- file:///$snippetsDir/input/map.pkl
-
-
- 16
- res1
-
- 2
-
foo: 1
bar: 2
-
- 16
- res2
-
- 3
-
foo: 1
bar: 2
-
- 16
- res3
-
- 3
-
childMap:
- 3
-
childFoo: 3
-
- 16
- res4
-
- 3
-
?
- 2
-
foo: 1
:
- 3
-
bar: 2

View File

@@ -0,0 +1,18 @@
- 1
- pair
- file:///$snippetsDir/input/pair.pkl
-
-
- 16
- res1
-
- 9
- 1
- 2
-
- 16
- res2
-
- 9
- foo
- bar

View File

@@ -0,0 +1,22 @@
- 1
- regex
- file:///$snippetsDir/input/regex.pkl
-
-
- 16
- res1
-
- 11
- abc
-
- 16
- res2
-
- 11
- ''
-
- 16
- res3
-
- 11
- (?m)^abc$

View File

@@ -0,0 +1,30 @@
- 1
- set
- file:///$snippetsDir/input/set.pkl
-
-
- 16
- res1
-
- 6
-
- 1
- 3
- 5
- 7
-
- 16
- res2
-
- 6
- []
-
- 16
- res3
-
- 6
-
- 1
- true
- ''
- null

View File

@@ -0,0 +1,67 @@
/**
* 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.server
import java.nio.file.Path
import kotlin.reflect.KClass
import org.junit.platform.commons.annotation.Testable
import org.pkl.commons.test.InputOutputTestEngine
import org.pkl.core.Loggers
import org.pkl.core.ModuleSource
import org.pkl.core.SecurityManagers
import org.pkl.core.StackFrameTransformers
import org.pkl.core.module.ModuleKeyFactories
@Testable class BinaryEvaluatorSnippetTests
class BinaryEvaluatorSnippetTestEngine : InputOutputTestEngine() {
override val testClass: KClass<*> = BinaryEvaluatorSnippetTests::class
private val snippetsDir = rootProjectDir.resolve("pkl-server/src/test/files/SnippetTests")
private val outputDir = snippetsDir.resolve("output")
override val inputDir: Path = snippetsDir.resolve("input")
override val isInputFile: (Path) -> Boolean = { true }
override fun expectedOutputFileFor(inputFile: Path): Path {
val relativePath = inputDir.relativize(inputFile).toString()
return outputDir.resolve(relativePath.dropLast(3) + "yaml")
}
private val evaluator =
BinaryEvaluator(
StackFrameTransformers.empty,
SecurityManagers.defaultManager,
Loggers.stdErr(),
listOf(ModuleKeyFactories.file),
listOf(),
mapOf(),
mapOf(),
null,
null,
null,
null
)
private fun String.stripFilePaths() = replace(snippetsDir.toString(), "/\$snippetsDir")
override fun generateOutputFor(inputFile: Path): Pair<Boolean, String> {
val bytes = evaluator.evaluate(ModuleSource.path(inputFile), null)
return true to bytes.debugRendering.stripFilePaths()
}
}

View File

@@ -0,0 +1,150 @@
/**
* 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.server
import java.nio.file.Path
import java.util.regex.Pattern
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.pkl.core.*
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.resource.ResourceReaders
class BinaryEvaluatorTest {
private val evaluator =
BinaryEvaluator(
StackFrameTransformers.defaultTransformer,
SecurityManagers.standard(
listOf(Pattern.compile(".*")),
listOf(Pattern.compile(".*")),
SecurityManagers.defaultTrustLevels,
Path.of("")
),
Loggers.noop(),
listOf(ModuleKeyFactories.standardLibrary),
listOf(ResourceReaders.environmentVariable(), ResourceReaders.externalProperty()),
mapOf(),
mapOf(),
null,
null,
null,
null
)
private fun evaluate(text: String, expression: String?) =
evaluator.evaluate(ModuleSource.text(text), expression)
@Test
fun `evaluate whole module`() {
val bytes = evaluate("foo = 1", null)
assertThat(bytes.debugRendering)
.isEqualTo(
"""
- 1
- text
- repl:text
-
-
- 16
- foo
- 1
"""
.trimIndent()
)
}
@Test
fun `evaluate subpath`() {
val bytes =
evaluate(
"""
foo {
bar = 2
}
"""
.trimIndent(),
"foo.bar"
)
assertThat(bytes.asInt()).isEqualTo(2)
}
@Test
fun `evaluate output text`() {
val bytes =
evaluate(
"""
foo {
bar = 2
}
output {
renderer = new YamlRenderer {}
}
"""
.trimIndent(),
"output.text"
)
assertThat(bytes.asString())
.isEqualTo(
"""
foo:
bar: 2
"""
.trimIndent()
)
}
@Test
fun `evaluate let expression`() {
val bytes = evaluate("foo = 1", "let (bar = 2) foo + bar")
assertThat(bytes.asInt()).isEqualTo(3)
}
@Test
fun `evaluate import expression`() {
val bytes = evaluate("", """import("pkl:release").current.documentation.homepage""")
assertThat(bytes.asString()).startsWith("https://pkl-lang.org/")
}
@Test
fun `evaluate expression with invalid syntax`() {
val error = assertThrows<PklException> { evaluate("foo = 1", "<>!!!") }
assertThat(error).hasMessageContaining("Mismatched input")
assertThat(error).hasMessageContaining("<>!!!")
}
@Test
fun `evaluate non-expression`() {
val error = assertThrows<PklException> { evaluate("bar = 2", "bar = 15") }
assertThat(error).hasMessageContaining("Mismatched input")
assertThat(error).hasMessageContaining("bar = 15")
}
@Test
fun `evaluate semantically invalid expression`() {
val error = assertThrows<PklException> { evaluate("foo = 1", "foo as String") }
assertThat(error).hasMessageContaining("Expected value of type `String`, but got type `Int`")
}
}

View File

@@ -0,0 +1,275 @@
/**
* 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.server
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.net.URI
import java.nio.file.Path
import java.time.Duration
import java.util.regex.Pattern
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.msgpack.core.MessagePack
import org.pkl.core.module.PathElement
import org.pkl.core.packages.Checksums
class MessagePackCodecTest {
private val encoder: MessageEncoder
private val decoder: MessageDecoder
init {
val inputStream = PipedInputStream()
val outputStream = PipedOutputStream(inputStream)
encoder = MessagePackEncoder(MessagePack.newDefaultPacker(outputStream))
decoder = MessagePackDecoder(MessagePack.newDefaultUnpacker(inputStream))
}
private fun roundtrip(message: Message) {
encoder.encode(message)
val decoded = decoder.decode()
assertThat(decoded).isEqualTo(message)
}
@Test
fun `round-trip CreateEvaluatorRequest`() {
val resourceReader1 =
ResourceReaderSpec(
scheme = "resourceReader1",
hasHierarchicalUris = true,
isGlobbable = true,
)
val resourceReader2 =
ResourceReaderSpec(
scheme = "resourceReader2",
hasHierarchicalUris = true,
isGlobbable = false,
)
val moduleReader1 =
ModuleReaderSpec(
scheme = "moduleReader1",
hasHierarchicalUris = true,
isGlobbable = true,
isLocal = true
)
val moduleReader2 =
ModuleReaderSpec(
scheme = "moduleReader2",
hasHierarchicalUris = true,
isGlobbable = false,
isLocal = false
)
roundtrip(
CreateEvaluatorRequest(
requestId = 123,
allowedModules = listOf("pkl", "file", "https").map(Pattern::compile),
allowedResources =
listOf("pkl", "file", "https", "resourceReader1", "resourceReader2")
.map(Pattern::compile),
clientResourceReaders = listOf(resourceReader1, resourceReader2),
clientModuleReaders = listOf(moduleReader1, moduleReader2),
modulePaths = listOf(Path.of("some/path.zip"), Path.of("other/path.zip")),
env = mapOf("KEY1" to "VALUE1", "KEY2" to "VALUE2"),
properties = mapOf("property1" to "value1", "property2" to "value2"),
timeout = Duration.ofSeconds(10),
rootDir = Path.of("root/dir"),
cacheDir = Path.of("cache/dir"),
outputFormat = "pcf",
project =
Project(
projectFileUri = URI("file:///dummy/PklProject"),
packageUri = null,
dependencies =
mapOf(
"foo" to
Project(
projectFileUri = URI("file:///foo"),
packageUri = URI("package://localhost:12110/foo@1.0.0"),
dependencies =
mapOf(
"bar" to
Project(
projectFileUri = URI("file:///bar"),
packageUri = URI("package://localhost:12110/bar@1.1.0"),
dependencies = emptyMap()
)
)
),
"baz" to
RemoteDependency(URI("package://localhost:12110/baz@1.1.0"), Checksums("abc123"))
)
)
)
)
}
@Test
fun `round-trip CreateEvaluatorResponse`() {
roundtrip(CreateEvaluatorResponse(requestId = 123, evaluatorId = 456, error = null))
}
@Test
fun `round-trip CloseEvaluator`() {
roundtrip(CloseEvaluator(evaluatorId = 123))
}
@Test
fun `round-trip EvaluateRequest`() {
roundtrip(
EvaluateRequest(
requestId = 123,
evaluatorId = 456,
moduleUri = URI("some/module.pkl"),
moduleText = null,
expr = "some + expression"
)
)
}
@Test
fun `round-trip EvaluateResponse`() {
roundtrip(
EvaluateResponse(
requestId = 123,
evaluatorId = 456,
result = byteArrayOf(1, 2, 3, 4, 5),
error = null
)
)
}
@Test
fun `round-trip LogMessage`() {
roundtrip(
LogMessage(
evaluatorId = 123,
level = 0,
message = "Hello, world!",
frameUri = "file:///some/module.pkl"
)
)
}
@Test
fun `round-trip ReadResourceRequest`() {
roundtrip(
ReadResourceRequest(requestId = 123, evaluatorId = 456, uri = URI("some/resource.json"))
)
}
@Test
fun `round-trip ReadResourceResponse`() {
roundtrip(
ReadResourceResponse(
requestId = 123,
evaluatorId = 456,
contents = byteArrayOf(1, 2, 3, 4, 5),
error = null
)
)
}
@Test
fun `round-trip ReadModuleRequest`() {
roundtrip(ReadModuleRequest(requestId = 123, evaluatorId = 456, uri = URI("some/module.pkl")))
}
@Test
fun `round-trip ReadModuleResponse`() {
roundtrip(
ReadModuleResponse(requestId = 123, evaluatorId = 456, contents = "x = 42", error = null)
)
}
@Test
fun `round-trip ListModulesRequest`() {
roundtrip(ListModulesRequest(requestId = 135, evaluatorId = 246, uri = URI("foo:/bar/baz/biz")))
}
@Test
fun `round-trip ListModulesResponse`() {
roundtrip(
ListModulesResponse(
requestId = 123,
evaluatorId = 234,
pathElements = listOf(PathElement("foo", true), PathElement("bar", false)),
error = null
)
)
roundtrip(
ListModulesResponse(
requestId = 123,
evaluatorId = 234,
pathElements = null,
error = "Something dun went wrong"
)
)
}
@Test
fun `round-trip ListResourcesRequest`() {
roundtrip(ListResourcesRequest(requestId = 987, evaluatorId = 1359, uri = URI("bar:/bazzy")))
}
@Test
fun `round-trip ListResourcesResponse`() {
roundtrip(
ListResourcesResponse(
requestId = 3851,
evaluatorId = 3019,
pathElements = listOf(PathElement("foo", true), PathElement("bar", false)),
error = null
)
)
roundtrip(
ListResourcesResponse(
requestId = 3851,
evaluatorId = 3019,
pathElements = null,
error = "something went wrong"
)
)
}
@Test
fun `decode request with missing request ID`() {
val bytes =
MessagePack.newDefaultBufferPacker()
.apply {
packArrayHeader(2)
packInt(MessageType.CREATE_EVALUATOR_REQUEST.code)
packMapHeader(1)
packString("clientResourceSchemes")
packArrayHeader(0)
}
.toByteArray()
val decoder = MessagePackDecoder(MessagePack.newDefaultUnpacker(bytes))
val exception = assertThrows<DecodeException> { decoder.decode() }
assertThat(exception.message).contains("requestId")
}
@Test
fun `decode invalid message header`() {
val bytes = MessagePack.newDefaultBufferPacker().apply { packInt(2) }.toByteArray()
val decoder = MessagePackDecoder(MessagePack.newDefaultUnpacker(bytes))
val exception = assertThrows<DecodeException> { decoder.decode() }
assertThat(exception).hasMessage("Malformed message header.")
assertThat(exception).hasRootCauseMessage("Expected Array, but got Integer (02)")
}
}

View File

@@ -0,0 +1,110 @@
/**
* 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.server
import java.lang.IllegalStateException
import org.msgpack.core.MessagePack
import org.msgpack.core.MessageUnpacker
import org.msgpack.value.ValueType
import org.pkl.core.util.yaml.YamlEmitter
/** Renders MessagePack structures in YAML. */
class MessagePackDebugRenderer(bytes: ByteArray) {
private val unpacker: MessageUnpacker = MessagePack.newDefaultUnpacker(bytes)
private val currIndent = StringBuilder("")
private val sb = StringBuilder()
private val indent = " "
private val yamlEmitter = YamlEmitter.create(sb, "1.2", indent)
private fun incIndent() {
currIndent.append(indent)
}
private fun decIndent() {
currIndent.setLength(currIndent.length - indent.length)
}
private fun newline() {
sb.append("\n")
sb.append(currIndent)
}
private fun renderKey() {
val mf = unpacker.nextFormat
when (mf.valueType!!) {
ValueType.STRING -> yamlEmitter.emit(unpacker.unpackString(), currIndent, true)
ValueType.MAP,
ValueType.ARRAY -> {
sb.append("? ")
incIndent()
renderValue()
decIndent()
newline()
}
else -> renderValue()
}
sb.append(": ")
}
private fun renderValue() {
val mf = unpacker.nextFormat
when (mf.valueType!!) {
ValueType.INTEGER,
ValueType.FLOAT,
ValueType.BOOLEAN,
ValueType.NIL -> sb.append(unpacker.unpackValue().toJson())
ValueType.STRING -> yamlEmitter.emit(unpacker.unpackString(), currIndent, false)
ValueType.ARRAY -> {
val size = unpacker.unpackArrayHeader()
if (size == 0) {
sb.append("[]")
return
}
for (i in 0 until size) {
newline()
sb.append("- ")
incIndent()
renderValue()
decIndent()
}
}
ValueType.MAP -> {
val size = unpacker.unpackMapHeader()
if (size == 0) {
sb.append("{}")
return
}
for (i in 0 until size) {
newline()
renderKey()
incIndent()
renderValue()
decIndent()
}
}
ValueType.BINARY,
ValueType.EXTENSION -> throw IllegalStateException("Unexpected value type ${mf.valueType}")
}
}
val output by lazy {
renderValue()
sb.toString().removePrefix("\n")
}
}
val ByteArray.debugRendering
get() = MessagePackDebugRenderer(this).output

View File

@@ -0,0 +1,994 @@
/**
* 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.server
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.net.URI
import java.nio.file.Path
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.regex.Pattern
import kotlin.io.path.createDirectories
import kotlin.io.path.outputStream
import kotlin.io.path.writeText
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.msgpack.core.MessagePack
import org.pkl.commons.test.PackageServer
import org.pkl.core.module.PathElement
class ServerTest {
companion object {
private const val useDirectTransport = false
private val executor: ExecutorService =
if (useDirectTransport) {
createDirectExecutor()
} else {
Executors.newCachedThreadPool()
}
@AfterAll
@JvmStatic
@Suppress("unused")
fun afterAll() {
executor.shutdown()
}
}
private val transports: Pair<MessageTransport, MessageTransport> = run {
if (useDirectTransport) {
MessageTransports.direct()
} else {
val in1 = PipedInputStream()
val out1 = PipedOutputStream(in1)
val in2 = PipedInputStream()
val out2 = PipedOutputStream(in2)
MessageTransports.stream(in1, out2) to MessageTransports.stream(in2, out1)
}
}
private val client: TestTransport = TestTransport(transports.first)
private val server: Server = Server(transports.second)
@BeforeEach
fun before() {
executor.execute { server.start() }
executor.execute { client.start() }
}
@AfterEach
fun after() {
client.close()
server.close()
}
@Test
fun `create and close evaluator`() {
val evaluatorId = client.sendCreateEvaluatorRequest(requestId = 123)
client.send(CloseEvaluator(evaluatorId = evaluatorId))
}
@Test
fun `evaluate module`() {
val evaluatorId = client.sendCreateEvaluatorRequest()
val requestId = 234L
client.send(
EvaluateRequest(
requestId = requestId,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText =
"""
foo {
bar = "bar"
}
"""
.trimIndent(),
expr = null
)
)
val response = client.receive<EvaluateResponse>()
assertThat(response.error).isNull()
assertThat(response.result).isNotNull
assertThat(response.requestId).isEqualTo(requestId)
val unpacker = MessagePack.newDefaultUnpacker(response.result)
val value = unpacker.unpackValue()
assertThat(value.isArrayValue)
}
@Test
fun `trace logs`() {
val evaluatorId = client.sendCreateEvaluatorRequest()
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText =
"""
foo = trace(1 + 2 + 3)
"""
.trimIndent(),
expr = null
)
)
val response = client.receive<LogMessage>()
assertThat(response.level).isEqualTo(0)
assertThat(response.message).isEqualTo("1 + 2 + 3 = 6")
client.receive<EvaluateResponse>()
}
@Test
fun `warn logs`() {
val evaluatorId = client.sendCreateEvaluatorRequest()
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText =
"""
@Deprecated { message = "use bar instead" }
function foo() = 5
result = foo()
"""
.trimIndent(),
expr = null
)
)
val response = client.receive<LogMessage>()
assertThat(response.level).isEqualTo(1)
assertThat(response.message).contains("use bar instead")
client.receive<EvaluateResponse>()
}
@Test
fun `read resource`() {
val reader =
ResourceReaderSpec(scheme = "bahumbug", hasHierarchicalUris = true, isGlobbable = false)
val evaluatorId = client.sendCreateEvaluatorRequest(resourceReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText = """res = read("bahumbug:/foo.pkl").text""",
expr = "res"
)
)
val readResourceMsg = client.receive<ReadResourceRequest>()
assertThat(readResourceMsg.uri.toString()).isEqualTo("bahumbug:/foo.pkl")
assertThat(readResourceMsg.evaluatorId).isEqualTo(evaluatorId)
client.send(
ReadResourceResponse(
requestId = readResourceMsg.requestId,
evaluatorId = evaluatorId,
contents = "my bahumbug".toByteArray(),
error = null
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.error).isNull()
val unpacker = MessagePack.newDefaultUnpacker(evaluateResponse.result)
val value = unpacker.unpackValue()
assertThat(value.asStringValue().asString()).isEqualTo("my bahumbug")
}
@Test
fun `read resource error`() {
val reader =
ResourceReaderSpec(scheme = "bahumbug", hasHierarchicalUris = true, isGlobbable = false)
val evaluatorId = client.sendCreateEvaluatorRequest(resourceReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText = """res = read("bahumbug:/foo.txt").text""",
expr = "res"
)
)
val readResourceMsg = client.receive<ReadResourceRequest>()
client.send(
ReadResourceResponse(
requestId = readResourceMsg.requestId,
evaluatorId = evaluatorId,
contents = null,
error = "cannot read my bahumbug"
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.error).contains("bahumbug:/foo.txt")
assertThat(evaluateResponse.error).doesNotContain("org.pkl.core.PklBugException")
}
@Test
fun `glob resource`() {
val reader = ResourceReaderSpec(scheme = "bird", hasHierarchicalUris = true, isGlobbable = true)
val evaluatorId = client.sendCreateEvaluatorRequest(resourceReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText =
"""
res = read*("bird:/**.txt").keys
"""
.trimIndent(),
expr = "res"
)
)
val listResourcesRequest = client.receive<ListResourcesRequest>()
assertThat(listResourcesRequest.uri.toString()).isEqualTo("bird:/")
client.send(
ListResourcesResponse(
requestId = listResourcesRequest.requestId,
evaluatorId = listResourcesRequest.evaluatorId,
pathElements = listOf(PathElement("foo.txt", false), PathElement("subdir", true)),
error = null
)
)
val listResourcesRequest2 = client.receive<ListResourcesRequest>()
assertThat(listResourcesRequest2.uri.toString()).isEqualTo("bird:/subdir/")
client.send(
ListResourcesResponse(
requestId = listResourcesRequest2.requestId,
evaluatorId = listResourcesRequest2.evaluatorId,
pathElements =
listOf(
PathElement("bar.txt", false),
),
error = null
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.result!!.debugYaml)
.isEqualTo(
"""
- 6
-
- bird:/foo.txt
- bird:/subdir/bar.txt
"""
.trimIndent()
)
}
@Test
fun `glob resource error`() {
val reader = ResourceReaderSpec(scheme = "bird", hasHierarchicalUris = true, isGlobbable = true)
val evaluatorId = client.sendCreateEvaluatorRequest(resourceReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText =
"""
res = read*("bird:/**.txt").keys
"""
.trimIndent(),
expr = "res"
)
)
val listResourcesRequest = client.receive<ListResourcesRequest>()
assertThat(listResourcesRequest.uri.toString()).isEqualTo("bird:/")
client.send(
ListResourcesResponse(
requestId = listResourcesRequest.requestId,
evaluatorId = listResourcesRequest.evaluatorId,
pathElements = null,
error = "didnt work"
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.error)
.isEqualTo(
"""
Pkl Error
I/O error resolving glob pattern `bird:/**.txt`.
IOException: didnt work
1 | res = read*("bird:/**.txt").keys
^^^^^^^^^^^^^^^^^^^^^
at text#res (repl:text)
1 | res
^^^
at (repl:text)
"""
.trimIndent()
)
}
@Test
fun `read module`() {
val reader =
ModuleReaderSpec(
scheme = "bird",
hasHierarchicalUris = true,
isLocal = true,
isGlobbable = false
)
val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText = """res = import("bird:/pigeon.pkl").value""",
expr = "res"
)
)
val readModuleMsg = client.receive<ReadModuleRequest>()
assertThat(readModuleMsg.uri.toString()).isEqualTo("bird:/pigeon.pkl")
assertThat(readModuleMsg.evaluatorId).isEqualTo(evaluatorId)
client.send(
ReadModuleResponse(
requestId = readModuleMsg.requestId,
evaluatorId = evaluatorId,
contents = "value = 5",
error = null
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.error).isNull()
val unpacker = MessagePack.newDefaultUnpacker(evaluateResponse.result)
val value = unpacker.unpackValue()
assertThat(value.asIntegerValue().asInt()).isEqualTo(5)
}
@Test
fun `read module error`() {
val reader =
ModuleReaderSpec(
scheme = "bird",
hasHierarchicalUris = true,
isLocal = true,
isGlobbable = false
)
val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText = """res = import("bird:/pigeon.pkl").value""",
expr = "res"
)
)
val readModuleMsg = client.receive<ReadModuleRequest>()
assertThat(readModuleMsg.uri.toString()).isEqualTo("bird:/pigeon.pkl")
assertThat(readModuleMsg.evaluatorId).isEqualTo(evaluatorId)
client.send(
ReadModuleResponse(
requestId = readModuleMsg.requestId,
evaluatorId = evaluatorId,
contents = null,
error = "Don't know where Pigeon is"
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.error).contains("Don't know where Pigeon is")
}
@Test
fun `glob module`() {
val reader =
ModuleReaderSpec(
scheme = "bird",
hasHierarchicalUris = true,
isLocal = true,
isGlobbable = true
)
val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText = """res = import*("bird:/**.pkl").keys""",
expr = "res"
)
)
val listModulesMsg = client.receive<ListModulesRequest>()
assertThat(listModulesMsg.uri.scheme).isEqualTo("bird")
assertThat(listModulesMsg.uri.path).isEqualTo("/")
client.send(
ListModulesResponse(
requestId = listModulesMsg.requestId,
evaluatorId = evaluatorId,
pathElements =
listOf(
PathElement("birds", true),
PathElement("majesticBirds", true),
PathElement("Person.pkl", false)
),
error = null
)
)
val listModulesMsg2 = client.receive<ListModulesRequest>()
assertThat(listModulesMsg2.uri.scheme).isEqualTo("bird")
assertThat(listModulesMsg2.uri.path).isEqualTo("/birds/")
client.send(
ListModulesResponse(
requestId = listModulesMsg2.requestId,
evaluatorId = listModulesMsg2.evaluatorId,
pathElements =
listOf(
PathElement("pigeon.pkl", false),
PathElement("parrot.pkl", false),
),
error = null
)
)
val listModulesMsg3 = client.receive<ListModulesRequest>()
assertThat(listModulesMsg3.uri.scheme).isEqualTo("bird")
assertThat(listModulesMsg3.uri.path).isEqualTo("/majesticBirds/")
client.send(
ListModulesResponse(
requestId = listModulesMsg3.requestId,
evaluatorId = listModulesMsg3.evaluatorId,
pathElements =
listOf(
PathElement("barnOwl.pkl", false),
PathElement("elfOwl.pkl", false),
),
error = null
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.result!!.debugRendering)
.isEqualTo(
"""
- 6
-
- bird:/Person.pkl
- bird:/birds/parrot.pkl
- bird:/birds/pigeon.pkl
- bird:/majesticBirds/barnOwl.pkl
- bird:/majesticBirds/elfOwl.pkl
"""
.trimIndent()
)
}
@Test
fun `glob module error`() {
val reader =
ModuleReaderSpec(
scheme = "bird",
hasHierarchicalUris = true,
isLocal = true,
isGlobbable = true
)
val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText = """res = import*("bird:/**.pkl").keys""",
expr = "res"
)
)
val listModulesMsg = client.receive<ListModulesRequest>()
assertThat(listModulesMsg.uri.scheme).isEqualTo("bird")
assertThat(listModulesMsg.uri.path).isEqualTo("/")
client.send(
ListModulesResponse(
requestId = listModulesMsg.requestId,
evaluatorId = evaluatorId,
pathElements = null,
error = "nope"
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.error)
.isEqualTo(
"""
Pkl Error
I/O error resolving glob pattern `bird:/**.pkl`.
IOException: nope
1 | res = import*("bird:/**.pkl").keys
^^^^^^^^^^^^^^^^^^^^^^^
at text#res (repl:text)
1 | res
^^^
at (repl:text)
"""
.trimIndent()
)
}
@Test
fun `read and evaluate module path from jar`(@TempDir tempDir: Path) {
val jarFile = tempDir.resolve("resource1.jar")
jarFile.outputStream().use { outStream ->
javaClass.getResourceAsStream("resource1.jar")!!.use { inStream ->
inStream.copyTo(outStream)
}
}
val evaluatorId = client.sendCreateEvaluatorRequest(modulePaths = listOf(jarFile))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("modulepath:/dir1/module.pkl"),
moduleText = null,
expr = "output.text"
)
)
val response = client.receive<EvaluateResponse>()
assertThat(response.error).isNull()
val tripleQuote = "\"\"\""
assertThat(response.result!!.debugYaml)
.isEqualTo(
"""
|
res1 {
uri = "modulepath:/dir1/resource1.txt"
text = $tripleQuote
content
$tripleQuote
base64 = "Y29udGVudAo="
}
res2 {
uri = "modulepath:/dir1/resource1.txt"
text = $tripleQuote
content
$tripleQuote
base64 = "Y29udGVudAo="
}
res3 {
ressy = "the module2 output"
}
"""
.trimIndent()
)
}
@Test
fun `import triple-dot path`() {
val reader =
ModuleReaderSpec(
scheme = "bird",
hasHierarchicalUris = true,
isLocal = true,
isGlobbable = true
)
val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("bird:/foo/bar/baz.pkl"),
moduleText =
"""
import ".../buz.pkl"
res = buz.res
"""
.trimIndent(),
expr = "res"
)
)
val readModuleRequest = client.receive<ReadModuleRequest>()
assertThat(readModuleRequest.uri).isEqualTo(URI("bird:/foo/buz.pkl"))
client.send(
ReadModuleResponse(
requestId = readModuleRequest.requestId,
evaluatorId = readModuleRequest.evaluatorId,
contents = null,
error = "not here"
)
)
val readModuleRequest2 = client.receive<ReadModuleRequest>()
assertThat(readModuleRequest2.uri).isEqualTo(URI("bird:/buz.pkl"))
client.send(
ReadModuleResponse(
requestId = readModuleRequest2.requestId,
evaluatorId = readModuleRequest2.evaluatorId,
contents = "res = 1",
error = null
)
)
val evaluatorResponse = client.receive<EvaluateResponse>()
assertThat(evaluatorResponse.result!!.debugYaml).isEqualTo("1")
}
@Test
fun `evaluate error`() {
val evaluatorId = client.sendCreateEvaluatorRequest()
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("repl:text"),
moduleText = """foo = 1""",
expr = "foo as String"
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.requestId).isEqualTo(1)
assertThat(evaluateResponse.error).contains("Expected value of type")
}
@Test
fun `evaluate client-provided module reader`() {
val reader =
ModuleReaderSpec(
scheme = "bird",
hasHierarchicalUris = true,
isLocal = false,
isGlobbable = false
)
val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("bird:/pigeon.pkl"),
moduleText = null,
expr = "output.text",
)
)
val readModuleRequest = client.receive<ReadModuleRequest>()
assertThat(readModuleRequest.uri.toString()).isEqualTo("bird:/pigeon.pkl")
client.send(
ReadModuleResponse(
requestId = readModuleRequest.requestId,
evaluatorId = evaluatorId,
contents =
"""
firstName = "Pigeon"
lastName = "Bird"
fullName = firstName + " " + lastName
"""
.trimIndent(),
error = null
)
)
val evaluateResponse = client.receive<EvaluateResponse>()
assertThat(evaluateResponse.result).isNotNull
assertThat(evaluateResponse.result!!.debugYaml)
.isEqualTo(
"""
|
firstName = "Pigeon"
lastName = "Bird"
fullName = "Pigeon Bird"
"""
.trimIndent()
)
}
@Test
fun `concurrent evaluations`() {
val reader =
ModuleReaderSpec(
scheme = "bird",
hasHierarchicalUris = true,
isLocal = false,
isGlobbable = false
)
val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader))
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = URI("bird:/pigeon.pkl"),
moduleText = null,
expr = "output.text",
)
)
client.send(
EvaluateRequest(
requestId = 2,
evaluatorId = evaluatorId,
moduleUri = URI("bird:/parrot.pkl"),
moduleText = null,
expr = "output.text"
)
)
// evaluation is single-threaded; `parrot.pkl` gets evaluated after `pigeon.pkl` completes.
val response11 = client.receive<ReadModuleRequest>()
assertThat(response11.uri.toString()).isEqualTo("bird:/pigeon.pkl")
client.send(
ReadModuleResponse(
response11.requestId,
evaluatorId,
contents =
"""
firstName = "Pigeon"
lastName = "Bird"
fullName = firstName + " " + lastName
"""
.trimIndent(),
error = null
)
)
val response12 = client.receive<EvaluateResponse>()
assertThat(response12.result).isNotNull
assertThat(response12.result!!.debugYaml)
.isEqualTo(
"""
|
firstName = "Pigeon"
lastName = "Bird"
fullName = "Pigeon Bird"
"""
.trimIndent()
)
val response21 = client.receive<ReadModuleRequest>()
assertThat(response21.uri.toString()).isEqualTo("bird:/parrot.pkl")
client.send(
ReadModuleResponse(
response21.requestId,
evaluatorId,
contents =
"""
firstName = "Parrot"
lastName = "Bird"
fullName = firstName + " " + lastName
"""
.trimIndent(),
error = null
)
)
val response22 = client.receive<EvaluateResponse>()
assertThat(response22.result).isNotNull
assertThat(response22.result!!.debugYaml)
.isEqualTo(
"""
|
firstName = "Parrot"
lastName = "Bird"
fullName = "Parrot Bird"
"""
.trimIndent()
)
}
@Test
fun `evaluate with project dependencies`(@TempDir tempDir: Path) {
val cacheDir = tempDir.resolve("cache").createDirectories()
PackageServer.populateCacheDir(cacheDir)
val libDir = tempDir.resolve("lib/").createDirectories()
libDir
.resolve("lib.pkl")
.writeText(
"""
text = "This is from lib"
"""
.trimIndent()
)
libDir
.resolve("PklProject")
.writeText(
"""
amends "pkl:Project"
package {
name = "lib"
baseUri = "package://localhost:12110/lib"
version = "5.0.0"
packageZipUrl = "https://localhost:12110/lib.zip"
}
"""
.trimIndent()
)
val projectDir = tempDir.resolve("proj/").createDirectories()
val module = projectDir.resolve("mod.pkl")
module.writeText(
"""
import "@birds/Bird.pkl"
import "@lib/lib.pkl"
res: Bird = new {
name = "Birdie"
favoriteFruit { name = "dragonfruit" }
}
libContents = lib
"""
.trimIndent()
)
val dollar = '$'
projectDir
.resolve("PklProject.deps.json")
.writeText(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "${dollar}skipChecksumVerification"
}
},
"package://localhost:12110/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:12110/fruit@1.0.5",
"checksums": {
"sha256": "${dollar}skipChecksumVerification"
}
},
"package://localhost:12110/lib@5": {
"type": "local",
"uri": "projectpackage://localhost:12110/lib@5.0.0",
"path": "../lib"
}
}
}
"""
.trimIndent()
)
val evaluatorId =
client.sendCreateEvaluatorRequest(
cacheDir = cacheDir,
project =
Project(
projectFileUri = projectDir.resolve("PklProject").toUri(),
packageUri = null,
dependencies =
mapOf(
"birds" to
RemoteDependency(packageUri = URI("package://localhost:12110/birds@0.5.0"), null),
"lib" to
Project(
projectFileUri = libDir.toUri().resolve("PklProject"),
packageUri = URI("package://localhost:12110/lib@5.0.0"),
dependencies = emptyMap()
)
)
)
)
client.send(
EvaluateRequest(
requestId = 1,
evaluatorId = evaluatorId,
moduleUri = module.toUri(),
moduleText = null,
expr = "output.text",
)
)
val resp2 = client.receive<EvaluateResponse>()
assertThat(resp2.error).isNull()
assertThat(resp2.result).isNotNull()
assertThat(resp2.result!!.debugRendering.trim())
.isEqualTo(
"""
|
res {
name = "Birdie"
favoriteFruit {
name = "dragonfruit"
}
}
libContents {
text = "This is from lib"
}
"""
.trimIndent()
)
}
private val ByteArray.debugYaml
get() = MessagePackDebugRenderer(this).output.trimIndent()
private fun TestTransport.sendCreateEvaluatorRequest(
requestId: Long = 123,
resourceReaders: List<ResourceReaderSpec> = listOf(),
moduleReaders: List<ModuleReaderSpec> = listOf(),
modulePaths: List<Path> = listOf(),
project: Project? = null,
cacheDir: Path? = null
): Long {
val message =
CreateEvaluatorRequest(
requestId = 123,
allowedResources = listOf(Pattern.compile(".*")),
allowedModules = listOf(Pattern.compile(".*")),
clientResourceReaders = resourceReaders,
clientModuleReaders = moduleReaders,
modulePaths = modulePaths,
env = mapOf(),
properties = mapOf(),
timeout = null,
rootDir = null,
cacheDir = cacheDir,
outputFormat = null,
project = project
)
send(message)
val response = receive<CreateEvaluatorResponse>()
assertThat(response.requestId).isEqualTo(requestId)
assertThat(response.evaluatorId).isNotNull
assertThat(response.error).isNull()
return response.evaluatorId!!
}
}

View File

@@ -0,0 +1,50 @@
/**
* 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.server
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.BlockingQueue
import org.assertj.core.api.Assertions.assertThat
internal class TestTransport(private val delegate: MessageTransport) : AutoCloseable {
private val incomingMessages: BlockingQueue<Message> = ArrayBlockingQueue(10)
fun start() {
delegate.start({ incomingMessages.put(it) }, { incomingMessages.put(it) })
}
override fun close() {
delegate.close()
}
fun send(message: ClientOneWayMessage) {
delegate.send(message)
}
fun send(message: ClientRequestMessage) {
delegate.send(message) { incomingMessages.put(it) }
}
fun send(message: ClientResponseMessage) {
delegate.send(message)
}
inline fun <reified T : Message> receive(): T {
val message = incomingMessages.take()
assertThat(message).isInstanceOf(T::class.java)
return message as T
}
}

View File

@@ -0,0 +1,53 @@
/**
* 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.server
import java.util.concurrent.AbstractExecutorService
import java.util.concurrent.ExecutorService
import java.util.concurrent.TimeUnit
import org.msgpack.core.MessagePack
import org.msgpack.value.ImmutableValue
fun ByteArray.unpack(): ImmutableValue = MessagePack.newDefaultUnpacker(this).unpackValue()
fun ByteArray.asInt(): Int = unpack().asIntegerValue().asInt()
fun ByteArray.asString(): String = unpack().asStringValue().asString()
fun createDirectExecutor(): ExecutorService =
object : AbstractExecutorService() {
override fun execute(command: Runnable) {
command.run()
}
override fun shutdown() {}
override fun shutdownNow(): MutableList<Runnable> {
throw UnsupportedOperationException("shutdownNow")
}
override fun isShutdown(): Boolean {
throw UnsupportedOperationException("isShutdown")
}
override fun isTerminated(): Boolean {
throw UnsupportedOperationException("isTerminated")
}
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
throw UnsupportedOperationException("awaitTermination")
}
}

View File

@@ -0,0 +1 @@
org.pkl.server.BinaryEvaluatorSnippetTestEngine