The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    ├── CODEOWNERS
    ├── dependabot.yml
    ├── pull_request_template.md
    └── workflows
    │   └── test.yaml
├── .gitignore
├── .go-version
├── CHANGELOG.md
├── LICENSE
├── README.md
├── buf.gen.yaml
├── buf.yaml
├── client.go
├── client_posix_test.go
├── client_test.go
├── client_unix_test.go
├── constants.go
├── discover.go
├── docs
    ├── README.md
    ├── extensive-go-plugin-tutorial.md
    ├── guide-plugin-write-non-go.md
    └── internals.md
├── error.go
├── error_test.go
├── examples
    ├── basic
    │   ├── .gitignore
    │   ├── README.md
    │   ├── main.go
    │   ├── plugin
    │   │   └── greeter_impl.go
    │   └── shared
    │   │   └── greeter_interface.go
    ├── bidirectional
    │   ├── README.md
    │   ├── buf.gen.yaml
    │   ├── buf.yaml
    │   ├── main.go
    │   ├── plugin-go-grpc
    │   │   └── main.go
    │   ├── proto
    │   │   ├── kv.pb.go
    │   │   ├── kv.proto
    │   │   └── kv_grpc.pb.go
    │   └── shared
    │   │   ├── grpc.go
    │   │   └── interface.go
    ├── grpc
    │   ├── .gitignore
    │   ├── README.md
    │   ├── buf.gen.yaml
    │   ├── buf.yaml
    │   ├── main.go
    │   ├── plugin-go-grpc
    │   │   └── main.go
    │   ├── plugin-go-netrpc
    │   │   └── main.go
    │   ├── plugin-python
    │   │   ├── .gitignore
    │   │   ├── plugin.py
    │   │   ├── proto
    │   │   │   ├── kv_pb2.py
    │   │   │   └── kv_pb2_grpc.py
    │   │   └── requirements.txt
    │   ├── proto
    │   │   ├── kv.pb.go
    │   │   ├── kv.proto
    │   │   └── kv_grpc.pb.go
    │   └── shared
    │   │   ├── grpc.go
    │   │   ├── interface.go
    │   │   └── rpc.go
    └── negotiated
    │   ├── .gitignore
    │   ├── README.md
    │   ├── main.go
    │   └── plugin-go
    │       └── main.go
├── go.mod
├── go.sum
├── grpc_broker.go
├── grpc_client.go
├── grpc_client_test.go
├── grpc_controller.go
├── grpc_server.go
├── grpc_stdio.go
├── internal
    ├── cmdrunner
    │   ├── addr_translator.go
    │   ├── cmd_reattach.go
    │   ├── cmd_runner.go
    │   ├── cmd_runner_test.go
    │   ├── notes_unix.go
    │   ├── notes_windows.go
    │   ├── process.go
    │   ├── process_posix.go
    │   ├── process_windows.go
    │   └── testdata
    │   │   ├── .gitignore
    │   │   ├── Makefile
    │   │   ├── README.md
    │   │   ├── go.mod
    │   │   ├── go.sum
    │   │   └── minimal.go
    ├── grpcmux
    │   ├── blocked_client_listener.go
    │   ├── blocked_server_listener.go
    │   ├── grpc_client_muxer.go
    │   ├── grpc_muxer.go
    │   └── grpc_server_muxer.go
    └── plugin
    │   ├── grpc_broker.pb.go
    │   ├── grpc_broker.proto
    │   ├── grpc_broker_grpc.pb.go
    │   ├── grpc_controller.pb.go
    │   ├── grpc_controller.proto
    │   ├── grpc_controller_grpc.pb.go
    │   ├── grpc_stdio.pb.go
    │   ├── grpc_stdio.proto
    │   └── grpc_stdio_grpc.pb.go
├── log_entry.go
├── mtls.go
├── mux_broker.go
├── plugin.go
├── plugin_test.go
├── process.go
├── protocol.go
├── rpc_client.go
├── rpc_client_test.go
├── rpc_server.go
├── runner
    └── runner.go
├── server.go
├── server_mux.go
├── server_test.go
├── server_unix_test.go
├── stream.go
├── test
    └── grpc
    │   ├── test.pb.go
    │   ├── test.proto
    │   └── test_grpc.pb.go
└── testing.go


/.github/CODEOWNERS:
--------------------------------------------------------------------------------
 1 | # Each line is a file pattern followed by one or more owners.
 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
 3 | 
 4 | # Default owner
 5 | * @hashicorp/team-ip-compliance
 6 | 
 7 | # Add override rules below. Each line is a file/folder pattern followed by one or more owners.
 8 | # Being an owner means those groups or individuals will be added as reviewers to PRs affecting
 9 | # those areas of the code.
10 | # Examples:
11 | # /docs/  @docs-team
12 | # *.js    @js-team
13 | # *.go    @go-team


--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
 1 | # Copyright (c) HashiCorp, Inc.
 2 | # SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | version: 2
 5 | 
 6 | updates:
 7 |   - package-ecosystem: "github-actions"
 8 |     directory: "/"
 9 |     schedule:
10 |       interval: "weekly"
11 |       day: "sunday"
12 |     commit-message:
13 |       prefix: "[chore] : "
14 |     groups:
15 |       actions:
16 |         patterns:
17 |           - "*"
18 | 
19 |   - package-ecosystem: "gomod"
20 |     directories:
21 |       - "/"
22 |       - "/internal/cmdrunner/testdata/"
23 |     schedule:
24 |       interval: "weekly"
25 |       day: "sunday"
26 |     commit-message:
27 |       prefix: "[chore] : "
28 |     groups:
29 |       go:
30 |         patterns:
31 |           - "*"
32 |         applies-to: "version-updates"
33 |       go-security:
34 |         patterns:
35 |           - "*"
36 |         applies-to: "security-updates"


--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
 1 | <!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 -->
 2 | ## Description
 3 | 
 4 | <!-- Provide a summary of what the PR does and why it is being submitted. -->
 5 | 
 6 | ## Related Issue
 7 | 
 8 | <!-- If this PR is linked to any issue, provide the issue number or description here. Any related JIRA tickets can also be added here. -->
 9 | 
10 | ## How Has This Been Tested?
11 | 
12 | <!-- Describe how the changes have been tested. Provide test instructions or details. -->
13 | 


--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
 1 | name: CI Tests
 2 | on:
 3 |   pull_request:
 4 |     paths-ignore:
 5 |       - 'README.md'
 6 |   push:
 7 |     branches:
 8 |       - 'main'
 9 |     paths-ignore:
10 |       - 'README.md'
11 | 
12 | permissions:
13 |   contents: read
14 | 
15 | jobs:
16 |   go-fmt-and-vet:
17 |     runs-on: ubuntu-latest
18 |     steps:
19 |     - name: Checkout Code
20 |       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21 |     - name: Setup Go
22 |       uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
23 |       with:
24 |         go-version: '>=1.21'
25 |         cache: true
26 |     - name: Go formatting
27 |       run: |
28 |         files=$(go fmt ./...)
29 |         if [ -n "$files" ]; then
30 |           echo "The following file(s) do not conform to go fmt:"
31 |           echo "$files"
32 |           exit 1
33 |         fi
34 |     - name: Lint code
35 |       uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
36 |   go-test:
37 |     needs: go-fmt-and-vet
38 |     runs-on: ubuntu-latest
39 |     steps:
40 |     - name: Checkout Code
41 |       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
42 |     - name: Setup Go
43 |       uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
44 |       with:
45 |         go-version: '>=1.21'
46 |         cache: true
47 |     - name: Run test and generate coverage report
48 |       run: |
49 |         go test -race ./... -v -coverprofile=coverage.out
50 |     - name: Upload Coverage Report
51 |       uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
52 |       with:
53 |           path: coverage.out
54 |           name: Coverage-report
55 |     - name: Display Coverage report
56 |       run: go tool cover -func=coverage.out
57 |     - name: Build Go
58 |       run: go build ./...
59 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | 


