Skip to content

Commit

Permalink
Improve plugin loading, and include sample plugin with docker contain…
Browse files Browse the repository at this point in the history
…er (#39)

* Allow passing in plugins path

* Update dockerfile to include the example plugin

* Load all plugins available in plugins directory
  • Loading branch information
mattjohnsonpint authored Jan 4, 2024
1 parent 004d9ab commit 7cb4604
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 31 deletions.
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# build hmruntime binary
FROM golang:alpine as runtime-builder
WORKDIR /src
COPY hmruntime/ ./
RUN go build .

# build example plugin
FROM node:20-alpine as plugin-builder
WORKDIR /src
COPY plugins/as/ ./
WORKDIR /src/hmplugin1
RUN npm install
RUN npm run build:release

# build runtime image
FROM ubuntu:20.04
LABEL maintainer="Hypermode <hello@hypermode.com>"
COPY --from=runtime-builder /src/hmruntime /usr/bin/hmruntime
COPY --from=plugin-builder /src/hmplugin1/build/release.wasm /plugins/hmplugin1.wasm

ENTRYPOINT ["hmruntime", "--plugins=/plugins"]
13 changes: 0 additions & 13 deletions hmruntime/Dockerfile

This file was deleted.

7 changes: 5 additions & 2 deletions hmruntime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ The following must be installed on your development workstation or build server:

To build the Hypermode runtime server: `go build`

To build the docker image: `docker build -t hmruntime .`
To build the docker image, from the root directory: `docker build -t hypermode/runtime .`

## Running

- To run the compiled program, invoke the `hmruntime` binary.
- To run from code (while developing), use `go run .` instead.
- To run using docker containers, use `docker run -p 8686:8686 -v <PLUGINS_PATH>:/plugins hmruntime:latest hmruntime --dgraph=http://host.docker.internal:8080`.
- To run using docker containers, first decide where plugins will be loaded from.
- To just use the default `hmplugin1` sample plugin, use `docker run -p 8686:8686 hypermode/runtime --dgraph=http://host.docker.internal:8080`.
- Or, mount a plugins directory on the host, use `docker run -p 8686:8686 -v <PLUGINS_PATH>:/plugins hypermode/runtime --dgraph=http://host.docker.internal:8080`.
- For example `-v ./plugins/as:/plugins`

## Notes

Expand Down
75 changes: 59 additions & 16 deletions hmruntime/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ var compiledModules = make(map[string]wazero.CompiledModule)
var functionsMap = make(map[string]functionInfo)

var dgraphUrl *string
var pluginsPath *string

func main() {
ctx := context.Background()

// Parse command-line flags
var port = flag.Int("port", 8686, "The HTTP port to listen on.")
dgraphUrl = flag.String("dgraph", "http://localhost:8080", "The Dgraph url to connect to.")
pluginsPath = flag.String("plugins", "../plugins/as", "The path to the plugins directory.")
flag.Parse()

// Initialize the WebAssembly runtime
Expand Down Expand Up @@ -71,22 +73,45 @@ func main() {
}

func loadPlugins(ctx context.Context) error {
entries, err := os.ReadDir(*pluginsPath)
if err != nil {
return fmt.Errorf("failed to read plugins directory: %v", err)
}

for _, entry := range entries {

// Determine if the entry represents a plugin.
var pluginName string
entryName := entry.Name()
if entry.IsDir() {
pluginName = entryName
path := fmt.Sprintf("%s/%s/build/debug.wasm", *pluginsPath, pluginName)
if _, err := os.Stat(path); err != nil {
continue
}
} else if strings.HasSuffix(entryName, ".wasm") {
pluginName = strings.TrimSuffix(entryName, ".wasm")
} else {
continue
}

// Load the plugin
err := loadPlugin(ctx, pluginName)
if err != nil {
log.Printf("Failed to load plugin '%s': %v\n", pluginName, err)
}
}

// For now, we have just one hardcoded plugin.
const pluginName = "hmplugin1"
return nil
}

// TODO: This will need work:
// - Plugins should probably be loaded from a repository, not from disk.
// - We'll need to figure out how to handle plugin updates.
// - We'll need to figure out hot/warm/cold plugin loading.
func loadPlugin(ctx context.Context, pluginName string) error {

err := loadPluginModule(ctx, pluginName)
if err != nil {
return err
}

// Temporarily, watch for changes to the plugin so we can reload it.
// TODO: Remove this when we have a better way to handle plugin updates.
err = watchForPluginChanges(ctx, pluginName)
if err != nil {
return err
Expand Down Expand Up @@ -169,8 +194,11 @@ func loadPluginModule(ctx context.Context, name string) error {
fmt.Printf("Loading plugin '%s'\n", name)
}

// TODO: Load the plugin from some repository instead of disk.
path := getPathForPlugin(name)
path, err := getPathForPlugin(name)
if err != nil {
return fmt.Errorf("failed to get path for plugin: %v", err)
}

plugin, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to load the plugin: %v", err)
Expand Down Expand Up @@ -228,10 +256,21 @@ func getModuleInstance(ctx context.Context, pluginName string) (wasm.Module, buf
return mod, buf, nil
}

func getPathForPlugin(name string) string {
// TODO: Decide whether to load the plugin in debug or release mode.
// For now use debug, so we get better stack traces on errors.
return "../plugins/as/" + name + "/build/debug.wasm"
func getPathForPlugin(name string) (string, error) {

// Normally the plugin will be directly in the plugins directory, by filename.
path := *pluginsPath + "/" + name + ".wasm"
if _, err := os.Stat(path); err == nil {
return path, nil
}

// For local development, the plugin will be in a subdirectory and we'll use the debug.wasm file.
path = *pluginsPath + "/" + name + "/build/debug.wasm"
if _, err := os.Stat(path); err == nil {
return path, nil
}

return "", fmt.Errorf("compiled wasm file not found for plugin '%s'", name)
}

func watchForPluginChanges(ctx context.Context, name string) error {
Expand Down Expand Up @@ -269,8 +308,12 @@ func watchForPluginChanges(ctx context.Context, name string) error {
}
}()

path := getPathForPlugin(name)
err := w.Add(path)
path, err := getPathForPlugin(name)
if err != nil {
return fmt.Errorf("failed to get path for plugin: %v", err)
}

err = w.Add(path)
if err != nil {
return fmt.Errorf("failed to watch plugin file: %v", err)
}
Expand Down

0 comments on commit 7cb4604

Please sign in to comment.