Skip to content

Commit

Permalink
feat(graalvm): document and move hybrid vfs implementation.
Browse files Browse the repository at this point in the history
Signed-off-by: Dario Valdespino <[email protected]>
  • Loading branch information
darvld committed Nov 2, 2023
1 parent 6fe9036 commit 8f5fbeb
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 148 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package elide.runtime.gvm.internals.vfs

import org.graalvm.polyglot.io.FileSystem
import java.net.URI
import java.nio.channels.SeekableByteChannel
import java.nio.file.*
import java.nio.file.DirectoryStream.Filter
import java.nio.file.attribute.FileAttribute
import kotlin.io.path.pathString

/**
* A hybrid [FileSystem] implementation using two layers: an in-memory [overlay], which takes priority for reads but
* ignores writes, and a [backing] layer that can be written to, and which handles any requests not satisfied by the
* overlay.
*
* Note that the current implementation is designed with a specific combination in mind: a JIMFS-backed embedded VFS
* as the overlay (using [EmbeddedGuestVFSImpl]), and a host-backed VFS as the base layer (using [HostVFSImpl]).
*
* Instances of this class can be acquired using [HybridVfs.acquire].
*/
internal class HybridVfs private constructor(
private val backing: FileSystem,
private val overlay: FileSystem,
) : FileSystem {
/**
* Convert this path to one that can be used by the [overlay] vfs.
*
* Because of incompatible types used by the underlying JIMFS and the platform-default file system, paths created
* using, for example, [Path.of], will not be recognized properly, and they must be transformed before use.
*/
private fun Path.forEmbedded(): Path {
return overlay.parsePath(pathString)
}

override fun parsePath(uri: URI?): Path {
return backing.parsePath(uri)
}

override fun parsePath(path: String?): Path {
return backing.parsePath(path)
}

override fun toAbsolutePath(path: Path?): Path {
return backing.toAbsolutePath(path)
}

override fun toRealPath(path: Path?, vararg linkOptions: LinkOption?): Path {
return backing.toRealPath(path, *linkOptions)
}

override fun createDirectory(dir: Path?, vararg attrs: FileAttribute<*>?) {
return backing.createDirectory(dir, *attrs)
}

override fun delete(path: Path?) {
backing.delete(path)
}

override fun checkAccess(path: Path, modes: MutableSet<out AccessMode>, vararg linkOptions: LinkOption) {
// if only READ is requested, try the in-memory vfs first
if (modes.size == 0 || (modes.size == 1 && modes.contains(AccessMode.READ))) runCatching {
// ensure the path is compatible with the embedded vfs before passing it
overlay.checkAccess(path.forEmbedded(), modes, *linkOptions)
return
}

// if WRITE or EXECUTE were requested, or if the in-memory vfs denied access,
// try using the host instead
backing.checkAccess(path, modes, *linkOptions)
}

override fun newByteChannel(
path: Path,
options: MutableSet<out OpenOption>,
vararg attrs: FileAttribute<*>,
): SeekableByteChannel {
// if only READ is requested, try the in-memory vfs first
if (options.size == 0 || (options.size == 1 && options.contains(StandardOpenOption.READ))) runCatching {
// ensure the path is compatible with the embedded vfs before passing it
return overlay.newByteChannel(path.forEmbedded(), options, *attrs)
}

// if write-related options were set, or the in-memory vfs failed to open the file
// (e.g. because it doesn't exist in the bundle), try using the host instead
return backing.newByteChannel(path, options, *attrs)
}

override fun newDirectoryStream(dir: Path, filter: Filter<in Path>?): DirectoryStream<Path> {
// try the in-memory vfs first
runCatching {
// ensure the path is compatible with the embedded vfs before passing it
return overlay.newDirectoryStream(dir.forEmbedded(), filter)
}

// if the in-memory vfs failed to open the directory, try using the host instead
return backing.newDirectoryStream(dir, filter)
}

override fun readAttributes(path: Path, attributes: String?, vararg options: LinkOption): MutableMap<String, Any> {
// try the in-memory vfs first
runCatching {
// ensure the path is compatible with the embedded vfs before passing it
return overlay.readAttributes(path.forEmbedded(), attributes, *options)
}

// if the in-memory vfs failed to read the file attributes, try using the host instead
return backing.readAttributes(path, attributes, *options)
}

companion object {
/**
* Configures a new [HybridVfs] using an in-memory VFS containing the provided [overlay] as [overlay], and the
* host file system as [backing] layer.
*
* @param overlay A list of bundles to be unpacked into the in-memory fs.
* @param writable Whether to allow writes to the backing layer.
* @return A new [HybridVfs] instance.
*/
fun acquire(writable: Boolean, overlay: List<URI>): HybridVfs {
// configure an in-memory vfs with the provided bundles as overlay
val inMemory = EmbeddedGuestVFSImpl.Builder.newBuilder()
.setBundlePaths(overlay)
.setReadOnly(false)
.build()

// use the host fs as backing layer
val host = HostVFSImpl.Builder.newBuilder()
.setReadOnly(!writable)
.build()

return HybridVfs(
backing = host,
overlay = inMemory,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,30 @@ package elide.runtime.plugins.vfs
import java.net.URI
import java.net.URL
import elide.runtime.core.DelicateElideApi
import elide.runtime.core.PolyglotEngineConfiguration
import elide.runtime.core.PolyglotEngineConfiguration.HostAccess.ALLOW_ALL
import elide.runtime.core.PolyglotEngineConfiguration.HostAccess.ALLOW_IO

/** Configuration DSL for the [Vfs] plugin. */
@DelicateElideApi public class VfsConfig internal constructor() {
@DelicateElideApi public class VfsConfig internal constructor(configuration: PolyglotEngineConfiguration) {
/** Private mutable list of registered bundles. */
private val bundles: MutableList<URI> = mutableListOf()

/** Internal list of bundles registered for use in the VFS. */
internal val registeredBundles: List<URI> get() = bundles

/** Whether the file system is writable. If false, write operations will throw an exception. */
public var writable: Boolean = false

/**
* Whether to use the host's file system instead of an embedded VFS. If true, bundles registered using [include] will
* not be applied.
*
* Enabled by default if the engine's [hostAccess][PolyglotEngineConfiguration.hostAccess] is set to [ALLOW_ALL] or
* [ALLOW_IO], otherwise false.
*/
internal var useHost: Boolean = false
internal var useHost: Boolean = configuration.hostAccess == ALLOW_ALL || configuration.hostAccess == ALLOW_IO

/** Register a [bundle] to be added to the VFS on creation. */
public fun include(bundle: URI) {
bundles.add(bundle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,14 @@ package elide.runtime.plugins.vfs
import org.graalvm.polyglot.io.FileSystem
import org.graalvm.polyglot.io.IOAccess
import java.net.URI
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import elide.runtime.Logging
import elide.runtime.core.*
import elide.runtime.core.EngineLifecycleEvent.ContextCreated
import elide.runtime.core.EngineLifecycleEvent.EngineCreated
import elide.runtime.core.EnginePlugin.InstallationScope
import elide.runtime.core.EnginePlugin.Key
import elide.runtime.core.PolyglotEngineConfiguration.HostAccess
import elide.runtime.core.PolyglotEngineConfiguration.HostAccess.ALLOW_ALL
import elide.runtime.core.PolyglotEngineConfiguration.HostAccess.ALLOW_IO
import elide.runtime.gvm.internals.vfs.EmbeddedGuestVFSImpl
import elide.runtime.gvm.internals.vfs.HostVFSImpl
import elide.runtime.plugins.vfs.internal.HybridVfs
import elide.runtime.gvm.internals.vfs.HybridVfs

/**
* Engine plugin providing configurable VFS support for polyglot contexts. Both embedded and host VFS implementations
Expand All @@ -56,20 +50,15 @@ import elide.runtime.plugins.vfs.internal.HybridVfs

internal fun onEngineCreated(@Suppress("unused_parameter") builder: PolyglotEngineBuilder) {
// select the VFS implementation depending on the configuration
val embedded = acquireEmbeddedVfs(config.writable, config.registeredBundles)

// if no host access is requested, use an embedded in-memory vfs
if (!config.useHost) {
fileSystem = if (!config.useHost) {
logging.debug("No host access requested, using in-memory vfs")

fileSystem = embedded
return
acquireEmbeddedVfs(config.writable, config.registeredBundles)
} else {
// if the configuration requires host access, we use a hybrid vfs
logging.debug("Host access requested, using hybrid vfs")
HybridVfs.acquire(config.writable, config.registeredBundles)
}

// if the configuration requires host access, we use a hybrid vfs
logging.debug("Host access requested, using hybrid vfs")
val host = acquireHostVfs(config.writable)
fileSystem = HybridVfs(host, embedded)
}

/** Configure a context builder to use a custom [fileSystem]. */
Expand All @@ -84,11 +73,7 @@ import elide.runtime.plugins.vfs.internal.HybridVfs

override fun install(scope: InstallationScope, configuration: VfsConfig.() -> Unit): Vfs {
// apply the configuration and create the plugin instance
val config = VfsConfig().apply(configuration)

// switch to the host's FS if requested in the general configuration
config.useHost = scope.configuration.hostAccess.useHostFs

val config = VfsConfig(scope.configuration).apply(configuration)
val instance = Vfs(config)

// subscribe to lifecycle events
Expand All @@ -98,21 +83,8 @@ import elide.runtime.plugins.vfs.internal.HybridVfs
return instance
}

private val HostAccess.useHostFs get() = this == ALLOW_IO || this == ALLOW_ALL

/** Build a new [FileSystem] delegating to the host FS. */
private fun acquireHostVfs(writable: Boolean): FileSystem {
return HostVFSImpl.Builder.newBuilder()
.setReadOnly(!writable)
.setWorkingDirectory(Path.of(".").absolutePathString())
.build()
}

/** Build a new embedded [FileSystem], optionally [writable], with the specified [root] path and [bundles]. */
private fun acquireEmbeddedVfs(
writable: Boolean,
bundles: List<URI>
): FileSystem {
/** Build a new embedded [FileSystem], optionally [writable], using the specified [bundles]. */
private fun acquireEmbeddedVfs(writable: Boolean, bundles: List<URI>): FileSystem {
return EmbeddedGuestVFSImpl.Builder.newBuilder()
.setBundlePaths(bundles)
.setReadOnly(!writable)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package elide.runtime.plugins.vfs
package elide.runtime.gvm.internals.vfs

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
Expand All @@ -12,9 +12,6 @@ import kotlin.io.path.createFile
import kotlin.io.path.readText
import kotlin.io.path.writeText
import kotlin.test.assertEquals
import elide.runtime.gvm.vfs.EmbeddedGuestVFS
import elide.runtime.gvm.vfs.HostVFS
import elide.runtime.plugins.vfs.internal.HybridVfs

internal class HybridVfsTest {
/** Temporary directory used for host-related test cases. */
Expand All @@ -40,12 +37,8 @@ internal class HybridVfsTest {
* @see useVfs
*/
private fun acquireVfs(): HybridVfs {
val host = HostVFS.acquire()
val embedded = EmbeddedGuestVFS.forBundle(
HybridVfsTest::class.java.getResource("/sample-vfs.tar")!!.toURI(),
)

return HybridVfs(host = host, inMemory = embedded)
val bundles = listOf(HybridVfsTest::class.java.getResource("/sample-vfs.tar")!!.toURI())
return HybridVfs.acquire(writable = true, overlay = bundles)
}

/** Convenience method used to [acquire][acquireVfs] a [HybridVfs] instance and use it in a test. */
Expand Down

0 comments on commit 8f5fbeb

Please sign in to comment.