--------------------------------------------------------------------------------
/.go-version:
--------------------------------------------------------------------------------
1 | 1.24.1
2 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
  1 | ## v1.6.2
  2 | 
  3 | ENHANCEMENTS:
  4 | 
  5 | * Added support for gRPC dial options to the `Dial` API [[GH-257](https://github.com/hashicorp/go-plugin/pull/257)]
  6 | 
  7 | BUGS:
  8 | 
  9 | * Fixed a bug where reattaching to a plugin that exits could kill an unrelated process [[GH-320](https://github.com/hashicorp/go-plugin/pull/320)]
 10 | 
 11 | ## v1.6.1
 12 | 
 13 | BUGS:
 14 | 
 15 | * Suppress spurious `os.ErrClosed` on plugin shutdown [[GH-299](https://github.com/hashicorp/go-plugin/pull/299)]
 16 | 
 17 | ENHANCEMENTS:
 18 | 
 19 | * deps: bump google.golang.org/grpc to v1.58.3 [[GH-296](https://github.com/hashicorp/go-plugin/pull/296)]
 20 | 
 21 | ## v1.6.0
 22 | 
 23 | CHANGES:
 24 | 
 25 | * plugin: Plugins written in other languages can optionally start to advertise whether they support gRPC broker multiplexing.
 26 |   If the environment variable `PLUGIN_MULTIPLEX_GRPC` is set, it is safe to include a seventh field containing a boolean
 27 |   value in the `|`-separated protocol negotiation line.
 28 | 
 29 | ENHANCEMENTS:
 30 | 
 31 | * Support muxing gRPC broker connections over a single listener [[GH-288](https://github.com/hashicorp/go-plugin/pull/288)]
 32 | * client: Configurable buffer size for reading plugin log lines [[GH-265](https://github.com/hashicorp/go-plugin/pull/265)]
 33 | * Use `buf` for proto generation [[GH-286](https://github.com/hashicorp/go-plugin/pull/286)]
 34 | * deps: bump golang.org/x/net to v0.17.0 [[GH-285](https://github.com/hashicorp/go-plugin/pull/285)]
 35 | * deps: bump golang.org/x/sys to v0.13.0 [[GH-285](https://github.com/hashicorp/go-plugin/pull/285)]
 36 | * deps: bump golang.org/x/text to v0.13.0 [[GH-285](https://github.com/hashicorp/go-plugin/pull/285)]
 37 | 
 38 | ## v1.5.2
 39 | 
 40 | ENHANCEMENTS:
 41 | 
 42 | client: New `UnixSocketConfig.TempDir` option allows setting the directory to use when creating plugin-specific Unix socket directories [[GH-282](https://github.com/hashicorp/go-plugin/pull/282)]
 43 | 
 44 | ## v1.5.1
 45 | 
 46 | BUGS:
 47 | 
 48 | * server: `PLUGIN_UNIX_SOCKET_DIR` is consistently used for gRPC broker sockets as well as the initial socket [[GH-277](https://github.com/hashicorp/go-plugin/pull/277)]
 49 | 
 50 | ENHANCEMENTS:
 51 | 
 52 | * client: New `UnixSocketConfig` option in `ClientConfig` to support making the client's Unix sockets group-writable [[GH-277](https://github.com/hashicorp/go-plugin/pull/277)]
 53 | 
 54 | ## v1.5.0
 55 | 
 56 | ENHANCEMENTS:
 57 | 
 58 | * client: New `runner.Runner` interface to support clients providing custom plugin command runner implementations [[GH-270](https://github.com/hashicorp/go-plugin/pull/270)]
 59 |     * Accessible via new `ClientConfig` field `RunnerFunc`, which is mutually exclusive with `Cmd` and `Reattach`
 60 |     * Reattaching support via `ReattachConfig` field `ReattachFunc`
 61 | * client: New `ClientConfig` field `SkipHostEnv` allows omitting the client process' own environment variables from the plugin command's environment [[GH-270](https://github.com/hashicorp/go-plugin/pull/270)]
 62 | * client: Add `ID()` method to `Client` for retrieving the pid or other unique ID of a running plugin [[GH-272](https://github.com/hashicorp/go-plugin/pull/272)]
 63 | * server: Support setting the directory to create Unix sockets in with the env var `PLUGIN_UNIX_SOCKET_DIR` [[GH-270](https://github.com/hashicorp/go-plugin/pull/270)]
 64 | * server: Support setting group write permission and a custom group name or gid owner with the env var `PLUGIN_UNIX_SOCKET_GROUP` [[GH-270](https://github.com/hashicorp/go-plugin/pull/270)]
 65 | 
 66 | ## v1.4.11-rc1
 67 | 
 68 | ENHANCEMENTS:
 69 | 
 70 | * deps: bump protoreflect to v1.15.1 [[GH-264](https://github.com/hashicorp/go-plugin/pull/264)]
 71 | 
 72 | ## v1.4.10
 73 | 
 74 | BUG FIXES:
 75 | 
 76 | * additional notes: ensure to close files [[GH-241](https://github.com/hashicorp/go-plugin/pull/241)]
 77 | 
 78 | ENHANCEMENTS:
 79 | 
 80 | * deps: Remove direct dependency on golang.org/x/net [[GH-240](https://github.com/hashicorp/go-plugin/pull/240)]
 81 | 
 82 | ## v1.4.9
 83 | 
 84 | ENHANCEMENTS:
 85 | 
 86 | * client: Remove log warning introduced in 1.4.5 when SecureConfig is nil. [[GH-238](https://github.com/hashicorp/go-plugin/pull/238)]
 87 | 
 88 | ## v1.4.8
 89 | 
 90 | BUG FIXES:
 91 | 
 92 | * Fix windows build: [[GH-227](https://github.com/hashicorp/go-plugin/pull/227)]
 93 | 
 94 | ## v1.4.7
 95 | 
 96 | ENHANCEMENTS:
 97 | 
 98 | * More detailed error message on plugin start failure: [[GH-223](https://github.com/hashicorp/go-plugin/pull/223)]
 99 | 
100 | ## v1.4.6
101 | 
102 | BUG FIXES:
103 | 
104 | * server: Prevent gRPC broker goroutine leak when using `GRPCServer` type `GracefulStop()` or `Stop()` methods [[GH-220](https://github.com/hashicorp/go-plugin/pull/220)]
105 | 
106 | ## v1.4.5
107 | 
108 | ENHANCEMENTS:
109 | 
110 | * client: log warning when SecureConfig is nil [[GH-207](https://github.com/hashicorp/go-plugin/pull/207)]
111 | 
112 | 
113 | ## v1.4.4
114 | 
115 | ENHANCEMENTS:
116 | 
117 | * client: increase level of plugin exit logs [[GH-195](https://github.com/hashicorp/go-plugin/pull/195)]
118 | 
119 | BUG FIXES:
120 | 
121 | * Bidirectional communication: fix bidirectional communication when AutoMTLS is enabled [[GH-193](https://github.com/hashicorp/go-plugin/pull/193)]
122 | * RPC: Trim a spurious log message for plugins using RPC [[GH-186](https://github.com/hashicorp/go-plugin/pull/186)]
123 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # Go Plugin System over RPC
  2 | 
  3 | `go-plugin` is a Go (golang) plugin system over RPC. It is the plugin system
  4 | that has been in use by HashiCorp tooling for over 4 years. While initially
  5 | created for [Packer](https://www.packer.io), it is additionally in use by
  6 | [Terraform](https://www.terraform.io), [Nomad](https://www.nomadproject.io),
  7 | [Vault](https://www.vaultproject.io),
  8 | [Boundary](https://www.boundaryproject.io),
  9 | and [Waypoint](https://www.waypointproject.io).
 10 | 
 11 | While the plugin system is over RPC, it is currently only designed to work
 12 | over a local [reliable] network. Plugins over a real network are not supported
 13 | and will lead to unexpected behavior.
 14 | 
 15 | This plugin system has been used on millions of machines across many different
 16 | projects and has proven to be battle hardened and ready for production use.
 17 | 
 18 | ## Features
 19 | 
 20 | The HashiCorp plugin system supports a number of features:
 21 | 
 22 | **Plugins are Go interface implementations.** This makes writing and consuming
 23 | plugins feel very natural. To a plugin author: you just implement an
 24 | interface as if it were going to run in the same process. For a plugin user:
 25 | you just use and call functions on an interface as if it were in the same
 26 | process. This plugin system handles the communication in between.
 27 | 
 28 | **Cross-language support.** Plugins can be written (and consumed) by
 29 | almost every major language. This library supports serving plugins via
 30 | [gRPC](http://www.grpc.io). gRPC-based plugins enable plugins to be written
 31 | in any language.
 32 | 
 33 | **Complex arguments and return values are supported.** This library
 34 | provides APIs for handling complex arguments and return values such
 35 | as interfaces, `io.Reader/Writer`, etc. We do this by giving you a library
 36 | (`MuxBroker`) for creating new connections between the client/server to
 37 | serve additional interfaces or transfer raw data.
 38 | 
 39 | **Bidirectional communication.** Because the plugin system supports
 40 | complex arguments, the host process can send it interface implementations
 41 | and the plugin can call back into the host process.
 42 | 
 43 | **Built-in Logging.** Any plugins that use the `log` standard library
 44 | will have log data automatically sent to the host process. The host
 45 | process will mirror this output prefixed with the path to the plugin
 46 | binary. This makes debugging with plugins simple. If the host system
 47 | uses [hclog](https://github.com/hashicorp/go-hclog) then the log data
 48 | will be structured. If the plugin also uses hclog, logs from the plugin
 49 | will be sent to the host hclog and be structured.
 50 | 
 51 | **Protocol Versioning.** A very basic "protocol version" is supported that
 52 | can be incremented to invalidate any previous plugins. This is useful when
 53 | interface signatures are changing, protocol level changes are necessary,
 54 | etc. When a protocol version is incompatible, a human friendly error
 55 | message is shown to the end user.
 56 | 
 57 | **Stdout/Stderr Syncing.** While plugins are subprocesses, they can continue
 58 | to use stdout/stderr as usual and the output will get mirrored back to
 59 | the host process. The host process can control what `io.Writer` these
 60 | streams go to to prevent this from happening.
 61 | 
 62 | **TTY Preservation.** Plugin subprocesses are connected to the identical
 63 | stdin file descriptor as the host process, allowing software that requires
 64 | a TTY to work. For example, a plugin can execute `ssh` and even though there
 65 | are multiple subprocesses and RPC happening, it will look and act perfectly
 66 | to the end user.
 67 | 
 68 | **Host upgrade while a plugin is running.** Plugins can be "reattached"
 69 | so that the host process can be upgraded while the plugin is still running.
 70 | This requires the host/plugin to know this is possible and daemonize
 71 | properly. `NewClient` takes a `ReattachConfig` to determine if and how to
 72 | reattach.
 73 | 
 74 | **Cryptographically Secure Plugins.** Plugins can be verified with an expected
 75 | checksum and RPC communications can be configured to use TLS. The host process
 76 | must be properly secured to protect this configuration.
 77 | 
 78 | ## Architecture
 79 | 
 80 | The HashiCorp plugin system works by launching subprocesses and communicating
 81 | over RPC (using standard `net/rpc` or [gRPC](http://www.grpc.io)). A single
 82 | connection is made between any plugin and the host process. For net/rpc-based
 83 | plugins, we use a [connection multiplexing](https://github.com/hashicorp/yamux)
 84 | library to multiplex any other connections on top. For gRPC-based plugins,
 85 | the HTTP2 protocol handles multiplexing.
 86 | 
 87 | This architecture has a number of benefits:
 88 | 
 89 |   * Plugins can't crash your host process: A panic in a plugin doesn't
 90 |     panic the plugin user.
 91 | 
 92 |   * Plugins are very easy to write: just write a Go application and `go build`.
 93 |     Or use any other language to write a gRPC server with a tiny amount of
 94 |     boilerplate to support go-plugin.
 95 | 
 96 |   * Plugins are very easy to install: just put the binary in a location where
 97 |     the host will find it (depends on the host but this library also provides
 98 |     helpers), and the plugin host handles the rest.
 99 | 
100 |   * Plugins can be relatively secure: The plugin only has access to the
101 |     interfaces and args given to it, not to the entire memory space of the
102 |     process. Additionally, go-plugin can communicate with the plugin over
103 |     TLS.
104 | 
105 | ## Usage
106 | 
107 | To use the plugin system, you must take the following steps. These are
108 | high-level steps that must be done. Examples are available in the
109 | `examples/` directory.
110 | 
111 |   1. Choose the interface(s) you want to expose for plugins.
112 | 
113 |   2. For each interface, implement an implementation of that interface
114 |      that communicates over a `net/rpc` connection or over a
115 |      [gRPC](http://www.grpc.io) connection or both. You'll have to implement
116 |      both a client and server implementation.
117 | 
118 |   3. Create a `Plugin` implementation that knows how to create the RPC
119 |      client/server for a given plugin type.
120 | 
121 |   4. Plugin authors call `plugin.Serve` to serve a plugin from the
122 |      `main` function.
123 | 
124 |   5. Plugin users use `plugin.Client` to launch a subprocess and request
125 |      an interface implementation over RPC.
126 | 
127 | That's it! In practice, step 2 is the most tedious and time consuming step.
128 | Even so, it isn't very difficult and you can see examples in the `examples/`
129 | directory as well as throughout our various open source projects.
130 | 
131 | For complete API documentation, see [GoDoc](https://godoc.org/github.com/hashicorp/go-plugin).
132 | 
133 | ## Roadmap
134 | 
135 | Our plugin system is constantly evolving. As we use the plugin system for
136 | new projects or for new features in existing projects, we constantly find
137 | improvements we can make.
138 | 
139 | At this point in time, the roadmap for the plugin system is:
140 | 
141 | **Semantic Versioning.** Plugins will be able to implement a semantic version.
142 | This plugin system will give host processes a system for constraining
143 | versions. This is in addition to the protocol versioning already present
144 | which is more for larger underlying changes.
145 | 
146 | ## What About Shared Libraries?
147 | 
148 | When we started using plugins (late 2012, early 2013), plugins over RPC
149 | were the only option since Go didn't support dynamic library loading. Today,
150 | Go supports the [plugin](https://golang.org/pkg/plugin/) standard library with
151 | a number of  limitations. Since 2012, our plugin system has stabilized 
152 | from tens of millions of users using it, and has many benefits we've come to 
153 | value greatly.
154 | 
155 | For example, we use this plugin system in
156 | [Vault](https://www.vaultproject.io) where dynamic library loading is
157 | not acceptable for security reasons. That is an extreme
158 | example, but we believe our library system has more upsides than downsides
159 | over dynamic library loading and since we've had it built and tested for years,
160 | we'll continue to use it.
161 | 
162 | Shared libraries have one major advantage over our system which is much
163 | higher performance. In real world scenarios across our various tools,
164 | we've never required any more performance out of our plugin system and it
165 | has seen very high throughput, so this isn't a concern for us at the moment.
166 | 


--------------------------------------------------------------------------------
/buf.gen.yaml:
--------------------------------------------------------------------------------
 1 | # Copyright (c) HashiCorp, Inc.
 2 | # SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | version: v1
 5 | plugins:
 6 |   - plugin: buf.build/protocolbuffers/go
 7 |     out: .
 8 |     opt:
 9 |       - paths=source_relative
10 |   - plugin: buf.build/grpc/go:v1.3.0
11 |     out: .
12 |     opt:
13 |       - paths=source_relative
14 |       - require_unimplemented_servers=false
15 | 


--------------------------------------------------------------------------------
/buf.yaml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 | 
4 | version: v1
5 | build:
6 |   excludes:
7 |     - examples/


--------------------------------------------------------------------------------
/client_posix_test.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | //go:build !windows
  5 | // +build !windows
  6 | 
  7 | package plugin
  8 | 
  9 | import (
 10 | 	"os"
 11 | 	"reflect"
 12 | 	"syscall"
 13 | 	"testing"
 14 | 	"time"
 15 | )
 16 | 
 17 | func TestClient_testInterfaceReattach(t *testing.T) {
 18 | 	// Setup the process for daemonization
 19 | 	process := helperProcess("test-interface-daemon")
 20 | 	if process.SysProcAttr == nil {
 21 | 		process.SysProcAttr = &syscall.SysProcAttr{}
 22 | 	}
 23 | 	process.SysProcAttr.Setsid = true
 24 | 	syscall.Umask(0)
 25 | 
 26 | 	c := NewClient(&ClientConfig{
 27 | 		Cmd:             process,
 28 | 		HandshakeConfig: testHandshake,
 29 | 		Plugins:         testPluginMap,
 30 | 	})
 31 | 
 32 | 	// Start it so we can get the reattach info
 33 | 	if _, err := c.Start(); err != nil {
 34 | 		t.Fatalf("err should be nil, got %s", err)
 35 | 	}
 36 | 
 37 | 	// New client with reattach info
 38 | 	reattach := c.ReattachConfig()
 39 | 	if reattach == nil {
 40 | 		c.Kill()
 41 | 		t.Fatal("reattach config should be non-nil")
 42 | 		return // Required for staticcheck SA5011
 43 | 	}
 44 | 
 45 | 	// Find the process and defer a kill so we know it is gone
 46 | 	p, err := os.FindProcess(reattach.Pid)
 47 | 	if err != nil {
 48 | 		c.Kill()
 49 | 		t.Fatalf("couldn't find process: %s", err)
 50 | 	}
 51 | 	defer func() { _ = p.Kill() }()
 52 | 
 53 | 	// Reattach
 54 | 	c = NewClient(&ClientConfig{
 55 | 		Reattach:        reattach,
 56 | 		HandshakeConfig: testHandshake,
 57 | 		Plugins:         testPluginMap,
 58 | 	})
 59 | 
 60 | 	// Start shouldn't error
 61 | 	if _, err := c.Start(); err != nil {
 62 | 		t.Fatalf("err: %s", err)
 63 | 	}
 64 | 
 65 | 	// It should still be alive
 66 | 	time.Sleep(1 * time.Second)
 67 | 	if c.Exited() {
 68 | 		t.Fatal("should not be exited")
 69 | 	}
 70 | 
 71 | 	// Grab the RPC client
 72 | 	client, err := c.Client()
 73 | 	if err != nil {
 74 | 		t.Fatalf("err should be nil, got %s", err)
 75 | 	}
 76 | 
 77 | 	// Grab the impl
 78 | 	raw, err := client.Dispense("test")
 79 | 	if err != nil {
 80 | 		t.Fatalf("err should be nil, got %s", err)
 81 | 	}
 82 | 
 83 | 	impl, ok := raw.(testInterface)
 84 | 	if !ok {
 85 | 		t.Fatalf("bad: %#v", raw)
 86 | 	}
 87 | 
 88 | 	result := impl.Double(21)
 89 | 	if result != 42 {
 90 | 		t.Fatalf("bad: %#v", result)
 91 | 	}
 92 | 
 93 | 	// Test the resulting reattach config
 94 | 	reattach2 := c.ReattachConfig()
 95 | 	if reattach2 == nil {
 96 | 		t.Fatal("reattach from reattached should not be nil")
 97 | 	}
 98 | 	if !reflect.DeepEqual(reattach, reattach2) {
 99 | 		t.Fatalf("bad: %#v", reattach)
100 | 	}
101 | 
102 | 	// Kill it
103 | 	c.Kill()
104 | 
105 | 	// Test that it knows it is exited
106 | 	if !c.Exited() {
107 | 		t.Fatal("should say client has exited")
108 | 	}
109 | }
110 | 


--------------------------------------------------------------------------------
/client_unix_test.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | //go:build !windows
  5 | // +build !windows
  6 | 
  7 | package plugin
  8 | 
  9 | import (
 10 | 	"fmt"
 11 | 	"os"
 12 | 	"os/exec"
 13 | 	"os/user"
 14 | 	"path/filepath"
 15 | 	"syscall"
 16 | 	"testing"
 17 | 
 18 | 	"github.com/hashicorp/go-hclog"
 19 | 	"github.com/hashicorp/go-plugin/internal/cmdrunner"
 20 | 	"github.com/hashicorp/go-plugin/runner"
 21 | )
 22 | 
 23 | func TestSetGroup(t *testing.T) {
 24 | 	group, err := user.LookupGroupId(fmt.Sprintf("%d", os.Getgid()))
 25 | 	if err != nil {
 26 | 		t.Fatal(err)
 27 | 	}
 28 | 	baseTempDir := t.TempDir()
 29 | 	baseTempDir, err = filepath.EvalSymlinks(baseTempDir)
 30 | 	if err != nil {
 31 | 		t.Fatal(err)
 32 | 	}
 33 | 	for name, tc := range map[string]struct {
 34 | 		group string
 35 | 	}{
 36 | 		"as integer": {fmt.Sprintf("%d", os.Getgid())},
 37 | 		"as name":    {group.Name},
 38 | 	} {
 39 | 		t.Run(name, func(t *testing.T) {
 40 | 			process := helperProcess("mock")
 41 | 			c := NewClient(&ClientConfig{
 42 | 				HandshakeConfig: testHandshake,
 43 | 				Plugins:         testPluginMap,
 44 | 				UnixSocketConfig: &UnixSocketConfig{
 45 | 					Group:   tc.group,
 46 | 					TempDir: baseTempDir,
 47 | 				},
 48 | 				RunnerFunc: func(l hclog.Logger, cmd *exec.Cmd, tmpDir string) (runner.Runner, error) {
 49 | 					// Run tests inside the RunnerFunc to ensure we don't race
 50 | 					// with the code that deletes tmpDir when the client fails
 51 | 					// to start properly.
 52 | 
 53 | 					// Test that it creates a directory with the proper owners and permissions.
 54 | 					if filepath.Dir(tmpDir) != baseTempDir {
 55 | 						t.Errorf("Expected base TempDir to be %s, but tmpDir was %s", baseTempDir, tmpDir)
 56 | 					}
 57 | 					info, err := os.Lstat(tmpDir)
 58 | 					if err != nil {
 59 | 						t.Fatal(err)
 60 | 					}
 61 | 					if info.Mode()&os.ModePerm != 0o770 {
 62 | 						t.Fatal(info.Mode())
 63 | 					}
 64 | 					stat, ok := info.Sys().(*syscall.Stat_t)
 65 | 					if !ok {
 66 | 						t.Fatal()
 67 | 					}
 68 | 					if stat.Gid != uint32(os.Getgid()) {
 69 | 						t.Fatalf("Expected %d, but got %d", os.Getgid(), stat.Gid)
 70 | 					}
 71 | 
 72 | 					// Check the correct environment variables were set to forward
 73 | 					// Unix socket config onto the plugin.
 74 | 					var foundUnixSocketDir, foundUnixSocketGroup bool
 75 | 					for _, env := range cmd.Env {
 76 | 						if env == fmt.Sprintf("%s=%s", EnvUnixSocketDir, tmpDir) {
 77 | 							foundUnixSocketDir = true
 78 | 						}
 79 | 						if env == fmt.Sprintf("%s=%s", EnvUnixSocketGroup, tc.group) {
 80 | 							foundUnixSocketGroup = true
 81 | 						}
 82 | 					}
 83 | 					if !foundUnixSocketDir {
 84 | 						t.Errorf("Did not find correct %s env in %v", EnvUnixSocketDir, cmd.Env)
 85 | 					}
 86 | 					if !foundUnixSocketGroup {
 87 | 						t.Errorf("Did not find correct %s env in %v", EnvUnixSocketGroup, cmd.Env)
 88 | 					}
 89 | 
 90 | 					process.Env = append(process.Env, cmd.Env...)
 91 | 					return cmdrunner.NewCmdRunner(l, process)
 92 | 				},
 93 | 			})
 94 | 			defer c.Kill()
 95 | 
 96 | 			_, err := c.Start()
 97 | 			if err != nil {
 98 | 				t.Fatalf("err should be nil, got %s", err)
 99 | 			}
100 | 		})
101 | 	}
102 | }
103 | 


--------------------------------------------------------------------------------
/constants.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | const (
 7 | 	// EnvUnixSocketDir specifies the directory that _plugins_ should create unix
 8 | 	// sockets in. Does not affect client behavior.
 9 | 	EnvUnixSocketDir = "PLUGIN_UNIX_SOCKET_DIR"
10 | 
11 | 	// EnvUnixSocketGroup specifies the owning, writable group to set for Unix
12 | 	// sockets created by _plugins_. Does not affect client behavior.
13 | 	EnvUnixSocketGroup = "PLUGIN_UNIX_SOCKET_GROUP"
14 | 
15 | 	envMultiplexGRPC = "PLUGIN_MULTIPLEX_GRPC"
16 | )
17 | 


--------------------------------------------------------------------------------
/discover.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | import (
 7 | 	"path/filepath"
 8 | )
 9 | 
10 | // Discover discovers plugins that are in a given directory.
11 | //
12 | // The directory doesn't need to be absolute. For example, "." will work fine.
13 | //
14 | // This currently assumes any file matching the glob is a plugin.
15 | // In the future this may be smarter about checking that a file is
16 | // executable and so on.
17 | //
18 | // TODO: test
19 | func Discover(glob, dir string) ([]string, error) {
20 | 	var err error
21 | 
22 | 	// Make the directory absolute if it isn't already
23 | 	if !filepath.IsAbs(dir) {
24 | 		dir, err = filepath.Abs(dir)
25 | 		if err != nil {
26 | 			return nil, err
27 | 		}
28 | 	}
29 | 
30 | 	return filepath.Glob(filepath.Join(dir, glob))
31 | }
32 | 


--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
 1 | # go-plugin Documentation
 2 | 
 3 | This directory contains documentation and guides for `go-plugin` and how
 4 | to integrate it into your projects. It is assumed that you know _what_
 5 | go-plugin is and _why_ you would want to use it. If not, please see the
 6 | [README](https://github.com/hashicorp/go-plugin/blob/master/README.md).
 7 | 
 8 | ## Table of Contents
 9 | 
10 | **[Writing Plugins Without Go](https://github.com/hashicorp/go-plugin/blob/master/docs/guide-plugin-write-non-go.md).**
11 | This shows how to write a plugin using a programming language other than
12 | Go.
13 | 
14 | **[Extensive Tutorial on Usage](https://github.com/hashicorp/go-plugin/blob/master/docs/extensive-go-plugin-tutorial.md).**
15 | A guide showing the usage and structure of go-plugin including a detailed
16 | walkthrough of setting up the plugins under the `examples` folder.


--------------------------------------------------------------------------------
/docs/guide-plugin-write-non-go.md:
--------------------------------------------------------------------------------
  1 | # Writing Plugins Without Go
  2 | 
  3 | This guide explains how to write a go-plugin compatible plugin using
  4 | a programming language other than Go. go-plugin supports plugins using
  5 | [gRPC](http://www.grpc.io). This makes it relatively simple to write plugins
  6 | using other languages!
  7 | 
  8 | Minimal knowledge about gRPC is assumed. We recommend reading the
  9 | [gRPC Go Tutorial](http://www.grpc.io/docs/tutorials/basic/go.html). This
 10 | alone is enough gRPC knowledge to continue.
 11 | 
 12 | This guide will implement the kv example in Python.
 13 | Full source code for the examples present in this guide
 14 | [is available in the examples/grpc folder](https://github.com/hashicorp/go-plugin/tree/master/examples/grpc).
 15 | 
 16 | ## 1. Implement the Service
 17 | 
 18 | The first step is to implement the gRPC server for the protocol buffers
 19 | service that your plugin defines. This is a standard gRPC server.
 20 | For the KV service, the service looks like this:
 21 | 
 22 | ```proto
 23 | service KV {
 24 |     rpc Get(GetRequest) returns (GetResponse);
 25 |     rpc Put(PutRequest) returns (Empty);
 26 | }
 27 | ```
 28 | 
 29 | We can implement that using Python as easily as:
 30 | 
 31 | ```python
 32 | class KVServicer(kv_pb2_grpc.KVServicer):
 33 |     """Implementation of KV service."""
 34 | 
 35 |     def Get(self, request, context):
 36 |         filename = "kv_"+request.key
 37 |         with open(filename, 'r') as f:
 38 |             result = kv_pb2.GetResponse()
 39 |             result.value = f.read()
 40 |             return result
 41 | 
 42 |     def Put(self, request, context):
 43 |         filename = "kv_"+request.key
 44 |         value = "{0}\n\nWritten from plugin-python".format(request.value)
 45 |         with open(filename, 'w') as f:
 46 |             f.write(value)
 47 | 
 48 |         return kv_pb2.Empty()
 49 | 
 50 | ```
 51 | 
 52 | Great! With that, we have a fully functioning implementation of the service.
 53 | You can test this using standard gRPC testing mechanisms.
 54 | 
 55 | ## 2. Serve the Service
 56 | 
 57 | Next, we need to create a gRPC server and serve the service we just made.
 58 | 
 59 | In Python:
 60 | 
 61 | ```python
 62 | # Make the server
 63 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
 64 | 
 65 | # Add our service
 66 | kv_pb2_grpc.add_KVServicer_to_server(KVServicer(), server)
 67 | 
 68 | # Listen on a port
 69 | server.add_insecure_port(':1234')
 70 | 
 71 | # Start
 72 | server.start()
 73 | ```
 74 | 
 75 | You can listen on any TCP address or Unix domain socket. go-plugin does
 76 | assume that connections are reliable (local), so you should not serve
 77 | your plugin across the network.
 78 | 
 79 | ## 3. Add the gRPC Health Checking Service
 80 | 
 81 | go-plugin requires the
 82 | [gRPC Health Checking Service](https://github.com/grpc/grpc/blob/master/doc/health-checking.md)
 83 | to be registered on your server. You must register the status of "plugin" to be SERVING.
 84 | 
 85 | The health checking service is used by go-plugin to determine if everything
 86 | is healthy with the connection. If you don't implement this service, your
 87 | process may be abruptly restarted and your plugins are likely to be unreliable.
 88 | 
 89 | ```
 90 | health = HealthServicer()
 91 | health.set("plugin", health_pb2.HealthCheckResponse.ServingStatus.Value('SERVING'))
 92 | health_pb2_grpc.add_HealthServicer_to_server(health, server)
 93 | ```
 94 | 
 95 | ## 4. Output Handshake Information
 96 | 
 97 | The final step is to output the handshake information to stdout. go-plugin
 98 | reads a single line from stdout to determine how to connect to your plugin,
 99 | what protocol it is using, etc.
100 | 
101 | 
102 | The structure is:
103 | 
104 | ```
105 | CORE-PROTOCOL-VERSION | APP-PROTOCOL-VERSION | NETWORK-TYPE | NETWORK-ADDR | PROTOCOL
106 | ```
107 | 
108 | Where:
109 | 
110 |   * `CORE-PROTOCOL-VERSION` is the protocol version for go-plugin itself.
111 |     The current value is `1`. Please use this value. Any other value will
112 |     cause your plugin to not load.
113 | 
114 |   * `APP-PROTOCOL-VERSION` is the protocol version for the application data.
115 |     This is determined by the application. You must reference the documentation
116 |     for your application to determine the desired value.
117 | 
118 |   * `NETWORK-TYPE` and `NETWORK-ADDR` are the networking information for
119 |     connecting to this plugin. The type must be "unix" or "tcp". The address
120 |     is a path to the Unix socket for "unix" and an IP address for "tcp".
121 | 
122 |   * `PROTOCOL` is the named protocol that the connection will use. If this
123 |     is omitted (older versions), this is "netrpc" for Go net/rpc. This can
124 |     also be "grpc". This is the protocol that the plugin wants to speak to
125 |     the host process with.
126 | 
127 | For our example that is:
128 | 
129 | ```
130 | 1|1|tcp|127.0.0.1:1234|grpc
131 | ```
132 | 
133 | The only element you'll have to be careful about is the second one (the
134 | `APP-PROTOCOL-VERISON`). This will depend on the application you're
135 | building a plugin for. Please reference their documentation for more
136 | information.
137 | 
138 | ## 5. Done!
139 | 
140 | And we're done!
141 | 
142 | Configure the host application (the application you're writing a plugin
143 | for) to execute your Python application. Configuring plugins is specific
144 | to the host application.
145 | 
146 | For our example, we used an environmental variable, and it looks like this:
147 | 
148 | ```sh
149 | $ export KV_PLUGIN="python plugin.py"
150 | ```
151 | 


--------------------------------------------------------------------------------
/docs/internals.md:
--------------------------------------------------------------------------------
 1 | # go-plugin Internals
 2 | 
 3 | This section discusses the internals of how go-plugin works.
 4 | 
 5 | go-plugin operates by either _serving_ a plugin or being a _client_
 6 | connecting to a remote plugin. The "client" is the host process or the
 7 | process that itself uses plugins. The "server" is the plugin process.
 8 | 
 9 | For a server:
10 | 
11 |   1. Output handshake to stdout
12 |   2. Wait for connection on control address
13 |   3. Serve plugins over control address
14 | 
15 | For a client:
16 | 
17 |   1. Launch a plugin binary
18 |   2. Read and verify handshake from plugin stdout
19 |   3. Connect to plugin control address using desired protocol
20 |   4. Dispense plugins using control connection
21 | 
22 | ## Handshake
23 | 
24 | The handshake is the initial communication between a plugin and a host
25 | process to determine how the host process can connect and communicate to
26 | the plugin. This handshake is done over the plugin process's stdout.
27 | 
28 | The `go-plugin` library itself handles the handshake when using the
29 | `Server` to serve a plugin. **You do not need to understand the internals
30 | of the handshake,** unless you're building a go-plugin compatible plugin
31 | in another language.
32 | 
33 | The handshake is a single line of data terminated with a newline character
34 | `\n`. It looks like the following:
35 | 
36 | ```
37 | 1|3|unix|/path/to/socket|grpc
38 | ```
39 | 
40 | The structure is:
41 | 
42 | ```
43 | CORE-PROTOCOL-VERSION | APP-PROTOCOL-VERSION | NETWORK-TYPE | NETWORK-ADDR | PROTOCOL
44 | ```
45 | 
46 | Where:
47 | 
48 |   * `CORE-PROTOCOL-VERSION` is the protocol version for go-plugin itself.
49 |     The current value is `1`. Please use this value. Any other value will
50 |     cause your plugin to not load.
51 | 
52 |   * `APP-PROTOCOL-VERSION` is the protocol version for the application data.
53 |     This is determined by the application. You must reference the documentation
54 |     for your application to determine the desired value.
55 | 
56 |   * `NETWORK-TYPE` and `NETWORK-ADDR` are the networking information for
57 |     connecting to this plugin. The type must be "unix" or "tcp". The address
58 |     is a path to the Unix socket for "unix" and an IP address for "tcp".
59 | 
60 |   * `PROTOCOL` is the named protocol that the connection will use. If this
61 |     is omitted (older versions), this is "netrpc" for Go net/rpc. This can
62 |     also be "grpc". This is the protocol that the plugin wants to speak to
63 |     the host process with.
64 | 
65 | ## Environment Variables
66 | 
67 | When serving a plugin over TCP, the following environment variables can be
68 | specified to restrict the port that will be assigned to be from within a
69 | specific range. If not values are provided, the port will be randomly assigned
70 | by the operating system.
71 | 
72 |  * `PLUGIN_MIN_PORT`: Specifies the minimum port value that will be assigned to the listener.
73 |  
74 |  * `PLUGIN_MAX_PORT`: Specifies the maximum port value that will be assigned to the listener.
75 | 


--------------------------------------------------------------------------------
/error.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | // This is a type that wraps error types so that they can be messaged
 7 | // across RPC channels. Since "error" is an interface, we can't always
 8 | // gob-encode the underlying structure. This is a valid error interface
 9 | // implementer that we will push across.
10 | type BasicError struct {
11 | 	Message string
12 | }
13 | 
14 | // NewBasicError is used to create a BasicError.
15 | //
16 | // err is allowed to be nil.
17 | func NewBasicError(err error) *BasicError {
18 | 	if err == nil {
19 | 		return nil
20 | 	}
21 | 
22 | 	return &BasicError{err.Error()}
23 | }
24 | 
25 | func (e *BasicError) Error() string {
26 | 	return e.Message
27 | }
28 | 


--------------------------------------------------------------------------------
/error_test.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | import (
 7 | 	"errors"
 8 | 	"testing"
 9 | )
10 | 
11 | func TestBasicError_ImplementsError(t *testing.T) {
12 | 	var _ error = new(BasicError)
13 | }
14 | 
15 | func TestBasicError_MatchesMessage(t *testing.T) {
16 | 	err := errors.New("foo")
17 | 	wrapped := NewBasicError(err)
18 | 
19 | 	if wrapped.Error() != err.Error() {
20 | 		t.Fatalf("bad: %#v", wrapped.Error())
21 | 	}
22 | }
23 | 
24 | func TestNewBasicError_nil(t *testing.T) {
25 | 	r := NewBasicError(nil)
26 | 	if r != nil {
27 | 		t.Fatalf("bad: %#v", r)
28 | 	}
29 | }
30 | 


--------------------------------------------------------------------------------
/examples/basic/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore binaries
2 | plugin/greeter
3 | basic


--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
 1 | Plugin Example
 2 | --------------
 3 | 
 4 | Compile the plugin itself via:
 5 | 
 6 |     go build -o ./plugin/greeter ./plugin/greeter_impl.go
 7 | 
 8 | Compile this driver via:
 9 | 
10 |     go build -o basic .
11 | 
12 | You can then launch the plugin sample via:
13 | 
14 |     ./basic
15 | 


--------------------------------------------------------------------------------
/examples/basic/main.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"log"
 9 | 	"os"
10 | 	"os/exec"
11 | 
12 | 	hclog "github.com/hashicorp/go-hclog"
13 | 	"github.com/hashicorp/go-plugin"
14 | 	"github.com/hashicorp/go-plugin/examples/basic/shared"
15 | )
16 | 
17 | func main() {
18 | 	// Create an hclog.Logger
19 | 	logger := hclog.New(&hclog.LoggerOptions{
20 | 		Name:   "plugin",
21 | 		Output: os.Stdout,
22 | 		Level:  hclog.Debug,
23 | 	})
24 | 
25 | 	// We're a host! Start by launching the plugin process.
26 | 	client := plugin.NewClient(&plugin.ClientConfig{
27 | 		HandshakeConfig: handshakeConfig,
28 | 		Plugins:         pluginMap,
29 | 		Cmd:             exec.Command("./plugin/greeter"),
30 | 		Logger:          logger,
31 | 	})
32 | 	defer client.Kill()
33 | 
34 | 	// Connect via RPC
35 | 	rpcClient, err := client.Client()
36 | 	if err != nil {
37 | 		log.Fatal(err)
38 | 	}
39 | 
40 | 	// Request the plugin
41 | 	raw, err := rpcClient.Dispense("greeter")
42 | 	if err != nil {
43 | 		log.Fatal(err)
44 | 	}
45 | 
46 | 	// We should have a Greeter now! This feels like a normal interface
47 | 	// implementation but is in fact over an RPC connection.
48 | 	greeter := raw.(shared.Greeter)
49 | 	fmt.Println(greeter.Greet())
50 | }
51 | 
52 | // handshakeConfigs are used to just do a basic handshake between
53 | // a plugin and host. If the handshake fails, a user friendly error is shown.
54 | // This prevents users from executing bad plugins or executing a plugin
55 | // directory. It is a UX feature, not a security feature.
56 | var handshakeConfig = plugin.HandshakeConfig{
57 | 	ProtocolVersion:  1,
58 | 	MagicCookieKey:   "BASIC_PLUGIN",
59 | 	MagicCookieValue: "hello",
60 | }
61 | 
62 | // pluginMap is the map of plugins we can dispense.
63 | var pluginMap = map[string]plugin.Plugin{
64 | 	"greeter": &shared.GreeterPlugin{},
65 | }
66 | 


--------------------------------------------------------------------------------
/examples/basic/plugin/greeter_impl.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import (
 7 | 	"os"
 8 | 
 9 | 	"github.com/hashicorp/go-hclog"
10 | 	"github.com/hashicorp/go-plugin"
11 | 	"github.com/hashicorp/go-plugin/examples/basic/shared"
12 | )
13 | 
14 | // Here is a real implementation of Greeter
15 | type GreeterHello struct {
16 | 	logger hclog.Logger
17 | }
18 | 
19 | func (g *GreeterHello) Greet() string {
20 | 	g.logger.Debug("message from GreeterHello.Greet")
21 | 	return "Hello!"
22 | }
23 | 
24 | // handshakeConfigs are used to just do a basic handshake between
25 | // a plugin and host. If the handshake fails, a user friendly error is shown.
26 | // This prevents users from executing bad plugins or executing a plugin
27 | // directory. It is a UX feature, not a security feature.
28 | var handshakeConfig = plugin.HandshakeConfig{
29 | 	ProtocolVersion:  1,
30 | 	MagicCookieKey:   "BASIC_PLUGIN",
31 | 	MagicCookieValue: "hello",
32 | }
33 | 
34 | func main() {
35 | 	logger := hclog.New(&hclog.LoggerOptions{
36 | 		Level:      hclog.Trace,
37 | 		Output:     os.Stderr,
38 | 		JSONFormat: true,
39 | 	})
40 | 
41 | 	greeter := &GreeterHello{
42 | 		logger: logger,
43 | 	}
44 | 	// pluginMap is the map of plugins we can dispense.
45 | 	var pluginMap = map[string]plugin.Plugin{
46 | 		"greeter": &shared.GreeterPlugin{Impl: greeter},
47 | 	}
48 | 
49 | 	logger.Debug("message from plugin", "foo", "bar")
50 | 
51 | 	plugin.Serve(&plugin.ServeConfig{
52 | 		HandshakeConfig: handshakeConfig,
53 | 		Plugins:         pluginMap,
54 | 	})
55 | }
56 | 


--------------------------------------------------------------------------------
/examples/basic/shared/greeter_interface.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package shared
 5 | 
 6 | import (
 7 | 	"net/rpc"
 8 | 
 9 | 	"github.com/hashicorp/go-plugin"
10 | )
11 | 
12 | // Greeter is the interface that we're exposing as a plugin.
13 | type Greeter interface {
14 | 	Greet() string
15 | }
16 | 
17 | // Here is an implementation that talks over RPC
18 | type GreeterRPC struct{ client *rpc.Client }
19 | 
20 | func (g *GreeterRPC) Greet() string {
21 | 	var resp string
22 | 	err := g.client.Call("Plugin.Greet", new(interface{}), &resp)
23 | 	if err != nil {
24 | 		// You usually want your interfaces to return errors. If they don't,
25 | 		// there isn't much other choice here.
26 | 		panic(err)
27 | 	}
28 | 
29 | 	return resp
30 | }
31 | 
32 | // Here is the RPC server that GreeterRPC talks to, conforming to
33 | // the requirements of net/rpc
34 | type GreeterRPCServer struct {
35 | 	// This is the real implementation
36 | 	Impl Greeter
37 | }
38 | 
39 | func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error {
40 | 	*resp = s.Impl.Greet()
41 | 	return nil
42 | }
43 | 
44 | // This is the implementation of plugin.Plugin so we can serve/consume this
45 | //
46 | // This has two methods: Server must return an RPC server for this plugin
47 | // type. We construct a GreeterRPCServer for this.
48 | //
49 | // Client must return an implementation of our interface that communicates
50 | // over an RPC client. We return GreeterRPC for this.
51 | //
52 | // Ignore MuxBroker. That is used to create more multiplexed streams on our
53 | // plugin connection and is a more advanced use case.
54 | type GreeterPlugin struct {
55 | 	// Impl Injection
56 | 	Impl Greeter
57 | }
58 | 
59 | func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
60 | 	return &GreeterRPCServer{Impl: p.Impl}, nil
61 | }
62 | 
63 | func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
64 | 	return &GreeterRPC{client: c}, nil
65 | }
66 | 


--------------------------------------------------------------------------------
/examples/bidirectional/README.md:
--------------------------------------------------------------------------------
 1 | # Counter Example
 2 | 
 3 | This example builds a simple key/counter store CLI where the mechanism
 4 | for storing and retrieving keys is pluggable. However, in this example we don't
 5 | trust the plugin to do the summation work. We use bi-directional plugins to
 6 | call back into the main proccess to do the sum of two numbers. To build this example:
 7 | 
 8 | ```sh
 9 | # This builds the main CLI
10 | $ go build -o counter
11 | 
12 | # This builds the plugin written in Go
13 | $ go build -o counter-go-grpc ./plugin-go-grpc
14 | 
15 | # This tells the Counter binary to use the "counter-go-grpc" binary
16 | $ export COUNTER_PLUGIN="./counter-go-grpc"
17 | 
18 | # Read and write
19 | $ ./counter put hello 1
20 | $ ./counter put hello 1
21 | 
22 | $ ./counter get hello
23 | 2
24 | ```
25 | 
26 | ### Plugin: plugin-go-grpc
27 | 
28 | This plugin uses gRPC to serve a plugin that is written in Go:
29 | 
30 | ```
31 | # This builds the plugin written in Go
32 | $ go build -o counter-go-grpc ./plugin-go-grpc
33 | 
34 | # This tells the KV binary to use the "kv-go-grpc" binary
35 | $ export COUNTER_PLUGIN="./counter-go-grpc"
36 | ```
37 | 
38 | ## Updating the Protocol
39 | 
40 | If you update the protocol buffers file, you can regenerate the file
41 | using the following command from this directory. You do not need to run
42 | this if you're just trying the example.
43 | 
44 | ```sh
45 | $ buf generate
46 | ```
47 | 


--------------------------------------------------------------------------------
/examples/bidirectional/buf.gen.yaml:
--------------------------------------------------------------------------------
 1 | # Copyright (c) HashiCorp, Inc.
 2 | # SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | version: v1
 5 | plugins:
 6 |   - plugin: buf.build/protocolbuffers/go
 7 |     out: .
 8 |     opt:
 9 |       - paths=source_relative
10 |   - plugin: buf.build/grpc/go:v1.3.0
11 |     out: .
12 |     opt:
13 |       - paths=source_relative
14 |       - require_unimplemented_servers=false
15 | 


--------------------------------------------------------------------------------
/examples/bidirectional/buf.yaml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 | 
4 | version: v1
5 | 


--------------------------------------------------------------------------------
/examples/bidirectional/main.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"io"
 9 | 	"log"
10 | 	"os"
11 | 	"os/exec"
12 | 	"strconv"
13 | 
14 | 	"github.com/hashicorp/go-plugin"
15 | 	"github.com/hashicorp/go-plugin/examples/bidirectional/shared"
16 | )
17 | 
18 | type addHelper struct{}
19 | 
20 | func (*addHelper) Sum(a, b int64) (int64, error) {
21 | 	return a + b, nil
22 | }
23 | 
24 | func main() {
25 | 	// We don't want to see the plugin logs.
26 | 	log.SetOutput(io.Discard)
27 | 
28 | 	// We're a host. Start by launching the plugin process.
29 | 	client := plugin.NewClient(&plugin.ClientConfig{
30 | 		HandshakeConfig: shared.Handshake,
31 | 		Plugins:         shared.PluginMap,
32 | 		Cmd:             exec.Command("sh", "-c", os.Getenv("COUNTER_PLUGIN")),
33 | 		AllowedProtocols: []plugin.Protocol{
34 | 			plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
35 | 	})
36 | 	defer client.Kill()
37 | 
38 | 	// Connect via RPC
39 | 	rpcClient, err := client.Client()
40 | 	if err != nil {
41 | 		fmt.Println("Error:", err.Error())
42 | 		os.Exit(1)
43 | 	}
44 | 
45 | 	// Request the plugin
46 | 	raw, err := rpcClient.Dispense("counter")
47 | 	if err != nil {
48 | 		fmt.Println("Error:", err.Error())
49 | 		os.Exit(1)
50 | 	}
51 | 
52 | 	// We should have a Counter store now! This feels like a normal interface
53 | 	// implementation but is in fact over an RPC connection.
54 | 	counter := raw.(shared.Counter)
55 | 
56 | 	os.Args = os.Args[1:]
57 | 	switch os.Args[0] {
58 | 	case "get":
59 | 		result, err := counter.Get(os.Args[1])
60 | 		if err != nil {
61 | 			fmt.Println("Error:", err.Error())
62 | 			os.Exit(1)
63 | 		}
64 | 
65 | 		fmt.Println(result)
66 | 
67 | 	case "put":
68 | 		i, err := strconv.Atoi(os.Args[2])
69 | 		if err != nil {
70 | 			fmt.Println("Error:", err.Error())
71 | 			os.Exit(1)
72 | 		}
73 | 
74 | 		err = counter.Put(os.Args[1], int64(i), &addHelper{})
75 | 		if err != nil {
76 | 			fmt.Println("Error:", err.Error())
77 | 			os.Exit(1)
78 | 		}
79 | 
80 | 	default:
81 | 		fmt.Println("Please only use 'get' or 'put'")
82 | 		os.Exit(1)
83 | 	}
84 | }
85 | 


--------------------------------------------------------------------------------
/examples/bidirectional/plugin-go-grpc/main.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import (
 7 | 	"encoding/json"
 8 | 	"os"
 9 | 
10 | 	"github.com/hashicorp/go-plugin"
11 | 	"github.com/hashicorp/go-plugin/examples/bidirectional/shared"
12 | )
13 | 
14 | // Here is a real implementation of KV that writes to a local file with
15 | // the key name and the contents are the value of the key.
16 | type Counter struct {
17 | }
18 | 
19 | type data struct {
20 | 	Value int64
21 | }
22 | 
23 | func (k *Counter) Put(key string, value int64, a shared.AddHelper) error {
24 | 	v, _ := k.Get(key)
25 | 
26 | 	r, err := a.Sum(v, value)
27 | 	if err != nil {
28 | 		return err
29 | 	}
30 | 
31 | 	buf, err := json.Marshal(&data{r})
32 | 	if err != nil {
33 | 		return err
34 | 	}
35 | 
36 | 	return os.WriteFile("kv_"+key, buf, 0644)
37 | }
38 | 
39 | func (k *Counter) Get(key string) (int64, error) {
40 | 	dataRaw, err := os.ReadFile("kv_" + key)
41 | 	if err != nil {
42 | 		return 0, err
43 | 	}
44 | 
45 | 	data := &data{}
46 | 	err = json.Unmarshal(dataRaw, data)
47 | 	if err != nil {
48 | 		return 0, err
49 | 	}
50 | 
51 | 	return data.Value, nil
52 | }
53 | 
54 | func main() {
55 | 	plugin.Serve(&plugin.ServeConfig{
56 | 		HandshakeConfig: shared.Handshake,
57 | 		Plugins: map[string]plugin.Plugin{
58 | 			"counter": &shared.CounterPlugin{Impl: &Counter{}},
59 | 		},
60 | 
61 | 		// A non-nil value here enables gRPC serving for this plugin...
62 | 		GRPCServer: plugin.DefaultGRPCServer,
63 | 	})
64 | }
65 | 


--------------------------------------------------------------------------------
/examples/bidirectional/proto/kv.proto:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | syntax = "proto3";
 5 | package proto;
 6 | option go_package = "./proto";
 7 | 
 8 | message GetRequest {
 9 |     string key = 1;
10 | }
11 | 
12 | message GetResponse {
13 |     int64 value = 1;
14 | }
15 | 
16 | message PutRequest {
17 |     uint32 add_server = 1;
18 |     string key = 2;
19 |     int64 value = 3;
20 | }
21 | 
22 | message Empty {}
23 | 
24 | message SumRequest {
25 |     int64 a = 1;
26 |     int64 b = 2;
27 | }
28 | 
29 | message SumResponse {
30 |     int64 r = 1;
31 | }
32 | 
33 | service Counter {
34 |     rpc Get(GetRequest) returns (GetResponse);
35 |     rpc Put(PutRequest) returns (Empty);
36 | }
37 | 
38 | service AddHelper {
39 |     rpc Sum(SumRequest) returns (SumResponse);
40 | }
41 | 


--------------------------------------------------------------------------------
/examples/bidirectional/proto/kv_grpc.pb.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
  5 | // versions:
  6 | // - protoc-gen-go-grpc v1.3.0
  7 | // - protoc             (unknown)
  8 | // source: proto/kv.proto
  9 | 
 10 | package proto
 11 | 
 12 | import (
 13 | 	context "context"
 14 | 	grpc "google.golang.org/grpc"
 15 | 	codes "google.golang.org/grpc/codes"
 16 | 	status "google.golang.org/grpc/status"
 17 | )
 18 | 
 19 | // This is a compile-time assertion to ensure that this generated file
 20 | // is compatible with the grpc package it is being compiled against.
 21 | // Requires gRPC-Go v1.32.0 or later.
 22 | const _ = grpc.SupportPackageIsVersion7
 23 | 
 24 | const (
 25 | 	Counter_Get_FullMethodName = "/proto.Counter/Get"
 26 | 	Counter_Put_FullMethodName = "/proto.Counter/Put"
 27 | )
 28 | 
 29 | // CounterClient is the client API for Counter service.
 30 | //
 31 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
 32 | type CounterClient interface {
 33 | 	Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error)
 34 | 	Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*Empty, error)
 35 | }
 36 | 
 37 | type counterClient struct {
 38 | 	cc grpc.ClientConnInterface
 39 | }
 40 | 
 41 | func NewCounterClient(cc grpc.ClientConnInterface) CounterClient {
 42 | 	return &counterClient{cc}
 43 | }
 44 | 
 45 | func (c *counterClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) {
 46 | 	out := new(GetResponse)
 47 | 	err := c.cc.Invoke(ctx, Counter_Get_FullMethodName, in, out, opts...)
 48 | 	if err != nil {
 49 | 		return nil, err
 50 | 	}
 51 | 	return out, nil
 52 | }
 53 | 
 54 | func (c *counterClient) Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*Empty, error) {
 55 | 	out := new(Empty)
 56 | 	err := c.cc.Invoke(ctx, Counter_Put_FullMethodName, in, out, opts...)
 57 | 	if err != nil {
 58 | 		return nil, err
 59 | 	}
 60 | 	return out, nil
 61 | }
 62 | 
 63 | // CounterServer is the server API for Counter service.
 64 | // All implementations should embed UnimplementedCounterServer
 65 | // for forward compatibility
 66 | type CounterServer interface {
 67 | 	Get(context.Context, *GetRequest) (*GetResponse, error)
 68 | 	Put(context.Context, *PutRequest) (*Empty, error)
 69 | }
 70 | 
 71 | // UnimplementedCounterServer should be embedded to have forward compatible implementations.
 72 | type UnimplementedCounterServer struct {
 73 | }
 74 | 
 75 | func (UnimplementedCounterServer) Get(context.Context, *GetRequest) (*GetResponse, error) {
 76 | 	return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
 77 | }
 78 | func (UnimplementedCounterServer) Put(context.Context, *PutRequest) (*Empty, error) {
 79 | 	return nil, status.Errorf(codes.Unimplemented, "method Put not implemented")
 80 | }
 81 | 
 82 | // UnsafeCounterServer may be embedded to opt out of forward compatibility for this service.
 83 | // Use of this interface is not recommended, as added methods to CounterServer will
 84 | // result in compilation errors.
 85 | type UnsafeCounterServer interface {
 86 | 	mustEmbedUnimplementedCounterServer()
 87 | }
 88 | 
 89 | func RegisterCounterServer(s grpc.ServiceRegistrar, srv CounterServer) {
 90 | 	s.RegisterService(&Counter_ServiceDesc, srv)
 91 | }
 92 | 
 93 | func _Counter_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 94 | 	in := new(GetRequest)
 95 | 	if err := dec(in); err != nil {
 96 | 		return nil, err
 97 | 	}
 98 | 	if interceptor == nil {
 99 | 		return srv.(CounterServer).Get(ctx, in)
100 | 	}
101 | 	info := &grpc.UnaryServerInfo{
102 | 		Server:     srv,
103 | 		FullMethod: Counter_Get_FullMethodName,
104 | 	}
105 | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
106 | 		return srv.(CounterServer).Get(ctx, req.(*GetRequest))
107 | 	}
108 | 	return interceptor(ctx, in, info, handler)
109 | }
110 | 
111 | func _Counter_Put_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
112 | 	in := new(PutRequest)
113 | 	if err := dec(in); err != nil {
114 | 		return nil, err
115 | 	}
116 | 	if interceptor == nil {
117 | 		return srv.(CounterServer).Put(ctx, in)
118 | 	}
119 | 	info := &grpc.UnaryServerInfo{
120 | 		Server:     srv,
121 | 		FullMethod: Counter_Put_FullMethodName,
122 | 	}
123 | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
124 | 		return srv.(CounterServer).Put(ctx, req.(*PutRequest))
125 | 	}
126 | 	return interceptor(ctx, in, info, handler)
127 | }
128 | 
129 | // Counter_ServiceDesc is the grpc.ServiceDesc for Counter service.
130 | // It's only intended for direct use with grpc.RegisterService,
131 | // and not to be introspected or modified (even as a copy)
132 | var Counter_ServiceDesc = grpc.ServiceDesc{
133 | 	ServiceName: "proto.Counter",
134 | 	HandlerType: (*CounterServer)(nil),
135 | 	Methods: []grpc.MethodDesc{
136 | 		{
137 | 			MethodName: "Get",
138 | 			Handler:    _Counter_Get_Handler,
139 | 		},
140 | 		{
141 | 			MethodName: "Put",
142 | 			Handler:    _Counter_Put_Handler,
143 | 		},
144 | 	},
145 | 	Streams:  []grpc.StreamDesc{},
146 | 	Metadata: "proto/kv.proto",
147 | }
148 | 
149 | const (
150 | 	AddHelper_Sum_FullMethodName = "/proto.AddHelper/Sum"
151 | )
152 | 
153 | // AddHelperClient is the client API for AddHelper service.
154 | //
155 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
156 | type AddHelperClient interface {
157 | 	Sum(ctx context.Context, in *SumRequest, opts ...grpc.CallOption) (*SumResponse, error)
158 | }
159 | 
160 | type addHelperClient struct {
161 | 	cc grpc.ClientConnInterface
162 | }
163 | 
164 | func NewAddHelperClient(cc grpc.ClientConnInterface) AddHelperClient {
165 | 	return &addHelperClient{cc}
166 | }
167 | 
168 | func (c *addHelperClient) Sum(ctx context.Context, in *SumRequest, opts ...grpc.CallOption) (*SumResponse, error) {
169 | 	out := new(SumResponse)
170 | 	err := c.cc.Invoke(ctx, AddHelper_Sum_FullMethodName, in, out, opts...)
171 | 	if err != nil {
172 | 		return nil, err
173 | 	}
174 | 	return out, nil
175 | }
176 | 
177 | // AddHelperServer is the server API for AddHelper service.
178 | // All implementations should embed UnimplementedAddHelperServer
179 | // for forward compatibility
180 | type AddHelperServer interface {
181 | 	Sum(context.Context, *SumRequest) (*SumResponse, error)
182 | }
183 | 
184 | // UnimplementedAddHelperServer should be embedded to have forward compatible implementations.
185 | type UnimplementedAddHelperServer struct {
186 | }
187 | 
188 | func (UnimplementedAddHelperServer) Sum(context.Context, *SumRequest) (*SumResponse, error) {
189 | 	return nil, status.Errorf(codes.Unimplemented, "method Sum not implemented")
190 | }
191 | 
192 | // UnsafeAddHelperServer may be embedded to opt out of forward compatibility for this service.
193 | // Use of this interface is not recommended, as added methods to AddHelperServer will
194 | // result in compilation errors.
195 | type UnsafeAddHelperServer interface {
196 | 	mustEmbedUnimplementedAddHelperServer()
197 | }
198 | 
199 | func RegisterAddHelperServer(s grpc.ServiceRegistrar, srv AddHelperServer) {
200 | 	s.RegisterService(&AddHelper_ServiceDesc, srv)
201 | }
202 | 
203 | func _AddHelper_Sum_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
204 | 	in := new(SumRequest)
205 | 	if err := dec(in); err != nil {
206 | 		return nil, err
207 | 	}
208 | 	if interceptor == nil {
209 | 		return srv.(AddHelperServer).Sum(ctx, in)
210 | 	}
211 | 	info := &grpc.UnaryServerInfo{
212 | 		Server:     srv,
213 | 		FullMethod: AddHelper_Sum_FullMethodName,
214 | 	}
215 | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
216 | 		return srv.(AddHelperServer).Sum(ctx, req.(*SumRequest))
217 | 	}
218 | 	return interceptor(ctx, in, info, handler)
219 | }
220 | 
221 | // AddHelper_ServiceDesc is the grpc.ServiceDesc for AddHelper service.
222 | // It's only intended for direct use with grpc.RegisterService,
223 | // and not to be introspected or modified (even as a copy)
224 | var AddHelper_ServiceDesc = grpc.ServiceDesc{
225 | 	ServiceName: "proto.AddHelper",
226 | 	HandlerType: (*AddHelperServer)(nil),
227 | 	Methods: []grpc.MethodDesc{
228 | 		{
229 | 			MethodName: "Sum",
230 | 			Handler:    _AddHelper_Sum_Handler,
231 | 		},
232 | 	},
233 | 	Streams:  []grpc.StreamDesc{},
234 | 	Metadata: "proto/kv.proto",
235 | }
236 | 


--------------------------------------------------------------------------------
/examples/bidirectional/shared/grpc.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package shared
  5 | 
  6 | import (
  7 | 	"context"
  8 | 
  9 | 	hclog "github.com/hashicorp/go-hclog"
 10 | 	plugin "github.com/hashicorp/go-plugin"
 11 | 	"github.com/hashicorp/go-plugin/examples/bidirectional/proto"
 12 | 	"google.golang.org/grpc"
 13 | )
 14 | 
 15 | // GRPCClient is an implementation of KV that talks over RPC.
 16 | type GRPCClient struct {
 17 | 	broker *plugin.GRPCBroker
 18 | 	client proto.CounterClient
 19 | }
 20 | 
 21 | func (m *GRPCClient) Put(key string, value int64, a AddHelper) error {
 22 | 	addHelperServer := &GRPCAddHelperServer{Impl: a}
 23 | 
 24 | 	var s *grpc.Server
 25 | 	serverFunc := func(opts []grpc.ServerOption) *grpc.Server {
 26 | 		s = grpc.NewServer(opts...)
 27 | 		proto.RegisterAddHelperServer(s, addHelperServer)
 28 | 
 29 | 		return s
 30 | 	}
 31 | 
 32 | 	brokerID := m.broker.NextId()
 33 | 	go m.broker.AcceptAndServe(brokerID, serverFunc)
 34 | 
 35 | 	_, err := m.client.Put(context.Background(), &proto.PutRequest{
 36 | 		AddServer: brokerID,
 37 | 		Key:       key,
 38 | 		Value:     value,
 39 | 	})
 40 | 
 41 | 	s.Stop()
 42 | 	return err
 43 | }
 44 | 
 45 | func (m *GRPCClient) Get(key string) (int64, error) {
 46 | 	resp, err := m.client.Get(context.Background(), &proto.GetRequest{
 47 | 		Key: key,
 48 | 	})
 49 | 	if err != nil {
 50 | 		return 0, err
 51 | 	}
 52 | 
 53 | 	return resp.Value, nil
 54 | }
 55 | 
 56 | // Here is the gRPC server that GRPCClient talks to.
 57 | type GRPCServer struct {
 58 | 	// This is the real implementation
 59 | 	Impl Counter
 60 | 
 61 | 	broker *plugin.GRPCBroker
 62 | }
 63 | 
 64 | func (m *GRPCServer) Put(ctx context.Context, req *proto.PutRequest) (*proto.Empty, error) {
 65 | 	conn, err := m.broker.Dial(req.AddServer)
 66 | 	if err != nil {
 67 | 		return nil, err
 68 | 	}
 69 | 	defer func() { _ = conn.Close() }()
 70 | 
 71 | 	a := &GRPCAddHelperClient{proto.NewAddHelperClient(conn)}
 72 | 	return &proto.Empty{}, m.Impl.Put(req.Key, req.Value, a)
 73 | }
 74 | 
 75 | func (m *GRPCServer) Get(ctx context.Context, req *proto.GetRequest) (*proto.GetResponse, error) {
 76 | 	v, err := m.Impl.Get(req.Key)
 77 | 	return &proto.GetResponse{Value: v}, err
 78 | }
 79 | 
 80 | // GRPCClient is an implementation of KV that talks over RPC.
 81 | type GRPCAddHelperClient struct{ client proto.AddHelperClient }
 82 | 
 83 | func (m *GRPCAddHelperClient) Sum(a, b int64) (int64, error) {
 84 | 	resp, err := m.client.Sum(context.Background(), &proto.SumRequest{
 85 | 		A: a,
 86 | 		B: b,
 87 | 	})
 88 | 	if err != nil {
 89 | 		hclog.Default().Info("add.Sum", "client", "start", "err", err)
 90 | 		return 0, err
 91 | 	}
 92 | 	return resp.R, err
 93 | }
 94 | 
 95 | // Here is the gRPC server that GRPCClient talks to.
 96 | type GRPCAddHelperServer struct {
 97 | 	// This is the real implementation
 98 | 	Impl AddHelper
 99 | }
100 | 
101 | func (m *GRPCAddHelperServer) Sum(ctx context.Context, req *proto.SumRequest) (resp *proto.SumResponse, err error) {
102 | 	r, err := m.Impl.Sum(req.A, req.B)
103 | 	if err != nil {
104 | 		return nil, err
105 | 	}
106 | 	return &proto.SumResponse{R: r}, err
107 | }
108 | 


--------------------------------------------------------------------------------
/examples/bidirectional/shared/interface.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | // Package shared contains shared data between the host and plugins.
 5 | package shared
 6 | 
 7 | import (
 8 | 	"context"
 9 | 
10 | 	"google.golang.org/grpc"
11 | 
12 | 	"github.com/hashicorp/go-plugin"
13 | 	"github.com/hashicorp/go-plugin/examples/bidirectional/proto"
14 | )
15 | 
16 | // Handshake is a common handshake that is shared by plugin and host.
17 | var Handshake = plugin.HandshakeConfig{
18 | 	ProtocolVersion:  1,
19 | 	MagicCookieKey:   "BASIC_PLUGIN",
20 | 	MagicCookieValue: "hello",
21 | }
22 | 
23 | // PluginMap is the map of plugins we can dispense.
24 | var PluginMap = map[string]plugin.Plugin{
25 | 	"counter": &CounterPlugin{},
26 | }
27 | 
28 | type AddHelper interface {
29 | 	Sum(int64, int64) (int64, error)
30 | }
31 | 
32 | // KV is the interface that we're exposing as a plugin.
33 | type Counter interface {
34 | 	Put(key string, value int64, a AddHelper) error
35 | 	Get(key string) (int64, error)
36 | }
37 | 
38 | // This is the implementation of plugin.Plugin so we can serve/consume this.
39 | // We also implement GRPCPlugin so that this plugin can be served over
40 | // gRPC.
41 | type CounterPlugin struct {
42 | 	plugin.NetRPCUnsupportedPlugin
43 | 	// Concrete implementation, written in Go. This is only used for plugins
44 | 	// that are written in Go.
45 | 	Impl Counter
46 | }
47 | 
48 | func (p *CounterPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
49 | 	proto.RegisterCounterServer(s, &GRPCServer{
50 | 		Impl:   p.Impl,
51 | 		broker: broker,
52 | 	})
53 | 	return nil
54 | }
55 | 
56 | func (p *CounterPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
57 | 	return &GRPCClient{
58 | 		client: proto.NewCounterClient(c),
59 | 		broker: broker,
60 | 	}, nil
61 | }
62 | 
63 | var _ plugin.GRPCPlugin = &CounterPlugin{}
64 | 


--------------------------------------------------------------------------------
/examples/grpc/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | /kv
3 | /kv-*
4 | /kv_*
5 | 


--------------------------------------------------------------------------------
/examples/grpc/README.md:
--------------------------------------------------------------------------------
 1 | # KV Example
 2 | 
 3 | This example builds a simple key/value store CLI where the mechanism
 4 | for storing and retrieving keys is pluggable. To build this example:
 5 | 
 6 | ```sh
 7 | # This builds the main CLI
 8 | $ go build -o kv
 9 | 
10 | # This builds the plugin written in Go
11 | $ go build -o kv-go-grpc ./plugin-go-grpc
12 | 
13 | # This tells the KV binary to use the "kv-go-grpc" binary
14 | $ export KV_PLUGIN="./kv-go-grpc"
15 | 
16 | # Read and write
17 | $ ./kv put hello world
18 | 
19 | $ ./kv get hello
20 | world
21 | ```
22 | 
23 | ### Plugin: plugin-go-grpc
24 | 
25 | This plugin uses gRPC to serve a plugin that is written in Go:
26 | 
27 | ```
28 | # This builds the plugin written in Go
29 | $ go build -o kv-go-grpc ./plugin-go-grpc
30 | 
31 | # This tells the KV binary to use the "kv-go-grpc" binary
32 | $ export KV_PLUGIN="./kv-go-grpc"
33 | ```
34 | 
35 | ### Plugin: plugin-go-netrpc
36 | 
37 | This plugin uses the builtin Go net/rpc mechanism to serve the plugin:
38 | 
39 | ```
40 | # This builds the plugin written in Go
41 | $ go build -o kv-go-netrpc ./plugin-go-netrpc
42 | 
43 | # This tells the KV binary to use the "kv-go-netrpc" binary
44 | $ export KV_PLUGIN="./kv-go-netrpc"
45 | ```
46 | 
47 | ### Plugin: plugin-python
48 | 
49 | This plugin is written in Python:
50 | 
51 | ```
52 | $ python -m venv plugin-python/.venv
53 | $ source plugin-python/.venv/bin/activate
54 | $ pip install -r plugin-python/requirements.txt
55 | $ export KV_PLUGIN="python plugin-python/plugin.py"
56 | ```
57 | 
58 | ## Updating the Protocol
59 | 
60 | If you update the protocol buffers file, you can regenerate the file
61 | using the following command from this directory. You do not need to run
62 | this if you're just trying the example.
63 | 
64 | ```sh
65 | $ buf generate
66 | ```
67 | 


--------------------------------------------------------------------------------
/examples/grpc/buf.gen.yaml:
--------------------------------------------------------------------------------
 1 | # Copyright (c) HashiCorp, Inc.
 2 | # SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | version: v1
 5 | plugins:
 6 |   - plugin: buf.build/protocolbuffers/go
 7 |     out: .
 8 |     opt:
 9 |       - paths=source_relative
10 |   - plugin: buf.build/grpc/go:v1.3.0
11 |     out: .
12 |     opt:
13 |       - paths=source_relative
14 |       - require_unimplemented_servers=false
15 |   - plugin: buf.build/protocolbuffers/python:v24.4
16 |     out: plugin-python
17 |   - plugin: buf.build/grpc/python:v1.58.1
18 |     out: plugin-python
19 | 


--------------------------------------------------------------------------------
/examples/grpc/buf.yaml:
--------------------------------------------------------------------------------
1 | # Copyright (c) HashiCorp, Inc.
2 | # SPDX-License-Identifier: MPL-2.0
3 | 
4 | version: v1
5 | 


--------------------------------------------------------------------------------
/examples/grpc/main.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"io"
 9 | 	"log"
10 | 	"os"
11 | 	"os/exec"
12 | 
13 | 	"github.com/hashicorp/go-plugin"
14 | 	"github.com/hashicorp/go-plugin/examples/grpc/shared"
15 | )
16 | 
17 | func run() error {
18 | 	// We're a host. Start by launching the plugin process.
19 | 	client := plugin.NewClient(&plugin.ClientConfig{
20 | 		HandshakeConfig: shared.Handshake,
21 | 		Plugins:         shared.PluginMap,
22 | 		Cmd:             exec.Command("sh", "-c", os.Getenv("KV_PLUGIN")),
23 | 		AllowedProtocols: []plugin.Protocol{
24 | 			plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
25 | 	})
26 | 	defer client.Kill()
27 | 
28 | 	// Connect via RPC
29 | 	rpcClient, err := client.Client()
30 | 	if err != nil {
31 | 		return err
32 | 	}
33 | 
34 | 	// Request the plugin
35 | 	raw, err := rpcClient.Dispense("kv_grpc")
36 | 	if err != nil {
37 | 		return err
38 | 	}
39 | 
40 | 	// We should have a KV store now! This feels like a normal interface
41 | 	// implementation but is in fact over an RPC connection.
42 | 	kv := raw.(shared.KV)
43 | 	os.Args = os.Args[1:]
44 | 	switch os.Args[0] {
45 | 	case "get":
46 | 		result, err := kv.Get(os.Args[1])
47 | 		if err != nil {
48 | 			return err
49 | 		}
50 | 
51 | 		fmt.Println(string(result))
52 | 
53 | 	case "put":
54 | 		err := kv.Put(os.Args[1], []byte(os.Args[2]))
55 | 		if err != nil {
56 | 			return err
57 | 		}
58 | 
59 | 	default:
60 | 		return fmt.Errorf("please only use 'get' or 'put', given: %q", os.Args[0])
61 | 	}
62 | 
63 | 	return nil
64 | }
65 | 
66 | func main() {
67 | 	// We don't want to see the plugin logs.
68 | 	log.SetOutput(io.Discard)
69 | 
70 | 	if err := run(); err != nil {
71 | 		fmt.Printf("error: %+v\n", err)
72 | 		os.Exit(1)
73 | 	}
74 | 
75 | 	os.Exit(0)
76 | }
77 | 


--------------------------------------------------------------------------------
/examples/grpc/plugin-go-grpc/main.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"os"
 9 | 
10 | 	"github.com/hashicorp/go-plugin"
11 | 	"github.com/hashicorp/go-plugin/examples/grpc/shared"
12 | )
13 | 
14 | // Here is a real implementation of KV that writes to a local file with
15 | // the key name and the contents are the value of the key.
16 | type KV struct{}
17 | 
18 | func (KV) Put(key string, value []byte) error {
19 | 	value = []byte(fmt.Sprintf("%s\n\nWritten from plugin-go-grpc", string(value)))
20 | 	return os.WriteFile("kv_"+key, value, 0644)
21 | }
22 | 
23 | func (KV) Get(key string) ([]byte, error) {
24 | 	return os.ReadFile("kv_" + key)
25 | }
26 | 
27 | func main() {
28 | 	plugin.Serve(&plugin.ServeConfig{
29 | 		HandshakeConfig: shared.Handshake,
30 | 		Plugins: map[string]plugin.Plugin{
31 | 			"kv": &shared.KVGRPCPlugin{Impl: &KV{}},
32 | 		},
33 | 
34 | 		// A non-nil value here enables gRPC serving for this plugin...
35 | 		GRPCServer: plugin.DefaultGRPCServer,
36 | 	})
37 | }
38 | 


--------------------------------------------------------------------------------
/examples/grpc/plugin-go-netrpc/main.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"os"
 9 | 
10 | 	"github.com/hashicorp/go-plugin"
11 | 	"github.com/hashicorp/go-plugin/examples/grpc/shared"
12 | )
13 | 
14 | // Here is a real implementation of KV that writes to a local file with
15 | // the key name and the contents are the value of the key.
16 | type KV struct{}
17 | 
18 | func (KV) Put(key string, value []byte) error {
19 | 	value = []byte(fmt.Sprintf("%s\n\nWritten from plugin-go-netrpc", string(value)))
20 | 	return os.WriteFile("kv_"+key, value, 0644)
21 | }
22 | 
23 | func (KV) Get(key string) ([]byte, error) {
24 | 	return os.ReadFile("kv_" + key)
25 | }
26 | 
27 | func main() {
28 | 	plugin.Serve(&plugin.ServeConfig{
29 | 		HandshakeConfig: shared.Handshake,
30 | 		Plugins: map[string]plugin.Plugin{
31 | 			"kv": &shared.KVPlugin{Impl: &KV{}},
32 | 		},
33 | 	})
34 | }
35 | 


--------------------------------------------------------------------------------
/examples/grpc/plugin-python/.gitignore:
--------------------------------------------------------------------------------
1 | /.venv


--------------------------------------------------------------------------------
/examples/grpc/plugin-python/plugin.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) HashiCorp, Inc.
 2 | # SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | from concurrent import futures
 5 | import sys
 6 | import time
 7 | 
 8 | import grpc
 9 | 
10 | from proto import kv_pb2
11 | from proto import kv_pb2_grpc
12 | 
13 | from grpc_health.v1.health import HealthServicer
14 | from grpc_health.v1 import health_pb2, health_pb2_grpc
15 | 
16 | class KVServicer(kv_pb2_grpc.KVServicer):
17 |     """Implementation of KV service."""
18 | 
19 |     def Get(self, request, context):
20 |         filename = "kv_"+request.key
21 |         with open(filename, 'r+b') as f:
22 |             result = kv_pb2.GetResponse()
23 |             result.value = f.read()
24 |             return result
25 | 
26 |     def Put(self, request, context):
27 |         filename = "kv_"+request.key
28 |         value = "{0}\n\nWritten from plugin-python".format(request.value)
29 |         with open(filename, 'w') as f:
30 |             f.write(value)
31 | 
32 |         return kv_pb2.Empty()
33 | 
34 | def serve():
35 |     # We need to build a health service to work with go-plugin
36 |     health = HealthServicer()
37 |     health.set("plugin", health_pb2.HealthCheckResponse.ServingStatus.Value('SERVING'))
38 | 
39 |     # Start the server.
40 |     server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
41 |     kv_pb2_grpc.add_KVServicer_to_server(KVServicer(), server)
42 |     health_pb2_grpc.add_HealthServicer_to_server(health, server)
43 |     server.add_insecure_port('127.0.0.1:1234')
44 |     server.start()
45 | 
46 |     # Output information
47 |     print("1|1|tcp|127.0.0.1:1234|grpc")
48 |     sys.stdout.flush()
49 | 
50 |     try:
51 |         while True:
52 |             time.sleep(60 * 60 * 24)
53 |     except KeyboardInterrupt:
54 |         server.stop(0)
55 | 
56 | if __name__ == '__main__':
57 |     serve()
58 | 


--------------------------------------------------------------------------------
/examples/grpc/plugin-python/proto/kv_pb2.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) HashiCorp, Inc.
 2 | # SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | # -*- coding: utf-8 -*-
 5 | # Generated by the protocol buffer compiler.  DO NOT EDIT!
 6 | # source: proto/kv.proto
 7 | """Generated protocol buffer code."""
 8 | from google.protobuf import descriptor as _descriptor
 9 | from google.protobuf import descriptor_pool as _descriptor_pool
10 | from google.protobuf import symbol_database as _symbol_database
11 | from google.protobuf.internal import builder as _builder
12 | # @@protoc_insertion_point(imports)
13 | 
14 | _sym_db = _symbol_database.Default()
15 | 
16 | 
17 | 
18 | 
19 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eproto/kv.proto\x12\x05proto\"\x1e\n\nGetRequest\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\"#\n\x0bGetResponse\x12\x14\n\x05value\x18\x01 \x01(\x0cR\x05value\"4\n\nPutRequest\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\x0cR\x05value\"\x07\n\x05\x45mpty2Z\n\x02KV\x12,\n\x03Get\x12\x11.proto.GetRequest\x1a\x12.proto.GetResponse\x12&\n\x03Put\x12\x11.proto.PutRequest\x1a\x0c.proto.EmptyB\tZ\x07./protob\x06proto3')
20 | 
21 | _globals = globals()
22 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
23 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'proto.kv_pb2', _globals)
24 | if _descriptor._USE_C_DESCRIPTORS == False:
25 |   _globals['DESCRIPTOR']._options = None
26 |   _globals['DESCRIPTOR']._serialized_options = b'Z\007./proto'
27 |   _globals['_GETREQUEST']._serialized_start=25
28 |   _globals['_GETREQUEST']._serialized_end=55
29 |   _globals['_GETRESPONSE']._serialized_start=57
30 |   _globals['_GETRESPONSE']._serialized_end=92
31 |   _globals['_PUTREQUEST']._serialized_start=94
32 |   _globals['_PUTREQUEST']._serialized_end=146
33 |   _globals['_EMPTY']._serialized_start=148
34 |   _globals['_EMPTY']._serialized_end=155
35 |   _globals['_KV']._serialized_start=157
36 |   _globals['_KV']._serialized_end=247
37 | # @@protoc_insertion_point(module_scope)
38 | 


--------------------------------------------------------------------------------
/examples/grpc/plugin-python/proto/kv_pb2_grpc.py:
--------------------------------------------------------------------------------
  1 | # Copyright (c) HashiCorp, Inc.
  2 | # SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
  5 | """Client and server classes corresponding to protobuf-defined services."""
  6 | import grpc
  7 | 
  8 | from proto import kv_pb2 as proto_dot_kv__pb2
  9 | 
 10 | 
 11 | class KVStub(object):
 12 |     """Missing associated documentation comment in .proto file."""
 13 | 
 14 |     def __init__(self, channel):
 15 |         """Constructor.
 16 | 
 17 |         Args:
 18 |             channel: A grpc.Channel.
 19 |         """
 20 |         self.Get = channel.unary_unary(
 21 |                 '/proto.KV/Get',
 22 |                 request_serializer=proto_dot_kv__pb2.GetRequest.SerializeToString,
 23 |                 response_deserializer=proto_dot_kv__pb2.GetResponse.FromString,
 24 |                 )
 25 |         self.Put = channel.unary_unary(
 26 |                 '/proto.KV/Put',
 27 |                 request_serializer=proto_dot_kv__pb2.PutRequest.SerializeToString,
 28 |                 response_deserializer=proto_dot_kv__pb2.Empty.FromString,
 29 |                 )
 30 | 
 31 | 
 32 | class KVServicer(object):
 33 |     """Missing associated documentation comment in .proto file."""
 34 | 
 35 |     def Get(self, request, context):
 36 |         """Missing associated documentation comment in .proto file."""
 37 |         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
 38 |         context.set_details('Method not implemented!')
 39 |         raise NotImplementedError('Method not implemented!')
 40 | 
 41 |     def Put(self, request, context):
 42 |         """Missing associated documentation comment in .proto file."""
 43 |         context.set_code(grpc.StatusCode.UNIMPLEMENTED)
 44 |         context.set_details('Method not implemented!')
 45 |         raise NotImplementedError('Method not implemented!')
 46 | 
 47 | 
 48 | def add_KVServicer_to_server(servicer, server):
 49 |     rpc_method_handlers = {
 50 |             'Get': grpc.unary_unary_rpc_method_handler(
 51 |                     servicer.Get,
 52 |                     request_deserializer=proto_dot_kv__pb2.GetRequest.FromString,
 53 |                     response_serializer=proto_dot_kv__pb2.GetResponse.SerializeToString,
 54 |             ),
 55 |             'Put': grpc.unary_unary_rpc_method_handler(
 56 |                     servicer.Put,
 57 |                     request_deserializer=proto_dot_kv__pb2.PutRequest.FromString,
 58 |                     response_serializer=proto_dot_kv__pb2.Empty.SerializeToString,
 59 |             ),
 60 |     }
 61 |     generic_handler = grpc.method_handlers_generic_handler(
 62 |             'proto.KV', rpc_method_handlers)
 63 |     server.add_generic_rpc_handlers((generic_handler,))
 64 | 
 65 | 
 66 |  # This class is part of an EXPERIMENTAL API.
 67 | class KV(object):
 68 |     """Missing associated documentation comment in .proto file."""
 69 | 
 70 |     @staticmethod
 71 |     def Get(request,
 72 |             target,
 73 |             options=(),
 74 |             channel_credentials=None,
 75 |             call_credentials=None,
 76 |             insecure=False,
 77 |             compression=None,
 78 |             wait_for_ready=None,
 79 |             timeout=None,
 80 |             metadata=None):
 81 |         return grpc.experimental.unary_unary(request, target, '/proto.KV/Get',
 82 |             proto_dot_kv__pb2.GetRequest.SerializeToString,
 83 |             proto_dot_kv__pb2.GetResponse.FromString,
 84 |             options, channel_credentials,
 85 |             insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
 86 | 
 87 |     @staticmethod
 88 |     def Put(request,
 89 |             target,
 90 |             options=(),
 91 |             channel_credentials=None,
 92 |             call_credentials=None,
 93 |             insecure=False,
 94 |             compression=None,
 95 |             wait_for_ready=None,
 96 |             timeout=None,
 97 |             metadata=None):
 98 |         return grpc.experimental.unary_unary(request, target, '/proto.KV/Put',
 99 |             proto_dot_kv__pb2.PutRequest.SerializeToString,
100 |             proto_dot_kv__pb2.Empty.FromString,
101 |             options, channel_credentials,
102 |             insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
103 | 


--------------------------------------------------------------------------------
/examples/grpc/plugin-python/requirements.txt:
--------------------------------------------------------------------------------
1 | grpcio==1.59.0
2 | grpcio-health-checking==1.59.0
3 | protobuf==4.25.8
4 | 


--------------------------------------------------------------------------------
/examples/grpc/proto/kv.proto:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | syntax = "proto3";
 5 | package proto;
 6 | option go_package = "./proto";
 7 | 
 8 | message GetRequest {
 9 |     string key = 1;
10 | }
11 | 
12 | message GetResponse {
13 |     bytes value = 1;
14 | }
15 | 
16 | message PutRequest {
17 |     string key = 1;
18 |     bytes value = 2;
19 | }
20 | 
21 | message Empty {}
22 | 
23 | service KV {
24 |     rpc Get(GetRequest) returns (GetResponse);
25 |     rpc Put(PutRequest) returns (Empty);
26 | }
27 | 


--------------------------------------------------------------------------------
/examples/grpc/proto/kv_grpc.pb.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
  5 | // versions:
  6 | // - protoc-gen-go-grpc v1.3.0
  7 | // - protoc             (unknown)
  8 | // source: proto/kv.proto
  9 | 
 10 | package proto
 11 | 
 12 | import (
 13 | 	context "context"
 14 | 	grpc "google.golang.org/grpc"
 15 | 	codes "google.golang.org/grpc/codes"
 16 | 	status "google.golang.org/grpc/status"
 17 | )
 18 | 
 19 | // This is a compile-time assertion to ensure that this generated file
 20 | // is compatible with the grpc package it is being compiled against.
 21 | // Requires gRPC-Go v1.32.0 or later.
 22 | const _ = grpc.SupportPackageIsVersion7
 23 | 
 24 | const (
 25 | 	KV_Get_FullMethodName = "/proto.KV/Get"
 26 | 	KV_Put_FullMethodName = "/proto.KV/Put"
 27 | )
 28 | 
 29 | // KVClient is the client API for KV service.
 30 | //
 31 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
 32 | type KVClient interface {
 33 | 	Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error)
 34 | 	Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*Empty, error)
 35 | }
 36 | 
 37 | type kVClient struct {
 38 | 	cc grpc.ClientConnInterface
 39 | }
 40 | 
 41 | func NewKVClient(cc grpc.ClientConnInterface) KVClient {
 42 | 	return &kVClient{cc}
 43 | }
 44 | 
 45 | func (c *kVClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) {
 46 | 	out := new(GetResponse)
 47 | 	err := c.cc.Invoke(ctx, KV_Get_FullMethodName, in, out, opts...)
 48 | 	if err != nil {
 49 | 		return nil, err
 50 | 	}
 51 | 	return out, nil
 52 | }
 53 | 
 54 | func (c *kVClient) Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*Empty, error) {
 55 | 	out := new(Empty)
 56 | 	err := c.cc.Invoke(ctx, KV_Put_FullMethodName, in, out, opts...)
 57 | 	if err != nil {
 58 | 		return nil, err
 59 | 	}
 60 | 	return out, nil
 61 | }
 62 | 
 63 | // KVServer is the server API for KV service.
 64 | // All implementations should embed UnimplementedKVServer
 65 | // for forward compatibility
 66 | type KVServer interface {
 67 | 	Get(context.Context, *GetRequest) (*GetResponse, error)
 68 | 	Put(context.Context, *PutRequest) (*Empty, error)
 69 | }
 70 | 
 71 | // UnimplementedKVServer should be embedded to have forward compatible implementations.
 72 | type UnimplementedKVServer struct {
 73 | }
 74 | 
 75 | func (UnimplementedKVServer) Get(context.Context, *GetRequest) (*GetResponse, error) {
 76 | 	return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
 77 | }
 78 | func (UnimplementedKVServer) Put(context.Context, *PutRequest) (*Empty, error) {
 79 | 	return nil, status.Errorf(codes.Unimplemented, "method Put not implemented")
 80 | }
 81 | 
 82 | // UnsafeKVServer may be embedded to opt out of forward compatibility for this service.
 83 | // Use of this interface is not recommended, as added methods to KVServer will
 84 | // result in compilation errors.
 85 | type UnsafeKVServer interface {
 86 | 	mustEmbedUnimplementedKVServer()
 87 | }
 88 | 
 89 | func RegisterKVServer(s grpc.ServiceRegistrar, srv KVServer) {
 90 | 	s.RegisterService(&KV_ServiceDesc, srv)
 91 | }
 92 | 
 93 | func _KV_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 94 | 	in := new(GetRequest)
 95 | 	if err := dec(in); err != nil {
 96 | 		return nil, err
 97 | 	}
 98 | 	if interceptor == nil {
 99 | 		return srv.(KVServer).Get(ctx, in)
100 | 	}
101 | 	info := &grpc.UnaryServerInfo{
102 | 		Server:     srv,
103 | 		FullMethod: KV_Get_FullMethodName,
104 | 	}
105 | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
106 | 		return srv.(KVServer).Get(ctx, req.(*GetRequest))
107 | 	}
108 | 	return interceptor(ctx, in, info, handler)
109 | }
110 | 
111 | func _KV_Put_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
112 | 	in := new(PutRequest)
113 | 	if err := dec(in); err != nil {
114 | 		return nil, err
115 | 	}
116 | 	if interceptor == nil {
117 | 		return srv.(KVServer).Put(ctx, in)
118 | 	}
119 | 	info := &grpc.UnaryServerInfo{
120 | 		Server:     srv,
121 | 		FullMethod: KV_Put_FullMethodName,
122 | 	}
123 | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
124 | 		return srv.(KVServer).Put(ctx, req.(*PutRequest))
125 | 	}
126 | 	return interceptor(ctx, in, info, handler)
127 | }
128 | 
129 | // KV_ServiceDesc is the grpc.ServiceDesc for KV service.
130 | // It's only intended for direct use with grpc.RegisterService,
131 | // and not to be introspected or modified (even as a copy)
132 | var KV_ServiceDesc = grpc.ServiceDesc{
133 | 	ServiceName: "proto.KV",
134 | 	HandlerType: (*KVServer)(nil),
135 | 	Methods: []grpc.MethodDesc{
136 | 		{
137 | 			MethodName: "Get",
138 | 			Handler:    _KV_Get_Handler,
139 | 		},
140 | 		{
141 | 			MethodName: "Put",
142 | 			Handler:    _KV_Put_Handler,
143 | 		},
144 | 	},
145 | 	Streams:  []grpc.StreamDesc{},
146 | 	Metadata: "proto/kv.proto",
147 | }
148 | 


--------------------------------------------------------------------------------
/examples/grpc/shared/grpc.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package shared
 5 | 
 6 | import (
 7 | 	"context"
 8 | 
 9 | 	"github.com/hashicorp/go-plugin/examples/grpc/proto"
10 | )
11 | 
12 | // GRPCClient is an implementation of KV that talks over RPC.
13 | type GRPCClient struct{ client proto.KVClient }
14 | 
15 | func (m *GRPCClient) Put(key string, value []byte) error {
16 | 	_, err := m.client.Put(context.Background(), &proto.PutRequest{
17 | 		Key:   key,
18 | 		Value: value,
19 | 	})
20 | 	return err
21 | }
22 | 
23 | func (m *GRPCClient) Get(key string) ([]byte, error) {
24 | 	resp, err := m.client.Get(context.Background(), &proto.GetRequest{
25 | 		Key: key,
26 | 	})
27 | 	if err != nil {
28 | 		return nil, err
29 | 	}
30 | 
31 | 	return resp.Value, nil
32 | }
33 | 
34 | // Here is the gRPC server that GRPCClient talks to.
35 | type GRPCServer struct {
36 | 	// This is the real implementation
37 | 	Impl KV
38 | }
39 | 
40 | func (m *GRPCServer) Put(
41 | 	ctx context.Context,
42 | 	req *proto.PutRequest) (*proto.Empty, error) {
43 | 	return &proto.Empty{}, m.Impl.Put(req.Key, req.Value)
44 | }
45 | 
46 | func (m *GRPCServer) Get(
47 | 	ctx context.Context,
48 | 	req *proto.GetRequest) (*proto.GetResponse, error) {
49 | 	v, err := m.Impl.Get(req.Key)
50 | 	return &proto.GetResponse{Value: v}, err
51 | }
52 | 


--------------------------------------------------------------------------------
/examples/grpc/shared/interface.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | // Package shared contains shared data between the host and plugins.
 5 | package shared
 6 | 
 7 | import (
 8 | 	"context"
 9 | 	"net/rpc"
10 | 
11 | 	"google.golang.org/grpc"
12 | 
13 | 	"github.com/hashicorp/go-plugin"
14 | 	"github.com/hashicorp/go-plugin/examples/grpc/proto"
15 | )
16 | 
17 | // Handshake is a common handshake that is shared by plugin and host.
18 | var Handshake = plugin.HandshakeConfig{
19 | 	// This isn't required when using VersionedPlugins
20 | 	ProtocolVersion:  1,
21 | 	MagicCookieKey:   "BASIC_PLUGIN",
22 | 	MagicCookieValue: "hello",
23 | }
24 | 
25 | // PluginMap is the map of plugins we can dispense.
26 | var PluginMap = map[string]plugin.Plugin{
27 | 	"kv_grpc": &KVGRPCPlugin{},
28 | 	"kv":      &KVPlugin{},
29 | }
30 | 
31 | // KV is the interface that we're exposing as a plugin.
32 | type KV interface {
33 | 	Put(key string, value []byte) error
34 | 	Get(key string) ([]byte, error)
35 | }
36 | 
37 | // This is the implementation of plugin.Plugin so we can serve/consume this.
38 | type KVPlugin struct {
39 | 	// Concrete implementation, written in Go. This is only used for plugins
40 | 	// that are written in Go.
41 | 	Impl KV
42 | }
43 | 
44 | func (p *KVPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
45 | 	return &RPCServer{Impl: p.Impl}, nil
46 | }
47 | 
48 | func (*KVPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
49 | 	return &RPCClient{client: c}, nil
50 | }
51 | 
52 | // This is the implementation of plugin.GRPCPlugin so we can serve/consume this.
53 | type KVGRPCPlugin struct {
54 | 	// GRPCPlugin must still implement the Plugin interface
55 | 	plugin.Plugin
56 | 	// Concrete implementation, written in Go. This is only used for plugins
57 | 	// that are written in Go.
58 | 	Impl KV
59 | }
60 | 
61 | func (p *KVGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
62 | 	proto.RegisterKVServer(s, &GRPCServer{Impl: p.Impl})
63 | 	return nil
64 | }
65 | 
66 | func (p *KVGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
67 | 	return &GRPCClient{client: proto.NewKVClient(c)}, nil
68 | }
69 | 


--------------------------------------------------------------------------------
/examples/grpc/shared/rpc.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package shared
 5 | 
 6 | import (
 7 | 	"net/rpc"
 8 | )
 9 | 
10 | // RPCClient is an implementation of KV that talks over RPC.
11 | type RPCClient struct{ client *rpc.Client }
12 | 
13 | func (m *RPCClient) Put(key string, value []byte) error {
14 | 	// We don't expect a response, so we can just use interface{}
15 | 	var resp interface{}
16 | 
17 | 	// The args are just going to be a map. A struct could be better.
18 | 	return m.client.Call("Plugin.Put", map[string]interface{}{
19 | 		"key":   key,
20 | 		"value": value,
21 | 	}, &resp)
22 | }
23 | 
24 | func (m *RPCClient) Get(key string) ([]byte, error) {
25 | 	var resp []byte
26 | 	err := m.client.Call("Plugin.Get", key, &resp)
27 | 	return resp, err
28 | }
29 | 
30 | // Here is the RPC server that RPCClient talks to, conforming to
31 | // the requirements of net/rpc
32 | type RPCServer struct {
33 | 	// This is the real implementation
34 | 	Impl KV
35 | }
36 | 
37 | func (m *RPCServer) Put(args map[string]interface{}, resp *interface{}) error {
38 | 	return m.Impl.Put(args["key"].(string), args["value"].([]byte))
39 | }
40 | 
41 | func (m *RPCServer) Get(key string, resp *[]byte) error {
42 | 	v, err := m.Impl.Get(key)
43 | 	*resp = v
44 | 	return err
45 | }
46 | 


--------------------------------------------------------------------------------
/examples/negotiated/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | kv
3 | kv-*
4 | kv_*
5 | !kv_*.py
6 | 


--------------------------------------------------------------------------------
/examples/negotiated/README.md:
--------------------------------------------------------------------------------
 1 | # Negotiated version KV Example
 2 | 
 3 | This example builds a simple key/value store CLI where the plugin version can
 4 | be negotiated between client and server.
 5 | 
 6 | ```sh
 7 | # This builds the main CLI
 8 | $ go build -o kv
 9 | 
10 | # This builds the plugin written in Go
11 | $ go build -o kv-plugin ./plugin-go
12 | 
13 | # Write a value using proto version 3 and grpc
14 | $ KV_PROTO=grpc ./kv put hello world
15 | 
16 | # Read it back using proto version 2 and netrpc
17 | $ KV_PROTO=netrpc ./kv get hello
18 | world
19 | 
20 | Written from plugin version 3
21 | Read by plugin version 2
22 | ```
23 | 
24 | # Negotiated Protocol
25 | 
26 | The Client sends the list of available plugin versions to the server. When
27 | presented with a list of plugin versions, the server iterates over them in
28 | reverse, and uses the highest numbered match to choose the plugins to execute.
29 | If a legacy client is used and no versions are sent to the server, the server
30 | will default to the oldest version in its configuration.
31 | 


--------------------------------------------------------------------------------
/examples/negotiated/main.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"io"
 9 | 	"log"
10 | 	"os"
11 | 	"os/exec"
12 | 
13 | 	"github.com/hashicorp/go-plugin"
14 | 	"github.com/hashicorp/go-plugin/examples/grpc/shared"
15 | )
16 | 
17 | func main() {
18 | 	// We don't want to see the plugin logs.
19 | 	log.SetOutput(io.Discard)
20 | 
21 | 	plugins := map[int]plugin.PluginSet{}
22 | 
23 | 	// Both version can be supported, but switch the implementation to
24 | 	// demonstrate version negoation.
25 | 	switch os.Getenv("KV_PROTO") {
26 | 	case "netrpc":
27 | 		plugins[2] = plugin.PluginSet{
28 | 			"kv": &shared.KVPlugin{},
29 | 		}
30 | 	case "grpc":
31 | 		plugins[3] = plugin.PluginSet{
32 | 			"kv": &shared.KVGRPCPlugin{},
33 | 		}
34 | 	default:
35 | 		fmt.Println("must set KV_PROTO to netrpc or grpc")
36 | 		os.Exit(1)
37 | 	}
38 | 
39 | 	// We're a host. Start by launching the plugin process.
40 | 	client := plugin.NewClient(&plugin.ClientConfig{
41 | 		HandshakeConfig:  shared.Handshake,
42 | 		VersionedPlugins: plugins,
43 | 		Cmd:              exec.Command("./kv-plugin"),
44 | 		AllowedProtocols: []plugin.Protocol{
45 | 			plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
46 | 	})
47 | 	defer client.Kill()
48 | 
49 | 	rpcClient, err := client.Client()
50 | 	if err != nil {
51 | 		fmt.Println("Error:", err.Error())
52 | 		os.Exit(1)
53 | 	}
54 | 
55 | 	// Request the plugin
56 | 	raw, err := rpcClient.Dispense("kv")
57 | 	if err != nil {
58 | 		fmt.Println("Error:", err.Error())
59 | 		os.Exit(1)
60 | 	}
61 | 
62 | 	// We should have a KV store now! This feels like a normal interface
63 | 	// implementation but is in fact over an RPC connection.
64 | 	kv := raw.(shared.KV)
65 | 	os.Args = os.Args[1:]
66 | 	switch os.Args[0] {
67 | 	case "get":
68 | 		result, err := kv.Get(os.Args[1])
69 | 		if err != nil {
70 | 			fmt.Println("Error:", err.Error())
71 | 			os.Exit(1)
72 | 		}
73 | 
74 | 		fmt.Println(string(result))
75 | 
76 | 	case "put":
77 | 		err := kv.Put(os.Args[1], []byte(os.Args[2]))
78 | 		if err != nil {
79 | 			fmt.Println("Error:", err.Error())
80 | 			os.Exit(1)
81 | 		}
82 | 
83 | 	default:
84 | 		fmt.Println("Please only use 'get' or 'put'")
85 | 		os.Exit(1)
86 | 	}
87 | }
88 | 


--------------------------------------------------------------------------------
/examples/negotiated/plugin-go/main.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"os"
 9 | 
10 | 	"github.com/hashicorp/go-plugin"
11 | 	"github.com/hashicorp/go-plugin/examples/grpc/shared"
12 | )
13 | 
14 | // Here is a real implementation of KV that uses grpc and  writes to a local
15 | // file with the key name and the contents are the value of the key.
16 | type KVGRPC struct{}
17 | 
18 | func (KVGRPC) Put(key string, value []byte) error {
19 | 	value = []byte(fmt.Sprintf("%s\n\nWritten from plugin version 3\n", string(value)))
20 | 	return os.WriteFile("kv_"+key, value, 0644)
21 | }
22 | 
23 | func (KVGRPC) Get(key string) ([]byte, error) {
24 | 	d, err := os.ReadFile("kv_" + key)
25 | 	if err != nil {
26 | 		return nil, err
27 | 	}
28 | 	return append(d, []byte("Read by plugin version 3\n")...), nil
29 | }
30 | 
31 | // Here is a real implementation of KV that writes to a local file with
32 | // the key name and the contents are the value of the key.
33 | type KV struct{}
34 | 
35 | func (KV) Put(key string, value []byte) error {
36 | 	value = []byte(fmt.Sprintf("%s\n\nWritten from plugin version 2\n", string(value)))
37 | 	return os.WriteFile("kv_"+key, value, 0644)
38 | }
39 | 
40 | func (KV) Get(key string) ([]byte, error) {
41 | 	d, err := os.ReadFile("kv_" + key)
42 | 	if err != nil {
43 | 		return nil, err
44 | 	}
45 | 	return append(d, []byte("Read by plugin version 2\n")...), nil
46 | }
47 | 
48 | func main() {
49 | 	plugin.Serve(&plugin.ServeConfig{
50 | 		HandshakeConfig: shared.Handshake,
51 | 		VersionedPlugins: map[int]plugin.PluginSet{
52 | 			// Version 2 only uses NetRPC
53 | 			2: {
54 | 				"kv": &shared.KVPlugin{Impl: &KV{}},
55 | 			},
56 | 			// Version 3 only uses GRPC
57 | 			3: {
58 | 				"kv": &shared.KVGRPCPlugin{Impl: &KVGRPC{}},
59 | 			},
60 | 		},
61 | 
62 | 		// A non-nil value here enables gRPC serving for this plugin...
63 | 		GRPCServer: plugin.DefaultGRPCServer,
64 | 	})
65 | }
66 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
 1 | module github.com/hashicorp/go-plugin
 2 | 
 3 | go 1.24
 4 | 
 5 | require (
 6 | 	github.com/golang/protobuf v1.5.4
 7 | 	github.com/hashicorp/go-hclog v1.6.3
 8 | 	github.com/hashicorp/yamux v0.1.2
 9 | 	github.com/jhump/protoreflect v1.17.0
10 | 	github.com/oklog/run v1.1.0
11 | 	google.golang.org/grpc v1.61.0
12 | 	google.golang.org/protobuf v1.36.6
13 | )
14 | 
15 | require (
16 | 	github.com/bufbuild/protocompile v0.14.1 // indirect
17 | 	github.com/fatih/color v1.13.0 // indirect
18 | 	github.com/mattn/go-colorable v0.1.12 // indirect
19 | 	github.com/mattn/go-isatty v0.0.17 // indirect
20 | 	golang.org/x/net v0.38.0 // indirect
21 | 	golang.org/x/sys v0.31.0 // indirect
22 | 	golang.org/x/text v0.23.0 // indirect
23 | 	google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
24 | )
25 | 


--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
 1 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
 2 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 6 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
 7 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 8 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 9 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
12 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
13 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
14 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
15 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
16 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
17 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
18 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
19 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
20 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
21 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
22 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
23 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
24 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
25 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
26 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
29 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
30 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
31 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
32 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
33 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
34 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
35 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
36 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
37 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
38 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
39 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
40 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
42 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
43 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
44 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
45 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
46 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
47 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
48 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
49 | google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
50 | google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
51 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
52 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
54 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
55 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
56 | 


--------------------------------------------------------------------------------
/grpc_client.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package plugin
  5 | 
  6 | import (
  7 | 	"context"
  8 | 	"crypto/tls"
  9 | 	"fmt"
 10 | 	"math"
 11 | 	"net"
 12 | 
 13 | 	"github.com/hashicorp/go-plugin/internal/plugin"
 14 | 	"google.golang.org/grpc"
 15 | 	"google.golang.org/grpc/credentials"
 16 | 	"google.golang.org/grpc/credentials/insecure"
 17 | 	"google.golang.org/grpc/health/grpc_health_v1"
 18 | )
 19 | 
 20 | func dialGRPCConn(tls *tls.Config, dialer func(context.Context, string) (net.Conn, error), dialOpts ...grpc.DialOption) (*grpc.ClientConn, error) {
 21 | 	// Build dialing options.
 22 | 	opts := make([]grpc.DialOption, 0)
 23 | 
 24 | 	// We use a custom dialer so that we can connect over unix domain sockets.
 25 | 	opts = append(opts, grpc.WithContextDialer(dialer))
 26 | 
 27 | 	// Fail right away
 28 | 	opts = append(opts, grpc.FailOnNonTempDialError(true))
 29 | 
 30 | 	// If we have no TLS configuration set, we need to explicitly tell grpc
 31 | 	// that we're connecting with an insecure connection.
 32 | 	if tls == nil {
 33 | 		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
 34 | 	} else {
 35 | 		opts = append(opts, grpc.WithTransportCredentials(
 36 | 			credentials.NewTLS(tls)))
 37 | 	}
 38 | 
 39 | 	opts = append(opts,
 40 | 		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(math.MaxInt32)),
 41 | 		grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(math.MaxInt32)))
 42 | 
 43 | 	// Add our custom options if we have any
 44 | 	opts = append(opts, dialOpts...)
 45 | 
 46 | 	// Connect. Note the first parameter is unused because we use a custom
 47 | 	// dialer that has the state to see the address.
 48 | 	conn, err := grpc.Dial("unused", opts...)
 49 | 	if err != nil {
 50 | 		return nil, err
 51 | 	}
 52 | 
 53 | 	return conn, nil
 54 | }
 55 | 
 56 | // newGRPCClient creates a new GRPCClient. The Client argument is expected
 57 | // to be successfully started already with a lock held.
 58 | func newGRPCClient(doneCtx context.Context, c *Client) (*GRPCClient, error) {
 59 | 	conn, err := dialGRPCConn(c.config.TLSConfig, c.dialer, c.config.GRPCDialOptions...)
 60 | 	if err != nil {
 61 | 		return nil, err
 62 | 	}
 63 | 
 64 | 	muxer, err := c.getGRPCMuxer(c.address)
 65 | 	if err != nil {
 66 | 		return nil, err
 67 | 	}
 68 | 
 69 | 	// Start the broker.
 70 | 	brokerGRPCClient := newGRPCBrokerClient(conn)
 71 | 	broker := newGRPCBroker(brokerGRPCClient, c.config.TLSConfig, c.unixSocketCfg, c.runner, muxer)
 72 | 	go broker.Run()
 73 | 	go func() { _ = brokerGRPCClient.StartStream() }()
 74 | 
 75 | 	// Start the stdio client
 76 | 	stdioClient, err := newGRPCStdioClient(doneCtx, c.logger.Named("stdio"), conn)
 77 | 	if err != nil {
 78 | 		return nil, err
 79 | 	}
 80 | 	go stdioClient.Run(c.config.SyncStdout, c.config.SyncStderr)
 81 | 
 82 | 	cl := &GRPCClient{
 83 | 		Conn:       conn,
 84 | 		Plugins:    c.config.Plugins,
 85 | 		doneCtx:    doneCtx,
 86 | 		broker:     broker,
 87 | 		controller: plugin.NewGRPCControllerClient(conn),
 88 | 	}
 89 | 
 90 | 	return cl, nil
 91 | }
 92 | 
 93 | // GRPCClient connects to a GRPCServer over gRPC to dispense plugin types.
 94 | type GRPCClient struct {
 95 | 	Conn    *grpc.ClientConn
 96 | 	Plugins map[string]Plugin
 97 | 
 98 | 	doneCtx context.Context
 99 | 	broker  *GRPCBroker
100 | 
101 | 	controller plugin.GRPCControllerClient
102 | }
103 | 
104 | // ClientProtocol impl.
105 | func (c *GRPCClient) Close() error {
106 | 	_ = c.broker.Close()
107 | 	_, _ = c.controller.Shutdown(c.doneCtx, &plugin.Empty{})
108 | 	return c.Conn.Close()
109 | }
110 | 
111 | // ClientProtocol impl.
112 | func (c *GRPCClient) Dispense(name string) (interface{}, error) {
113 | 	raw, ok := c.Plugins[name]
114 | 	if !ok {
115 | 		return nil, fmt.Errorf("unknown plugin type: %s", name)
116 | 	}
117 | 
118 | 	p, ok := raw.(GRPCPlugin)
119 | 	if !ok {
120 | 		return nil, fmt.Errorf("plugin %q doesn't support gRPC", name)
121 | 	}
122 | 
123 | 	return p.GRPCClient(c.doneCtx, c.broker, c.Conn)
124 | }
125 | 
126 | // ClientProtocol impl.
127 | func (c *GRPCClient) Ping() error {
128 | 	client := grpc_health_v1.NewHealthClient(c.Conn)
129 | 	_, err := client.Check(context.Background(), &grpc_health_v1.HealthCheckRequest{
130 | 		Service: GRPCServiceName,
131 | 	})
132 | 
133 | 	return err
134 | }
135 | 


--------------------------------------------------------------------------------
/grpc_client_test.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package plugin
  5 | 
  6 | import (
  7 | 	"context"
  8 | 	"reflect"
  9 | 	"testing"
 10 | 
 11 | 	grpctest "github.com/hashicorp/go-plugin/test/grpc"
 12 | 	"github.com/jhump/protoreflect/grpcreflect"
 13 | 	"google.golang.org/grpc"
 14 | )
 15 | 
 16 | func TestGRPC_App(t *testing.T) {
 17 | 	t.Run("default", func(t *testing.T) {
 18 | 		testGRPCClientApp(t, false)
 19 | 	})
 20 | 	t.Run("mux", func(t *testing.T) {
 21 | 		testGRPCClientApp(t, true)
 22 | 	})
 23 | }
 24 | 
 25 | func testGRPCClientApp(t *testing.T, multiplex bool) {
 26 | 	client, server := TestPluginGRPCConn(t, multiplex, map[string]Plugin{
 27 | 		"test": new(testGRPCInterfacePlugin),
 28 | 	})
 29 | 	defer func() { _ = client.Close() }()
 30 | 	defer server.Stop()
 31 | 
 32 | 	raw, err := client.Dispense("test")
 33 | 	if err != nil {
 34 | 		t.Fatalf("err: %s", err)
 35 | 	}
 36 | 
 37 | 	impl, ok := raw.(testInterface)
 38 | 	if !ok {
 39 | 		t.Fatalf("bad: %#v", raw)
 40 | 	}
 41 | 
 42 | 	result := impl.Double(21)
 43 | 	if result != 42 {
 44 | 		t.Fatalf("bad: %#v", result)
 45 | 	}
 46 | 
 47 | 	err = impl.Bidirectional()
 48 | 	if err != nil {
 49 | 		t.Fatal(err)
 50 | 	}
 51 | }
 52 | 
 53 | func TestGRPCConn_BidirectionalPing(t *testing.T) {
 54 | 	conn, _ := TestGRPCConn(t, func(s *grpc.Server) {
 55 | 		grpctest.RegisterPingPongServer(s, &pingPongServer{})
 56 | 	})
 57 | 	defer func() { _ = conn.Close() }()
 58 | 	pingPongClient := grpctest.NewPingPongClient(conn)
 59 | 
 60 | 	pResp, err := pingPongClient.Ping(context.Background(), &grpctest.PingRequest{})
 61 | 	if err != nil {
 62 | 		t.Fatal(err)
 63 | 	}
 64 | 	if pResp.Msg != "pong" {
 65 | 		t.Fatal("Bad PingPong")
 66 | 	}
 67 | }
 68 | 
 69 | func TestGRPCC_Stream(t *testing.T) {
 70 | 	t.Run("default", func(t *testing.T) {
 71 | 		testGRPCStream(t, false)
 72 | 	})
 73 | 	t.Run("mux", func(t *testing.T) {
 74 | 		testGRPCStream(t, true)
 75 | 	})
 76 | }
 77 | 
 78 | func testGRPCStream(t *testing.T, multiplex bool) {
 79 | 	client, server := TestPluginGRPCConn(t, multiplex, map[string]Plugin{
 80 | 		"test": new(testGRPCInterfacePlugin),
 81 | 	})
 82 | 	defer func() { _ = client.Close() }()
 83 | 	defer server.Stop()
 84 | 
 85 | 	raw, err := client.Dispense("test")
 86 | 	if err != nil {
 87 | 		t.Fatalf("err: %s", err)
 88 | 	}
 89 | 
 90 | 	impl, ok := raw.(testStreamer)
 91 | 	if !ok {
 92 | 		t.Fatalf("bad: %#v", raw)
 93 | 	}
 94 | 
 95 | 	expected := []int32{21, 22, 23, 24, 25, 26}
 96 | 	result, err := impl.Stream(21, 27)
 97 | 	if err != nil {
 98 | 		t.Fatal(err)
 99 | 	}
100 | 
101 | 	if !reflect.DeepEqual(result, expected) {
102 | 		t.Fatalf("expected: %v\ngot: %v", expected, result)
103 | 	}
104 | }
105 | 
106 | func TestGRPC_Ping(t *testing.T) {
107 | 	t.Run("default", func(t *testing.T) {
108 | 		testGRPCClientPing(t, false)
109 | 	})
110 | 	t.Run("mux", func(t *testing.T) {
111 | 		testGRPCClientPing(t, true)
112 | 	})
113 | }
114 | 
115 | func testGRPCClientPing(t *testing.T, multiplex bool) {
116 | 	client, server := TestPluginGRPCConn(t, multiplex, map[string]Plugin{
117 | 		"test": new(testGRPCInterfacePlugin),
118 | 	})
119 | 	defer func() { _ = client.Close() }()
120 | 	defer server.Stop()
121 | 
122 | 	// Run a couple pings
123 | 	if err := client.Ping(); err != nil {
124 | 		t.Fatalf("err: %s", err)
125 | 	}
126 | 	if err := client.Ping(); err != nil {
127 | 		t.Fatalf("err: %s", err)
128 | 	}
129 | 
130 | 	// Close the remote end
131 | 	server.server.Stop()
132 | 
133 | 	// Test ping fails
134 | 	if err := client.Ping(); err == nil {
135 | 		t.Fatal("should error")
136 | 	}
137 | }
138 | 
139 | func TestGRPC_Reflection(t *testing.T) {
140 | 	t.Run("default", func(t *testing.T) {
141 | 		testGRPCClientReflection(t, false)
142 | 	})
143 | 	t.Run("mux", func(t *testing.T) {
144 | 		testGRPCClientReflection(t, true)
145 | 	})
146 | }
147 | 
148 | func testGRPCClientReflection(t *testing.T, multiplex bool) {
149 | 	ctx := context.Background()
150 | 
151 | 	client, server := TestPluginGRPCConn(t, multiplex, map[string]Plugin{
152 | 		"test": new(testGRPCInterfacePlugin),
153 | 	})
154 | 	defer func() { _ = client.Close() }()
155 | 	defer server.Stop()
156 | 
157 | 	refClient := grpcreflect.NewClientAuto(ctx, client.Conn)
158 | 
159 | 	svcs, err := refClient.ListServices()
160 | 	if err != nil {
161 | 		t.Fatalf("err: %s", err)
162 | 	}
163 | 
164 | 	// TODO: maybe only assert some specific services here to make test more resilient
165 | 	expectedSvcs := []string{"grpc.health.v1.Health", "grpc.reflection.v1.ServerReflection", "grpc.reflection.v1alpha.ServerReflection", "grpctest.Test", "plugin.GRPCBroker", "plugin.GRPCController", "plugin.GRPCStdio"}
166 | 
167 | 	if !reflect.DeepEqual(svcs, expectedSvcs) {
168 | 		t.Fatalf("expected: %v\ngot: %v", expectedSvcs, svcs)
169 | 	}
170 | 
171 | 	healthDesc, err := refClient.ResolveService("grpc.health.v1.Health")
172 | 	if err != nil {
173 | 		t.Fatalf("err: %s", err)
174 | 	}
175 | 
176 | 	methods := healthDesc.GetMethods()
177 | 	var methodNames []string
178 | 	for _, m := range methods {
179 | 		methodNames = append(methodNames, m.GetName())
180 | 	}
181 | 
182 | 	expectedMethodNames := []string{"Check", "Watch"}
183 | 
184 | 	if !reflect.DeepEqual(methodNames, expectedMethodNames) {
185 | 		t.Fatalf("expected: %v\ngot: %v", expectedMethodNames, methodNames)
186 | 	}
187 | }
188 | 


--------------------------------------------------------------------------------
/grpc_controller.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | import (
 7 | 	"context"
 8 | 
 9 | 	"github.com/hashicorp/go-plugin/internal/plugin"
10 | )
11 | 
12 | // GRPCControllerServer handles shutdown calls to terminate the server when the
13 | // plugin client is closed.
14 | type grpcControllerServer struct {
15 | 	server *GRPCServer
16 | }
17 | 
18 | // Shutdown stops the grpc server. It first will attempt a graceful stop, then a
19 | // full stop on the server.
20 | func (s *grpcControllerServer) Shutdown(ctx context.Context, _ *plugin.Empty) (*plugin.Empty, error) {
21 | 	resp := &plugin.Empty{}
22 | 
23 | 	// TODO: figure out why GracefullStop doesn't work.
24 | 	s.server.Stop()
25 | 	return resp, nil
26 | }
27 | 


--------------------------------------------------------------------------------
/grpc_server.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package plugin
  5 | 
  6 | import (
  7 | 	"bytes"
  8 | 	"crypto/tls"
  9 | 	"encoding/json"
 10 | 	"fmt"
 11 | 	"io"
 12 | 	"net"
 13 | 
 14 | 	hclog "github.com/hashicorp/go-hclog"
 15 | 	"github.com/hashicorp/go-plugin/internal/grpcmux"
 16 | 	"github.com/hashicorp/go-plugin/internal/plugin"
 17 | 	"google.golang.org/grpc"
 18 | 	"google.golang.org/grpc/credentials"
 19 | 	"google.golang.org/grpc/health"
 20 | 	"google.golang.org/grpc/health/grpc_health_v1"
 21 | 	"google.golang.org/grpc/reflection"
 22 | )
 23 | 
 24 | // GRPCServiceName is the name of the service that the health check should
 25 | // return as passing.
 26 | const GRPCServiceName = "plugin"
 27 | 
 28 | // DefaultGRPCServer can be used with the "GRPCServer" field for Server
 29 | // as a default factory method to create a gRPC server with no extra options.
 30 | func DefaultGRPCServer(opts []grpc.ServerOption) *grpc.Server {
 31 | 	return grpc.NewServer(opts...)
 32 | }
 33 | 
 34 | // GRPCServer is a ServerType implementation that serves plugins over
 35 | // gRPC. This allows plugins to easily be written for other languages.
 36 | //
 37 | // The GRPCServer outputs a custom configuration as a base64-encoded
 38 | // JSON structure represented by the GRPCServerConfig config structure.
 39 | type GRPCServer struct {
 40 | 	// Plugins are the list of plugins to serve.
 41 | 	Plugins map[string]Plugin
 42 | 
 43 | 	// Server is the actual server that will accept connections. This
 44 | 	// will be used for plugin registration as well.
 45 | 	Server func([]grpc.ServerOption) *grpc.Server
 46 | 
 47 | 	// TLS should be the TLS configuration if available. If this is nil,
 48 | 	// the connection will not have transport security.
 49 | 	TLS *tls.Config
 50 | 
 51 | 	// DoneCh is the channel that is closed when this server has exited.
 52 | 	DoneCh chan struct{}
 53 | 
 54 | 	// Stdout/StderrLis are the readers for stdout/stderr that will be copied
 55 | 	// to the stdout/stderr connection that is output.
 56 | 	Stdout io.Reader
 57 | 	Stderr io.Reader
 58 | 
 59 | 	config      GRPCServerConfig
 60 | 	server      *grpc.Server
 61 | 	broker      *GRPCBroker
 62 | 	stdioServer *grpcStdioServer
 63 | 
 64 | 	logger hclog.Logger
 65 | 
 66 | 	muxer *grpcmux.GRPCServerMuxer
 67 | }
 68 | 
 69 | // ServerProtocol impl.
 70 | func (s *GRPCServer) Init() error {
 71 | 	// Create our server
 72 | 	var opts []grpc.ServerOption
 73 | 	if s.TLS != nil {
 74 | 		opts = append(opts, grpc.Creds(credentials.NewTLS(s.TLS)))
 75 | 	}
 76 | 	s.server = s.Server(opts)
 77 | 
 78 | 	// Register the health service
 79 | 	healthCheck := health.NewServer()
 80 | 	healthCheck.SetServingStatus(
 81 | 		GRPCServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
 82 | 	grpc_health_v1.RegisterHealthServer(s.server, healthCheck)
 83 | 
 84 | 	// Register the reflection service
 85 | 	reflection.Register(s.server)
 86 | 
 87 | 	// Register the broker service
 88 | 	brokerServer := newGRPCBrokerServer()
 89 | 	plugin.RegisterGRPCBrokerServer(s.server, brokerServer)
 90 | 	s.broker = newGRPCBroker(brokerServer, s.TLS, unixSocketConfigFromEnv(), nil, s.muxer)
 91 | 	go s.broker.Run()
 92 | 
 93 | 	// Register the controller
 94 | 	controllerServer := &grpcControllerServer{server: s}
 95 | 	plugin.RegisterGRPCControllerServer(s.server, controllerServer)
 96 | 
 97 | 	// Register the stdio service
 98 | 	s.stdioServer = newGRPCStdioServer(s.logger, s.Stdout, s.Stderr)
 99 | 	plugin.RegisterGRPCStdioServer(s.server, s.stdioServer)
100 | 
101 | 	// Register all our plugins onto the gRPC server.
102 | 	for k, raw := range s.Plugins {
103 | 		p, ok := raw.(GRPCPlugin)
104 | 		if !ok {
105 | 			return fmt.Errorf("%q is not a GRPC-compatible plugin", k)
106 | 		}
107 | 
108 | 		if err := p.GRPCServer(s.broker, s.server); err != nil {
109 | 			return fmt.Errorf("error registering %q: %s", k, err)
110 | 		}
111 | 	}
112 | 
113 | 	return nil
114 | }
115 | 
116 | // Stop calls Stop on the underlying grpc.Server and Close on the underlying
117 | // grpc.Broker if present.
118 | func (s *GRPCServer) Stop() {
119 | 	s.server.Stop()
120 | 
121 | 	if s.broker != nil {
122 | 		_ = s.broker.Close()
123 | 		s.broker = nil
124 | 	}
125 | }
126 | 
127 | // GracefulStop calls GracefulStop on the underlying grpc.Server and Close on
128 | // the underlying grpc.Broker if present.
129 | func (s *GRPCServer) GracefulStop() {
130 | 	s.server.GracefulStop()
131 | 
132 | 	if s.broker != nil {
133 | 		_ = s.broker.Close()
134 | 		s.broker = nil
135 | 	}
136 | }
137 | 
138 | // Config is the GRPCServerConfig encoded as JSON then base64.
139 | func (s *GRPCServer) Config() string {
140 | 	// Create a buffer that will contain our final contents
141 | 	var buf bytes.Buffer
142 | 
143 | 	// Wrap the base64 encoding with JSON encoding.
144 | 	if err := json.NewEncoder(&buf).Encode(s.config); err != nil {
145 | 		// We panic since ths shouldn't happen under any scenario. We
146 | 		// carefully control the structure being encoded here and it should
147 | 		// always be successful.
148 | 		panic(err)
149 | 	}
150 | 
151 | 	return buf.String()
152 | }
153 | 
154 | func (s *GRPCServer) Serve(lis net.Listener) {
155 | 	defer close(s.DoneCh)
156 | 	err := s.server.Serve(lis)
157 | 	if err != nil {
158 | 		s.logger.Error("grpc server", "error", err)
159 | 	}
160 | }
161 | 
162 | // GRPCServerConfig is the extra configuration passed along for consumers
163 | // to facilitate using GRPC plugins.
164 | type GRPCServerConfig struct {
165 | 	StdoutAddr string `json:"stdout_addr"`
166 | 	StderrAddr string `json:"stderr_addr"`
167 | }
168 | 


--------------------------------------------------------------------------------
/grpc_stdio.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package plugin
  5 | 
  6 | import (
  7 | 	"bufio"
  8 | 	"bytes"
  9 | 	"context"
 10 | 	"io"
 11 | 
 12 | 	empty "github.com/golang/protobuf/ptypes/empty"
 13 | 	hclog "github.com/hashicorp/go-hclog"
 14 | 	"github.com/hashicorp/go-plugin/internal/plugin"
 15 | 	"google.golang.org/grpc"
 16 | 	"google.golang.org/grpc/codes"
 17 | 	"google.golang.org/grpc/status"
 18 | )
 19 | 
 20 | // grpcStdioBuffer is the buffer size we try to fill when sending a chunk of
 21 | // stdio data. This is currently 1 KB for no reason other than that seems like
 22 | // enough (stdio data isn't that common) and is fairly low.
 23 | const grpcStdioBuffer = 1 * 1024
 24 | 
 25 | // grpcStdioServer implements the Stdio service and streams stdiout/stderr.
 26 | type grpcStdioServer struct {
 27 | 	stdoutCh <-chan []byte
 28 | 	stderrCh <-chan []byte
 29 | }
 30 | 
 31 | // newGRPCStdioServer creates a new grpcStdioServer and starts the stream
 32 | // copying for the given out and err readers.
 33 | //
 34 | // This must only be called ONCE per srcOut, srcErr.
 35 | func newGRPCStdioServer(log hclog.Logger, srcOut, srcErr io.Reader) *grpcStdioServer {
 36 | 	stdoutCh := make(chan []byte)
 37 | 	stderrCh := make(chan []byte)
 38 | 
 39 | 	// Begin copying the streams
 40 | 	go copyChan(log, stdoutCh, srcOut)
 41 | 	go copyChan(log, stderrCh, srcErr)
 42 | 
 43 | 	// Construct our server
 44 | 	return &grpcStdioServer{
 45 | 		stdoutCh: stdoutCh,
 46 | 		stderrCh: stderrCh,
 47 | 	}
 48 | }
 49 | 
 50 | // StreamStdio streams our stdout/err as the response.
 51 | func (s *grpcStdioServer) StreamStdio(
 52 | 	_ *empty.Empty,
 53 | 	srv plugin.GRPCStdio_StreamStdioServer,
 54 | ) error {
 55 | 	// Share the same data value between runs. Sending this over the wire
 56 | 	// marshals it so we can reuse this.
 57 | 	var data plugin.StdioData
 58 | 
 59 | 	for {
 60 | 		// Read our data
 61 | 		select {
 62 | 		case data.Data = <-s.stdoutCh:
 63 | 			data.Channel = plugin.StdioData_STDOUT
 64 | 
 65 | 		case data.Data = <-s.stderrCh:
 66 | 			data.Channel = plugin.StdioData_STDERR
 67 | 
 68 | 		case <-srv.Context().Done():
 69 | 			return nil
 70 | 		}
 71 | 
 72 | 		// Not sure if this is possible, but if we somehow got here and
 73 | 		// we didn't populate any data at all, then just continue.
 74 | 		if len(data.Data) == 0 {
 75 | 			continue
 76 | 		}
 77 | 
 78 | 		// Send our data to the client.
 79 | 		if err := srv.Send(&data); err != nil {
 80 | 			return err
 81 | 		}
 82 | 	}
 83 | }
 84 | 
 85 | // grpcStdioClient wraps the stdio service as a client to copy
 86 | // the stdio data to output writers.
 87 | type grpcStdioClient struct {
 88 | 	log         hclog.Logger
 89 | 	stdioClient plugin.GRPCStdio_StreamStdioClient
 90 | }
 91 | 
 92 | // newGRPCStdioClient creates a grpcStdioClient. This will perform the
 93 | // initial connection to the stdio service. If the stdio service is unavailable
 94 | // then this will be a no-op. This allows this to work without error for
 95 | // plugins that don't support this.
 96 | func newGRPCStdioClient(
 97 | 	ctx context.Context,
 98 | 	log hclog.Logger,
 99 | 	conn *grpc.ClientConn,
100 | ) (*grpcStdioClient, error) {
101 | 	client := plugin.NewGRPCStdioClient(conn)
102 | 
103 | 	// Connect immediately to the endpoint
104 | 	stdioClient, err := client.StreamStdio(ctx, &empty.Empty{})
105 | 
106 | 	// If we get an Unavailable or Unimplemented error, this means that the plugin isn't
107 | 	// updated and linking to the latest version of go-plugin that supports
108 | 	// this. We fall back to the previous behavior of just not syncing anything.
109 | 	if status.Code(err) == codes.Unavailable || status.Code(err) == codes.Unimplemented {
110 | 		log.Warn("stdio service not available, stdout/stderr syncing unavailable")
111 | 		stdioClient = nil
112 | 		err = nil
113 | 	}
114 | 	if err != nil {
115 | 		return nil, err
116 | 	}
117 | 
118 | 	return &grpcStdioClient{
119 | 		log:         log,
120 | 		stdioClient: stdioClient,
121 | 	}, nil
122 | }
123 | 
124 | // Run starts the loop that receives stdio data and writes it to the given
125 | // writers. This blocks and should be run in a goroutine.
126 | func (c *grpcStdioClient) Run(stdout, stderr io.Writer) {
127 | 	// This will be nil if stdio is not supported by the plugin
128 | 	if c.stdioClient == nil {
129 | 		c.log.Warn("stdio service unavailable, run will do nothing")
130 | 		return
131 | 	}
132 | 
133 | 	for {
134 | 		c.log.Trace("waiting for stdio data")
135 | 		data, err := c.stdioClient.Recv()
136 | 		if err != nil {
137 | 			if err == io.EOF ||
138 | 				status.Code(err) == codes.Unavailable ||
139 | 				status.Code(err) == codes.Canceled ||
140 | 				status.Code(err) == codes.Unimplemented ||
141 | 				err == context.Canceled {
142 | 				c.log.Debug("received EOF, stopping recv loop", "err", err)
143 | 				return
144 | 			}
145 | 
146 | 			c.log.Error("error receiving data", "err", err)
147 | 			return
148 | 		}
149 | 
150 | 		// Determine our output writer based on channel
151 | 		var w io.Writer
152 | 		switch data.Channel {
153 | 		case plugin.StdioData_STDOUT:
154 | 			w = stdout
155 | 
156 | 		case plugin.StdioData_STDERR:
157 | 			w = stderr
158 | 
159 | 		default:
160 | 			c.log.Warn("unknown channel, dropping", "channel", data.Channel)
161 | 			continue
162 | 		}
163 | 
164 | 		// Write! In the event of an error we just continue.
165 | 		if c.log.IsTrace() {
166 | 			c.log.Trace("received data", "channel", data.Channel.String(), "len", len(data.Data))
167 | 		}
168 | 		if _, err := io.Copy(w, bytes.NewReader(data.Data)); err != nil {
169 | 			c.log.Error("failed to copy all bytes", "err", err)
170 | 		}
171 | 	}
172 | }
173 | 
174 | // copyChan copies an io.Reader into a channel.
175 | func copyChan(log hclog.Logger, dst chan<- []byte, src io.Reader) {
176 | 	bufsrc := bufio.NewReader(src)
177 | 
178 | 	for {
179 | 		// Make our data buffer. We allocate a new one per loop iteration
180 | 		// so that we can send it over the channel.
181 | 		var data [grpcStdioBuffer]byte
182 | 
183 | 		// Read the data, this will block until data is available
184 | 		n, err := bufsrc.Read(data[:])
185 | 
186 | 		// We have to check if we have data BEFORE err != nil. The bufio
187 | 		// docs guarantee n == 0 on EOF but its better to be safe here.
188 | 		if n > 0 {
189 | 			// We have data! Send it on the channel. This will block if there
190 | 			// is no reader on the other side. We expect that go-plugin will
191 | 			// connect immediately to the stdio server to drain this so we want
192 | 			// this block to happen for backpressure.
193 | 			dst <- data[:n]
194 | 		}
195 | 
196 | 		// If we hit EOF we're done copying
197 | 		if err == io.EOF {
198 | 			log.Debug("stdio EOF, exiting copy loop")
199 | 			return
200 | 		}
201 | 
202 | 		// Any other error we just exit the loop. We don't expect there to
203 | 		// be errors since our use case for this is reading/writing from
204 | 		// a in-process pipe (os.Pipe).
205 | 		if err != nil {
206 | 			log.Warn("error copying stdio data, stopping copy", "err", err)
207 | 			return
208 | 		}
209 | 	}
210 | }
211 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/addr_translator.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package cmdrunner
 5 | 
 6 | // addrTranslator implements stateless identity functions, as the host and plugin
 7 | // run in the same context wrt Unix and network addresses.
 8 | type addrTranslator struct{}
 9 | 
10 | func (*addrTranslator) PluginToHost(pluginNet, pluginAddr string) (string, string, error) {
11 | 	return pluginNet, pluginAddr, nil
12 | }
13 | 
14 | func (*addrTranslator) HostToPlugin(hostNet, hostAddr string) (string, string, error) {
15 | 	return hostNet, hostAddr, nil
16 | }
17 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/cmd_reattach.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package cmdrunner
 5 | 
 6 | import (
 7 | 	"context"
 8 | 	"fmt"
 9 | 	"net"
10 | 	"os"
11 | 
12 | 	"github.com/hashicorp/go-plugin/runner"
13 | )
14 | 
15 | // ReattachFunc returns a function that allows reattaching to a plugin running
16 | // as a plain process. The process may or may not be a child process.
17 | func ReattachFunc(pid int, addr net.Addr) runner.ReattachFunc {
18 | 	return func() (runner.AttachedRunner, error) {
19 | 		p, err := os.FindProcess(pid)
20 | 		if err != nil {
21 | 			// On Unix systems, FindProcess never returns an error.
22 | 			// On Windows, for non-existent pids it returns:
23 | 			// os.SyscallError - 'OpenProcess: the paremter is incorrect'
24 | 			return nil, ErrProcessNotFound
25 | 		}
26 | 
27 | 		// Attempt to connect to the addr since on Unix systems FindProcess
28 | 		// doesn't actually return an error if it can't find the process.
29 | 		conn, err := net.Dial(addr.Network(), addr.String())
30 | 		if err != nil {
31 | 			return nil, ErrProcessNotFound
32 | 		}
33 | 		_ = conn.Close()
34 | 
35 | 		return &CmdAttachedRunner{
36 | 			pid:     pid,
37 | 			process: p,
38 | 		}, nil
39 | 	}
40 | }
41 | 
42 | // CmdAttachedRunner is mostly a subset of CmdRunner, except the Wait function
43 | // does not assume the process is a child of the host process, and so uses a
44 | // different implementation to wait on the process.
45 | type CmdAttachedRunner struct {
46 | 	pid     int
47 | 	process *os.Process
48 | 
49 | 	addrTranslator
50 | }
51 | 
52 | func (c *CmdAttachedRunner) Wait(_ context.Context) error {
53 | 	return pidWait(c.pid)
54 | }
55 | 
56 | func (c *CmdAttachedRunner) Kill(_ context.Context) error {
57 | 	return c.process.Kill()
58 | }
59 | 
60 | func (c *CmdAttachedRunner) ID() string {
61 | 	return fmt.Sprintf("%d", c.pid)
62 | }
63 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/cmd_runner.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package cmdrunner
  5 | 
  6 | import (
  7 | 	"context"
  8 | 	"errors"
  9 | 	"fmt"
 10 | 	"io"
 11 | 	"os"
 12 | 	"os/exec"
 13 | 
 14 | 	"github.com/hashicorp/go-hclog"
 15 | 	"github.com/hashicorp/go-plugin/runner"
 16 | )
 17 | 
 18 | var (
 19 | 	_ runner.Runner = (*CmdRunner)(nil)
 20 | 
 21 | 	// ErrProcessNotFound is returned when a client is instantiated to
 22 | 	// reattach to an existing process and it isn't found.
 23 | 	ErrProcessNotFound = errors.New("reattachment process not found")
 24 | )
 25 | 
 26 | const unrecognizedRemotePluginMessage = `This usually means
 27 |   the plugin was not compiled for this architecture,
 28 |   the plugin is missing dynamic-link libraries necessary to run,
 29 |   the plugin is not executable by this process due to file permissions, or
 30 |   the plugin failed to negotiate the initial go-plugin protocol handshake
 31 | %s`
 32 | 
 33 | // CmdRunner implements the runner.Runner interface. It mostly just passes through
 34 | // to exec.Cmd methods.
 35 | type CmdRunner struct {
 36 | 	logger hclog.Logger
 37 | 	cmd    *exec.Cmd
 38 | 
 39 | 	stdout io.ReadCloser
 40 | 	stderr io.ReadCloser
 41 | 
 42 | 	// Cmd info is persisted early, since the process information will be removed
 43 | 	// after Kill is called.
 44 | 	path string
 45 | 	pid  int
 46 | 
 47 | 	addrTranslator
 48 | }
 49 | 
 50 | // NewCmdRunner returns an implementation of runner.Runner for running a plugin
 51 | // as a subprocess. It must be passed a cmd that hasn't yet been started.
 52 | func NewCmdRunner(logger hclog.Logger, cmd *exec.Cmd) (*CmdRunner, error) {
 53 | 	stdout, err := cmd.StdoutPipe()
 54 | 	if err != nil {
 55 | 		return nil, err
 56 | 	}
 57 | 
 58 | 	stderr, err := cmd.StderrPipe()
 59 | 	if err != nil {
 60 | 		return nil, err
 61 | 	}
 62 | 
 63 | 	return &CmdRunner{
 64 | 		logger: logger,
 65 | 		cmd:    cmd,
 66 | 		stdout: stdout,
 67 | 		stderr: stderr,
 68 | 		path:   cmd.Path,
 69 | 	}, nil
 70 | }
 71 | 
 72 | func (c *CmdRunner) Start(_ context.Context) error {
 73 | 	c.logger.Debug("starting plugin", "path", c.cmd.Path, "args", c.cmd.Args)
 74 | 	err := c.cmd.Start()
 75 | 	if err != nil {
 76 | 		return err
 77 | 	}
 78 | 
 79 | 	c.pid = c.cmd.Process.Pid
 80 | 	c.logger.Debug("plugin started", "path", c.path, "pid", c.pid)
 81 | 	return nil
 82 | }
 83 | 
 84 | func (c *CmdRunner) Wait(_ context.Context) error {
 85 | 	return c.cmd.Wait()
 86 | }
 87 | 
 88 | func (c *CmdRunner) Kill(_ context.Context) error {
 89 | 	if c.cmd.Process != nil {
 90 | 		err := c.cmd.Process.Kill()
 91 | 		// Swallow ErrProcessDone, we support calling Kill multiple times.
 92 | 		if !errors.Is(err, os.ErrProcessDone) {
 93 | 			return err
 94 | 		}
 95 | 		return nil
 96 | 	}
 97 | 
 98 | 	return nil
 99 | }
100 | 
101 | func (c *CmdRunner) Stdout() io.ReadCloser {
102 | 	return c.stdout
103 | }
104 | 
105 | func (c *CmdRunner) Stderr() io.ReadCloser {
106 | 	return c.stderr
107 | }
108 | 
109 | func (c *CmdRunner) Name() string {
110 | 	return c.path
111 | }
112 | 
113 | func (c *CmdRunner) ID() string {
114 | 	return fmt.Sprintf("%d", c.pid)
115 | }
116 | 
117 | // peTypes is a list of Portable Executable (PE) machine types from https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
118 | // mapped to GOARCH types. It is not comprehensive, and only includes machine types that Go supports.
119 | var peTypes = map[uint16]string{
120 | 	0x14c:  "386",
121 | 	0x1c0:  "arm",
122 | 	0x6264: "loong64",
123 | 	0x8664: "amd64",
124 | 	0xaa64: "arm64",
125 | }
126 | 
127 | func (c *CmdRunner) Diagnose(_ context.Context) string {
128 | 	return fmt.Sprintf(unrecognizedRemotePluginMessage, additionalNotesAboutCommand(c.cmd.Path))
129 | }
130 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/cmd_runner_test.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package cmdrunner
 5 | 
 6 | import (
 7 | 	"os"
 8 | 	"path/filepath"
 9 | 	"strings"
10 | 	"testing"
11 | )
12 | 
13 | func TestAdditionalNotesAboutCommand(t *testing.T) {
14 | 	files := []string{
15 | 		"windows-amd64.exe",
16 | 		"windows-386.exe",
17 | 		"linux-amd64",
18 | 		"darwin-amd64",
19 | 		"darwin-arm64",
20 | 	}
21 | 	for _, file := range files {
22 | 		fullFile := filepath.Join("testdata", file)
23 | 		if _, err := os.Stat(fullFile); os.IsNotExist(err) {
24 | 			t.Skipf("testdata executables not present; please run 'make' in testdata/ directory for this test")
25 | 		}
26 | 
27 | 		notes := additionalNotesAboutCommand(fullFile)
28 | 		if strings.Contains(file, "windows") && !strings.Contains(notes, "PE") {
29 | 			t.Errorf("Expected notes to contain Windows information:\n%s", notes)
30 | 		}
31 | 		if strings.Contains(file, "linux") && !strings.Contains(notes, "ELF") {
32 | 			t.Errorf("Expected notes to contain Linux information:\n%s", notes)
33 | 		}
34 | 		if strings.Contains(file, "darwin") && !strings.Contains(notes, "MachO") {
35 | 			t.Errorf("Expected notes to contain macOS information:\n%s", notes)
36 | 		}
37 | 
38 | 		if strings.Contains(file, "amd64") && !strings.Contains(notes, "amd64") && !strings.Contains(notes, "EM_X86_64") && !strings.Contains(notes, "CpuAmd64") {
39 | 			t.Errorf("Expected notes to contain amd64 information:\n%s", notes)
40 | 		}
41 | 
42 | 		if strings.Contains(file, "arm64") && !strings.Contains(notes, "CpuArm64") {
43 | 			t.Errorf("Expected notes to contain arm64 information:\n%s", notes)
44 | 		}
45 | 		if strings.Contains(file, "386") && !strings.Contains(notes, "386") {
46 | 			t.Errorf("Expected notes to contain 386 information:\n%s", notes)
47 | 		}
48 | 
49 | 	}
50 | }
51 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/notes_unix.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | //go:build !windows
 5 | // +build !windows
 6 | 
 7 | package cmdrunner
 8 | 
 9 | import (
10 | 	"debug/elf"
11 | 	"debug/macho"
12 | 	"debug/pe"
13 | 	"fmt"
14 | 	"os"
15 | 	"os/user"
16 | 	"runtime"
17 | 	"strconv"
18 | 	"syscall"
19 | )
20 | 
21 | // additionalNotesAboutCommand tries to get additional information about a command that might help diagnose
22 | // why it won't run correctly. It runs as a best effort only.
23 | func additionalNotesAboutCommand(path string) string {
24 | 	notes := ""
25 | 	stat, err := os.Stat(path)
26 | 	if err != nil {
27 | 		return notes
28 | 	}
29 | 
30 | 	notes += "\nAdditional notes about plugin:\n"
31 | 	notes += fmt.Sprintf("  Path: %s\n", path)
32 | 	notes += fmt.Sprintf("  Mode: %s\n", stat.Mode())
33 | 	statT, ok := stat.Sys().(*syscall.Stat_t)
34 | 	if ok {
35 | 		currentUsername := "?"
36 | 		if u, err := user.LookupId(strconv.FormatUint(uint64(os.Getuid()), 10)); err == nil {
37 | 			currentUsername = u.Username
38 | 		}
39 | 		currentGroup := "?"
40 | 		if g, err := user.LookupGroupId(strconv.FormatUint(uint64(os.Getgid()), 10)); err == nil {
41 | 			currentGroup = g.Name
42 | 		}
43 | 		username := "?"
44 | 		if u, err := user.LookupId(strconv.FormatUint(uint64(statT.Uid), 10)); err == nil {
45 | 			username = u.Username
46 | 		}
47 | 		group := "?"
48 | 		if g, err := user.LookupGroupId(strconv.FormatUint(uint64(statT.Gid), 10)); err == nil {
49 | 			group = g.Name
50 | 		}
51 | 		notes += fmt.Sprintf("  Owner: %d [%s] (current: %d [%s])\n", statT.Uid, username, os.Getuid(), currentUsername)
52 | 		notes += fmt.Sprintf("  Group: %d [%s] (current: %d [%s])\n", statT.Gid, group, os.Getgid(), currentGroup)
53 | 	}
54 | 
55 | 	if elfFile, err := elf.Open(path); err == nil {
56 | 		defer func() { _ = elfFile.Close() }()
57 | 		notes += fmt.Sprintf("  ELF architecture: %s (current architecture: %s)\n", elfFile.Machine, runtime.GOARCH)
58 | 	} else if machoFile, err := macho.Open(path); err == nil {
59 | 		defer func() { _ = machoFile.Close() }()
60 | 		notes += fmt.Sprintf("  MachO architecture: %s (current architecture: %s)\n", machoFile.Cpu, runtime.GOARCH)
61 | 	} else if peFile, err := pe.Open(path); err == nil {
62 | 		defer func() { _ = peFile.Close() }()
63 | 		machine, ok := peTypes[peFile.Machine]
64 | 		if !ok {
65 | 			machine = "unknown"
66 | 		}
67 | 		notes += fmt.Sprintf("  PE architecture: %s (current architecture: %s)\n", machine, runtime.GOARCH)
68 | 	}
69 | 	return notes
70 | }
71 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/notes_windows.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | //go:build windows
 5 | // +build windows
 6 | 
 7 | package cmdrunner
 8 | 
 9 | import (
10 | 	"debug/elf"
11 | 	"debug/macho"
12 | 	"debug/pe"
13 | 	"fmt"
14 | 	"os"
15 | 	"runtime"
16 | )
17 | 
18 | // additionalNotesAboutCommand tries to get additional information about a command that might help diagnose
19 | // why it won't run correctly. It runs as a best effort only.
20 | func additionalNotesAboutCommand(path string) string {
21 | 	notes := ""
22 | 	stat, err := os.Stat(path)
23 | 	if err != nil {
24 | 		return notes
25 | 	}
26 | 
27 | 	notes += "\nAdditional notes about plugin:\n"
28 | 	notes += fmt.Sprintf("  Path: %s\n", path)
29 | 	notes += fmt.Sprintf("  Mode: %s\n", stat.Mode())
30 | 
31 | 	if elfFile, err := elf.Open(path); err == nil {
32 | 		defer elfFile.Close()
33 | 		notes += fmt.Sprintf("  ELF architecture: %s (current architecture: %s)\n", elfFile.Machine, runtime.GOARCH)
34 | 	} else if machoFile, err := macho.Open(path); err == nil {
35 | 		defer machoFile.Close()
36 | 		notes += fmt.Sprintf("  MachO architecture: %s (current architecture: %s)\n", machoFile.Cpu, runtime.GOARCH)
37 | 	} else if peFile, err := pe.Open(path); err == nil {
38 | 		defer peFile.Close()
39 | 		machine, ok := peTypes[peFile.Machine]
40 | 		if !ok {
41 | 			machine = "unknown"
42 | 		}
43 | 		notes += fmt.Sprintf("  PE architecture: %s (current architecture: %s)\n", machine, runtime.GOARCH)
44 | 	}
45 | 	return notes
46 | }
47 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/process.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package cmdrunner
 5 | 
 6 | import "time"
 7 | 
 8 | // pidAlive checks whether a pid is alive.
 9 | func pidAlive(pid int) bool {
10 | 	return _pidAlive(pid)
11 | }
12 | 
13 | // pidWait blocks for a process to exit.
14 | func pidWait(pid int) error {
15 | 	ticker := time.NewTicker(1 * time.Second)
16 | 	defer ticker.Stop()
17 | 
18 | 	for range ticker.C {
19 | 		if !pidAlive(pid) {
20 | 			break
21 | 		}
22 | 	}
23 | 
24 | 	return nil
25 | }
26 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/process_posix.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | //go:build !windows
 5 | // +build !windows
 6 | 
 7 | package cmdrunner
 8 | 
 9 | import (
10 | 	"os"
11 | 	"syscall"
12 | )
13 | 
14 | // _pidAlive tests whether a process is alive or not by sending it Signal 0,
15 | // since Go otherwise has no way to test this.
16 | func _pidAlive(pid int) bool {
17 | 	proc, err := os.FindProcess(pid)
18 | 	if err == nil {
19 | 		err = proc.Signal(syscall.Signal(0))
20 | 	}
21 | 
22 | 	return err == nil
23 | }
24 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/process_windows.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package cmdrunner
 5 | 
 6 | import (
 7 | 	"syscall"
 8 | )
 9 | 
10 | const (
11 | 	// Weird name but matches the MSDN docs
12 | 	exit_STILL_ACTIVE = 259
13 | 
14 | 	processDesiredAccess = syscall.STANDARD_RIGHTS_READ |
15 | 		syscall.PROCESS_QUERY_INFORMATION |
16 | 		syscall.SYNCHRONIZE
17 | )
18 | 
19 | // _pidAlive tests whether a process is alive or not
20 | func _pidAlive(pid int) bool {
21 | 	h, err := syscall.OpenProcess(processDesiredAccess, false, uint32(pid))
22 | 	if err != nil {
23 | 		return false
24 | 	}
25 | 	defer syscall.CloseHandle(h)
26 | 
27 | 	var ec uint32
28 | 	if e := syscall.GetExitCodeProcess(h, &ec); e != nil {
29 | 		return false
30 | 	}
31 | 
32 | 	return ec == exit_STILL_ACTIVE
33 | }
34 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/testdata/.gitignore:
--------------------------------------------------------------------------------
1 | windows-amd64.exe
2 | windows-386.exe
3 | linux-amd64
4 | darwin-amd64
5 | darwin-arm64
6 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/testdata/Makefile:
--------------------------------------------------------------------------------
 1 | default: all
 2 | .PHONY: default
 3 | 
 4 | .PHONY: clean
 5 | clean:
 6 | 	rm -f windows-amd64.exe windows-386.exe linux-amd64 darwin-amd64 darwin-arm64
 7 | 
 8 | .PHONY: all
 9 | all: windows-amd64.exe windows-386.exe linux-amd64 darwin-amd64 darwin-arm64
10 | 
11 | .PHONY: windows-amd64.exe
12 | windows-amd64.exe:
13 | 	GOOS=windows GOARCH=amd64 go build -o $@
14 | 
15 | .PHONY: windows-386.exe
16 | windows-386.exe:
17 | 	GOOS=windows GOARCH=386 go build -o $@
18 | 
19 | .PHONY: linux-amd64
20 | linux-amd64:
21 | 	GOOS=linux GOARCH=amd64 go build -o $@
22 | 
23 | .PHONY: darwin-amd64
24 | darwin-amd64:
25 | 	GOOS=darwin GOARCH=amd64 go build -o $@
26 | 
27 | .PHONY: darwin-arm64
28 | darwin-arm64:
29 | 	GOOS=darwin GOARCH=arm64 go build -o $@


--------------------------------------------------------------------------------
/internal/cmdrunner/testdata/README.md:
--------------------------------------------------------------------------------
1 | This folder contains a minimal Go program so that we can obtain example binaries of programs for various architectures for use in tests.


--------------------------------------------------------------------------------
/internal/cmdrunner/testdata/go.mod:
--------------------------------------------------------------------------------
 1 | module example.com/testdata
 2 | 
 3 | go 1.24
 4 | 
 5 | replace github.com/hashicorp/go-plugin => ../../../
 6 | 
 7 | require github.com/hashicorp/go-plugin v1.4.7
 8 | 
 9 | require google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
10 | 
11 | require (
12 | 	github.com/fatih/color v1.13.0 // indirect
13 | 	github.com/golang/protobuf v1.5.4 // indirect
14 | 	github.com/hashicorp/go-hclog v1.6.3 // indirect
15 | 	github.com/hashicorp/yamux v0.1.2 // indirect
16 | 	github.com/mattn/go-colorable v0.1.12 // indirect
17 | 	github.com/mattn/go-isatty v0.0.17 // indirect
18 | 	github.com/oklog/run v1.1.0 // indirect
19 | 	golang.org/x/net v0.38.0 // indirect
20 | 	golang.org/x/sys v0.31.0 // indirect
21 | 	golang.org/x/text v0.23.0 // indirect
22 | 	google.golang.org/grpc v1.73.0 // indirect
23 | 	google.golang.org/protobuf v1.36.6 // indirect
24 | )
25 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/testdata/go.sum:
--------------------------------------------------------------------------------
 1 | github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
 2 | github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 6 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
 7 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 8 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 9 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
10 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
11 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
12 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
13 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
14 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
15 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
16 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
17 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
18 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
19 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
20 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
21 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
22 | github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
23 | github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
24 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
25 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
26 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
27 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
28 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
29 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
30 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
31 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
32 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
33 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
35 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
36 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
37 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
38 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
39 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
40 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
41 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
42 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
43 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
44 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
45 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
46 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
47 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
48 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
49 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
50 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
51 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
52 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
53 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
54 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
55 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
56 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
57 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
58 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
59 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
60 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
61 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
62 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
63 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
64 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
65 | google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
66 | google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
67 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
68 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
69 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
70 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
71 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
72 | 


--------------------------------------------------------------------------------
/internal/cmdrunner/testdata/minimal.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package main
 5 | 
 6 | import "github.com/hashicorp/go-plugin"
 7 | 
 8 | func main() {
 9 | 	plugin.Serve(nil)
10 | }
11 | 


--------------------------------------------------------------------------------
/internal/grpcmux/blocked_client_listener.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package grpcmux
 5 | 
 6 | import (
 7 | 	"io"
 8 | 	"net"
 9 | 
10 | 	"github.com/hashicorp/yamux"
11 | )
12 | 
13 | var _ net.Listener = (*blockedClientListener)(nil)
14 | 
15 | // blockedClientListener accepts connections for a specific gRPC broker stream
16 | // ID on the client (host) side of the connection.
17 | type blockedClientListener struct {
18 | 	session *yamux.Session
19 | 	waitCh  chan struct{}
20 | 	doneCh  <-chan struct{}
21 | }
22 | 
23 | func newBlockedClientListener(session *yamux.Session, doneCh <-chan struct{}) *blockedClientListener {
24 | 	return &blockedClientListener{
25 | 		waitCh:  make(chan struct{}, 1),
26 | 		doneCh:  doneCh,
27 | 		session: session,
28 | 	}
29 | }
30 | 
31 | func (b *blockedClientListener) Accept() (net.Conn, error) {
32 | 	select {
33 | 	case <-b.waitCh:
34 | 		return b.session.Accept()
35 | 	case <-b.doneCh:
36 | 		return nil, io.EOF
37 | 	}
38 | }
39 | 
40 | func (b *blockedClientListener) Addr() net.Addr {
41 | 	return b.session.Addr()
42 | }
43 | 
44 | func (b *blockedClientListener) Close() error {
45 | 	// We don't close the session, the client muxer is responsible for that.
46 | 	return nil
47 | }
48 | 
49 | func (b *blockedClientListener) unblock() {
50 | 	b.waitCh <- struct{}{}
51 | }
52 | 


--------------------------------------------------------------------------------
/internal/grpcmux/blocked_server_listener.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package grpcmux
 5 | 
 6 | import (
 7 | 	"io"
 8 | 	"net"
 9 | )
10 | 
11 | var _ net.Listener = (*blockedServerListener)(nil)
12 | 
13 | // blockedServerListener accepts connections for a specific gRPC broker stream
14 | // ID on the server (plugin) side of the connection.
15 | type blockedServerListener struct {
16 | 	addr     net.Addr
17 | 	acceptCh chan acceptResult
18 | 	doneCh   <-chan struct{}
19 | }
20 | 
21 | type acceptResult struct {
22 | 	conn net.Conn
23 | 	err  error
24 | }
25 | 
26 | func newBlockedServerListener(addr net.Addr, doneCh <-chan struct{}) *blockedServerListener {
27 | 	return &blockedServerListener{
28 | 		addr:     addr,
29 | 		acceptCh: make(chan acceptResult),
30 | 		doneCh:   doneCh,
31 | 	}
32 | }
33 | 
34 | func (b *blockedServerListener) Accept() (net.Conn, error) {
35 | 	select {
36 | 	case accept := <-b.acceptCh:
37 | 		return accept.conn, accept.err
38 | 	case <-b.doneCh:
39 | 		return nil, io.EOF
40 | 	}
41 | }
42 | 
43 | func (b *blockedServerListener) Addr() net.Addr {
44 | 	return b.addr
45 | }
46 | 
47 | func (b *blockedServerListener) Close() error {
48 | 	return nil
49 | }
50 | 


--------------------------------------------------------------------------------
/internal/grpcmux/grpc_client_muxer.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package grpcmux
  5 | 
  6 | import (
  7 | 	"fmt"
  8 | 	"net"
  9 | 	"sync"
 10 | 
 11 | 	"github.com/hashicorp/go-hclog"
 12 | 	"github.com/hashicorp/yamux"
 13 | )
 14 | 
 15 | var _ GRPCMuxer = (*GRPCClientMuxer)(nil)
 16 | 
 17 | // GRPCClientMuxer implements the client (host) side of the gRPC broker's
 18 | // GRPCMuxer interface for multiplexing multiple gRPC broker connections over
 19 | // a single net.Conn.
 20 | //
 21 | // The client dials the initial net.Conn eagerly, and creates a yamux.Session
 22 | // as the implementation for multiplexing any additional connections.
 23 | //
 24 | // Each net.Listener returned from Listener will block until the client receives
 25 | // a knock that matches its gRPC broker stream ID. There is no default listener
 26 | // on the client, as it is a client for the gRPC broker's control services. (See
 27 | // GRPCServerMuxer for more details).
 28 | type GRPCClientMuxer struct {
 29 | 	logger  hclog.Logger
 30 | 	session *yamux.Session
 31 | 
 32 | 	acceptMutex     sync.Mutex
 33 | 	acceptListeners map[uint32]*blockedClientListener
 34 | }
 35 | 
 36 | func NewGRPCClientMuxer(logger hclog.Logger, addr net.Addr) (*GRPCClientMuxer, error) {
 37 | 	// Eagerly establish the underlying connection as early as possible.
 38 | 	logger.Debug("making new client mux initial connection", "addr", addr)
 39 | 	conn, err := net.Dial(addr.Network(), addr.String())
 40 | 	if err != nil {
 41 | 		return nil, err
 42 | 	}
 43 | 	if tcpConn, ok := conn.(*net.TCPConn); ok {
 44 | 		// Make sure to set keep alive so that the connection doesn't die
 45 | 		_ = tcpConn.SetKeepAlive(true)
 46 | 	}
 47 | 
 48 | 	cfg := yamux.DefaultConfig()
 49 | 	cfg.Logger = logger.Named("yamux").StandardLogger(&hclog.StandardLoggerOptions{
 50 | 		InferLevels: true,
 51 | 	})
 52 | 	cfg.LogOutput = nil
 53 | 	sess, err := yamux.Client(conn, cfg)
 54 | 	if err != nil {
 55 | 		return nil, err
 56 | 	}
 57 | 
 58 | 	logger.Debug("client muxer connected", "addr", addr)
 59 | 	m := &GRPCClientMuxer{
 60 | 		logger:          logger,
 61 | 		session:         sess,
 62 | 		acceptListeners: make(map[uint32]*blockedClientListener),
 63 | 	}
 64 | 
 65 | 	return m, nil
 66 | }
 67 | 
 68 | func (m *GRPCClientMuxer) Enabled() bool {
 69 | 	return m != nil
 70 | }
 71 | 
 72 | func (m *GRPCClientMuxer) Listener(id uint32, doneCh <-chan struct{}) (net.Listener, error) {
 73 | 	ln := newBlockedClientListener(m.session, doneCh)
 74 | 
 75 | 	m.acceptMutex.Lock()
 76 | 	m.acceptListeners[id] = ln
 77 | 	m.acceptMutex.Unlock()
 78 | 
 79 | 	return ln, nil
 80 | }
 81 | 
 82 | func (m *GRPCClientMuxer) AcceptKnock(id uint32) error {
 83 | 	m.acceptMutex.Lock()
 84 | 	defer m.acceptMutex.Unlock()
 85 | 
 86 | 	ln, ok := m.acceptListeners[id]
 87 | 	if !ok {
 88 | 		return fmt.Errorf("no listener for id %d", id)
 89 | 	}
 90 | 	ln.unblock()
 91 | 	return nil
 92 | }
 93 | 
 94 | func (m *GRPCClientMuxer) Dial() (net.Conn, error) {
 95 | 	stream, err := m.session.Open()
 96 | 	if err != nil {
 97 | 		return nil, fmt.Errorf("error dialling new client stream: %w", err)
 98 | 	}
 99 | 
100 | 	return stream, nil
101 | }
102 | 
103 | func (m *GRPCClientMuxer) Close() error {
104 | 	return m.session.Close()
105 | }
106 | 


--------------------------------------------------------------------------------
/internal/grpcmux/grpc_muxer.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package grpcmux
 5 | 
 6 | import (
 7 | 	"net"
 8 | )
 9 | 
10 | // GRPCMuxer enables multiple implementations of net.Listener to accept
11 | // connections over a single "main" multiplexed net.Conn, and dial multiple
12 | // client connections over the same multiplexed net.Conn.
13 | //
14 | // The first multiplexed connection is used to serve the gRPC broker's own
15 | // control services: plugin.GRPCBroker, plugin.GRPCController, plugin.GRPCStdio.
16 | //
17 | // Clients must "knock" before dialling, to tell the server side that the
18 | // next net.Conn should be accepted onto a specific stream ID. The knock is a
19 | // bidirectional streaming message on the plugin.GRPCBroker service.
20 | type GRPCMuxer interface {
21 | 	// Enabled determines whether multiplexing should be used. It saves users
22 | 	// of the interface from having to compare an interface with nil, which
23 | 	// is a bit awkward to do correctly.
24 | 	Enabled() bool
25 | 
26 | 	// Listener returns a multiplexed listener that will wait until AcceptKnock
27 | 	// is called with a matching ID before its Accept function returns.
28 | 	Listener(id uint32, doneCh <-chan struct{}) (net.Listener, error)
29 | 
30 | 	// AcceptKnock unblocks the listener with the matching ID, and returns an
31 | 	// error if it hasn't been created yet.
32 | 	AcceptKnock(id uint32) error
33 | 
34 | 	// Dial makes a new multiplexed client connection. To dial a specific ID,
35 | 	// a knock must be sent first.
36 | 	Dial() (net.Conn, error)
37 | 
38 | 	// Close closes connections and releases any resources associated with the
39 | 	// muxer.
40 | 	Close() error
41 | }
42 | 


--------------------------------------------------------------------------------
/internal/grpcmux/grpc_server_muxer.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package grpcmux
  5 | 
  6 | import (
  7 | 	"errors"
  8 | 	"fmt"
  9 | 	"net"
 10 | 	"sync"
 11 | 	"time"
 12 | 
 13 | 	"github.com/hashicorp/go-hclog"
 14 | 	"github.com/hashicorp/yamux"
 15 | )
 16 | 
 17 | var _ GRPCMuxer = (*GRPCServerMuxer)(nil)
 18 | var _ net.Listener = (*GRPCServerMuxer)(nil)
 19 | 
 20 | // GRPCServerMuxer implements the server (plugin) side of the gRPC broker's
 21 | // GRPCMuxer interface for multiplexing multiple gRPC broker connections over
 22 | // a single net.Conn.
 23 | //
 24 | // The server side needs a listener to serve the gRPC broker's control services,
 25 | // which includes the service we will receive knocks on. That means we always
 26 | // accept the first connection onto a "default" main listener, and if we accept
 27 | // any further connections without receiving a knock first, they are also given
 28 | // to the default listener.
 29 | //
 30 | // When creating additional multiplexed listeners for specific stream IDs, we
 31 | // can't control the order in which gRPC servers will call Accept() on each
 32 | // listener, but we do need to control which gRPC server accepts which connection.
 33 | // As such, each multiplexed listener blocks waiting on a channel. It will be
 34 | // unblocked when a knock is received for the matching stream ID.
 35 | type GRPCServerMuxer struct {
 36 | 	addr   net.Addr
 37 | 	logger hclog.Logger
 38 | 
 39 | 	sessionErrCh chan error
 40 | 	sess         *yamux.Session
 41 | 
 42 | 	knockCh chan uint32
 43 | 
 44 | 	acceptMutex    sync.Mutex
 45 | 	acceptChannels map[uint32]chan acceptResult
 46 | }
 47 | 
 48 | func NewGRPCServerMuxer(logger hclog.Logger, ln net.Listener) *GRPCServerMuxer {
 49 | 	m := &GRPCServerMuxer{
 50 | 		addr:   ln.Addr(),
 51 | 		logger: logger,
 52 | 
 53 | 		sessionErrCh: make(chan error),
 54 | 
 55 | 		knockCh:        make(chan uint32, 1),
 56 | 		acceptChannels: make(map[uint32]chan acceptResult),
 57 | 	}
 58 | 
 59 | 	go m.acceptSession(ln)
 60 | 
 61 | 	return m
 62 | }
 63 | 
 64 | // acceptSessionAndMuxAccept is responsible for establishing the yamux session,
 65 | // and then kicking off the acceptLoop function.
 66 | func (m *GRPCServerMuxer) acceptSession(ln net.Listener) {
 67 | 	defer close(m.sessionErrCh)
 68 | 
 69 | 	m.logger.Debug("accepting initial connection", "addr", m.addr)
 70 | 	conn, err := ln.Accept()
 71 | 	if err != nil {
 72 | 		m.sessionErrCh <- err
 73 | 		return
 74 | 	}
 75 | 
 76 | 	m.logger.Debug("initial server connection accepted", "addr", m.addr)
 77 | 	cfg := yamux.DefaultConfig()
 78 | 	cfg.Logger = m.logger.Named("yamux").StandardLogger(&hclog.StandardLoggerOptions{
 79 | 		InferLevels: true,
 80 | 	})
 81 | 	cfg.LogOutput = nil
 82 | 	m.sess, err = yamux.Server(conn, cfg)
 83 | 	if err != nil {
 84 | 		m.sessionErrCh <- err
 85 | 		return
 86 | 	}
 87 | }
 88 | 
 89 | func (m *GRPCServerMuxer) session() (*yamux.Session, error) {
 90 | 	select {
 91 | 	case err := <-m.sessionErrCh:
 92 | 		if err != nil {
 93 | 			return nil, err
 94 | 		}
 95 | 	case <-time.After(5 * time.Second):
 96 | 		return nil, errors.New("timed out waiting for connection to be established")
 97 | 	}
 98 | 
 99 | 	// Should never happen.
100 | 	if m.sess == nil {
101 | 		return nil, errors.New("no connection established and no error received")
102 | 	}
103 | 
104 | 	return m.sess, nil
105 | }
106 | 
107 | // Accept accepts all incoming connections and routes them to the correct
108 | // stream ID based on the most recent knock received.
109 | func (m *GRPCServerMuxer) Accept() (net.Conn, error) {
110 | 	session, err := m.session()
111 | 	if err != nil {
112 | 		return nil, fmt.Errorf("error establishing yamux session: %w", err)
113 | 	}
114 | 
115 | 	for {
116 | 		conn, acceptErr := session.Accept()
117 | 
118 | 		select {
119 | 		case id := <-m.knockCh:
120 | 			m.acceptMutex.Lock()
121 | 			acceptCh, ok := m.acceptChannels[id]
122 | 			m.acceptMutex.Unlock()
123 | 
124 | 			if !ok {
125 | 				if conn != nil {
126 | 					_ = conn.Close()
127 | 				}
128 | 				return nil, fmt.Errorf("received knock on ID %d that doesn't have a listener", id)
129 | 			}
130 | 			m.logger.Debug("sending conn to brokered listener", "id", id)
131 | 			acceptCh <- acceptResult{
132 | 				conn: conn,
133 | 				err:  acceptErr,
134 | 			}
135 | 		default:
136 | 			m.logger.Debug("sending conn to default listener")
137 | 			return conn, acceptErr
138 | 		}
139 | 	}
140 | }
141 | 
142 | func (m *GRPCServerMuxer) Addr() net.Addr {
143 | 	return m.addr
144 | }
145 | 
146 | func (m *GRPCServerMuxer) Close() error {
147 | 	session, err := m.session()
148 | 	if err != nil {
149 | 		return err
150 | 	}
151 | 
152 | 	return session.Close()
153 | }
154 | 
155 | func (m *GRPCServerMuxer) Enabled() bool {
156 | 	return m != nil
157 | }
158 | 
159 | func (m *GRPCServerMuxer) Listener(id uint32, doneCh <-chan struct{}) (net.Listener, error) {
160 | 	sess, err := m.session()
161 | 	if err != nil {
162 | 		return nil, err
163 | 	}
164 | 
165 | 	ln := newBlockedServerListener(sess.Addr(), doneCh)
166 | 	m.acceptMutex.Lock()
167 | 	m.acceptChannels[id] = ln.acceptCh
168 | 	m.acceptMutex.Unlock()
169 | 
170 | 	return ln, nil
171 | }
172 | 
173 | func (m *GRPCServerMuxer) Dial() (net.Conn, error) {
174 | 	sess, err := m.session()
175 | 	if err != nil {
176 | 		return nil, err
177 | 	}
178 | 
179 | 	stream, err := sess.OpenStream()
180 | 	if err != nil {
181 | 		return nil, fmt.Errorf("error dialling new server stream: %w", err)
182 | 	}
183 | 
184 | 	return stream, nil
185 | }
186 | 
187 | func (m *GRPCServerMuxer) AcceptKnock(id uint32) error {
188 | 	m.knockCh <- id
189 | 	return nil
190 | }
191 | 


--------------------------------------------------------------------------------
/internal/plugin/grpc_broker.proto:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | syntax = "proto3";
 5 | package plugin;
 6 | option go_package = "./plugin";
 7 | 
 8 | message ConnInfo {
 9 |     uint32 service_id = 1;
10 |     string network = 2;
11 |     string address = 3;
12 |     message Knock {
13 |         bool knock = 1;
14 |         bool ack = 2;
15 |         string error = 3;
16 |     }
17 |     Knock knock = 4;
18 | }
19 | 
20 | service GRPCBroker {
21 |     rpc StartStream(stream ConnInfo) returns (stream ConnInfo);
22 | }
23 | 


--------------------------------------------------------------------------------
/internal/plugin/grpc_broker_grpc.pb.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
  5 | // versions:
  6 | // - protoc-gen-go-grpc v1.3.0
  7 | // - protoc             (unknown)
  8 | // source: internal/plugin/grpc_broker.proto
  9 | 
 10 | package plugin
 11 | 
 12 | import (
 13 | 	context "context"
 14 | 	grpc "google.golang.org/grpc"
 15 | 	codes "google.golang.org/grpc/codes"
 16 | 	status "google.golang.org/grpc/status"
 17 | )
 18 | 
 19 | // This is a compile-time assertion to ensure that this generated file
 20 | // is compatible with the grpc package it is being compiled against.
 21 | // Requires gRPC-Go v1.32.0 or later.
 22 | const _ = grpc.SupportPackageIsVersion7
 23 | 
 24 | const (
 25 | 	GRPCBroker_StartStream_FullMethodName = "/plugin.GRPCBroker/StartStream"
 26 | )
 27 | 
 28 | // GRPCBrokerClient is the client API for GRPCBroker service.
 29 | //
 30 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
 31 | type GRPCBrokerClient interface {
 32 | 	StartStream(ctx context.Context, opts ...grpc.CallOption) (GRPCBroker_StartStreamClient, error)
 33 | }
 34 | 
 35 | type gRPCBrokerClient struct {
 36 | 	cc grpc.ClientConnInterface
 37 | }
 38 | 
 39 | func NewGRPCBrokerClient(cc grpc.ClientConnInterface) GRPCBrokerClient {
 40 | 	return &gRPCBrokerClient{cc}
 41 | }
 42 | 
 43 | func (c *gRPCBrokerClient) StartStream(ctx context.Context, opts ...grpc.CallOption) (GRPCBroker_StartStreamClient, error) {
 44 | 	stream, err := c.cc.NewStream(ctx, &GRPCBroker_ServiceDesc.Streams[0], GRPCBroker_StartStream_FullMethodName, opts...)
 45 | 	if err != nil {
 46 | 		return nil, err
 47 | 	}
 48 | 	x := &gRPCBrokerStartStreamClient{stream}
 49 | 	return x, nil
 50 | }
 51 | 
 52 | type GRPCBroker_StartStreamClient interface {
 53 | 	Send(*ConnInfo) error
 54 | 	Recv() (*ConnInfo, error)
 55 | 	grpc.ClientStream
 56 | }
 57 | 
 58 | type gRPCBrokerStartStreamClient struct {
 59 | 	grpc.ClientStream
 60 | }
 61 | 
 62 | func (x *gRPCBrokerStartStreamClient) Send(m *ConnInfo) error {
 63 | 	return x.ClientStream.SendMsg(m)
 64 | }
 65 | 
 66 | func (x *gRPCBrokerStartStreamClient) Recv() (*ConnInfo, error) {
 67 | 	m := new(ConnInfo)
 68 | 	if err := x.ClientStream.RecvMsg(m); err != nil {
 69 | 		return nil, err
 70 | 	}
 71 | 	return m, nil
 72 | }
 73 | 
 74 | // GRPCBrokerServer is the server API for GRPCBroker service.
 75 | // All implementations should embed UnimplementedGRPCBrokerServer
 76 | // for forward compatibility
 77 | type GRPCBrokerServer interface {
 78 | 	StartStream(GRPCBroker_StartStreamServer) error
 79 | }
 80 | 
 81 | // UnimplementedGRPCBrokerServer should be embedded to have forward compatible implementations.
 82 | type UnimplementedGRPCBrokerServer struct {
 83 | }
 84 | 
 85 | func (UnimplementedGRPCBrokerServer) StartStream(GRPCBroker_StartStreamServer) error {
 86 | 	return status.Errorf(codes.Unimplemented, "method StartStream not implemented")
 87 | }
 88 | 
 89 | // UnsafeGRPCBrokerServer may be embedded to opt out of forward compatibility for this service.
 90 | // Use of this interface is not recommended, as added methods to GRPCBrokerServer will
 91 | // result in compilation errors.
 92 | type UnsafeGRPCBrokerServer interface {
 93 | 	mustEmbedUnimplementedGRPCBrokerServer()
 94 | }
 95 | 
 96 | func RegisterGRPCBrokerServer(s grpc.ServiceRegistrar, srv GRPCBrokerServer) {
 97 | 	s.RegisterService(&GRPCBroker_ServiceDesc, srv)
 98 | }
 99 | 
100 | func _GRPCBroker_StartStream_Handler(srv interface{}, stream grpc.ServerStream) error {
101 | 	return srv.(GRPCBrokerServer).StartStream(&gRPCBrokerStartStreamServer{stream})
102 | }
103 | 
104 | type GRPCBroker_StartStreamServer interface {
105 | 	Send(*ConnInfo) error
106 | 	Recv() (*ConnInfo, error)
107 | 	grpc.ServerStream
108 | }
109 | 
110 | type gRPCBrokerStartStreamServer struct {
111 | 	grpc.ServerStream
112 | }
113 | 
114 | func (x *gRPCBrokerStartStreamServer) Send(m *ConnInfo) error {
115 | 	return x.ServerStream.SendMsg(m)
116 | }
117 | 
118 | func (x *gRPCBrokerStartStreamServer) Recv() (*ConnInfo, error) {
119 | 	m := new(ConnInfo)
120 | 	if err := x.ServerStream.RecvMsg(m); err != nil {
121 | 		return nil, err
122 | 	}
123 | 	return m, nil
124 | }
125 | 
126 | // GRPCBroker_ServiceDesc is the grpc.ServiceDesc for GRPCBroker service.
127 | // It's only intended for direct use with grpc.RegisterService,
128 | // and not to be introspected or modified (even as a copy)
129 | var GRPCBroker_ServiceDesc = grpc.ServiceDesc{
130 | 	ServiceName: "plugin.GRPCBroker",
131 | 	HandlerType: (*GRPCBrokerServer)(nil),
132 | 	Methods:     []grpc.MethodDesc{},
133 | 	Streams: []grpc.StreamDesc{
134 | 		{
135 | 			StreamName:    "StartStream",
136 | 			Handler:       _GRPCBroker_StartStream_Handler,
137 | 			ServerStreams: true,
138 | 			ClientStreams: true,
139 | 		},
140 | 	},
141 | 	Metadata: "internal/plugin/grpc_broker.proto",
142 | }
143 | 


--------------------------------------------------------------------------------
/internal/plugin/grpc_controller.pb.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | // Code generated by protoc-gen-go. DO NOT EDIT.
  5 | // versions:
  6 | // 	protoc-gen-go v1.31.0
  7 | // 	protoc        (unknown)
  8 | // source: internal/plugin/grpc_controller.proto
  9 | 
 10 | package plugin
 11 | 
 12 | import (
 13 | 	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
 14 | 	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
 15 | 	reflect "reflect"
 16 | 	sync "sync"
 17 | )
 18 | 
 19 | const (
 20 | 	// Verify that this generated code is sufficiently up-to-date.
 21 | 	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
 22 | 	// Verify that runtime/protoimpl is sufficiently up-to-date.
 23 | 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 24 | )
 25 | 
 26 | type Empty struct {
 27 | 	state         protoimpl.MessageState
 28 | 	sizeCache     protoimpl.SizeCache
 29 | 	unknownFields protoimpl.UnknownFields
 30 | }
 31 | 
 32 | func (x *Empty) Reset() {
 33 | 	*x = Empty{}
 34 | 	if protoimpl.UnsafeEnabled {
 35 | 		mi := &file_internal_plugin_grpc_controller_proto_msgTypes[0]
 36 | 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 37 | 		ms.StoreMessageInfo(mi)
 38 | 	}
 39 | }
 40 | 
 41 | func (x *Empty) String() string {
 42 | 	return protoimpl.X.MessageStringOf(x)
 43 | }
 44 | 
 45 | func (*Empty) ProtoMessage() {}
 46 | 
 47 | func (x *Empty) ProtoReflect() protoreflect.Message {
 48 | 	mi := &file_internal_plugin_grpc_controller_proto_msgTypes[0]
 49 | 	if protoimpl.UnsafeEnabled && x != nil {
 50 | 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 51 | 		if ms.LoadMessageInfo() == nil {
 52 | 			ms.StoreMessageInfo(mi)
 53 | 		}
 54 | 		return ms
 55 | 	}
 56 | 	return mi.MessageOf(x)
 57 | }
 58 | 
 59 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead.
 60 | func (*Empty) Descriptor() ([]byte, []int) {
 61 | 	return file_internal_plugin_grpc_controller_proto_rawDescGZIP(), []int{0}
 62 | }
 63 | 
 64 | var File_internal_plugin_grpc_controller_proto protoreflect.FileDescriptor
 65 | 
 66 | var file_internal_plugin_grpc_controller_proto_rawDesc = []byte{
 67 | 	0x0a, 0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69,
 68 | 	0x6e, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
 69 | 	0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22,
 70 | 	0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x32, 0x3a, 0x0a, 0x0e, 0x47, 0x52, 0x50, 0x43,
 71 | 	0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x08, 0x53, 0x68,
 72 | 	0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x12, 0x0d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e,
 73 | 	0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x45,
 74 | 	0x6d, 0x70, 0x74, 0x79, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e,
 75 | 	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 76 | }
 77 | 
 78 | var (
 79 | 	file_internal_plugin_grpc_controller_proto_rawDescOnce sync.Once
 80 | 	file_internal_plugin_grpc_controller_proto_rawDescData = file_internal_plugin_grpc_controller_proto_rawDesc
 81 | )
 82 | 
 83 | func file_internal_plugin_grpc_controller_proto_rawDescGZIP() []byte {
 84 | 	file_internal_plugin_grpc_controller_proto_rawDescOnce.Do(func() {
 85 | 		file_internal_plugin_grpc_controller_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_plugin_grpc_controller_proto_rawDescData)
 86 | 	})
 87 | 	return file_internal_plugin_grpc_controller_proto_rawDescData
 88 | }
 89 | 
 90 | var file_internal_plugin_grpc_controller_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
 91 | var file_internal_plugin_grpc_controller_proto_goTypes = []interface{}{
 92 | 	(*Empty)(nil), // 0: plugin.Empty
 93 | }
 94 | var file_internal_plugin_grpc_controller_proto_depIdxs = []int32{
 95 | 	0, // 0: plugin.GRPCController.Shutdown:input_type -> plugin.Empty
 96 | 	0, // 1: plugin.GRPCController.Shutdown:output_type -> plugin.Empty
 97 | 	1, // [1:2] is the sub-list for method output_type
 98 | 	0, // [0:1] is the sub-list for method input_type
 99 | 	0, // [0:0] is the sub-list for extension type_name
100 | 	0, // [0:0] is the sub-list for extension extendee
101 | 	0, // [0:0] is the sub-list for field type_name
102 | }
103 | 
104 | func init() { file_internal_plugin_grpc_controller_proto_init() }
105 | func file_internal_plugin_grpc_controller_proto_init() {
106 | 	if File_internal_plugin_grpc_controller_proto != nil {
107 | 		return
108 | 	}
109 | 	if !protoimpl.UnsafeEnabled {
110 | 		file_internal_plugin_grpc_controller_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
111 | 			switch v := v.(*Empty); i {
112 | 			case 0:
113 | 				return &v.state
114 | 			case 1:
115 | 				return &v.sizeCache
116 | 			case 2:
117 | 				return &v.unknownFields
118 | 			default:
119 | 				return nil
120 | 			}
121 | 		}
122 | 	}
123 | 	type x struct{}
124 | 	out := protoimpl.TypeBuilder{
125 | 		File: protoimpl.DescBuilder{
126 | 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
127 | 			RawDescriptor: file_internal_plugin_grpc_controller_proto_rawDesc,
128 | 			NumEnums:      0,
129 | 			NumMessages:   1,
130 | 			NumExtensions: 0,
131 | 			NumServices:   1,
132 | 		},
133 | 		GoTypes:           file_internal_plugin_grpc_controller_proto_goTypes,
134 | 		DependencyIndexes: file_internal_plugin_grpc_controller_proto_depIdxs,
135 | 		MessageInfos:      file_internal_plugin_grpc_controller_proto_msgTypes,
136 | 	}.Build()
137 | 	File_internal_plugin_grpc_controller_proto = out.File
138 | 	file_internal_plugin_grpc_controller_proto_rawDesc = nil
139 | 	file_internal_plugin_grpc_controller_proto_goTypes = nil
140 | 	file_internal_plugin_grpc_controller_proto_depIdxs = nil
141 | }
142 | 


--------------------------------------------------------------------------------
/internal/plugin/grpc_controller.proto:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | syntax = "proto3";
 5 | package plugin;
 6 | option go_package = "./plugin";
 7 | 
 8 | message Empty {
 9 | }
10 | 
11 | // The GRPCController is responsible for telling the plugin server to shutdown.
12 | service GRPCController {
13 |     rpc Shutdown(Empty) returns (Empty);
14 | }
15 | 


--------------------------------------------------------------------------------
/internal/plugin/grpc_controller_grpc.pb.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
  5 | // versions:
  6 | // - protoc-gen-go-grpc v1.3.0
  7 | // - protoc             (unknown)
  8 | // source: internal/plugin/grpc_controller.proto
  9 | 
 10 | package plugin
 11 | 
 12 | import (
 13 | 	context "context"
 14 | 	grpc "google.golang.org/grpc"
 15 | 	codes "google.golang.org/grpc/codes"
 16 | 	status "google.golang.org/grpc/status"
 17 | )
 18 | 
 19 | // This is a compile-time assertion to ensure that this generated file
 20 | // is compatible with the grpc package it is being compiled against.
 21 | // Requires gRPC-Go v1.32.0 or later.
 22 | const _ = grpc.SupportPackageIsVersion7
 23 | 
 24 | const (
 25 | 	GRPCController_Shutdown_FullMethodName = "/plugin.GRPCController/Shutdown"
 26 | )
 27 | 
 28 | // GRPCControllerClient is the client API for GRPCController service.
 29 | //
 30 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
 31 | type GRPCControllerClient interface {
 32 | 	Shutdown(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error)
 33 | }
 34 | 
 35 | type gRPCControllerClient struct {
 36 | 	cc grpc.ClientConnInterface
 37 | }
 38 | 
 39 | func NewGRPCControllerClient(cc grpc.ClientConnInterface) GRPCControllerClient {
 40 | 	return &gRPCControllerClient{cc}
 41 | }
 42 | 
 43 | func (c *gRPCControllerClient) Shutdown(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) {
 44 | 	out := new(Empty)
 45 | 	err := c.cc.Invoke(ctx, GRPCController_Shutdown_FullMethodName, in, out, opts...)
 46 | 	if err != nil {
 47 | 		return nil, err
 48 | 	}
 49 | 	return out, nil
 50 | }
 51 | 
 52 | // GRPCControllerServer is the server API for GRPCController service.
 53 | // All implementations should embed UnimplementedGRPCControllerServer
 54 | // for forward compatibility
 55 | type GRPCControllerServer interface {
 56 | 	Shutdown(context.Context, *Empty) (*Empty, error)
 57 | }
 58 | 
 59 | // UnimplementedGRPCControllerServer should be embedded to have forward compatible implementations.
 60 | type UnimplementedGRPCControllerServer struct {
 61 | }
 62 | 
 63 | func (UnimplementedGRPCControllerServer) Shutdown(context.Context, *Empty) (*Empty, error) {
 64 | 	return nil, status.Errorf(codes.Unimplemented, "method Shutdown not implemented")
 65 | }
 66 | 
 67 | // UnsafeGRPCControllerServer may be embedded to opt out of forward compatibility for this service.
 68 | // Use of this interface is not recommended, as added methods to GRPCControllerServer will
 69 | // result in compilation errors.
 70 | type UnsafeGRPCControllerServer interface {
 71 | 	mustEmbedUnimplementedGRPCControllerServer()
 72 | }
 73 | 
 74 | func RegisterGRPCControllerServer(s grpc.ServiceRegistrar, srv GRPCControllerServer) {
 75 | 	s.RegisterService(&GRPCController_ServiceDesc, srv)
 76 | }
 77 | 
 78 | func _GRPCController_Shutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 79 | 	in := new(Empty)
 80 | 	if err := dec(in); err != nil {
 81 | 		return nil, err
 82 | 	}
 83 | 	if interceptor == nil {
 84 | 		return srv.(GRPCControllerServer).Shutdown(ctx, in)
 85 | 	}
 86 | 	info := &grpc.UnaryServerInfo{
 87 | 		Server:     srv,
 88 | 		FullMethod: GRPCController_Shutdown_FullMethodName,
 89 | 	}
 90 | 	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
 91 | 		return srv.(GRPCControllerServer).Shutdown(ctx, req.(*Empty))
 92 | 	}
 93 | 	return interceptor(ctx, in, info, handler)
 94 | }
 95 | 
 96 | // GRPCController_ServiceDesc is the grpc.ServiceDesc for GRPCController service.
 97 | // It's only intended for direct use with grpc.RegisterService,
 98 | // and not to be introspected or modified (even as a copy)
 99 | var GRPCController_ServiceDesc = grpc.ServiceDesc{
100 | 	ServiceName: "plugin.GRPCController",
101 | 	HandlerType: (*GRPCControllerServer)(nil),
102 | 	Methods: []grpc.MethodDesc{
103 | 		{
104 | 			MethodName: "Shutdown",
105 | 			Handler:    _GRPCController_Shutdown_Handler,
106 | 		},
107 | 	},
108 | 	Streams:  []grpc.StreamDesc{},
109 | 	Metadata: "internal/plugin/grpc_controller.proto",
110 | }
111 | 


--------------------------------------------------------------------------------
/internal/plugin/grpc_stdio.proto:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | syntax = "proto3";
 5 | package plugin;
 6 | option go_package = "./plugin";
 7 | 
 8 | import "google/protobuf/empty.proto";
 9 | 
10 | // GRPCStdio is a service that is automatically run by the plugin process
11 | // to stream any stdout/err data so that it can be mirrored on the plugin
12 | // host side.
13 | service GRPCStdio {
14 |   // StreamStdio returns a stream that contains all the stdout/stderr.
15 |   // This RPC endpoint must only be called ONCE. Once stdio data is consumed
16 |   // it is not sent again.
17 |   //
18 |   // Callers should connect early to prevent blocking on the plugin process.
19 |   rpc StreamStdio(google.protobuf.Empty) returns (stream StdioData);
20 | }
21 | 
22 | // StdioData is a single chunk of stdout or stderr data that is streamed
23 | // from GRPCStdio.
24 | message StdioData {
25 |   enum Channel {
26 |     INVALID = 0;
27 |     STDOUT = 1;
28 |     STDERR = 2;
29 |   }
30 | 
31 |   Channel channel = 1;
32 |   bytes data = 2;
33 | }
34 | 


--------------------------------------------------------------------------------
/internal/plugin/grpc_stdio_grpc.pb.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
  5 | // versions:
  6 | // - protoc-gen-go-grpc v1.3.0
  7 | // - protoc             (unknown)
  8 | // source: internal/plugin/grpc_stdio.proto
  9 | 
 10 | package plugin
 11 | 
 12 | import (
 13 | 	context "context"
 14 | 	grpc "google.golang.org/grpc"
 15 | 	codes "google.golang.org/grpc/codes"
 16 | 	status "google.golang.org/grpc/status"
 17 | 	emptypb "google.golang.org/protobuf/types/known/emptypb"
 18 | )
 19 | 
 20 | // This is a compile-time assertion to ensure that this generated file
 21 | // is compatible with the grpc package it is being compiled against.
 22 | // Requires gRPC-Go v1.32.0 or later.
 23 | const _ = grpc.SupportPackageIsVersion7
 24 | 
 25 | const (
 26 | 	GRPCStdio_StreamStdio_FullMethodName = "/plugin.GRPCStdio/StreamStdio"
 27 | )
 28 | 
 29 | // GRPCStdioClient is the client API for GRPCStdio service.
 30 | //
 31 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
 32 | type GRPCStdioClient interface {
 33 | 	// StreamStdio returns a stream that contains all the stdout/stderr.
 34 | 	// This RPC endpoint must only be called ONCE. Once stdio data is consumed
 35 | 	// it is not sent again.
 36 | 	//
 37 | 	// Callers should connect early to prevent blocking on the plugin process.
 38 | 	StreamStdio(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (GRPCStdio_StreamStdioClient, error)
 39 | }
 40 | 
 41 | type gRPCStdioClient struct {
 42 | 	cc grpc.ClientConnInterface
 43 | }
 44 | 
 45 | func NewGRPCStdioClient(cc grpc.ClientConnInterface) GRPCStdioClient {
 46 | 	return &gRPCStdioClient{cc}
 47 | }
 48 | 
 49 | func (c *gRPCStdioClient) StreamStdio(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (GRPCStdio_StreamStdioClient, error) {
 50 | 	stream, err := c.cc.NewStream(ctx, &GRPCStdio_ServiceDesc.Streams[0], GRPCStdio_StreamStdio_FullMethodName, opts...)
 51 | 	if err != nil {
 52 | 		return nil, err
 53 | 	}
 54 | 	x := &gRPCStdioStreamStdioClient{stream}
 55 | 	if err := x.ClientStream.SendMsg(in); err != nil {
 56 | 		return nil, err
 57 | 	}
 58 | 	if err := x.ClientStream.CloseSend(); err != nil {
 59 | 		return nil, err
 60 | 	}
 61 | 	return x, nil
 62 | }
 63 | 
 64 | type GRPCStdio_StreamStdioClient interface {
 65 | 	Recv() (*StdioData, error)
 66 | 	grpc.ClientStream
 67 | }
 68 | 
 69 | type gRPCStdioStreamStdioClient struct {
 70 | 	grpc.ClientStream
 71 | }
 72 | 
 73 | func (x *gRPCStdioStreamStdioClient) Recv() (*StdioData, error) {
 74 | 	m := new(StdioData)
 75 | 	if err := x.ClientStream.RecvMsg(m); err != nil {
 76 | 		return nil, err
 77 | 	}
 78 | 	return m, nil
 79 | }
 80 | 
 81 | // GRPCStdioServer is the server API for GRPCStdio service.
 82 | // All implementations should embed UnimplementedGRPCStdioServer
 83 | // for forward compatibility
 84 | type GRPCStdioServer interface {
 85 | 	// StreamStdio returns a stream that contains all the stdout/stderr.
 86 | 	// This RPC endpoint must only be called ONCE. Once stdio data is consumed
 87 | 	// it is not sent again.
 88 | 	//
 89 | 	// Callers should connect early to prevent blocking on the plugin process.
 90 | 	StreamStdio(*emptypb.Empty, GRPCStdio_StreamStdioServer) error
 91 | }
 92 | 
 93 | // UnimplementedGRPCStdioServer should be embedded to have forward compatible implementations.
 94 | type UnimplementedGRPCStdioServer struct {
 95 | }
 96 | 
 97 | func (UnimplementedGRPCStdioServer) StreamStdio(*emptypb.Empty, GRPCStdio_StreamStdioServer) error {
 98 | 	return status.Errorf(codes.Unimplemented, "method StreamStdio not implemented")
 99 | }
100 | 
101 | // UnsafeGRPCStdioServer may be embedded to opt out of forward compatibility for this service.
102 | // Use of this interface is not recommended, as added methods to GRPCStdioServer will
103 | // result in compilation errors.
104 | type UnsafeGRPCStdioServer interface {
105 | 	mustEmbedUnimplementedGRPCStdioServer()
106 | }
107 | 
108 | func RegisterGRPCStdioServer(s grpc.ServiceRegistrar, srv GRPCStdioServer) {
109 | 	s.RegisterService(&GRPCStdio_ServiceDesc, srv)
110 | }
111 | 
112 | func _GRPCStdio_StreamStdio_Handler(srv interface{}, stream grpc.ServerStream) error {
113 | 	m := new(emptypb.Empty)
114 | 	if err := stream.RecvMsg(m); err != nil {
115 | 		return err
116 | 	}
117 | 	return srv.(GRPCStdioServer).StreamStdio(m, &gRPCStdioStreamStdioServer{stream})
118 | }
119 | 
120 | type GRPCStdio_StreamStdioServer interface {
121 | 	Send(*StdioData) error
122 | 	grpc.ServerStream
123 | }
124 | 
125 | type gRPCStdioStreamStdioServer struct {
126 | 	grpc.ServerStream
127 | }
128 | 
129 | func (x *gRPCStdioStreamStdioServer) Send(m *StdioData) error {
130 | 	return x.ServerStream.SendMsg(m)
131 | }
132 | 
133 | // GRPCStdio_ServiceDesc is the grpc.ServiceDesc for GRPCStdio service.
134 | // It's only intended for direct use with grpc.RegisterService,
135 | // and not to be introspected or modified (even as a copy)
136 | var GRPCStdio_ServiceDesc = grpc.ServiceDesc{
137 | 	ServiceName: "plugin.GRPCStdio",
138 | 	HandlerType: (*GRPCStdioServer)(nil),
139 | 	Methods:     []grpc.MethodDesc{},
140 | 	Streams: []grpc.StreamDesc{
141 | 		{
142 | 			StreamName:    "StreamStdio",
143 | 			Handler:       _GRPCStdio_StreamStdio_Handler,
144 | 			ServerStreams: true,
145 | 		},
146 | 	},
147 | 	Metadata: "internal/plugin/grpc_stdio.proto",
148 | }
149 | 


--------------------------------------------------------------------------------
/log_entry.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | import (
 7 | 	"encoding/json"
 8 | 	"time"
 9 | )
10 | 
11 | // logEntry is the JSON payload that gets sent to Stderr from the plugin to the host
12 | type logEntry struct {
13 | 	Message   string        `json:"@message"`
14 | 	Level     string        `json:"@level"`
15 | 	Timestamp time.Time     `json:"timestamp"`
16 | 	KVPairs   []*logEntryKV `json:"kv_pairs"`
17 | }
18 | 
19 | // logEntryKV is a key value pair within the Output payload
20 | type logEntryKV struct {
21 | 	Key   string      `json:"key"`
22 | 	Value interface{} `json:"value"`
23 | }
24 | 
25 | // flattenKVPairs is used to flatten KVPair slice into []interface{}
26 | // for hclog consumption.
27 | func flattenKVPairs(kvs []*logEntryKV) []interface{} {
28 | 	var result []interface{}
29 | 	for _, kv := range kvs {
30 | 		result = append(result, kv.Key)
31 | 		result = append(result, kv.Value)
32 | 	}
33 | 
34 | 	return result
35 | }
36 | 
37 | // parseJSON handles parsing JSON output
38 | func parseJSON(input []byte) (*logEntry, error) {
39 | 	var raw map[string]interface{}
40 | 	entry := &logEntry{}
41 | 
42 | 	err := json.Unmarshal(input, &raw)
43 | 	if err != nil {
44 | 		return nil, err
45 | 	}
46 | 
47 | 	// Parse hclog-specific objects
48 | 	if v, ok := raw["@message"]; ok {
49 | 		entry.Message = v.(string)
50 | 		delete(raw, "@message")
51 | 	}
52 | 
53 | 	if v, ok := raw["@level"]; ok {
54 | 		entry.Level = v.(string)
55 | 		delete(raw, "@level")
56 | 	}
57 | 
58 | 	if v, ok := raw["@timestamp"]; ok {
59 | 		t, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", v.(string))
60 | 		if err != nil {
61 | 			return nil, err
62 | 		}
63 | 		entry.Timestamp = t
64 | 		delete(raw, "@timestamp")
65 | 	}
66 | 
67 | 	// Parse dynamic KV args from the hclog payload.
68 | 	for k, v := range raw {
69 | 		entry.KVPairs = append(entry.KVPairs, &logEntryKV{
70 | 			Key:   k,
71 | 			Value: v,
72 | 		})
73 | 	}
74 | 
75 | 	return entry, nil
76 | }
77 | 


--------------------------------------------------------------------------------
/mtls.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | import (
 7 | 	"bytes"
 8 | 	"crypto/ecdsa"
 9 | 	"crypto/elliptic"
10 | 	"crypto/rand"
11 | 	"crypto/x509"
12 | 	"crypto/x509/pkix"
13 | 	"encoding/pem"
14 | 	"math/big"
15 | 	"time"
16 | )
17 | 
18 | // generateCert generates a temporary certificate for plugin authentication. The
19 | // certificate and private key are returns in PEM format.
20 | func generateCert() (cert []byte, privateKey []byte, err error) {
21 | 	key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
22 | 	if err != nil {
23 | 		return nil, nil, err
24 | 	}
25 | 
26 | 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
27 | 	sn, err := rand.Int(rand.Reader, serialNumberLimit)
28 | 	if err != nil {
29 | 		return nil, nil, err
30 | 	}
31 | 
32 | 	host := "localhost"
33 | 
34 | 	template := &x509.Certificate{
35 | 		Subject: pkix.Name{
36 | 			CommonName:   host,
37 | 			Organization: []string{"HashiCorp"},
38 | 		},
39 | 		DNSNames: []string{host},
40 | 		ExtKeyUsage: []x509.ExtKeyUsage{
41 | 			x509.ExtKeyUsageClientAuth,
42 | 			x509.ExtKeyUsageServerAuth,
43 | 		},
44 | 		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign,
45 | 		BasicConstraintsValid: true,
46 | 		SerialNumber:          sn,
47 | 		NotBefore:             time.Now().Add(-30 * time.Second),
48 | 		NotAfter:              time.Now().Add(262980 * time.Hour),
49 | 		IsCA:                  true,
50 | 	}
51 | 
52 | 	der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
53 | 	if err != nil {
54 | 		return nil, nil, err
55 | 	}
56 | 
57 | 	var certOut bytes.Buffer
58 | 	if err := pem.Encode(&certOut, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil {
59 | 		return nil, nil, err
60 | 	}
61 | 
62 | 	keyBytes, err := x509.MarshalECPrivateKey(key)
63 | 	if err != nil {
64 | 		return nil, nil, err
65 | 	}
66 | 
67 | 	var keyOut bytes.Buffer
68 | 	if err := pem.Encode(&keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}); err != nil {
69 | 		return nil, nil, err
70 | 	}
71 | 
72 | 	cert = certOut.Bytes()
73 | 	privateKey = keyOut.Bytes()
74 | 
75 | 	return cert, privateKey, nil
76 | }
77 | 


--------------------------------------------------------------------------------
/mux_broker.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package plugin
  5 | 
  6 | import (
  7 | 	"encoding/binary"
  8 | 	"fmt"
  9 | 	"log"
 10 | 	"net"
 11 | 	"sync"
 12 | 	"sync/atomic"
 13 | 	"time"
 14 | 
 15 | 	"github.com/hashicorp/yamux"
 16 | )
 17 | 
 18 | // MuxBroker is responsible for brokering multiplexed connections by unique ID.
 19 | //
 20 | // It is used by plugins to multiplex multiple RPC connections and data
 21 | // streams on top of a single connection between the plugin process and the
 22 | // host process.
 23 | //
 24 | // This allows a plugin to request a channel with a specific ID to connect to
 25 | // or accept a connection from, and the broker handles the details of
 26 | // holding these channels open while they're being negotiated.
 27 | //
 28 | // The Plugin interface has access to these for both Server and Client.
 29 | // The broker can be used by either (optionally) to reserve and connect to
 30 | // new multiplexed streams. This is useful for complex args and return values,
 31 | // or anything else you might need a data stream for.
 32 | type MuxBroker struct {
 33 | 	nextId  uint32
 34 | 	session *yamux.Session
 35 | 	streams map[uint32]*muxBrokerPending
 36 | 
 37 | 	sync.Mutex
 38 | }
 39 | 
 40 | type muxBrokerPending struct {
 41 | 	ch     chan net.Conn
 42 | 	doneCh chan struct{}
 43 | }
 44 | 
 45 | func newMuxBroker(s *yamux.Session) *MuxBroker {
 46 | 	return &MuxBroker{
 47 | 		session: s,
 48 | 		streams: make(map[uint32]*muxBrokerPending),
 49 | 	}
 50 | }
 51 | 
 52 | // Accept accepts a connection by ID.
 53 | //
 54 | // This should not be called multiple times with the same ID at one time.
 55 | func (m *MuxBroker) Accept(id uint32) (net.Conn, error) {
 56 | 	var c net.Conn
 57 | 	p := m.getStream(id)
 58 | 	select {
 59 | 	case c = <-p.ch:
 60 | 		close(p.doneCh)
 61 | 	case <-time.After(5 * time.Second):
 62 | 		m.Lock()
 63 | 		defer m.Unlock()
 64 | 		delete(m.streams, id)
 65 | 
 66 | 		return nil, fmt.Errorf("timeout waiting for accept")
 67 | 	}
 68 | 
 69 | 	// Ack our connection
 70 | 	if err := binary.Write(c, binary.LittleEndian, id); err != nil {
 71 | 		_ = c.Close()
 72 | 		return nil, err
 73 | 	}
 74 | 
 75 | 	return c, nil
 76 | }
 77 | 
 78 | // AcceptAndServe is used to accept a specific stream ID and immediately
 79 | // serve an RPC server on that stream ID. This is used to easily serve
 80 | // complex arguments.
 81 | //
 82 | // The served interface is always registered to the "Plugin" name.
 83 | func (m *MuxBroker) AcceptAndServe(id uint32, v interface{}) {
 84 | 	conn, err := m.Accept(id)
 85 | 	if err != nil {
 86 | 		log.Printf("[ERR] plugin: plugin acceptAndServe error: %s", err)
 87 | 		return
 88 | 	}
 89 | 
 90 | 	serve(conn, "Plugin", v)
 91 | }
 92 | 
 93 | // Close closes the connection and all sub-connections.
 94 | func (m *MuxBroker) Close() error {
 95 | 	return m.session.Close()
 96 | }
 97 | 
 98 | // Dial opens a connection by ID.
 99 | func (m *MuxBroker) Dial(id uint32) (net.Conn, error) {
100 | 	// Open the stream
101 | 	stream, err := m.session.OpenStream()
102 | 	if err != nil {
103 | 		return nil, err
104 | 	}
105 | 
106 | 	// Write the stream ID onto the wire.
107 | 	if err := binary.Write(stream, binary.LittleEndian, id); err != nil {
108 | 		_ = stream.Close()
109 | 		return nil, err
110 | 	}
111 | 
112 | 	// Read the ack that we connected. Then we're off!
113 | 	var ack uint32
114 | 	if err := binary.Read(stream, binary.LittleEndian, &ack); err != nil {
115 | 		_ = stream.Close()
116 | 		return nil, err
117 | 	}
118 | 	if ack != id {
119 | 		_ = stream.Close()
120 | 		return nil, fmt.Errorf("bad ack: %d (expected %d)", ack, id)
121 | 	}
122 | 
123 | 	return stream, nil
124 | }
125 | 
126 | // NextId returns a unique ID to use next.
127 | //
128 | // It is possible for very long-running plugin hosts to wrap this value,
129 | // though it would require a very large amount of RPC calls. In practice
130 | // we've never seen it happen.
131 | func (m *MuxBroker) NextId() uint32 {
132 | 	return atomic.AddUint32(&m.nextId, 1)
133 | }
134 | 
135 | // Run starts the brokering and should be executed in a goroutine, since it
136 | // blocks forever, or until the session closes.
137 | //
138 | // Uses of MuxBroker never need to call this. It is called internally by
139 | // the plugin host/client.
140 | func (m *MuxBroker) Run() {
141 | 	for {
142 | 		stream, err := m.session.AcceptStream()
143 | 		if err != nil {
144 | 			// Once we receive an error, just exit
145 | 			break
146 | 		}
147 | 
148 | 		// Read the stream ID from the stream
149 | 		var id uint32
150 | 		if err := binary.Read(stream, binary.LittleEndian, &id); err != nil {
151 | 			_ = stream.Close()
152 | 			continue
153 | 		}
154 | 
155 | 		// Initialize the waiter
156 | 		p := m.getStream(id)
157 | 		select {
158 | 		case p.ch <- stream:
159 | 		default:
160 | 		}
161 | 
162 | 		// Wait for a timeout
163 | 		go m.timeoutWait(id, p)
164 | 	}
165 | }
166 | 
167 | func (m *MuxBroker) getStream(id uint32) *muxBrokerPending {
168 | 	m.Lock()
169 | 	defer m.Unlock()
170 | 
171 | 	p, ok := m.streams[id]
172 | 	if ok {
173 | 		return p
174 | 	}
175 | 
176 | 	m.streams[id] = &muxBrokerPending{
177 | 		ch:     make(chan net.Conn, 1),
178 | 		doneCh: make(chan struct{}),
179 | 	}
180 | 	return m.streams[id]
181 | }
182 | 
183 | func (m *MuxBroker) timeoutWait(id uint32, p *muxBrokerPending) {
184 | 	// Wait for the stream to either be picked up and connected, or
185 | 	// for a timeout.
186 | 	timeout := false
187 | 	select {
188 | 	case <-p.doneCh:
189 | 	case <-time.After(5 * time.Second):
190 | 		timeout = true
191 | 	}
192 | 
193 | 	m.Lock()
194 | 	defer m.Unlock()
195 | 
196 | 	// Delete the stream so no one else can grab it
197 | 	delete(m.streams, id)
198 | 
199 | 	// If we timed out, then check if we have a channel in the buffer,
200 | 	// and if so, close it.
201 | 	if timeout {
202 | 		s := <-p.ch
203 | 		_ = s.Close()
204 | 	}
205 | }
206 | 


--------------------------------------------------------------------------------
/plugin.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | // The plugin package exposes functions and helpers for communicating to
 5 | // plugins which are implemented as standalone binary applications.
 6 | //
 7 | // plugin.Client fully manages the lifecycle of executing the application,
 8 | // connecting to it, and returning the RPC client for dispensing plugins.
 9 | //
10 | // plugin.Serve fully manages listeners to expose an RPC server from a binary
11 | // that plugin.Client can connect to.
12 | package plugin
13 | 
14 | import (
15 | 	"context"
16 | 	"errors"
17 | 	"net/rpc"
18 | 
19 | 	"google.golang.org/grpc"
20 | )
21 | 
22 | // Plugin is the interface that is implemented to serve/connect to an
23 | // inteface implementation.
24 | type Plugin interface {
25 | 	// Server should return the RPC server compatible struct to serve
26 | 	// the methods that the Client calls over net/rpc.
27 | 	Server(*MuxBroker) (interface{}, error)
28 | 
29 | 	// Client returns an interface implementation for the plugin you're
30 | 	// serving that communicates to the server end of the plugin.
31 | 	Client(*MuxBroker, *rpc.Client) (interface{}, error)
32 | }
33 | 
34 | // GRPCPlugin is the interface that is implemented to serve/connect to
35 | // a plugin over gRPC.
36 | type GRPCPlugin interface {
37 | 	// GRPCServer should register this plugin for serving with the
38 | 	// given GRPCServer. Unlike Plugin.Server, this is only called once
39 | 	// since gRPC plugins serve singletons.
40 | 	GRPCServer(*GRPCBroker, *grpc.Server) error
41 | 
42 | 	// GRPCClient should return the interface implementation for the plugin
43 | 	// you're serving via gRPC. The provided context will be canceled by
44 | 	// go-plugin in the event of the plugin process exiting.
45 | 	GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error)
46 | }
47 | 
48 | // NetRPCUnsupportedPlugin implements Plugin but returns errors for the
49 | // Server and Client functions. This will effectively disable support for
50 | // net/rpc based plugins.
51 | //
52 | // This struct can be embedded in your struct.
53 | type NetRPCUnsupportedPlugin struct{}
54 | 
55 | func (p NetRPCUnsupportedPlugin) Server(*MuxBroker) (interface{}, error) {
56 | 	return nil, errors.New("net/rpc plugin protocol not supported")
57 | }
58 | 
59 | func (p NetRPCUnsupportedPlugin) Client(*MuxBroker, *rpc.Client) (interface{}, error) {
60 | 	return nil, errors.New("net/rpc plugin protocol not supported")
61 | }
62 | 


--------------------------------------------------------------------------------
/process.go:
--------------------------------------------------------------------------------
1 | // Copyright (c) HashiCorp, Inc.
2 | // SPDX-License-Identifier: MPL-2.0
3 | 
4 | package plugin
5 | 


--------------------------------------------------------------------------------
/protocol.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | import (
 7 | 	"io"
 8 | 	"net"
 9 | )
10 | 
11 | // Protocol is an enum representing the types of protocols.
12 | type Protocol string
13 | 
14 | const (
15 | 	ProtocolInvalid Protocol = ""
16 | 	ProtocolNetRPC  Protocol = "netrpc"
17 | 	ProtocolGRPC    Protocol = "grpc"
18 | )
19 | 
20 | // ServerProtocol is an interface that must be implemented for new plugin
21 | // protocols to be servers.
22 | type ServerProtocol interface {
23 | 	// Init is called once to configure and initialize the protocol, but
24 | 	// not start listening. This is the point at which all validation should
25 | 	// be done and errors returned.
26 | 	Init() error
27 | 
28 | 	// Config is extra configuration to be outputted to stdout. This will
29 | 	// be automatically base64 encoded to ensure it can be parsed properly.
30 | 	// This can be an empty string if additional configuration is not needed.
31 | 	Config() string
32 | 
33 | 	// Serve is called to serve connections on the given listener. This should
34 | 	// continue until the listener is closed.
35 | 	Serve(net.Listener)
36 | }
37 | 
38 | // ClientProtocol is an interface that must be implemented for new plugin
39 | // protocols to be clients.
40 | type ClientProtocol interface {
41 | 	io.Closer
42 | 
43 | 	// Dispense dispenses a new instance of the plugin with the given name.
44 | 	Dispense(string) (interface{}, error)
45 | 
46 | 	// Ping checks that the client connection is still healthy.
47 | 	Ping() error
48 | }
49 | 


--------------------------------------------------------------------------------
/rpc_client.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package plugin
  5 | 
  6 | import (
  7 | 	"crypto/tls"
  8 | 	"fmt"
  9 | 	"io"
 10 | 	"net"
 11 | 	"net/rpc"
 12 | 
 13 | 	"github.com/hashicorp/yamux"
 14 | )
 15 | 
 16 | // RPCClient connects to an RPCServer over net/rpc to dispense plugin types.
 17 | type RPCClient struct {
 18 | 	broker  *MuxBroker
 19 | 	control *rpc.Client
 20 | 	plugins map[string]Plugin
 21 | 
 22 | 	// These are the streams used for the various stdout/err overrides
 23 | 	stdout, stderr net.Conn
 24 | }
 25 | 
 26 | // newRPCClient creates a new RPCClient. The Client argument is expected
 27 | // to be successfully started already with a lock held.
 28 | func newRPCClient(c *Client) (*RPCClient, error) {
 29 | 	// Connect to the client
 30 | 	conn, err := net.Dial(c.address.Network(), c.address.String())
 31 | 	if err != nil {
 32 | 		return nil, err
 33 | 	}
 34 | 	if tcpConn, ok := conn.(*net.TCPConn); ok {
 35 | 		// Make sure to set keep alive so that the connection doesn't die
 36 | 		_ = tcpConn.SetKeepAlive(true)
 37 | 	}
 38 | 
 39 | 	if c.config.TLSConfig != nil {
 40 | 		conn = tls.Client(conn, c.config.TLSConfig)
 41 | 	}
 42 | 
 43 | 	// Create the actual RPC client
 44 | 	result, err := NewRPCClient(conn, c.config.Plugins)
 45 | 	if err != nil {
 46 | 		_ = conn.Close()
 47 | 		return nil, err
 48 | 	}
 49 | 
 50 | 	// Begin the stream syncing so that stdin, out, err work properly
 51 | 	err = result.SyncStreams(
 52 | 		c.config.SyncStdout,
 53 | 		c.config.SyncStderr)
 54 | 	if err != nil {
 55 | 		_ = result.Close()
 56 | 		return nil, err
 57 | 	}
 58 | 
 59 | 	return result, nil
 60 | }
 61 | 
 62 | // NewRPCClient creates a client from an already-open connection-like value.
 63 | // Dial is typically used instead.
 64 | func NewRPCClient(conn io.ReadWriteCloser, plugins map[string]Plugin) (*RPCClient, error) {
 65 | 	// Create the yamux client so we can multiplex
 66 | 	mux, err := yamux.Client(conn, nil)
 67 | 	if err != nil {
 68 | 		_ = conn.Close()
 69 | 		return nil, err
 70 | 	}
 71 | 
 72 | 	// Connect to the control stream.
 73 | 	control, err := mux.Open()
 74 | 	if err != nil {
 75 | 		_ = mux.Close()
 76 | 		return nil, err
 77 | 	}
 78 | 
 79 | 	// Connect stdout, stderr streams
 80 | 	stdstream := make([]net.Conn, 2)
 81 | 	for i := range stdstream {
 82 | 		stdstream[i], err = mux.Open()
 83 | 		if err != nil {
 84 | 			_ = mux.Close()
 85 | 			return nil, err
 86 | 		}
 87 | 	}
 88 | 
 89 | 	// Create the broker and start it up
 90 | 	broker := newMuxBroker(mux)
 91 | 	go broker.Run()
 92 | 
 93 | 	// Build the client using our broker and control channel.
 94 | 	return &RPCClient{
 95 | 		broker:  broker,
 96 | 		control: rpc.NewClient(control),
 97 | 		plugins: plugins,
 98 | 		stdout:  stdstream[0],
 99 | 		stderr:  stdstream[1],
100 | 	}, nil
101 | }
102 | 
103 | // SyncStreams should be called to enable syncing of stdout,
104 | // stderr with the plugin.
105 | //
106 | // This will return immediately and the syncing will continue to happen
107 | // in the background. You do not need to launch this in a goroutine itself.
108 | //
109 | // This should never be called multiple times.
110 | func (c *RPCClient) SyncStreams(stdout io.Writer, stderr io.Writer) error {
111 | 	go copyStream("stdout", stdout, c.stdout)
112 | 	go copyStream("stderr", stderr, c.stderr)
113 | 	return nil
114 | }
115 | 
116 | // Close closes the connection. The client is no longer usable after this
117 | // is called.
118 | func (c *RPCClient) Close() error {
119 | 	// Call the control channel and ask it to gracefully exit. If this
120 | 	// errors, then we save it so that we always return an error but we
121 | 	// want to try to close the other channels anyways.
122 | 	var empty struct{}
123 | 	returnErr := c.control.Call("Control.Quit", true, &empty)
124 | 
125 | 	// Close the other streams we have
126 | 	if err := c.control.Close(); err != nil {
127 | 		return err
128 | 	}
129 | 	if err := c.stdout.Close(); err != nil {
130 | 		return err
131 | 	}
132 | 	if err := c.stderr.Close(); err != nil {
133 | 		return err
134 | 	}
135 | 	if err := c.broker.Close(); err != nil {
136 | 		return err
137 | 	}
138 | 
139 | 	// Return back the error we got from Control.Quit. This is very important
140 | 	// since we MUST return non-nil error if this fails so that Client.Kill
141 | 	// will properly try a process.Kill.
142 | 	return returnErr
143 | }
144 | 
145 | func (c *RPCClient) Dispense(name string) (interface{}, error) {
146 | 	p, ok := c.plugins[name]
147 | 	if !ok {
148 | 		return nil, fmt.Errorf("unknown plugin type: %s", name)
149 | 	}
150 | 
151 | 	var id uint32
152 | 	if err := c.control.Call(
153 | 		"Dispenser.Dispense", name, &id); err != nil {
154 | 		return nil, err
155 | 	}
156 | 
157 | 	conn, err := c.broker.Dial(id)
158 | 	if err != nil {
159 | 		return nil, err
160 | 	}
161 | 
162 | 	return p.Client(c.broker, rpc.NewClient(conn))
163 | }
164 | 
165 | // Ping pings the connection to ensure it is still alive.
166 | //
167 | // The error from the RPC call is returned exactly if you want to inspect
168 | // it for further error analysis. Any error returned from here would indicate
169 | // that the connection to the plugin is not healthy.
170 | func (c *RPCClient) Ping() error {
171 | 	var empty struct{}
172 | 	return c.control.Call("Control.Ping", true, &empty)
173 | }
174 | 


--------------------------------------------------------------------------------
/rpc_client_test.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package plugin
  5 | 
  6 | import (
  7 | 	"bytes"
  8 | 	"io"
  9 | 	"os"
 10 | 	"sync"
 11 | 	"testing"
 12 | 	"time"
 13 | 
 14 | 	hclog "github.com/hashicorp/go-hclog"
 15 | )
 16 | 
 17 | func TestClient_App(t *testing.T) {
 18 | 	pluginLogger := hclog.New(&hclog.LoggerOptions{
 19 | 		Level:      hclog.Trace,
 20 | 		Output:     os.Stderr,
 21 | 		JSONFormat: true,
 22 | 	})
 23 | 
 24 | 	testPlugin := &testInterfaceImpl{
 25 | 		logger: pluginLogger,
 26 | 	}
 27 | 
 28 | 	client, _ := TestPluginRPCConn(t, map[string]Plugin{
 29 | 		"test": &testInterfacePlugin{Impl: testPlugin},
 30 | 	}, nil)
 31 | 	defer func() { _ = client.Close() }()
 32 | 
 33 | 	raw, err := client.Dispense("test")
 34 | 	if err != nil {
 35 | 		t.Fatalf("err: %s", err)
 36 | 	}
 37 | 
 38 | 	impl, ok := raw.(testInterface)
 39 | 	if !ok {
 40 | 		t.Fatalf("bad: %#v", raw)
 41 | 	}
 42 | 
 43 | 	result := impl.Double(21)
 44 | 	if result != 42 {
 45 | 		t.Fatalf("bad: %#v", result)
 46 | 	}
 47 | }
 48 | 
 49 | func TestClient_syncStreams(t *testing.T) {
 50 | 	// Create streams for the server that we can talk to
 51 | 	stdout_r, stdout_w := io.Pipe()
 52 | 	stderr_r, stderr_w := io.Pipe()
 53 | 
 54 | 	client, _ := TestPluginRPCConn(t, map[string]Plugin{}, &TestOptions{
 55 | 		ServerStdout: stdout_r,
 56 | 		ServerStderr: stderr_r,
 57 | 	})
 58 | 
 59 | 	// Start the data copying
 60 | 	var stdout_out, stderr_out safeBuffer
 61 | 	stdout := &safeBuffer{
 62 | 		b: bytes.NewBufferString("stdouttest"),
 63 | 	}
 64 | 	stderr := &safeBuffer{
 65 | 		b: bytes.NewBufferString("stderrtest"),
 66 | 	}
 67 | 	go func() { _ = client.SyncStreams(&stdout_out, &stderr_out) }()
 68 | 	go func() { _, _ = io.Copy(stdout_w, stdout) }()
 69 | 	go func() { _, _ = io.Copy(stderr_w, stderr) }()
 70 | 
 71 | 	// Unfortunately I can't think of a better way to make sure all the
 72 | 	// copies above go through so let's just exit.
 73 | 	time.Sleep(100 * time.Millisecond)
 74 | 
 75 | 	// Close everything, and lets test the result
 76 | 	_ = client.Close()
 77 | 	_ = stdout_w.Close()
 78 | 	_ = stderr_w.Close()
 79 | 
 80 | 	if v := stdout_out.String(); v != "stdouttest" {
 81 | 		t.Fatalf("bad: %q", v)
 82 | 	}
 83 | 	if v := stderr_out.String(); v != "stderrtest" {
 84 | 		t.Fatalf("bad: %q", v)
 85 | 	}
 86 | }
 87 | 
 88 | type safeBuffer struct {
 89 | 	sync.Mutex
 90 | 	b *bytes.Buffer
 91 | }
 92 | 
 93 | func (s *safeBuffer) Write(p []byte) (n int, err error) {
 94 | 	s.Lock()
 95 | 	defer s.Unlock()
 96 | 	if s.b == nil {
 97 | 		s.b = new(bytes.Buffer)
 98 | 	}
 99 | 	return s.b.Write(p)
100 | }
101 | 
102 | func (s *safeBuffer) Read(p []byte) (n int, err error) {
103 | 	s.Lock()
104 | 	defer s.Unlock()
105 | 	if s.b == nil {
106 | 		s.b = new(bytes.Buffer)
107 | 	}
108 | 	return s.b.Read(p)
109 | }
110 | 
111 | func (s *safeBuffer) String() string {
112 | 	s.Lock()
113 | 	defer s.Unlock()
114 | 	if s.b == nil {
115 | 		s.b = new(bytes.Buffer)
116 | 	}
117 | 	return s.b.String()
118 | }
119 | 


--------------------------------------------------------------------------------
/rpc_server.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package plugin
  5 | 
  6 | import (
  7 | 	"errors"
  8 | 	"fmt"
  9 | 	"io"
 10 | 	"log"
 11 | 	"net"
 12 | 	"net/rpc"
 13 | 	"sync"
 14 | 
 15 | 	"github.com/hashicorp/yamux"
 16 | )
 17 | 
 18 | // RPCServer listens for network connections and then dispenses interface
 19 | // implementations over net/rpc.
 20 | //
 21 | // After setting the fields below, they shouldn't be read again directly
 22 | // from the structure which may be reading/writing them concurrently.
 23 | type RPCServer struct {
 24 | 	Plugins map[string]Plugin
 25 | 
 26 | 	// Stdout, Stderr are what this server will use instead of the
 27 | 	// normal stdin/out/err. This is because due to the multi-process nature
 28 | 	// of our plugin system, we can't use the normal process values so we
 29 | 	// make our own custom one we pipe across.
 30 | 	Stdout io.Reader
 31 | 	Stderr io.Reader
 32 | 
 33 | 	// DoneCh should be set to a non-nil channel that will be closed
 34 | 	// when the control requests the RPC server to end.
 35 | 	DoneCh chan<- struct{}
 36 | 
 37 | 	lock sync.Mutex
 38 | }
 39 | 
 40 | // ServerProtocol impl.
 41 | func (s *RPCServer) Init() error { return nil }
 42 | 
 43 | // ServerProtocol impl.
 44 | func (s *RPCServer) Config() string { return "" }
 45 | 
 46 | // ServerProtocol impl.
 47 | func (s *RPCServer) Serve(lis net.Listener) {
 48 | 	defer s.done()
 49 | 
 50 | 	for {
 51 | 		conn, err := lis.Accept()
 52 | 		if err != nil {
 53 | 			severity := "ERR"
 54 | 			if errors.Is(err, net.ErrClosed) {
 55 | 				severity = "DEBUG"
 56 | 			}
 57 | 			log.Printf("[%s] plugin: plugin server: %s", severity, err)
 58 | 			return
 59 | 		}
 60 | 
 61 | 		go s.ServeConn(conn)
 62 | 	}
 63 | }
 64 | 
 65 | // ServeConn runs a single connection.
 66 | //
 67 | // ServeConn blocks, serving the connection until the client hangs up.
 68 | func (s *RPCServer) ServeConn(conn io.ReadWriteCloser) {
 69 | 	// First create the yamux server to wrap this connection
 70 | 	mux, err := yamux.Server(conn, nil)
 71 | 	if err != nil {
 72 | 		_ = conn.Close()
 73 | 		log.Printf("[ERR] plugin: error creating yamux server: %s", err)
 74 | 		return
 75 | 	}
 76 | 
 77 | 	// Accept the control connection
 78 | 	control, err := mux.Accept()
 79 | 	if err != nil {
 80 | 		_ = mux.Close()
 81 | 		if err != io.EOF {
 82 | 			log.Printf("[ERR] plugin: error accepting control connection: %s", err)
 83 | 		}
 84 | 
 85 | 		return
 86 | 	}
 87 | 
 88 | 	// Connect the stdstreams (in, out, err)
 89 | 	stdstream := make([]net.Conn, 2)
 90 | 	for i := range stdstream {
 91 | 		stdstream[i], err = mux.Accept()
 92 | 		if err != nil {
 93 | 			_ = mux.Close()
 94 | 			log.Printf("[ERR] plugin: accepting stream %d: %s", i, err)
 95 | 			return
 96 | 		}
 97 | 	}
 98 | 
 99 | 	// Copy std streams out to the proper place
100 | 	go copyStream("stdout", stdstream[0], s.Stdout)
101 | 	go copyStream("stderr", stdstream[1], s.Stderr)
102 | 
103 | 	// Create the broker and start it up
104 | 	broker := newMuxBroker(mux)
105 | 	go broker.Run()
106 | 
107 | 	// Use the control connection to build the dispenser and serve the
108 | 	// connection.
109 | 	server := rpc.NewServer()
110 | 	_ = server.RegisterName("Control", &controlServer{
111 | 		server: s,
112 | 	})
113 | 	_ = server.RegisterName("Dispenser", &dispenseServer{
114 | 		broker:  broker,
115 | 		plugins: s.Plugins,
116 | 	})
117 | 	server.ServeConn(control)
118 | }
119 | 
120 | // done is called internally by the control server to trigger the
121 | // doneCh to close which is listened to by the main process to cleanly
122 | // exit.
123 | func (s *RPCServer) done() {
124 | 	s.lock.Lock()
125 | 	defer s.lock.Unlock()
126 | 
127 | 	if s.DoneCh != nil {
128 | 		close(s.DoneCh)
129 | 		s.DoneCh = nil
130 | 	}
131 | }
132 | 
133 | // dispenseServer dispenses variousinterface implementations for Terraform.
134 | type controlServer struct {
135 | 	server *RPCServer
136 | }
137 | 
138 | // Ping can be called to verify the connection (and likely the binary)
139 | // is still alive to a plugin.
140 | func (c *controlServer) Ping(
141 | 	null bool, response *struct{},
142 | ) error {
143 | 	*response = struct{}{}
144 | 	return nil
145 | }
146 | 
147 | func (c *controlServer) Quit(
148 | 	null bool, response *struct{},
149 | ) error {
150 | 	// End the server
151 | 	c.server.done()
152 | 
153 | 	// Always return true
154 | 	*response = struct{}{}
155 | 
156 | 	return nil
157 | }
158 | 
159 | // dispenseServer dispenses variousinterface implementations for Terraform.
160 | type dispenseServer struct {
161 | 	broker  *MuxBroker
162 | 	plugins map[string]Plugin
163 | }
164 | 
165 | func (d *dispenseServer) Dispense(
166 | 	name string, response *uint32,
167 | ) error {
168 | 	// Find the function to create this implementation
169 | 	p, ok := d.plugins[name]
170 | 	if !ok {
171 | 		return fmt.Errorf("unknown plugin type: %s", name)
172 | 	}
173 | 
174 | 	// Create the implementation first so we know if there is an error.
175 | 	impl, err := p.Server(d.broker)
176 | 	if err != nil {
177 | 		// We turn the error into an errors error so that it works across RPC
178 | 		return errors.New(err.Error())
179 | 	}
180 | 
181 | 	// Reserve an ID for our implementation
182 | 	id := d.broker.NextId()
183 | 	*response = id
184 | 
185 | 	// Run the rest in a goroutine since it can only happen once this RPC
186 | 	// call returns. We wait for a connection for the plugin implementation
187 | 	// and serve it.
188 | 	go func() {
189 | 		conn, err := d.broker.Accept(id)
190 | 		if err != nil {
191 | 			log.Printf("[ERR] go-plugin: plugin dispense error: %s: %s", name, err)
192 | 			return
193 | 		}
194 | 
195 | 		serve(conn, "Plugin", impl)
196 | 	}()
197 | 
198 | 	return nil
199 | }
200 | 
201 | func serve(conn io.ReadWriteCloser, name string, v interface{}) {
202 | 	server := rpc.NewServer()
203 | 	if err := server.RegisterName(name, v); err != nil {
204 | 		log.Printf("[ERR] go-plugin: plugin dispense error: %s", err)
205 | 		return
206 | 	}
207 | 
208 | 	server.ServeConn(conn)
209 | }
210 | 


--------------------------------------------------------------------------------
/runner/runner.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package runner
 5 | 
 6 | import (
 7 | 	"context"
 8 | 	"io"
 9 | )
10 | 
11 | // Runner defines the interface required by go-plugin to manage the lifecycle of
12 | // of a plugin and attempt to negotiate a connection with it. Note that this
13 | // is orthogonal to the protocol and transport used, which is negotiated over stdout.
14 | type Runner interface {
15 | 	// Start should start the plugin and ensure any work required for servicing
16 | 	// other interface methods is done. If the context is cancelled, it should
17 | 	// only abort any attempts to _start_ the plugin. Waiting and shutdown are
18 | 	// handled separately.
19 | 	Start(ctx context.Context) error
20 | 
21 | 	// Diagnose makes a best-effort attempt to return any debug information that
22 | 	// might help users understand why a plugin failed to start and negotiate a
23 | 	// connection.
24 | 	Diagnose(ctx context.Context) string
25 | 
26 | 	// Stdout is used to negotiate the go-plugin protocol.
27 | 	Stdout() io.ReadCloser
28 | 
29 | 	// Stderr is used for forwarding plugin logs to the host process logger.
30 | 	Stderr() io.ReadCloser
31 | 
32 | 	// Name is a human-friendly name for the plugin, such as the path to the
33 | 	// executable. It does not have to be unique.
34 | 	Name() string
35 | 
36 | 	AttachedRunner
37 | }
38 | 
39 | // AttachedRunner defines a limited subset of Runner's interface to represent the
40 | // reduced responsibility for plugin lifecycle when attaching to an already running
41 | // plugin.
42 | type AttachedRunner interface {
43 | 	// Wait should wait until the plugin stops running, whether in response to
44 | 	// an out of band signal or in response to calling Kill().
45 | 	Wait(ctx context.Context) error
46 | 
47 | 	// Kill should stop the plugin and perform any cleanup required.
48 | 	Kill(ctx context.Context) error
49 | 
50 | 	// ID is a unique identifier to represent the running plugin. e.g. pid or
51 | 	// container ID.
52 | 	ID() string
53 | 
54 | 	AddrTranslator
55 | }
56 | 
57 | // AddrTranslator translates addresses between the execution context of the host
58 | // process and the plugin. For example, if the plugin is in a container, the file
59 | // path for a Unix socket may be different between the host and the container.
60 | //
61 | // It is only intended to be used by the host process.
62 | type AddrTranslator interface {
63 | 	// Called before connecting on any addresses received back from the plugin.
64 | 	PluginToHost(pluginNet, pluginAddr string) (hostNet string, hostAddr string, err error)
65 | 
66 | 	// Called on any host process addresses before they are sent to the plugin.
67 | 	HostToPlugin(hostNet, hostAddr string) (pluginNet string, pluginAddr string, err error)
68 | }
69 | 
70 | // ReattachFunc can be passed to a client's reattach config to reattach to an
71 | // already running plugin instead of starting it ourselves.
72 | type ReattachFunc func() (AttachedRunner, error)
73 | 


--------------------------------------------------------------------------------
/server_mux.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | import (
 7 | 	"fmt"
 8 | 	"os"
 9 | )
10 | 
11 | // ServeMuxMap is the type that is used to configure ServeMux
12 | type ServeMuxMap map[string]*ServeConfig
13 | 
14 | // ServeMux is like Serve, but serves multiple types of plugins determined
15 | // by the argument given on the command-line.
16 | //
17 | // This command doesn't return until the plugin is done being executed. Any
18 | // errors are logged or output to stderr.
19 | func ServeMux(m ServeMuxMap) {
20 | 	if len(os.Args) != 2 {
21 | 		fmt.Fprintf(os.Stderr,
22 | 			"Invoked improperly. This is an internal command that shouldn't\n"+
23 | 				"be manually invoked.\n")
24 | 		os.Exit(1)
25 | 	}
26 | 
27 | 	opts, ok := m[os.Args[1]]
28 | 	if !ok {
29 | 		fmt.Fprintf(os.Stderr, "Unknown plugin: %s\n", os.Args[1])
30 | 		os.Exit(1)
31 | 	}
32 | 
33 | 	Serve(opts)
34 | }
35 | 


--------------------------------------------------------------------------------
/server_unix_test.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | //go:build !windows
 5 | // +build !windows
 6 | 
 7 | package plugin
 8 | 
 9 | import (
10 | 	"fmt"
11 | 	"os"
12 | 	"os/user"
13 | 	"syscall"
14 | 	"testing"
15 | )
16 | 
17 | func TestUnixSocketGroupPermissions(t *testing.T) {
18 | 	group, err := user.LookupGroupId(fmt.Sprintf("%d", os.Getgid()))
19 | 	if err != nil {
20 | 		t.Fatal(err)
21 | 	}
22 | 	for name, tc := range map[string]struct {
23 | 		group string
24 | 	}{
25 | 		"as integer": {fmt.Sprintf("%d", os.Getgid())},
26 | 		"as name":    {group.Name},
27 | 	} {
28 | 		t.Run(name, func(t *testing.T) {
29 | 			ln, err := serverListener_unix(UnixSocketConfig{Group: tc.group})
30 | 			if err != nil {
31 | 				t.Fatal(err)
32 | 			}
33 | 			defer func() { _ = ln.Close() }()
34 | 
35 | 			info, err := os.Lstat(ln.Addr().String())
36 | 			if err != nil {
37 | 				t.Fatal(err)
38 | 			}
39 | 			if info.Mode()&os.ModePerm != 0o660 {
40 | 				t.Fatal(info.Mode())
41 | 			}
42 | 			stat, ok := info.Sys().(*syscall.Stat_t)
43 | 			if !ok {
44 | 				t.Fatal()
45 | 			}
46 | 			if stat.Gid != uint32(os.Getgid()) {
47 | 				t.Fatalf("Expected %d, but got %d", os.Getgid(), stat.Gid)
48 | 			}
49 | 		})
50 | 	}
51 | }
52 | 


--------------------------------------------------------------------------------
/stream.go:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | package plugin
 5 | 
 6 | import (
 7 | 	"io"
 8 | 	"log"
 9 | )
10 | 
11 | func copyStream(name string, dst io.Writer, src io.Reader) {
12 | 	if src == nil {
13 | 		panic(name + ": src is nil")
14 | 	}
15 | 	if dst == nil {
16 | 		panic(name + ": dst is nil")
17 | 	}
18 | 	if _, err := io.Copy(dst, src); err != nil && err != io.EOF {
19 | 		log.Printf("[ERR] plugin: stream copy '%s' error: %s", name, err)
20 | 	}
21 | }
22 | 


--------------------------------------------------------------------------------
/test/grpc/test.proto:
--------------------------------------------------------------------------------
 1 | // Copyright (c) HashiCorp, Inc.
 2 | // SPDX-License-Identifier: MPL-2.0
 3 | 
 4 | syntax = "proto3";
 5 | 
 6 | package grpctest;
 7 | 
 8 | option go_package = "./grpctest";
 9 | 
10 | import "google/protobuf/empty.proto";
11 | 
12 | message TestRequest {
13 |     int32 Input = 1;
14 | }
15 | 
16 | message TestResponse {
17 |     int32 Output = 2;
18 | }
19 | 
20 | message PrintKVRequest {
21 |     string Key = 1;
22 |     oneof Value {
23 |         string ValueString = 2;
24 |         int32 ValueInt = 3;
25 |     }
26 | }
27 | 
28 | message PrintKVResponse {
29 | 
30 | }
31 | 
32 | message BidirectionalRequest {
33 |     uint32 id = 1;
34 | }
35 | 
36 | message BidirectionalResponse {
37 |     uint32 id = 1;
38 | }
39 | 
40 | message PrintStdioRequest {
41 |     bytes stdout = 1;
42 |     bytes stderr = 2;
43 | }
44 | 
45 | service Test {
46 |     rpc Double(TestRequest) returns (TestResponse) {}
47 |     rpc PrintKV(PrintKVRequest) returns (PrintKVResponse) {}
48 |     rpc Bidirectional(BidirectionalRequest) returns (BidirectionalResponse) {}
49 |     rpc Stream(stream TestRequest) returns (stream TestResponse) {}
50 |     rpc PrintStdio(PrintStdioRequest) returns (google.protobuf.Empty) {}
51 | }
52 | 
53 | message PingRequest {
54 | }
55 | 
56 | message PongResponse {
57 |     string msg = 1;
58 | }
59 | 
60 | service PingPong {
61 |     rpc Ping(PingRequest) returns (PongResponse) {}
62 | }
63 | 


--------------------------------------------------------------------------------
/testing.go:
--------------------------------------------------------------------------------
  1 | // Copyright (c) HashiCorp, Inc.
  2 | // SPDX-License-Identifier: MPL-2.0
  3 | 
  4 | package plugin
  5 | 
  6 | import (
  7 | 	"bytes"
  8 | 	"context"
  9 | 	"io"
 10 | 	"net"
 11 | 	"net/rpc"
 12 | 	"testing"
 13 | 
 14 | 	hclog "github.com/hashicorp/go-hclog"
 15 | 	"github.com/hashicorp/go-plugin/internal/grpcmux"
 16 | 	"google.golang.org/grpc"
 17 | 	"google.golang.org/grpc/credentials/insecure"
 18 | )
 19 | 
 20 | // TestOptions allows specifying options that can affect the behavior of the
 21 | // test functions
 22 | type TestOptions struct {
 23 | 	//ServerStdout causes the given value to be used in place of a blank buffer
 24 | 	//for RPCServer's Stdout
 25 | 	ServerStdout io.ReadCloser
 26 | 
 27 | 	//ServerStderr causes the given value to be used in place of a blank buffer
 28 | 	//for RPCServer's Stderr
 29 | 	ServerStderr io.ReadCloser
 30 | }
 31 | 
 32 | // The testing file contains test helpers that you can use outside of
 33 | // this package for making it easier to test plugins themselves.
 34 | 
 35 | // TestConn is a helper function for returning a client and server
 36 | // net.Conn connected to each other.
 37 | func TestConn(t testing.TB) (net.Conn, net.Conn) {
 38 | 	// Listen to any local port. This listener will be closed
 39 | 	// after a single connection is established.
 40 | 	l, err := net.Listen("tcp", "127.0.0.1:0")
 41 | 	if err != nil {
 42 | 		t.Fatalf("err: %s", err)
 43 | 	}
 44 | 
 45 | 	// Start a goroutine to accept our client connection
 46 | 	var serverConn net.Conn
 47 | 	doneCh := make(chan struct{})
 48 | 	go func() {
 49 | 		defer close(doneCh)
 50 | 		defer func() { _ = l.Close() }()
 51 | 		var err error
 52 | 		serverConn, err = l.Accept()
 53 | 		if err != nil {
 54 | 			t.Fatalf("err: %s", err)
 55 | 		}
 56 | 	}()
 57 | 
 58 | 	// Connect to the server
 59 | 	clientConn, err := net.Dial("tcp", l.Addr().String())
 60 | 	if err != nil {
 61 | 		t.Fatalf("err: %s", err)
 62 | 	}
 63 | 
 64 | 	// Wait for the server side to acknowledge it has connected
 65 | 	<-doneCh
 66 | 
 67 | 	return clientConn, serverConn
 68 | }
 69 | 
 70 | // TestRPCConn returns a rpc client and server connected to each other.
 71 | func TestRPCConn(t testing.TB) (*rpc.Client, *rpc.Server) {
 72 | 	clientConn, serverConn := TestConn(t)
 73 | 
 74 | 	server := rpc.NewServer()
 75 | 	go server.ServeConn(serverConn)
 76 | 
 77 | 	client := rpc.NewClient(clientConn)
 78 | 	return client, server
 79 | }
 80 | 
 81 | // TestPluginRPCConn returns a plugin RPC client and server that are connected
 82 | // together and configured.
 83 | func TestPluginRPCConn(t testing.TB, ps map[string]Plugin, opts *TestOptions) (*RPCClient, *RPCServer) {
 84 | 	// Create two net.Conns we can use to shuttle our control connection
 85 | 	clientConn, serverConn := TestConn(t)
 86 | 
 87 | 	// Start up the server
 88 | 	server := &RPCServer{Plugins: ps, Stdout: new(bytes.Buffer), Stderr: new(bytes.Buffer)}
 89 | 	if opts != nil {
 90 | 		if opts.ServerStdout != nil {
 91 | 			server.Stdout = opts.ServerStdout
 92 | 		}
 93 | 		if opts.ServerStderr != nil {
 94 | 			server.Stderr = opts.ServerStderr
 95 | 		}
 96 | 	}
 97 | 	go server.ServeConn(serverConn)
 98 | 
 99 | 	// Connect the client to the server
100 | 	client, err := NewRPCClient(clientConn, ps)
101 | 	if err != nil {
102 | 		t.Fatalf("err: %s", err)
103 | 	}
104 | 
105 | 	return client, server
106 | }
107 | 
108 | // TestGRPCConn returns a gRPC client conn and grpc server that are connected
109 | // together and configured. The register function is used to register services
110 | // prior to the Serve call. This is used to test gRPC connections.
111 | func TestGRPCConn(t testing.TB, register func(*grpc.Server)) (*grpc.ClientConn, *grpc.Server) {
112 | 	// Create a listener
113 | 	l, err := net.Listen("tcp", "127.0.0.1:0")
114 | 	if err != nil {
115 | 		t.Fatalf("err: %s", err)
116 | 	}
117 | 
118 | 	server := grpc.NewServer()
119 | 	register(server)
120 | 	go func() { _ = server.Serve(l) }()
121 | 
122 | 	// Connect to the server
123 | 	conn, err := grpc.Dial(
124 | 		l.Addr().String(),
125 | 		grpc.WithBlock(),
126 | 		grpc.WithTransportCredentials(insecure.NewCredentials()),
127 | 	)
128 | 	if err != nil {
129 | 		t.Fatalf("err: %s", err)
130 | 	}
131 | 
132 | 	// Connection successful, close the listener
133 | 	_ = l.Close()
134 | 
135 | 	return conn, server
136 | }
137 | 
138 | // TestPluginGRPCConn returns a plugin gRPC client and server that are connected
139 | // together and configured. This is used to test gRPC connections.
140 | func TestPluginGRPCConn(t testing.TB, multiplex bool, ps map[string]Plugin) (*GRPCClient, *GRPCServer) {
141 | 	// Create a listener
142 | 	ln, err := serverListener(UnixSocketConfig{})
143 | 	if err != nil {
144 | 		t.Fatal(err)
145 | 	}
146 | 
147 | 	logger := hclog.New(&hclog.LoggerOptions{
148 | 		Level: hclog.Debug,
149 | 	})
150 | 
151 | 	// Start up the server
152 | 	var muxer *grpcmux.GRPCServerMuxer
153 | 	if multiplex {
154 | 		muxer = grpcmux.NewGRPCServerMuxer(logger, ln)
155 | 		ln = muxer
156 | 	}
157 | 	server := &GRPCServer{
158 | 		Plugins: ps,
159 | 		DoneCh:  make(chan struct{}),
160 | 		Server:  DefaultGRPCServer,
161 | 		Stdout:  new(bytes.Buffer),
162 | 		Stderr:  new(bytes.Buffer),
163 | 		logger:  logger,
164 | 		muxer:   muxer,
165 | 	}
166 | 	if err := server.Init(); err != nil {
167 | 		t.Fatalf("err: %s", err)
168 | 	}
169 | 	go server.Serve(ln)
170 | 
171 | 	client := &Client{
172 | 		address:  ln.Addr(),
173 | 		protocol: ProtocolGRPC,
174 | 		config: &ClientConfig{
175 | 			Plugins:             ps,
176 | 			GRPCBrokerMultiplex: multiplex,
177 | 		},
178 | 		logger: logger,
179 | 	}
180 | 
181 | 	grpcClient, err := newGRPCClient(context.Background(), client)
182 | 	if err != nil {
183 | 		t.Fatal(err)
184 | 	}
185 | 
186 | 	return grpcClient, server
187 | }
188 | 


--------------------------------------------------------------------------------