From 2661c2e2fc5b14ccb0288271763ab3e8f7e2d88f Mon Sep 17 00:00:00 2001 From: magodo Date: Mon, 4 Sep 2023 17:06:09 +0800 Subject: [PATCH 01/11] WIP --- client.go | 329 ------------------------ client_start_other.go | 332 +++++++++++++++++++++++++ client_start_wasm.go | 306 +++++++++++++++++++++++ go.mod | 8 +- go.sum | 54 +--- internal/wasmrunner/addr_translator.go | 13 + internal/wasmrunner/wasm_runner.go | 97 ++++++++ server.go | 289 +-------------------- server_serve_other.go | 299 ++++++++++++++++++++++ server_serve_wasm.go | 220 ++++++++++++++++ 10 files changed, 1287 insertions(+), 660 deletions(-) create mode 100644 client_start_other.go create mode 100644 client_start_wasm.go create mode 100644 internal/wasmrunner/addr_translator.go create mode 100644 internal/wasmrunner/wasm_runner.go create mode 100644 server_serve_other.go create mode 100644 server_serve_wasm.go diff --git a/client.go b/client.go index 6f293278..e92620d4 100644 --- a/client.go +++ b/client.go @@ -516,335 +516,6 @@ func (c *Client) Kill() { c.l.Unlock() } -// Start the underlying subprocess, communicating with it to negotiate -// a port for RPC connections, and returning the address to connect via RPC. -// -// This method is safe to call multiple times. Subsequent calls have no effect. -// Once a client has been started once, it cannot be started again, even if -// it was killed. -func (c *Client) Start() (addr net.Addr, err error) { - c.l.Lock() - defer c.l.Unlock() - - if c.address != nil { - return c.address, nil - } - - // If one of cmd or reattach isn't set, then it is an error. We wrap - // this in a {} for scoping reasons, and hopeful that the escape - // analysis will pop the stack here. - { - var mutuallyExclusiveOptions int - if c.config.Cmd != nil { - mutuallyExclusiveOptions += 1 - } - if c.config.Reattach != nil { - mutuallyExclusiveOptions += 1 - } - if c.config.RunnerFunc != nil { - mutuallyExclusiveOptions += 1 - } - if mutuallyExclusiveOptions != 1 { - return nil, fmt.Errorf("exactly one of Cmd, or Reattach, or RunnerFunc must be set") - } - - if c.config.SecureConfig != nil && c.config.Reattach != nil { - return nil, ErrSecureConfigAndReattach - } - } - - if c.config.Reattach != nil { - return c.reattach() - } - - if c.config.VersionedPlugins == nil { - c.config.VersionedPlugins = make(map[int]PluginSet) - } - - // handle all plugins as versioned, using the handshake config as the default. - version := int(c.config.ProtocolVersion) - - // Make sure we're not overwriting a real version 0. If ProtocolVersion was - // non-zero, then we have to just assume the user made sure that - // VersionedPlugins doesn't conflict. - if _, ok := c.config.VersionedPlugins[version]; !ok && c.config.Plugins != nil { - c.config.VersionedPlugins[version] = c.config.Plugins - } - - var versionStrings []string - for v := range c.config.VersionedPlugins { - versionStrings = append(versionStrings, strconv.Itoa(v)) - } - - env := []string{ - fmt.Sprintf("%s=%s", c.config.MagicCookieKey, c.config.MagicCookieValue), - fmt.Sprintf("PLUGIN_MIN_PORT=%d", c.config.MinPort), - fmt.Sprintf("PLUGIN_MAX_PORT=%d", c.config.MaxPort), - fmt.Sprintf("PLUGIN_PROTOCOL_VERSIONS=%s", strings.Join(versionStrings, ",")), - } - - cmd := c.config.Cmd - if cmd == nil { - // It's only possible to get here if RunnerFunc is non-nil, but we'll - // still use cmd as a spec to populate metadata for the external - // implementation to consume. - cmd = exec.Command("") - } - if !c.config.SkipHostEnv { - cmd.Env = append(cmd.Env, os.Environ()...) - } - cmd.Env = append(cmd.Env, env...) - cmd.Stdin = os.Stdin - - if c.config.SecureConfig != nil { - if ok, err := c.config.SecureConfig.Check(cmd.Path); err != nil { - return nil, fmt.Errorf("error verifying checksum: %s", err) - } else if !ok { - return nil, ErrChecksumsDoNotMatch - } - } - - // Setup a temporary certificate for client/server mtls, and send the public - // certificate to the plugin. - if c.config.AutoMTLS { - c.logger.Info("configuring client automatic mTLS") - certPEM, keyPEM, err := generateCert() - if err != nil { - c.logger.Error("failed to generate client certificate", "error", err) - return nil, err - } - cert, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - c.logger.Error("failed to parse client certificate", "error", err) - return nil, err - } - - cmd.Env = append(cmd.Env, fmt.Sprintf("PLUGIN_CLIENT_CERT=%s", certPEM)) - - c.config.TLSConfig = &tls.Config{ - Certificates: []tls.Certificate{cert}, - ClientAuth: tls.RequireAndVerifyClientCert, - MinVersion: tls.VersionTLS12, - ServerName: "localhost", - } - } - - var runner runner.Runner - switch { - case c.config.RunnerFunc != nil: - c.hostSocketDir, err = os.MkdirTemp("", "") - if err != nil { - return nil, err - } - c.logger.Trace("created temporary directory for unix sockets", "dir", c.hostSocketDir) - runner, err = c.config.RunnerFunc(c.logger, cmd, c.hostSocketDir) - if err != nil { - return nil, err - } - default: - runner, err = cmdrunner.NewCmdRunner(c.logger, cmd) - if err != nil { - return nil, err - } - - } - - c.runner = runner - startCtx, startCtxCancel := context.WithTimeout(context.Background(), c.config.StartTimeout) - defer startCtxCancel() - err = runner.Start(startCtx) - if err != nil { - return nil, err - } - - // Make sure the command is properly cleaned up if there is an error - defer func() { - rErr := recover() - - if err != nil || rErr != nil { - runner.Kill(context.Background()) - } - - if rErr != nil { - panic(rErr) - } - }() - - // Create a context for when we kill - c.doneCtx, c.ctxCancel = context.WithCancel(context.Background()) - - // Start goroutine that logs the stderr - c.clientWaitGroup.Add(1) - c.stderrWaitGroup.Add(1) - // logStderr calls Done() - go c.logStderr(runner.Name(), runner.Stderr()) - - c.clientWaitGroup.Add(1) - go func() { - // ensure the context is cancelled when we're done - defer c.ctxCancel() - - defer c.clientWaitGroup.Done() - - // wait to finish reading from stderr since the stderr pipe reader - // will be closed by the subsequent call to cmd.Wait(). - c.stderrWaitGroup.Wait() - - // Wait for the command to end. - err := runner.Wait(context.Background()) - if err != nil { - c.logger.Error("plugin process exited", "plugin", runner.Name(), "id", runner.ID(), "error", err.Error()) - } else { - // Log and make sure to flush the logs right away - c.logger.Info("plugin process exited", "plugin", runner.Name(), "id", runner.ID()) - } - - os.Stderr.Sync() - - // Set that we exited, which takes a lock - c.l.Lock() - defer c.l.Unlock() - c.exited = true - }() - - // Start a goroutine that is going to be reading the lines - // out of stdout - linesCh := make(chan string) - c.clientWaitGroup.Add(1) - go func() { - defer c.clientWaitGroup.Done() - defer close(linesCh) - - scanner := bufio.NewScanner(runner.Stdout()) - for scanner.Scan() { - linesCh <- scanner.Text() - } - if scanner.Err() != nil { - c.logger.Error("error encountered while scanning stdout", "error", scanner.Err()) - } - }() - - // Make sure after we exit we read the lines from stdout forever - // so they don't block since it is a pipe. - // The scanner goroutine above will close this, but track it with a wait - // group for completeness. - c.clientWaitGroup.Add(1) - defer func() { - go func() { - defer c.clientWaitGroup.Done() - for range linesCh { - } - }() - }() - - // Some channels for the next step - timeout := time.After(c.config.StartTimeout) - - // Start looking for the address - c.logger.Debug("waiting for RPC address", "plugin", runner.Name()) - select { - case <-timeout: - err = errors.New("timeout while waiting for plugin to start") - case <-c.doneCtx.Done(): - err = errors.New("plugin exited before we could connect") - case line, ok := <-linesCh: - // Trim the line and split by "|" in order to get the parts of - // the output. - line = strings.TrimSpace(line) - parts := strings.SplitN(line, "|", 6) - if len(parts) < 4 { - errText := fmt.Sprintf("Unrecognized remote plugin message: %s", line) - if !ok { - errText += "\n" + "Failed to read any lines from plugin's stdout" - } - additionalNotes := runner.Diagnose(context.Background()) - if additionalNotes != "" { - errText += "\n" + additionalNotes - } - err = errors.New(errText) - return - } - - // Check the core protocol. Wrapped in a {} for scoping. - { - var coreProtocol int - coreProtocol, err = strconv.Atoi(parts[0]) - if err != nil { - err = fmt.Errorf("Error parsing core protocol version: %s", err) - return - } - - if coreProtocol != CoreProtocolVersion { - err = fmt.Errorf("Incompatible core API version with plugin. "+ - "Plugin version: %s, Core version: %d\n\n"+ - "To fix this, the plugin usually only needs to be recompiled.\n"+ - "Please report this to the plugin author.", parts[0], CoreProtocolVersion) - return - } - } - - // Test the API version - version, pluginSet, err := c.checkProtoVersion(parts[1]) - if err != nil { - return addr, err - } - - // set the Plugins value to the compatible set, so the version - // doesn't need to be passed through to the ClientProtocol - // implementation. - c.config.Plugins = pluginSet - c.negotiatedVersion = version - c.logger.Debug("using plugin", "version", version) - - network, address, err := runner.PluginToHost(parts[2], parts[3]) - if err != nil { - return addr, err - } - - switch network { - case "tcp": - addr, err = net.ResolveTCPAddr("tcp", address) - case "unix": - addr, err = net.ResolveUnixAddr("unix", address) - default: - err = fmt.Errorf("Unknown address type: %s", address) - } - - // If we have a server type, then record that. We default to net/rpc - // for backwards compatibility. - c.protocol = ProtocolNetRPC - if len(parts) >= 5 { - c.protocol = Protocol(parts[4]) - } - - found := false - for _, p := range c.config.AllowedProtocols { - if p == c.protocol { - found = true - break - } - } - if !found { - err = fmt.Errorf("Unsupported plugin protocol %q. Supported: %v", - c.protocol, c.config.AllowedProtocols) - return addr, err - } - - // See if we have a TLS certificate from the server. - // Checking if the length is > 50 rules out catching the unused "extra" - // data returned from some older implementations. - if len(parts) >= 6 && len(parts[5]) > 50 { - err := c.loadServerCert(parts[5]) - if err != nil { - return nil, fmt.Errorf("error parsing server cert: %s", err) - } - } - } - - c.address = addr - return -} - // loadServerCert is used by AutoMTLS to read an x.509 cert returned by the // server, and load it as the RootCA and ClientCA for the client TLSConfig. func (c *Client) loadServerCert(cert string) error { diff --git a/client_start_other.go b/client_start_other.go new file mode 100644 index 00000000..35a8ac05 --- /dev/null +++ b/client_start_other.go @@ -0,0 +1,332 @@ +//go:build !wasm + +package plugin + +// Start the underlying subprocess, communicating with it to negotiate +// a port for RPC connections, and returning the address to connect via RPC. +// +// This method is safe to call multiple times. Subsequent calls have no effect. +// Once a client has been started once, it cannot be started again, even if +// it was killed. +func (c *Client) Start() (addr net.Addr, err error) { + c.l.Lock() + defer c.l.Unlock() + + if c.address != nil { + return c.address, nil + } + + // If one of cmd or reattach isn't set, then it is an error. We wrap + // this in a {} for scoping reasons, and hopeful that the escape + // analysis will pop the stack here. + { + var mutuallyExclusiveOptions int + if c.config.Cmd != nil { + mutuallyExclusiveOptions += 1 + } + if c.config.Reattach != nil { + mutuallyExclusiveOptions += 1 + } + if c.config.RunnerFunc != nil { + mutuallyExclusiveOptions += 1 + } + if mutuallyExclusiveOptions != 1 { + return nil, fmt.Errorf("exactly one of Cmd, or Reattach, or RunnerFunc must be set") + } + + if c.config.SecureConfig != nil && c.config.Reattach != nil { + return nil, ErrSecureConfigAndReattach + } + } + + if c.config.Reattach != nil { + return c.reattach() + } + + if c.config.VersionedPlugins == nil { + c.config.VersionedPlugins = make(map[int]PluginSet) + } + + // handle all plugins as versioned, using the handshake config as the default. + version := int(c.config.ProtocolVersion) + + // Make sure we're not overwriting a real version 0. If ProtocolVersion was + // non-zero, then we have to just assume the user made sure that + // VersionedPlugins doesn't conflict. + if _, ok := c.config.VersionedPlugins[version]; !ok && c.config.Plugins != nil { + c.config.VersionedPlugins[version] = c.config.Plugins + } + + var versionStrings []string + for v := range c.config.VersionedPlugins { + versionStrings = append(versionStrings, strconv.Itoa(v)) + } + + env := []string{ + fmt.Sprintf("%s=%s", c.config.MagicCookieKey, c.config.MagicCookieValue), + fmt.Sprintf("PLUGIN_MIN_PORT=%d", c.config.MinPort), + fmt.Sprintf("PLUGIN_MAX_PORT=%d", c.config.MaxPort), + fmt.Sprintf("PLUGIN_PROTOCOL_VERSIONS=%s", strings.Join(versionStrings, ",")), + } + + cmd := c.config.Cmd + if cmd == nil { + // It's only possible to get here if RunnerFunc is non-nil, but we'll + // still use cmd as a spec to populate metadata for the external + // implementation to consume. + cmd = exec.Command("") + } + if !c.config.SkipHostEnv { + cmd.Env = append(cmd.Env, os.Environ()...) + } + cmd.Env = append(cmd.Env, env...) + cmd.Stdin = os.Stdin + + if c.config.SecureConfig != nil { + if ok, err := c.config.SecureConfig.Check(cmd.Path); err != nil { + return nil, fmt.Errorf("error verifying checksum: %s", err) + } else if !ok { + return nil, ErrChecksumsDoNotMatch + } + } + + // Setup a temporary certificate for client/server mtls, and send the public + // certificate to the plugin. + if c.config.AutoMTLS { + c.logger.Info("configuring client automatic mTLS") + certPEM, keyPEM, err := generateCert() + if err != nil { + c.logger.Error("failed to generate client certificate", "error", err) + return nil, err + } + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + c.logger.Error("failed to parse client certificate", "error", err) + return nil, err + } + + cmd.Env = append(cmd.Env, fmt.Sprintf("PLUGIN_CLIENT_CERT=%s", certPEM)) + + c.config.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAndVerifyClientCert, + MinVersion: tls.VersionTLS12, + ServerName: "localhost", + } + } + + var runner runner.Runner + switch { + case c.config.RunnerFunc != nil: + c.hostSocketDir, err = os.MkdirTemp("", "") + if err != nil { + return nil, err + } + c.logger.Trace("created temporary directory for unix sockets", "dir", c.hostSocketDir) + runner, err = c.config.RunnerFunc(c.logger, cmd, c.hostSocketDir) + if err != nil { + return nil, err + } + default: + runner, err = cmdrunner.NewCmdRunner(c.logger, cmd) + if err != nil { + return nil, err + } + + } + + c.runner = runner + startCtx, startCtxCancel := context.WithTimeout(context.Background(), c.config.StartTimeout) + defer startCtxCancel() + err = runner.Start(startCtx) + if err != nil { + return nil, err + } + + // Make sure the command is properly cleaned up if there is an error + defer func() { + rErr := recover() + + if err != nil || rErr != nil { + runner.Kill(context.Background()) + } + + if rErr != nil { + panic(rErr) + } + }() + + // Create a context for when we kill + c.doneCtx, c.ctxCancel = context.WithCancel(context.Background()) + + // Start goroutine that logs the stderr + c.clientWaitGroup.Add(1) + c.stderrWaitGroup.Add(1) + // logStderr calls Done() + go c.logStderr(runner.Name(), runner.Stderr()) + + c.clientWaitGroup.Add(1) + go func() { + // ensure the context is cancelled when we're done + defer c.ctxCancel() + + defer c.clientWaitGroup.Done() + + // wait to finish reading from stderr since the stderr pipe reader + // will be closed by the subsequent call to cmd.Wait(). + c.stderrWaitGroup.Wait() + + // Wait for the command to end. + err := runner.Wait(context.Background()) + if err != nil { + c.logger.Error("plugin process exited", "plugin", runner.Name(), "id", runner.ID(), "error", err.Error()) + } else { + // Log and make sure to flush the logs right away + c.logger.Info("plugin process exited", "plugin", runner.Name(), "id", runner.ID()) + } + + os.Stderr.Sync() + + // Set that we exited, which takes a lock + c.l.Lock() + defer c.l.Unlock() + c.exited = true + }() + + // Start a goroutine that is going to be reading the lines + // out of stdout + linesCh := make(chan string) + c.clientWaitGroup.Add(1) + go func() { + defer c.clientWaitGroup.Done() + defer close(linesCh) + + scanner := bufio.NewScanner(runner.Stdout()) + for scanner.Scan() { + linesCh <- scanner.Text() + } + if scanner.Err() != nil { + c.logger.Error("error encountered while scanning stdout", "error", scanner.Err()) + } + }() + + // Make sure after we exit we read the lines from stdout forever + // so they don't block since it is a pipe. + // The scanner goroutine above will close this, but track it with a wait + // group for completeness. + c.clientWaitGroup.Add(1) + defer func() { + go func() { + defer c.clientWaitGroup.Done() + for range linesCh { + } + }() + }() + + // Some channels for the next step + timeout := time.After(c.config.StartTimeout) + + // Start looking for the address + c.logger.Debug("waiting for RPC address", "plugin", runner.Name()) + select { + case <-timeout: + err = errors.New("timeout while waiting for plugin to start") + case <-c.doneCtx.Done(): + err = errors.New("plugin exited before we could connect") + case line, ok := <-linesCh: + // Trim the line and split by "|" in order to get the parts of + // the output. + line = strings.TrimSpace(line) + parts := strings.SplitN(line, "|", 6) + if len(parts) < 4 { + errText := fmt.Sprintf("Unrecognized remote plugin message: %s", line) + if !ok { + errText += "\n" + "Failed to read any lines from plugin's stdout" + } + additionalNotes := runner.Diagnose(context.Background()) + if additionalNotes != "" { + errText += "\n" + additionalNotes + } + err = errors.New(errText) + return + } + + // Check the core protocol. Wrapped in a {} for scoping. + { + var coreProtocol int + coreProtocol, err = strconv.Atoi(parts[0]) + if err != nil { + err = fmt.Errorf("Error parsing core protocol version: %s", err) + return + } + + if coreProtocol != CoreProtocolVersion { + err = fmt.Errorf("Incompatible core API version with plugin. "+ + "Plugin version: %s, Core version: %d\n\n"+ + "To fix this, the plugin usually only needs to be recompiled.\n"+ + "Please report this to the plugin author.", parts[0], CoreProtocolVersion) + return + } + } + + // Test the API version + version, pluginSet, err := c.checkProtoVersion(parts[1]) + if err != nil { + return addr, err + } + + // set the Plugins value to the compatible set, so the version + // doesn't need to be passed through to the ClientProtocol + // implementation. + c.config.Plugins = pluginSet + c.negotiatedVersion = version + c.logger.Debug("using plugin", "version", version) + + network, address, err := runner.PluginToHost(parts[2], parts[3]) + if err != nil { + return addr, err + } + + switch network { + case "tcp": + addr, err = net.ResolveTCPAddr("tcp", address) + case "unix": + addr, err = net.ResolveUnixAddr("unix", address) + default: + err = fmt.Errorf("Unknown address type: %s", address) + } + + // If we have a server type, then record that. We default to net/rpc + // for backwards compatibility. + c.protocol = ProtocolNetRPC + if len(parts) >= 5 { + c.protocol = Protocol(parts[4]) + } + + found := false + for _, p := range c.config.AllowedProtocols { + if p == c.protocol { + found = true + break + } + } + if !found { + err = fmt.Errorf("Unsupported plugin protocol %q. Supported: %v", + c.protocol, c.config.AllowedProtocols) + return addr, err + } + + // See if we have a TLS certificate from the server. + // Checking if the length is > 50 rules out catching the unused "extra" + // data returned from some older implementations. + if len(parts) >= 6 && len(parts[5]) > 50 { + err := c.loadServerCert(parts[5]) + if err != nil { + return nil, fmt.Errorf("error parsing server cert: %s", err) + } + } + } + + c.address = addr + return +} diff --git a/client_start_wasm.go b/client_start_wasm.go new file mode 100644 index 00000000..6de1b950 --- /dev/null +++ b/client_start_wasm.go @@ -0,0 +1,306 @@ +//go:build js && wasm + +package plugin + +import ( + "bufio" + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "os" + "strconv" + "strings" + "time" + + "github.com/hashicorp/go-plugin/internal/wasmrunner" + "github.com/hashicorp/go-plugin/runner" +) + +// Start the underlying subprocess, communicating with it to negotiate +// a port for RPC connections, and returning the address to connect via RPC. +// +// This method is safe to call multiple times. Subsequent calls have no effect. +// Once a client has been started once, it cannot be started again, even if +// it was killed. +func (c *Client) Start() (addr net.Addr, err error) { + c.l.Lock() + defer c.l.Unlock() + + if c.address != nil { + return c.address, nil + } + + if c.config.Cmd == nil { + return nil, fmt.Errorf("Cmd must be set") + } + if c.config.Reattach != nil { + return nil, fmt.Errorf("Reattach is not supported") + } + if c.config.RunnerFunc != nil { + return nil, fmt.Errorf("RunnerFunc is not supported") + } + if c.config.SecureConfig != nil { + return nil, fmt.Errorf("SecureConfig is not supported") + } + + if c.config.VersionedPlugins == nil { + c.config.VersionedPlugins = make(map[int]PluginSet) + } + + // handle all plugins as versioned, using the handshake config as the default. + version := int(c.config.ProtocolVersion) + + // Make sure we're not overwriting a real version 0. If ProtocolVersion was + // non-zero, then we have to just assume the user made sure that + // VersionedPlugins doesn't conflict. + if _, ok := c.config.VersionedPlugins[version]; !ok && c.config.Plugins != nil { + c.config.VersionedPlugins[version] = c.config.Plugins + } + + var versionStrings []string + for v := range c.config.VersionedPlugins { + versionStrings = append(versionStrings, strconv.Itoa(v)) + } + + env := []string{ + fmt.Sprintf("%s=%s", c.config.MagicCookieKey, c.config.MagicCookieValue), + fmt.Sprintf("PLUGIN_MIN_PORT=%d", c.config.MinPort), + fmt.Sprintf("PLUGIN_MAX_PORT=%d", c.config.MaxPort), + fmt.Sprintf("PLUGIN_PROTOCOL_VERSIONS=%s", strings.Join(versionStrings, ",")), + } + + cmd := c.config.Cmd + if !c.config.SkipHostEnv { + cmd.Env = append(cmd.Env, os.Environ()...) + } + cmd.Env = append(cmd.Env, env...) + cmd.Stdin = os.Stdin + + // Setup a temporary certificate for client/server mtls, and send the public + // certificate to the plugin. + if c.config.AutoMTLS { + c.logger.Info("configuring client automatic mTLS") + certPEM, keyPEM, err := generateCert() + if err != nil { + c.logger.Error("failed to generate client certificate", "error", err) + return nil, err + } + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + c.logger.Error("failed to parse client certificate", "error", err) + return nil, err + } + + cmd.Env = append(cmd.Env, fmt.Sprintf("PLUGIN_CLIENT_CERT=%s", certPEM)) + + c.config.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAndVerifyClientCert, + MinVersion: tls.VersionTLS12, + ServerName: "localhost", + } + } + + var runner runner.Runner + runner, err = wasmrunner.NewWasmRunner(c.logger, cmd) + if err != nil { + return nil, err + } + + c.runner = runner + startCtx, startCtxCancel := context.WithTimeout(context.Background(), c.config.StartTimeout) + defer startCtxCancel() + err = runner.Start(startCtx) + if err != nil { + return nil, err + } + + // Make sure the command is properly cleaned up if there is an error + defer func() { + rErr := recover() + + if err != nil || rErr != nil { + runner.Kill(context.Background()) + } + + if rErr != nil { + panic(rErr) + } + }() + + // Create a context for when we kill + c.doneCtx, c.ctxCancel = context.WithCancel(context.Background()) + + // Start goroutine that logs the stderr + c.clientWaitGroup.Add(1) + c.stderrWaitGroup.Add(1) + // logStderr calls Done() + go c.logStderr(runner.Name(), runner.Stderr()) + + c.clientWaitGroup.Add(1) + go func() { + // ensure the context is cancelled when we're done + defer c.ctxCancel() + + defer c.clientWaitGroup.Done() + + // wait to finish reading from stderr since the stderr pipe reader + // will be closed by the subsequent call to cmd.Wait(). + c.stderrWaitGroup.Wait() + + // Wait for the command to end. + err := runner.Wait(context.Background()) + if err != nil { + c.logger.Error("plugin process exited", "plugin", runner.Name(), "id", runner.ID(), "error", err.Error()) + } else { + // Log and make sure to flush the logs right away + c.logger.Info("plugin process exited", "plugin", runner.Name(), "id", runner.ID()) + } + + os.Stderr.Sync() + + // Set that we exited, which takes a lock + c.l.Lock() + defer c.l.Unlock() + c.exited = true + }() + + // Start a goroutine that is going to be reading the lines + // out of stdout + linesCh := make(chan string) + c.clientWaitGroup.Add(1) + go func() { + defer c.clientWaitGroup.Done() + defer close(linesCh) + + scanner := bufio.NewScanner(runner.Stdout()) + for scanner.Scan() { + linesCh <- scanner.Text() + } + if scanner.Err() != nil { + c.logger.Error("error encountered while scanning stdout", "error", scanner.Err()) + } + }() + + // Make sure after we exit we read the lines from stdout forever + // so they don't block since it is a pipe. + // The scanner goroutine above will close this, but track it with a wait + // group for completeness. + c.clientWaitGroup.Add(1) + defer func() { + go func() { + defer c.clientWaitGroup.Done() + for range linesCh { + } + }() + }() + + // Some channels for the next step + timeout := time.After(c.config.StartTimeout) + + // Start looking for the address + c.logger.Debug("waiting for RPC address", "plugin", runner.Name()) + select { + case <-timeout: + err = errors.New("timeout while waiting for plugin to start") + case <-c.doneCtx.Done(): + err = errors.New("plugin exited before we could connect") + case line, ok := <-linesCh: + // Trim the line and split by "|" in order to get the parts of + // the output. + line = strings.TrimSpace(line) + parts := strings.SplitN(line, "|", 6) + if len(parts) < 4 { + errText := fmt.Sprintf("Unrecognized remote plugin message: %s", line) + if !ok { + errText += "\n" + "Failed to read any lines from plugin's stdout" + } + additionalNotes := runner.Diagnose(context.Background()) + if additionalNotes != "" { + errText += "\n" + additionalNotes + } + err = errors.New(errText) + return + } + + // Check the core protocol. Wrapped in a {} for scoping. + { + var coreProtocol int + coreProtocol, err = strconv.Atoi(parts[0]) + if err != nil { + err = fmt.Errorf("Error parsing core protocol version: %s", err) + return + } + + if coreProtocol != CoreProtocolVersion { + err = fmt.Errorf("Incompatible core API version with plugin. "+ + "Plugin version: %s, Core version: %d\n\n"+ + "To fix this, the plugin usually only needs to be recompiled.\n"+ + "Please report this to the plugin author.", parts[0], CoreProtocolVersion) + return + } + } + + // Test the API version + version, pluginSet, err := c.checkProtoVersion(parts[1]) + if err != nil { + return addr, err + } + + // set the Plugins value to the compatible set, so the version + // doesn't need to be passed through to the ClientProtocol + // implementation. + c.config.Plugins = pluginSet + c.negotiatedVersion = version + c.logger.Debug("using plugin", "version", version) + + network, address, err := runner.PluginToHost(parts[2], parts[3]) + if err != nil { + return addr, err + } + + switch network { + // case "tcp": + // addr, err = net.ResolveTCPAddr("tcp", address) + // case "unix": + // addr, err = net.ResolveUnixAddr("unix", address) + default: + err = fmt.Errorf("Unknown address type: %s", address) + } + + // If we have a server type, then record that. We default to net/rpc + // for backwards compatibility. + c.protocol = ProtocolNetRPC + if len(parts) >= 5 { + c.protocol = Protocol(parts[4]) + } + + found := false + for _, p := range c.config.AllowedProtocols { + if p == c.protocol { + found = true + break + } + } + if !found { + err = fmt.Errorf("Unsupported plugin protocol %q. Supported: %v", + c.protocol, c.config.AllowedProtocols) + return addr, err + } + + // See if we have a TLS certificate from the server. + // Checking if the length is > 50 rules out catching the unused "extra" + // data returned from some older implementations. + if len(parts) >= 6 && len(parts[5]) > 50 { + err := c.loadServerCert(parts[5]) + if err != nil { + return nil, fmt.Errorf("error parsing server cert: %s", err) + } + } + } + + c.address = addr + return +} diff --git a/go.mod b/go.mod index c3d07d45..4d12f05d 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,14 @@ module github.com/hashicorp/go-plugin -go 1.17 +go 1.21.0 require ( github.com/golang/protobuf v1.5.0 github.com/hashicorp/go-hclog v0.14.1 github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb github.com/jhump/protoreflect v1.15.1 + github.com/magodo/chanio v0.0.0-20230901115729-fd5b8225ff0b + github.com/magodo/go-wasmww v0.0.0-20230904090519-70aa9affe685 github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 github.com/oklog/run v1.0.0 google.golang.org/grpc v1.38.0 @@ -15,8 +17,12 @@ require ( require ( github.com/bufbuild/protocompile v0.4.0 // indirect github.com/fatih/color v1.7.0 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/hack-pad/go-webworkers v0.1.0 // indirect + github.com/hack-pad/safejs v0.1.1 // indirect github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.10 // indirect + github.com/pkg/errors v0.9.1 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/sys v0.5.0 // indirect diff --git a/go.sum b/go.sum index e1c54c38..b7a336aa 100644 --- a/go.sum +++ b/go.sum @@ -36,19 +36,22 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hack-pad/go-webworkers v0.1.0 h1:QHBJpkXJgW0QRi2iiUGcxwGnmy7lQJL0F8UfsgMXKhA= +github.com/hack-pad/go-webworkers v0.1.0/go.mod h1:/rmjjgnlw0CursmeqRtP0NGIqo8CR+0o6AtzFydUHJ4= +github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8= +github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= -github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= -github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= -github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magodo/chanio v0.0.0-20230901115729-fd5b8225ff0b h1:SQWe6H8SBiS3JOLcYZkFtC0jf7dxqygAqVC5+Jxc2Fo= +github.com/magodo/chanio v0.0.0-20230901115729-fd5b8225ff0b/go.mod h1:h8znBXxSpGVpiLc79RwAaMndx+d3DrnynGlmbxPWcX8= +github.com/magodo/go-wasmww v0.0.0-20230904090519-70aa9affe685 h1:l60FGC0J8WQk8TRcpHogFYxlPTQ+JI57XLn8JflJ0ZI= +github.com/magodo/go-wasmww v0.0.0-20230904090519-70aa9affe685/go.mod h1:xJ1RIRulStU21Ev+0w0sR8ixbi3McvuZI2U7viLiI6A= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -58,75 +61,46 @@ github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1: github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -150,14 +124,10 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743 h1:yqElulDvOF26oZ2O+2/aoX7mQ8DY/6+p39neytrycd8= google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/wasmrunner/addr_translator.go b/internal/wasmrunner/addr_translator.go new file mode 100644 index 00000000..958da3b9 --- /dev/null +++ b/internal/wasmrunner/addr_translator.go @@ -0,0 +1,13 @@ +package wasmrunner + +// addrTranslator implements stateless identity functions, as the host and plugin +// run in the same context wrt WASM. +type addrTranslator struct{} + +func (*addrTranslator) PluginToHost(pluginNet, pluginAddr string) (string, string, error) { + return pluginNet, pluginAddr, nil +} + +func (*addrTranslator) HostToPlugin(hostNet, hostAddr string) (string, string, error) { + return hostNet, hostAddr, nil +} diff --git a/internal/wasmrunner/wasm_runner.go b/internal/wasmrunner/wasm_runner.go new file mode 100644 index 00000000..30ea18af --- /dev/null +++ b/internal/wasmrunner/wasm_runner.go @@ -0,0 +1,97 @@ +//go:build js && wasm + +package wasmrunner + +import ( + "context" + "fmt" + "io" + "os/exec" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin/runner" + "github.com/magodo/go-wasmww" +) + +const unrecognizedRemotePluginMessage = `This usually means + the plugin was not compiled for WASM, + the plugin failed to negotiate the initial go-plugin protocol handshake +%s` + +var _ runner.Runner = (*WasmRunner)(nil) + +type WasmRunner struct { + logger hclog.Logger + ww *wasmww.WasmWebWorkerConn + + stdout io.ReadCloser + stderr io.ReadCloser + + addrTranslator +} + +func NewWasmRunner(logger hclog.Logger, cmd *exec.Cmd) (*WasmRunner, error) { + ww := &wasmww.WasmWebWorkerConn{ + Path: cmd.Path, + Args: cmd.Args, + Env: cmd.Env, + } + + stdout, err := ww.StdoutPipe() + if err != nil { + return nil, err + } + + stderr, err := ww.StderrPipe() + if err != nil { + return nil, err + } + + return &WasmRunner{ + logger: logger, + ww: ww, + stdout: stdout, + stderr: stderr, + }, nil +} + +func (c *WasmRunner) Start(_ context.Context) error { + c.logger.Debug("starting plugin", "path", c.ww.Path, "args", c.ww.Args) + err := c.ww.Start() + if err != nil { + return err + } + + c.logger.Debug("plugin started", "path", c.ww.Path, "name", c.ww.Name) + return nil +} + +func (c *WasmRunner) Wait(_ context.Context) error { + c.ww.Wait() + return nil +} + +func (c *WasmRunner) Kill(_ context.Context) error { + c.ww.Terminate() + return nil +} + +func (c *WasmRunner) Stdout() io.ReadCloser { + return c.stdout +} + +func (c *WasmRunner) Stderr() io.ReadCloser { + return c.stderr +} + +func (c *WasmRunner) Name() string { + return c.ww.Path +} + +func (c *WasmRunner) ID() string { + return c.ww.Name +} + +func (c *WasmRunner) Diagnose(ctx context.Context) string { + return fmt.Sprintf(unrecognizedRemotePluginMessage, c.ww.Path) +} diff --git a/server.go b/server.go index 4e9a22c0..40ee1028 100644 --- a/server.go +++ b/server.go @@ -6,14 +6,10 @@ package plugin import ( "context" "crypto/tls" - "crypto/x509" - "encoding/base64" "errors" "fmt" - "io" "net" "os" - "os/signal" "os/user" "runtime" "sort" @@ -213,291 +209,8 @@ func protocolVersion(opts *ServeConfig) (int, Protocol, PluginSet) { return protoVersion, protoType, pluginSet } -// Serve serves the plugins given by ServeConfig. -// -// Serve doesn't return until the plugin is done being executed. Any -// fixable errors will be output to os.Stderr and the process will -// exit with a status code of 1. Serve will panic for unexpected -// conditions where a user's fix is unknown. -// -// This is the method that plugins should call in their main() functions. -func Serve(opts *ServeConfig) { - exitCode := -1 - // We use this to trigger an `os.Exit` so that we can execute our other - // deferred functions. In test mode, we just output the err to stderr - // and return. - defer func() { - if opts.Test == nil && exitCode >= 0 { - os.Exit(exitCode) - } - - if opts.Test != nil && opts.Test.CloseCh != nil { - close(opts.Test.CloseCh) - } - }() - - if opts.Test == nil { - // Validate the handshake config - if opts.MagicCookieKey == "" || opts.MagicCookieValue == "" { - fmt.Fprintf(os.Stderr, - "Misconfigured ServeConfig given to serve this plugin: no magic cookie\n"+ - "key or value was set. Please notify the plugin author and report\n"+ - "this as a bug.\n") - exitCode = 1 - return - } - - // First check the cookie - if os.Getenv(opts.MagicCookieKey) != opts.MagicCookieValue { - fmt.Fprintf(os.Stderr, - "This binary is a plugin. These are not meant to be executed directly.\n"+ - "Please execute the program that consumes these plugins, which will\n"+ - "load any plugins automatically\n") - exitCode = 1 - return - } - } - - // negotiate the version and plugins - // start with default version in the handshake config - protoVersion, protoType, pluginSet := protocolVersion(opts) - - logger := opts.Logger - if logger == nil { - // internal logger to os.Stderr - logger = hclog.New(&hclog.LoggerOptions{ - Level: hclog.Trace, - Output: os.Stderr, - JSONFormat: true, - }) - } - - // Register a listener so we can accept a connection - listener, err := serverListener(os.Getenv(EnvUnixSocketDir)) - if err != nil { - logger.Error("plugin init error", "error", err) - return - } - - // Close the listener on return. We wrap this in a func() on purpose - // because the "listener" reference may change to TLS. - defer func() { - listener.Close() - }() - - var tlsConfig *tls.Config - if opts.TLSProvider != nil { - tlsConfig, err = opts.TLSProvider() - if err != nil { - logger.Error("plugin tls init", "error", err) - return - } - } - - var serverCert string - clientCert := os.Getenv("PLUGIN_CLIENT_CERT") - // If the client is configured using AutoMTLS, the certificate will be here, - // and we need to generate our own in response. - if tlsConfig == nil && clientCert != "" { - logger.Info("configuring server automatic mTLS") - clientCertPool := x509.NewCertPool() - if !clientCertPool.AppendCertsFromPEM([]byte(clientCert)) { - logger.Error("client cert provided but failed to parse", "cert", clientCert) - } - - certPEM, keyPEM, err := generateCert() - if err != nil { - logger.Error("failed to generate server certificate", "error", err) - panic(err) - } - - cert, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - logger.Error("failed to parse server certificate", "error", err) - panic(err) - } - - tlsConfig = &tls.Config{ - Certificates: []tls.Certificate{cert}, - ClientAuth: tls.RequireAndVerifyClientCert, - ClientCAs: clientCertPool, - MinVersion: tls.VersionTLS12, - RootCAs: clientCertPool, - ServerName: "localhost", - } - - // We send back the raw leaf cert data for the client rather than the - // PEM, since the protocol can't handle newlines. - serverCert = base64.RawStdEncoding.EncodeToString(cert.Certificate[0]) - } - - // Create the channel to tell us when we're done - doneCh := make(chan struct{}) - - // Create our new stdout, stderr files. These will override our built-in - // stdout/stderr so that it works across the stream boundary. - var stdout_r, stderr_r io.Reader - stdout_r, stdout_w, err := os.Pipe() - if err != nil { - fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err) - os.Exit(1) - } - stderr_r, stderr_w, err := os.Pipe() - if err != nil { - fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err) - os.Exit(1) - } - - // If we're in test mode, we tee off the reader and write the data - // as-is to our normal Stdout and Stderr so that they continue working - // while stdio works. This is because in test mode, we assume we're running - // in `go test` or some equivalent and we want output to go to standard - // locations. - if opts.Test != nil { - // TODO(mitchellh): This isn't super ideal because a TeeReader - // only works if the reader side is actively read. If we never - // connect via a plugin client, the output still gets swallowed. - stdout_r = io.TeeReader(stdout_r, os.Stdout) - stderr_r = io.TeeReader(stderr_r, os.Stderr) - } - - // Build the server type - var server ServerProtocol - switch protoType { - case ProtocolNetRPC: - // If we have a TLS configuration then we wrap the listener - // ourselves and do it at that level. - if tlsConfig != nil { - listener = tls.NewListener(listener, tlsConfig) - } - - // Create the RPC server to dispense - server = &RPCServer{ - Plugins: pluginSet, - Stdout: stdout_r, - Stderr: stderr_r, - DoneCh: doneCh, - } - - case ProtocolGRPC: - // Create the gRPC server - server = &GRPCServer{ - Plugins: pluginSet, - Server: opts.GRPCServer, - TLS: tlsConfig, - Stdout: stdout_r, - Stderr: stderr_r, - DoneCh: doneCh, - logger: logger, - } - - default: - panic("unknown server protocol: " + protoType) - } - - // Initialize the servers - if err := server.Init(); err != nil { - logger.Error("protocol init", "error", err) - return - } - - logger.Debug("plugin address", "network", listener.Addr().Network(), "address", listener.Addr().String()) - - // Output the address and service name to stdout so that the client can - // bring it up. In test mode, we don't do this because clients will - // attach via a reattach config. - if opts.Test == nil { - fmt.Printf("%d|%d|%s|%s|%s|%s\n", - CoreProtocolVersion, - protoVersion, - listener.Addr().Network(), - listener.Addr().String(), - protoType, - serverCert) - os.Stdout.Sync() - } else if ch := opts.Test.ReattachConfigCh; ch != nil { - // Send back the reattach config that can be used. This isn't - // quite ready if they connect immediately but the client should - // retry a few times. - ch <- &ReattachConfig{ - Protocol: protoType, - ProtocolVersion: protoVersion, - Addr: listener.Addr(), - Pid: os.Getpid(), - Test: true, - } - } - - // Eat the interrupts. In test mode we disable this so that go test - // can be cancelled properly. - if opts.Test == nil { - ch := make(chan os.Signal, 1) - signal.Notify(ch, os.Interrupt) - go func() { - count := 0 - for { - <-ch - count++ - logger.Trace("plugin received interrupt signal, ignoring", "count", count) - } - }() - } - - // Set our stdout, stderr to the stdio stream that clients can retrieve - // using ClientConfig.SyncStdout/err. We only do this for non-test mode - // or if the test mode explicitly requests it. - // - // In test mode, we use a multiwriter so that the data continues going - // to the normal stdout/stderr so output can show up in test logs. We - // also send to the stdio stream so that clients can continue working - // if they depend on that. - if opts.Test == nil || opts.Test.SyncStdio { - if opts.Test != nil { - // In test mode we need to maintain the original values so we can - // reset it. - defer func(out, err *os.File) { - os.Stdout = out - os.Stderr = err - }(os.Stdout, os.Stderr) - } - os.Stdout = stdout_w - os.Stderr = stderr_w - } - - // Accept connections and wait for completion - go server.Serve(listener) - - ctx := context.Background() - if opts.Test != nil && opts.Test.Context != nil { - ctx = opts.Test.Context - } - select { - case <-ctx.Done(): - // Cancellation. We can stop the server by closing the listener. - // This isn't graceful at all but this is currently only used by - // tests and its our only way to stop. - listener.Close() - - // If this is a grpc server, then we also ask the server itself to - // end which will kill all connections. There isn't an easy way to do - // this for net/rpc currently but net/rpc is more and more unused. - if s, ok := server.(*GRPCServer); ok { - s.Stop() - } - - // Wait for the server itself to shut down - <-doneCh - - case <-doneCh: - // Note that given the documentation of Serve we should probably be - // setting exitCode = 0 and using os.Exit here. That's how it used to - // work before extracting this library. However, for years we've done - // this so we'll keep this functionality. - } -} - func serverListener(dir string) (net.Listener, error) { - if runtime.GOOS == "windows" { + if runtime.GOOS == "windows" || runtime.GOOS == "js" { return serverListener_tcp() } diff --git a/server_serve_other.go b/server_serve_other.go new file mode 100644 index 00000000..699c0b26 --- /dev/null +++ b/server_serve_other.go @@ -0,0 +1,299 @@ +//go:build !wasm + +package plugin + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "fmt" + "io" + "os" + "os/signal" + + "github.com/hashicorp/go-hclog" +) + +// Serve serves the plugins given by ServeConfig. +// +// Serve doesn't return until the plugin is done being executed. Any +// fixable errors will be output to os.Stderr and the process will +// exit with a status code of 1. Serve will panic for unexpected +// conditions where a user's fix is unknown. +// +// This is the method that plugins should call in their main() functions. +func Serve(opts *ServeConfig) { + exitCode := -1 + // We use this to trigger an `os.Exit` so that we can execute our other + // deferred functions. In test mode, we just output the err to stderr + // and return. + defer func() { + if opts.Test == nil && exitCode >= 0 { + os.Exit(exitCode) + } + + if opts.Test != nil && opts.Test.CloseCh != nil { + close(opts.Test.CloseCh) + } + }() + + if opts.Test == nil { + // Validate the handshake config + if opts.MagicCookieKey == "" || opts.MagicCookieValue == "" { + fmt.Fprintf(os.Stderr, + "Misconfigured ServeConfig given to serve this plugin: no magic cookie\n"+ + "key or value was set. Please notify the plugin author and report\n"+ + "this as a bug.\n") + exitCode = 1 + return + } + + // First check the cookie + if os.Getenv(opts.MagicCookieKey) != opts.MagicCookieValue { + fmt.Fprintf(os.Stderr, + "This binary is a plugin. These are not meant to be executed directly.\n"+ + "Please execute the program that consumes these plugins, which will\n"+ + "load any plugins automatically\n") + exitCode = 1 + return + } + } + + // negotiate the version and plugins + // start with default version in the handshake config + protoVersion, protoType, pluginSet := protocolVersion(opts) + + logger := opts.Logger + if logger == nil { + // internal logger to os.Stderr + logger = hclog.New(&hclog.LoggerOptions{ + Level: hclog.Trace, + Output: os.Stderr, + JSONFormat: true, + }) + } + + // Register a listener so we can accept a connection + listener, err := serverListener(os.Getenv(EnvUnixSocketDir)) + if err != nil { + logger.Error("plugin init error", "error", err) + return + } + + // Close the listener on return. We wrap this in a func() on purpose + // because the "listener" reference may change to TLS. + defer func() { + listener.Close() + }() + + var tlsConfig *tls.Config + if opts.TLSProvider != nil { + tlsConfig, err = opts.TLSProvider() + if err != nil { + logger.Error("plugin tls init", "error", err) + return + } + } + + var serverCert string + clientCert := os.Getenv("PLUGIN_CLIENT_CERT") + // If the client is configured using AutoMTLS, the certificate will be here, + // and we need to generate our own in response. + if tlsConfig == nil && clientCert != "" { + logger.Info("configuring server automatic mTLS") + clientCertPool := x509.NewCertPool() + if !clientCertPool.AppendCertsFromPEM([]byte(clientCert)) { + logger.Error("client cert provided but failed to parse", "cert", clientCert) + } + + certPEM, keyPEM, err := generateCert() + if err != nil { + logger.Error("failed to generate server certificate", "error", err) + panic(err) + } + + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + logger.Error("failed to parse server certificate", "error", err) + panic(err) + } + + tlsConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCertPool, + MinVersion: tls.VersionTLS12, + RootCAs: clientCertPool, + ServerName: "localhost", + } + + // We send back the raw leaf cert data for the client rather than the + // PEM, since the protocol can't handle newlines. + serverCert = base64.RawStdEncoding.EncodeToString(cert.Certificate[0]) + } + + // Create the channel to tell us when we're done + doneCh := make(chan struct{}) + + // Create our new stdout, stderr files. These will override our built-in + // stdout/stderr so that it works across the stream boundary. + var stdout_r, stderr_r io.Reader + stdout_r, stdout_w, err := os.Pipe() + if err != nil { + fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err) + os.Exit(1) + } + stderr_r, stderr_w, err := os.Pipe() + if err != nil { + fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err) + os.Exit(1) + } + + // If we're in test mode, we tee off the reader and write the data + // as-is to our normal Stdout and Stderr so that they continue working + // while stdio works. This is because in test mode, we assume we're running + // in `go test` or some equivalent and we want output to go to standard + // locations. + if opts.Test != nil { + // TODO(mitchellh): This isn't super ideal because a TeeReader + // only works if the reader side is actively read. If we never + // connect via a plugin client, the output still gets swallowed. + stdout_r = io.TeeReader(stdout_r, os.Stdout) + stderr_r = io.TeeReader(stderr_r, os.Stderr) + } + + // Build the server type + var server ServerProtocol + switch protoType { + case ProtocolNetRPC: + // If we have a TLS configuration then we wrap the listener + // ourselves and do it at that level. + if tlsConfig != nil { + listener = tls.NewListener(listener, tlsConfig) + } + + // Create the RPC server to dispense + server = &RPCServer{ + Plugins: pluginSet, + Stdout: stdout_r, + Stderr: stderr_r, + DoneCh: doneCh, + } + + case ProtocolGRPC: + // Create the gRPC server + server = &GRPCServer{ + Plugins: pluginSet, + Server: opts.GRPCServer, + TLS: tlsConfig, + Stdout: stdout_r, + Stderr: stderr_r, + DoneCh: doneCh, + logger: logger, + } + + default: + panic("unknown server protocol: " + protoType) + } + + // Initialize the servers + if err := server.Init(); err != nil { + logger.Error("protocol init", "error", err) + return + } + + logger.Debug("plugin address", "network", listener.Addr().Network(), "address", listener.Addr().String()) + + // Output the address and service name to stdout so that the client can + // bring it up. In test mode, we don't do this because clients will + // attach via a reattach config. + if opts.Test == nil { + fmt.Printf("%d|%d|%s|%s|%s|%s\n", + CoreProtocolVersion, + protoVersion, + listener.Addr().Network(), + listener.Addr().String(), + protoType, + serverCert) + os.Stdout.Sync() + } else if ch := opts.Test.ReattachConfigCh; ch != nil { + // Send back the reattach config that can be used. This isn't + // quite ready if they connect immediately but the client should + // retry a few times. + ch <- &ReattachConfig{ + Protocol: protoType, + ProtocolVersion: protoVersion, + Addr: listener.Addr(), + Pid: os.Getpid(), + Test: true, + } + } + + // Eat the interrupts. In test mode we disable this so that go test + // can be cancelled properly. + if opts.Test == nil { + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + go func() { + count := 0 + for { + <-ch + count++ + logger.Trace("plugin received interrupt signal, ignoring", "count", count) + } + }() + } + + // Set our stdout, stderr to the stdio stream that clients can retrieve + // using ClientConfig.SyncStdout/err. We only do this for non-test mode + // or if the test mode explicitly requests it. + // + // In test mode, we use a multiwriter so that the data continues going + // to the normal stdout/stderr so output can show up in test logs. We + // also send to the stdio stream so that clients can continue working + // if they depend on that. + if opts.Test == nil || opts.Test.SyncStdio { + if opts.Test != nil { + // In test mode we need to maintain the original values so we can + // reset it. + defer func(out, err *os.File) { + os.Stdout = out + os.Stderr = err + }(os.Stdout, os.Stderr) + } + os.Stdout = stdout_w + os.Stderr = stderr_w + } + + // Accept connections and wait for completion + go server.Serve(listener) + + ctx := context.Background() + if opts.Test != nil && opts.Test.Context != nil { + ctx = opts.Test.Context + } + select { + case <-ctx.Done(): + // Cancellation. We can stop the server by closing the listener. + // This isn't graceful at all but this is currently only used by + // tests and its our only way to stop. + listener.Close() + + // If this is a grpc server, then we also ask the server itself to + // end which will kill all connections. There isn't an easy way to do + // this for net/rpc currently but net/rpc is more and more unused. + if s, ok := server.(*GRPCServer); ok { + s.Stop() + } + + // Wait for the server itself to shut down + <-doneCh + + case <-doneCh: + // Note that given the documentation of Serve we should probably be + // setting exitCode = 0 and using os.Exit here. That's how it used to + // work before extracting this library. However, for years we've done + // this so we'll keep this functionality. + } +} diff --git a/server_serve_wasm.go b/server_serve_wasm.go new file mode 100644 index 00000000..860c5572 --- /dev/null +++ b/server_serve_wasm.go @@ -0,0 +1,220 @@ +//go:build js && wasm + +package plugin + +import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" + "fmt" + "io" + "os" + + "github.com/hashicorp/go-hclog" + "github.com/magodo/chanio" +) + +// Serve serves the plugins given by ServeConfig. +// +// Serve doesn't return until the plugin is done being executed. Any +// fixable errors will be output to os.Stderr and the process will +// exit with a status code of 1. Serve will panic for unexpected +// conditions where a user's fix is unknown. +// +// This is the method that plugins should call in their main() functions. +func Serve(opts *ServeConfig) { + exitCode := -1 + + // We use this to trigger an `os.Exit` so that we can execute our other + // deferred functions. In test mode, we just output the err to stderr + // and return. + defer func() { + if exitCode >= 0 { + os.Exit(exitCode) + } + }() + + if opts.Test != nil { + fmt.Fprintf(os.Stderr, "Test is not supported\n") + exitCode = 1 + return + } + + // Validate the handshake config + if opts.MagicCookieKey == "" || opts.MagicCookieValue == "" { + fmt.Fprintf(os.Stderr, + "Misconfigured ServeConfig given to serve this plugin: no magic cookie\n"+ + "key or value was set. Please notify the plugin author and report\n"+ + "this as a bug.\n") + exitCode = 1 + return + } + + // First check the cookie + if os.Getenv(opts.MagicCookieKey) != opts.MagicCookieValue { + fmt.Fprintf(os.Stderr, + "This binary is a plugin. These are not meant to be executed directly.\n"+ + "Please execute the program that consumes these plugins, which will\n"+ + "load any plugins automatically\n") + exitCode = 1 + return + } + + // negotiate the version and plugins + // start with default version in the handshake config + protoVersion, protoType, pluginSet := protocolVersion(opts) + + logger := opts.Logger + if logger == nil { + // internal logger to os.Stderr + logger = hclog.New(&hclog.LoggerOptions{ + Level: hclog.Trace, + Output: os.Stderr, + JSONFormat: true, + }) + } + + // Register a listener so we can accept a connection + listener, err := serverListener(os.Getenv(EnvUnixSocketDir)) + if err != nil { + logger.Error("plugin init error", "error", err) + return + } + + // Close the listener on return. We wrap this in a func() on purpose + // because the "listener" reference may change to TLS. + defer func() { + listener.Close() + }() + + var tlsConfig *tls.Config + if opts.TLSProvider != nil { + tlsConfig, err = opts.TLSProvider() + if err != nil { + logger.Error("plugin tls init", "error", err) + return + } + } + + var serverCert string + clientCert := os.Getenv("PLUGIN_CLIENT_CERT") + // If the client is configured using AutoMTLS, the certificate will be here, + // and we need to generate our own in response. + if tlsConfig == nil && clientCert != "" { + logger.Info("configuring server automatic mTLS") + clientCertPool := x509.NewCertPool() + if !clientCertPool.AppendCertsFromPEM([]byte(clientCert)) { + logger.Error("client cert provided but failed to parse", "cert", clientCert) + } + + certPEM, keyPEM, err := generateCert() + if err != nil { + logger.Error("failed to generate server certificate", "error", err) + panic(err) + } + + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + logger.Error("failed to parse server certificate", "error", err) + panic(err) + } + + tlsConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCertPool, + MinVersion: tls.VersionTLS12, + RootCAs: clientCertPool, + ServerName: "localhost", + } + + // We send back the raw leaf cert data for the client rather than the + // PEM, since the protocol can't handle newlines. + serverCert = base64.RawStdEncoding.EncodeToString(cert.Certificate[0]) + } + + // Create the channel to tell us when we're done + doneCh := make(chan struct{}) + + // Create our new stdout, stderr files. These will override our built-in + // stdout/stderr so that it works across the stream boundary. + var stdout_r, stderr_r io.Reader + stdout_r, stdout_w, err := chanio.Pipe() + if err != nil { + fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err) + os.Exit(1) + } + stderr_r, stderr_w, err := chanio.Pipe() + if err != nil { + fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err) + os.Exit(1) + } + + // Build the server type + var server ServerProtocol + switch protoType { + case ProtocolNetRPC: + // If we have a TLS configuration then we wrap the listener + // ourselves and do it at that level. + if tlsConfig != nil { + listener = tls.NewListener(listener, tlsConfig) + } + + // Create the RPC server to dispense + server = &RPCServer{ + Plugins: pluginSet, + Stdout: stdout_r, + Stderr: stderr_r, + DoneCh: doneCh, + } + + case ProtocolGRPC: + // Create the gRPC server + server = &GRPCServer{ + Plugins: pluginSet, + Server: opts.GRPCServer, + TLS: tlsConfig, + Stdout: stdout_r, + Stderr: stderr_r, + DoneCh: doneCh, + logger: logger, + } + + default: + panic("unknown server protocol: " + protoType) + } + + // Initialize the servers + if err := server.Init(); err != nil { + logger.Error("protocol init", "error", err) + return + } + + logger.Debug("plugin address", "network", listener.Addr().Network(), "address", listener.Addr().String()) + + // Output the address and service name to stdout so that the client can + // bring it up. + fmt.Printf("%d|%d|%s|%s|%s|%s\n", + CoreProtocolVersion, + protoVersion, + listener.Addr().Network(), + listener.Addr().String(), + protoType, + serverCert) + os.Stdout.Sync() + + // Set our stdout, stderr to the stdio stream that clients can retrieve + // using ClientConfig.SyncStdout/err. + os.Stdout = stdout_w + os.Stderr = stderr_w + + // Accept connections and wait for completion + go server.Serve(listener) + + // Wait for the server itself to shut down + <-doneCh + // Note that given the documentation of Serve we should probably be + // setting exitCode = 0 and using os.Exit here. That's how it used to + // work before extracting this library. However, for years we've done + // this so we'll keep this functionality. +} From a15419de6b0b55160a09a4b0beb414037c63a937 Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 6 Sep 2023 11:20:05 +0800 Subject: [PATCH 02/11] basic example successfully run --- client_start_other.go | 17 ++ client_start_wasm.go | 6 +- examples/basic_wasm/.gitignore | 5 + examples/basic_wasm/README.md | 18 ++ examples/basic_wasm/httpserve/main.go | 28 +++ examples/basic_wasm/index.html | 13 ++ examples/basic_wasm/main.go | 1 + examples/basic_wasm/plugin/greeter_impl.go | 1 + examples/basic_wasm/shared | 1 + go.mod | 8 +- go.sum | 8 +- internal/wasmrunner/wasm_runner.go | 4 + rpc_client.go | 6 +- rpc_client_dial_other.go | 17 ++ rpc_client_dial_wasm.go | 15 ++ rpc_server.go | 1 + server.go | 2 +- server_net_wasm.go | 233 +++++++++++++++++++++ server_serve_wasm.go | 14 +- 19 files changed, 377 insertions(+), 21 deletions(-) create mode 100644 examples/basic_wasm/.gitignore create mode 100644 examples/basic_wasm/README.md create mode 100644 examples/basic_wasm/httpserve/main.go create mode 100644 examples/basic_wasm/index.html create mode 120000 examples/basic_wasm/main.go create mode 120000 examples/basic_wasm/plugin/greeter_impl.go create mode 120000 examples/basic_wasm/shared create mode 100644 rpc_client_dial_other.go create mode 100644 rpc_client_dial_wasm.go create mode 100644 server_net_wasm.go diff --git a/client_start_other.go b/client_start_other.go index 35a8ac05..154082b0 100644 --- a/client_start_other.go +++ b/client_start_other.go @@ -2,6 +2,23 @@ package plugin +import ( + "bufio" + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/hashicorp/go-plugin/internal/cmdrunner" + "github.com/hashicorp/go-plugin/runner" +) + // Start the underlying subprocess, communicating with it to negotiate // a port for RPC connections, and returning the address to connect via RPC. // diff --git a/client_start_wasm.go b/client_start_wasm.go index 6de1b950..e61495e2 100644 --- a/client_start_wasm.go +++ b/client_start_wasm.go @@ -262,10 +262,8 @@ func (c *Client) Start() (addr net.Addr, err error) { } switch network { - // case "tcp": - // addr, err = net.ResolveTCPAddr("tcp", address) - // case "unix": - // addr, err = net.ResolveUnixAddr("unix", address) + case "webworker": + addr = WebWorkerAddr{Name: address} default: err = fmt.Errorf("Unknown address type: %s", address) } diff --git a/examples/basic_wasm/.gitignore b/examples/basic_wasm/.gitignore new file mode 100644 index 00000000..818e43aa --- /dev/null +++ b/examples/basic_wasm/.gitignore @@ -0,0 +1,5 @@ +# Ignore Go WASM glue file +wasm_exec.js +# Ignore binaries +plugin/greeter +basic diff --git a/examples/basic_wasm/README.md b/examples/basic_wasm/README.md new file mode 100644 index 00000000..33cfa903 --- /dev/null +++ b/examples/basic_wasm/README.md @@ -0,0 +1,18 @@ +Plugin Example +-------------- + +Copy the Go WASM glue file via: + + cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . + +Compile the plugin itself via: + + GOOS=js GOARCH=wasm go build -o ./plugin/greeter ./plugin/greeter_impl.go + +Compile this driver via: + + GOOS=js GOARCH=wasm go build -o basic . + +You can then launch the plugin sample via: + + go run ./httpserve diff --git a/examples/basic_wasm/httpserve/main.go b/examples/basic_wasm/httpserve/main.go new file mode 100644 index 00000000..b60191c9 --- /dev/null +++ b/examples/basic_wasm/httpserve/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" +) + +func main() { + port := flag.Int("p", 8080, "port") + dir := flag.String("dir", ".", "root directory") + flag.Parse() + + handler := http.FileServer(http.Dir(*dir)) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "basic", "greeter": + w.Header().Set("Content-Type", "application/wasm") + } + handler.ServeHTTP(w, r) + }) + + addr := fmt.Sprintf(":%d", *port) + fmt.Println("serving at " + addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} diff --git a/examples/basic_wasm/index.html b/examples/basic_wasm/index.html new file mode 100644 index 00000000..3d74bea2 --- /dev/null +++ b/examples/basic_wasm/index.html @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/examples/basic_wasm/main.go b/examples/basic_wasm/main.go new file mode 120000 index 00000000..960c9cda --- /dev/null +++ b/examples/basic_wasm/main.go @@ -0,0 +1 @@ +../basic/main.go \ No newline at end of file diff --git a/examples/basic_wasm/plugin/greeter_impl.go b/examples/basic_wasm/plugin/greeter_impl.go new file mode 120000 index 00000000..1e96408e --- /dev/null +++ b/examples/basic_wasm/plugin/greeter_impl.go @@ -0,0 +1 @@ +../../basic/plugin/greeter_impl.go \ No newline at end of file diff --git a/examples/basic_wasm/shared b/examples/basic_wasm/shared new file mode 120000 index 00000000..d8318356 --- /dev/null +++ b/examples/basic_wasm/shared @@ -0,0 +1 @@ +../basic/shared \ No newline at end of file diff --git a/go.mod b/go.mod index 4d12f05d..a7bb6d40 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,13 @@ go 1.21.0 require ( github.com/golang/protobuf v1.5.0 + github.com/hack-pad/go-webworkers v0.1.0 + github.com/hack-pad/safejs v0.1.1 github.com/hashicorp/go-hclog v0.14.1 github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb github.com/jhump/protoreflect v1.15.1 - github.com/magodo/chanio v0.0.0-20230901115729-fd5b8225ff0b - github.com/magodo/go-wasmww v0.0.0-20230904090519-70aa9affe685 + github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc + github.com/magodo/go-wasmww v0.0.0-20230905142830-ccf6326ae16e github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 github.com/oklog/run v1.0.0 google.golang.org/grpc v1.38.0 @@ -18,8 +20,6 @@ require ( github.com/bufbuild/protocompile v0.4.0 // indirect github.com/fatih/color v1.7.0 // indirect github.com/google/uuid v1.3.1 // indirect - github.com/hack-pad/go-webworkers v0.1.0 // indirect - github.com/hack-pad/safejs v0.1.1 // indirect github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.10 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index b7a336aa..5880a037 100644 --- a/go.sum +++ b/go.sum @@ -48,10 +48,10 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= -github.com/magodo/chanio v0.0.0-20230901115729-fd5b8225ff0b h1:SQWe6H8SBiS3JOLcYZkFtC0jf7dxqygAqVC5+Jxc2Fo= -github.com/magodo/chanio v0.0.0-20230901115729-fd5b8225ff0b/go.mod h1:h8znBXxSpGVpiLc79RwAaMndx+d3DrnynGlmbxPWcX8= -github.com/magodo/go-wasmww v0.0.0-20230904090519-70aa9affe685 h1:l60FGC0J8WQk8TRcpHogFYxlPTQ+JI57XLn8JflJ0ZI= -github.com/magodo/go-wasmww v0.0.0-20230904090519-70aa9affe685/go.mod h1:xJ1RIRulStU21Ev+0w0sR8ixbi3McvuZI2U7viLiI6A= +github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc h1:OA/l08Abuwg0v3g6piwgnHzK8QcV+rgUwrmvKSSVT28= +github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc/go.mod h1:h8znBXxSpGVpiLc79RwAaMndx+d3DrnynGlmbxPWcX8= +github.com/magodo/go-wasmww v0.0.0-20230905142830-ccf6326ae16e h1:EgfHnVB7njFrY7S8si7eK2Nez20Rj7VDkd9UCqNqe/U= +github.com/magodo/go-wasmww v0.0.0-20230905142830-ccf6326ae16e/go.mod h1:CFCTBp2DLx2Jf1UALF1MsNk9X5gq0Ir+r2/60/Rbx+M= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= diff --git a/internal/wasmrunner/wasm_runner.go b/internal/wasmrunner/wasm_runner.go index 30ea18af..dea948ea 100644 --- a/internal/wasmrunner/wasm_runner.go +++ b/internal/wasmrunner/wasm_runner.go @@ -95,3 +95,7 @@ func (c *WasmRunner) ID() string { func (c *WasmRunner) Diagnose(ctx context.Context) string { return fmt.Sprintf(unrecognizedRemotePluginMessage, c.ww.Path) } + +func (c *WasmRunner) WebWorker() *wasmww.WasmWebWorkerConn { + return c.ww +} diff --git a/rpc_client.go b/rpc_client.go index 142454df..5aa02061 100644 --- a/rpc_client.go +++ b/rpc_client.go @@ -27,14 +27,10 @@ type RPCClient struct { // to be successfully started already with a lock held. func newRPCClient(c *Client) (*RPCClient, error) { // Connect to the client - conn, err := net.Dial(c.address.Network(), c.address.String()) + conn, err := dialRPC(c) if err != nil { return nil, err } - if tcpConn, ok := conn.(*net.TCPConn); ok { - // Make sure to set keep alive so that the connection doesn't die - tcpConn.SetKeepAlive(true) - } if c.config.TLSConfig != nil { conn = tls.Client(conn, c.config.TLSConfig) diff --git a/rpc_client_dial_other.go b/rpc_client_dial_other.go new file mode 100644 index 00000000..7b6eed5d --- /dev/null +++ b/rpc_client_dial_other.go @@ -0,0 +1,17 @@ +//go:build !wasm + +package plugin + +import "net" + +func dialRPC(c *Client) (net.Conn, error) { + conn, err := net.Dial(c.address.Network(), c.address.String()) + if err != nil { + return nil, err + } + if tcpConn, ok := conn.(*net.TCPConn); ok { + // Make sure to set keep alive so that the connection doesn't die + tcpConn.SetKeepAlive(true) + } + return conn, nil +} diff --git a/rpc_client_dial_wasm.go b/rpc_client_dial_wasm.go new file mode 100644 index 00000000..7f288c0b --- /dev/null +++ b/rpc_client_dial_wasm.go @@ -0,0 +1,15 @@ +//go:build wasm && js + +package plugin + +import ( + "net" + + "github.com/hashicorp/go-plugin/internal/wasmrunner" +) + +func dialRPC(c *Client) (net.Conn, error) { + ww := c.runner.(*wasmrunner.WasmRunner).WebWorker() + conn := NewWebWorkerConnForClient(ww.Name, ww.EventChannel(), ww.PostMessage) + return conn, nil +} diff --git a/rpc_server.go b/rpc_server.go index cec0a3d9..9a7ffc63 100644 --- a/rpc_server.go +++ b/rpc_server.go @@ -114,6 +114,7 @@ func (s *RPCServer) ServeConn(conn io.ReadWriteCloser) { broker: broker, plugins: s.Plugins, }) + server.ServeConn(control) } diff --git a/server.go b/server.go index 40ee1028..fde21090 100644 --- a/server.go +++ b/server.go @@ -210,7 +210,7 @@ func protocolVersion(opts *ServeConfig) (int, Protocol, PluginSet) { } func serverListener(dir string) (net.Listener, error) { - if runtime.GOOS == "windows" || runtime.GOOS == "js" { + if runtime.GOOS == "windows" { return serverListener_tcp() } diff --git a/server_net_wasm.go b/server_net_wasm.go new file mode 100644 index 00000000..232ea1f2 --- /dev/null +++ b/server_net_wasm.go @@ -0,0 +1,233 @@ +//go:build js && wasm + +package plugin + +import ( + "bytes" + "fmt" + "io" + "net" + "os" + "time" + + "github.com/hack-pad/go-webworkers/worker" + "github.com/hack-pad/safejs" + "github.com/magodo/go-wasmww" +) + +var _ net.Listener = &WebWorkerListener{} + +// WebWorkerListener implements the net.Listener +type WebWorkerListener struct { + self *wasmww.GlobalSelfConn + ch <-chan worker.MessageEvent + closeFn wasmww.WebWorkerCloseFunc + + // acceptCh is a 1 buffered channel, which only allow the 1st receive. + // Currently, the web worker is only a dedicated one, which means + // only one client can connect to this web worker at one point. + acceptCh chan any +} + +func NewWebWorkerListener() (net.Listener, error) { + self, err := wasmww.SelfConn() + if err != nil { + return nil, err + } + ch, closeFn, err := self.SetupConn() + if err != nil { + return nil, err + } + acceptCh := make(chan any, 1) + acceptCh <- struct{}{} + return &WebWorkerListener{ + self: self, + ch: ch, + closeFn: closeFn, + acceptCh: acceptCh, + }, nil +} + +func (l *WebWorkerListener) Accept() (net.Conn, error) { + _, ok := <-l.acceptCh + if !ok { + return nil, net.ErrClosed + } + + var name string + if v, err := l.self.Name(); err == nil { + name = v + } + return NewWebWorkerConnForServer(name, l.ch, l.self.PostMessage, l.acceptCh), nil +} + +func (l *WebWorkerListener) Addr() net.Addr { + var name string + if v, err := l.self.Name(); err == nil { + name = v + } + return WebWorkerAddr{Name: name} +} + +func (l *WebWorkerListener) Close() error { + return l.closeFn() +} + +// WebWorkerAddr implements the net.Addr +type WebWorkerAddr struct{ Name string } + +var _ net.Addr = WebWorkerAddr{} + +func (WebWorkerAddr) Network() string { + return "webworker" +} + +func (addr WebWorkerAddr) String() string { + return addr.Name +} + +// WebWorkerConn implements the net.Conn +type WebWorkerConn struct { + name string + ch <-chan worker.MessageEvent + timerR *time.Timer + timerW *time.Timer + postFunc postMessageFunc + readBuf bytes.Buffer + + // server only + acceptCh chan any +} + +type postMessageFunc func(message safejs.Value, transfers []safejs.Value) error + +var _ net.Conn = &WebWorkerConn{} + +func NewWebWorkerConnForServer(name string, ch <-chan worker.MessageEvent, postFunc postMessageFunc, acceptCh chan any) *WebWorkerConn { + return &WebWorkerConn{ + name: name, + ch: ch, + postFunc: postFunc, + acceptCh: acceptCh, + } +} + +func NewWebWorkerConnForClient(name string, ch <-chan worker.MessageEvent, postFunc postMessageFunc) *WebWorkerConn { + return &WebWorkerConn{ + name: name, + ch: ch, + postFunc: postFunc, + } +} + +func (conn *WebWorkerConn) Close() error { + if conn.acceptCh != nil { + conn.acceptCh <- struct{}{} + } + return nil +} + +func (conn *WebWorkerConn) LocalAddr() net.Addr { + return WebWorkerAddr{Name: conn.name} +} + +func (conn *WebWorkerConn) Read(b []byte) (n int, err error) { + var ( + event worker.MessageEvent + ok bool + ) + if timeout := conn.timerR; timeout != nil { + select { + case <-timeout.C: + return 0, os.ErrDeadlineExceeded + default: + } + } + // If there is unread bytes in the buffer, just read them out + if conn.readBuf.Len() != 0 { + return io.ReadAtLeast(&conn.readBuf, b, min(len(b), conn.readBuf.Len())) + } + + if timeout := conn.timerR; timeout != nil { + select { + case event, ok = <-conn.ch: + case <-timeout.C: + return 0, os.ErrDeadlineExceeded + } + } else { + event, ok = <-conn.ch + } + + if !ok { + // Channel closed + return 0, io.EOF + } + + data, err := event.Data() + if err != nil { + return 0, err + } + arrayBufLen, err := data.Length() + if err != nil { + return 0, err + } + buf := make([]byte, arrayBufLen) + n, err = safejs.CopyBytesToGo(buf, data) + if err != nil { + return 0, err + } + if n != arrayBufLen { + return 0, fmt.Errorf("CopyBytesToGo expect to copy %d bytes, actually %d bytes", arrayBufLen, n) + } + n = copy(b, buf) + + // If there are left bytes not read to the target buffer, store them in the readBuf. + if n < len(buf) { + if _, err := conn.readBuf.Write(buf[n:]); err != nil { + return 0, err + } + } + return n, nil +} + +func (conn *WebWorkerConn) Write(b []byte) (n int, err error) { + arraybuf, err := safejs.MustGetGlobal("Uint8Array").New(len(b)) + if err != nil { + return 0, nil + } + n, err = safejs.CopyBytesToJS(arraybuf, b) + if err != nil { + return 0, nil + } + if n != len(b) { + return 0, fmt.Errorf("CopyBytesToJS expect to copy %d bytes, actually %d bytes", len(b), n) + } + if err := conn.postFunc(arraybuf, nil); err != nil { + return 0, err + } + return len(b), nil +} + +func (*WebWorkerConn) RemoteAddr() net.Addr { + return WebWorkerAddr{Name: "remote"} +} + +func (conn *WebWorkerConn) SetDeadline(t time.Time) error { + if err := conn.SetReadDeadline(t); err != nil { + return err + } + if err := conn.SetWriteDeadline(t); err != nil { + return err + } + return nil +} + +func (conn *WebWorkerConn) SetReadDeadline(t time.Time) error { + conn.timerR = time.NewTimer(time.Until(t)) + return nil +} + +func (conn *WebWorkerConn) SetWriteDeadline(t time.Time) error { + conn.timerW = time.NewTimer(time.Until(t)) + return nil +} diff --git a/server_serve_wasm.go b/server_serve_wasm.go index 860c5572..f87f819d 100644 --- a/server_serve_wasm.go +++ b/server_serve_wasm.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/magodo/chanio" + "github.com/magodo/go-wasmww" ) // Serve serves the plugins given by ServeConfig. @@ -75,7 +76,7 @@ func Serve(opts *ServeConfig) { } // Register a listener so we can accept a connection - listener, err := serverListener(os.Getenv(EnvUnixSocketDir)) + listener, err := NewWebWorkerListener() if err != nil { logger.Error("plugin init error", "error", err) return @@ -205,8 +206,15 @@ func Serve(opts *ServeConfig) { // Set our stdout, stderr to the stdio stream that clients can retrieve // using ClientConfig.SyncStdout/err. - os.Stdout = stdout_w - os.Stderr = stderr_w + self, err := wasmww.SelfConn() + if err != nil { + panic("failed to get self: " + err.Error()) + } + + self.SetWriteSync( + []wasmww.MsgWriter{self.NewMsgWriterToIoWriter(stdout_w)}, + []wasmww.MsgWriter{self.NewMsgWriterToIoWriter(stderr_w)}, + ) // Accept connections and wait for completion go server.Serve(listener) From 466492ce6b40ce696ebe7351c2be9b9f9d0838a0 Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 6 Sep 2023 15:14:38 +0800 Subject: [PATCH 03/11] New example grpc_wasm (not work thought) --- examples/grpc_wasm/.gitignore | 7 +++ examples/grpc_wasm/README.md | 19 +++++++ examples/grpc_wasm/index.html | 13 +++++ examples/grpc_wasm/main.go | 62 +++++++++++++++++++++++ examples/grpc_wasm/plugin-go-grpc/main.go | 45 ++++++++++++++++ examples/grpc_wasm/proto | 1 + examples/grpc_wasm/shared | 1 + 7 files changed, 148 insertions(+) create mode 100644 examples/grpc_wasm/.gitignore create mode 100644 examples/grpc_wasm/README.md create mode 100644 examples/grpc_wasm/index.html create mode 100644 examples/grpc_wasm/main.go create mode 100644 examples/grpc_wasm/plugin-go-grpc/main.go create mode 120000 examples/grpc_wasm/proto create mode 120000 examples/grpc_wasm/shared diff --git a/examples/grpc_wasm/.gitignore b/examples/grpc_wasm/.gitignore new file mode 100644 index 00000000..d13ad4d5 --- /dev/null +++ b/examples/grpc_wasm/.gitignore @@ -0,0 +1,7 @@ +wasm_exec.js +*.wasm +*.pyc +kv +kv-* +kv_* +!kv_*.py diff --git a/examples/grpc_wasm/README.md b/examples/grpc_wasm/README.md new file mode 100644 index 00000000..01624cd0 --- /dev/null +++ b/examples/grpc_wasm/README.md @@ -0,0 +1,19 @@ +# KV Example + +This example builds a simple key/value store CLI where the mechanism +for storing and retrieving keys is pluggable. To build this example: + +```sh +# This copies the Go WASM glue code +$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . + +# This builds the main CLI +$ GOOS=js GOARCH=wasm go build -o kv.wasm + +# This builds the plugin written in Go +$ GOOS=js GOARCH=wasm go build -o kv-go-grpc.wasm ./plugin-go-grpc + +# This launches the HTTP server +# (install goexec: go install github.com/shurcooL/goexec) +goexec 'http.ListenAndServe(`:8080`, http.FileServer(http.Dir(`.`)))' +``` diff --git a/examples/grpc_wasm/index.html b/examples/grpc_wasm/index.html new file mode 100644 index 00000000..6392edda --- /dev/null +++ b/examples/grpc_wasm/index.html @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/examples/grpc_wasm/main.go b/examples/grpc_wasm/main.go new file mode 100644 index 00000000..f2226eb0 --- /dev/null +++ b/examples/grpc_wasm/main.go @@ -0,0 +1,62 @@ +//go:build wasm && js + +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin/examples/grpc/shared" +) + +func main() { + // We don't want to see the plugin logs. + log.SetOutput(ioutil.Discard) + + // We're a host. Start by launching the plugin process. + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: shared.Handshake, + Plugins: shared.PluginMap, + Cmd: exec.Command("kv-go-grpc.wasm"), + AllowedProtocols: []plugin.Protocol{ + plugin.ProtocolNetRPC, plugin.ProtocolGRPC}, + }) + defer client.Kill() + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + fmt.Println("Error:", err.Error()) + os.Exit(1) + } + + // Request the plugin + raw, err := rpcClient.Dispense("kv_grpc") + if err != nil { + fmt.Println("Error:", err.Error()) + os.Exit(1) + } + + kv := raw.(shared.KV) + if err := kv.Put("hello", []byte("world")); err != nil { + fmt.Println("Error:", err.Error()) + os.Exit(1) + } + + result, err := kv.Get(os.Args[1]) + if err != nil { + fmt.Println("Error:", err.Error()) + os.Exit(1) + } + + fmt.Println(string(result)) + + os.Exit(0) +} diff --git a/examples/grpc_wasm/plugin-go-grpc/main.go b/examples/grpc_wasm/plugin-go-grpc/main.go new file mode 100644 index 00000000..0574b5db --- /dev/null +++ b/examples/grpc_wasm/plugin-go-grpc/main.go @@ -0,0 +1,45 @@ +//go:build wasm && js + +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "fmt" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-plugin/examples/grpc/shared" +) + +// Here is a real implementation of KV that writes to a local file with +// the key name and the contents are the value of the key. +type KV struct { + m map[string]string +} + +func (kv KV) Put(key string, value []byte) error { + value = []byte(fmt.Sprintf("%s\n\nWritten from plugin-go-grpc", string(value))) + kv.m[key] = string(value) + return nil +} + +func (kv KV) Get(key string) ([]byte, error) { + v, ok := kv.m[key] + if !ok { + return nil, fmt.Errorf("key %q not exist", key) + } + return []byte(v), nil +} + +func main() { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: shared.Handshake, + Plugins: map[string]plugin.Plugin{ + "kv": &shared.KVGRPCPlugin{Impl: &KV{m: map[string]string{}}}, + }, + + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/examples/grpc_wasm/proto b/examples/grpc_wasm/proto new file mode 120000 index 00000000..9a16040f --- /dev/null +++ b/examples/grpc_wasm/proto @@ -0,0 +1 @@ +../grpc/proto \ No newline at end of file diff --git a/examples/grpc_wasm/shared b/examples/grpc_wasm/shared new file mode 120000 index 00000000..5c0c2804 --- /dev/null +++ b/examples/grpc_wasm/shared @@ -0,0 +1 @@ +../grpc/shared \ No newline at end of file From 91e2843dff4417e9fc2cbe6828bc5a561b0cffdf Mon Sep 17 00:00:00 2001 From: magodo Date: Tue, 12 Sep 2023 16:50:55 +0800 Subject: [PATCH 04/11] Migrate to the latest go-wasmww (with shared worker support) --- go.mod | 4 ++-- go.sum | 8 ++++---- server_net_wasm.go | 22 ++++++++++------------ server_serve_wasm.go | 9 ++++----- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index a7bb6d40..8b780b28 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ go 1.21.0 require ( github.com/golang/protobuf v1.5.0 - github.com/hack-pad/go-webworkers v0.1.0 github.com/hack-pad/safejs v0.1.1 github.com/hashicorp/go-hclog v0.14.1 github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb github.com/jhump/protoreflect v1.15.1 github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc - github.com/magodo/go-wasmww v0.0.0-20230905142830-ccf6326ae16e + github.com/magodo/go-wasmww v0.0.0-20230912084813-af188676507e + github.com/magodo/go-webworkers v0.0.0-20230912024750-bb9fd84a26ba github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 github.com/oklog/run v1.0.0 google.golang.org/grpc v1.38.0 diff --git a/go.sum b/go.sum index 5880a037..6b497c45 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,6 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hack-pad/go-webworkers v0.1.0 h1:QHBJpkXJgW0QRi2iiUGcxwGnmy7lQJL0F8UfsgMXKhA= -github.com/hack-pad/go-webworkers v0.1.0/go.mod h1:/rmjjgnlw0CursmeqRtP0NGIqo8CR+0o6AtzFydUHJ4= github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8= github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= @@ -50,8 +48,10 @@ github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgf github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc h1:OA/l08Abuwg0v3g6piwgnHzK8QcV+rgUwrmvKSSVT28= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc/go.mod h1:h8znBXxSpGVpiLc79RwAaMndx+d3DrnynGlmbxPWcX8= -github.com/magodo/go-wasmww v0.0.0-20230905142830-ccf6326ae16e h1:EgfHnVB7njFrY7S8si7eK2Nez20Rj7VDkd9UCqNqe/U= -github.com/magodo/go-wasmww v0.0.0-20230905142830-ccf6326ae16e/go.mod h1:CFCTBp2DLx2Jf1UALF1MsNk9X5gq0Ir+r2/60/Rbx+M= +github.com/magodo/go-wasmww v0.0.0-20230912084813-af188676507e h1:R92Yt53lmws+eiTkWFsc6QR3gYAu8P8KaA2jq8aTItg= +github.com/magodo/go-wasmww v0.0.0-20230912084813-af188676507e/go.mod h1:L+DYcWhxOxB4AQ3VIqzxPiSj/b9/+fGJgKWpSQ5Lois= +github.com/magodo/go-webworkers v0.0.0-20230912024750-bb9fd84a26ba h1:sRelpkDvxmvamsLWa1FDcg+dmxGm3WuQ8bY1lmAC4co= +github.com/magodo/go-webworkers v0.0.0-20230912024750-bb9fd84a26ba/go.mod h1:75pYYUWzQZKwI7HXgCOD6CAqEvNhln+yyxV3vIJWxaE= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= diff --git a/server_net_wasm.go b/server_net_wasm.go index 232ea1f2..6c46b5e1 100644 --- a/server_net_wasm.go +++ b/server_net_wasm.go @@ -10,18 +10,17 @@ import ( "os" "time" - "github.com/hack-pad/go-webworkers/worker" "github.com/hack-pad/safejs" "github.com/magodo/go-wasmww" + "github.com/magodo/go-webworkers/types" ) var _ net.Listener = &WebWorkerListener{} // WebWorkerListener implements the net.Listener type WebWorkerListener struct { - self *wasmww.GlobalSelfConn - ch <-chan worker.MessageEvent - closeFn wasmww.WebWorkerCloseFunc + self *wasmww.SelfConn + ch <-chan types.MessageEventMessage // acceptCh is a 1 buffered channel, which only allow the 1st receive. // Currently, the web worker is only a dedicated one, which means @@ -30,11 +29,11 @@ type WebWorkerListener struct { } func NewWebWorkerListener() (net.Listener, error) { - self, err := wasmww.SelfConn() + self, err := wasmww.NewSelfConn() if err != nil { return nil, err } - ch, closeFn, err := self.SetupConn() + ch, err := self.SetupConn() if err != nil { return nil, err } @@ -43,7 +42,6 @@ func NewWebWorkerListener() (net.Listener, error) { return &WebWorkerListener{ self: self, ch: ch, - closeFn: closeFn, acceptCh: acceptCh, }, nil } @@ -70,7 +68,7 @@ func (l *WebWorkerListener) Addr() net.Addr { } func (l *WebWorkerListener) Close() error { - return l.closeFn() + return l.self.Close() } // WebWorkerAddr implements the net.Addr @@ -89,7 +87,7 @@ func (addr WebWorkerAddr) String() string { // WebWorkerConn implements the net.Conn type WebWorkerConn struct { name string - ch <-chan worker.MessageEvent + ch <-chan types.MessageEventMessage timerR *time.Timer timerW *time.Timer postFunc postMessageFunc @@ -103,7 +101,7 @@ type postMessageFunc func(message safejs.Value, transfers []safejs.Value) error var _ net.Conn = &WebWorkerConn{} -func NewWebWorkerConnForServer(name string, ch <-chan worker.MessageEvent, postFunc postMessageFunc, acceptCh chan any) *WebWorkerConn { +func NewWebWorkerConnForServer(name string, ch <-chan types.MessageEventMessage, postFunc postMessageFunc, acceptCh chan any) *WebWorkerConn { return &WebWorkerConn{ name: name, ch: ch, @@ -112,7 +110,7 @@ func NewWebWorkerConnForServer(name string, ch <-chan worker.MessageEvent, postF } } -func NewWebWorkerConnForClient(name string, ch <-chan worker.MessageEvent, postFunc postMessageFunc) *WebWorkerConn { +func NewWebWorkerConnForClient(name string, ch <-chan types.MessageEventMessage, postFunc postMessageFunc) *WebWorkerConn { return &WebWorkerConn{ name: name, ch: ch, @@ -133,7 +131,7 @@ func (conn *WebWorkerConn) LocalAddr() net.Addr { func (conn *WebWorkerConn) Read(b []byte) (n int, err error) { var ( - event worker.MessageEvent + event types.MessageEventMessage ok bool ) if timeout := conn.timerR; timeout != nil { diff --git a/server_serve_wasm.go b/server_serve_wasm.go index f87f819d..43b2f330 100644 --- a/server_serve_wasm.go +++ b/server_serve_wasm.go @@ -206,14 +206,13 @@ func Serve(opts *ServeConfig) { // Set our stdout, stderr to the stdio stream that clients can retrieve // using ClientConfig.SyncStdout/err. - self, err := wasmww.SelfConn() - if err != nil { + if _, err := wasmww.NewSelfConn(); err != nil { panic("failed to get self: " + err.Error()) } - self.SetWriteSync( - []wasmww.MsgWriter{self.NewMsgWriterToIoWriter(stdout_w)}, - []wasmww.MsgWriter{self.NewMsgWriterToIoWriter(stderr_w)}, + wasmww.SetWriteSync( + []wasmww.MsgWriter{wasmww.NewMsgWriterToIoWriter(stdout_w)}, + []wasmww.MsgWriter{wasmww.NewMsgWriterToIoWriter(stderr_w)}, ) // Accept connections and wait for completion From b3c5e17bfb120cfa596abfa935cd4e3e23369762 Mon Sep 17 00:00:00 2001 From: magodo Date: Tue, 12 Sep 2023 21:11:28 +0800 Subject: [PATCH 05/11] Update go-wasmww --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8b780b28..ced4b830 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb github.com/jhump/protoreflect v1.15.1 github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc - github.com/magodo/go-wasmww v0.0.0-20230912084813-af188676507e - github.com/magodo/go-webworkers v0.0.0-20230912024750-bb9fd84a26ba + github.com/magodo/go-wasmww v0.0.0-20230912131045-bd57c3c25542 + github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 github.com/oklog/run v1.0.0 google.golang.org/grpc v1.38.0 diff --git a/go.sum b/go.sum index 6b497c45..9deb5297 100644 --- a/go.sum +++ b/go.sum @@ -48,10 +48,10 @@ github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgf github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc h1:OA/l08Abuwg0v3g6piwgnHzK8QcV+rgUwrmvKSSVT28= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc/go.mod h1:h8znBXxSpGVpiLc79RwAaMndx+d3DrnynGlmbxPWcX8= -github.com/magodo/go-wasmww v0.0.0-20230912084813-af188676507e h1:R92Yt53lmws+eiTkWFsc6QR3gYAu8P8KaA2jq8aTItg= -github.com/magodo/go-wasmww v0.0.0-20230912084813-af188676507e/go.mod h1:L+DYcWhxOxB4AQ3VIqzxPiSj/b9/+fGJgKWpSQ5Lois= -github.com/magodo/go-webworkers v0.0.0-20230912024750-bb9fd84a26ba h1:sRelpkDvxmvamsLWa1FDcg+dmxGm3WuQ8bY1lmAC4co= -github.com/magodo/go-webworkers v0.0.0-20230912024750-bb9fd84a26ba/go.mod h1:75pYYUWzQZKwI7HXgCOD6CAqEvNhln+yyxV3vIJWxaE= +github.com/magodo/go-wasmww v0.0.0-20230912131045-bd57c3c25542 h1:R2a9roNObo8BxaYAv4y29Ud6iHJ7tlbaGUpl6wrwhAc= +github.com/magodo/go-wasmww v0.0.0-20230912131045-bd57c3c25542/go.mod h1:3Da4y024Wt+xMHGKEdkLx8wBn4ZF0wC4XtzHKzNCLjA= +github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee h1:XaNsxpodBifEAUm5iFCT0oxFw+OVmlCRxwBfqOd/Gi8= +github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee/go.mod h1:75pYYUWzQZKwI7HXgCOD6CAqEvNhln+yyxV3vIJWxaE= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= From 5c473352b2fa95591690a3bcebcc4a359c59ed8f Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 13 Sep 2023 11:35:48 +0800 Subject: [PATCH 06/11] Migrate to using the Shared Web Worker --- client_start_wasm.go | 2 +- internal/wasmrunner/wasm_runner.go | 47 +++++------- rpc_client_dial_wasm.go | 2 +- server_net_wasm.go | 114 +++++++++++++++++------------ server_serve_wasm.go | 2 +- 5 files changed, 89 insertions(+), 78 deletions(-) diff --git a/client_start_wasm.go b/client_start_wasm.go index e61495e2..90fb5646 100644 --- a/client_start_wasm.go +++ b/client_start_wasm.go @@ -263,7 +263,7 @@ func (c *Client) Start() (addr net.Addr, err error) { switch network { case "webworker": - addr = WebWorkerAddr{Name: address} + addr, err = ParseWebWorkerAddr(address) default: err = fmt.Errorf("Unknown address type: %s", address) } diff --git a/internal/wasmrunner/wasm_runner.go b/internal/wasmrunner/wasm_runner.go index dea948ea..138800a3 100644 --- a/internal/wasmrunner/wasm_runner.go +++ b/internal/wasmrunner/wasm_runner.go @@ -21,8 +21,9 @@ const unrecognizedRemotePluginMessage = `This usually means var _ runner.Runner = (*WasmRunner)(nil) type WasmRunner struct { - logger hclog.Logger - ww *wasmww.WasmWebWorkerConn + logger hclog.Logger + wwConn *wasmww.WasmSharedWebWorkerConn + mgmtConn *wasmww.WasmSharedWebWorkerMgmtConn stdout io.ReadCloser stderr io.ReadCloser @@ -31,71 +32,59 @@ type WasmRunner struct { } func NewWasmRunner(logger hclog.Logger, cmd *exec.Cmd) (*WasmRunner, error) { - ww := &wasmww.WasmWebWorkerConn{ + ww := &wasmww.WasmSharedWebWorkerConn{ Path: cmd.Path, Args: cmd.Args, Env: cmd.Env, } - stdout, err := ww.StdoutPipe() - if err != nil { - return nil, err - } - - stderr, err := ww.StderrPipe() - if err != nil { - return nil, err - } - return &WasmRunner{ logger: logger, - ww: ww, - stdout: stdout, - stderr: stderr, + wwConn: ww, }, nil } func (c *WasmRunner) Start(_ context.Context) error { - c.logger.Debug("starting plugin", "path", c.ww.Path, "args", c.ww.Args) - err := c.ww.Start() + c.logger.Debug("starting plugin", "path", c.wwConn.Path, "args", c.wwConn.Args) + mgmtConn, err := c.wwConn.Start() if err != nil { return err } + c.mgmtConn = mgmtConn - c.logger.Debug("plugin started", "path", c.ww.Path, "name", c.ww.Name) + c.logger.Debug("plugin started", "path", c.wwConn.Path, "name", c.wwConn.Name) return nil } func (c *WasmRunner) Wait(_ context.Context) error { - c.ww.Wait() + c.wwConn.Wait() return nil } func (c *WasmRunner) Kill(_ context.Context) error { - c.ww.Terminate() - return nil + return c.wwConn.Close() } func (c *WasmRunner) Stdout() io.ReadCloser { - return c.stdout + return c.mgmtConn.Stdout() } func (c *WasmRunner) Stderr() io.ReadCloser { - return c.stderr + return c.mgmtConn.Stderr() } func (c *WasmRunner) Name() string { - return c.ww.Path + return c.wwConn.Path } func (c *WasmRunner) ID() string { - return c.ww.Name + return c.wwConn.Name } func (c *WasmRunner) Diagnose(ctx context.Context) string { - return fmt.Sprintf(unrecognizedRemotePluginMessage, c.ww.Path) + return fmt.Sprintf(unrecognizedRemotePluginMessage, c.wwConn.Path) } -func (c *WasmRunner) WebWorker() *wasmww.WasmWebWorkerConn { - return c.ww +func (c *WasmRunner) WebWorker() *wasmww.WasmSharedWebWorkerConn { + return c.wwConn } diff --git a/rpc_client_dial_wasm.go b/rpc_client_dial_wasm.go index 7f288c0b..61b9ca5e 100644 --- a/rpc_client_dial_wasm.go +++ b/rpc_client_dial_wasm.go @@ -10,6 +10,6 @@ import ( func dialRPC(c *Client) (net.Conn, error) { ww := c.runner.(*wasmrunner.WasmRunner).WebWorker() - conn := NewWebWorkerConnForClient(ww.Name, ww.EventChannel(), ww.PostMessage) + conn := NewWebWorkerConnForClient(ww.Name, ww.URL, ww.EventChannel(), ww.PostMessage, ww.Close) return conn, nil } diff --git a/server_net_wasm.go b/server_net_wasm.go index 6c46b5e1..5041366e 100644 --- a/server_net_wasm.go +++ b/server_net_wasm.go @@ -8,6 +8,7 @@ import ( "io" "net" "os" + "strings" "time" "github.com/hack-pad/safejs" @@ -19,17 +20,12 @@ var _ net.Listener = &WebWorkerListener{} // WebWorkerListener implements the net.Listener type WebWorkerListener struct { - self *wasmww.SelfConn - ch <-chan types.MessageEventMessage - - // acceptCh is a 1 buffered channel, which only allow the 1st receive. - // Currently, the web worker is only a dedicated one, which means - // only one client can connect to this web worker at one point. - acceptCh chan any + self *wasmww.SelfSharedConn + ch <-chan *wasmww.SelfSharedConnPort } func NewWebWorkerListener() (net.Listener, error) { - self, err := wasmww.NewSelfConn() + self, err := wasmww.NewSelfSharedConn() if err != nil { return nil, err } @@ -37,26 +33,33 @@ func NewWebWorkerListener() (net.Listener, error) { if err != nil { return nil, err } - acceptCh := make(chan any, 1) - acceptCh <- struct{}{} return &WebWorkerListener{ - self: self, - ch: ch, - acceptCh: acceptCh, + self: self, + ch: ch, }, nil } func (l *WebWorkerListener) Accept() (net.Conn, error) { - _, ok := <-l.acceptCh + port, ok := <-l.ch if !ok { return nil, net.ErrClosed } - var name string - if v, err := l.self.Name(); err == nil { - name = v + name, err := l.self.Name() + if err != nil { + return nil, err } - return NewWebWorkerConnForServer(name, l.ch, l.self.PostMessage, l.acceptCh), nil + location, err := l.self.Location() + if err != nil { + return nil, err + } + + ch, err := port.SetupConn() + if err != nil { + return nil, err + } + + return NewWebWorkerConnForServer(name, location.Href, ch, port.PostMessage, port.Close), nil } func (l *WebWorkerListener) Addr() net.Addr { @@ -64,7 +67,11 @@ func (l *WebWorkerListener) Addr() net.Addr { if v, err := l.self.Name(); err == nil { name = v } - return WebWorkerAddr{Name: name} + var url string + if v, err := l.self.Location(); err == nil { + url = v.Href + } + return WebWorkerAddr{Name: name, URL: url} } func (l *WebWorkerListener) Close() error { @@ -72,61 +79,76 @@ func (l *WebWorkerListener) Close() error { } // WebWorkerAddr implements the net.Addr -type WebWorkerAddr struct{ Name string } +type WebWorkerAddr struct { + Name string + URL string +} var _ net.Addr = WebWorkerAddr{} +func ParseWebWorkerAddr(addr string) (*WebWorkerAddr, error) { + name, url, ok := strings.Cut(addr, ":") + if !ok { + return nil, fmt.Errorf("malformed address: %s", addr) + } + return &WebWorkerAddr{ + Name: name, + URL: url, + }, nil +} + func (WebWorkerAddr) Network() string { return "webworker" } func (addr WebWorkerAddr) String() string { - return addr.Name + return fmt.Sprintf("%s:%s", addr.Name, addr.URL) } // WebWorkerConn implements the net.Conn type WebWorkerConn struct { - name string - ch <-chan types.MessageEventMessage - timerR *time.Timer - timerW *time.Timer - postFunc postMessageFunc - readBuf bytes.Buffer + localAddr net.Addr + remoteAddr net.Addr + ch <-chan types.MessageEventMessage + closeFunc connCloseFunc + postFunc connPostMessageFunc - // server only - acceptCh chan any + timerR *time.Timer + timerW *time.Timer + readBuf bytes.Buffer } -type postMessageFunc func(message safejs.Value, transfers []safejs.Value) error +type connPostMessageFunc func(message safejs.Value, transfers []safejs.Value) error +type connCloseFunc func() error var _ net.Conn = &WebWorkerConn{} -func NewWebWorkerConnForServer(name string, ch <-chan types.MessageEventMessage, postFunc postMessageFunc, acceptCh chan any) *WebWorkerConn { +func NewWebWorkerConnForServer(name, url string, ch <-chan types.MessageEventMessage, postFunc connPostMessageFunc, closeFunc connCloseFunc) *WebWorkerConn { return &WebWorkerConn{ - name: name, - ch: ch, - postFunc: postFunc, - acceptCh: acceptCh, + localAddr: WebWorkerAddr{Name: name, URL: url}, + remoteAddr: WebWorkerAddr{Name: "outside"}, + ch: ch, + postFunc: postFunc, + closeFunc: closeFunc, } } -func NewWebWorkerConnForClient(name string, ch <-chan types.MessageEventMessage, postFunc postMessageFunc) *WebWorkerConn { +func NewWebWorkerConnForClient(name, url string, ch <-chan types.MessageEventMessage, postFunc connPostMessageFunc, closeFunc connCloseFunc) *WebWorkerConn { return &WebWorkerConn{ - name: name, - ch: ch, - postFunc: postFunc, + localAddr: WebWorkerAddr{Name: "outside"}, + remoteAddr: WebWorkerAddr{Name: name, URL: url}, + ch: ch, + postFunc: postFunc, + closeFunc: closeFunc, } } func (conn *WebWorkerConn) Close() error { - if conn.acceptCh != nil { - conn.acceptCh <- struct{}{} - } - return nil + return conn.closeFunc() } func (conn *WebWorkerConn) LocalAddr() net.Addr { - return WebWorkerAddr{Name: conn.name} + return conn.localAddr } func (conn *WebWorkerConn) Read(b []byte) (n int, err error) { @@ -206,8 +228,8 @@ func (conn *WebWorkerConn) Write(b []byte) (n int, err error) { return len(b), nil } -func (*WebWorkerConn) RemoteAddr() net.Addr { - return WebWorkerAddr{Name: "remote"} +func (conn *WebWorkerConn) RemoteAddr() net.Addr { + return conn.remoteAddr } func (conn *WebWorkerConn) SetDeadline(t time.Time) error { diff --git a/server_serve_wasm.go b/server_serve_wasm.go index 43b2f330..84d85c2a 100644 --- a/server_serve_wasm.go +++ b/server_serve_wasm.go @@ -206,7 +206,7 @@ func Serve(opts *ServeConfig) { // Set our stdout, stderr to the stdio stream that clients can retrieve // using ClientConfig.SyncStdout/err. - if _, err := wasmww.NewSelfConn(); err != nil { + if _, err := wasmww.NewSelfSharedConn(); err != nil { panic("failed to get self: " + err.Error()) } From 61f6f4f65c8e17f7e21b22c3c106ef849ccd85fa Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 13 Sep 2023 20:26:45 +0800 Subject: [PATCH 07/11] Support grpc --- client.go | 16 -------- client_dialer_other.go | 24 +++++++++++ client_dialer_wasm.go | 19 +++++++++ examples/grpc_wasm/main.go | 2 +- go.mod | 2 +- go.sum | 4 +- rpc_client_dial_wasm.go | 6 +-- server_net_wasm.go | 82 +++++++++++++++++++++----------------- server_serve_wasm.go | 4 -- 9 files changed, 94 insertions(+), 65 deletions(-) create mode 100644 client_dialer_other.go create mode 100644 client_dialer_wasm.go diff --git a/client.go b/client.go index e92620d4..eeb3ac6e 100644 --- a/client.go +++ b/client.go @@ -674,22 +674,6 @@ func (c *Client) Protocol() Protocol { return c.protocol } -func netAddrDialer(addr net.Addr) func(string, time.Duration) (net.Conn, error) { - return func(_ string, _ time.Duration) (net.Conn, error) { - // Connect to the client - conn, err := net.Dial(addr.Network(), addr.String()) - if err != nil { - return nil, err - } - if tcpConn, ok := conn.(*net.TCPConn); ok { - // Make sure to set keep alive so that the connection doesn't die - tcpConn.SetKeepAlive(true) - } - - return conn, nil - } -} - // dialer is compatible with grpc.WithDialer and creates the connection // to the plugin. func (c *Client) dialer(_ string, timeout time.Duration) (net.Conn, error) { diff --git a/client_dialer_other.go b/client_dialer_other.go new file mode 100644 index 00000000..a801f919 --- /dev/null +++ b/client_dialer_other.go @@ -0,0 +1,24 @@ +//go:build !wasm + +package plugin + +import ( + "net" + "time" +) + +func netAddrDialer(addr net.Addr) func(string, time.Duration) (net.Conn, error) { + return func(_ string, _ time.Duration) (net.Conn, error) { + // Connect to the client + conn, err := net.Dial(addr.Network(), addr.String()) + if err != nil { + return nil, err + } + if tcpConn, ok := conn.(*net.TCPConn); ok { + // Make sure to set keep alive so that the connection doesn't die + tcpConn.SetKeepAlive(true) + } + + return conn, nil + } +} diff --git a/client_dialer_wasm.go b/client_dialer_wasm.go new file mode 100644 index 00000000..2fd11175 --- /dev/null +++ b/client_dialer_wasm.go @@ -0,0 +1,19 @@ +//go:build js && wasm + +package plugin + +import ( + "net" + "time" +) + +func netAddrDialer(addr net.Addr) func(string, time.Duration) (net.Conn, error) { + return func(_ string, _ time.Duration) (net.Conn, error) { + // NOTE: This only works when the addr is an address in the web worker. + conn, err := NewWebWorkerConnForClient(addr.(WebWorkerAddr)) + if err != nil { + return nil, err + } + return conn, nil + } +} diff --git a/examples/grpc_wasm/main.go b/examples/grpc_wasm/main.go index f2226eb0..133e3880 100644 --- a/examples/grpc_wasm/main.go +++ b/examples/grpc_wasm/main.go @@ -50,7 +50,7 @@ func main() { os.Exit(1) } - result, err := kv.Get(os.Args[1]) + result, err := kv.Get("hello") if err != nil { fmt.Println("Error:", err.Error()) os.Exit(1) diff --git a/go.mod b/go.mod index ced4b830..f1bc2ef4 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb github.com/jhump/protoreflect v1.15.1 github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc - github.com/magodo/go-wasmww v0.0.0-20230912131045-bd57c3c25542 + github.com/magodo/go-wasmww v0.0.0-20230913121652-f3c074162429 github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 github.com/oklog/run v1.0.0 diff --git a/go.sum b/go.sum index 9deb5297..690987ac 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgf github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc h1:OA/l08Abuwg0v3g6piwgnHzK8QcV+rgUwrmvKSSVT28= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc/go.mod h1:h8znBXxSpGVpiLc79RwAaMndx+d3DrnynGlmbxPWcX8= -github.com/magodo/go-wasmww v0.0.0-20230912131045-bd57c3c25542 h1:R2a9roNObo8BxaYAv4y29Ud6iHJ7tlbaGUpl6wrwhAc= -github.com/magodo/go-wasmww v0.0.0-20230912131045-bd57c3c25542/go.mod h1:3Da4y024Wt+xMHGKEdkLx8wBn4ZF0wC4XtzHKzNCLjA= +github.com/magodo/go-wasmww v0.0.0-20230913121652-f3c074162429 h1:gSjl5GydHzd+rncWbS8KAtl2XooX/m/g//CIZUVZF6E= +github.com/magodo/go-wasmww v0.0.0-20230913121652-f3c074162429/go.mod h1:3Da4y024Wt+xMHGKEdkLx8wBn4ZF0wC4XtzHKzNCLjA= github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee h1:XaNsxpodBifEAUm5iFCT0oxFw+OVmlCRxwBfqOd/Gi8= github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee/go.mod h1:75pYYUWzQZKwI7HXgCOD6CAqEvNhln+yyxV3vIJWxaE= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= diff --git a/rpc_client_dial_wasm.go b/rpc_client_dial_wasm.go index 61b9ca5e..15a00259 100644 --- a/rpc_client_dial_wasm.go +++ b/rpc_client_dial_wasm.go @@ -4,12 +4,8 @@ package plugin import ( "net" - - "github.com/hashicorp/go-plugin/internal/wasmrunner" ) func dialRPC(c *Client) (net.Conn, error) { - ww := c.runner.(*wasmrunner.WasmRunner).WebWorker() - conn := NewWebWorkerConnForClient(ww.Name, ww.URL, ww.EventChannel(), ww.PostMessage, ww.Close) - return conn, nil + return NewWebWorkerConnForClient(c.address.(WebWorkerAddr)) } diff --git a/server_net_wasm.go b/server_net_wasm.go index 5041366e..b5e3a2d3 100644 --- a/server_net_wasm.go +++ b/server_net_wasm.go @@ -45,21 +45,7 @@ func (l *WebWorkerListener) Accept() (net.Conn, error) { return nil, net.ErrClosed } - name, err := l.self.Name() - if err != nil { - return nil, err - } - location, err := l.self.Location() - if err != nil { - return nil, err - } - - ch, err := port.SetupConn() - if err != nil { - return nil, err - } - - return NewWebWorkerConnForServer(name, location.Href, ch, port.PostMessage, port.Close), nil + return NewWebWorkerConnForServer(port) } func (l *WebWorkerListener) Addr() net.Addr { @@ -86,12 +72,12 @@ type WebWorkerAddr struct { var _ net.Addr = WebWorkerAddr{} -func ParseWebWorkerAddr(addr string) (*WebWorkerAddr, error) { +func ParseWebWorkerAddr(addr string) (WebWorkerAddr, error) { name, url, ok := strings.Cut(addr, ":") if !ok { - return nil, fmt.Errorf("malformed address: %s", addr) + return WebWorkerAddr{}, fmt.Errorf("malformed address: %s", addr) } - return &WebWorkerAddr{ + return WebWorkerAddr{ Name: name, URL: url, }, nil @@ -123,24 +109,47 @@ type connCloseFunc func() error var _ net.Conn = &WebWorkerConn{} -func NewWebWorkerConnForServer(name, url string, ch <-chan types.MessageEventMessage, postFunc connPostMessageFunc, closeFunc connCloseFunc) *WebWorkerConn { +func NewWebWorkerConnForServer(port *wasmww.SelfSharedConnPort) (*WebWorkerConn, error) { + self, err := wasmww.NewSelfSharedConn() + if err != nil { + return nil, err + } + name, err := self.Name() + if err != nil { + return nil, err + } + location, err := self.Location() + if err != nil { + return nil, err + } + ch, err := port.SetupConn() + if err != nil { + return nil, err + } return &WebWorkerConn{ - localAddr: WebWorkerAddr{Name: name, URL: url}, + localAddr: WebWorkerAddr{Name: name, URL: location.Href}, remoteAddr: WebWorkerAddr{Name: "outside"}, ch: ch, - postFunc: postFunc, - closeFunc: closeFunc, - } + postFunc: port.PostMessage, + closeFunc: port.Close, + }, nil } -func NewWebWorkerConnForClient(name, url string, ch <-chan types.MessageEventMessage, postFunc connPostMessageFunc, closeFunc connCloseFunc) *WebWorkerConn { +func NewWebWorkerConnForClient(addr WebWorkerAddr) (*WebWorkerConn, error) { + ww := &wasmww.WasmSharedWebWorkerConn{ + Name: addr.Name, + URL: addr.URL, + } + if err := ww.Connect(); err != nil { + return nil, err + } return &WebWorkerConn{ localAddr: WebWorkerAddr{Name: "outside"}, - remoteAddr: WebWorkerAddr{Name: name, URL: url}, - ch: ch, - postFunc: postFunc, - closeFunc: closeFunc, - } + remoteAddr: WebWorkerAddr{Name: ww.Name, URL: ww.URL}, + ch: ww.EventChannel(), + postFunc: ww.PostMessage, + closeFunc: ww.Close, + }, nil } func (conn *WebWorkerConn) Close() error { @@ -156,13 +165,6 @@ func (conn *WebWorkerConn) Read(b []byte) (n int, err error) { event types.MessageEventMessage ok bool ) - if timeout := conn.timerR; timeout != nil { - select { - case <-timeout.C: - return 0, os.ErrDeadlineExceeded - default: - } - } // If there is unread bytes in the buffer, just read them out if conn.readBuf.Len() != 0 { return io.ReadAtLeast(&conn.readBuf, b, min(len(b), conn.readBuf.Len())) @@ -243,11 +245,19 @@ func (conn *WebWorkerConn) SetDeadline(t time.Time) error { } func (conn *WebWorkerConn) SetReadDeadline(t time.Time) error { + if t.IsZero() { + conn.timerR = nil + return nil + } conn.timerR = time.NewTimer(time.Until(t)) return nil } func (conn *WebWorkerConn) SetWriteDeadline(t time.Time) error { + if t.IsZero() { + conn.timerW = nil + return nil + } conn.timerW = time.NewTimer(time.Until(t)) return nil } diff --git a/server_serve_wasm.go b/server_serve_wasm.go index 84d85c2a..8b6adbed 100644 --- a/server_serve_wasm.go +++ b/server_serve_wasm.go @@ -206,10 +206,6 @@ func Serve(opts *ServeConfig) { // Set our stdout, stderr to the stdio stream that clients can retrieve // using ClientConfig.SyncStdout/err. - if _, err := wasmww.NewSelfSharedConn(); err != nil { - panic("failed to get self: " + err.Error()) - } - wasmww.SetWriteSync( []wasmww.MsgWriter{wasmww.NewMsgWriterToIoWriter(stdout_w)}, []wasmww.MsgWriter{wasmww.NewMsgWriterToIoWriter(stderr_w)}, From 93cba5fde5bee4b7e51b0d105802764402ed517f Mon Sep 17 00:00:00 2001 From: magodo Date: Thu, 14 Sep 2023 11:52:18 +0800 Subject: [PATCH 08/11] Wasmrunner: Wait for the mgmt conn to finish --- internal/wasmrunner/wasm_runner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/wasmrunner/wasm_runner.go b/internal/wasmrunner/wasm_runner.go index 138800a3..4f3f8b09 100644 --- a/internal/wasmrunner/wasm_runner.go +++ b/internal/wasmrunner/wasm_runner.go @@ -57,7 +57,7 @@ func (c *WasmRunner) Start(_ context.Context) error { } func (c *WasmRunner) Wait(_ context.Context) error { - c.wwConn.Wait() + c.mgmtConn.Wait() return nil } From 612df0188f48289ec2f680db9af9874c3b4a1650 Mon Sep 17 00:00:00 2001 From: magodo Date: Thu, 14 Sep 2023 13:25:37 +0800 Subject: [PATCH 09/11] Wasmrunner: Close mgmtConn for kill --- internal/wasmrunner/wasm_runner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/wasmrunner/wasm_runner.go b/internal/wasmrunner/wasm_runner.go index 4f3f8b09..66fdeaed 100644 --- a/internal/wasmrunner/wasm_runner.go +++ b/internal/wasmrunner/wasm_runner.go @@ -62,7 +62,7 @@ func (c *WasmRunner) Wait(_ context.Context) error { } func (c *WasmRunner) Kill(_ context.Context) error { - return c.wwConn.Close() + return c.mgmtConn.Close() } func (c *WasmRunner) Stdout() io.ReadCloser { From 5d1381c1ae5a16ac21099681d5344d453956f632 Mon Sep 17 00:00:00 2001 From: magodo Date: Thu, 14 Sep 2023 16:34:40 +0800 Subject: [PATCH 10/11] Update go-wasmww to keep the newlines of stdout/stderr, also showcase the stdout/stderr plugin behavior via kv example --- examples/grpc_wasm/main.go | 26 ++++++++++++++++++++++- examples/grpc_wasm/plugin-go-grpc/main.go | 8 +++++++ go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/examples/grpc_wasm/main.go b/examples/grpc_wasm/main.go index 133e3880..1f452eb3 100644 --- a/examples/grpc_wasm/main.go +++ b/examples/grpc_wasm/main.go @@ -11,7 +11,9 @@ import ( "log" "os" "os/exec" + "time" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin/examples/grpc/shared" ) @@ -20,13 +22,32 @@ func main() { // We don't want to see the plugin logs. log.SetOutput(ioutil.Discard) + // When using WASM, we are not reassigning the os.Stdout/Stderr, but reimplement the writeSync used underlying. + // This means: + // - Previously, the `log` package (and the `hclog`) used in the provider will write to the plugin process's original stderr. + // Only those explicit write to stdout/stderr (e.g. via fmt.Fprint()), routes to the client via RPC. + // - Now, no matter using `log` or explicit write, the logs all route to the client via RPC. + // + // Hence, we'll need a logger below to filter the non-interesting trace logs. + // Also, we'll need to define the SyncStdout/Stderr in the client option, to avoid routing them to io.Discard. + logger := hclog.New(&hclog.LoggerOptions{ + Name: "plugin", + Output: os.Stdout, + // Debug level is used to filter the plugin trace logs + Level: hclog.Debug, + }) + // We're a host. Start by launching the plugin process. client := plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: shared.Handshake, Plugins: shared.PluginMap, Cmd: exec.Command("kv-go-grpc.wasm"), AllowedProtocols: []plugin.Protocol{ - plugin.ProtocolNetRPC, plugin.ProtocolGRPC}, + plugin.ProtocolNetRPC, plugin.ProtocolGRPC, + }, + Logger: logger, + SyncStdout: os.Stdout, + SyncStderr: os.Stderr, }) defer client.Kill() @@ -58,5 +79,8 @@ func main() { fmt.Println(string(result)) + // Give the grpc stdio copyChan go routines some time to finish copying + time.Sleep(time.Millisecond * 100) + os.Exit(0) } diff --git a/examples/grpc_wasm/plugin-go-grpc/main.go b/examples/grpc_wasm/plugin-go-grpc/main.go index 0574b5db..ca081341 100644 --- a/examples/grpc_wasm/plugin-go-grpc/main.go +++ b/examples/grpc_wasm/plugin-go-grpc/main.go @@ -7,6 +7,8 @@ package main import ( "fmt" + "log" + "os" "github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin/examples/grpc/shared" @@ -19,12 +21,18 @@ type KV struct { } func (kv KV) Put(key string, value []byte) error { + fmt.Fprintln(os.Stderr, "PUT (stderr)") + fmt.Fprintln(os.Stdout, "PUT (stdout)") + log.Println("PUT (log)") value = []byte(fmt.Sprintf("%s\n\nWritten from plugin-go-grpc", string(value))) kv.m[key] = string(value) return nil } func (kv KV) Get(key string) ([]byte, error) { + fmt.Fprintln(os.Stderr, "GET (stderr)") + fmt.Fprintln(os.Stdout, "GET (stdout)") + log.Println("GET (log)") v, ok := kv.m[key] if !ok { return nil, fmt.Errorf("key %q not exist", key) diff --git a/go.mod b/go.mod index f1bc2ef4..679b9095 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb github.com/jhump/protoreflect v1.15.1 github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc - github.com/magodo/go-wasmww v0.0.0-20230913121652-f3c074162429 + github.com/magodo/go-wasmww v0.0.0-20230914064632-eea21c04fd19 github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 github.com/oklog/run v1.0.0 diff --git a/go.sum b/go.sum index 690987ac..4e09e419 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgf github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc h1:OA/l08Abuwg0v3g6piwgnHzK8QcV+rgUwrmvKSSVT28= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc/go.mod h1:h8znBXxSpGVpiLc79RwAaMndx+d3DrnynGlmbxPWcX8= -github.com/magodo/go-wasmww v0.0.0-20230913121652-f3c074162429 h1:gSjl5GydHzd+rncWbS8KAtl2XooX/m/g//CIZUVZF6E= -github.com/magodo/go-wasmww v0.0.0-20230913121652-f3c074162429/go.mod h1:3Da4y024Wt+xMHGKEdkLx8wBn4ZF0wC4XtzHKzNCLjA= +github.com/magodo/go-wasmww v0.0.0-20230914064632-eea21c04fd19 h1:spNynARmmILlTjkiULzQdx3eZDT5SBpWrQJMj1O0fZ0= +github.com/magodo/go-wasmww v0.0.0-20230914064632-eea21c04fd19/go.mod h1:3Da4y024Wt+xMHGKEdkLx8wBn4ZF0wC4XtzHKzNCLjA= github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee h1:XaNsxpodBifEAUm5iFCT0oxFw+OVmlCRxwBfqOd/Gi8= github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee/go.mod h1:75pYYUWzQZKwI7HXgCOD6CAqEvNhln+yyxV3vIJWxaE= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= From cb062573f984ae615bb3bfcbf9b2c4550cb8687f Mon Sep 17 00:00:00 2001 From: magodo Date: Thu, 14 Sep 2023 18:50:16 +0800 Subject: [PATCH 11/11] Update dep for go-wasmww --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 679b9095..a14823f4 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb github.com/jhump/protoreflect v1.15.1 github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc - github.com/magodo/go-wasmww v0.0.0-20230914064632-eea21c04fd19 + github.com/magodo/go-wasmww v0.0.0-20230914104912-44ee4b8129d0 github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 github.com/oklog/run v1.0.0 diff --git a/go.sum b/go.sum index 4e09e419..9f420e5e 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgf github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc h1:OA/l08Abuwg0v3g6piwgnHzK8QcV+rgUwrmvKSSVT28= github.com/magodo/chanio v0.0.0-20230905063744-5f1bf45eacbc/go.mod h1:h8znBXxSpGVpiLc79RwAaMndx+d3DrnynGlmbxPWcX8= -github.com/magodo/go-wasmww v0.0.0-20230914064632-eea21c04fd19 h1:spNynARmmILlTjkiULzQdx3eZDT5SBpWrQJMj1O0fZ0= -github.com/magodo/go-wasmww v0.0.0-20230914064632-eea21c04fd19/go.mod h1:3Da4y024Wt+xMHGKEdkLx8wBn4ZF0wC4XtzHKzNCLjA= +github.com/magodo/go-wasmww v0.0.0-20230914104912-44ee4b8129d0 h1:SfYASTr54LsLpjaMJednJND9DEmBtJIR58j2nF0PoqM= +github.com/magodo/go-wasmww v0.0.0-20230914104912-44ee4b8129d0/go.mod h1:3Da4y024Wt+xMHGKEdkLx8wBn4ZF0wC4XtzHKzNCLjA= github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee h1:XaNsxpodBifEAUm5iFCT0oxFw+OVmlCRxwBfqOd/Gi8= github.com/magodo/go-webworkers v0.0.0-20230912130354-7cb7781df4ee/go.mod h1:75pYYUWzQZKwI7HXgCOD6CAqEvNhln+yyxV3vIJWxaE= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=