Skip to content

davidsbond/plugin

Repository files navigation

plugin

Go Reference License: MIT CI

A gRPC-based plugin system.

How it works

This package allows you to build external plugins from your main application via the use of gRPC and UNIX domain sockets.

Each plugin exists as a standalone binary that shares a machine with the application intending to invoke it. When your application starts, you call the plugin.Use function, providing the path under which the binary resides. From here, the binary is executed where it starts a local gRPC server listening on a unique UNIX domain socket dedicated to it.

The plugin system then obtains metadata from the plugin, ensuring it is running as expected. From here, commands defined via protocol buffers can be invoked.

graph TD
;
    Application["Your application"] <-->|Sends Commands| Socket["/tmp/d1hf45ohpe2r63pr1bk.sock"]
    Plugin <-->|Handles Commands| Socket
    Application -->|Executes| Plugin["/var/plugins/example d1hf45ohpe2r63pr1bk"]
Loading

Defining commands

This package utilizes protocol buffers when defining commands to allow for strong typing and interoperability across languages. A command consists of a unique name and two protobuf messages, an Input and Output:

syntax = "proto3";

package command;

message AddInput {
  int64 a = 1;
  int64 b = 2;
}

message AddOutput {
  int64 result = 1;
}

Once generated, you use these messages to define a command. These commands are specified when calling plugin.Run. Below is an example of a main.go file where we create an example plugin with an add command that returns the result of adding two numbers:

package main

import (
	"context"
	"log"

	"github.com/davidsbond/plugin"
	command "github.com/yourusername/path/to/protos"
)

func main() {
	config := plugin.Config{
		Name: "example",
		Commands: []plugin.CommandHandler{
			&plugin.Command[*command.AddInput, *command.AddOutput]{
				Use: "add",
				Run: func(ctx context.Context, input *command.AddInput) (*command.AddOutput, error) {
					return &command.AddOutput{
						Result: input.GetA() + input.GetB(),
					}, nil
				},
			},
		},
	}

	if err := plugin.Run(config); err != nil {
		log.Fatal(err)
	}
}

This main.go file should be compiled and placed on the same machine as the application you wish to invoke it.

Using plugins

The plugin.Use function is used to execute the plugin binary, starting its gRPC server and creating a client that communicates with it via a UNIX domain socket. Below is an example of how we start using our add command:

package main

import (
	"context"
	"fmt"

	"github.com/davidsbond/plugin"
	command "github.com/yourusername/path/to/protos"
)

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

	p, err := plugin.Use(ctx, "./example")
	if err != nil {
		panic(err)
	}

	// Remember the call Close() when you're done using the plugin!
	defer p.Close()

	output := &command.AddOutput{}
	input := &command.AddInput{
		A: 2,
		B: 5,
	}

	if err = p.Exec(ctx, "add", input, output); err != nil {
		panic(err)
	}

	fmt.Println(output.GetResult())
}

For a more detailed look at how the package works, view the documentation on pkg.go.dev, view the test files or the plugin implementation used for testing.

About

A gRPC-based plugin system

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •